前記事「Raspberry pi pico/pico2をVScode PlatformIOでC++開発」の続き
前回、FreeRtOS/USBCDCを使ってテストプログラムを作りました。今回はさらに周期割り込みとPIOを使って周期回転計を製作しました
テストベンチの組立
上の写真は完成形ですが、まずはブレッドボードで実験ベンチを組んでみます
先に回路図。いつもは手書きですが、気まぐれで清書してみました
秋月販売の4桁LEDディスプレイOSL40391をダイナミック点灯させる。トリマを使ってベンチ内でテスト用のパルス発生(本チャン用は不要)
回路図とベンチ
その様子は
①でもチラチラ外観出してましたがテストベンチの全景です。ベンチ内で簡易的に疑似回転信号を出してパルステストするときは、GP0とGP15を接続して、青色のトリマ抵抗で疑似回転数を調整して表示を確認します
PIO
サイト「Raspberry Pi Pico+Arduinoで周波数を測定する」を参考にさせていただきました。
参考サイトのPIOコードはHighの時間を測定する方式で、デューティ1:1の時に好結果がでますが、今回の応用ではデューティ1:1とは限らないので少し手を加えました
.pio_version 0 // only requires PIO version 0
.program periodic
begin:
pull block ; FIFO からデータを OSR に読み込む
mov x, osr ; x レジスタに初期カウント値を格納
wait 0 pin 0 ; pin 0 がローになるまで待機(ハイからロー)
wait 1 pin 0 ; pin 0 がハイになるまで待機(ローからハイ)
high_to_low:
jmp pin count ; pin 0 がローなら count ラベルにジャンプ
jmp low_to_high ; pin 0 がハイなら low_to_high ラベルにジャンプ
count:
jmp x-- high_to_low ; x レジスタをデクリメントし、ゼロでない限り high_to_low に戻る
low_to_high:
jmp pin exitloop ; pin 0 がローなら count ラベルにジャンプ
jmp x-- low_to_high ; pin 0 がハイなら exitloop にジャンプ
exitloop:
mov isr, x ; カウント結果を ISR に格納
push block ; 結果を FIFO に送信
% c-sdk {
static inline void periodic_program_init(PIO pio, uint sm, uint offset, uint pin) {
pio_sm_config c = periodic_program_get_default_config(offset);
// sm_config_set_out_pins(&c, pin, 1);
pio_gpio_init(pio, pin);
// Set the pin direction to output at the PIO
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, false);
sm_config_set_in_pins(&c, pin);
sm_config_set_jmp_pin(&c, pin);
// クロック分周なしでステートマシン実行
sm_config_set_clkdiv(&c, 1.0f);
pio_sm_init(pio, sm, offset, &c);
// Set the state machine running
pio_sm_set_enabled(pio, sm, true);
}
%}
PIOの条件分岐命令にはダウンカウントで飛ぶ命令しかないので、大きい数値を入れてダウンしていく方式がとられる
メインからFIFOに2^31をいれてPIOを呼ぶ。PIOではパルスのlow待ち・hight待ちを経て、hight期間+low期間のカウントをFIFOの値からダウンカウントして一周期分終了したらFIFOに格納してメインに戻る
一周期は(2^31-戻り値)/31.25MHzで得られる、という原理です
VScodeからpioasmを呼ぶ
拡張機能「Raspberry Pi Pico」ではbuildでpioasmも一括アセンブルできたのですが、さすがにplatformIOではスクリプトを組まないとできないらしいので、単純にVScodeとpioasmは別に配置して、ターミナルで実行する方式にしました。
以前picoSDKをインストールしてあるので、そこからpioasm.exeを取り出し、パスをシンプルにした「d:\pioasm\」フォルダを作成し格納しました
「cd src」は.pioソースファイルがプロジェクトフォルダ下の\srcフォルダにある前提です。VScodeで開いている間は一回チェンジしてしまえば、あとは「d:\pioasm\・・・」の繰り返しでアセンブルできるので、↑キーによるコマンドリピートでできます
一括buildでなくともターミナルでアセンブルできれば、まあ良しとします
「VScode内で完結」を目指した方法ですが、VScodeのターミナルではなくWindowsターミナルでもできるので、別ウィンドウでやれるほうが効率良いかもしれません
メインプログラム
arduinoとpicoSDK混在できる
LEDダイナミック点灯に周期割り込み「event_timer」を1msで駆動します。割り込みの中でLEDのdataとcolumnにそれぞれ8bit/4bitで出力しますが、picoSDKのバス出力の記述gpio_set_mask()で一気に出力しています。その他のGPIOはarduino記述のdigitalWrite()などです。arduinoとpicoSDK混在できるので便利です
周期割込みの宣言 add_repeating_timer_us()関数はpico専用の記述で、ESP32で使っていたxTimerCreate()がうまくいかないときはこちらで・・・すでに周知でしょうけど
メインプログラム
#include <Arduino.h>
#include <stdio.h>
#include <stdlib.h>
#include <FreeRTOS.h>
#include <task.h>
#include "periodic.pio.h"
#include "hardware/adc.h"
#include "hardware/pwm.h"
// SEG A-DP -> GPIO2-9, COL 1-4 -> GPIO10-13
#define SEG_GPIO 2
#define COL_GPIO 10
#define MON 16
#define PLSIN 15
PIO pio = pio0;
uint32_t sm;
uint32_t COUNT_START = 2147483648;
TaskHandle_t mloopTaskHandle = NULL;
TaskHandle_t blinkTaskHandle = NULL;
// 0-9 & blank 7segment pattern
uint8_t bits[11] = {
0x3f, // 0
0x06, // 1
0x5b, // 2
0x4f, // 3
0x66, // 4
0x6d, // 5
0x7d, // 6
0x07, // 7
0x7f, // 8
0x67, // 9
0x00 // blank
};
uint32_t col[4] = {0xe, 0xd, 0xb, 0x7};
volatile uint8_t ledbuf[4] = {0};
volatile int timec;
bool event_timer(__unused struct repeating_timer *t) {
static uint8_t scanc = 0;
// gpio_put(MON,1); // digitalWrite(MON,1);でも可
uint32_t mask = 0b0011111111111100; // GP2-13 clear
gpio_clr_mask(mask);
mask = (bits[ledbuf[scanc]] << SEG_GPIO) | (col[scanc] << COL_GPIO);
gpio_set_mask(mask);
scanc = ++scanc & 0x3;
// gpio_put(MON,0); // digitalWrite(MON,0);でも可
return true;
}
void dispLED(int val) {
ledbuf[3] = val % 10;
ledbuf[2] = val / 10 % 10;
ledbuf[1] = val / 100 % 10;
ledbuf[0] = val / 1000 % 10;
for (int i = 0; i < 3; i++) { // 0サプレス処理(先頭の'0'をスペースに置き換え)
if (ledbuf[i] == 0) {
ledbuf[i] = 10; // 先頭の'0'をスペースに置換
} else {
break; // 最初に'0'でない桁が現れたら終了
}
}
}
uint slice_num;
void blink_task(void *pvParameters) {
bool isOn = false;
while (1) {
digitalWrite(25, isOn = !isOn);
vTaskDelay(500);
}
}
void mloop_task(void *pvParameters) {
while(1) {
vTaskDelay(500);
if (pio_sm_get_rx_fifo_level(pio, sm) > 0) {
uint32_t count = pio_sm_get(pio, sm);
pio_sm_set_enabled(pio, sm, false); // ステートマシン停止
float fval = round(1.0 / ((COUNT_START - count) * (1.0f / (double)clock_get_hz(clk_sys)) * 2.0f / 60.0f));
// fval /= 20.0f; // 12000rpmMax x2パルスのBLDM用
Serial.printf("%5.0frpm ", fval);
dispLED((int)fval);
pio_sm_clear_fifos(pio, sm); // ステートマシン再起動
pio_sm_set_enabled(pio, sm, true);
pio_sm_put(pio, sm, COUNT_START);
} else {
dispLED(0); // パルスカウント無しのときは0表示
}
// adc値->10~200Hz
uint16_t result = adc_read();
float f = 10.0f + ((float)result-20.0f) / 4075.0f * 190.0f;
uint32_t top = 125000000/(125*(uint32_t)f)-1;
top /= 2;
pwm_set_wrap(slice_num, top); // 1/adc読取値を正規化してPWM出力(周期を調整する)
pwm_set_chan_level(slice_num, PWM_CHAN_A, top / 2);
Serial.printf("result:%d, wrap:%d chan:%d\r\n", result, top, top/2);
}
}
void setup() {
uint32_t offset = pio_add_program(pio, &periodic_program);
sm = pio_claim_unused_sm(pio, true);
periodic_program_init(pio, sm, offset, PLSIN);
// ステートマシン起動
pio_sm_clear_fifos(pio, sm);
pio_sm_set_enabled(pio, sm, true);
pio_sm_put(pio, sm, COUNT_START);
adc_init();
adc_gpio_init(26); // Make sure GPIO is high-impedance, no pullups etc
adc_select_input(0); // Select ADC input 0 (GPIO26)
for (int gpio = SEG_GPIO; gpio < SEG_GPIO + 12; gpio++) {
gpio_init(gpio);
gpio_set_dir(gpio, GPIO_OUT);
gpio_set_outover(gpio, GPIO_OVERRIDE_NORMAL);
}
gpio_set_function(0, GPIO_FUNC_PWM);
slice_num = pwm_gpio_to_slice_num(0);
pwm_set_clkdiv(slice_num, 255.0f); // PWM周期を設定
pwm_set_wrap(slice_num, 48921);
pwm_set_chan_level(slice_num, PWM_CHAN_A, 24460);
pwm_set_enabled(slice_num, true); // PWM出力イネーブル
pinMode(25, OUTPUT);
pinMode(MON, OUTPUT);
gpio_init(PLSIN);
gpio_set_dir(PLSIN,GPIO_IN);
static repeating_timer_t timer;
add_repeating_timer_us(1000, &event_timer, NULL, &timer);
xTaskCreate(mloop_task, "mloop", 1024, NULL, 1, &mloopTaskHandle);
xTaskCreate(blink_task, "blink", 256, NULL, 1, &blinkTaskHandle);
}
void wait_timec(unsigned int n) {
timec = n;
while(timec != 0) vTaskDelay(1);
}
void loop() {}
プログラムはpicoSDKのExample、platformIOのExample、参考サイトのプログラムをつぎはぎのようにくみ上げたものです。悪しからず
カウンタの精度
SGで確認しました
50.000Hz:3000rpm、400.000Hz:24000rpm表示はOK
ちょっとずらして下の写真を得る
50.01Hzで3001rpm
400.01Hzで24001rpm(4桁なので20000の桁は表示できていない)




モーター回転が目的なので、回転計としてはかなり満足な性能です
あとは、パルス入力のデバウンス除去などおいおい工夫していく予定です