1.はじめに
本記事はESP32に3相同期・三角波・相補PWMを打たせ、山割込みする のArduino UNO R4 wifi版になります。
マイコンボードを取っ替え引っ替え、どいつもこいつもモータ制御が出来るよう仕立てては喜んでいる狂人逸材は希少では? と我ながら思いますが、意外と集めてみると居そうで怖いです。
3相同期、三角波、相補PWM、山割り込みについては下記の記事に詳しく書いているので、何それ食えるの? という人は参照下さい。
・使用したボード:Arduino UNO R4 wifi 純正品
・開発環境:Arduino IDE 2.3.2
サンプルコードを動作させた際の動画例は下記です。
arduino UNO R4 wifi、開発環境:arduino IDE で強制転流できました。相補PWMで問題なく動作してます。 pic.twitter.com/RR2XqVxuNS
— モータ制御マン (@motorcontrolman) October 1, 2024
章構成について、1.1~1.2に渡りポエムを記載していますが、ポエムをすっとばしてコードだけを見たいという方は2. 3相同期・三角波・相補PWMを打たせ、山割込みする まですっ飛ばして下さい。
なお、本記事のサブタイトルは『読もう!マイコンマニュアル ~マイコンを「狙った通りに」動かす~ 』になります。そういう記事をご要望の方はむしろ1.1~1.2を熟読してください。(そっちのほうが著者としても嬉しい)
1.1 (ポエム)Youはなぜそのマイコンを使うの?
Arduino UNO R4 wifiは5000円もする割にクロックが48MHzとモータ制御用途では微妙なスペックになっています。(R3でモータ制御する場合もあるので、それと比べたら贅沢だが…。 詳しいスペックについてはイチケンさんのレビュー記事を参照)
じゃあなんでそんなマイコンボードでモータ制御するのかと聞かれたら、下記の2点に尽きます。
①Simulink Support Pakcegeに対応している(2024b以降)
②FPUを搭載している
①に関しては、2024/10/12現在としてなぜかwifiだけが対応、minimaが対応していないがためにwifiを(しぶしぶ)使っているというのが正直なところです。価格面、JTAGデバッグピンが付いているという点においてどう考えてもminimaのほうが優位ですが、対応してないからしゃーない。
1.2 (ポエム)ESP32じゃだめなの?
冒頭でも挙げた通り、本記事のタイトルをそのままESP32に挿げ替えた記事を過去投稿しています。
そしてESP32も、下記2つの特徴を有しています。
①Simulink Support Pakcegeに対応
②FPUを搭載
しかもクロック周波数は最大240MHzと、Arduino UNO R4 wifiよりも遥かに高いのです。価格面でもESP32は約1500円でUNO R4と比較すると圧倒的なコスパです。
上記からすると、ESP32を選ばない理由はどこにも無いように見えますが両方を試した著者が選ばなかった理由が下記になります。
1.2.1 EPS32のPWM出力機能MCPWMが理解しにくい
MCPWMのマニュアルは全て英語ですが、言語の壁を取り払ったとして理解しやすいような記述になっていないように感じられます。(あくまで主観)
この記事を書いた際は、MCPWMを読んでコードを記載するアプローチではなく動きそうなコードを拾ってくる→自分のやりたい事が出来るようにアレンジするアプローチを取っているのですが、3相同期・三角波の実現はマニュアルから辿り着く事が出来ず、それっぽい設定を入れてみては実機挙動で確認することでなんとか達成しています。
また、達成手段としてはMCPWMのヘッダファイルを編集する必要があった という非常に苦しいもので、正直なところESP32でのモータ制御は余り深入りしたくないというのが当時の感想でした。
一方でのArduino UNO R4(wifi, minima)ですが搭載マイコンであるRenesas RA4M1は日本語マニュアルが公開されています。
このマニュアルが他のマニュアルと比べてどれぐらい読みやすいか、という議論はさておきとして日本語で書いてあるというのは非常に大きい。「読みやすいよ~」という私の投稿に対するXでのリアクションをいくつか紹介。
すごい大事
— 高須正和@ニコ技深センコミュニティ Nico-Tech Shenzhen (@tks) September 14, 2024
日本のメイカー向け産業や、産業がメイカーに目を向けることを絶賛応援しています https://t.co/ow7seLMBSB
これについて思うことある。なんだかんだ石を作った国のマニュアルがあることはとても便利だし学習も早いと思う。(最近中文データシートばかり見ていた) https://t.co/pUeVoIqKq9
— 😸🐱😼♨なむるる♨🙀😽😹🌾 (@namururu) September 13, 2024
マニュアルが読みやすいかどうかなんて大事?という意見もあるかと思いますが、1エンジニアの考えとしては『何やってるんだか良く分からんけど動いている』と『なぜ動いているか明瞭に説明できる』は雲泥の差があります。 特にモータ制御はマイコン下回りのちょっとした設定違いで意図しない動作を招くため、理解しにくいESP32は正直使いたくないというのが率直な感想になります。
(そもそも論としてESP32でモータ制御すんなよという意見もあるかと思いますが、考えないものとする)
1.2.2 Duty設定関数の処理時間がやたらと長い
これもMCPWMを使用している事に基づいた特徴ですが、3相×上下=6chのDuty設定に12usもかかっていました。
duty設定関数を止めたら処理時間がほとんどなくなったので推測通りのようだ。6chのduty設定に12usぐらいかかってるという。
— モータ制御マン (@motorcontrolman) December 16, 2022
ちなみにduty設定はMCPWMという周辺機能を使ってます。duty入力の型がご親切にもfloatなのでこのような結果になっていると思われる。https://t.co/TmCXO1MKLQ pic.twitter.com/D0PYemMFPF
今になって考えると、これではクロック周波数が240MHzもあっても意味が無いですね…。
なお、MCPWMを使わずにレジスタを直接叩くことで大幅に改善されます。MCPWMはレジスタを間接的に叩く機能であるため処理時間が長いのだとは思います。(STM32マイコンにおけるHALライブラリみたいなもんです。こいつも処理時間が長い…)
早速試させて頂きました。
— モータ制御マン (@motorcontrolman) December 21, 2022
PWM0Aと0Bを設定した際の処理時間を実測したところ350nsぐらいでした、元のMCPWMからすると驚きの早さです…。
デッドタイムはMCPWMの標準関数の設定値を引き継いで動いてくれてます。(ありがたい) pic.twitter.com/fZnQSRJmrC
レジスタを直接叩く必要があるのであれば、結局のところMCPWMのマニュアルを見るのではなくEPS32のマニュアルを見る必要があると思いますが…実物見てませんが、あんまり読む気にはならない。
1.2.3 Simulink連携の挙動が怪しい
本件については過去にデータを残し忘れましたが、確かDuty設定関数mcpwm_set_dutyをS-function builderでコールした場合はDutyが正しく出力されていなかったような記憶です。(2年前の話なので既に改良されているかも) いずれにせよ、1.2.1、1.2.2の理由からESP32でのモータ制御はもういいや、と判断。今回晴れてArduino UNO R4 wifiがSimulink Support Pakcageの対象となったため、これを用いたモータ制御環境の立ち上げに着手しています。
2. 3相同期・三角波・相補PWMを打たせ、山割込みする
2.1 ピン配置
RA4M1にはCh0~Ch7が存在し、Ch0,1が32bit、Ch2~7が16bitになります。
PWM出力であれば16bitで十分なため、Ch2~7を用いるものとしますが結論としてはCh2を山割込み用に、Ch3,6,7を相補PMW出力用に用います。選定理由については、下記の一覧表を見れば分かる通り16bitタイマでA相、B相が隣り合っているのはCh3,6,7であったためです。
arduinoでのポート名 | RA4M1でのピン名 | タイマ機能名 | 機能 |
---|---|---|---|
D13 | P102 | GTIOC 2B | 山トリガ出力 |
D12 | P410 | GTIOC 6B | W相下アーム信号 |
D11 | P411 | GTIOC 6A | W相上アーム信号 |
D10 | P103 | GTIOC 2A | |
D9 | P303 | GTIOC 7B | V相下アーム信号 |
D8 | P304 | GTIOC 7A | V相上アーム信号 |
D7 | P112 | GTIOC 3B | U相下アーム信号 |
D6 | P111 | GTIOC 3A | U相上アーム信号 |
D5 | P107 | GTIOC 0A | |
D4 | P106 | GTIOC 0B | 処理負荷計測用 |
D3 | P105 | GTIOC 1A | |
D2 | P104 | GTIOC 1B | 割込み入力ピン |
D1 | P302 | ||
D0 | P301 |
いきなり情報が増えましたが、要するにarduinoの出力ポートではD6,7,8,9,11,12を相補PWM出力用に用いています。
また、D13から山トリガ用信号を出力、D13とD2を物理的に繋ぐことで山割込みを実現させます。D4は処理負荷計測用にON/OFF出力させていますが、これは別に使わなくても良いです。
山割り込みの実現方法はマイコン内部で閉じる方法ももちろんあるかとは思いますが、私のゴールはあくまでもSimulink連携でモータ制御することになるため物理接続での実現となっています。(日本語読みやすいとはいえ、マイコン内部でタイマー割込みで関数コールするのが面倒というのも勿論理由としてある。)
2.2 コードサンプル
Arduino IDEで動作確認済みサンプルを下記に示します。arduinoのスケッチにコピペするだけで良いように作っていますが、環境違いでコンパイル通らなかったらすいません。
折角なので(?)強制転流できる内容にしていますが、モータ接続して電源供給するとモータ回っちゃうので気を付けて下さい。
#define CARRIORCNT 2400 - 1 // クロック周波数48000000 2400で山谷キャリア10kHz
#define Ts 100E-6f
#define DEADTIMECNT 48 // 1us
#define interruptPin 2
#define TWOPI 6.283185307f
#define SQRT_2DIV3 0.816496581f
#define SQRT3_DIV2 0.86602540378f
#include <math.h>
float theta = 0.0f;
void setup() {
pinMode(4, OUTPUT); // D4ピンを出力に設定(処理負荷計測用)
pinMode(interruptPin, INPUT_PULLUP); // D2ピンを入力に設定(山割り込み用)
// 割込み関数を設定
attachInterrupt(digitalPinToInterrupt(interruptPin), interruptDo, RISING);
// GTP16用 モジュールストップ状態を解除
R_MSTP->MSTPCRD_b.MSTPD6 = 0;
// PWM出力用端子の設定
// ピンを出力に設定
R_PFS->PORT[1].PIN[2].PmnPFS_b.PDR = 1;
R_PFS->PORT[1].PIN[11].PmnPFS_b.PDR = 1;
R_PFS->PORT[1].PIN[12].PmnPFS_b.PDR = 1;
R_PFS->PORT[4].PIN[11].PmnPFS_b.PDR = 1;
R_PFS->PORT[4].PIN[10].PmnPFS_b.PDR = 1;
R_PFS->PORT[3].PIN[4].PmnPFS_b.PDR = 1;
R_PFS->PORT[3].PIN[3].PmnPFS_b.PDR = 1;
//周辺選択ビットでGPTを設定
R_PFS->PORT[1].PIN[2].PmnPFS_b.PSEL = 0b00011;
R_PFS->PORT[1].PIN[11].PmnPFS_b.PSEL = 0b00011;
R_PFS->PORT[1].PIN[12].PmnPFS_b.PSEL = 0b00011;
R_PFS->PORT[4].PIN[11].PmnPFS_b.PSEL = 0b00011;
R_PFS->PORT[4].PIN[10].PmnPFS_b.PSEL = 0b00011;
R_PFS->PORT[3].PIN[4].PmnPFS_b.PSEL = 0b00011;
R_PFS->PORT[3].PIN[3].PmnPFS_b.PSEL = 0b00011;
// ポートモード制御ビットで周辺機能に設定
R_PFS->PORT[1].PIN[2].PmnPFS_b.PMR = 1;
R_PFS->PORT[1].PIN[11].PmnPFS_b.PMR = 1;
R_PFS->PORT[1].PIN[12].PmnPFS_b.PMR = 1;
R_PFS->PORT[4].PIN[11].PmnPFS_b.PMR = 1;
R_PFS->PORT[4].PIN[10].PmnPFS_b.PMR = 1;
R_PFS->PORT[3].PIN[4].PmnPFS_b.PMR = 1;
R_PFS->PORT[3].PIN[3].PmnPFS_b.PMR = 1;
// PWMタイマの同期
// GTSTR、GTSTPによるカウンタスタート/ストップを許可
R_GPT2->GTSSR_b.CSTRT = 1;
R_GPT3->GTSSR_b.CSTRT = 1;
R_GPT6->GTSSR_b.CSTRT = 1;
R_GPT7->GTSSR_b.CSTRT = 1;
//GTSTPによるカウンタストップ(全てのタイマでレジスタは共通)
R_GPT2->GTSTP = 0b11001100;
// 搬送波を三角波に設定(三角波PWMモード1、バッファ転送は谷のみ。山谷の場合はPWMモード2を選択)
R_GPT2->GTCR_b.MD = 0b100;
R_GPT3->GTCR_b.MD = 0b100;
R_GPT6->GTCR_b.MD = 0b100;
R_GPT7->GTCR_b.MD = 0b100;
// GTIOCA, GTIOCB端子の出力許可
R_GPT2->GTIOR_b.OBE = 1;
R_GPT3->GTIOR_b.OAE = 1;
R_GPT3->GTIOR_b.OBE = 1;
R_GPT6->GTIOR_b.OAE = 1;
R_GPT6->GTIOR_b.OBE = 1;
R_GPT7->GTIOR_b.OAE = 1;
R_GPT7->GTIOR_b.OBE = 1;
// Dutyのバッファ設定
// 01bでシングルバッファ
R_GPT2->GTBER_b.CCRB = 0b01;
R_GPT3->GTBER_b.CCRA = 0b01;
R_GPT3->GTBER_b.CCRB = 0b01;
R_GPT6->GTBER_b.CCRA = 0b01;
R_GPT6->GTBER_b.CCRB = 0b01;
R_GPT7->GTBER_b.CCRA = 0b01;
R_GPT7->GTBER_b.CCRB = 0b01;
// PWMのHigh,Row設定
// GPT2はBchからGPT3、6、7の山側でトリガ信号を発生させるための設定が必要
R_GPT2->GTIOR_b.GTIOB = 0b10011;
// GPT3、6、7は相補PWM実現のためAchとBchで別の設定が必要
R_GPT3->GTIOR_b.GTIOA = 0b00011;
R_GPT3->GTIOR_b.GTIOB = 0b10011;
R_GPT6->GTIOR_b.GTIOA = 0b00011;
R_GPT6->GTIOR_b.GTIOB = 0b10011;
R_GPT7->GTIOR_b.GTIOA = 0b00011;
R_GPT7->GTIOR_b.GTIOB = 0b10011;
// キャリア周波数設定
R_GPT2->GTPR = CARRIORCNT;
R_GPT3->GTPR = CARRIORCNT;
R_GPT6->GTPR = CARRIORCNT;
R_GPT7->GTPR = CARRIORCNT;
// デッドタイム自動設定
R_GPT3->GTDTCR_b.TDE = 1;
R_GPT6->GTDTCR_b.TDE = 1;
R_GPT7->GTDTCR_b.TDE = 1;
//デッドタイム設定
R_GPT6->GTDVU = DEADTIMECNT;
R_GPT7->GTDVU = DEADTIMECNT;
R_GPT3->GTDVU = DEADTIMECNT;
// Duty設定
R_GPT2->GTCCR[3] = 10; // GPT2はBchを使用、かつバッファ設定のため[3]にDutyを設定
R_GPT6->GTCCR[2] = CARRIORCNT - 1000; // バッファ設定のため[2]にDutyを設定 山谷を逆として扱うため引き算にする
R_GPT7->GTCCR[2] = CARRIORCNT - 1000;
R_GPT3->GTCCR[2] = CARRIORCNT - 1000;
// カウンタリセット
R_GPT2->GTCNT = 0;
R_GPT3->GTCNT = 0;
R_GPT6->GTCNT = 0;
R_GPT7->GTCNT = 0;
//GTSTRによるカウンタスタート(全てのタイマでレジスタは共通)
R_GPT2->GTSTR = 0b11001100;
}
void interruptDo(){
float omega_ref = 400.0f;
float sinTheta;
float cosTheta;
float dq[2];
float ab[2];
float uvw[3];
float Duty[3];
uint16_t CNT[3];
float twoDivVdc = 0.2f; // Vdc = 10V
R_PORT1->PODR_b.PODR6 = 1;
//Calc Theta
theta = theta + omega_ref * Ts;
if(theta > TWOPI) theta = 0.0f;
sinTheta = sinf(theta);
cosTheta = cosf(theta);
// Calc Voltage
dq[0] = 0.0f;
dq[1] = 0.5f;
ab[0] = dq[0] * cosTheta - dq[1] * sinTheta;
ab[1] = dq[0] * sinTheta + dq[1] * cosTheta;
uvw[0] = SQRT_2DIV3 * ab[0];
uvw[1] = SQRT_2DIV3 * ( -0.5f * ab[0] + SQRT3_DIV2 * ab[1] );
uvw[2] = - uvw[0] - uvw[1];
// 50% Center
Duty[0] = uvw[0] * twoDivVdc + 0.5f;
Duty[1] = uvw[1] * twoDivVdc + 0.5f;
Duty[2] = uvw[2] * twoDivVdc + 0.5f;
CNT[0] = uint16_t( Duty[0] * (float)CARRIORCNT );
CNT[1] = uint16_t( Duty[1] * (float)CARRIORCNT );
CNT[2] = uint16_t( Duty[2] * (float)CARRIORCNT );
// 反転処理
R_GPT3->GTCCR[2] = CARRIORCNT - CNT[0];
R_GPT7->GTCCR[2] = CARRIORCNT - CNT[1];
R_GPT6->GTCCR[2] = CARRIORCNT - CNT[2];
R_PORT1->PODR_b.PODR6 = 0;
}
void loop() {
}
2.3 コード解説
コード内にもコメントは付けているため、必要な部分だけに絞って。
2.3.1 PWMの使用
サンプルコードでは同様の設定を7ピン分行っていますが、PWMの使用に必要な設定を1ピン分だけ抽出すると下記になります。
// GTP16用 モジュールストップ状態を解除
R_MSTP->MSTPCRD_b.MSTPD6 = 0;
// PWM出力用端子の設定
// ピンを出力に設定
R_PFS->PORT[1].PIN[2].PmnPFS_b.PDR = 1;
//周辺選択ビットでGPTを設定
R_PFS->PORT[1].PIN[2].PmnPFS_b.PSEL = 0b00011;
// ポートモード制御ビットで周辺機能に設定
R_PFS->PORT[1].PIN[2].PmnPFS_b.PMR = 1;
2.3.2 相補PWMのための設定
下記にGPT163に対する相補PWMの設定のみを抽出したコードを示します。
// PWMのHigh,Row設定
R_GPT3->GTIOR_b.GTIOA = 0b00011;
R_GPT3->GTIOR_b.GTIOB = 0b10011;
// デッドタイム自動設定
R_GPT3->GTDTCR_b.TDE = 1;
//デッドタイム設定
R_GPT3->GTDVU = DEADTIMECNT;
//Duty設定(シングルバッファ)
R_GPT3->GTCCR[2] = 1000;
デッドタイム自動設定 R_GPT3->GTDTCR_b.TDE = 1
を用いることで、AchのみにDutyを設定することでBch側のDutyが自動設定されます。ここに関しては便利な機能ですが、相補PWMには勝手にしてくれません。(ハード側で反転される可能性もあるからね…)
どこで相補にしているかというと、R_GPT3->GTIOR_b.GTIOA
および R_GPT3->GTIOR_b.GTIOB
の設定で相補にしています。なお、GTIOBの設定を省略したところBchからDuty出力自体がされなくなりました。不親切!
Achの設定(初期出力はLow)
Bchの設定(初期出力はHigh)
Achに対しBchを反転しているというより、初期出力をAchとBchでHigh/Lowと相補とすることで結果的に相補PWMを実現しているという感じです。
上記設定をすることで得られる、デッドタイム自動設定によるAch、Bch出力端子波形としては下記になります。
…あれ、なんか変じゃねーかこの波形!?
2.3.3 出力波形の何が変か & 対策方法
過去記事にも書いた通り、私の考える搬送波と出力波の関係は下記になります。
すなわち、Duty指令が搬送波を上回る場合はON出力、下回る場合はOFF出力になります。上記のマイコンマニュアルから抜粋した図22.41では、これとは逆になっています。
では、R_GPT3->GTIOR_b.GTIOA
および R_GPT3->GTIOR_b.GTIOB
の設定を逆にすれば良いのかというとこれではダメです。何がダメかというと、Achに対しBchのレジスタはデッドタイム分だけ減算しているため、見事に上下アームショートが発生します。
減算は下記緑部分に記載されています。R_GPT3->GTIOR_b.GTIOA = 0b10011
、R_GPT3->GTIOR_b.GTIOA = 0b00011
に設定した場合の出力波形をそれぞれ赤線・青線で、上下アームショート区間をピンクにて示しています。
じゃあどうすれば良いかというと、カウンタ値の設定時に、カウンタ最大値から元のカウンタ設定値を減算した値を設定します。すなわち、Duty指令に100カウントを設定したい場合は
R_GPT3->GTCCR[2] = CARRIORCNT - 100;
のように設定すれば良いのです。
2.3.4 バッファ設定
バッファって何? というと、レジスタ値を即時反映させず、所望のタイミングに合わせて反映させる事を言っています。下記の図が分かりやすいですね。
図からもある程度読み取れますが、バッファを用いる場合、Achのカウンタ値を直接GTCCRAレジスタに設定せず、GTCCRCレジスタに設定します。
GTCCRAレジスタはGTCCR[0]
、GTCCRCレジスタはGTCCR[2]
で設定するため、バッファを用いている場合はサンプルコードのようにGTCCR[2]
にDutyを設定する必要があります。(GTCCR[0]
は自動反映されるためコード内での設定は不要)
なお備考ですが、デッドタイム自動設定の場合、BchのDutyはキャリア谷で設定されるためAchの書き込み時にバッファを用いない場合は想定通りの動作を得ることが出来ません。(実際問題、著者の環境ではモータから異音が生じました)
おわりに
いつも通り、サンプルコードだけ書く内容にするつもりが大作になってしまった。解説するところが多すぎるからしょうがないというか、解説してて気になったところを深堀していく性分なのでどうしようもないですね。(ただ、そのぶん自分の勉強になっている側面が大きいので満更でもない)