STM32のprintfをTeraTermに出す — コピペで動いたのに意味が分からず2時間溶かした話【Nucleo-L432KC】
はじめに
マイコンの開発をしていると、「いま変数にどんな値が入ってるの?」を知りたい場面が山ほど出てきます。Arduinoなら Serial.print() で一発ですが、STM32だとそうはいきません。
僕は printf をTeraTermに出すだけで 2時間溶かしました。しかも厄介なのが、ネットで見つけたコードをコピペしたら一応動いてしまったこと。動いたのに自分が何を書いたのか全然分かっていない状態で、案の定そのあとTeraTermでうまく表示されず、原因も切り分けられずにハマりました。
この記事は、その printf を
- そもそもなぜマイコンだと一手間いるのか
- コピペした各行が何をしているのか
- TeraTermで表示されないときに何を疑えばいいのか
を、自分がハマった順番でひとつずつ解きほぐしていきます。同じところで止まっている人の2時間を、5分にできたら嬉しいです。
環境
- Nucleo-L432KC(STM32L432KCUx)
- STM32CubeIDE 1.19.0
- ターミナル:Tera Term
前提:なぜ printf はそのままだと何も出ないのか
ここが全部の出発点でした。
PCのCで printf を書けば画面に出ます。これは、OSが「文字の出力先(標準出力)=画面」をあらかじめ用意してくれているからです。
ところがマイコンには、その**「出力先」が最初から存在しません**。printf 自体は「文字を組み立てる」ところまではやってくれるのですが、組み立てた文字をどこに送るかを誰も決めていないので、何もしないと文字はそのまま捨てられます。だから「コードは合ってるのに無反応」が起きます。
つまりやることは1つ。printf の出口を、自分でUSART(シリアル通信)に配線してやること。これを「リターゲット(出力先のすげ替え)」と呼びます。
全体像をバケツリレーで描くとこうなります。
printf を呼ぶと、最終的に _write という関数が呼ばれます。この _write を自分で用意して、中で「USART2に流せ」と書く——それだけで文字がPCまで届くようになります。
なぜ「USART2」なのか(ここを外すと一生出ません)
Nucleo-L432KCにはUSARTが複数ありますが、基板上のST-Link経由でPCの仮想COMポート(VCP)に物理的に繋がっているのはUSART2だけです(TX=PA2 / RX=PA15)。
これが地味に重要で、USART2を選んでおけば、配線ゼロ・USBケーブル1本だけでPCに文字を出せます。逆にここをUSART1などにすると、外付けのUSB-TTL変換器を繋がない限り永遠に無反応です。最初に出力先を選ぶ段階で、ここを間違えるとハードもコードも正しいのに出ない、という地獄が待っています。
設定:CubeMXでUSART2を有効化する
Connectivity → USART2 を選び、Mode を Asynchronous(非同期) にします。
パラメータは以下(今回の値)。
| 項目 | 値 |
|---|---|
| Baud Rate | 115200 Bits/s |
| Word Length | 8 Bits |
| Parity | None |
| Stop Bits | 1 |
| Over Sampling | 16 Samples |
| Hardware Flow Control | Disable |
Word Length 8 ・ Parity None ・ Stop Bits 1 の組み合わせは、シリアル通信でド定番の 「8N1」 という設定です。深く考えず、まずはこれでOK。
そして このBaud Rate(115200)は、あとでTeraTerm側にも同じ数字を入れます。ここがズレると文字化けの直接原因になるので、自分が何にしたかを覚えておいてください。
設定したら Ctrl+S でコード生成。これで MX_USART2_UART_Init() が自動で作られ、上の表の値がそのままコードになります。
コード:3点セットで printf を配線する
やることは3つだけです。
① #include <stdio.h>
printf を使うためのおまじない…ではなく、printf の宣言(プロトタイプ)が書かれたヘッダを読み込んでいます。これが無いとコンパイラが printf を知らずに怒ります。
/* Includes ----------------------------------------*/
#include "main.h"
#include <stdio.h>
② setbuf(stdout, NULL);(main の頭で1回)
int main(void)
{
/* USER CODE BEGIN 1 */
setbuf(stdout, NULL);
/* USER CODE END 1 */
Cの標準出力は、普段は文字をバッファ(一時置き場)に溜めて、改行が来るかバッファが満タンになってからまとめて出す仕組みになっています。マイコンだとこれが曲者で、「改行を付けないといつまでも出てこない」という現象が起きます。
setbuf(stdout, NULL) は、そのバッファをOFFにする命令です。意味は「溜めずに、printfした瞬間に出せ」。これを起動時に1回呼んでおくと、出力が素直になります。
setbuf(stdout, NULL)はsetvbuf(stdout, NULL, _IONBF, 0)と書いても同じ意味(どちらもバッファOFF)です。
③ _write 関数(main の外、USER CODE BEGIN 4 に)
ここが心臓部です。
/* USER CODE BEGIN 4 */
int _write(int file, char *ptr, int len)
{
HAL_UART_Transmit(&huart2, (uint8_t *)ptr, len, 10);
return len;
}
/* USER CODE END 4 */
printf は、最終的にこの _write を呼びます。引数の意味はこうです。
-
file… どの出口か(標準出力なら1)。今回は中で区別せず、全部USART2に流すシンプルな作りです。 -
ptr… 送る文字列の先頭を指すポインタ("Hello world" がメモリ上のどこにあるか)。 -
len… 何バイト送るか。"Hello world" なら 11。
そして中身の HAL_UART_Transmit(&huart2, (uint8_t *)ptr, len, 10) の引数はこうです。
-
&huart2… どのUARTで送るか=USART2=PCに繋がっている出口。 -
(uint8_t *)ptr… 送るバイト列。char *をuint8_t *に**キャスト(型の読み替え)**しています。中身は同じで、HALが「符号なしバイト」を欲しがるので型だけ合わせています。 -
len… バイト数。 -
10… タイムアウト10ミリ秒。「10ms以内に送り終わらなければ諦める」という意味です。
最後の return len; は、「lenバイト出し切ったよ」とライブラリに報告しています。
⚠️ タイムアウトの
10は、短い文字列・115200ならまず問題ありませんが、長い文字列を一気に出すと途中で切れることがあります。安全側に振るならHAL_MAX_DELAY(送り終わるまで待つ)にしておくと安心です。
置き場所も地味に大事で、_write を main.c の中(USER CODE BEGIN 4)に書くと、main.c のグローバルである huart2 をそのまま参照できます。別ファイルに書くと extern UART_HandleTypeDef huart2; が必要になって、そこで詰まりがちです。
これで printf の配線は完了。さっそく出してみます。
/* USER CODE BEGIN WHILE */
while (1)
{
printf("Hello world");
}
…が、ここで2つ目の壁にぶつかりました。
つまずきポイント:出たのにTeraTermで「回らない」
壁1:改行が無くて1行に連結される
最初、"Hello world" の後ろに改行を付けていませんでした。すると当然、文字が切れ目なく繋がって出ます。
Hello worldHello worldHello worldHello worldHello world...
しかも while(1) の中にウェイトが無いので、CPUが全力で延々と撃ち続けて画面が洪水になります。直し方はこう。
while (1)
{
printf("Hello world\r\n"); // 改行を付ける
HAL_Delay(1000); // 1秒に1回だけ出す
}
\r\n の意味も押さえておくと安心です。
-
\n(LF)… 1行下げる -
\r(CR)… カーソルを行の先頭に戻す
TeraTermは両方ないと「下がるけど左に戻らない」階段状の表示になりがちです。だから \r\n をセットで付けるのが安全。これでようやく1行ずつ綺麗に並びます。
壁2:TeraTerm側の設定とポート
ここが僕の本当の犯人でした。コードを直しても最初はうまく回らず、原因を切り分けたら PC側の接続まわり でした。コードは合っていたのに、です。
TeraTermは、設定 → シリアルポート から以下を合わせます。
| 項目 | 値 |
|---|---|
| ポート | COM5(STMicroelectronics … のVCP) |
| スピード | 115200(←CubeMXと一致させる) |
| データ | 8 bit |
| パリティ | none |
| ストップビット | 1 bit |
| フロー制御 | none |
ポイントは2つ。
- スピードはCubeMXと同じ115200にする。 ここが9600のままだと、波形は出ているのに文字として復元できず、文字化けします。
- 正しいポート(STMicroelectronicsのVCP)を選ぶ。 別のCOMポートを掴んでいたり、そもそもPCとの接続が不安定だったりすると、コードが完璧でも何も出ません。
僕はここで時間を溶かしました。「コードを直す」前に「出力先(ターミナルの設定とポート)を疑う」——これが分かっていれば一瞬でした。
この「コードの外側、ハードや接続も疑う」感覚は、前回のセンサー(BNO055)記事でも同じでした。組み込みは、ソフトとハードの両方を行き来して切り分けるのが基本だと、改めて思い知らされた回です。
応用:実際のデータを出してデバッグに使う
printf が通れば、ここからが本番です。「動かない」を「中の値を見る」に変えられます。
例えば、サーボのduty比や、センサーから読んだ角度を流せば、ロジックが合っているか一目で分かります。
int angle = 90;
printf("angle = %d deg\r\n", angle);
これが、これまでの記事(PWMでサーボ/I2Cでセンサー)で「動いているはずなのに確証が持てない」場面を、一気に潰してくれます。
次の壁の予告:
%f(float)を出そうとすると、デフォルトのままだと表示されないことがあります。これは別の設定が絡む話なので、PD制御でゲインや角度を実数で出すときに改めて取り上げます。
まとめ
- マイコンの
printfは出力先が無いので、自分で配線(リターゲット)する必要がある - 配線は3点セット:
#include <stdio.h>/setbuf(stdout, NULL)/_write - Nucleo-L432KCはUSART2がVCPに繋がっているので、ここを選べば配線ゼロでPCに出せる
- 出力が変なときは
\r\nとHAL_Delayを確認 - それでも出ないときは、コードより先にTeraTermの設定(115200・8N1)とポート・接続を疑う
コピペで動いてしまうと「分かった気」になりますが、一度こうして1行ずつ意味を追うと、次にハマったとき自分で切り分けられるようになります。僕の2時間が、誰かの数分になりますように。
次回は、ここで作った printf を武器に、サーボとセンサーをPD制御で統合していきます。



