目次
この記事でできること
- STM32のUSARTをUART(非同期)として設定できる
- printfをPC(TeraTerm)へシリアル出力できる
- UARTとUSARTの違い(STM32の見え方)が分かる
- よくあるトラブル(表示されない/文字化け等)を回避できる
関連記事:


はじめに
STM32開発ボードでLチカができたのでちょっと気分が良いgoldear@JiN_C125です。
前回はSTM32H723ZG Nucleo 開発ボードに実装されているUser LEDを点滅させることができました。LEDでの点灯/消灯で動作をデバッグする方法もありますが、いかんせん効率が悪いです。点灯パターンで特定のエラーと関連付けるのにも限界があります。
本記事では、STM32H723ZGでUSARTをUART(非同期)として使い、printfデバッグ環境を作っていきます。以降の開発(OLED、SD、FFTなど)で必須になるため、最初に土台を固めていきましょう。
開発環境
- 開発基板:STMicro Nucleo STM32H723ZG (秋月電子)
- 開発環境:STM32CubeIDE
- MCU設定:STM32CubeMX
- ターミナル:TeraTerm(Windows)
- 接続:USB-microB
TeraTermは以下からダウンロードできます。
全体の流れ
- CubeMXでUSARTをAsynchronous(UART)として有効化
- printfの出力先をUARTへリダイレクト(_write実装)
- TeraTermでログが表示されることを確認
- 内部(UART/USART、改行、ブロッキング送信)を理解
- ハマりポイント・注意点を確認
開発基板とターミナルを通じて通信できるとLチカとはまた違った達成感があるので、これも組み込み界のHelloWorldと思って実装していきましょう。
Step1. (CubeMX設定)
ユーザーマニュアル UM2407 7.6.5「Virtual COM port (VCP): LPUART/USART」よりSTLINKのUSARTは以下の通り分かります。前回紹介したように回路図から調べてもOKです。
- USART3 TX → PD8
- USART3 RX → PD9
MCUの PD8, PD9 をUSART3に設定します。
次にUSARTを有効化します。
設定手順:
- Connectivity → USART3 → Asynchronous を選択
- 通信パラメータ(ボーレート等)を設定
- TX/RXのピン割り当てを確認
Mode Disable から Asynchronous に変更することで、UART(非同期)として動作し、通信設定が可能となります。合わせて PD8, PD9 ピンにUser Labelを付与しておきましょう。基本的にはSymbol Pinと同じ名前で良いかと。
次に Configuration -> Parameter Settings から通信パラメータを設定します。デフォルト設定のままで問題ありませんが、下記のパラメータになっていることを確認します。
Word : 8bit
Parity : None
Stopbit : 1
(115200-8-N-1)
Nucleo は ST-LINK 経由で Virtual COM Port が出るため、USB接続だけでPCと通信できることが多いです。
NucleoをPCに接続すると、デバイスマネージャーが認識していることを確認できます。
Step2. (コード実装)
CubeMXでコード生成を行うと、USART3の初期化コードが自動生成されます。
MX_USART3_UART_Init();
次に printf の出力先をUSART3へ接続します。
STM32CubeIDE(newlib/newlib-nano等)の設定によって、printf の出力が __io_putchar() 経由になったり、_write() 経由になったりします。
実務的には どちらでも動くように両方実装しておくと、設定差に強くなります。
今回の実装ファイルは以下の通りです。配置場所はプロジェクト方針に合わせて調整してください。(ここでは plat 配下に置く例)
#ifndef PLAT_UART_STDIO_H_
#define PLAT_UART_STDIO_H_
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief printf の出力先を UART に接続する(将来拡張用)
*
* 現状は NOP でもOK。
* DMA化/リングバッファ化する場合に、この関数で初期化を入れる想定。
*/
void plat_uart_stdio_init(void);
#ifdef __cplusplus
}
#endif
#endif /* PLAT_UART_STDIO_H_ */
[/text]
[text title="uart_stdio.c"]
#include "plat/uart_stdio.h"
#include "usart.h" // huartX がここに extern 宣言される(CubeMX生成)
#include "stm32h7xx_hal.h"
#include <stdint.h>
#include <sys/unistd.h> // STDOUT_FILENO / STDERR_FILENO
void plat_uart_stdio_init(void)
{
// NOP(将来、リングバッファ/DMA化する場合にここで初期化)
}
/**
* @brief 1文字送信(CubeIDE設定によって printf がここに来ることが多い)
*
* \n が来たら \r\n に変換して、TeraTerm等で改行が崩れないようにする
*/
int __io_putchar(int ch)
{
uint8_t c = (uint8_t)ch;
// 使用するUARTに合わせる(CubeMXで生成されたハンドル名)
// 例:USART3を使っているなら huart3
if (c == '\n') {
uint8_t cr = '\r';
(void)HAL_UART_Transmit(&huart3, &cr, 1, HAL_MAX_DELAY);
}
(void)HAL_UART_Transmit(&huart3, &c, 1, HAL_MAX_DELAY);
return ch;
}
/**
* @brief write() syscall(printfがこちらを使う構成もあるので保険で実装)
*
* newlib系では printf → _write() 経由になることがある
* __io_putchar() と両方実装しておくと、プロジェクト設定差に強くなる
*/
int _write(int file, char *ptr, int len)
{
if ((file != STDOUT_FILENO) && (file != STDERR_FILENO)) {
return -1;
}
for (int i = 0; i < len; i++) {
__io_putchar((unsigned char)ptr[i]);
}
return len;
}
上記の uart_stdio.c では、改行(\n)をCR+LF(\r\n)へ変換しています。
表示崩れ防止のためにこの変換を入れています。
Step3. (動作確認)
main.c 側で uart_stdio を取り込み、printf を実行してみます。
以下は動作確認用の最小例です(差分イメージ)。
#include <stdio.h>
#include "plat/uart_stdio.h"
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART3_UART_Init();
plat_uart_stdio_init();
printf("GN-beluga_fw boot!\n");
while (1)
{
printf("Hello beluga!\n");
HAL_Delay(1000);
}
}
TeraTerm 側は下記の設定にします。
データ : 8bit
パリティ : none
ストップ : 1bit
フロー制御 : none
表示例:
GN-beluga_fw boot! Hello Beluga! Hello Beluga! Hello Beluga!
ここまで表示できれば成功です。
以降、OLEDやSDのデバッグでも printf が強力な武器になります。
Step4. (内部解説)
UARTとUSARTの違い
- UART :非同期シリアル通信専用
- USART:同期+非同期の両対応(UARTを含む上位互換)
STM32CubeMXで「USART」と表示されても、Asynchronous(非同期)modeで使っていれば UART として動作しています。
printfがUARTに流れる仕組み
printfは内部的に「文字を出力する関数」を呼びます。
その出口を __io_putchar() や _write() で用意することで、printfの出力をUARTから送信するように設定します。
printfの出力フローは以下のようなイメージです。
printf → (__io_putchar または _write) → HAL_UART_Transmit → UART送信 → PC表示
今回は動けばOKというスタンスで実装しましたが、実は printf の裏側では Cランタイムやリンカの仕組みが動いています。
このあたりもいずれ深掘りしてみようと思います。
改行はなぜ \r\n が必要?
使用するターミナルによって改行コードの解釈が微妙に違います。
\n だけでも動く環境もありますが、ログが1行に繋がったり、表示が崩れることがあります。
こういった環境依存トラブルを避けるため、
本記事では安全サイドに倒して \r\n(CR+LF)に統一しています。
HAL_UART_Transmitはブロッキング送信
今回の実装は分かりやすさ優先で「送信完了まで待つ(ブロッキング)」方式です。
送信中はCPUが待ち状態になるため、大量ログを出す用途では処理が重くなることがあります。
実運用ではリングバッファやDMA送信による非ブロッキング化を行うのが一般的です。
このあたりは次回以降の「ログ基盤」記事で扱う予定です。
Step5. (ハマりポイント・注意点)
今回は大きなトラブルはありませんでしたが、UARTまわりは環境依存でハマりやすいポイントが多いです。
よくある注意点をまとめておきます。
1) COMポートが出ない
- USBがST-LINK側に挿さっているか確認
- 充電専用ケーブルを疑う(通信不可のケーブルがある)
- デバイスマネージャーでポートが認識されているか確認
2) 文字化けする
- ボーレート不一致(CubeMXとTeraTermを一致させる)
- 8-N-1(8bit/none/1bit)になっているか確認
3) 何も表示されない
- USART初期化(MX_USART3_UART_Init)が実行されているか確認
- huartの取り違え(huart1/2/3)を確認
- 別のUSARTを設定していないか確認(TX/RXのピン含む)
4) 改行が崩れる
- \n だけだと環境によっては改行にならない
- 基本は \r\n を送る(今回の実装は \n を \r\n へ変換している)
5) printfを多用すると動作が重い
- HAL_UART_Transmitはブロッキング送信(待ちが発生する)
- 大量ログ時はDMA送信やバッファ化、ログレベル制御を検討する
まとめ
今回はUSARTをUART(非同期)として設定し、printfの出力をシリアル経由でPCに表示できるようにしました。マイコン内部の状態を文字として確認できるようになり、デバッグ効率が一気に上がります。
次回はこのprintfを発展させ、ログレベル付きのログ基盤へと作り込んでいきます。printfを直接使うのではなく、printf_info / printf_error / printf_fatal のようなログ基盤を作って運用しやすさを向上させます。
前回

次回: (準備中)










コメント