cargo-embed

cargo-embed は cargo-flash の兄貴分です。

cargo-flash と同じようにターゲットへフラッシュできますが、RTT ターミナルや GDB サーバーを開くこともできます。

さらに、今後も多くの機能が追加される予定です!

Installation

cargo-embedprobe-rs ツール群の一部としてインストールされます。詳しくは インストール ページを参照してください。

Usage

ほかの cargo コマンドと同じように使用できます。

Terminal window
cargo embed [OPTIONS] [CONFIG_PROFILE]

これにより、次の処理が順に実行されます。

  1. バイナリをビルドする
  2. プローブを検出する
  3. (有効な場合)内容を接続されたターゲットに書き込む
  4. (有効な場合)ターゲットをリセットする
  5. (有効な場合)RTT ホスト側を開始する
  6. (有効な場合)GDB デバッグを開始する

Configuration

プロジェクトディレクトリにある Embed.toml(または .embed.toml)というファイルで cargo-embed を設定できます。

設定ファイルの優先順位:

  1. Embed.local.*
  2. .embed.local.*
  3. Embed.*
  4. .embed.*
  5. デフォルト設定

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!’ が表示されるはずです!

RTT 経由の Hello World 出力

Keyboard shortcuts

CommandAction
^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 ハンドラを同梱していません。これにより、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つのチャネルの出力

ご覧のとおり、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 = true
up_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 経由で RTT データをリアルタイムプロット

ソケット経由で送信されるのは生のバイト列であるため、タイムスタンプは追加されず、パースや 行分割も行われないことに注意してください。これらは TCP エンドポイント側で自由に自分で 実装できます。


これで快適にデバッグできるはずです。コーディングを楽しんでください!