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) {
// コマンド解析および実行処理
}