Skip to content

メカナム4輪サンプルコード解説

#include "main.h"
#include "Altair_library_for_CubeIDE/altair.h"

CAN_HandleTypeDef hcan1;
CAN_HandleTypeDef hcan2;

TIM_HandleTypeDef htim1;
TIM_HandleTypeDef htim2;
TIM_HandleTypeDef htim3;
TIM_HandleTypeDef htim4;
TIM_HandleTypeDef htim5;
TIM_HandleTypeDef htim10;
TIM_HandleTypeDef htim11;
TIM_HandleTypeDef htim12;
TIM_HandleTypeDef htim13;
TIM_HandleTypeDef htim14;

USART_HandleTypeDef husart2;
USART_HandleTypeDef husart3;

// --- Altair ライブラリ用オブジェクト ---
// モータドライバ(4輪メカナム)
MotorDriver motorFR; // Front Right
MotorDriver motorFL; // Front Left
MotorDriver motorBR; // Back Right
MotorDriver motorBL; // Back Left

// エンコーダ
Encoder encoderFR;
Encoder encoderFL;
Encoder encoderBR;
Encoder encoderBL;

EncoderData encoderDataFR;
EncoderData encoderDataFL;
EncoderData encoderDataBR;
EncoderData encoderDataBL;

// PID コントローラ
Pid pidFR;
Pid pidFL;
Pid pidBR;
Pid pidBL;

// 運動学
Kinematics kinematics;

// 制御パラメータ
#define CONTROL_PERIOD_MS      10
#define WHEEL_DIAMETER_MM     100.0f
#define ROBOT_DIAMETER_MM    1000.0f
// エンコーダの PPR(ご使用のエンコーダに合わせて変更してください)
#define ENCODER_PPR          8192

// CAN 受信で使用するスケーリング(int16 -> 物理量)
#define CAN_VEL_SCALE_MM_S     1.0f   // 1LSB = 1 mm/s
#define CAN_OMEGA_SCALE_DEG_S  100.0f   // 1LSB = 10 deg/s

// 目標速度 [mm/s, deg/s]
float targetVx = 0.0f;
float targetVy = 0.0f;
float targetOmega = 0.0f;

// CAN ライブラリ受信デバッグ用(CubeMonitor で監視する)
volatile uint32_t can_rx_count = 0;      // 受信回数
volatile int16_t  can_last_vx_raw = 0;   // 生データ vx
volatile int16_t  can_last_vy_raw = 0;   // 生データ vy
volatile int16_t  can_last_om_raw = 0;   // 生データ omega
volatile uint8_t  can_last_dlc    = 0;   // 直近フレーム DLC

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_CAN1_Init(void);
static void MX_CAN2_Init(void);
static void MX_TIM1_Init(void);
static void MX_TIM2_Init(void);
static void MX_TIM3_Init(void);
static void MX_TIM4_Init(void);
static void MX_TIM5_Init(void);
static void MX_TIM10_Init(void);
static void MX_TIM11_Init(void);
static void MX_TIM12_Init(void);
static void MX_TIM13_Init(void);
static void MX_TIM14_Init(void);
static void MX_USART2_Init(void);
static void MX_USART3_Init(void);


int main(void){

  HAL_Init();
  SystemClock_Config();

  MX_GPIO_Init();
  MX_CAN1_Init();
  MX_CAN2_Init();
  MX_TIM1_Init();
  MX_TIM2_Init();
  MX_TIM3_Init();
  MX_TIM4_Init();
  MX_TIM5_Init();
  MX_TIM10_Init();
  MX_TIM11_Init();
  MX_TIM12_Init();
  MX_TIM13_Init();
  MX_TIM14_Init();
  MX_USART2_Init();
  MX_USART3_Init();

  // CAN1 を Altair ライブラリで初期化(割り込み・フィルタ設定)
  Can_Init(&hcan1);

  // --- ここから 4輪メカナム制御 ---

  // モータ初期化(ピン割り当てに応じてタイマを接続)
  // MD
  //  * B14(TIM12 CH1) B15(TIM12 CH2)  -> Front Right
  //  * A8 (TIM1  CH1) A9 (TIM1  CH2)  -> Front Left
  //  * A6 (TIM13 CH1) A7 (TIM14 CH1)  -> Back Right
  //  * B8 (TIM10 CH1) B9 (TIM11 CH1)  -> Back Left
  MotorDriver_Init(&motorFR, &htim12, TIM_CHANNEL_1, &htim12, TIM_CHANNEL_2);
  MotorDriver_Init(&motorFL, &htim1,  TIM_CHANNEL_1, &htim1,  TIM_CHANNEL_2);
  MotorDriver_Init(&motorBR, &htim13, TIM_CHANNEL_1, &htim14, TIM_CHANNEL_1);
  MotorDriver_Init(&motorBL, &htim10, TIM_CHANNEL_1, &htim11, TIM_CHANNEL_1);

  // エンコーダ初期化
  // encoder
  //  * A0 A1  TIMER5
  //  * B3 A15 TIMER2
  //  * B6 B7 TIMER4
  //  * C6 C7 TIMER3
  Encoder_Init(&encoderFR, &htim5, WHEEL_DIAMETER_MM, ENCODER_PPR, CONTROL_PERIOD_MS);
  Encoder_Init(&encoderFL, &htim2, WHEEL_DIAMETER_MM, ENCODER_PPR, CONTROL_PERIOD_MS);
  Encoder_Init(&encoderBR, &htim4, WHEEL_DIAMETER_MM, ENCODER_PPR, CONTROL_PERIOD_MS);
  Encoder_Init(&encoderBL, &htim3, WHEEL_DIAMETER_MM, ENCODER_PPR, CONTROL_PERIOD_MS);

  // PID 初期化(まずはP制御のみ。ゲインは実機で要調整)
  Pid_Init(&pidFR);
  Pid_Init(&pidFL);
  Pid_Init(&pidBR);
  Pid_Init(&pidBL);
  Pid_setGain(&pidFR, 10.0, 0.0, 0.0, 0.0);
  Pid_setGain(&pidFL, 10.0, 0.0, 0.0, 0.0);
  Pid_setGain(&pidBR, 10.0, 0.0, 0.0, 0.0);
  Pid_setGain(&pidBL, 10.0, 0.0, 0.0, 0.0);

  // メカナム運動学設定
  kinematics.mode = MEKANUM;
  kinematics.robot_diameter = ROBOT_DIAMETER_MM;
  kinematics.wheel_radius   = WHEEL_DIAMETER_MM / 2.0f;

  uint32_t lastControlTick = HAL_GetTick();

  while (1){

    uint32_t now = HAL_GetTick();
    if (now - lastControlTick >= CONTROL_PERIOD_MS)
    {
      lastControlTick += CONTROL_PERIOD_MS;

      // --- CAN 受信処理 ---
      if (g_can1_rx_data.new_data_flag)
      {
        can_rx_count++;
        can_last_dlc = g_can1_rx_data.dlc;

        // PC からの Vx/Vy/Omega 形式(ID 0x123)を想定
        if (g_can1_rx_data.std_id == 0x123 && g_can1_rx_data.dlc >= 6)
        {
          int16_t vx_raw = (int16_t)((g_can1_rx_data.data[1] << 8) | g_can1_rx_data.data[0]);
          int16_t vy_raw = (int16_t)((g_can1_rx_data.data[3] << 8) | g_can1_rx_data.data[2]);
          int16_t om_raw = (int16_t)((g_can1_rx_data.data[5] << 8) | g_can1_rx_data.data[4]);

          can_last_vx_raw = vx_raw;
          can_last_vy_raw = vy_raw;
          can_last_om_raw = om_raw;

          targetVx     = (float)vx_raw / CAN_VEL_SCALE_MM_S;    // [mm/s]
          targetVy     = (float)vy_raw / CAN_VEL_SCALE_MM_S;    // [mm/s]
          targetOmega  = (float)om_raw / CAN_OMEGA_SCALE_DEG_S; // [deg/s]
        }

        // 読み取り完了
        g_can1_rx_data.new_data_flag = 0;
      }

      // --- エンコーダ更新 ---
      Encoder_Interrupt(&encoderFR, &encoderDataFR);
      Encoder_Interrupt(&encoderFL, &encoderDataFL);
      Encoder_Interrupt(&encoderBR, &encoderDataBR);
      Encoder_Interrupt(&encoderBL, &encoderDataBL);

      // --- 運動学:目標ホイール速度 [rps] ---
      float speedFR, speedFL, speedBR, speedBL;
      Kinematics_GetTargetSpeeds(&kinematics,
                                 targetVx,
                                 targetVy,
                                 targetOmega,
                                 &speedFR, &speedFL, &speedBR, &speedBL);

      // --- PID 制御(目標 rps vs 実測 rps)---
      double uFR = Pid_control(&pidFR, speedFR, encoderDataFR.rps, CONTROL_PERIOD_MS);
      double uFL = Pid_control(&pidFL, speedFL, encoderDataFL.rps, CONTROL_PERIOD_MS);
      double uBR = Pid_control(&pidBR, speedBR, encoderDataBR.rps, CONTROL_PERIOD_MS);
      double uBL = Pid_control(&pidBL, speedBL, encoderDataBL.rps, CONTROL_PERIOD_MS);

      // 出力を -99〜99 [%] にクリップ
      if (uFR > 99.0) uFR = 99.0;
      if (uFR < -99.0) uFR = -99.0;
      if (uFL > 99.0) uFL = 99.0;
      if (uFL < -99.0) uFL = -99.0;
      if (uBR > 99.0) uBR = 99.0;
      if (uBR < -99.0) uBR = -99.0;
      if (uBL > 99.0) uBL = 99.0;
      if (uBL < -99.0) uBL = -99.0;

      // モーターへ反映(機体の配線方向により符号は適宜反転してください)
      MotorDriver_setSpeed(&motorFR, (int)uFR);
      MotorDriver_setSpeed(&motorFL, (int)uFL);
      MotorDriver_setSpeed(&motorBR, (int)uBR);
      MotorDriver_setSpeed(&motorBL, (int)uBL);
    }
  }
}


//以下設定

このコードの全体像

このサンプルは、Altair ライブラリを使って「4 輪メカナムロボット」を速度指令で動かす最小構成のコードです。

  • PC などから CAN で送られてくる \(V_x, V_y, \omega\)(前後・左右・旋回の目標速度)を受信
  • メカナムの運動学から「各ホイールの目標回転数」を計算
  • エンコーダで測ったホイールの実回転数と比較して PID 制御
  • モータドライバに PWM 出力を流して、目標どおりの速度になるよう制御

という流れになっています。

グローバル変数とオブジェクト

  • CAN_HandleTypeDef hcan1, hcan2
    → STM32CubeMX が生成する CAN ペリフェラルのハンドル。Can_Init(&hcan1); で Altair ライブラリに渡します。

  • MotorDriver motorFR, motorFL, motorBR, motorBL
    → 4 輪分のモータドライバオブジェクト(Front/Back, Right/Left)。
    MotorDriver_Init() で「どのタイマのどのチャネルがどの方向の PWM か」を教えています。

  • Encoder encoderFR ...EncoderData encoderDataFR ...
    → 4 輪分のエンコーダを扱うための構造体。Encoder_Init() で、

  • どのタイマをエンコーダモードで使うか
  • 車輪直径 WHEEL_DIAMETER_MM
  • エンコーダの PPR ENCODER_PPR
  • 制御周期 CONTROL_PERIOD_MS を設定します。Encoder_Interrupt() を周期ごとに呼ぶことで、encoderDataX.rps(回転数 [rps])が更新されます。

  • Pid pidFR ...
    → 4 輪分の PID コントローラ。Pid_setGain()\(K_p, K_i, K_d\) とバイアスを設定しています。ここでは \(P\) 制御のみで、\(K_p = 10\)、他は 0 です(実機で必ず調整が必要)。

  • Kinematics kinematics
    → メカナム運動学を計算するための構造体。
    kinematics.mode = MEKANUM; でメカナムモードを指定し、 robot_diameter(車体直径)と wheel_radius(車輪半径)を与えることで、 \(V_x, V_y, \omega\) から各ホイールの回転数を求められるようにしています。

制御パラメータと定数

  • CONTROL_PERIOD_MS
    → 制御ループの周期 [ms]。ここでは 10ms(=100Hz 制御)。

  • WHEEL_DIAMETER_MM / ROBOT_DIAMETER_MM
    → 車輪直径・機体直径 [mm]。メカナムの運動学で使用します。実機寸法に合わせてください。

  • ENCODER_PPR
    → エンコーダの 1 回転あたりパルス数(PPR)。使用するエンコーダに合わせて必ず変更します。

  • CAN_VEL_SCALE_MM_S, CAN_OMEGA_SCALE_DEG_S
    → CAN で送られてくる 16bit 整数を「物理量」に直すためのスケーリング係数です。
    ここではコメントと数値が少し食い違っているので、実際に使うときは次のように整理するとよいです:

    • 例:CAN_VEL_SCALE_MM_S = 1.0f なら「1 LSB = 1 mm/s」
    • 例:CAN_OMEGA_SCALE_DEG_S = 10.0f なら「1 LSB = 10 deg/s」 など、自分のプロトコルに合わせて決めてください。

main 関数の初期化

  HAL_Init();
  SystemClock_Config();

  MX_GPIO_Init();
  MX_CAN1_Init();
  ...
  MX_USART3_Init();

  Can_Init(&hcan1);

ここまでは STM32 のお決まり処理です。

  • HAL / クロック / GPIO / タイマ / CAN / USART などを CubeMX 生成コードで初期化
  • Can_Init(&hcan1); で Altair ライブラリ側の CAN 設定(フィルタ・割り込み)を実施

その後で、Altair のモータドライバ・エンコーダ・PID・運動学の設定を行っています。

モータとエンコーダの初期化

  MotorDriver_Init(&motorFR, &htim12, TIM_CHANNEL_1, &htim12, TIM_CHANNEL_2);
  ...
  Encoder_Init(&encoderFR, &htim5, WHEEL_DIAMETER_MM, ENCODER_PPR, CONTROL_PERIOD_MS);
  • モータ:どのタイマのどのチャネルが「正転」「逆転」の PWM かを指定
    コメントに、STM32 のピン番号とタイマ対応が書いてあります。基板の配線に合わせてここを変更します。

  • エンコーダ:どのタイマをエンコーダモードとして使うかを指定
    WHEEL_DIAMETER_MMENCODER_PPR から、「何パルスで何 mm 進んだか」を内部で計算します。

制御ループの流れ

  uint32_t lastControlTick = HAL_GetTick();

  while (1) {
    uint32_t now = HAL_GetTick();
    if (now - lastControlTick >= CONTROL_PERIOD_MS) {
      lastControlTick += CONTROL_PERIOD_MS;
      ...
    }
  }

HAL_GetTick() を使って、10ms ごとに制御処理を呼び出すループになっています。中身の流れは次のとおりです。

  1. CAN から速度指令を受信
  2. エンコーダからホイール速度を更新
  3. 運動学で「各ホイールの目標回転数」を計算
  4. PID で各ホイールを制御
  5. モータドライバに出力

1. CAN 受信と速度指令の更新

  if (g_can1_rx_data.new_data_flag) {
    ...
    if (g_can1_rx_data.std_id == 0x123 && g_can1_rx_data.dlc >= 6) {
      int16_t vx_raw = ...;
      int16_t vy_raw = ...;
      int16_t om_raw = ...;

      targetVx    = (float)vx_raw / CAN_VEL_SCALE_MM_S;
      targetVy    = (float)vy_raw / CAN_VEL_SCALE_MM_S;
      targetOmega = (float)om_raw / CAN_OMEGA_SCALE_DEG_S;
    }
    g_can1_rx_data.new_data_flag = 0;
  }
  • ID 0x123 のフレームを「\(V_x, V_y, \omega\) 指令」とみなしています。
  • データは 16bit 符号付き整数(little endian)で
  • Byte0-1: \(V_x\)
  • Byte2-3: \(V_y\)
  • Byte4-5: \(\omega\) という前提で復元しています。
  • スケーリング係数で割ることで、[mm/s]・[deg/s] の実際の物理量に変換しています。

2. エンコーダ更新

  Encoder_Interrupt(&encoderFR, &encoderDataFR);
  ...
  • 各ホイールごとに Encoder_Interrupt() を呼ぶことで、
  • 回転数 rps
  • 進んだ距離 [mm] などが encoderDataX に更新されます。

3. 運動学でホイール目標速度を計算

  float speedFR, speedFL, speedBR, speedBL;
  Kinematics_GetTargetSpeeds(&kinematics,
                             targetVx,
                             targetVy,
                             targetOmega,
                             &speedFR, &speedFL, &speedBR, &speedBL);
  • 与えられた \(V_x, V_y, \omega\) から、4 輪それぞれの「目標回転数 [rps]」を計算します。
  • メカナム特有の「斜めローラ付きホイールによる自由度 3 の移動」をここで実現しています。

4. PID 制御とモータ出力

  double uFR = Pid_control(&pidFR, speedFR, encoderDataFR.rps, CONTROL_PERIOD_MS);
  ...
  // -99〜99[%] にクリップ
  MotorDriver_setSpeed(&motorFR, (int)uFR);
  ...
  • 目標回転数(speedFR など)とエンコーダ実測値(encoderDataFR.rps)の差を PID で補正して、 -99〜99[%] のデューティ比としてモータドライバに渡しています。
  • MotorDriver_setSpeed() は、符号(正転/逆転)と大きさ(デューティ)を内部で PWM に変換します。
    実際の機体の配線により「前に進むはずが後ろに進む」場合は、個々のホイールごとに符号を反転させてください。

実機で調整すべきポイント

  • ENCODER_PPR : 使っているエンコーダの仕様に合わせる
  • WHEEL_DIAMETER_MM, ROBOT_DIAMETER_MM : 実寸に合わせる
  • CONTROL_PERIOD_MS : 制御周波数(100Hz〜1kHz くらいが目安)
  • CAN_VEL_SCALE_MM_S, CAN_OMEGA_SCALE_DEG_S : 上位機器との CAN プロトコルに合わせる
  • Pid_setGain()\(K_p, K_i, K_d\) : 実機試験でチューニング(最初は \(P\) のみで小さめから)
  • MotorDriver_setSpeed() の前後での符号 : 機体の動きと合わせて必要なら反転