組み込み開発におけるデバッグ出力の重要性
組み込みシステム開発において、実行状態を可視化する手段は不可欠です。JTAG デバッガが使用できない環境や、実機での動作検証時には、シリアル通信を通じたログ出力が最も有効な手段となります。C 言語の標準出力関数である printf を USART ハードウェアに紐付けることで、開発効率は劇的に向上します。
通信ペリフェラルの選定基準
STM32 マイクロコントローラにおいて、デバッグ用UART として USART1 が選定されることが一般的です。これはバス構成に起因します。例えば STM32F4 シリーズでは、USART1 は APB2 バスに接続されており、最大 84MHz のクロック供給が可能です。一方、USART2 や USART3 は APB1 バスに接続され、クロック周波数が制限される場合があります。
高いクロック源は、ボーレート生成の精度向上に寄与します。また、PA9(TX) および PA10(RX) ピンは、SWD デバッグインターフェース(PA13/PA14)と競合せず、多くの開発ボードで外部に引き出されているため、物理的な接続性にも優れています。
STM32CubeMX による設定手順
開発環境として STM32CubeMX を使用し、対象チップを選択した後、USART1 の設定を行います。
通信パラメータの最適化
単に 115200bps を設定するだけでなく、フレーム構成を確認する必要があります。一般的な非同期通信フレームは以下の要素で構成されます。
[Start Bit][Data Bits][Parity][Stop Bits]
推奨される設定値は以下の通りです。
| 設定項目 | 推奨値 | 備考 |
|---|---|---|
| Baud Rate | 115200 / 921600 | 高速 logging には 921600 を推奨 |
| Word Length | 8 Bits | ASCII コードを網羅 |
| Stop Bits | 1 | 汎用性が高い |
| Parity | None | オーバーヘッド削減 |
| Flow Control | None | 配線簡素化 |
HAL ライブラリの初期化構造体は以下のようになります。変数名はプロジェクトに合わせて変更可能です。
UART_HandleTypeDef h_uart_console;
h_uart_console.Instance = USART1;
h_uart_console.Init.BaudRate = 115200;
h_uart_console.Init.WordLength = UART_WORDLENGTH_8B;
h_uart_console.Init.StopBits = UART_STOPBITS_1;
h_uart_console.Init.Parity = UART_PARITY_NONE;
h_uart_console.Init.Mode = UART_MODE_TX_RX;
h_uart_console.Init.HwFlowCtl = UART_HWCONTROL_NONE;
h_uart_console.Init.OverSampling = UART_OVERSAMPLING_16;
ログ出力のみを行う場合、Mode を UART_MODE_TX に限定することでリソースを節約できます。
ピン配置と競合回避
PA9 ピンは USART1_TX として機能しますが、同時に TIM1_CH2 や USB_VBUS などの機能も兼ねています。CubeMX 上で複数のペリフェラルを有効化した際にピン競合が発生した場合は、AFIO によるリマップ機能を利用するか、タイマーなどのPeripheral を別ピンへ移動させる必要があります。
クロックツリーとボーレート精度
USART1 の動作クロックはシステムクロックではなく、APB2 バスのクロック(PCLK2)に依存します。システムクロックを 168MHz に設定していても、APB2 分頻器により 84MHz になっている場合、ボーレート計算の基準は 84MHz となります。
実際のボーレート誤差は 2% 以内に収める必要があります。誤差が大きい場合、特に長距離通信やノイズ環境でフレームエラーが発生します。内部 RC 発振器(HSI)を使用する場合、温度変動による周波数漂移に注意し、可能であれば外部晶振(HSE)を使用してください。
データ送信方式の選択
送信処理には polling と interrupt の 2 種類があります。
Polling 方式
実装が簡単ですが、送信完了まで CPU がブロックされます。
HAL_UART_Transmit(&h_uart_console, (uint8_t*)"Msg\r\n", 5, 1000);
リアルタイム性が要求されるタスクでは避けるべきです。
Interrupt 方式
NVIC 設定で USART1 グローバル割り込みを有効化し、非同期送信を行います。
uint8_t msg_buf[] = "Async Data\r\n";
HAL_UART_Transmit_IT(&h_uart_console, msg_buf, sizeof(msg_buf));
送信完了コールバックで後処理を行います。
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 送信完了処理
}
}
printf 関数のリダイレクト実装
標準ライブラリはデフォルトでは出力先を持っていません。システムコールをフックすることで、USART へ出力を振り向けます。
GCC 環境における _write 実装
newlib は _write 関数を弱定義として提供しています。これをオーバーライドします。
#include <sys/stat.h>
#include <unistd.h>
extern UART_HandleTypeDef h_uart_console;
int _write(int file, char *ptr, int len) {
if (file != STDOUT_FILENO && file != STDERR_FILENO) {
return -1;
}
HAL_StatusTypeDef stat = HAL_UART_Transmit(&h_uart_console, (uint8_t*)ptr, len, 100);
return (stat == HAL_OK) ? len : -1;
}
ブロッキングを防ぐため、タイムアウト値を設定し、長文の場合は分割送信を行うなどの工夫が必要です。
Keil MDK 環境における fputc 実装
ARM コンパイラを使用する場合は fputc を実装します。
int fputc(int ch, FILE *f) {
HAL_UART_Transmit(&h_uart_console, (uint8_t*)&ch, 1, 10);
return ch;
}
Keil のオプションで「Use MicroLIB」を有効にすると、この実装が認識されやすくなります。
Semihosting の無効化
デバッガ接続時にホスト PC へ出力する semihosting 機能は、 standalone 動作時に HardFault を引き起こす原因となります。リンクオプションで -specs=nosys.specs を指定し、独自の実装を使用してください。
スタックサイズの確保
printf は内部で较大的なスタック領域を消費します。特に浮動小数点処理を含む場合、デフォルトのスタックサイズでは溢れる可能性があります。スタートアップファイルにて、スタックサイズを少なくとも 2KB 以上に増やすことを推奨します。
主要なトラブルシューティング
出力が得られない場合、以下のポイントを確認します。
- 文字化け: クロック設定とボーレート誤差を確認。論理アナライザで実波形を計測。
- 無出力: 電源電圧、リセット状態、BOOT ピンの設定を確認。GPIO の初期化が完了しているか LED 点滅などで検証。
- 処理停止: 割り込みが禁止されていないか確認。HAL ドライバは内部で割り込みを待機する場合があります。
高度な logging システムの構築
FreeRTOS における排他制御
マルチタスク環境では、ログが混在するのを防ぐためミューテックスを使用します。
osMutexId_t uart_mutex;
int _write(int file, char *ptr, int len) {
if (osKernelGetState() == osKernelRunning) {
osMutexAcquire(uart_mutex, osWaitForever);
}
HAL_UART_Transmit(&h_uart_console, (uint8_t*)ptr, len, 100);
if (osKernelGetState() == osKernelRunning) {
osMutexRelease(uart_mutex);
}
return len;
}
DMA 転送の活用
CPU 負荷を低減するため、DMA 転送を利用します。送信完了まで CPU を解放できます。
uint8_t dma_buf[256];
volatile uint8_t dma_busy = 0;
void log_dma(const char* str) {
size_t len = strlen(str);
if (len > 255) len = 255;
if (!dma_busy) {
memcpy(dma_buf, str, len);
dma_busy = 1;
HAL_UART_Transmit_DMA(&h_uart_console, dma_buf, len);
}
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
dma_busy = 0;
}
}
ログレベルの管理
マクロを使用して、ビルド時にログ出力レベルを制御します。
#define LOG_INFO 1
#define LOG_DEBUG 2
#ifndef CURRENT_LOG_LEVEL
#define CURRENT_LOG_LEVEL LOG_DEBUG
#endif
#define LOG_PRINT(level, fmt, ...) \
if (level <= CURRENT_LOG_LEVEL) printf(fmt, ##__VA_ARGS__)
リリースビルドではレベルを上げ、デバッグ情報の出力を抑制することで、パフォーマンスとセキュリティを確保できます。