ODrive ファームウェア UART ストリーム実装の解析

UART 周辺機器の初期化構成

ファームウェアの起動プロセスにおいて、UART 通信機能はボード初期化段階で設定されます。システム設定に基づき、複数の UART ポートの中から必要なものを選択し、ボーレートなどのパラメータを適用します。


// システム設定に基づき UART チャンネル A を有効化
if (sys_config.uart_ch_a_enabled) {
    uart_handle_ch1->Init.BaudRate = sys_config.baudrate_ch_a;
    HAL_UART_MspInit(uart_handle_ch1); // UART4 に相当
}

// システム設定に基づき UART チャンネル B を有効化
if (sys_config.uart_ch_b_enabled) {
    uart_handle_ch2->Init.BaudRate = sys_config.baudrate_ch_b;
    HAL_UART_MspInit(uart_handle_ch2); // UART2 に相当
}

デフォルト構成では UART チャンネル A のみが有効であり、ボーレートは 115200bps に設定されています。ヘッダーファイルの定義を確認すると、理論的には 3 つのポートまでサポート可能ですが、現時点では 2 つまでの同時運用が想定されています。


struct SystemConfig {
    bool uart_ch_a_enabled = true;
    bool uart_ch_b_enabled = false;
    bool uart_ch_c_enabled = false;
    uint32_t baudrate_ch_a = 115200;
    uint32_t baudrate_ch_b = 115200;
    uint32_t baudrate_ch_c = 115200;
};

RTOS タスクとイベントキューの生成

通信処理は専用の RTOS タスクとして実行されます。まず、UART 関連のイベントを通知するためのメッセージキューを生成します。


// UART 信号通知用のキュー定義
osMessageQDef(uart_signal_q, 4, uint32_t);
uart_signal_queue = osMessageCreate(osMessageQ(uart_signal_q), NULL);

メインの RTOS タスク内では、USB デバイスや ADC の初期化に続き、通信レイヤーのセットアップが行われます。


static void rtos_main_task(void*) {
    MX_USB_DEVICE_Init();
    start_general_purpose_adc();

    // 軸オブジェクト構築後に通信層を初期化
    setup_communication_layers();
}

通信初期化関数では、競合を防ぐために複数の UART が同時に有効にならないようチェックを行い、有効なポートに対してサーバー処理を開始します。


void setup_communication_layers(void) {
    // 同時有効化による競合の防止
    if (sys_config.uart_ch_a_enabled && sys_config.uart_ch_b_enabled) {
        system_state.is_misconfigured = true;
        return;
    }

    if (sys_config.uart_ch_a_enabled && uart_handle_ch1) {
        launch_uart_service(uart_handle_ch1);
    } else if (sys_config.uart_ch_b_enabled && uart_handle_ch2) {
        launch_uart_service(uart_handle_ch2);
    }
}

DMA による受信バッファ管理

UART サーバーの起動処理では、DMA を使用した循環バッファによる受信設定を行います。割り込みではなく、ポーリングにより DMA の転送状況を確認する方式を採用しています。


void launch_uart_service(UART_HandleTypeDef* huart) {
    active_uart_handle = huart;
    tx_stream_handle.uart_ptr = huart;

    // 循環バッファを用いた DMA 受信の開始
    HAL_UART_Receive_DMA(active_uart_handle, rx_dma_circle_buf, sizeof(rx_dma_circle_buf));
    last_dma_read_index = 0;

    // UART 処理タスクの生成
    osThreadDef(uart_listener_def, uart_listener_task, osPriorityNormal, 0, stack_size_uart / sizeof(StackType_t));
    uart_thread_handle = osThreadCreate(osThread(uart_listener_def), NULL);
}

受信バッファは固定サイズで確保され、DMA が常に書き込みを続ける構造になっています。


#define UART_TX_BUF_SIZE 64
#define UART_RX_BUF_SIZE 64

// DMA 循環バッファ(1ms 周期でポインタを追跡)
static uint8_t rx_dma_circle_buf[UART_RX_BUF_SIZE];

現在の実装では、UART A および UART B のどちらを選択しても同一のメモリ領域を参照するため、同時通信はバッファ競合を引き起こす可能性があります。

UART タスク内のデータ処理ロジック

UART リスナータスクでは、プロトコル種別に応じて処理ハンドラを起動します。


static void uart_listener_task(void * ctx) {
    (void) ctx;

    if (sys_config.uart_proto_type == PROTOCOL_FIBRE) {
        fibre_interface.start({});
    } else if (sys_config.uart_proto_type == PROTOCOL_ASCII) {
        ascii_handler.initialize();
    }

    // イベント待ちループ
    while (true) {
        osEvent evt = osMessageGet(uart_signal_queue, osWaitForever);
        if (evt.status != osEventMessage) {
            continue;
        }
        // イベント処理へ遷移
    }
}

イベントキューから通知を受け取ると、DMA の転送状態を確認し、新規データの有無を判定します。


case 1: {
    // 制御ループ周期(8kHz)でトリガー
    // 受信状態がビジーでない場合、DMA 転送をリセット
    if (active_uart_handle->RxState != HAL_UART_STATE_BUSY_RX) {
        HAL_UART_AbortReceive(active_uart_handle);
        HAL_UART_Receive_DMA(active_uart_handle, rx_dma_circle_buf, sizeof(rx_dma_circle_buf));
        last_dma_read_index = 0;
    }

    // DMA の現在の書き込みポインタを取得
    uint32_t current_dma_idx = UART_RX_BUF_SIZE - active_uart_handle->hdmarx->Instance->NDTR;
    if (current_dma_idx > UART_RX_BUF_SIZE) {
        continue; // 防御プログラミング
    }

    // バッファの折り返しを考慮してデータ処理
    if (current_dma_idx < last_dma_read_index) {
        // 循環バッファの末尾から先頭への折り返し分
        uart_rx_stream.process_data(rx_dma_circle_buf + last_dma_read_index,
                UART_RX_BUF_SIZE - last_dma_read_index);
        last_dma_read_index = 0;
    }
    if (current_dma_idx > last_dma_read_index) {
        // 連続したデータ分
        uart_rx_stream.process_data(rx_dma_circle_buf + last_dma_read_index,
                current_dma_idx - last_dma_read_index);
        last_dma_read_index = current_dma_idx;
    }
} break;

データ受信完了後は、登録されたコールバック関数を通じてプロトコル層へ渡されます。


void AsciiHandler::initialize() {
    TransferHandle handle;
    rx_channel->start_read(rx_buffer, &handle, MEMBER_CB(this, on_read_complete));
}

ASCII プロトコルでは、改行コードや区切り文字を検出しながらバッファ内のデータを解析します。


void AsciiHandler::on_read_complete(ReadResult res) {
    if (res.status != kStreamOk) {
        return;
    }

    for (;;) {
        // 行末文字(\r, \n, !)を検索
        uint8_t* line_end = std::find_if(rx_buffer, res.end, [](uint8_t c) {
            return c == '\r' || c == '\n' || c == '!';
        });

        if (line_end >= res.end) {
            break;
        }

        if (reading_active) {
            parse_and_execute({rx_buffer, line_end});
        } else {
            // 新規行開始まで待機
            reading_active = true;
        }
        
        // 処理済みデータを破棄し、残りをバッファ先頭へ移動
        size_t remaining_bytes = res.end - line_end - 1;
        memmove(rx_buffer, line_end + 1, remaining_bytes);
        res.end = rx_buffer + remaining_bytes;
    }

    // バッファ溢れ対策
    if (res.end >= rx_buffer + sizeof(rx_buffer)) {
        res.end = rx_buffer;
        reading_active = false;
    }

    TransferHandle handle;
    rx_channel->start_read({res.end, rx_buffer + sizeof(rx_buffer)}, &handle, MEMBER_CB(this, on_read_complete));
}

最終的に、各行のデータはコマンド処理関数へと引き渡され、実行されます。


void AsciiHandler::parse_and_execute(cbufptr_t cmd_buffer) {
    // コマンド解析および実行処理
}

タグ: ODrive STM32 UART DMA FreeRTOS

8月3日 00:14 投稿