SSブログ

macOSからPi Picoを使う - その4 [Pi Pico]

うまくいけば仕事と趣味とで20台近くあるRaspberry Piのうち半分はPi Picoに置き換えられそうな気がしてきた。僕にとってはモデルとしてはPi PicoとPi 4Bの両端があればいい、ということになりそう。あとはそれぞれが順にパフォーマンスを上げてくれればありがたい....

7  PWM

Pi PicoにはA/DはあるけどD/Aはない。A/DがENOB=9.5なのでそれと同程度でいいとすればPWMのローパスでいいじゃん、ということなんだろう。それに応えようとしているのか、本家より高いベースクロックが設定されている。

7.1  PWMのハードウェア

データシートの中でスライス(slice)と呼んでいるPWMブロックが8つあって、それぞれが2つの出力を持っている。データシートの中にあるひとつのスライスのブロックダイアグラムがこれ
0520pwmblock.png
カウンタは16ビットで、アップカウントとアップダウンができる、つまりノコギリ状と三角波とができる、レジスタがダブルバッファになっていてデューティなどを変えたときにグリッチがでない、などとデータシートには書いてある。

16ビットカウンタはTOPというレジスタにある値までクロックをカウントしていく。TOPの値になると0に戻ってカウントを繰り返す。CCレジスタというのがあって、カウンタの値とCCレジスタの値を比較してCCの方が大きいとき1を出力してカウンタ値のほうが大きいと0を出力することでPWMを作る。CCレジスタは32ビットで下位と上位がそれぞれA出力とB出力のCCの値になっている。CCはデフォルトで0が入っているのでデフォルトでの出力は定数0になる。

CCの値を変えるとデューティが変わるけど、そのときに位相がずれるのがまずい(パルス幅に関わらず立ち上がり位置が同じになるので、基本波の位相はデューティを変えるとずれる)場合にはカウンタをアップダウンにできる。ただしその場合周期は倍になる。またTOPやCCレジスタはダブルバッファになっていて周期がひと段落するまで切り替わらないようになっている。

分周器はADCと同じように1以下の値(n/16の形の有理数)も設定できるので、クロックに対して中途半端な周期を平均的にではあるけど作ることができる。またB出力はクロックの代わりにも使えるので、長い周期のPWMも出力できるようになっている。

スライスごとにIRQ出力を持っているので、割り込みがかけられる。DMAも起動できる。

複数のスライスをカウンタ初期値を決めて同時にスタートすることができて、そうすることで複数のスライスの位相を制御できる。また「オンザフライ(On-the-fly)」、つまり走らせている途中で位相を調整することもできる。

データシートから読めるのはこんなところか。たかがPWMというかもしれないけど、案外ちょっとした細かい配慮がいっぱい詰め込まれていることがわかってそういうところは僕は嬉しくなる。

7.2  headrware_pwm

sdkにPWMのためのheadrware_pwmモジュールがある。ヘッダは
#include "hardware/pwm.h"
まずピンをPWMに使うことを指定する。
enum gpio_function { 
  GPIO_FUNC_XIP = 0, GPIO_FUNC_SPI = 1, GPIO_FUNC_UART = 2, GPIO_FUNC_I2C = 3, 
  GPIO_FUNC_PWM = 4, GPIO_FUNC_SIO = 5, GPIO_FUNC_PIO0 = 6, GPIO_FUNC_PIO1 = 7, 
  GPIO_FUNC_GPCK = 8, GPIO_FUNC_USB = 9, GPIO_FUNC_NULL = 0xf 
};
void gpio_set_function(uint gpio, enum gpio_function fn);
を使って、ピンpigoをGPIO_FUNC_PWMに設定する。この関数はpwmモジュールのものではない。

GPIOのピンは特定のPWMスライスと結びついているので
static inline uint pwm_gpio_to_slice_num(uint gpio);

enum pwm_chan {
    PWM_CHAN_A = 0,
    PWM_CHAN_B = 1
};
static inline uint pwm_gpio_to_channel(uint gpio);
ピンがどのスライスのABどちらの出力に対応しているかを調べる。対応表はデータシートにあるのでわかっていれば必要ない。

PWMの基本設定は
static inline void pwm_set_wrap(uint slice_num, uint16_t wrap);
static inline void pwm_set_chan_level(uint slice_num, uint chan, uint16_t level);

static inline void pwm_set_both_levels(uint slice_num, uint16_t level_a, uint16_t level_b);
static inline void pwm_set_gpio_level(uint gpio, uint16_t level);
で、wrapというのはTOPレジスタの値、levelというのはCCレジスタの値である。出力2つをいっぺんに設定したりgpioピン番号から設定したりというのもある。

これだけあればとりあえず
static inline void pwm_set_enabled(uint slice_num, bool enabled);
とすると信号が出る。このままだとシステムクロックのままなので分周器の設定
static inline void pwm_set_clkdiv_int_frac(uint slice_num, uint8_t integer, uint8_t fract);
static inline void pwm_set_clkdiv(uint slice_num, float divider);
整数部と小数点以下で指定するか浮動小数点で指定するかが選べる。小数点以下は1/16の倍数を指定する(16以上だと設定そのものが無視される)。浮動小数点では1/16の倍数に丸められる。

もちろん小数点以下がある場合、基本波より低い周波数成分も現れる。この場合、基本波の1/16の周波数以下の周波数成分が現れるかどうか(パターンがランダムかどうか)ってPWMの後ろにアナログ回路をくっつけるときに問題になること(音として出力するような場合)があるかもしれない。どうなってるかはソースを見ないとわからないので、それが問題になる人は確認してほしい。

いくつかのモードがあったけど、これで指定する。
enum pwm_clkdiv_mode{
    PWM_DIV_FREE_RUNNING = 0, ///< Free-running counting at rate dictated by fractional divider
    PWM_DIV_B_HIGH = 1,       ///< Fractional divider is gated by the PWM B pin
    PWM_DIV_B_RISING = 2,     ///< Fractional divider advances with each rising edge of the PWM B pin
    PWM_DIV_B_FALLING = 3    ///< Fractional divider advances with each falling edge of the PWM B pin
};

static inline void pwm_set_clkdiv_mode(uint slice_num, enum pwm_clkdiv_mode mode);

static inline void pwm_set_phase_correct(uint slice_num, bool phase_correct);
static inline void pwm_set_output_polarity(uint slice_num, bool a, bool b);
pwm_set_phase_correct()はノコギリ波にするか三角波にするかの選択で、pwm_set_output_polarity()ではtrueだと出力の論理を反転させる。出力ピンのあとにトランジスタのバッファを追加したときpolarity関数を呼ぶだけですむのは小さな親切。

位相の調整は
static inline uint16_t pwm_get_counter(uint slice_num);
static inline void pwm_set_counter(uint slice_num, uint16_t c);

static inline void pwm_advance_count(uint slice_num);
static inline void pwm_retard_count(uint slice_num);
get_counter()はカウンタの瞬時値を読んで、set_counter()は値をセットする。advance_count()とretard_count()は1だけ進めたり遅らせたりする。

いちいち関数を呼ぶのが面倒な場合は設定用の構造体
typedef struct {
    uint32_t csr;
    uint32_t div;
    uint32_t top;
} pwm_config;
があってそれに対して
static inline pwm_config pwm_get_default_config(void);
tatic inline void pwm_config_set_phase_correct(pwm_config *c, bool phase_correct);
static inline void pwm_config_set_clkdiv(pwm_config *c, float div);
static inline void pwm_config_set_clkdiv_int(pwm_config *c, uint div);
static inline void pwm_config_set_clkdiv_mode(pwm_config *c, enum pwm_clkdiv_mode mode);
static inline void pwm_config_set_output_polarity(pwm_config *c, bool a, bool b);
static inline void pwm_config_set_wrap(pwm_config *c, uint16_t wrap);
なんかで構造体を作って、
static inline void pwm_init(uint slice_num, pwm_config *c, bool start);
で一気に設定する。startはfalseだと設定するだけで、信号は出ない。

そのほかに、割り込み処理用に
static inline void pwm_set_irq_enabled(uint slice_num, bool enabled);
static inline void pwm_set_irq_mask_enabled(uint32_t slice_mask, bool enabled);
static inline void pwm_clear_irq(uint slice_num);
static inline uint32_t pwm_get_irq_status_mask(void);
static inline void pwm_force_irq(uint slice_num);
なんかがある。


nice!(0)  コメント(0) 

nice! 0

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。