CubeIDEでのB-G431B-ESC1の開発方法

B-G431B-ESC1はかなりハイスペックなESCです。
マイコンに自分でプログラムを書き込んで使うため、燃えたりするリスクはありますが、扱えるようになればかなり便利です。CubeIDEを用いた開発方法を解説します。

:::note alert
MOSFETの開閉を誤るとESCは燃えます。デッドタイムなどの設定もかなり重要になるので、この記事に書いてあることはしっかりと確認しましょう。
:::

公式サイト

https://www.st.com/ja/evaluation-tools/b-g431b-esc1.html

回路図

https://www.st.com/resource/en/schematic_pack/mb1419-g431cbu6-c01_schematic.pdf

# .iocファイルの設定(CubeMX)

まずプロジェクトを作成する際、ターゲットはSTM32G431CBU6を選びましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843466/97de8fd9-db8f-499f-a8fb-47076c163407.png)

### クロック
ボードには最初から外部クロックが載っているので、System CoreのRCCでHSEをCrystalにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843466/36ff21de-1707-4606-b86f-022bc353d0e0.png)


クロックは以下のようにHSEを使ってPLLCLKから供給し、100MHzにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843466/6cbdf461-3ff4-4931-a37d-b2ff12afe71e.png)

### ピン配置
以下のようにピンを仮設定してください。(黄色いピン)
![スクリーンショット 2025-04-20 184413.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843466/7a23225c-9856-4788-931d-ad3c861092f1.png)


### 出力用PWM
TIM1のPWMのModeを以下のように設定します。(Nは出力反転。ローサイドとハイサイドのMOSFETで開閉を逆にするため。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843466/a5274f45-2c40-4d2e-9b35-ab2946ac42a7.png)

Configurationを以下のように設定します。Counter Periodは2500 - 1ですが、Center Aligned modeを使うので、実質的な周波数は40kHzではなく半分の20kHzになります。ADCをいい感じのタイミングで読む(Low-Side Current Sense)ためにトリガーを使うので、RCRを1にしてTRGOをUpdate Eventにします。MOSFETは開閉に時間がかかるので、ハイサイドとローサイドが同時に開くのを防ぐために、Dead Timeを75(750ns)にします。データシートによると開閉時間は200nsらしく、マージンを設けて750nsにします。 基本的にこれを設定しておけば、低めのDutyで動かせば燃えるリスクはかなり低減します。

:::note alert
逆にこれを設定しないと**出力関係なく燃える**ので要注意
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843466/6629d53d-27e7-4dd9-a985-3d56e2f6fb91.png)

各チャンネルのModeは全てPWM mode 2にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843466/ca83a90b-f419-4b05-adb8-c80a1bb1e47c.png)

### 電流センサー
#### OPAMP
シャント抵抗にかかる電圧を取得するためにオペアンプで増幅します。OPAMP1~3を全て以下のように設定してください。Modeは色々ありますが、これが良いです。倍率は16です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843466/64cbc396-ac0f-4a7e-82f8-6d015f4fd688.png)

#### ADC
ADCはModeを以下のように、VOPAMP1~3をADC1とADC2で選択してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843466/84db41eb-021f-4cc5-8cce-176074dad35a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843466/3e3932c3-bbf6-4534-afc5-d00ef3d223ed.png)

ADC1のConfigurationは以下のようにします。ADC1とADC2を同期させて、TIM1のトリガーのタイミングで電流を読みます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843466/95d85312-4a34-446e-8c39-eb2cad7942ee.png)

ADC2
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843466/443b4ab5-4ca3-44cb-8e2c-a2bdc29f8b30.png)

ADCの完了割り込みも有効にしておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843466/6804544f-7d8c-4869-8857-d3bb5c772a32.png)

コードをジェネレートしましょう。

# プログラム
とりあえず強制転流のみのプログラムです。
main.cのユーザーコード内に書いていきましょう。

インクルード
```c
/* USER CODE BEGIN Includes */
#include "math.h"
/* USER CODE END Includes */
```

変数宣言
```c
/* USER CODE BEGIN PV */
const float UVW_UNIT_VECTOR[3][2] = {
	{1.0f, 0.0f},
	{-0.5f, 0.86602540378f},
	{-0.5f, -0.86602540378f}
};

float feedback_current[3] = {0.0f};;
float offset_current[3] = {0.0f};

float duty_out = 0.1f;
float freq_out = 10.0f;
/* USER CODE END PV */
```

関数宣言
```c
/* USER CODE BEGIN 0 */
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc){
	feedback_current[0] = -((ADC1->JDR1 - offset_current[0]) / 4095.0f * 3.3f / 16.0f) * (7000.0f / 12.0f);
	feedback_current[1] = -((ADC2->JDR1 - offset_current[1]) / 4095.0f * 3.3f / 16.0f) * (7000.0f / 12.0f);
	feedback_current[2] = -((ADC2->JDR2 - offset_current[2]) / 4095.0f * 3.3f / 16.0f) * (7000.0f / 12.0f);
}

void outputDuty(float alpha, float beta){
	float norm = sqrt(alpha * alpha + beta * beta);
	if(norm > 1.0f) {
		alpha /= norm;
		beta /= norm;
		norm = 1.0f;
	}
	float u = alpha * UVW_UNIT_VECTOR[0][0] + beta * UVW_UNIT_VECTOR[0][1];
	float v = alpha * UVW_UNIT_VECTOR[1][0] + beta * UVW_UNIT_VECTOR[1][1];
	float w = alpha * UVW_UNIT_VECTOR[2][0] + beta * UVW_UNIT_VECTOR[2][1];
	__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 1250.0f - 1000.0f * u);
	__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, 1250.0f - 1000.0f * v);
	__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, 1250.0f - 1000.0f * w);
}
/* USER CODE END 0 */
```

Init
```c
    /* USER CODE BEGIN 2 */
    HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);
    HAL_ADCEx_Calibration_Start(&hadc2, ADC_SINGLE_ENDED);
    HAL_ADC_Start(&hadc1);
    HAL_ADC_Start(&hadc2);
    HAL_ADCEx_InjectedStart_IT(&hadc1);
    
    HAL_OPAMP_Start(&hopamp1);
    HAL_OPAMP_Start(&hopamp2);
    HAL_OPAMP_Start(&hopamp3);
    
    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);
    
    HAL_Delay(100);
    
    for(int i = 0; i < 10; i ++){
        offset_current[0] += ADC1->JDR1 / 10.0f;
        offset_current[1] += ADC2->JDR1 / 10.0f;
        offset_current[2] += ADC2->JDR2 / 10.0f;
        HAL_Delay(10);
    }
    /* USER CODE END 2 */
```

メインループ
```c
	/* USER CODE BEGIN WHILE */
	while (1)
	{
		/* USER CODE END WHILE */

		/* USER CODE BEGIN 3 */
		float angle_out = HAL_GetTick() / 1000.0f * 2.0f * M_PI * freq_out;
		outputDuty(duty_out * cos(angle_out), duty_out * sin(angle_out));
	}
	/* USER CODE END 3 */

```
:::note warn
duty_outは0.1(10%)にしてあります。
強制転流は電流が流れやすく、発熱もしやすいため、実験する際は電源装置で12Vくらいの低い電圧で、1Aくらいの電流制限で行ってください。
:::

https://x.com/Doraemonjayo/status/1914686232397013108

電流値はデバッグしながらLive Expressionsなどからみてください。(三相交流にはなっていますが、電源装置に表示される電流値と結構値が違うので、もしかすると計算式が間違っているかもしれないです。)

:::note info
エンコーダやホールセンサを用いた角度の取得は、各自でモーターに合わせてコーディングしてください。

モーターによって異なるため、この記事に例を示してしまうと、それをコピペした際に危険を伴うと判断したため、オープンループである強制転流のみの例を示しました。

ベクトル制御について、ある程度の知識を得てから行うようにしてください。
ベクトル制御についても、いつか解説しようと思います。
:::