3軸リアクションホイール倒立振子を作るアドカレの14日目です。
1. はじめに:FOCの「出力段」を作る
これまでの設計で、AIによる姿勢制御指令(Day 11)と、FOCの理論的枠組み(Day 12)が完成しました。本日からはいよいよ、STM32G4マイコンを用いたFOCの実装フェーズに入ります。
本記事の目的は、FOCの出力段にあたる 「SVPWM(空間ベクトル変調)」 の生成機能を実装することです。計算された電圧指令値 $(V_d, V_q)$ を、実際のモータを駆動するための3相PWM信号へと変換するプロセスです。
今回は安全性を考慮し、モータやインバータ、電流センサを接続しない STM32単体でのOpen Loopテスト を行います。マイコン内部で生成された波形が理論通りであるかを、シリアルプロッタおよびオシロスコープを用いて電気的に検証します。
2. ハードウェア設定 (STM32CubeMX)
FOC制御において、PWMの生成品質は制御性能に直結します。STM32G4シリーズが持つ高機能タイマー TIM1 を使用し、FOCに最適化された設定を行います。
2.1 タイマー基本設定
- Timer: Advanced Control Timer 1 (TIM1)
- Clock Source: Internal Clock (170 MHz)
-
Channel 1-3: PWM Generation CHx & CHxN
- 相補PWM出力(Complementary PWM)を有効にします。これにより、ハイサイド・ローサイドのFETを対で制御する信号が生成されます。
2.2 重要パラメータ: Center Aligned Mode
ここがFOC実装における重要なポイントです。一般的なモータ制御で使われる「Edge Aligned(鋸波)」モードではなく、「Center Aligned(三角波)」モード を選択します。
- Counter Mode: Center Aligned Mode 1
選定理由:
Center Aligned Modeでは、PWMのON区間がキャリア周期の中央に配置されます。これにより以下のメリットが得られます。
- 高調波ノイズの低減: スイッチングのタイミングが分散され、電流リプルが減少します。
- 電流計測との同期: カウンタの頂点(または底)で電流計測(ADCトリガ)を行うことで、スイッチングノイズの影響を受けない平均電流をサンプリングすることが容易になります。
2.3 PWMキャリア周波数の設定
FOCの制御周期となるPWM周波数は 20 kHz に設定します。
- Prescaler (PSC): 0
- Counter Period (ARR): 4250
計算根拠:
STM32G4のタイマークロックは170 MHzです。Center Aligned Modeでは、カウンタが $0 \to ARR \to 0$ とアップダウンカウントを行うため、1周期に要するクロック数は $ARR \times 2$ となります。
$$
\text{ARR} = \frac{f_{CLK}}{2 \times f_{PWM}} = \frac{170 \times 10^6}{2 \times 20 \times 10^3} = 4250
$$
3. ソフトウェア実装:SVPWMロジック
SVPWMの実装には、三角関数を多用するベクトル空間での計算よりも、計算負荷の軽い 「Min-Max法」 を採用します。これは、3相正弦波電圧にゼロ相成分(三次高調波)を注入することで、等価的に空間ベクトル変調を実現する手法です。
3.1 アルゴリズム実装
入力となる電圧ベクトル $(V_\alpha, V_\beta)$ から、各相のコンペアレジスタ値(CCR)を算出する関数を実装します。
数式処理:
-
逆Clarke変換:
2相直交座標系の電圧指令を、一時的な3相電圧 $(V_a, V_b, V_c)$ に変換します。$$
\begin{cases}
V_a = V_\alpha \
V_b = -\frac{1}{2}V_\alpha + \frac{\sqrt{3}}{2}V_\beta \
V_c = -\frac{1}{2}V_\alpha - \frac{\sqrt{3}}{2}V_\beta
\end{cases}
$$ -
ゼロ相電圧の注入 (Min-Max法):
SVPWM特有の「鞍型波形」を生成するためのオフセット電圧 $V_{offset}$ を算出します。$$
V_{offset} = -0.5 \times (\max(V_a, V_b, V_c) + \min(V_a, V_b, V_c))
$$このオフセットを各相に加算することで、線間電圧を変えずに相電圧のピークを抑え、電圧利用率を向上させます。
$$
\begin{cases}
V_{pwm_a} = V_a + V_{offset} \
V_{pwm_b} = V_b + V_{offset} \
V_{pwm_c} = V_c + V_{offset}
\end{cases}
$$ -
レジスタセット:
算出されたDuty比(0.0~1.0)をTIM1のARR値(4250)にスケーリングし、TIM1->CCR1~CCR3に書き込みます。
3.2 テスト用コード (Open Loop)
電流フィードバックを行わず、メインループ内で電圧ベクトルを強制的に回転させるコードで動作確認を行います。
/* USER CODE BEGIN Includes */
#include <math.h>
#include "main.h"
/* USER CODE END Includes */
/* USER CODE BEGIN PV */
// PWM周期 (ARR設定値)
#define PWM_PERIOD_COUNTS 4250.0f
/* USER CODE END PV */
/* USER CODE BEGIN 0 */
/**
* @brief SVPWM出力関数
* @param V_alpha: 固定座標系の電圧指令 (-1.0 ~ 1.0)
* @param V_beta : 固定座標系の電圧指令 (-1.0 ~ 1.0)
*/
void FOC_SVPWM_Update(float V_alpha, float V_beta) {
// 1. 逆Clarke変換 (alpha, beta -> U, V, W)
// Va = Valpha
// Vb = -0.5 * Valpha + (sqrt(3)/2) * Vbeta
// Vc = -0.5 * Valpha - (sqrt(3)/2) * Vbeta
// sqrt(3)/2 = 0.8660254f
float Va = V_alpha;
float Vb = -0.5f * V_alpha + 0.8660254f * V_beta;
float Vc = -0.5f * V_alpha - 0.8660254f * V_beta;
// 2. Min-Max法によるSVPWM (コモンモード電圧の注入)
// 3相の中で一番大きい電圧と小さい電圧を見つける
float V_max = fmaxf(Va, fmaxf(Vb, Vc));
float V_min = fminf(Va, fminf(Vb, Vc));
// 鞍型にするためのオフセット電圧
float V_common = -0.5f * (V_max + V_min);
// 3. オフセットを加算してデューティ比に変換
// 入力(-1.0~1.0) -> CCR(0~ARR)
// Center Alignedなので、duty 0.0 が中心、-1.0が0、+1.0がARRになるようにマップする
// 式: CCR = ( (V + V_common) + 1.0 ) * 0.5 * ARR
// クリップ処理 (過変調防止)
// ※厳密には正規化が必要ですが、テストなので簡易リミッタで
float u_out = Va + V_common;
float v_out = Vb + V_common;
float w_out = Vc + V_common;
// レジスタ書き込み
TIM1->CCR1 = (uint32_t)((u_out + 1.0f) * 0.5f * PWM_PERIOD_COUNTS);
TIM1->CCR2 = (uint32_t)((v_out + 1.0f) * 0.5f * PWM_PERIOD_COUNTS);
TIM1->CCR3 = (uint32_t)((w_out + 1.0f) * 0.5f * PWM_PERIOD_COUNTS);
}
/* USER CODE END 0 */
/* USER CODE BEGIN 2 */
// PWM出力開始 (相補出力CHxNも忘れずに)
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3);
HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1);
HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_2);
HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_3);
// テスト用変数
float theta = 0.0f;
float voltage_amp = 0.8f; // 電圧振幅 0.8 (最大1.0だが安全マージン)
/* USER CODE END 2 */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE BEGIN 3 */
// 1. 角度を進める (50Hz程度で回転)
theta += 0.05f;
if (theta > 6.283185f) theta -= 6.283185f;
// 2. 電圧ベクトル生成 (逆Park変換の簡易版)
// 本来は Id, Iq から計算しますが、テストなので直接 Alpha, Beta を生成
float valpha = voltage_amp * cosf(theta);
float vbeta = voltage_amp * sinf(theta);
// 3. SVPWM実行
FOC_SVPWM_Update(valpha, vbeta);
// 4. 結果確認 (CCRレジスタの値をプロット)
// ExcelやSerialPlotterで波形を確認してください
printf("%.0f,%.0f,%.0f\r\n",
(float)TIM1->CCR1,
(float)TIM1->CCR2,
(float)TIM1->CCR3);
HAL_Delay(1); // 適当なウェイト
}
/* USER CODE END 3 */
4. 検証1:DAC/シリアルプロッタによる波形確認
STM32内部で計算され、CCRレジスタに設定される直前の値をUARTで出力し、シリアルプロッタで可視化しました。

考察:
取得された波形は綺麗な正弦波ではなく、「頭が凹んだM字型(鞍型波形)」 になっていることが確認できます。
これがSVPWM(Min-Max法)によるゼロ相電圧注入の結果です。この波形形状により、通常の正弦波PWMと比較して、DCバス電圧に対する線間電圧の最大振幅(電圧利用率)を 約15.5% ($\frac{2}{\sqrt{3}} \approx 1.1547$) 向上させることができます。
5. 検証2:オシロスコープによる実出力確認
次に、実際のGPIOピン(TIM1_CH1, CH2)にオシロスコープのプローブを接続し、物理的な電気信号を確認します。

5.1 PWMパルスの確認
確認事項:
- スイッチング周波数: 時間軸を確認すると、設定通り 20 kHz(周期 50 µs)でパルスが出力されています。
- Duty比の変化: 時間経過とともにパルス幅が変動しており、変調がかかっていることが分かります。これがインバータのFETを駆動する実際の信号となります。
5.2 線間電圧の確認(正弦波の復元)
SVPWMの相電圧(対GND)はM字型ですが、モータにかかる電圧である「線間電圧(相間電圧)」は正弦波になるはずです。これを検証するため、2chオシロスコープのMath機能(CH1 - CH2)を使用して $V_{uv}$ を観測します。
確認事項:
- Math波形(緑色): PWMのパルス密度が描くシルエット(平均電圧)に着目すると、綺麗な正弦波(サインカーブ) が現れていることが確認できます。
結論:
Min-Max法によって注入されたM字型の歪み成分(同相成分)は、相間の差分を取ることで完全にキャンセルされています。これにより、モータには純粋な正弦波電圧が印加され、効率的かつ低振動な駆動が可能であることが物理的に証明されました。
6. まとめ
本日はFOCの実装第一歩として、STM32G4のTIM1を用いたSVPWM出力の実装と検証を行いました。
- Center Aligned Mode の採用により、FOCに適した低ノイズなPWM設定を行いました。
- Min-Max法 を用いた軽量なアルゴリズムで、電圧利用率の高いSVPWMを実装しました。
- 波形検証 において、計算上の「M字波形」と、物理出力としての「線間正弦波」の両方が理論通りであることを確認しました。
これで、モータを回すための「出力」機能は完成です。
次回予告 [Day 15]
FOC制御ループを閉じるためには、モータに流れる電流を正確に把握する必要があります。次回はFOC実装の最難関である 「電流センシング(ADC同期測定)」 の実装を行います。PWMのタイミングに合わせてノイズフリーな電流値を取得する、高度なADCトリガ設定に挑みます。

