モータシステムと配線構成の概要
RoboMaster向けの駆動系は、減速機内蔵型とダイレクトドライブ型に大別され、それぞれに対応する電子速度制御装置(ESC)がセットで提供されています。代表格としてはM3508モータとC620 ESC、統合型ESCを備えるGM6020、そしてM2006とC610 ESCの組み合わせが挙げられます。これらの仕様書や駆動サンプルは公式サイトで公開されています。
直流ブラシレスモータは機械的な整流子を排除し、電子的なコミュテーションにより回転を制御します。適切な回転数と向きを実現するには、ESCが出力する電流の大きさおよび極性を高频で切り替える必要があります。RMシリーズのモータ内部にはロータの位置を検出するホール素子が埋め込まれており、クローズドループ制御に必要な速度および角度フィードバック信号を供給します。
配線においては、モータの3相ケーブルをESCに接続し、ESCの電源端子(XT30)をバッテリーに、信号端子(2ピンCAN)をマイコンボードに接続するのが標準的なフローです。公式のセンターボードを活用すれば、複数のESCへの給電と信号分配を一元化でき、4輪走行プラットフォームの構築が容易になります。実機におけるモータの役割分担は明確です。例えば標準的な歩兵機体では、走行用車輪にM3508、アングル調整用のジンバル軸にGM6020、弾薬送出機構にM2006、そして射撃用のフライホイールにはPWM制御の専用モータ(または減速機を外したM3508)が配置されます。トルクと回転数の要求仕様に基づいて機種選定を行うのが通例です。
CANバスの物理層とタイミング設定
RMシリーズの制御系はほぼ例外なくCANバスを介して構築されています。UARTやSPIと比較するとCANは仕様複雑ですが、リアルタイム性と耐ノイズ性で優れています。物理層の特徴として、CANはCAN_HとCAN_Lの2線で構成される差分伝送方式を採用しています。論理「0」は顕性(Dominant)、論理「1」は隠性(Recessive)と定義され、両者が衝突した場合は顕性が優先されるアビトラクション機構が自動的に動作します。これにより、マルチマスタ環境でのデータ衝突を安全に回避できます。
通信速度(ボーレート)は通常1Mbpsに設定されます。タイミングパラメータの決定には、同期セグメント、位相セグメント1(BS1)、位相セグメント2(BS2)、および同期ジャンプ幅(SJW)の調整が必要です。STM32のCAN制御レジスタを用いる場合、以下のようにペリフェラルクロックから分周比とセグメント長を計算して設定します。
CAN_HandleTypeDef hcan;
hcan.Instance = CAN1;
hcan.Init.Prescaler = 7;
hcan.Init.Mode = CAN_MODE_NORMAL;
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan.Init.TimeSeg1 = CAN_BS1_2TQ;
hcan.Init.TimeSeg2 = CAN_BS2_3TQ;
フィルタリング機構とフレーム構成
CANバスはブロードキャスト型通信であるため、すべてのメッセージをCPUで処理すると負荷が増大します。これを回避するため、ハードウェアフィルタを用いて必要なIDのみを抽出します。フィルタモードにはマスクモードとリストモードがありますが、実戦ではマスクモードが多用されます。マスクモードの概念は、特定ビット領域を無視(マスク)して残りのビットで識別IDを判定する仕組みです。例えば、下位Nビットを0でマスクし、上位ビットが特定の値に一致すれば受信許可を出すように設定できます。RMのESCではフィードバックIDが0x201〜0x204などの範囲に割り当てられているため、全ビットを許可するマスク(0x00000000)とID(0x00000000)を組み合わせることで、接続された全モータのデータをフィルタリング処理なしで受信する設定が一般的です。
CAN_FilterTypeDef filter_cfg;
filter_cfg.FilterActivation = ENABLE;
filter_cfg.FilterMode = CAN_FILTERMODE_IDMASK;
filter_cfg.FilterScale = CAN_FILTERSCALE_32BIT;
filter_cfg.FilterIdHigh = 0x0000;
filter_cfg.FilterIdLow = 0x0000;
filter_cfg.FilterMaskIdHigh = 0x0000;
filter_cfg.FilterMaskIdLow = 0x0000;
filter_cfg.FilterBank = 0;
filter_cfg.FilterFIFOAssignment = CAN_RX_FIFO0;
HAL_CAN_ConfigFilter(&hcan, &filter_cfg);
CAN標準フレームは、仲裁フィールド(ID)、コントロールフィールド(DLC:データ長)、データフィールド(最大8バイト)、CRCおよびACKフィールドで構成されます。ESCとの通信では、DLCを8に固定し、定義されたバイト順に制御コマンドまたはステータス値をパッキングおよびアンパッキングします。
受信データ復号アルゴリズム
CAN受信割り込みからコールバックされる関数内で、生データを解釈し制御ループに渡す処理を実装します。以下の例では、ステートメント構造を整理し、変数名を物理量に近づけて再構築しています。12ビットエンコーダのオーバーフロー処理は、差分値の符号と大きさに基づいて回転カウントを補正するロジックに置き換えています。
void decode_esc_feedback_packet(const uint8_t *raw_frame, motor_state_t *target_unit)
{
target_unit->frame_count++;
if (target_unit->calibration_mode) {
run_zero_offset_detection(target_unit, raw_frame);
return;
}
uint16_t previous_pos = target_unit->current_pos;
// 12-bit position extraction (Bytes 0-1)
target_unit->current_pos = (uint16_t)((raw_frame[0] << 8) | raw_frame[1]);
int16_t delta = (int16_t)(target_unit->current_pos - previous_pos);
// Handle 4096 wrap-around for 12-bit encoder
if (delta > 4096) {
target_unit->revolution_counter--;
delta -= 8192;
} else if (delta < -4096) {
target_unit->revolution_counter++;
delta += 8192;
}
target_unit->speed_raw = delta;
target_unit->absolute_pos = (int32_t)(target_unit->revolution_counter * 8192L) + delta;
target_unit->angle_degree = target_unit->absolute_pos / 11.377;
// RPM extraction (Bytes 2-3)
target_unit->rotational_rpm = (int16_t)((raw_frame[2] << 8) | raw_frame[3]);
// Current extraction (Bytes 4-5)
target_unit->output_current = (int16_t)((raw_frame[4] << 8) | raw_frame[5]);
}
制御指令の送信フロー
1ms周期のタイマ割り込みなどでトリガーされる送信処理では、キューイング機構を用いてデータ競合を避けつつ、バス負荷を分散させます。以下の実装では、送信バッファの管理ロジックをリングバッファ方式に変更し、HALライブラリの呼び出し順序を最適化しています。複数のモータ指令を1つのCANフレームに圧縮する処理を独立関数に分離し、可読性と拡張性を向上させています。
typedef struct {
uint8_t bytes[8];
uint8_t len;
} can_payload_t;
void dispatch_control_commands(CAN_HandleTypeDef *hcan, can_tx_queue_t *tx_ring, uint16_t cmd_id)
{
if (can_tx_fifo_is_empty(tx_ring)) return;
CAN_TxHeaderTypeDef header;
header.StdId = cmd_id;
header.IDE = CAN_ID_STD;
header.RTR = CAN_RTR_DATA;
header.DLC = 8;
uint32_t transmit_mailbox;
while (!can_tx_fifo_is_empty(tx_ring) &&
HAL_CAN_GetTxMailboxesFreeLevel(hcan) > 0) {
can_payload_t packet;
can_tx_fifo_pop(tx_ring, &packet);
header.DLC = packet.len;
if (HAL_CAN_AddTxMessage(hcan, &header, packet.bytes, &transmit_mailbox) == HAL_OK) {
target_unit->send_status = TRANSMIT_PENDING;
}
}
}
void pack_motor_command(motor_state_t *units[], uint8_t count)
{
can_payload_t outbound_frame = {0};
uint16_t target_id = (count <= 4) ? 0x200 : 0x1FF;
for (uint8_t i = 0; i < count; i++) {
uint8_t slot_idx = (i * 2);
int16_t current_val = units[i]->requested_current;
outbound_frame.bytes[slot_idx] = (uint8_t)(current_val >> 8);
outbound_frame.bytes[slot_idx + 1] = (uint8_t)(current_val & 0xFF);
}
outbound_frame.len = 8;
can_tx_fifo_push(&main_tx_ring, outbound_frame);
dispatch_control_commands(&hcan1, &main_tx_ring, target_id);
}