cargo-embed は cargo-flash の兄貴分です。
cargo-flash と同じようにターゲットへフラッシュできますが、RTT ターミナルや GDB サーバーを開くこともできます。
さらに、今後も多くの機能が追加される予定です!
Installation
cargo-embed は probe-rs ツール群の一部としてインストールされます。詳しくは インストール ページを参照してください。
Usage
ほかの cargo コマンドと同じように使用できます。
cargo embed [OPTIONS] [CONFIG_PROFILE]これにより、次の処理が順に実行されます。
- バイナリをビルドする
- プローブを検出する
- (有効な場合)内容を接続されたターゲットに書き込む
- (有効な場合)ターゲットをリセットする
- (有効な場合)RTT ホスト側を開始する
- (有効な場合)GDB デバッグを開始する
Configuration
プロジェクトディレクトリにある Embed.toml(または
.embed.toml)というファイルで cargo-embed を設定できます。
設定ファイルの優先順位:
Embed.local.*.embed.local.*Embed.*.embed.*- デフォルト設定
TOML ファイルの代わりに、JSON または YAML ファイルも使用できます。自分に合ったものを 選んでください!
簡単な例を示します。
[default.general]chip = "STM32F401CCUx"
[default.rtt]enabled = true利用可能なすべてのオプションは
default.toml
にあります。
この例では、TOML 構文を使って最上位のプロファイルキー default の下に各オプションを
設定しています。
Embed.toml はプロジェクトの一部として扱い、つまり Git 履歴に追加するべきです。ローカル
専用の設定オーバーライドには、Embed.local.toml(または
.embed.local.toml)ファイルを作成し、それを .gitignore に追加できます。ローカル
ファイルが優先されます。
Profiles
Embed.toml 内のデータは 2 階層の構造になっています。外側の層は
設定プロファイル名で、各プロファイルの内側には異なるオプションを持つ一連の
セクションがあります。デフォルトのプロファイル名は “default” です ;)
cargo-embed を呼び出す際には、位置引数 CONFIG_PROFILE として別のプロファイル名を渡せます。これにより、そのプロファイル配下の設定が
default の代わりに読み込まれます(Usage を参照)。
たとえば、Embed.toml では次のようになります。
[default.general]chip = "STM32F401CCUx"
# "default" プロファイルから継承されるため、これを再度設定する必要はありません#[with_rtt.general]#chip = "STM32F401CCUx"
[with_rtt.rtt]enabled = trueこれで cargo embed with_rtt を実行すると RTT が有効になり、cargo embed
は RTT なしのデフォルト設定 “default” を使用します。
RTT
RTT は real time transfers の略で、デバッグホストとデバッグ対象の間でデータ を転送する仕組みです。
本質的には、ターゲットとデバッグホストが読み書きする、設定可能な数のリングバッファを 提供します。このプロトコルはもともと Segger によって公開されましたが、実際のところ、リングバッファを使っている以外に特別な 魔法があるわけではありません。この 仕組みにより、ターゲットからホストへ、またその逆方向にも非常に高速にデータを転送 できます。
RTT の機能:
- 高速な双方向データ転送
- 設定可能な数のチャンネル(バッファ)
- チャンネルはブロッキングにもノンブロッキングにもできます - 好きな方を選べます
このガイドを使えば、probe-rs を使った開発をすぐに高速化できるはずです。
Target
ターゲット側には、rtt-target という、 ターゲットメモリ内に RTT 構造をセットアップし、それらに対してデータを読み書き するための小さなライブラリを提供しています。
ホストファームウェアの最小例は次のとおりです。
#![no_std]#![no_main]
use microbit as _;use panic_halt as _;use rtt_target::{rprintln, rtt_init_print};
#[cortex_m_rt::entry]fn main() -> ! { rtt_init_print!(); loop { rprintln!("Hello, world!"); }}Host
ホスト側では、次を実行するだけです。
cargo embed
Embed.toml ファイルで RTT を有効にしておいてください。
これで、“Terminal” という名前のデフォルトチャンネルに、たくさんの ‘Hello World!’ が表示されるはずです!
Keyboard shortcuts
| Command | Action |
|---|---|
^c | 終了 |
Fn{n} | タブ n に切り替える |
^{n} | タブ n に切り替える |
Tab | 次のタブに切り替える |
Shift+Tab | 前のタブに切り替える |
| Any character | 文字を未送信の入力に追加する |
Backspace | 未送信の入力の最後の文字を削除する |
Enter | 未送信の入力を送信する |
PgUp | 半画面分上にスクロールする |
UpArrow | 上にスクロールする |
PgDn | 半画面分下にスクロールする |
DownArrow | 下にスクロールする |
^l | 現在のタブをクリアする |
panic しても大丈夫!
もちろん、すべての panic を RTT 経由で簡単にログ出力できます!以下は、もっとも単純な panic ハンドラの例です。
#![no_std]#![no_main]
use core::panic::PanicInfo;use microbit as _;use rtt_target::{rprintln, rtt_init_print};
#[cortex_m_rt::entry]fn main() -> ! { rtt_init_print!(); loop { rprintln!("Hello, world!");
for _ in 0..1_000_000 { cortex_m::asm::nop(); }
panic!("This is an intentional panic."); }}
#[inline(never)]#[panic_handler]fn panic(info: &PanicInfo) -> ! { rprintln!("{}", info); loop {} // ここではコンパイラフェンスが必要になるかもしれません。}開いている rttui ビューには panic が表示されるはずです。
私たちは意図的にデフォルトの panic ハンドラを同梱していません。これにより、panic を ログ出力するチャンネルを自分で選べます。
では、どうすればチャンネルを増やせるのでしょうか。続きをお読みください!
チャンネルを好きなだけ!
次のスニペットのように、用意されているマクロを使って複数のチャンネルを定義できます
#![no_main]#![no_std]
use microbit as _;use panic_halt as _;
use core::fmt::Write;use cortex_m_rt::entry;
use rtt_target::rtt_init;
#[entry]fn main() -> ! { let channels = rtt_init! { up: { 0: { size: 512, mode: BlockIfFull, name: "Up zero", } 1: { size: 128, name: "Up one", } 2: { size: 128, name: "Up two", } } down: { 0: { size: 512, mode: BlockIfFull, name: "Down zero", } } };
let mut output2 = channels.up.1; writeln!( output2, "Hi! I will turn anything you type on channel 0 into upper case." ) .ok();
let mut output = channels.up.0; let mut log = channels.up.2; let mut input = channels.down.0; let mut buf = [0u8; 512]; let mut count: u8 = 0;
loop { let bytes = input.read(&mut buf[..]); if bytes > 0 { for c in buf.iter_mut() { c.make_ascii_uppercase(); }
let mut p = 0; while p < bytes { p += output.write(&buf[p..bytes]); } }
writeln!(log, "Messsge no. {}/{}", count, bytes).ok();
count += 1;
for _ in 0..1_000_000 { cortex_m::asm::nop(); } }}この例では、3 つの up チャネルと 1 つの down チャネルを定義します。3 つ目の up チャネルは継続的にログを出力し、2 つ目は情報メッセージを 1 回だけ表示します。1 つ目が 何をするかは、2 つ目のチャネルを見ればわかるでしょう ;)
ホスト側では次のように表示されます
ご覧のとおり、3 つの up チャネルがすべて表示されています。F キーでそれらを 切り替えられます。down チャネルは対応する up チャネルに自動的に関連付けられ、 対応する down チャネルがあるチャネルには入力フィールドも自動的に表示されます。これは チャネル番号によって行われ、up チャネルと down チャネルで同じ番号である必要があります。 これは rttui のデフォルトの挙動で、設定で変更できます。RTT 自体は、up/down の番号の どのような組み合わせでも扱えます。
TCP ソケット経由の外部フロントエンド
チャネルごとに TCP ソケットを設定することで、Cargo Embed の外部からデータを簡単に利用できます。
これは rtt.channels 設定で行います。
たとえば、channel 0 では Defmt による通常のログを設定しつつ、channel 1 の
センサーデータを TCP 経由でリアルタイムプロットアプリにストリームできます。また、channel 2 で
バッテリー電圧も送信できるかもしれません(別のソケット宛てに)。もちろん、前のセクションで
示したように、ファームウェア側でもこれらのチャネルを一致させる必要があります。
[default.rtt]# 書き込み後に RTTUI を開くかどうか。enabled = trueup_channels = [ { channel = 0, mode = "BlockIfFull", format = "Defmt" }, { channel = 1, mode = "BlockIfFull", format = "String", socket = "127.0.0.1:12345" }, { channel = 2, mode = "BlockIfFull", format = "String", socket = "127.0.0.1:12346" },]# UI の設定:tabs = [ { up_channel = 0, name = "Log" }, { up_channel = 1, name = "sensor-data", hide = true }, { up_channel = 2, name = "battery-level", hide = true },]このスクリーンショットは、単一のソケットでこれをどのように利用できるかを示しています。ロガーは
log::trace!() をチャネル 1 にリダイレクトするよう設定されており、その出力がリアルタイム
プロットアプリに送られます。
ソケット経由で送信されるのは生のバイト列であるため、タイムスタンプは追加されず、パースや 行分割も行われないことに注意してください。これらは TCP エンドポイント側で自由に自分で 実装できます。
これで快適にデバッグできるはずです。コーディングを楽しんでください!