組み込みでprintfを使う方法

PCのプログラミングでは一般的に使用されるprintfが、マイコンではそのままでは何も表示してくれません。出力した文字の行き先がどこにも接続されていないためです。本記事では、組み込み開発でprintfを使えるようにする代表的な3つの方法(UARTリダイレクト・セミホスティング・ITM(SWO))の仕組みと使い分けを紹介します。あわせて、printfデバッグ共通の注意点も解説します。

なぜprintfはそのままでは動かないのか

PCでは、OSが標準出力を端末(ターミナル)へ接続します。一方マイコンにはOSも画面もありません。組み込み向けの標準Cライブラリ(GNU系ツールチェーンのGCC + newlibが代表的)は、printfの書式処理まではしますが、最終的な「1文字をどこへ出すか」は低レベル入出力関数としてユーザ実装に委ねる構造になっています。つまりprintfを使うとは、この最下層の出力先を自分で用意することにほかなりません。この記事では、開発用PCを「ホスト」、マイコン実機を「ターゲット」、ホスト側のシリアル端末ソフトを「ターミナル」と呼びます。

方法1:UARTへのリダイレクト(最も標準的)

printfの出力先をUART(シリアルポート)へつなぎ替え、PCのターミナルソフトで表示する方法です。実装としては、Cライブラリが呼ぶ低レベルの出力関数を用意し、その中でUARTの送信関数を呼びます。GCC+newlib環境では、システムコールスタブの _write() を実装します。

なお、環境によっては _write() ではなく _write_r() 側が呼ばれる構成もあります(newlibの設定やツールチェーンの差によります)。生成されるsyscallsのひな形やライブラリの仕様を確認してください。

/* newlib環境:printfの出力をUARTへ流す */
int _write(int file, char *ptr, int len)
{
    (void)file;
    for (int i = 0; i < len; i++) {
        uart_putc(ptr[i]);   /* 1文字送信(送信完了待ち) */
    }
    return len;
}

この最小実装ではfileを区別していないため、実務ではfd=1(stdout)とfd=2(stderr)だけをUARTへ出力し、それ以外のfdにはnewlibが期待する形式でエラーを返す(戻り値-1、errno設定)という方針にすると安全です。uart_putc() は使用するマイコンのUART送信関数に置き換えます。一部のマイコンベンダが提供する統合開発環境(IDE)のように __io_putchar() を差し替える方式のツールチェーンもありますが、考え方は同じです。実装時の注意点は次の3つです。

  • 改行で出力されない場合はバッファリングが原因のことがある。stdoutのバッファリング方式(行バッファ・フルバッファ・非バッファ)はCライブラリの設定や isatty() の実装に依存するため、ベアメタル環境では既定値が環境ごとに異なる。対策としては、文末に改行(LF)を入れる、fflush(stdout) する、setvbuf(stdout, NULL, _IONBF, 0) で無効化する、のいずれかを試す。とくにベアメタル環境では、標準出力が「端末(tty)」として扱われず、改行しても出力されない、または出力が途中まで溜まることがあります。まずは setvbuf(stdout, NULL, _IONBF, 0); で非バッファ化しておくと安定します。
  • newlib-nano系(--specs=nano.specs を使う構成など)ではサイズ削減のため浮動小数点の書式(%f)が無効な構成が多い。必要ならリンク時オプション -u _printf_float を指定し、リンクされていない書式処理を強制的に取り込む(環境によっては -Wl,-u,_printf_float のようにリンカへ渡す形になったり、IDEでは「use float with printf」等の設定項目になっている場合もある。フラッシュ消費が数KB〜十数KB増えることがある点に注意)。
  • 上記実装は送信完了までブロックする。8N1(1文字10ビット)を想定すると、115200 bpsでは1文字あたり約87μsかかるため、長い文字列の連発は処理時間を圧迫する。設定が異なれば所要時間も変わる。

デバッガなしで実機単独でも動くため、量産前の評価や現地調査でも使える最も実用的な方法です。

方法2:セミホスティング(デバッガ経由)

セミホスティングは、ターゲット(マイコン実機)上のコードが発行した入出力要求を、デバッガ経由でホスト側に処理させる、Armアーキテクチャ向けに規定された仕組みです。printfの出力はIDEのコンソール画面に表示されます。UARTの配線が不要で、設定だけで使い始められるのが利点です。

一方で重大な注意点が2つあります。第一に、出力のたびにCPUがBKPT命令でデバッグ状態に入り、デバッガがホスト側で処理を終えるまでホールトされるため、実行速度が大幅に低下します。第二に、セミホスティングの呼び出しはブレーク命令(BKPT。慣例的に 0xAB が使われます)を実行してデバッガに処理を依頼する仕組みです。そのため、デバッガが介入しない(未接続または無効の)状態でこの命令が実行されると、DebugMonitorの有無など環境設定によっては、HardFaultが発生する、ロックアップして停止するなど、「起動しない」症状になり得ます。デバッグ用の設定を残したまま量産機にプログラムを書き込み、「現場で起動しない」という事故は実際に起きています。リリースビルドでは必ず無効化する必要があります。

方法3:ITM/SWO出力(Cortex-M0/M0+やCortex-M23などを除く多くのコア)

Arm Cortex-M系(例:Cortex-M3/M4/M7/M33など)の多くのコアには、ITM(Instrumentation Trace Macrocell)というトレース用の出力機構があり、SWOピン1本でデバッガへ文字を送れます。出力はIDEのトレースビューア(SWVビューア、ITMビューアなどと呼ばれます)に表示されます。ITMは文字を送出する機構、SWOはそのデータを伝送するピンで、両者は別のものです。マイコンベンダが提供する標準ソフトウェアインターフェースの文字送出関数(ITM_SendChar() など)を _write() の中で呼べば、printfの出力先にできます。UARTより高速・低負荷で、動作タイミングへの影響を抑えたい場面に向きます。ただし、ITMはCoreSightのオプション機能であり、利用できるかどうかはコアとデバッグ回路の実装に依存します。Cortex-M0/M0+やCortex-M23などでは非搭載が一般的で、通常は使えません。デバッガとSWOピンの接続・設定が必要なことも条件です。

ITM(SWO)は、トレースの有効化(ITM/TPIUの設定)とデバッガ側の受信設定が揃ってはじめて表示されます。設定が済んでいない間は出力が捨てられる前提で設計し、必要なら送信前に有効かどうかを判定します。

3つの方法の比較

方法 追加配線 デバッガ 速度・負荷 実機単独動作
UARTリダイレクト TXピン1本 不要 中(ブロッキング) 可能
セミホスティング 不要 必須 低速(CPU停止あり) 不可(停止する)
ITM(SWO) SWOピン1本 必要 高速・低負荷 可(デバッガ未接続時は表示不可/出力は破棄され得る)

開発初期はセミホスティングで素早く始め、常用はUARTリダイレクトへ、タイミング制約が厳しい箇所はITM(SWO)へ、という使い分けが現実的です。

printfデバッグ共通の注意点

  • 割り込みハンドラ内で使わない:ブロッキングで割り込み時間が延び、他の割り込みやリアルタイム性を壊す。printf自体もリエントラントではない。割り込み内ではフラグだけを立てて送信はタスクやメインループでおこなう、ログ文字列はリングバッファに積んでUARTはDMAでまとめて送る、といった形に分離する。
  • printfを入れること自体が動作タイミングを変える点に注意する:printfを入れると直る/消すと再発する場合、タイミング起因のバグを疑う。
  • %s にNULLポインタや壊れた文字列を渡さない:printf内部でフォールト(障害)が発生することがあり、デバッグ手段が新たなバグ源になり得る。
  • 量産コードに大量のprintfを残さない:最終的にはログレベル制御やリングバッファへの記録など「ログ設計」へ発展させる。

まとめ

printfが動かないのはマイコンが特殊だからではなく、出力先を自分で決める設計になっているからです。仕組みを理解すれば、UART・セミホスティング・ITMを場面に応じて使い分けられるようになります。printfはあくまで開発時の道具と割り切り、製品コードではログ設計へ移行するとよいでしょう。

組み込みソフトの世界 トップへ戻る