ESP32に３相同期・三角波・相補PWMを打たせ、山割込みする

# はじめに

本記事は[Arduino Dueに３相同期・三角波・相補PWMを打たせる](https://qiita.com/motorcontrolman/items/ef6670f9dff0f63cdc50)、および[Arduino DueでPWM山同期AD変換を行う](https://qiita.com/motorcontrolman/items/ba2cd0fb200501186194)のESP32版になります。
(記事の構成上、AD変換は本記事に含んでいませんがもう少し頑張れば出来る、はず)
それぞれの単語の意味については上記記事を参照下さい。

・使用したボード：ESP32-DevKitC
・開発環境：Arduino IDE 2.0.3
・ArduinoにおけるESP32 ライブラリバージョン：1.0.6

# 0. 前準備
ESP32の開発環境としてArduino IDEを用いる場合、対象ボードとしてESP32を追加する必要が原則としてあります。(IDEのバージョンが2.0.3では不要だったかも…うろ覚え)

その場合は下記リンク等を参考に追加しましょう。
[ESP32-Arduino IDEを活用した開発環境の準備](https://spiceman.jp/esp32-arduino-ide/)

# 1. 【最重要】ヘッダファイルの編集
いきなりそれかよと思われる方も多いかと思いますが、残念ながら避けては通れません。

## 1.1 編集が必要なファイルおよび格納場所

ESP32にはモータ制御用のAPIとして[MCPWM](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/mcpwm.html)が準備されており、表題の「３相同期・三角波・相補PWM」を打たせる場合にもこいつを使用します。

しかしながら、「3相同期」を行うための設定が<b>なぜか</b>デフォルトのライブラリに含まれていない場合があります。(少なくともライブラリバージョン1.0.6ではそうなっていた)

本現象への対策として、インストールされたライブラリの実体であるソースコードを編集します。具体的には、MCPWMのヘッダファイルである<b>『mcpwm.h』</b>を編集します。

『mcpwm.h』がどこにあるかというと、標準インストールであれば

`C:\Users\ユーザー名\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6\tools\sdk\include\driver\driver`

にあるはずです。無い場合はESP32ライブラリのインストールが正しく出来ているかを確認しましょう。

### 2024/9/13追記
Arduino IDE 2.3.2では
`C:\Users\ユーザー名\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.0\tools\sdk\esp32\include\driver\include\driver`
にありました。


## 1.2 編集内容
143行目ぐらいに構造体 `mcpwm_sync_signal_t` の定義文があると思いますので、メンバに `MCPWM_SELECT_SYNC_INT0` があるかどうかを確認しましょう。恐らくデフォルトでは存在しないので、下記コードを参考に追加しましょう。

```c:mcpwm.h
typedef enum {
    MCPWM_SELECT_SYNC0 = 4,  /*!<Select SYNC0 as input*/
    MCPWM_SELECT_SYNC1,      /*!<Select SYNC1 as input*/
    MCPWM_SELECT_SYNC2,      /*!<Select SYNC2 as input*/
    MCPWM_SELECT_SYNC_INT0 = 1, //存在しない場合追加
} mcpwm_sync_signal_t;
```

`MCPWM_SELECT_SYNC_INT0`はタイマ0の同期割込みと思われますが詳細はよく分かってません。(動いた実績重視)
似たような名前の `MCPWM_SELECT_SYNC0` では3相同期にすることが出来ませんでした。

### 2024/9/13追記
mcpwm.hを編集せず、キャストを使う方法でもOKでした。(SimpleFOCのコードがそうなっていた)
キャストを使わない場合は

```
mcpwm_sync_enable(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_SELECT_SYNC_INT0, 0);
```
と書きますが、キャストを使う場合は

```
mcpwm_sync_enable(MCPWM_UNIT_0, MCPWM_TIMER_0, (mcpwm_sync_signal_t)1, 0);
```
と書けばOKです。こっちのほうがどう見ても簡単だな…。

## 余談 Arduinoにおけるボードライブラリの格納場所
Arduino IDEが2.0以前においては、IDEにて ファイル→環境設定 を開くこと`preferences.txt` の格納場所を確認でき、ここからボードライブラリの格納場所を追うことが出来ました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302506/e8db0bcf-f42a-2e9a-490f-e83b9a9e23fc.png)

しかしながら、Arduino IDE 2.0 では`preferences.txt`の格納場所を確認できなくなっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302506/e28acb70-af34-dd6f-8e63-e09554f3579f.png)

地味に不便ですね…。

# 2. ３相同期・三角波・相補PWMを打たせ、山割込みする

## 2.1 コードサンプル
Arduino IDEで動作確認済みサンプルを下記に示します。

```c:motorcontrol.ino
#include <Arduino.h>
#include "driver/mcpwm.h"
#include "soc/mcpwm_reg.h"
#include "soc/mcpwm_struct.h"

#define GPIO_PWM0A_OUT 15   //Set GPIO 15 as PWM0A
#define GPIO_PWM0B_OUT 02   //Set GPIO 02 as PWM0B
#define GPIO_PWM1A_OUT 0   //Set GPIO 00 as PWM1A
#define GPIO_PWM1B_OUT 04   //Set GPIO 04 as PWM1B
#define GPIO_PWM2A_OUT 16   //Set GPIO 16 as PWM2A
#define GPIO_PWM2B_OUT 17   //Set GPIO 17 as PWM2B  

mcpwm_dev_t *MCPWM[1] = {&MCPWM0};

// PWM山割込みに同期した関数 「XX」は省略可能(汎用ポインタが使用できない場合のエラー回避として追加したもの)
void IRAM_ATTR isr_handler(void *XX)
{
  MCPWM[MCPWM_UNIT_0]->int_clr.val = MCPWM[MCPWM_UNIT_0]->int_st.val;
  
  digitalWrite(13, HIGH);

  float Duty_u = 20;
  float Duty_v = 30;
  float Duty_w = 50;

  mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, Duty_u);
  mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_B, Duty_u);
  mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_1, MCPWM_OPR_A, Duty_v);
  mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_1, MCPWM_OPR_B, Duty_v);   
  mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_2, MCPWM_OPR_A, Duty_w);
  mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_2, MCPWM_OPR_B, Duty_w);
 
  digitalWrite(13, LOW);

}

void setup() {
  mcpwm_config_t pwm_config;

  pinMode(13, OUTPUT);

  // 3相PWMピンの割付け　Achに対しBchは相補の関係
  mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM0A, GPIO_PWM0A_OUT);
  mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM0B, GPIO_PWM0B_OUT);
  mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM1A, GPIO_PWM1A_OUT);
  mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM1B, GPIO_PWM1B_OUT);
  mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM2A, GPIO_PWM2A_OUT);
  mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM2B, GPIO_PWM2B_OUT);

  // PWMの設定    
  pwm_config.frequency = 20000;         //キャリア周波数 
  pwm_config.cmpr_a = 50.0;             // Duty初期設定(単位：%) 
  pwm_config.cmpr_b = pwm_config.cmpr_a;  // AchとBchを同じ値に設定       
  pwm_config.counter_mode = MCPWM_UP_DOWN_COUNTER;  // 三角波PWM
  pwm_config.duty_mode = MCPWM_DUTY_MODE_0;   // アクティブハイ

  // 設定の反映
  mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_0, &pwm_config);   //Configure PWM0A & PWM0B with above settings
  mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_1, &pwm_config);   //Configure PWM1A & PWM1B with above settings
  mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_2, &pwm_config);   //Configure PWM2A & PWM2B with above settings   

  // 相補PWMのデッドタイム
  mcpwm_deadtime_enable(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_ACTIVE_HIGH_COMPLIMENT_MODE, 8, 8);
  mcpwm_deadtime_enable(MCPWM_UNIT_0, MCPWM_TIMER_1, MCPWM_ACTIVE_HIGH_COMPLIMENT_MODE, 8, 8);   
  mcpwm_deadtime_enable(MCPWM_UNIT_0, MCPWM_TIMER_2, MCPWM_ACTIVE_HIGH_COMPLIMENT_MODE, 8, 8);
    
  // 割り込み設定
  // 関数isr_handlerを割込み関数として設定
  MCPWM[MCPWM_UNIT_0]->int_ena.val = BIT(6);// BIT6~8で山割込み、BIT3~5で谷割込み
  ESP_ERROR_CHECK(mcpwm_isr_register(MCPWM_UNIT_0, isr_handler,
                                     NULL, ESP_INTR_FLAG_IRAM, NULL));
                                       
  // 3相PWMの同期
  mcpwm_sync_enable(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_SELECT_SYNC_INT0, 0);
  mcpwm_sync_enable(MCPWM_UNIT_0, MCPWM_TIMER_1, MCPWM_SELECT_SYNC_INT0, 0);
  mcpwm_sync_enable(MCPWM_UNIT_0, MCPWM_TIMER_2, MCPWM_SELECT_SYNC_INT0, 0); 
    
  MCPWM[MCPWM_UNIT_0]->timer[MCPWM_TIMER_0].sync.out_sel = 1;
  
}

void loop() {
}


```

ESP32へ書き込んだ結果
(黄色：山割込み中にトグルさせている13pin
 青：15pin=タイマ0出力信号、Duty20%
 赤：0pin=タイマ1出力信号、Duty30%
 緑：16pin=タイマ2出力信号、Duty50%)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302506/42632ee0-0399-aa1b-c686-c5fda56a6800.png)

## 2.2 ポイント解説
### 2.2.1 3相同期
1.2にてヘッダファイルに追加した設定 `MCPWM_SELECT_SYNC_INT0` をタイマ0~2の同期設定に適用することで3相同期を実現しています。

```c
// 3相PWMの同期
mcpwm_sync_enable(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_SELECT_SYNC_INT0, 0);
mcpwm_sync_enable(MCPWM_UNIT_0, MCPWM_TIMER_1, MCPWM_SELECT_SYNC_INT0, 0);
mcpwm_sync_enable(MCPWM_UNIT_0, MCPWM_TIMER_2, MCPWM_SELECT_SYNC_INT0, 0); 
    
```

その下の

```
MCPWM[MCPWM_UNIT_0]->timer[MCPWM_TIMER_0].sync.out_sel = 1;
```

は同期の開始のようなものと思いますが、正しい意味はよく分かっていません。(コード削除したら狙った波形が出なくなりました)

### 2.2.2 三角波

ここは単純に

```c
pwm_config.counter_mode = MCPWM_UP_DOWN_COUNTER;  // 三角波PWM
```
だけの設定でOKです。

### 2.2.3 相補PWM
タイマ0~2のPWMにてAchとBchがそれぞれ相補の関係になっているので、ピンアサイン・デッドタイム設定・Duty設定を実施することで相補PWMが出力されます。

<b>注意点</b>
AchのDutyがBchに自動設定されたりはしないので、Duty変更する場合は必ず

```c
mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, Duty_u);
mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_B, Duty_u);
```

のようにAch,Bchに同じDutyを設定しなければなりません。
ちなみに異なるDutyを設定した場合はその通りに出力されるため、<b>場合によっては上下短絡します。</b>不親切設計。

### 2.2.4 山割込み
詳しくはサンプルコード見た方が良いかと思いますが、初期化関数に含まれる

```c
// 割り込み設定
// 関数isr_handlerを割込み関数として設定
MCPWM[MCPWM_UNIT_0]->int_ena.val = BIT(6);// BIT6~8で山割込み、BIT3~5で谷割込み
ESP_ERROR_CHECK(mcpwm_isr_register(MCPWM_UNIT_0, isr_handler,
                                     NULL, ESP_INTR_FLAG_IRAM, NULL));
```

で山割り込みの関数として `isr_handler` を登録しています。
コメントにある通り、BIT6~8で谷割込み、BIT3~5で山割込みを有効化できるようです。

```c
MCPWM[MCPWM_UNIT_0]->int_ena.val = BIT(6) | BIT(7) | BIT(8);
```

のように、6~8すべてのビットを立てた場合も動作としては変わりませんでした。

また、山谷割込みがしたい場合は

```c
MCPWM[MCPWM_UNIT_0]->int_ena.val = BIT(6) | BIT(3);
```

のように記述することで可能となります。

割込み関数 `isr_handler`は

```c
// PWM山割込みに同期した関数 「XX」は省略可能(汎用ポインタが使用できない場合のエラー回避として追加したもの)
void IRAM_ATTR isr_handler(void *XX)
```

にて定義しています。
`IRAM_ATTR` は関数を内部RAMに配置するための設定らしいです。

参考：[ESP32 タイマー割込み（その１）](https://blog.goo.ne.jp/jh7ubc/e/d3a0c2a0df3fbcf751cf28992439be13)


`isr_handler` 関数の冒頭に記載された
```c
MCPWM[MCPWM_UNIT_0]->int_clr.val = MCPWM[MCPWM_UNIT_0]->int_st.val;
```

は「割込みクリアビットに1を立てる処理」とのことで、こいつを書き忘れると変な動作になるので注意が必要です。（よく忘れます）

下記参考URL
https://docs.espressif.com/projects/esp-idf/en/release-v3.3/api-reference/peripherals/esp_slave_protocol.html

# おわりに
色々な意味で、ESP32はモータ制御に向いていないような気がしてならないのですが基本設定だけは出来たので記事として残しておくことにしました。
これからチャレンジされる誰かの助けになれれば幸いです。