鳥なき里のマイコン屋(131) ラズパイPico、PWM、外部クロック、周波数カウンタ

Joseph Halfmoon

昨日の投稿にてラズパイPicoのPWM出力を使ってみました(トホホ2つ。)基本的な設定はデフォのまま動作させましたが、PWMのカウントの元になるクロックは何なの、周波数はいくら、などいくつも疑問を持ちました。今回は実験しながらそのあたりをハッキリさせていきたいと思います。

(末尾に実験に使ったコード全文を掲げました。)

前にも書きましたが、ラズパイPicoのマイコンであるRP2040と他の多くのマイコンとの違いの一つが、タイマ、カウンタです。「普通のマイコン」は時間を測るタイマとしての機能と入出力を制御するためのカウンタ、コンパレータ等の機能が一体化していることが多いです。タイマ・カウンタ1本につき4チャネルの入出力を同時制御できるコンパレータが付いたブロックを合計8個で全32チャネルとか、32ビットクラスのマイコンでは結構豪華なことが多いです。それに対してRP2040では、タイマは時間を測ることに特化してます。入出力を制御するためのカウンタ、コンパレータ機能はタイマから分離して、PWM専門機能に分離というスタイル。それぞれの機能はシンプル化してます。RP2040のPWM機能は8スライスで合計16チャネルの端子を扱えるので本数的には少ない、ということは無いですが、ちょっとクセがあります。他のマイコンのタイマ・カウンタでやっていたことを置き換えるときには注意が必要じゃないかと思います。

内部クロック源は clk_sys 限定

まず気になったのが、PWM内のカウンタを駆動するクロック信号です。RP2040のPWMの1スライスは、内部に16ビットのカウンタを1本持ち、出力を2端子か、出力1端子+入力1端子として設定できます。入力端子を外部クロックとしてカウンタを駆動することはできるのですが、内部のクロック源を使おうとする場合、システムクロックである

clk_sys

を分周したクロックしか使えないようです。clk_sysに接続するクロック源そのものは選択可能ですが、これを変えるとclk_sysを使っている多くの回路にも影響するので、必要なければ根元をいじることはやりたくないです。他の多くのマイコンは先っぽ側の各タイマ/カウンタ毎にクロックを選べるのが普通です。その点では選択肢が狭いとも感じます。そのかわり分周器は整数部8ビット、小数点以下4ビットというスタイルの他では見かけない微妙な制御が可能な分周器です。分周器をうまく使って所望のクロックにせよ、という意図でしょうか。

PWM出力の復習。単なる分周器化。

clk_sys をPWMスライス内の分周回路で分周したクロックを外部に出力し、clk_sysの動作周波数を実測してみたいと思います(そのままのクロック速度だとAnalog Discovery2では測りずらいので「分周器」で遅くして出力。)

分周器は128分周(整数128、小数点以下0)に指定。カウンタは1ビットのカウント(0と1を繰り返す)というミニマム設定です。PWM出力のAのコンパレータには0を設定、Bには1を設定とこちらもミニマム設定です。チャネルAは出力0のまま、チャネルBは単なる2分周器として動作するはず。

gpio_set_function(PWMOUTA, GPIO_FUNC_PWM);
gpio_set_function(PWMOUTB, GPIO_FUNC_PWM);
pwm_slice1 = pwm_gpio_to_slice_num(PWMOUTA);
pwm_set_clkdiv_int_frac(pwm_slice1, 128, 0);
pwm_set_wrap(pwm_slice1, 1);
pwm_set_chan_level(pwm_slice1, PWM_CHAN_A, 0);
pwm_set_chan_level(pwm_slice1, PWM_CHAN_B, 1);
pwm_set_enabled(pwm_slice1, true);

動作中のPWM出力の波形を観察。黄色がチャネルA、青がチャネルB。ノイズが見苦しいですが、予定通り出力されているように見えます。
PWMout01
実測した周波数に、PWMカウンタでの2分周、分周器での128分周を加味し、元の clk_sys の周波数を計算してみます。

0.48821 [MHz] * 2 * 128 ≒ 124.98 [MHz]

四捨五入して125MHz、これがC/C++SDKを使ってデフォルト値でビルドした場合のシステムクロックということだと思います。次にこの周波数が内部でも観測できるか確かめてみます。

クロック周波数の自己測定

クロック周波数はタイマ/カウンタブロックを適切にプログラミングすれば測定できるものです。しかし、近代的なマイコンには、クロック周波数を測定するための専用カウンタが設けられていることが多いです。RP2040にも Frequency Conter と呼ばれるカウンタが備わっており、これに内部/外部のクロック源を接続することでその周波数を確認することができます。クロックにかかわるAPIについては以下にドキュメントがあります。

Raspberry Pi Pico SDK Documentation hardware_clocks

C/C++SDKから使用するのは非常に簡単で、以下のように引数に測定したいクロック源を指定して関数を呼んでやるだけです。以下では clk_sys と外部クロック入力GPIN0の2つを測定しています。

printf("clk_sys: %d[kHz]\r\n", frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS));
printf("GPIN0: %d[kHz]\r\n", frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLKSRC_GPIN0));

測定結果は以下のようです。外付けのオシロで測定した125MHzと、内部で測定した周波数は一致しました。また、外部クロック入力端子には5kHzの矩形波を与えていたので、これも一致でOKです。

clk_sys: 125000[kHz]
GPIN0: 5[kHz]

便利な frequency_count_khz()関数なのですが、落とし穴もあります。デフォルトの設定では 外部クロック入力の1kHzは測定できませんでした。2kHzにすると測定可でした。周波数カウンタは、リファレンスクロックのある期間に到来する被測定クロックを数えている筈なので、リファレンスクロックに比べて外部クロックが遅すぎると検出できないと推測されます。遅いクロックを測る場合は、リファレンスクロックを別に設定する必要があると思われます。

外部クロックの入力とその周波数測定

ついでというと何ですが、外部クロックをクロックジェネレータに入力してみました。クロック源として内部で使うことも可能ですが、ここでは使わず、そのまま外部端子にスルーで出力いたしました。RP2040の場合、以下のような制限があります。

  • クロック入力端子は2端子限定(GPIO20/22)
  • クロック出力端子は4端子限定(GPIO21/23/24/25)

なおラズパイPicoの場合、GPIO25はオンボードのLEDに接続されているのでクロック出力としては使いずらいです。実際にGPIO20から外部クロックを入れ、GPIO21からスルーで出力する設定がこちら。特にポートの設定は不要なようです。C/C++SDKの「高水準」のありがたみでしょうか。

clock_gpio_init(GPOUT0, CLOCKS_CLK_GPOUT0_CTRL_AUXSRC_VALUE_CLKSRC_GPIN0, 1);
clock_configure_gpin(clk_gpout0, GPIN0, 1000, 1000);

ここにも小ネタがありました。clock_gpio_init()関数の3番目の引数は「分周比の設定」みたいな感じなのです。ここを1/2/4と3通りに書き換えてみましたが、出力クロックが分周されるようなことはありませんでした。使い方不明です。

また、clock_configure_gpin()関数の3番目、4番目の引数はソースの周波数、設定する周波数など書き込むところに見えます。当然ですが、適当な値を書いても外部クロックの周波数に影響はありませんでした。書き込んだ値はこの関数の戻り値に反映されるだけですかね。ソフトウエアで参照するだけの仮の値?

こうして外部クロックを「生かして」あれば、先ほどの周波数測定関数にて外部クロックの周波数測定は問題なく行えます。勿論クロック源として外部クロックを使うことも可能。

鳥なき里のマイコン屋(130) VS CodeでラズパイPico、GPIOで割り込み に戻る

鳥なき里のマイコン屋(132) ラズパイPico、何気に便利なpicotool へ進む

実験に使ったコード全文
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/clocks.h"
#include "hardware/gpio.h"
#include "hardware/pwm.h"

#define PWMOUTA (2)
#define PWMOUTB (3)
#define GPIN0 (20)
#define GPOUT0 (21)
#define LED (25)

uint pwm_slice1;

int main()
{
    bool ledStat = false;

    stdio_init_all();
    puts("start freqTST0.");
    
    gpio_init(LED);
    gpio_set_dir(LED, GPIO_OUT);
    
    clock_gpio_init(GPOUT0, CLOCKS_CLK_GPOUT0_CTRL_AUXSRC_VALUE_CLKSRC_GPIN0, 1);
    clock_configure_gpin(clk_gpout0, GPIN0, 1000, 1000);

    gpio_set_function(PWMOUTA, GPIO_FUNC_PWM);
    gpio_set_function(PWMOUTB, GPIO_FUNC_PWM);
    pwm_slice1 = pwm_gpio_to_slice_num(PWMOUTA);
    pwm_set_clkdiv_int_frac(pwm_slice1, 128, 0);
    pwm_set_wrap(pwm_slice1, 1);
    pwm_set_chan_level(pwm_slice1, PWM_CHAN_A, 0);
    pwm_set_chan_level(pwm_slice1, PWM_CHAN_B, 1);
    pwm_set_enabled(pwm_slice1, true);

    for (int i=0; i<200; i++) {
        gpio_put(LED, (ledStat = ~ledStat & 1));
        printf("clk_sys: %d[kHz]\r\n", frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS));
        printf("GPIN0:   %d[kHz]\r\n", frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLKSRC_GPIN0));
        sleep_ms(2000);
    }

    puts("end freqTST0.");
    return 0;
}