理系的な戯れ

理工学系とくにロボットやドローンに関する計算・プログラミング等の話題を扱って、そのようなことに興味がある人たちのお役に立てればと思っております。

Raspberry Pi PicoでPWM出力

Raspberry Pi PicoでPWM出力

はじめに

Raspberry Pi Pico(以下Pico)の心臓部のRP2040ですが、これが単体で売り出されるようです。 趣味の人たちが、これで自作のマイコンボードを作って遊ぶことも考えられます。

今回は、PicoでPWMを出してみたいと思います。 モータ等を制御する際にもPWMは必要なので重要です。

RP2040のPWM

8つのスライスに2つのチャンネル

RP2040のPWMについては公式のデータシート では4章の「Peripherals」の中の5節に記述されています。

RP2040は8つのスライス夫々で2つのPWM信号の出力又は信号の周波数やパルス幅を測定することができます。 16のPWM信号を出したり、調べたりすることができます。

スライスと言う言葉が書かれていて感覚的にはよくわからない用語ですが、 チャンネルと言うと判り易いような気がしますが、RP2040では各スライスの二つの信号をチャンネルと言っているようです.

PWM用カウンタ

PWMを実現するのはカウンタと呼ばれるもので、一定時間ごとに一づつ大きくなります。そのため時計として使えます。 際限なく大きくなるかと言うとそうではなく、数えられる桁数がビット数で決まっています。

RP2040 のPWMカウンタは16ビットなので65535まで数えたら次は0になります.

サンプルプログラム

まずは公式のサンプルプログラムを以下に紹介します。

/**
 * Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

// Output PWM signals on pins 0 and 1

#include "pico/stdlib.h"
#include "hardware/pwm.h"

int main() {
    /// \tag::setup_pwm[]

    // Tell GPIO 0 and 1 they are allocated to the PWM
    gpio_set_function(0, GPIO_FUNC_PWM);
    gpio_set_function(1, GPIO_FUNC_PWM);

    // Find out which PWM slice is connected to GPIO 0 (it's slice 0)
    uint slice_num = pwm_gpio_to_slice_num(0);

    // Set period of 4 cycles (0 to 3 inclusive)
    pwm_set_wrap(slice_num, 3);
    // Set channel A output high for one cycle before dropping
    pwm_set_chan_level(slice_num, PWM_CHAN_A, 1);
    // Set initial B output high for three cycles before dropping
    pwm_set_chan_level(slice_num, PWM_CHAN_B, 3);
    // Set the PWM running
    pwm_set_enabled(slice_num, true);
    /// \end::setup_pwm[]

    // Note we could also use pwm_set_gpio_level(gpio, x) which looks up the
    // correct slice and channel for a given GPIO.
}

上記のプログラムで基本的なPWM信号は出力されます。 ただし、このサンプルでのPWM信号の周波数は無茶苦茶大きくて、 サンプルとしてはもう少しどうにかならなかったものかと思うわけです。

このサンプルでは、システムクロックでカウンタがアップカウントされていきます。 ちなみにシステムクロックは何もしないと125MHzです。

そして

 pwm_set_wrap(slice_num, 3);

と言う行でwrapと言うものに、3を設定しています。

このwrapという値は、カウンタがこの値になったら、カウンタ値を0に戻す値となります。

なので、このサンプルはカウンタが3になったら0に戻ることを繰り返します。

つまり(1÷125000000)×4秒が1周期の時間です。

めちゃくちゃ早くて、オシロで見たら、矩形波には見えませんでした。

これには最初、このサンプルうまくうごいてないのでは?と戸惑いました。

周期とDuty

PWM信号の周期とDuty

PWM信号とは下図のようなものですが、周期とDutyを決めてプログラムにそれを どう表現すればいいのかさえ分かればいいのです。

PWM信号
PWM信号

先ほどの話でwrapを設定すると周期が決められそうだということが判りました。 カウンタの値がwrapに一致するとPWM信号はHighになり、カウンタは0になります。 実はデータシートにはwrapはTOPとも書かれていて、少々混乱しますが、どちらも同じことです。

16ビットカウンタなのでwrapに設定できる最大値は65535です。 これ以上の周期を設定できませんが、カウントアップする周期を変えることで、もう少しフレキシブルになります。

PWMが使うクロックはシステムクロックが元となるのですが、これを割って小さくすることが可能です。

どんな数で割るかをどう設定するかが問題となります。

Dutyの設定については、サンプルプログラムでは、以下のところで行っています。

 pwm_set_chan_level(slice_num, PWM_CHAN_A, 1);

PWM_CHAN_Aに1を設定しています。

これはカウンタが1になったら、PWM信号がLowになることを表しています。

このサンプルの場合はPWM信号の周期は4クロック分で、 1クロック分のDutyの信号となります。

クロックの分割について

常にシステムクロックでカウントアップされてしまうと、 周期に設定できる最大値はだいぶ小さな値になってしまいます。 そこでシステムクロックを割って(分周すると言います。)、小さくする事で、大きな周期を設定する事ができます。

サンプルプログラムには無いのですが、以下の関数によりクロックを分周できます。

pwm_set_clkdiv(slice_num, 100.0);

100.0という数値でクロックを割った値がカウンタをアップカウントするクロックの周波数になります。 システムクロックは125MHzなのでその100分の1の値で割るという事です。

ちなみに、この値の型はfloatです。

この値のとりうる値は次の計算式で計算します。


\begin{eqnarray}
DIV\_INT+ \frac{DIV\_FRAC}{16}
\end{eqnarray}

DIV\_INTの値は0から255までの整数です。

DIV\_FRACの値は0から15までの整数となります。

実は、これらの値を直接指定する関数もありますが、前述の関数を使うと1行で済むので楽だと思います。

周期または周波数の設定

前述した、分割数をclkdivとし、システムクロックをsysclockとしてwrapの値をwrapとすると、周期Tは以下の式で計算できる事になります。


\begin{eqnarray}
T=\frac{(wrap+1) \cdot clkdiv}{sysclock}
\end{eqnarray}

また、周波数fは周期の逆数になるので、以下のようになります。


\begin{eqnarray}
f=\frac{sysclock}{(wrap+1) \cdot clkdiv}
\end{eqnarray}

システムクロックは125MHzです。 PWMの周期を0.02(s)つまり周波数で50Hzに設定しようとすると、複数の設定があると思いますが、例えば以下のようにすると、その値に設定できます。


\begin{eqnarray}
wrap&=&24999\\
clkdiv&=&100
\end{eqnarray}

これを周波数の式に代入すると


\begin{eqnarray}
f&=&\frac{125\times 10^6}{(24999+1) \cdot 100}\\
&=&\frac{125\times 10^6}{25000 \cdot 100}=50
\end{eqnarray}

以上で周期が設定できる。

Dutyの設定

Dutyはパルスの幅のことですが、周期を100%として%で表される事が多いです。

しかし、Dutyを直接時間で指定する場面も多くあるので、ここでは時間として話を進めたいと思います。

Dutyは前述したPWM_CHAN_Aの値できまる。この値とDutyとの関係は


\begin{eqnarray}
Duty&=&\frac{T \cdot PWM\_CHAN\_A}{wrap+1}\\
&=&\frac{(wrap+1) \cdot clkdiv}{sysclock} \frac{PWM\_CHAN\_A}{wrap+1}\\
&=&\frac{clkdiv \cdot PWM\_CHAN\_A}{sysclock}
\end{eqnarray}

以上より、所望のDutyに対して、PWM_CHAN_Aを決定する式は以下のようになる。


\begin{eqnarray}

PWM\_CHAN\_A&=&\frac{Duty  \cdot sysclock}{clkdiv}
\end{eqnarray}

ちなみに、一つのスライスにはAチャンネル、Bチャンネルがあり、 以上はAチャンネルの設定についてで、Bチャンネルの場合は以下のように、PWM_CHAN_Bを設定するとよい。

pwm_set_chan_level(slice_num, PWM_CHAN_B, 1330);

ESCを用いてブラシレスモータを回す

以上で、任意の周期とDutyを設定できるようになります。

ESCとブラシレスモータにPicoをつないで、モータを回してみたいと思います。

ESCは最小のDutyがモータ停止、最大のDutyがモータ最高回転に制御する装置ですが、Dutyの最小値と、最大値は 前もってESCに教えてやる必要があります。これをESCキャリブレーションと言いますが、以下のプログラムは スタート時にESCキャリブレーションの行程が入っています。

  • 電源投入時Dutyを最大にしておく
  • 数秒後、Duty を最小にする

この行程でESCキャリブレーションが終了します。

以下のプログラムはその後、数秒ごとに停止、回転を繰り返します。

以上のことを踏まえてサンプルプログラムを修正したのが以下となります。

#include "pico/stdlib.h"
#include "hardware/pwm.h"

int main() {
    /// \tag::setup_pwm[]

    // Tell GPIO 0 and 1 they are allocated to the PWM
    gpio_set_function(0, GPIO_FUNC_PWM);
    gpio_set_function(1, GPIO_FUNC_PWM);

    // Find out which PWM slice is connected to GPIO 0 (it's slice 0)
    uint slice_num = pwm_gpio_to_slice_num(0);

    // Set period of 0.02s 
    pwm_set_wrap(slice_num, 24999);
    pwm_set_clkdiv(slice_num, 100.0);
    // Set channel A Duty
    pwm_set_chan_level(slice_num, PWM_CHAN_A, 2315);
    // Set initial B Duty
    pwm_set_chan_level(slice_num, PWM_CHAN_B, 1330);
    // Set the PWM running
    pwm_set_enabled(slice_num, true);
    /// \end::setup_pwm[]

    //Start  ESC Calibration
    sleep_ms(2000);
    pwm_set_chan_level(slice_num, PWM_CHAN_A, 1330);
    sleep_ms(5000);
    //End Start  ESC Calibration

    while(true){
      pwm_set_chan_level(slice_num, PWM_CHAN_A, 1600);
      sleep_ms(2000);
      pwm_set_chan_level(slice_num, PWM_CHAN_A, 1330);
      sleep_ms(2000);
    }
}

おわりに

実際にPicoでPWM信号を作り、ESCに入力してブラシレスモータを回している動画を貼りますので よかったらご覧ください。


Raspberry Pi PicoでPWM信号を生成してESCに入力しブラシレスモータを回す それでは、また!