🫖

Raspberry Pi Pico Wでバーサライタを作成② プログラム

2024/05/06に公開

ここでは、MicroPythonを用いて、Raspberry Pi Pico Wでバーサライタ用のプログラムを作成する。以下の記事の続きである。
https://zenn.dev/ythk/articles/ythk-versawriter1

シフトレジスタ74HC595の制御

RaspberryPi Pico Wに以下通り接続してある。

シフトレジスタのピン 接続先
SER GPIO 13
SCLK GPIO 14
RCLK GPIO 15

基本的な使い方

SCLKの立ち上がりでSERの値がレジスタにセットされ、RCLKの立ち上がりでレジスタの値が出力される。つまり基本的な使い方は以下の通り。

シフトレジスタの基本的な使い方
from machine import Pin

SER  = Pin(13, Pin.OUT)
SCLK = Pin(14, Pin.OUT)
RCLK = Pin(15, Pin.OUT)

def control_LED(pattern :list[bool]):
    RCLK.low()
    for i in range(16):
        SCLK.low()
        SER.value(pattern[i])
        SCLK.high()
    RCLK.high()

# 1つおきにLEDを点灯させる
pattern = [i%2==1 for i in range(16)]
control_LED(pattern)

もちろんこれでも良いのだが、LED配列の点灯パターンをlist[bool]にするのは取り回しが不便なので、2進数で表現することにする。すると等価な回路は以下の通り。

2進数による指定
from machine import Pin

SER  = Pin(13, Pin.OUT)
SCLK = Pin(14, Pin.OUT)
RCLK = Pin(15, Pin.OUT)

def control_LED(pattern :int):
    RCLK.low()
    for i in range(16):
        SCLK.low()
        SER.value(pattern & 1)
        SCLK.high()
        pattern >>= 1
    RCLK.high()

# 1つおきにLEDを点灯させる
pattern = 0b1010101010101010
control_LED(pattern)

PIOを用いた制御

測定とか特にしていないが、この関数をバーサライタが1回転する間に100回以上も呼び出すことを考えると、実行時間が気になってしまう。問題があるかどうかも調べずに、最適化するのは正しい行動ではない気がするが、勉強と楽しさのために、PIO(プログラマブルIO)を使ってみることにする。アセンブラを書く必要があるが、1クロックで1命令を実行できるそうで、ぜひ使いこなしたい。

PIOを用いた制御
from rp2 import asm_pio, PIO
from machine import Pin

SER = Pin(13, Pin.OUT, value=0)
SCLK = Pin(14, Pin.OUT, value=0)
RCLK = Pin(15, Pin.OUT, value=0)

@asm_pio(
    out_init=PIO.OUT_LOW,
    out_shiftdir=PIO.SHIFT_RIGHT,
    sideset_init=(PIO.OUT_LOW, PIO.OUT_LOW),
)
def controlLED():
    pull() # 送信FIFO(sm.putの引数)を出力シフトレジスタに書き込み
    set(x, 15)  # レジスタxに即値15を書き込み
    label("loop") #jmp命令用の目印
    out(pins, 1).side(0b00) # 出力シフトレジスタを1ビットだけpinsレジスタに書き込み、ビットシフト。同時にSCLKとRCLKをLOWに
    jmp(x_dec, "loop").side(0b01) # xが0でなければloopに戻る。そうでなければxをデクリメント。同時にSCLKをHIGHに
    nop().side(0b11) # SCLKとRCLKをHIGHに

pattern = 0b1010101010101010
sm = rp2.StateMachine(0, controlLED, out_base=SER, sideset_base=SCLK)
sm.active(1)    # ステートマシンを有効化。送信FIFOが空ならストールしている
sm.put(pattern) # 送信FIFOにFIFO
ちょっとした解説
アセンブラとサイドセット

分岐jmp、出力シフトレジスタの値の書き込みout、送信FIFOの値を出力シフトレジスタに書き込みpull、レジスタへ即値の書き込みsetなどの命令や、分岐先の定義label、何もしないnopなどの疑似命令が使える。このようなアセンブラを最大32命令まで書き込めるステートマシンが8台搭載されている。
さらにサイドセットといって、通常の命令と同時に事前に指定したGPIOの出力を変更できる機能もある。特に命令を節約したいわけではないが、使った方が簡単に書けるので使用している。

@asm_pioデコレーター

ここで使用するピンの指定する。初期値を指定したピンだけが使用可能になるので、初期値の設定というよりは、使用するピンの指定と考えるべきである。out_initsideset_initへの指定をタプルにすることで、複数のピンを扱うこともできる。その場合GPIOは連番になっている必要がある。つまり、今回の例ではRCLKのGPIO番号はSRLKの番号+1になっている必要がある。

rp2.StateMachine

rp2.StateMachineでインスタンスを作り、activeメソッドで有効化する。rp2.StateMachineの引数は、ステートマシンのid、呼び出したい関数、out命令の書き込み先、sidesetの対象など。@asm_pioでサイドセット等を複数指定している場合でも、先頭のピンだけを渡す。

回転角度の計算

フォトセンサーの出力の読み取り


LED基板の裏面の配線

扇風機の外周の1箇所に、白いラインを取り付けている[1]。回転するバーサライタがこのラインを通過するタイミングを、フォトセンサーで検知したい。今回はフォトセンサーの出力端子をADC2(GPIO28)に接続してある。ADCのインスタンスを作成した後、read_u16()を呼ぶことで、測定値が0〜65535の整数で返ってくる。閾値をどこにするかは、実際に試しながら決める。

フォトセンサーの出力の読み取り
from machine import ADC
from time import sleep

PHOTO = ADC(2)

while True:
    adc_value = PHOTO.read_u16()
    isOrigin = adc_value > 50000
    print(isOrigin, adc_value)
    sleep(0.1)

回転角度の計算

回転周期periodを求めれば、最後に白いライン(以下、原点)を通過してからの経過時間deltaを用いて、現在の角度が\theta = 2\pi * \text{delta} / \text{period}と計算できる。ただしいくつか工夫が必要である。

  • 白いラインの通過中に複数回読み取りが行われるので、一定時間(今回は10000μs)が経過しない限りは、原点通過とはみなさないようにする必要がある。
  • 扇風機の回転周期は一定ではないので、随時periodを更新する必要がある。普通ならperiod = deltaで十分なのだが、ノイズが乗らないようにここではperiod = (period + delta) // 2と更新している。
角度の検出
from machine import ADC
from time import ticks_us, ticks_diff

PHOTO = ADC(2)

period = 10000          # 一周にかかる時間(us)
ticks_prev = ticks_us() # 前回原点を通過した時刻

while True:
    ticks_now = ticks_us()
    delta = ticks_diff(ticks_now, ticks_prev)
    isOrigin = PHOTO.read_u16() > 50000
    if isOrigin and delta > 10000: # 原点通過したら
        ticks_prev = ticks_now
        period = (period + delta) // 2
        delta = 0

    theta = 6.283185307179586 * delta / period
    print(theta)  # 本当はここで、シフトレジスタにパターンを送る

イラスト等を表示する場合は、一周を等分してピクセルのように扱った方が良いかもしれない。またイラストを動かしたいのなら、回転回数もあると便利かもしれない。そこで以下のようにしておく。

使いやすく工夫
from machine import ADC
from time import ticks_us, ticks_diff

NMAX = 1000 # 回転回数の上限
XMAX = 360  # 円周座標の分割数

PHOTO = ADC(2)

n = 0  # 回転回数
period = 1  # 一周にかかる時間(us)
ticks_prev = ticks_us()  # 前回原点を通過した時刻

while True:
    ticks_now = ticks_us()
    delta = ticks_diff(ticks_now, ticks_prev)
    isOrigin = PHOTO.read_u16() > 40000
    if isOrigin and delta > 10000: # 原点通過したら
        n = (n + 1) % NMAX
        ticks_prev = ticks_now
        period = (period + delta) // 2
        delta = 0

    x = (XMAX * delta // period) % XMAX
    print(n, x)  # 本当はここで、シフトレジスタにパターンを送る

回転回数nを時間のように扱いアニメーション表示させると、回転速度によってアニメーションの速さが変わってしまうので、普通にtime.ticks_ms()を使うのが良いかもしれない。

動作確認

以下のコードで動作確認を行った。

mainpy
from rp2 import asm_pio, PIO
from machine import Pin, ADC
from time import ticks_us, ticks_diff

NMAX = 1000 # 回転数の上限
XMAX = 360  # 円周座標の分割数

SER = Pin(13, Pin.OUT)
SCLK = Pin(14, Pin.OUT)
RCLK = Pin(15, Pin.OUT)
PHOTO = ADC(2)


@asm_pio(
    out_init=PIO.OUT_LOW,
    out_shiftdir=PIO.SHIFT_RIGHT,
    sideset_init=(PIO.OUT_LOW, PIO.OUT_LOW),
)
def controlLED():
    pull()
    set(x, 15)
    label("loop")
    out(pins, 1).side(0b00)
    jmp(x_dec, "loop").side(0b01)
    nop().side(0b11)


def ledPattern1(n, x):
    if x < XMAX // 2:
        return 0b0000000011111111 # 外側の半分だけ点灯
    else:
        return 0b1111111100000000 # 内側の半分だけ点灯

def ledPattern2(n, x):
    if x%2==0:
        return 0b1111111111111111 # 全て点灯
    else:
        return 0b0000000000000000 # 全て消灯


sm = rp2.StateMachine(0, controlLED, out_base=SER, sideset_base=SCLK)
sm.active(1)

n = 0  # 回転回数
period = 1  # 一周にかかる時間(us)
ticks_prev = ticks_us()  #  前回原点を通過した時刻

while True:
    ticks_now = ticks_us()
    delta = ticks_diff(ticks_now, ticks_prev)
    isOrigin = PHOTO.read_u16() > 40000
    if isOrigin and delta > 10000:  # 一周した
        n = (n + 1) % NMAX
        ticks_prev = ticks_now
        period = (period + delta) // 2
        delta = 0

    x = (XMAX * delta // period) % XMAX
    sm.put(ledPattern1(n, x))
    #sm.put(ledPattern2(n, x))

これで問題なく、pattern1, pattern2ともに想定通りの動作が確認できた。円周方向の分割数も360分割でも全然問題ない。ただしこれ以上分割してもピクセルサイズがLEDの直径より小さくなってしまうので、意味がなさそうである。なおpattern2の縞模様も肉眼でははっきり綺麗に見えるのだが、写真や動画には上手に映らなかった・・・。[2]


pattern1の実行中

脚注
  1. ゴム板にマスキングテープを貼ったもの。ゴム板を使っているのは、万が一センサーとぶつかることがあっても、大破しないような工夫である。ただし軟からすぎると、動作中に揺れてしまうので注意が必要。 ↩︎

  2. pattern1も露出とシャッタスピードを工夫しないと映らない。 ↩︎

Discussion