執筆者: 瀕死
最終更新: 2024/07/10
瀕死(ひんし)と申します。はじめに書くことが思いつきませんでした。 いつもより長編の本稿でもよろしくお願いいたします。
RP2040開発ボードにおいてProgrammable I/Oを使う方法について解説する。
命令だけでなく指令やC言語側の機能においても解説する。
PIOとは多用途のハードウェアインターフェイスで、CPUとは別のステートマシンで高速かつ正確なタイミングにI/Oを処理できる機能である。
以下にPIOと周辺の模式図を示す。
以下にステートマシンの模式図を示す。
レジスタの細かい仕様はデータシートを見てほしい。
PIOは0~7番の割り込みフラグをそれぞれ持っている。 IRQ0~IRQ3はシステムレベルの割り込みである。
PIOのプログラムにはPIOアセンブリを用いて記述する。 PIOアセンブラはPIOプログラムを解析しバイナリコードを出力する。
.から始まる指令でアセンブリを制御できる。
指令は8種類あるが、本稿では.lang_opt
と.origin
を除いた6種類を紹介する。
以下、丸かっこは省略可能なパラメータである。
.define (PUBLIC) <symbol> <value>
<symbol>
を <value>
で定義する。.program
より前に記載されるとそのファイル中でグローバルとなる。.program
のローカル定義となる。PUBLIC
が指定されている場合、ヘッダファイルに追加されるため他のファイルで利用できる。例を以下に示す。
.define LED_PIN 25
program <name>
<name>
という名前で新しいプログラムを記述する。<name>
を使う。<name>
はCの変数命名規則に従う必要がある。.program
までが<name>
プログラムになる。例を以下に示す。
.define LED_PIN 25
.program led_blink
.define OUT_BIT 1
out pins OUT_BIT
.program hoge
nop
.side_set <count> (opt) (pindirs)
opt
で命令毎にサイドセットピンの出力を設定する必要がなくすことができる。pindirs
でpinのHIGHとLOWの代わりに役割を入力(HI-Z)へ切り変えられるようにする。例を以下に示す。
.program hoge
.side_set 1
nop side 1 ; サイドセットピンをHIGHに
nop side 0 ; サイドセットピンをLOWに
.wrap_target
.wrap
と.wrap_target
との間がループする。.wrap
.wrap_target
の使い方に準ずる。例を以下に示す。
.program wrap_test
nop ; do something
.wrap_target
nop ; do something in loop
.wrap
.word <value>
例を以下に示す。
.program hoge
.word 0x0000 ; jmp 0 と同意
値には以下の種類がある。
分類 | 表記例 |
---|---|
符号付10進数 | 2, -9 |
16進数 | 0xf |
2進数 | 0b0110 |
シンボル | LED_PIN (.defineによる定義) |
ラベル | :loop (実は内部で.defineされる) |
(式) | (2+1) (丸かっこで囲む) |
以下のものは式となる。
;
や//
から行末までがコメントとなる。 C言語スタイルのブロックコメント/* */
も使用できる。
<symbol>:
によってラベルを作成できる。
例を以下に示す。
.program hoge
loop:
; do something
jmp loop
<instruction> (side <side_set_value>) ([<delay_value>])
<instruction>
が存在する。.side_set <count>
の<count>
のbit数だけ<side_set_value>
を指定する。opt
がない場合は、すべての命令で(side <side_set_value>)
が必要。<delay_value>
サイクルだけ、命令実行後に遅延が設定される。[<delay_value>]
と<side_set_value>
合わせて5bitで収まる必要がある。nop
nop
はmov y y
としてアセンブルされる。 副作用がないので遅延やサイドセットに便利である。
PIO命令は以下のとおりである。
命令 | 機能 |
---|---|
jmp | pcの値を特定のアドレスに設定する |
wait | 設定した状態に変化するまで待機する |
in | ISRをシフトし、値をISRの空いた領域に書く |
out | OSRをシフトし、あふれた値を出力先に書く |
push | ISRからFIFOに32bitデータを書き込み、ISRとISRのシフトカウンタをクリアする |
pull | FIFOからOSRに32bitデータを読み込み、OSRのシフトカウンタをクリアする |
mov | PIOのSM内でデータをコピーする。 |
irq | 割り込みフラグのセットorクリアする |
set | 5bitの値を設定する |
jmp (<cond>) <target>
<target>
にはラベルか最初の命令を1としたオフセット値が使える使える条件は以下のとおりである。
記述 | 真になる条件 |
---|---|
(書かない) | 常に |
!x | xが0のとき |
x-- | xが0でないとき、条件の真偽に関係なくデクリメントする |
!y | !xと同様 |
y-- | x--と同様 |
X!=Y | xとyの値が異なるとき |
PIN | JMP_PIN レジスタで指定されたピンがHIGHのとき |
!OSRE | OSRが空でないとき |
PIO命令は注意すべき点や、レジスタの値によって挙動が変わることがあるので、 詰まったらデータシートを見るべきである。
.program hoge
.wrap_target
pull
set x 7
send_bit:
out pins 1
jmp x-- send_bit; xが0でないならsend_bitに
.wrap
wait <polarity> gpio <gpio_num>
wait <polarity> pin <pin_num>
wait <polarity> irq <irq_num> ( rel )
<polarity>
は0か1<polarity>
になることが条件<polarity>
が1の場合は、irqをクリアするwait 1 gpio 5 ; gpio5がHIGHになるまで待機
in <source>, <bit_count>
<bit_count>
だけシフトし、空いた領域に<source>
の値を書き込むsm_config_set_in_shift()
関数で設定できる<bit_count>
だけ増加させる<bit_count>
に32を指定する場合、0と記述するin pins 1 ; ISRにピンの状態を書き込む
out <destination>, <bit_count>
<bit_count>
だけシフトし、そのbitを<destination>
に書き込むsm_config_set_out_shift()
関数で設定できる<bit_count>
だけ増加させる<bit_count>
に32を指定する場合、0と記述するout pins 1 ; OSRの値に応じてピンの状態を変化させる
push (iffull)
push (iffull) block ; 上と全く同じ
push (iffull) noblock
iffull
ならばISRシフトカウンタの値が指定値に達している場合のみpushされるnonblock
ならばRX FIFOがいっぱいでも待機しないpush
pull (ifempty)
pull (ifempty) block ; 上と全く同じ
pull (ifempty) noblock
iffull
ならばOSRシフトカウンタの値が指定値に達している場合のみpullされるnonblock
ならばTX FIFOが空でも待機しないpull
mov <destination>, (op) <source>
<source>
から<destination>
にop
した値をコピーするmov x null
irq <irq_num> (rel)
irq set <irq_num> (rel) ; 上と同じ
irq nowait <irq_num> (rel); 上と同じ
irq wait <irq_num> (rel)
irq clear <irq_num> (rel)
wait
の場合は、フラグをセットして、クリアされるまで待機するrelを付けると割り込みフラグ番号が以下のように計算される
(<irq_num> & 4) + ((SM番号 + <irq_num>) & 3)
set <destination>, <value>
<destination>
を<value>
に設定する<value>
は5bitset x,1
.pio
ファイルのPIOプログラムの下に以下を追記し、初期化処理等を記述できる。
% c-sdk {
// c言語で記述できる
}
多くの場合、以下のような初期化を行う関数を記述する。
% c-sdk {
#define CLKDIV 50000
void pio_led_blink_init(PIO pio, uint sm, uint offset) {
// {.programで指定したプログラム名}_program_get_default_config(offset);
// である必要がある
pio_sm_config c = led_blink_program_get_default_config(offset);
// ----------------------------------------------GPIO関係
// .define LED 25していることに注意
pio_gpio_init(pio, LED);
// ピンが入力か出力か設定
pio_sm_set_consecutive_pindirs(pio, sm, LED, 1, true);
// out命令で用いられるピンの指定
sm_config_set_out_pins(&c, LED, 1);
// ----------------------------------------------FIFO関係
// TX FIFO (CPUから見て出力なので)に接続
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
// 32bit outするごとに自動でpullする
sm_config_set_out_shift(&c, true, true, 32);
// ----------------------------------------------その他
// ステートマシンの1サイクルを125MHzを分周して作る
sm_config_set_clkdiv_int_frac(&c, CLKDIV, 0x00);
// ステートマシンを初期化し設定する
pio_sm_init(pio, sm, offset, &c);
}
}
上記以外で初期化においてよく使われる関数をいくつか紹介する。
sm_config_set_in_pins(&c, PIN); // set base OUT pin
sm_config_set_set_pins(&c, PIN, count); // set base SET pin
sm_config_set_sideset_pins(&c, PIN); // set base SIDESET pin
各種命令のpins
で用いるピンを設定する。
sm_config_set_in_shift(&c, true, true, 32);
第二引数から、shift_right
、autopush
、bit数
CmakeLists.txr
を以下のように編集する。 以下では実行可能ファイル名はa
、pioファイル名はtest.pio
としている。
target_link_libraries
にhardware_pio
を追記するpico_generate_pio_header(a ${CMAKE_SOURCE_DIR}/test.pio)
を追加するmain.cpp
にインクルードファイルを追加する。
#include "hardware/pio.h"
#include "test.pio.h"
main.cpp
は以下のとおりとする。
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/pio.h"
#include "test.pio.h"
// pio0とpio1がある
PIO pio = pio0;
uint offsetLedBlink;
uint smLedBlink;
int main() {
stdio_init_all();
// ${作製したプログラムの名前}_programの参照を渡す
offsetLedBlink = pio_add_program(pio, &led_blink_program);
smLedBlink = pio_claim_unused_sm(pio, true);
pio_led_blink_init(pio, smLedBlink, offsetLedBlink);
pio_sm_set_enabled(pio, smLedBlink, true);
while(true) {
// smのTX FIFOに32bit渡す
// 0xAは0b1010である
pio_sm_put_blocking(pio, smLedBlink, 0xAAAAAAAA);
}
}
test.pio
は以下のとおりとする。」
.define PUBLIC CLKDIV 50000
.define PUBLIC LED 25
.program led_blink
.wrap_target
out pins, 1 [31] ; OSRから1bit読み取ってピンの電圧に反映。
nop [31] ; 待機
.wrap
% c-sdk {
void pio_led_blink_init(PIO pio, uint sm, uint offset) {
// {.programで指定したプログラム名}_program_get_default_config(offset);
// である必要がある
pio_sm_config c = led_blink_program_get_default_config(offset);
// ----------------------------------------------GPIO関係
// .define LED 25していることに注意
pio_gpio_init(pio, LED);
// ピンが入力か出力か設定
pio_sm_set_consecutive_pindirs(pio, sm, LED, 1, true);
// out命令で用いられるピンの指定
sm_config_set_out_pins(&c, LED, 1);
// ----------------------------------------------FIFO関係
// TX FIFO (CPUから見て出力なので)に接続
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
// 32bit outするごとに自動でpullする
sm_config_set_out_shift(&c, true, true, 32);
// ----------------------------------------------その他
// ステートマシンの1サイクルを125MHzを分周して作る
sm_config_set_clkdiv_int_frac(&c, CLKDIV, 0x00);
// ステートマシンを初期化し設定する
pio_sm_init(pio, sm, offset, &c);
}
%}
ビルドして実行すると高速で内蔵LEDが点滅する。
次回はPS2の接続仕様について解説する予定です。
PIOについての解説も続けていく予定ですのでこれからもよろしくお願いします。