こんにちは、shotaです。
前回の記事ではMCPWM機能でモータを回す方法について書きました。
今回はその続きで、MCPWMの周波数とデューティ比の関係について書きます。
ESP-IDFのソースコードも読むので、普段より奥が深い内容です。
ブロクで書いたサンプルコードはGitHubに公開しています。
https://github.com/ShotaAk/especial/tree/master/examples
モータドライブプログラムの振り返り
まず前回作成したソースコードをもう一度見てみましょう。
ソースコードの全体はこちらからも閲覧できます。
--- 省略 ---
// ----- MCPWMの設定 -----
// GPIOの割り当て
mcpwm_gpio_init(UNIT, SIGNAL_PH[LEFT], GPIO_PH[LEFT]);
mcpwm_gpio_init(UNIT, SIGNAL_EN[LEFT], GPIO_EN[LEFT]);
mcpwm_gpio_init(UNIT, SIGNAL_PH[RIGHT], GPIO_PH[RIGHT]);
mcpwm_gpio_init(UNIT, SIGNAL_EN[RIGHT], GPIO_EN[RIGHT]);
// MCPWMの詳細設定
// frequencyの値によりdutyの分解能が変わるので注意すること
// frequency = 10kHz -> 1%刻みのduty
// frequency = 100kHz -> 10%刻みのduty
mcpwm_config_t pwm_config;
pwm_config.frequency = 10*1000; // PWM周波数= 10kHz,
pwm_config.cmpr_a = 0; // デューティサイクルの初期値(0%)
pwm_config.cmpr_b = 0; // デューティサイクルの初期値(0%)
pwm_config.counter_mode = MCPWM_UP_COUNTER;
pwm_config.duty_mode = DUTY_MODE; // アクティブハイ
mcpwm_init(UNIT, TIMER[LEFT], &pwm_config);
mcpwm_init(UNIT, TIMER[RIGHT], &pwm_config);
MCPWMの詳細設定のところで、PWM周波数(frequency)について触れていますね。
また、PWM周波数は10kHzに設定しています。
では、PWM周波数を変更すると一体どうなるのでしょうか?
PWM周波数を10倍の100kHzにしてみた
次のようにコードを編集してPWM周波数を100kHzにします。
// MCPWMの詳細設定
// frequencyの値によりdutyの分解能が変わるので注意すること
// frequency = 10kHz -> 1%刻みのduty
// frequency = 100kHz -> 10%刻みのduty
mcpwm_config_t pwm_config;
pwm_config.frequency = 100*1000; // PWM周波数= 100kHz,
pwm_config.cmpr_a = 0; // デューティサイクルの初期値(0%)
pwm_config.cmpr_b = 0; // デューティサイクルの初期値(0%)
出力のコードは変更しません。
1%きざみで、デューティ比を0%→50%、50%→0%に変化させます。
const int WAIT_TIME_MS = 100;
const int DUTY_STEP = 1;
const int DUTY_MAX = 50;
const int DUTY_MIN = 0;
// モータON
gpio_set_level(GPIO_NSLEEP, 1);
while (1) {
for(int go_back=0; go_back<=1; go_back++){
// 加速
for(int duty=DUTY_MIN; duty<DUTY_MAX; duty+=DUTY_STEP){
motor_drive(LEFT, duty, go_back);
motor_drive(RIGHT, duty, go_back);
vTaskDelay(WAIT_TIME_MS / portTICK_RATE_MS);
}
// 減速
for(int duty=DUTY_MAX; duty>=DUTY_MIN; duty-=DUTY_STEP){
motor_drive(LEFT, duty, go_back);
motor_drive(RIGHT, duty, go_back);
vTaskDelay(WAIT_TIME_MS / portTICK_RATE_MS);
}
}
}
それでは、ビルドして実行してみましょう。
ん?????何おかしいですね。。。
わかりにくい人は音を出して再生してみてください。
1%きざみでデューティ比が変化するはずなのに、もっと大きな段階で変化しているようです。
10%きざみで変化しているように見えます。
このように、MCPWM機能ではPWM周波数がデューティ比の分解能に影響します。
いったいどういう実装になっているのか調べてみましょう。
PWM周波数とデューティ比の関係を調べる
ここからはESP-IDFのソースコードを読み解いて調査します。
マニアックな話になるのでご注意ください。
実際にセットされた周波数とデューティ比を確認する
まずは本当に周波数が100kHzになったのか、デューティ比が1%きざみで変化しているのか確認しましょう。
確認にはMCPWMのmcpwm_get_frequency関数と、mcpwm_get_duty関数を使います。
(オシロスコープで出力波形を確認するのもおすすめです)
それぞれどんな関数なのかは公式マニュアルに記載されています。
次のようにコードを編集しました。
mcpwm_init関数の後にmcpwm_get_frequencyとprintfを追加しました。
mcpwm_init(UNIT, TIMER[LEFT], &pwm_config);
mcpwm_init(UNIT, TIMER[RIGHT], &pwm_config);
// セットされた周波数を確認
uint32_t freq_l = mcpwm_get_frequency(UNIT, TIMER[LEFT]);
uint32_t freq_r = mcpwm_get_frequency(UNIT, TIMER[RIGHT]);
printf("周波数LEFT : %d Hz\n",freq_l);
printf("周波数RIGHT: %d Hz\n",freq_r);
実行した結果がこちらです。
$ make flash monitor --- 省略 --- I (68) cpu_start: Starting scheduler on PRO CPU. I (0) cpu_start: Starting scheduler on APP CPU. I (70) gpio: GPIO[33]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 周波数LEFT : 100000 Hz 周波数RIGHT: 100000 Hz
100kHzがセットされていますね。問題なさそうです。
つづいて、デューティ比を確認してみましょう。
次のようにコードを編集しました。
正転時だけデューティ比を表示します。
static void drive_forward(mcpwm_timer_t timer_num , float duty)
{
mcpwm_set_signal_low(UNIT, timer_num, OPR_PH);
mcpwm_set_duty(UNIT, timer_num, OPR_EN, duty);
// set_signal_low/highを実行した後は、毎回set_duty_typeを実行すること
mcpwm_set_duty_type(UNIT, timer_num, OPR_EN, DUTY_MODE);
// セットされたデューティ比を確認
float configured_duty = mcpwm_get_duty(UNIT, timer_num, OPR_EN);
printf("タイマー:%d, デューティ比:%.3f%%\n",timer_num, configured_duty);
}
実行結果がこちらです。
--- 省略 --- I (68) cpu_start: Starting scheduler on PRO CPU. I (0) cpu_start: Starting scheduler on APP CPU. I (71) gpio: GPIO[33]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 周波数LEFT : 100000 Hz 周波数RIGHT: 100000 Hz タイマー:0, デューティ比:0.000% タイマー:1, デューティ比:0.000% タイマー:0, デューティ比:0.000% タイマー:1, デューティ比:0.000% タイマー:0, デューティ比:0.000% タイマー:1, デューティ比:0.000% タイマー:0, デューティ比:0.000% タイマー:1, デューティ比:0.000% タイマー:0, デューティ比:0.000% タイマー:1, デューティ比:0.000% タイマー:0, デューティ比:10.000% タイマー:1, デューティ比:10.000% タイマー:0, デューティ比:10.000% タイマー:1, デューティ比:10.000% タイマー:0, デューティ比:10.000% タイマー:1, デューティ比:10.000% タイマー:0, デューティ比:10.000% タイマー:1, デューティ比:10.000% タイマー:0, デューティ比:10.000% タイマー:1, デューティ比:10.000% タイマー:0, デューティ比:10.000% タイマー:1, デューティ比:10.000% タイマー:0, デューティ比:10.000% タイマー:1, デューティ比:10.000% タイマー:0, デューティ比:10.000% タイマー:1, デューティ比:10.000% タイマー:0, デューティ比:10.000% タイマー:1, デューティ比:10.000% タイマー:0, デューティ比:10.000% タイマー:1, デューティ比:10.000% タイマー:0, デューティ比:20.000% タイマー:1, デューティ比:20.000% タイマー:0, デューティ比:20.000% タイマー:1, デューティ比:20.000% タイマー:0, デューティ比:20.000% タイマー:1, デューティ比:20.000% タイマー:0, デューティ比:20.000% タイマー:1, デューティ比:20.000% タイマー:0, デューティ比:20.000% タイマー:1, デューティ比:20.000% タイマー:0, デューティ比:20.000% タイマー:1, デューティ比:20.000% タイマー:0, デューティ比:20.000% タイマー:1, デューティ比:20.000% タイマー:0, デューティ比:20.000% タイマー:1, デューティ比:20.000% タイマー:0, デューティ比:20.000% タイマー:1, デューティ比:20.000% タイマー:0, デューティ比:20.000% タイマー:1, デューティ比:20.000% タイマー:0, デューティ比:30.000% --- 省略 ---
タイマー0が左側モータドライバへの出力で、タイマー1が右側モータドライバへの出力です。
やはり10%きざみでデューティ比が変化しています。
なぜ100kHzのとき10%きざみになるのか調査
PWM周波数とデューティ比の謎について調査します!
まずは、mcpwm_get_duty関数が何を返しているのか、ソースコードを読んでみましょう。
ESP-IDFのソースコードはGitHubにあるので、そこからMCPWMライブラリのコードを探します。
float mcpwm_get_duty(mcpwm_unit_t mcpwm_num, mcpwm_timer_t timer_num, mcpwm_operator_t op_num)
{
float duty;
MCPWM_CHECK(mcpwm_num < MCPWM_UNIT_MAX, MCPWM_UNIT_NUM_ERROR, ESP_ERR_INVALID_ARG);
MCPWM_CHECK(timer_num < MCPWM_TIMER_MAX, MCPWM_TIMER_ERROR, ESP_ERR_INVALID_ARG);
MCPWM_CHECK(op_num < MCPWM_OPR_MAX, MCPWM_OP_ERROR, ESP_ERR_INVALID_ARG);
portENTER_CRITICAL(&mcpwm_spinlock);
duty = 100.0 * (MCPWM[mcpwm_num]->channel[timer_num].cmpr_value[op_num].cmpr_val) / (MCPWM[mcpwm_num]->timer[timer_num].period.period);
portEXIT_CRITICAL(&mcpwm_spinlock);
return duty;
}
この関数内の下記の項目でdutyを計算し、returnしていますね。
duty = 100.0 * (MCPWM[mcpwm_num]->channel[timer_num].cmpr_value[op_num].cmpr_val) / (MCPWM[mcpwm_num]->timer[timer_num].period.period);
return duty;
この式だけで見ても、何を計算しているのか不明です。
次に、下記2つの変数がどこで設定されているのかを見てみましょう。
cmpr_valはmcpwm_set_duty関数内で設定されています。
esp_err_t mcpwm_set_duty(mcpwm_unit_t mcpwm_num, mcpwm_timer_t timer_num, mcpwm_operator_t op_num, float duty)
{
uint32_t set_duty;
MCPWM_CHECK(mcpwm_num < MCPWM_UNIT_MAX, MCPWM_UNIT_NUM_ERROR, ESP_ERR_INVALID_ARG);
MCPWM_CHECK(timer_num < MCPWM_TIMER_MAX, MCPWM_TIMER_ERROR, ESP_ERR_INVALID_ARG);
MCPWM_CHECK(op_num < MCPWM_OPR_MAX, MCPWM_OP_ERROR, ESP_ERR_INVALID_ARG);
portENTER_CRITICAL(&mcpwm_spinlock);
set_duty = (MCPWM[mcpwm_num]->timer[timer_num].period.period) * (duty) / 100;
MCPWM[mcpwm_num]->channel[timer_num].cmpr_value[op_num].cmpr_val = set_duty;
MCPWM[mcpwm_num]->channel[timer_num].cmpr_cfg.a_upmethod = BIT(0);
MCPWM[mcpwm_num]->channel[timer_num].cmpr_cfg.b_upmethod = BIT(0);
portEXIT_CRITICAL(&mcpwm_spinlock);
return ESP_OK;
}
この関数内の下記の項目で、cmpr_valが設定されています。
uint32_t set_duty;
set_duty = (MCPWM[mcpwm_num]->timer[timer_num].period.period) * (duty) / 100;
MCPWM[mcpwm_num]->channel[timer_num].cmpr_value[op_num].cmpr_val = set_duty;
get_duty関数と似たような計算をしていますね。
後はMCPWM[mcpwm_num]->timer[timer_num].period.periodが分かれば計算できそうです。
今度はmcpwm_set_frequency関数を見ます。
mcpwm_set_frequency関数は初登場の関数ですが、mcpwm_init関数内で呼ばれているので、実は毎回使っていた関数なのです。
esp_err_t mcpwm_init(mcpwm_unit_t mcpwm_num, mcpwm_timer_t timer_num, const mcpwm_config_t *mcpwm_conf)
{
MCPWM_CHECK(mcpwm_num < MCPWM_UNIT_MAX, MCPWM_UNIT_NUM_ERROR, ESP_ERR_INVALID_ARG);
MCPWM_CHECK(timer_num < MCPWM_TIMER_MAX, MCPWM_TIMER_ERROR, ESP_ERR_INVALID_ARG);
periph_module_enable(PERIPH_PWM0_MODULE + mcpwm_num);
portENTER_CRITICAL(&mcpwm_spinlock);
MCPWM[mcpwm_num]->clk_cfg.prescale = MCPWM_CLK_PRESCL;
mcpwm_set_frequency(mcpwm_num, timer_num, mcpwm_conf->frequency);
--- 省略 ---
それでは、mcpwm_set_frequency関数を見てみましょう
esp_err_t mcpwm_set_frequency(mcpwm_unit_t mcpwm_num, mcpwm_timer_t timer_num, uint32_t frequency)
{
uint32_t mcpwm_num_of_pulse;
uint32_t previous_period;
uint32_t set_duty_a, set_duty_b;
MCPWM_CHECK(mcpwm_num < MCPWM_UNIT_MAX, MCPWM_UNIT_NUM_ERROR, ESP_ERR_INVALID_ARG);
MCPWM_CHECK(timer_num < MCPWM_TIMER_MAX, MCPWM_TIMER_ERROR, ESP_ERR_INVALID_ARG);
portENTER_CRITICAL(&mcpwm_spinlock);
mcpwm_num_of_pulse = MCPWM_CLK / (frequency * (TIMER_CLK_PRESCALE + 1));
previous_period = MCPWM[mcpwm_num]->timer[timer_num].period.period;
MCPWM[mcpwm_num]->timer[timer_num].period.prescale = TIMER_CLK_PRESCALE;
MCPWM[mcpwm_num]->timer[timer_num].period.period = mcpwm_num_of_pulse;
MCPWM[mcpwm_num]->timer[timer_num].period.upmethod = 0;
set_duty_a = (((MCPWM[mcpwm_num]->channel[timer_num].cmpr_value[0].cmpr_val) * mcpwm_num_of_pulse) / previous_period);
set_duty_b = (((MCPWM[mcpwm_num]->channel[timer_num].cmpr_value[1].cmpr_val) * mcpwm_num_of_pulse) / previous_period);
MCPWM[mcpwm_num]->channel[timer_num].cmpr_value[0].cmpr_val = set_duty_a;
MCPWM[mcpwm_num]->channel[timer_num].cmpr_value[1].cmpr_val = set_duty_b;
MCPWM[mcpwm_num]->channel[timer_num].cmpr_cfg.a_upmethod = 0;
MCPWM[mcpwm_num]->channel[timer_num].cmpr_cfg.b_upmethod = 0;
portEXIT_CRITICAL(&mcpwm_spinlock);
return ESP_OK;
}
この関数内の下記の項目でMCPWM[mcpwm_num]->timer[timer_num].period.periodが計算されています。
mcpwm_num_of_pulse = MCPWM_CLK / (frequency * (TIMER_CLK_PRESCALE + 1));
MCPWM[mcpwm_num]->timer[timer_num].period.period = mcpwm_num_of_pulse;
また新しいパラメータが出てきましたね。。。
これらのパラメータは、同ファイル(driver/mcpwm.c)の上部で定義されています。
#define MCPWM_BASE_CLK (2 * APB_CLK_FREQ) //2*APB_CLK_FREQ 160Mhz #define MCPWM_CLK_PRESCL 15 //MCPWM clock prescale #define TIMER_CLK_PRESCALE 9 //MCPWM timer prescales #define MCPWM_CLK (MCPWM_BASE_CLK/(MCPWM_CLK_PRESCL +1))
またまた新しいパラメータがでてきました。。。。
まずは、TIMER_CLK_PRESCALEについて。
これは9と定義されていますね。
MCPWM_CLKはMCPWM_BASE_CLK/(MCPWM_CLK_PRESCL +1)と定義されています。
MCPWM_CLK_PRESCLは15と定義されています。
MCPWM_BASE_CLKは2 * APB_CLK_FREQと定義されいます。
またまたまた新しいパラメータAPB_CLK_FREQが出てきました。
APB_CLK_FREQはsoc.hで定義されています。
//Periheral Clock {{
#define APB_CLK_FREQ_ROM ( 26*1000000 )
#define CPU_CLK_FREQ_ROM APB_CLK_FREQ_ROM
#define CPU_CLK_FREQ APB_CLK_FREQ
#define APB_CLK_FREQ ( 80*1000000 ) //unit: Hz
APB_CLK_FREQ は 80*1000000 Hz -> 80 MHz と定義されています。
なぜ100kHzのとき10%きざみになるのか、答え
これで全てがそろいました。もう一度パラメータの定義をたどっていきましょう。
// soc.hよりAPB_CLKの定義
APB_CLK_FREQ = 80*1000000 Hz = 80 MHz
// mcpwm.cよりMCPWM_CLKの計算
MCPWM_BASE_CLK = 2 * APB_CLK_FREQ
= 160 MHz
MCPWM_CLK_PRESCL = 15
MCPWM_CLK = MCPWM_BASE_CLK/(MCPWM_CLK_PRESCL +1) = 160 MHz / (15 + 1)
= 10 MHz
// mcpwm_set_frequencyよりMCPWM[mcpwm_num]->timer[timer_num].period.periodの計算
TIMER_CLK_PRESCALE = 9
mcpwm_num_of_pulse = MCPWM_CLK / (frequency * (TIMER_CLK_PRESCALE + 1))
= 10 MHz / (freq * (9 + 1))
= 1 MHz / freq
MCPWM[mcpwm_num]->timer[timer_num].period.period = mcpwm_num_of_pulse
= 1 MHz / freq
// mcpwm_set_dutyよりMCPWM[mcpwm_num]->channel[timer_num].cmpr_value[op_num].cmpr_valの計算
set_duty = (MCPWM[mcpwm_num]->timer[timer_num].period.period) * (duty) / 100
= (1 MHz / freq) * duty / 100
MCPWM[mcpwm_num]->channel[timer_num].cmpr_value[op_num].cmpr_val = set_duty
= (1 MHz / freq) * duty / 100
ようやく周波数とデューティの関係式が出てきました。
mcpwm_num_of_pulseがPWMの1周期で発生するパルス数です。これは1 MHz / freqで求まります。
cmpr_valがPWM波形のON/OFFを切り替えるタイミングのパルス数です。
例えば、1周期100パルス(num_of_pulse=100)のPWM信号で、デューティ比30%のPWM信号を設定したいとき、
cmpr_valは30パルスとなります。
つまり、mcpwm_num_of_pulseがPWM信号の分解能なのです。
PWM周波数を10kHzにしたとき、mcpwm_num_of_pulseは 1MHz/10kHz = 100パルスとなり、
1 %きざみでデューティ比を設定できます。
PWM周波数を100kHzにしたときは、mcpwm_num_of_pulseが 1MHz/100kHz = 10パルスになるため、
10 %きざみでしかデューティ比を設定できません。
やっと答えが見つかりましたね。
実装はわかったけど、そもそも仕様はどうなってるの?
今回はソースコードを読んで、MCPWMの周波数とデューティの関係について理解しました。
しかし本来は、実装の前に仕様があるはずです。
マニュアルかデータシートに書かれているはずです。調べてみましょう。
参考にする資料はESP32 Technical Reference Manualです。
このTechinical Reference ManualにはESP32のメモリとペリフェラル(ADCやPWM等の周辺機能)の使い方が詳しく書かれています。
ESP32のレジスタを直接叩きたい人はこのマニュアルを読んでみましょう。
APB_CLKってなに?
マニュアルの3.2 System ClockにあるFigure 6を見てみます。
APB_CLKはAPB GENブロックから生成され、Peri(ペリフェラル)に入力されていることがわかります。
また、APB GENブロックにはCPU_CLKが入力されていることもわかります。
また、16. MCPWMの16.3.1.1 Prescaler Submoduleを見ると、160MHzのクロックがプリスケーラに入力され、PWM_CLKが生成されていることもわかります。
う〜〜〜〜〜ん、、、、、しんどくなってきましたね。。。
この辺にしておきましょう。。。。(私もつらくなりました。)
ESP-IDFのコードを書き換える
ここまで読むと、「ESP-IDFのコードを書き換えたら、100kHzのPWM周波数で、1%きざみでデューティ比を変化できるんじゃね?」と思うかもしれません。
やってみましょう!!!
まず、ESP-IDFのコードを編集します。
# ESP-IDFのmcwpm.cを編集 # 私はテキストエディタにVimを使っています。 $ vim $IDF_PATH/components/driver/mcpwm.c
次のようにTIMER_CLK_PRESCALEを9から0に書き換えましょう。
#define MCPWM_BASE_CLK (2 * APB_CLK_FREQ) //2*APB_CLK_FREQ 160Mhz #define MCPWM_CLK_PRESCL 15 //MCPWM clock prescale #define TIMER_CLK_PRESCALE 0 //MCPWM timer prescales
モータドライバのプロジェクトに移動し、ビルド、書き込み、シリアルモニタを表示します。
$ cd ~/esp/especial/examples/4_motor_driver $ make flash monitor --- 省略 --- I (0) cpu_start: Starting scheduler on APP CPU. I (72) gpio: GPIO[33]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 周波数LEFT : 100000 Hz 周波数RIGHT: 100000 Hz タイマー:0, デューティ比:5.000% タイマー:1, デューティ比:5.000% タイマー:0, デューティ比:6.000% タイマー:1, デューティ比:6.000% タイマー:0, デューティ比:7.000% タイマー:1, デューティ比:7.000% タイマー:0, デューティ比:8.000% タイマー:1, デューティ比:8.000% タイマー:0, デューティ比:9.000% タイマー:1, デューティ比:9.000% タイマー:0, デューティ比:10.000% タイマー:1, デューティ比:10.000% タイマー:0, デューティ比:11.000% タイマー:1, デューティ比:11.000% タイマー:0, デューティ比:12.000% タイマー:1, デューティ比:12.000% タイマー:0, デューティ比:13.000% タイマー:1, デューティ比:13.000% タイマー:0, デューティ比:14.000% タイマー:1, デューティ比:14.000% タイマー:0, デューティ比:15.000% タイマー:1, デューティ比:15.000% タイマー:0, デューティ比:16.000% タイマー:1, デューティ比:16.000% タイマー:0, デューティ比:17.000% タイマー:1, デューティ比:17.000% タイマー:0, デューティ比:18.000% タイマー:1, デューティ比:18.000% タイマー:0, デューティ比:19.000% タイマー:1, デューティ比:19.000% タイマー:0, デューティ比:20.000% タイマー:1, デューティ比:20.000% タイマー:0, デューティ比:21.000% タイマー:1, デューティ比:21.000% タイマー:0, デューティ比:22.000% タイマー:1, デューティ比:22.000% タイマー:0, デューティ比:23.000% タイマー:1, デューティ比:23.000% タイマー:0, デューティ比:24.000% タイマー:1, デューティ比:24.000% タイマー:0, デューティ比:25.000% タイマー:1, デューティ比:25.000% タイマー:0, デューティ比:26.000% タイマー:1, デューティ比:26.000% タイマー:0, デューティ比:27.000% タイマー:1, デューティ比:27.000% タイマー:0, デューティ比:28.000% タイマー:1, デューティ比:28.000% タイマー:0, デューティ比:29.000% タイマー:1, デューティ比:29.000% タイマー:0, デューティ比:30.000% タイマー:1, デューティ比:30.000%
100kHzかつ、1%きざみでデューティ比を変化できました。
モータを回した動画はこちらです。
こちらは前回の10kHzでモータを回したときの動画です。
(違いがありますかね?)
次回の記事
モータを回せたので、次はエンコーダの値を読み取ってみましょう。



