golang/Rustのラズパイ/ラズピコ向けのクロス環境について

ラズパイ/ラズピコ限定の話になりますが、現時点での整理です

① golang : ラズパイ用にはcgoがあるとターゲットマシン用にgolangとc用に二種類のコンパイラが必要になるので、単純化のためにはcgo freeのライブラリだけ使って、(現時点ではcgoライブラリはほぼ回避手段があるはず)クロスコンパイルを実行する

たとえば、ラズパイzero用バイナリならば、以下のようなコンパイルオプションつけて実行できる

% GOOS=linux GOARCH=arm GOARM=6 go build -o rasp_zero

ラズピコで組み込み用にはgolangというのは選択肢にはならないしそもそも手段があるのか、golang使うならRustでしょう

② Rust:ラズパイ用はCross + Dockerが一番素直か、Intel MacならばVM上のUbuntuでもtoolchainが存在してるからできるけど

https://www.freecodecamp.org/news/embedded-rust-programming-on-raspberry-pi-zero-w/#heading-how-to-set-up-cross-compilation

ラズピコは組み込みだからクロスビルドしかなくて、Baker link. ENV + Rancher Desktop(docker)がCross + Dockerに相当する機能で使える、ラズピコはデバッガ(debug probe)を簡単に使えるという点でもBaker link. ENVが良さそう、おそらくBaker link. ENVと類似のツールはこの先出てくるかもしれないけど、いずれにしてもそれは仮想環境を使ったもので開発環境を自分で作ることが必要なケースはそれほどないはず(Docker用のイメージ作成は別にして)、いまだラズピコでRustはかなりニッチではあるから歩みは遅いかもしれないが

 

admin

CrossでRustのコンパイル時のパラメタ制限解除

Rustのクロスコンパイル環境でCrossを使う

以前の記事で、cross使う時には

CROSS_CONTAINER_OPTS=”–platform linux/amd64″

を先頭に付加しないと動作しなかったけれども、今日使ってみると、

% CROSS_CONTAINER_OPTS="--platform linux/amd64" cross build --target arm-unknown-linux-gnueabihf  —release 
The application panicked (crashed).
Message:  byte index 1 is not a char boundary; it is inside '—' (bytes 0..3) of `—release`
Location: /rustc/eeb90cda1969383f56a2637cbd3037bdf598841c/library/core/src/str/mod.rs:659

のようなエラーが出て起動できない、単純に削除して$ cross build *********で動作するようになってました(そこに至るまでにはcrossで–releaseを付加するとなぜがエラーは継続して、–releaseを削除すると動いて、その後に–release追加というこれも不可思議な現象を経由してます)

ちなみにIntel MacでDockerで環境インストして動かしてみると、なぜかDockerのuser権限なくてもコンパイルできてしまいます

 

admin

golangにおけるcgo freeドライバに関連して

二年ぐらい前には、まだcgoしか無いようなドライバもそこそこ存在してましたが、cgo freeのドライバに変えてコンパイルをすっきりさせた方が良さそうです

cgo freeでは無いドライバが含まれるコードを普通にコンパイルしてラズパイzeroで実行させると、

$ ./rasp_zero
2025/01/12 17:17:17 "Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work. This is a stub": 
	create table foo (id integer not null primary key, name text);
	delete from foo;

と言われるし、cgo対応のlinkerはいまいち不明だし、というわけで以下のリンクのサンプルコード(実は上のエラーメッセージはオリジナルのソースをコンパイルしてます)をcgo freeドライバに入れ替え

https://zenn.dev/teasy/articles/go-sqlite3-sample

github.com/mattn/go-sqlite3

を、

modernc.org/sqlite

に変更して、db openのコードを変更するだけでcgo freeとなって、

% GOOS=linux GOARCH=arm GOARM=6 go build -o rasp_zero

でコンパイルするとラズパイzeroで動くバイナリが出力されます

実はcgo free以外に、M1 Macの動作ではgo run ***では動作しても、./***では起動も終了もしないという不思議な現象が出てましたが、この件も一緒に解決、ただしこの現象ではIntel Macでは出ないという怪しげな現象でした

orm(例えばgormなど)もcgo freeにできるので、今後はcgoが必要なのはおそらくよほどな時になりそうです

<コードは>

sql.open()が変わっただけです

package main

import (
	"database/sql"
	"fmt"
	"log"
	"os"

	_ "modernc.org/sqlite"
)

func main() {
	os.Remove("./foo.db")

	// original
	// db, err := sql.Open("sqlite3", "./foo.db")
	//
	db, err := sql.Open("sqlite", "./foo.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	sqlStmt := `
	create table foo (id integer not null primary key, name text);
	delete from foo;
	`
	_, err = db.Exec(sqlStmt)
	if err != nil {
		log.Printf("%q: %s\n", err, sqlStmt)
		return
	}

	tx, err := db.Begin()
	if err != nil {
		log.Fatal(err)
	}
	stmt, err := tx.Prepare("insert into foo(id, name) values(?, ?)")
	if err != nil {
		log.Fatal(err)
	}
	defer stmt.Close()
	for i := 0; i < 100; i++ {
		_, err = stmt.Exec(i, fmt.Sprintf("こんにちわ世界%03d", i))
		if err != nil {
			log.Fatal(err)
		}
	}
	tx.Commit()

	rows, err := db.Query("select id, name from foo")
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()
	for rows.Next() {
		var id int
		var name string
		err = rows.Scan(&id, &name)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(id, name)
	}
	err = rows.Err()
	if err != nil {
		log.Fatal(err)
	}

	stmt, err = db.Prepare("select name from foo where id = ?")
	if err != nil {
		log.Fatal(err)
	}
	defer stmt.Close()
	var name string
	err = stmt.QueryRow("3").Scan(&name)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(name)

	_, err = db.Exec("delete from foo")
	if err != nil {
		log.Fatal(err)
	}

	_, err = db.Exec("insert into foo(id, name) values(1, 'foo'), (2, 'bar'), (3, 'baz')")
	if err != nil {
		log.Fatal(err)
	}

	rows, err = db.Query("select id, name from foo")
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()
	for rows.Next() {
		var id int
		var name string
		err = rows.Scan(&id, &name)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(id, name)
	}
	err = rows.Err()
	if err != nil {
		log.Fatal(err)
	}
}

P.S.  2025/1/13

”実はcgo free以外に、M1 Macの動作ではgo run ***では動作しても、./***では起動も終了もしないという不思議な現象が出てました”

この件に関連して、DB(sqlite3)を使う別のコードではビルドもできず、やはりIntel Macでは問題なくビルドできて動作したので、おそらくVenturaのアップデートと関連ありますね

 

admin

Docker Desktopがマルウェア扱いになる

おそらくMac OSのsequoiaと関連していてDocker Desktopのアップデートが原因思われますが、このようなエラーが出て起動できません

検索すると、https://zenn.dev/masakiee/articles/4c6f7e68a5a31c

同じような事例は見えるのでworkaroundで緊急避難はできるようですが、

恒久対策は検討中、

https://www.dockerstatus.com

January 8, 2025 10:41 PST
January 8, 2025 18:41 UTC[Identified] The root cause has been identified: some files in existing installations are incorrectly signed which can be fixed by copying correctly signed files from the Docker.app application bundle.

We are working on a proper fix.

まあDocker Desktopは使わないで、Rancher Desktop(Baker link.ですでにインスト済み)に移行してしまうというのもありだろうね、Rustのクロス環境はPodman + Podman DesktopだからあまりDocker Desktopにこだわる必要もない

ついでに、% docker versionで見てみると、Client(rancher-desktop)もServerもgolangで書かれてるんだね

% docker version
Client:
 Version:           27.2.1-rd
 API version:       1.45 (downgraded from 1.47)
 Go version:        go1.22.7
 Git commit:        cc0ee3e
 Built:             Tue Sep 10 15:41:09 2024
 OS/Arch:           darwin/arm64
 Context:           rancher-desktop

Server:
 Engine:
  Version:          26.1.5
  API version:      1.45 (minimum version 1.24)
  Go version:       go1.22.5
  Git commit:       411e817ddf710ff8e08fa193da80cb78af708191
  Built:            Fri Jul 26 17:51:06 2024
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          v1.7.17
  GitCommit:        3a4de459a68952ffb703bbe7f2290861a75b6b67
 runc:
  Version:          1.1.14
  GitCommit:        2c9f5602f0ba3d9da1c2596322dfc4e156844890
 docker-init:
  Version:          0.19.0
  GitCommit:        

 

admin

Baker link. 起動手順についての補足

Baker link. ENVは今少し不安定(Dockerの対応するコンテナを削除しないと動かない時がある)のように見えるので、手順を補足説明

<環境>

M1 MacBook Air sequoia

cargo/rustc 1.82.0

Rancher Desktop 1.16.0

<手順>

基本は、

https://baker-link.org/Blink/Baker_link_Env_page.html

ですが、途中いくつか鬼門あるので手順を記載

① historyから作成済みならば、作成済みプロジェクトの履歴からクリックして開く

② 「コンテナで再度開く」を選択

ここでコンテナにファイルが存在しない状態、つまりVScodeでソースコードが開かない時があって、その場合にはDockerのコンテナ(dockerという名前)を削除して最初からやるとうまくいく、imageはbaker-link-envという名前でこれが残っていればコンテナ展開されるだけ、このイメージがDockerに存在していなければBaker link. ENVがダウンロードするんだろう

③ probe-rs見つからないと言われる -> install/failどちらも無視

④ F5でデバッガ起動

⑤ 「cargo buildを見つけられません」のポップアップ -> 「このままデバッグ」を選択(すでに作成済みなので)

もし現在のソースファイルでビルドされていなければ、bash(Docker コンソール)でビルドする(qemu-system-aarch64使っているのでそこそこ遅い)

F5を押したタイミングでラズピコにDocker上のビルドされたバイナリを転送するようです

⑥ デバッグボタンが現れる

 

⑦ デバッグボタン列の一番左の青ボタンでデバッグが起動する

この時にはdfmtコンソール現れてデバッグ開始できる状態になってます

コンソールで前回の出力と連続しているけれども、INF0 Program start!がソースの22行目に埋め込まれてるinfo!(“Program start!”);からの出力でデバッグ開始の確認になります

 

admin

Baker link.で作成済みのプロジェクトを再度開くとき

作成したBaker link.のプロジェクトを再度開く時にどうするのかなと思って、再度作成をしてしまうとすでに作成されたプロジェクトに階層構造でつながって新たに作成されます

 

 

Baker link. Envのhistoryボタンをクリックすると過去に作成されたプロジェクトが出てくるのでそこをクリックすれば良いだけです、新規に階層構造で作成されたプロジェクトはFinderで削除実施

画像は削除した履歴(三階層の二番目と三番目)を指定しているのでLog出力で見つからないと言われてます

 

admin

ラズピコのRustデバッグ環境をBaker link.で作る

今年の九月ごろにリリースされていますが、Baker link.がdebug-probe(ラズパイ財団純正)でも動くという記述あったので使ってみた

<確認環境>

・M1 Mac Sequia 15.2

・debug-probe

<Baker link. tutorial>

https://baker-link.org/introduction.html

tutorial実行で必要な開発環境としては、

① Docker Desktop(suse版):Rancherなので実際はDockerが起動される

② VScode

③ probe-rs

開発コンソールには、Baker link.Envをインストールして、そこから起動できます

<つかえたところ>

なぜかVScode起動後の『コンテナでサイド開く』クリックでprobe-rs見つからないと言われ(ちゃんとインストしてるのに)たので再度コンソールからインスト、以下はVScode ポップアップメニューのインストールを選んだ後のメッセージ、サイドのインストでも同じメッセージ出たけど無視して進めたら通った

Installation failed. Go to https://probe.rs to check out the setup and troubleshooting instructions. baker link

probe-rsのインストコマンド、

% curl --proto '=https' --tlsv1.2 -LsSf https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.sh | sh

F5起動(デバッグスタート)すると実行ファイル見つからないと言われる、ならばとDockerのコンソールからコマンドラインでビルドしてやるとF5でデバッガ起動して普通に使えるようになりました

ハードはHUB75 LEDアレイ駆動時そのままの接続、

コードエディタはVScodeそのまま、新規にウインドを開きます

現在までに種々のデバッグ環境が公開されてますが、Dockerを使う(クロス環境のCross + Podmanに似ている)のが一番筋が良さそうだからこれが今のデフォルト

P.S. リソースは大量消費しているようだけど宿命

 

admin

Arduino言語でのHUB75 LEDアレイ制御の一応の完成形

基本の表示機能の作成から、

https://isehara-3lv.sakura.ne.jp/blog/2024/12/16/hub75パネルを動かしてみた/

① 画像イメージ情報を3パターン用意

② LED駆動用にUSBの口が二つあるACアダプタ(二つで3Aの容量)用意してラズピコのケーブルも接続できるようにして物としてのまとまりをよくする

USB – DCプラグケーブル購入して、ジャックは見繕って加工してつないだ

③ M5Stackをコントローラーにして3パターンの画像切り替えと表示オフ機能を持たせた

ラズピコとM5Stackのコードと、LEDアレイとラズピコ接続回路図は以下のリンクから、

https://github.com/chateight/hub75_led_array_drive

Arduino言語ではこれでほぼ終わりの予定

 

admin

HUB75パネルをRustで駆動してみた

Arduino言語との速度比較のために、Rustでループ処理部分をベンチマークしてみた

プラットホームはembassyを使って、そのソースコードに埋め込み、表示パターンは階調表示のあるテストパターン、routine.rsは表示データの初期化を行うだけですが

コードは二つに分割

<main.rs>

//
// % cargo run --bin raspico

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_rp::gpio::{Level, Output};
use {defmt_rtt as _, panic_probe as _};

mod routine;
use routine::DispData;

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_rp::init(Default::default());
    let mut r1 = Output::new(p.PIN_2, Level::High);
    let mut r2 = Output::new(p.PIN_5, Level::High);
    let mut g1 = Output::new(p.PIN_3, Level::High);
    let mut g2 = Output::new(p.PIN_8, Level::High);
    let mut b1 = Output::new(p.PIN_4, Level::High);
    let mut b2 = Output::new(p.PIN_9, Level::High);
    let mut a = Output::new(p.PIN_10, Level::Low);
    let mut b = Output::new(p.PIN_16, Level::Low);
    let mut c = Output::new(p.PIN_18, Level::Low);
    let mut d = Output::new(p.PIN_20, Level::Low);
    let _e = Output::new(p.PIN_22, Level::Low);
    let mut clk = Output::new(p.PIN_11, Level::Low);
    let mut lat = Output::new(p.PIN_12, Level::High);
    let mut oe = Output::new(p.PIN_13, Level::High);

    let a_array: [u8; 16] = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1];
    let b_array: [u8; 16] = [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1];
    let c_array: [u8; 16] = [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1];
    let d_array: [u8; 16] = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1];

    let mut disp_data = DispData {
        _array64: [0; 64],
        array4d: [[[[0; 64]; 32]; 3]; 2],
    };
    let new_disp_data = disp_data.new();

    loop {

        for i in 0..16 {
            // row number
            if a_array[i] == 0 {
                a.set_low();
            } else {
                a.set_high();
            };
            if b_array[i] == 0 {
                b.set_low();
            } else {
                b.set_high();
            };
            if c_array[i] == 0 {
                c.set_low();
            } else {
                c.set_high();
            };
            if d_array[i] == 0 {
                d.set_low();
            } else {
                d.set_high();
            };

            for b in 0..15 {
                // shading
                lat.set_high();
                for k in 0..64 {
                    // column data
                    clk.set_low();
                    //
                    if new_disp_data.array4d[0][2][i][k] > b {
                        r1.set_high();
                    } else {
                        r1.set_low();
                    }
                    if new_disp_data.array4d[0][2][16 + i][k] > b {
                        r2.set_high();
                    } else {
                        r2.set_low();
                    }
                    if new_disp_data.array4d[0][1][i][k] > b {
                        g1.set_high();
                    } else {
                        g1.set_low();
                    }
                    if new_disp_data.array4d[0][1][16 + i][k] > b {
                        g2.set_high();
                    } else {
                        g2.set_low();
                    }
                    if new_disp_data.array4d[0][0][i][k] > b {
                        b1.set_high();
                    } else {
                        b1.set_low();
                    }
                    if new_disp_data.array4d[0][0][16 + i][k] > b {
                        b2.set_high();
                    } else {
                        b2.set_low();
                    }
                    clk.set_high();
                }
                oe.set_high();
                lat.set_low();
                oe.set_low();
            }
        }
    }
}

<routine.rs>

表示データの作成をするだけ、


static ARRAY64: [u8; 64] = [
    0, 1, 1, 2, 3, 5, 8, 13, 13, 8, 5, 3, 2, 1, 1, 0, 0, 1, 1, 2, 3, 5, 8, 13, 13, 8, 5, 3, 2, 1,
    1, 0, 0, 1, 1, 2, 3, 5, 8, 13, 13, 8, 5, 3, 2, 1, 1, 0, 0, 1, 1, 2, 3, 5, 8, 13, 13, 8, 5, 3,
    2, 1, 1, 0,
];

pub struct DispData {
    pub _array64: [u8; 64],
    pub array4d: [[[[u8; 64]; 32]; 3]; 2],
}

impl DispData {
    pub fn new(&mut self) -> Self {
        let mut dis_data = DispData {
            _array64: ARRAY64,
            array4d: [[[[0; 64]; 32]; 3]; 2],
        };
        for j in 0..3 {
            for i in 0..32 {
                dis_data.array4d[0][j][i] = ARRAY64;
            }
        }
        dis_data
    }
}

<実行速度>

およそ10ms(Arduino言語ではおよそ14msだから3割程度高速化)、繰り返し(メモリ読み出しとgpio書き込み)回数は一回のリフレッシュループで、

64*3*16*32 = 98,304(16行*3色*パネル上下*16階調)ということで一回のr/g/bデータの書き込みでおよそ1μs費やしている勘定で、クロック数ではおよそ130クロックとなります、階調処理での条件分岐が処理時間では大きそうですね

写真は行アドレス信号のDなので一周期がリフレッシュ周期

結果だけ見るとRustの優位性はそれほど無いように見えるかもしれないけど、メモリ管理やコンパイラの出来の良さ、複数タスク管理をasync/awaitで簡単に実現できるから他の言語で実現(Arduino言語ならRTOS使うことになるけれども、async/awaitのほうが軽量だと)するよりもアドバンテージはあると思う

P.S. 2024/12/24

この程度(3割)の改善ならば、pico 2を使えば簡単に逆転できそうだよね、消費電力とか別にすればね、ハードの進化は偉大なりか

 

admin

 

Rustでの非同期処理(async/await)

Rustの組み込み用のフレームワークにembassyがありますが、そのサンプルコードの中では非同期処理が使われているので、概略を見てみる

https://qiita.com/Kumassy/items/fec47952d70b5073b1b7

参考はちょっと古いですが、上記のリンクですが、tokioのバージョンでタイマー待ちの処理が変わっているので最新に合わせてコードを変えています

<環境>

・M1 Macbook Air

・rustc 1.82.0

・tokio = { version = “1.42”, features = [“full”] }
#[tokio::main]はコンパイル時に展開されて、
Rustがデフォルトでは持っていない非同期処理を可能とする
<サンプルコード>
//
// original from https://qiita.com/Kumassy/items/fec47952d70b5073b1b7
//
// [dependencies]
// tokio = { version = "1.42", features = ["full"] }
use std::{thread, time};
use tokio::time::{sleep, Duration};

async fn hello() -> String {
    let one_sec = time::Duration::from_millis(1000);
    // in case of async application, it looks better to use tokio::time crate
    // delay_for : before tokio 0.2
    sleep(Duration::from_millis(1000)).await;
    thread::sleep(one_sec);

    "hello, async fn".to_string()
}

// tokio: to support async runtime
#[tokio::main]
// the async keyword of the fn main() is necessary
async fn main() {
    // excute sysnc function and wait completion
    let greeting: String = hello().await;
    println!("{}", greeting);

    // new task create(spawn)
    // return the type Result
    // 
    let handle = tokio::spawn(async {
        sleep(Duration::from_millis(1000)).await;
        "spawn thread elapsed one second".to_string()
    });

    // define async block
    //
    let world = async {
        let one_sec = time::Duration::from_millis(1000);
        thread::sleep(one_sec);

        println!("hello, async block");
    };

    // rcv spwan thread return
    let result: String = handle.await.unwrap();
    println!("{}", result);

    // async block call and wait
    world.await;

    // wait to avoid main thread terminate before spawn task complete
    sleep(Duration::from_millis(1500)).await;
}

やっていること、

① async fn hello()での非同期処理

② asyncブロック(let world)での非同期処理

③ async非同期タスク(tokio::spawn)からの戻り値(let result: String)の取得

<実行結果>

hello, async fn
spawn thread elapsed one second
hello, async block

複数箇所に時間待ちを入れているので、その時間待ちが入って出力されます

let one_sec = time::Duration::from_millis(1000); 
sleep(Duration::from_millis(1000)).await;

tokioには時間街のためのsleep()関数がありますが、std::time::Durationとの違いはsleep()関数自体がasyncブロックの如く機能するということなので使い分けは必要です

 

admin