トップ画像
RP2040開発ボードでProgrammable I/Oを使う

執筆者: 瀕死

最終更新: 2024/07/10

はじめに

瀕死(ひんし)と申します。はじめに書くことが思いつきませんでした。 いつもより長編の本稿でもよろしくお願いいたします。

目的

RP2040開発ボードにおいてProgrammable I/Oを使う方法について解説する。

命令だけでなく指令やC言語側の機能においても解説する。

目次

  • RP2040開発ボードでProgrammable I/Oを使う
    • はじめに
    • 目的
    • 目次
    • Programmable I/Oの構成
    • PIO Assembler
    • Directives
      • .define
      • .program
      • .side_set
      • .wrap_target
      • .wrap
      • .word
    • コメント
    • ラベル
    • 命令
    • 疑似命令
    • 命令セット
      • JMP
        • 条件
      • WAIT
      • IN
        • 入力元
      • OUT
        • 出力先
      • PUSH
      • PULL
      • MOV
        • 出力先
        • 演算
        • 入力元
      • IRQ
        • rel
      • SET
        • 出力先
    • C/C++による制御
      • 初期化
        • pins登録系
        • FIFO系
      • ステートマシンとのやり取り
    • 実践
      • 事前準備
      • Lチカ
    • おわりに
    • 参考文献

Programmable I/Oの構成

PIOとは多用途のハードウェアインターフェイスで、CPUとは別のステートマシンで高速かつ正確なタイミングにI/Oを処理できる機能である。

以下にPIOと周辺の模式図を示す。

以下にステートマシンの模式図を示す。

レジスタの細かい仕様はデータシートを見てほしい。

PIOは0~7番の割り込みフラグをそれぞれ持っている。 IRQ0~IRQ3はシステムレベルの割り込みである。

PIO Assembler

PIOのプログラムにはPIOアセンブリを用いて記述する。 PIOアセンブラはPIOプログラムを解析しバイナリコードを出力する。

Directives

.から始まる指令でアセンブリを制御できる。

指令は8種類あるが、本稿では.lang_opt.originを除いた6種類を紹介する。

以下、丸かっこは省略可能なパラメータである。

.define

.define (PUBLIC) <symbol> <value>
  • <symbol><value> で定義する。
  • 最初の.programより前に記載されるとそのファイル中でグローバルとなる。
  • それ以外ではその.programのローカル定義となる。
  • PUBLICが指定されている場合、ヘッダファイルに追加されるため他のファイルで利用できる。

例を以下に示す。

.define LED_PIN 25

.program

program <name>
  • <name>という名前で新しいプログラムを記述する。
  • C言語側から呼び出すのにも<name>を使う。
  • <name>はCの変数命名規則に従う必要がある。
  • 次の.programまでが<name>プログラムになる。
  • PIO命令はプログラム内でのみ使用できる。

例を以下に示す。

.define LED_PIN 25

.program led_blink
.define OUT_BIT 1
  out pins OUT_BIT

.program hoge
  nop

.side_set

.side_set <count> (opt) (pindirs)
  • ほかの命令による入出力とは並行して出力できるサイドセットピンの本数を指定する。
  • optで命令毎にサイドセットピンの出力を設定する必要がなくすことができる。
  • 最初のPIO命令より先に配置する。
  • pindirsでpinのHIGHとLOWの代わりに役割を入力(HI-Z)へ切り変えられるようにする。
  • 遅延と合わせて5bitまで

例を以下に示す。

.program hoge
.side_set 1
  nop side 1  ; サイドセットピンをHIGHに
  nop side 0  ; サイドセットピンをLOW

.wrap_target

.wrap_target
  • .wrap.wrap_targetとの間がループする。
  • JMP命令と違い無遅延で実行される。
  • プログラム外で無効。
  • プログラムごとに1つまで配置できる。
  • 配置しなかった場合、プログラムの先頭に配置される。

.wrap

.wrap
  • 配置しなかった場合、プログラムの最後に配置される。
  • それ以外は.wrap_targetの使い方に準ずる。

例を以下に示す。

.program wrap_test
  nop           ; do something
.wrap_target
  nop           ; do something in loop
.wrap

.word

.word <value>
  • 16bitのリテラルをPIO命令として保存する。

例を以下に示す。

.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>])
  • 16bit固定長の命令で、9種類の命令:<instruction>が存在する。
  • .side_set <count><count>のbit数だけ<side_set_value>を指定する。
  • optがない場合は、すべての命令で(side <side_set_value>)が必要。
  • <delay_value>サイクルだけ、命令実行後に遅延が設定される。
  • [<delay_value>]<side_set_value>合わせて5bitで収まる必要がある。

疑似命令

nop

nopmov 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

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

wait <polarity> gpio <gpio_num>
wait <polarity> pin <pin_num>
wait <polarity> irq <irq_num> ( rel )
  • 条件が満たされるまで待機する
  • <polarity>は0か1
  • gpioやpinが<polarity>になることが条件
  • irqにおいて<polarity>が1の場合は、irqをクリアする
  • pinはIN_BASEで指定したGPIOを基準に指定する

wait 1 gpio 5 ; gpio5がHIGHになるまで待機

IN

in <source>, <bit_count>
  • ISRを<bit_count>だけシフトし、空いた領域に<source>の値を書き込む
  • シフト方向をsm_config_set_in_shift()関数で設定できる
  • ISRシフトカウンタの値を<bit_count>だけ増加させる
  • 自動pushが有効ならばISRシフトカウンタの値に応じてpushも同サイクルで実行される
  • 自動pushのときにRX FIFOがいっぱいならば待機する
  • <bit_count>に32を指定する場合、0と記述する

入力元

  • pins
  • x
  • y
  • null (0が読み取られる)
  • ISR
  • OSR

in pins 1 ; ISRにピンの状態を書き込む

OUT

out <destination>, <bit_count>
  • OSRを<bit_count>だけシフトし、そのbitを<destination>に書き込む
  • シフト方向をsm_config_set_out_shift()関数で設定できる
  • OSRシフトカウンタの値を<bit_count>だけ増加させる
  • 自動pullが有効ならばOSRシフトカウンタの値に応じてpullも同サイクルで実行される
  • 自動pullのときにTX FIFOがいっぱいならば待機する
  • <bit_count>に32を指定する場合、0と記述する

出力先

  • pins
  • x
  • y
  • null (消去される)
  • pindirs (入力(HI-Z)と出力を切り替えられる)
  • pc
  • ISR
  • exec (命令として実行される)

out pins 1  ; OSRの値に応じてピンの状態を変化させる

PUSH

push (iffull)
push (iffull) block ; 上と全く同じ
push (iffull) noblock
  • ISRをRX FIFOに32bit書き込む
  • 書き込み後にISRはゼロクリアされる
  • iffullならばISRシフトカウンタの値が指定値に達している場合のみpushされる
  • nonblockならばRX FIFOがいっぱいでも待機しない
    • その場合FIFOには何も書き込まないが、ISRはゼロクリアされる
    • さらにFDEBUG_RXSTALL フラグが設定される

push

PULL

pull (ifempty)
pull (ifempty) block ; 上と全く同じ
pull (ifempty) noblock
  • OSRにTX FIFOから32bit書き込む
  • iffullならばOSRシフトカウンタの値が指定値に達している場合のみpullされる
  • nonblockならばTX FIFOが空でも待機しない
    • その場合OSRにはxレジスタの値が書き込まれる

pull

MOV

mov <destination>, (op) <source>
  • <source>から<destination>opした値をコピーする
  • 自動pullを使う場合はOSRに書き込むと干渉の恐れがある

出力先

  • pins
  • x
  • y
  • exec (遅延なしで命令として実行される)
  • pc
  • ISR
  • OSR

演算

  • ~, ! 論理否定
  • :: ビット逆転

入力元

  • pins
  • x
  • y
  • null (0が読み取られる)
  • STATUS (EXECCTRL_STATUS_SELで設定されたフラグの状態)
  • ISR
  • OSR

mov x null

IRQ

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

relを付けると割り込みフラグ番号が以下のように計算される

(<irq_num> & 4) + ((SM番号 + <irq_num>) & 3)

SET

set <destination>, <value>
  • <destination><value>に設定する
  • <value>は5bit

出力先

  • pins
  • x
  • y
  • pindirs

set x,1

C/C++による制御

.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);
  }

}

上記以外で初期化においてよく使われる関数をいくつか紹介する。

pins登録系

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で用いるピンを設定する。

FIFO系

sm_config_set_in_shift(&c, true, true, 32);

第二引数から、shift_rightautopushbit数

ステートマシンとのやり取り

実践

事前準備

CmakeLists.txrを以下のように編集する。 以下では実行可能ファイル名はa、pioファイル名はtest.pioとしている。

  • target_link_librarieshardware_pioを追記する
  • pico_generate_pio_header(a ${CMAKE_SOURCE_DIR}/test.pio)を追加する

main.cppにインクルードファイルを追加する。

#include "hardware/pio.h"
#include "test.pio.h"

Lチカ

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についての解説も続けていく予定ですのでこれからもよろしくお願いします。

参考文献

取得に失敗しました

2024年度 入部