STM32用Xmodem-1kシリアル通信ブートローダの実装

本稿では、STM32マイクロコントローラ向けに、Xmodem-1kプロトコルを介したシリアル通信によるファームウェアアップデート機能を実装するブートローダについて解説します。このブートローダは、将来的なリモートアップデートを見据え、外部SPIフラッシュメモリ(W25Qシリーズ)インターフェースもサポートする設計となっています。

機能概要

効率的なファームウェアアップデートを実現するため、このブートローダは以下の主要コンポーネントで構成されています。

  • フラッシュメモリ管理: 内部フラッシュをブートローダ領域とアプリケーション領域に分割し、各領域へのデータ読み書き操作および状態管理を行います。
  • シリアル通信割り込み処理: 受信データに基づいて、コマンドモードからファイルダウンロードモードへの状態遷移を管理します。
  • Xmodem-1kプロトコル実装: アップデートパッケージの受信、データ整合性の検証、およびエラー処理を行います。
  • アプリケーション実行制御: アップデート完了後、新しいアプリケーションプログラムへの実行制御を安全に移行します。

システム全体の動作フロー

ファームウェアアップデートの全体的なプロセスは以下の通りです。

  1. 初期化フェーズ: システム起動時に、割り込み優先度グループの設定とシリアルポート(USART1)の初期化を行います。
  2. IAP状態のリセット: インアプリケーションプログラミング(IAP)に関連する内部状態(受信モード、データバッファカウンタ、フラッシュ消去フラグなど)を初期化します。
  3. 外部フラッシュ(W25Q)の初期化: W25Qメモリの初期化を試行します。成功した場合は、ファームウェアアップデートの有無を確認するロジックに進みます。失敗した場合は、既存のアプリケーションへ直接実行をジャンプさせます。
  4. アップデートモードへの移行: ホストPCからシリアルポートを介して特定のコマンド(例:「download」)を受信すると、ブートローダはXmodem-1kダウンロードモードへ移行します。
  5. ファームウェアの受信と検証: Xmodem-1kプロトコルに従い、ファームウェアパッケージを受信し、CRCチェックなどの手順でデータが破損していないか検証します。
  6. アプリケーションの実行: 全てのパッケージが正常に受信され、内部フラッシュに書き込まれた後、ブートローダは新しいアプリケーションプログラムの開始アドレスへジャンプし、実行を開始します。

// main.c の主要な処理フロー
int main(void)
{
    uint8_t w25q_init_success = 0;
    // 割り込み優先度グループを設定
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    // USART1を115200bpsで初期化
    Usart1_Init(115200);
 
    // IAP関連のグローバル状態を初期化(受信モード、バッファ、消去フラグのリセット)
    Bootloader_State_Reset();
    // W25QXX SPIフラッシュの初期化を試行
    w25q_init_success = W25QXX_Init();

    if (w25q_init_success == 1)
    {
        // W25QXX初期化成功時、ファームウェアアップデートの有無を確認
        // ローカルまたはリモートアップデートのデータチェックロジックへ
        Check_Firmware_Update_Status();
    }
    else
    {
        // W25QXX初期化失敗時、直接アプリケーションへジャンプ
        Jump_To_Application(APP_START_ADDRESS);
    }
    
    // 通常はここには到達しないが、フォールバックとしてアプリケーションへジャンプ
    Jump_To_Application(APP_START_ADDRESS);

    // メインループ(通常はアプリケーションへジャンプするため、このループには入らない)
    for (;;)
    {
    }
}

フラッシュメモリのパーティショニングと管理

フラッシュ領域の分割

使用するSTM32C8T6マイクロコントローラの内部フラッシュメモリは、総容量64KB(1KB/ページ、合計64ページ)です。このうち、最初の16KB(16ページ)をブートローダ自身が使用する領域とし、残りの48KBをアプリケーションプログラムの格納領域として割り当てます。


// STM32C8T6 内部フラッシュメモリのパラメータ定義
#define INTERNAL_FLASH_BASE_ADDR        ((uint32_t)0x08000000) // フラッシュの開始アドレス
#define FLASH_PAGE_SIZE_BYTES           (1024U)                // 1ページあたりのバイト数
#define TOTAL_FLASH_PAGE_COUNT          (64U)                  // 総ページ数
#define FLASH_END_ADDR                  (INTERNAL_FLASH_BASE_ADDR + TOTAL_FLASH_PAGE_COUNT * FLASH_PAGE_SIZE_BYTES - 1) // フラッシュの最終アドレス

// ブートローダ領域の定義 (最初の16KB、すなわち16ページ)
#define BOOTLOADER_REGION_SIZE_KB       (16U)
#define APP_START_ADDRESS               (INTERNAL_FLASH_BASE_ADDR + BOOTLOADER_REGION_SIZE_KB * FLASH_PAGE_SIZE_BYTES) // アプリケーションの開始アドレス

// 指定されたページ番号からそのページの開始アドレスを計算するマクロ
#define GET_FLASH_PAGE_ADDR(page_num)   (INTERNAL_FLASH_BASE_ADDR + (page_num) * FLASH_PAGE_SIZE_BYTES)

フラッシュ操作機能

内部フラッシュの操作と状態管理を行う主要な関数群は以下の通りです。

  • `Flash_Erase_Page_Tracker`: フラッシュページの消去状態を追跡・管理し、不要な重複消去を防ぐための内部メカニズムを提供します。
  • `Get_Page_Number_From_Address`: 指定されたメモリアドレスが属するフラッシュのページ番号を計算して返します。
  • `Internal_Flash_Write_Words`: 指定されたアドレスへワード(32ビット)単位でデータを書き込みます。書き込み対象のフラッシュページは、必要に応じて事前に消去されます。フラッシュは書き込み前にアンロックされ、書き込み後にロックされます。
  • `Internal_Flash_Read_Word`: 指定されたアドレスから1ワード(32ビット)のデータを読み出します。
  • `Internal_Flash_Read_Bytes`: 指定された開始アドレスから、指定された長さのバイトデータを読み出し、与えられたバッファに格納します。
  • `Write_App_Firmware_To_Flash`: 受信したアプリケーションファームウェアのバイナリデータを、アプリケーション領域の内部フラッシュに書き込みます。データはワード単位に処理され、内部バッファが満たされるたびに`Internal_Flash_Write_Words`関数を呼び出して書き込みます。

// フラッシュ操作関数のプロトタイプ宣言
uint32_t Get_Page_Number_From_Address(uint32_t address);
void     Internal_Flash_Write_Words(uint32_t write_addr, uint32_t *data_buffer, uint32_t word_count);
uint32_t Internal_Flash_Read_Word(uint32_t address);
void     Internal_Flash_Read_Bytes(uint32_t address, uint8_t *buffer, uint32_t length);
void     Write_App_Firmware_To_Flash(uint32_t app_address, uint8_t *firmware_data, uint32_t firmware_size);
int      Flash_Erase_Page_Tracker(uint8_t action_flag, uint32_t address); // action_flag: 1=トラック/設定, 0=リセット

シリアルデータ受信とモード切り替え

ブートローダは、シリアルポートを介して特定のコマンド文字列「download」を受信すると、ファームウェアダウンロードモードに自動的に切り替わります。これにより、ホストデバイスからのXmodem-1kプロトコルに基づくデータ転送が可能になります。


// USART1 割り込みハンドラ
void USART1_IRQHandler(void)
{
    uint8_t received_byte;
    uint32_t current_buf_idx = boot_iap_control.receive_buffer_idx;
    
    // 受信データレジスタが空でないか確認
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        received_byte = USART_ReceiveData(USART1); // 受信データを読み出す
        
        // 受信バッファにデータを格納し、インデックスを進める
        boot_iap_control.receive_buffer[current_buf_idx] = received_byte;
        current_buf_idx++;

        // バッファオーバーフローを防止するため、バッファサイズを超えたら先頭に戻る
        if (current_buf_idx >= (sizeof(boot_iap_control.receive_buffer) - 1)) // 1バイトは安全マージン
        {
            current_buf_idx = 0;
        }

        // ファイルダウンロードモードの場合
        if (boot_iap_control.download_mode_active == 1)
        {
            // Xmodem-1kのSTXバイト以外が最初に受信されたらバッファをリセット
            if (boot_iap_control.receive_buffer[0] != XMDM_STX_BYTE)
            {
                current_buf_idx = 0;
            }
        }
        // コマンドモードの場合
        else // if (boot_iap_control.download_mode_active == 0)
        {
            // "download\r\n" コマンド文字列を検出
            if (current_buf_idx >= 10) // "download\r\n" は10バイト
            {
                if ((boot_iap_control.receive_buffer[current_buf_idx - 2] == '\r') && 
                    (boot_iap_control.receive_buffer[current_buf_idx - 1] == '\n'))
                {
                    // 受信したコマンドを一時バッファにコピーし、nullターミネータを追加して比較
                    char cmd_check_buffer[11]; // "download\r\n" + null terminator
                    for (int i = 0; i < 10; i++)
                    {
                        cmd_check_buffer[i] = boot_iap_control.receive_buffer[i];
                    }
                    cmd_check_buffer[10] = '\0'; // nullターミネータ

                    if (strcmp(cmd_check_buffer, "download\r\n") == 0)
                    {
                        boot_iap_control.download_mode_active = 1; // ダウンロードモードへ移行
                    }
                }
                current_buf_idx = 0; // コマンド処理後、バッファをリセット
            }
        }
        boot_iap_control.receive_buffer_idx = current_buf_idx;
    }
}

Xmodem-1kプロトコルによるファームウェア更新

ファームウェアの更新は、Xmodem-1kプロトコル仕様に厳密に従って実行されます。以下に、プロトコル関連の定義とIAP処理の核となる関数群を示します。


// Xmodem-1kプロトコル関連の定義
#define XMDM_RETRIES                  15        // 転送開始時のリトライ回数
#define XMDM_SOH_BYTE                 0x01      // Xmodemデータヘッダ (128バイトデータ)
#define XMDM_STX_BYTE                 0x02      // 1K-Xmodemデータヘッダ (1024バイトデータ)
#define XMDM_EOT_BYTE                 0x04      // 送信終了 (End Of Transmission)
#define XMDM_ACK_BYTE                 0x06      // 肯定応答 (Acknowledge)
#define XMDM_NAK_BYTE                 0x15      // 否定応答 (Negative Acknowledge)
#define XMDM_CAN_BYTE                 0x18      // 転送キャンセル (Cancel)
#define XMDM_EOF_BYTE                 0x1A      // データパディングバイト
#define XMDM_PACKET_OK                0         // パケット処理成功
#define XMDM_PACKET_ERROR             -1        // パケット処理エラー
#define MAX_COMM_RETRIES              25        // 通信リトライ最大回数

#define W25Q_PAGE_SIZE_BYTES          4096      // W25Qメモリの最小消去単位
#define W25Q_STORAGE_START_ADDR       0x000000  // W25Qメモリのストレージ開始アドレス

// IAP制御用グローバル構造体
struct bootloader_iap_context
{
    unsigned int new_firmware_flag;        // 新しいファームウェアの有無を示すフラグ (例: 0x12345678で存在を示す)
    unsigned int total_file_size;          // 受信するファームウェアの総バイト数
      
    uint8_t download_mode_active;          // 現在の受信モード: 0=コマンドモード, 1=ファイルダウンロードモード
    
    char command_buffer[24];               // シリアルコマンド検出用の一時バッファ
    
    char receive_buffer[2048];             // シリアル受信データおよびXmodemデータの一時バッファ
    unsigned int receive_buffer_idx;       // receive_buffer の現在の書き込みインデックス
    
    unsigned int current_packet_id;        // Xmodemプロトコルにおける現在のパケット番号
      
    char temp_verify_buffer[2048];         // フラッシュ読み出し検証やリモートコピー用の一時バッファ
};

extern struct bootloader_iap_context boot_iap_control;

// 関数プロトタイプ
void Jump_To_Application(uint32_t app_entry_address); // 指定されたアドレスからアプリケーションを実行
int  Check_Firmware_Update_Status(void);             // 起動時にアップデートの必要性を確認し、必要に応じてアプリケーションへジャンプ
void Bootloader_State_Reset(void);                   // IAP関連の状態を初期化

`Bootloader_State_Reset`

IAPシステムの状態を初期化します。具体的には、受信モードを初期値にリセットし、データバッファカウンタをクリアし、内部フラッシュの消去フラグを初期化します。

`Check_Firmware_Update_Status`

電源投入後、ブートローダは一定期間、ホストからのアップグレード開始コマンド(例:「download」)をシリアルポートで待ち受けます。この期間内にコマンドが受信されなかった場合、ブートローダは既存のアプリケーションへ直接ジャンプします。コマンドが受信された場合、Xmodem-1kプロトコルによるダウンロードプロセスを開始し、その結果に基づいて最終的なアプリケーションへのジャンプを決定します。

`Initiate_File_Download`

この関数は、Xmodem-1kプロトコルに従ってファームウェアファイルの受信プロセス全体を管理します。 プロトコル仕様に基づき、ホストPCに対してアップグレードパッケージの送信を促すために、定期的に「C」文字を送信します。最初の`XMDM_STX_BYTE`(Xmodem-1kデータヘッダ)を受信するまで、この試行を`XMDM_RETRIES`回まで繰り返します。XMDM_STXを受信し、データ転送が開始されると、パケットの受信と検証は`Process_Xmodem_Packet`関数に委ねられます。


// ファームウェアファイルのダウンロードプロセスを開始する関数
int Initiate_File_Download(void)
{
    uint8_t current_retry_count = 0;
    uint8_t first_byte_received;
    unsigned char start_transfer_char[1] = {'C'}; // Xmodem転送開始を促す文字

    boot_iap_control.current_packet_id = 1; // 最初のパケットIDを1に設定

    // Xmodemプロトコルに従い、ホストに'C'を送信して転送開始を要求
    // XMDM_RETRIES回まで、一定間隔で試行
    for (current_retry_count = 0; current_retry_count < XMDM_RETRIES; current_retry_count++)
    {
        Usart1_SendArray(start_transfer_char, 1); // シリアルポート経由で'C'を送信
        Delay_Ms(200); // 短い遅延

        // 受信バッファの最初のバイトを確認
        first_byte_received = boot_iap_control.receive_buffer[0];
        if (first_byte_received == XMDM_STX_BYTE)
        {
            break; // STX (Start of Text) バイトを受信したら、準備完了としてループを抜ける
        }
        else
        {
            Delay_Ms(1000); // 1秒間待機して再試行
        }
    }

    // メインの受信ループ: データパケットまたは終了シグナルを待つ
    while (1)
    {
        first_byte_received = boot_iap_control.receive_buffer[0];
        if (XMDM_STX_BYTE == first_byte_received)
        {
            Process_Xmodem_Packet(); // Xmodemデータパケットの処理
        }
        else if (XMDM_EOT_BYTE == first_byte_received)
        {
            // EOT (End of Transmission) バイトを受信: 転送が正常に終了
            // 必要に応じて最終的な処理を実行(例: ファームウェア全体のCRCチェックなど)
            Jump_To_Application(APP_START_ADDRESS); // 新しいアプリケーションへジャンプ
            break; // ループを終了
        }
        else if (XMDM_CAN_BYTE == first_byte_received)
        {
            // CAN (Cancel) バイトを受信: ホストによって転送がキャンセルされた
            // エラー処理または再試行ロジックをここに実装可能
            break; // ループを終了
        }
        Delay_Ms(200); // ポーリング間隔
    }
    return XMDM_PACKET_OK; // 正常終了を示す
}

`Process_Xmodem_Packet`

この関数は、Xmodem-1kデータパケットの受信、検証、および内部フラッシュへの書き込みを実行します。 受信データが`XMDM_STX_BYTE`で始まるパケットである場合、パケット長、ヘッダ、受信したCRC値、現在のパケットID、およびパケットシーケンス番号(IDの補数)を検証します。 検証に成功した場合、受信バッファをクリアし、受信したデータペイロードを`Write_App_Firmware_To_Flash`関数を使用して内部フラッシュの適切な位置に書き込みます。書き込み後、フラッシュに書き込まれたデータを再び読み出し、CRCを計算して、オリジナルのCRCと比較することで書き込みの整合性を二重に確認します。 CRCチェックが成功した場合、ホストに`XMDM_ACK_BYTE`を送信し、次のパケットIDとフラッシュへの書き込みオフセットを更新します。失敗した場合は、全ての中断を無効にし、システムをリセットします。 パケット検証が失敗した場合、`XMDM_NAK_BYTE`をホストに送信し、現在のパケットの再送を要求します。


// Xmodemデータパケットを処理する関数
void Process_Xmodem_Packet(void)
{
    #define XMDM_DATA_SIZE_1K           1024  // 1K-Xmodemのデータペイロードサイズ
    #define XMDM_HEADER_LEN             3     // STX + パケットID + ID補数 の長さ
    
    static uint32_t current_flash_offset = 0; // アプリケーション領域内での現在の書き込みオフセット
    
    uint16_t received_packet_crc = 0;
    int      is_packet_valid     = 0;
    uint16_t written_data_crc    = 0;
    uint16_t readback_data_crc   = 0;

    unsigned char response_char[1] = {0};
    unsigned char *packet_raw_data = (unsigned char *)boot_iap_control.receive_buffer;

    // 受信パケットデータ部分(ヘッダの次から)のCRC16を計算
    received_packet_crc = xm_crc16_ccitt((unsigned char *)(packet_raw_data + XMDM_HEADER_LEN), XMDM_DATA_SIZE_1K); // Xmodem-1kパケット形式を参照
    
    // データ整合性の包括的な検証
    is_packet_valid = 1;
    is_packet_valid = is_packet_valid && (boot_iap_control.receive_buffer_idx == (XMDM_HEADER_LEN + XMDM_DATA_SIZE_1K + 2)); // 2 for CRC bytes
    is_packet_valid = is_packet_valid && (packet_raw_data[0] == XMDM_STX_BYTE); // STXバイトチェック
    is_packet_valid = is_packet_valid && (((received_packet_crc >> 8) == packet_raw_data[XMDM_HEADER_LEN + XMDM_DATA_SIZE_1K]) && // 受信CRC上位バイト
                                          ((uint8_t)(received_packet_crc) == packet_raw_data[XMDM_HEADER_LEN + XMDM_DATA_SIZE_1K + 1])); // 受信CRC下位バイト
    is_packet_valid = is_packet_valid && (((unsigned char)boot_iap_control.current_packet_id) == packet_raw_data[1]); // パケットIDチェック
    is_packet_valid = is_packet_valid && (((unsigned char)packet_raw_data[1] == (unsigned char)(~packet_raw_data[2]))); // パケットID補数チェック

    if (1 == is_packet_valid)
    {
        boot_iap_control.receive_buffer_idx = 0; // 受信バッファをクリアして次のパケットに備える
        
        // アプリケーションバイナリをフラッシュの適切なオフセットに書き込む
        Write_App_Firmware_To_Flash((APP_START_ADDRESS + current_flash_offset), 
                                    (unsigned char *)(packet_raw_data + XMDM_HEADER_LEN), 
                                    XMDM_DATA_SIZE_1K); 
        
        // フラッシュに書き込んだデータのCRCを計算(書き込み検証用)
        written_data_crc = xm_crc16_ccitt((unsigned char *)(packet_raw_data + XMDM_HEADER_LEN), XMDM_DATA_SIZE_1K);

        // フラッシュから書き込んだデータを読み出し、そのCRCを再計算して検証
        Internal_Flash_Read_Bytes((APP_START_ADDRESS + current_flash_offset), 
                                  (unsigned char *)boot_iap_control.temp_verify_buffer, 
                                  XMDM_DATA_SIZE_1K); 
        readback_data_crc = xm_crc16_ccitt((unsigned char *)boot_iap_control.temp_verify_buffer, XMDM_DATA_SIZE_1K);

        if (written_data_crc != readback_data_crc)
        {
            // 書き込み検証失敗: エラーメッセージを出力し、システムをリセット
            boot_iap_control.receive_buffer_idx = 0;
            printf("Error: Flash write verification failed. Restarting device...\r\n");
            __set_FAULTMASK(1); // 全ての中断を無効化
            NVIC_SystemReset(); // システムをリセット
        }
        else
        {
            // 書き込み検証成功: ACKを送信し、次のパケットへ進む
            boot_iap_control.receive_buffer_idx = 0;
            response_char[0]                     = XMDM_ACK_BYTE; // ACKを送信
            Usart1_SendArray(response_char, 1);
            boot_iap_control.current_packet_id++; // 次のパケットIDに更新
            current_flash_offset += XMDM_DATA_SIZE_1K; // 次の書き込みオフセットを更新
        }
    }
    else // パケット検証失敗
    {
        boot_iap_control.receive_buffer_idx = 0;
        response_char[0]                     = XMDM_NAK_BYTE; // NAKを送信して再送を要求
        Usart1_SendArray(response_char, 1);
    }
    // 受信バッファインデックスをリセット(既に成功/失敗処理でクリア済みだが念のため)
    boot_iap_control.receive_buffer_idx = 0;
}

新しいアプリケーションへの実行制御の移行

`Jump_To_Application`

ファームウェアのアップグレードが内部フラッシュに正常に書き込まれた後、ブートローダは新しいアプリケーションへの実行制御を安全かつ確実に行う必要があります。この移行プロセスは以下の主要なステップで構成されます。

  1. アプリケーションエントリポイント情報の読み出し: 指定されたアプリケーションの開始アドレスから最初の8バイトを読み出します。これらのバイトには、新しいアプリケーションの初期メインスタックポインタ(MSP)値と、リセットハンドラのアドレス(アプリケーションの実際のコード実行開始点)が含まれています。
  2. ベクタテーブルオフセットの設定: マイクロコントローラの中断ベクタテーブルは、新しいアプリケーションの開始アドレスに合わせて再設定されます。これにより、アプリケーションが使用する割り込みが正しく処理されるよう、割り込みベクタのオフセットが調整されます。
  3. メインスタックポインタ(MSP)の設定: コアのMSPレジスタを、新しいアプリケーションが使用するスタック領域の初期値に設定します。これにより、アプリケーションは自身のスタック環境で正しく動作を開始できます。
  4. アプリケーションの実行開始: 新しいアプリケーションのリセットハンドラのアドレスへ直接ジャンプし、新しいファームウェアの実行を開始します。

// 指定されたアドレスからアプリケーションを実行する関数
void Jump_To_Application(uint32_t app_entry_address)  
{
    uint8_t  app_startup_info[8] = {0};
    uint32_t app_initial_msp;
    uint32_t app_reset_handler_addr;

    #define INTERNAL_ROM_BASE_ADDR ((uint32_t)0x08000000) // STM32内部フラッシュのベースアドレス
         
    uint32_t vector_table_base_addr;
    uint32_t vector_table_offset_val;

    // 1. 新しいファームウェアのスタックポインタ(MSP)とリセットハンドラアドレスを読み出す
    Internal_Flash_Read_Bytes(app_entry_address, app_startup_info, 8); 
    app_initial_msp      = (app_startup_info[0]       ) | 
                           (app_startup_info[1] <<  8) |  
                           (app_startup_info[2] << 16) | 
                           (app_startup_info[3] << 24);
    app_reset_handler_addr = (app_startup_info[4]       ) | 
                             (app_startup_info[5] <<  8) |  
                             (app_startup_info[6] << 16) | 
                             (app_startup_info[7] << 24);

    // 2. 中断ベクタテーブルのオフセットを設定
    // アプリケーションがフラッシュまたはRAMのどこから起動するかを判断
    vector_table_base_addr   = (app_entry_address >= INTERNAL_ROM_BASE_ADDR) ? (NVIC_VectTab_FLASH) : (NVIC_VectTab_RAM);
    vector_table_offset_val =  app_entry_address - vector_table_base_addr;

    NVIC_SetVectorTable(vector_table_base_addr, vector_table_offset_val);            
         
    // 3. メインスタックポインタ(MSP)をアプリケーションの初期値に設定
    __set_MSP(app_initial_msp);                              
         
    // 4. 新しいアプリケーションのリセットハンドラへジャンプし、実行を開始
    ((void(*)())(app_reset_handler_addr))();                      
}

タグ: STM32 BootLoader Xmodem-1k シリアル通信 ファームウェアアップデート

8月5日 02:43 投稿