STM32CubeMXで始める組み込み開発実践入門

STM32CubeMXの全体像

STM32CubeはSTMicroelectronicsが提供する統合ソフトウェアエコシステムであり、STM32マイコンおよびマイクロプロセッサ向けの開発を支援します。開発者はIARやKeilといった既存のIDEと組み合わせて利用するだけでなく、STM32CubeMX、STM32CubeProgrammer、STM32CubeMonitorなどのツールを自由に取り込むことができます。

代表的なSTM32開発手法を次に示します。

  • レジスタ直接操作:51系マイコンと同じ感覚で行う古典的手法。コードはコンパクトで高速ですが、膨大なレジスタ情報を覚える必要があり、移植性は低いです。
  • 標準ペリフェラルライブラリ(SPL):STがかつて提供していた関数ベースのライブラリ。現在はHALライブラリに置き換わり、更新は停止しています。
  • HALライブラリ + STM32CubeMX:STが推奨する現行の開発フロー。GUIでマイコンのピン配置やペリフェラルを設定し、初期化コードを自動生成します。最新のプロジェクトではこの方式が標準です。

STM32Cubeエコシステムの主要コンポーネント

  • STM32CubeMX:グラフィカルなコンフィギュレーションツール。Cortex‑Mコア向けにはCコードを、Cortex‑Aコア向けにはDevice Treeを生成します。
  • STM32CubeIDE:Eclipseベースの統合開発環境。CubeMX機能を内蔵し、コンパイル・デバッグ・書き込みまで一貫して行えます。
  • STM32CubeProgrammer:書き込み専用ツール。JTAG、SWD、UART、USB DFUなど様々なインタフェースでフラッシュメモリの読み書きやベリファイが可能です。
  • STM32CubeMonitor:実行時の電力プロファイルや通信データを可視化するモニタリングツール群。
  • STM32Cube MCU Package:各STM32シリーズ向けのHALドライバ、ミドルウェア、サンプルコードを含むソフトウェアパッケージ。
  • STM32Cube Expansion:STまたはサードパーティが提供する拡張パッケージ。特定のアプリケーション(例:BLEスタック、TouchGFX)を追加します。

以下、具体的な実験を通じて基本的なペリフェラルの設定方法とコードを解説します。(ベースはSTM32F103RCT6、必要に応じてF407やL451も使用)

1. 最初のプロジェクト(GPIO出力)

解説の共通準備として、次の手順でプロジェクトを作成します。

  1. チップ選択(例:STM32F103RCT6)
  2. クロックソース設定(HSE:8MHz水晶、LSE:32.768kHz)
  3. クロックツリー構成(PLL、SYSCLK、AHB、APB1/APB2)
  4. GPIOピン設定
  5. Cortex‑M内核設定(デバッグIF:SW、NVIC優先度グループ)
  6. 初期化コード生成
  7. ユーザコード追加

ペリフェラル毎に上記のベースプロジェクトを複製・拡張します。

コード例(LED0を500ms間隔で点滅)

// main.c ユーザコード領域内
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    HAL_GPIO_TogglePin(LED0_GPIO_Port, LED0_Pin);
    HAL_Delay(500);
  }

補足:CubeMXが生成するコードは /* USER CODE BEGIN ... *//* USER CODE END ... */ の間にユーザが追加した部分のみ保持されます。それ以外の場所に書いたコードは、再生成時に上書き削除されるため注意が必要です。

2. ボタン入力とGPIO割り込み(External Interrupt)

ハードウェア構成

  • KEY0 – PE4:プルアップ入力、下降エッジで割り込み、LED0をトグル
  • KEY1 – PE3:プルアップ入力、下降エッジで割り込み、LED1をトグル
  • KEY_UP – PA0:プルダウン入力、立ち上がりエッジで割り込み、ブザー制御

CubeMX設定のポイント

  • 該当ピンのGPIOモードを GPIO_EXTIx に設定
  • NVICで該当ラインの割り込みを有効化(プリエンプト優先度を例えば1に設定)
  • コード生成後、HAL_GPIO_EXTI_Callback() をユーザコード領域に記述

割り込みコールバック(書き換え例)

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    HAL_Delay(10);  // 簡易デバウンス(割り込み内でのDelayは推奨しないが、説明用)

    if (GPIO_Pin == WK_UP_Pin) {
        if (HAL_GPIO_ReadPin(WK_UP_GPIO_Port, WK_UP_Pin) == GPIO_PIN_SET) {
            HAL_GPIO_TogglePin(BEEP_GPIO_Port, BEEP_Pin);
        }
    }
    else if (GPIO_Pin == KEY0_Pin) {
        if (HAL_GPIO_ReadPin(KEY0_GPIO_Port, KEY0_Pin) == GPIO_PIN_RESET) {
            HAL_GPIO_TogglePin(LED0_GPIO_Port, LED0_Pin);
        }
    }
    else if (GPIO_Pin == KEY1_Pin) {
        if (HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) == GPIO_PIN_RESET) {
            HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
        }
    }
}

3. UART通信(ポーリング・割り込み・printfリダイレクト)

STM32のUSART/UARTは非同期全二重シリアル通信を提供します。ボーレートは共通パラメータで、データビット・パリティ・ストップビットも設定可能。

ポーリング送受信

#define TX_BUF_SIZE 5
uint8_t sendbuf[TX_BUF_SIZE] = {0x41,0x42,0x43,0x44,0x45}; // "ABCDE"

HAL_UART_Transmit(&huart1, sendbuf, TX_BUF_SIZE, HAL_MAX_DELAY);

// 受信
uint8_t recvbuf[10];
HAL_UART_Receive(&huart1, recvbuf, 6, 1000);   // 1秒タイムアウト
HAL_UART_Transmit(&huart1, recvbuf, 6, HAL_MAX_DELAY);

printf関数のリダイレクト

usart.c に次の関数を追加します。

int __io_putchar(int ch)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

プロジェクト設定で「Use MicroLIB」を有効にし、文字コードをGBK(日本語環境)に設定すると日本語が正しく表示されます。

割り込み受信

uint8_t rxBuf[6];
HAL_UART_Receive_IT(&huart1, rxBuf, 6);

// コールバック
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        HAL_UART_Transmit(&huart1, rxBuf, 6, HAL_MAX_DELAY);
        HAL_UART_Receive_IT(&huart1, rxBuf, 6);  // 次回受信の再設定
    }
}

4. ウォッチドッグ(IWDG / WWDG)

システムの暴走検出とリセットに使われます。

独立ウォッチドッグ(IWDG)

LSI(40kHz)をクロック源とし、プリスケーラとリロード値でタイムアウトを設定。1秒周期に設定する例:

// CubeMX: IWDGを有効、プリスケーラ=16分周(IWDG_Prescaler_16)、リロード=2500
HAL_IWDG_Refresh(&hiwdg);  // 1秒以内に呼ばないとリセット

// リセット要因の判定
if (__HAL_RCC_GET_FLAG(RCC_FLAG_IWDGRST) != RESET) {
    printf("IWDG Reset!\r\n");
    __HAL_RCC_CLEAR_RESET_FLAGS();
}

ウィンドウウォッチドッグ(WWDG)

APB1クロック(36MHz)で動作。上位ウィンドウ(早期にリセットがかかる)と下位ウィンドウ(0x3F)の間でのみ餌やりが許可されます。

// CubeMX: WWDGプリスケーラ=8、ウィンドウ値=0x5F、カウンタ初期=0x7F
// 超えないようにループ内で餌やる
HAL_WWDG_Refresh(&hwwdg);

// または割り込みで早期警戒時に餌やり
void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef *hwwdg)
{
    HAL_WWDG_Refresh(hwwdg);
    printf("Fed in early wakeup\r\n");
}

5. タイマの基本(基本タイマ TIM6 の更新割り込み)

基本タイマはアップカウントのみ可能。PSC=7199, ARR=4999 で500ms周期の割り込みを実現(72MHzクロック時)。

HAL_TIM_Base_Start_IT(&htim6);

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6) {
        HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
    }
}

6. PWM出力(汎用タイマ TIM3_CH2)

PB5から1kHz、デューティ可変のPWMを出力します。システムクロック72MHz、PSC=71, ARR=999 → 出力周波数 1kHz。

HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);

while (1) {
    // デューティを250→499→0…と変化
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, current_duty);
    HAL_Delay(10);
}

7. 入力キャプチャ(高レベルパルス幅測定)

PA0に接続されたKEY_UPの押下時間を測定。TIM5_CH1、プリスケーラ71で1us分解能。

// キャプチャコールバックで立ち上がり/立ち下がりエッジを管理
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM5) {
        static uint8_t captureDone = 0;
        static uint16_t capturedVal = 0;
        if (!captureDone) {
            // 立ち上がりエッジ:カウンタリセット、次は立ち下がり
            __HAL_TIM_SET_COUNTER(htim, 0);
            __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING);
            captureDone = 1;
        } else {
            // 立ち下がりエッジ:値を読み出し、次の測定に備える
            capturedVal = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
            captureDone = 0;
            __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);
            // パルス幅 = capturedVal [us](オーバーフロー処理は省略)
        }
    }
}

8. ADC(単一チャネル変換)

ADC1、チャネル1(PA1)を使用。VREF=3.3V、12ビット分解能0.8mV。サンプリングタイム239.5サイクルで高精度。

// adc.c にユーティリティ関数を追加
uint32_t ADC_GetAverage(uint32_t ch, uint8_t times)
{
    uint32_t sum = 0;
    for (uint8_t i = 0; i < times; i++) {
        ADC_ChannelConfTypeDef sConfig = {0};
        sConfig.Channel = ch;
        sConfig.Rank = ADC_REGULAR_RANK_1;
        sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;
        HAL_ADC_ConfigChannel(&hadc1, &sConfig);
        HAL_ADC_Start(&hadc1);
        HAL_ADC_PollForConversion(&hadc1, 10);
        sum += HAL_ADC_GetValue(&hadc1);
        HAL_Delay(5);
    }
    return sum / times;
}

9. CAN通信(bxCAN)

STM32F103やL451の内蔵CANコントローラ。Max 1Mbps。

CubeMX設定

  • CAN1のTX/RXピンを選択
  • クロックツリーでAPB1を80MHzなどに設定
  • Bit Timingパラメータ:Prescaler=10, BS1=13, BS2=2 → 500kbps, サンプルポイント87.5%
  • Operating Mode = Normal
  • NVICでRX0割り込みを有効化

送信例(標準フレーム)

static void CAN_SendStd(uint16_t id, uint8_t *data, uint8_t len)
{
    CAN_TxHeaderTypeDef txHeader = {0};
    uint32_t mailbox;
    txHeader.StdId = id;
    txHeader.IDE = CAN_ID_STD;
    txHeader.RTR = CAN_RTR_DATA;
    txHeader.DLC = len;
    HAL_CAN_AddTxMessage(&hcan1, &txHeader, data, &mailbox);
}

受信コールバック(FIFO0)

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    CAN_RxHeaderTypeDef rxHeader;
    uint8_t rxData[8];
    HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData);
    // rxData を処理
}

10. CRC(ハードウェアCRC)

CubeMXでCRCを有効にし、データ整合性チェックに利用。

extern CRC_HandleTypeDef hcrc;
uint8_t testData[] = {0x01, 0x02, 0x03, 0x04};
uint32_t crcResult = HAL_CRC_Calculate(&hcrc, (uint32_t*)testData, sizeof(testData));

CubeMXの「Input Data Format」がBYTESになっていることを確認。

タグ: STM32 STM32CubeMX HAL GPIO UART

7月17日 00:29 投稿