Raspberry Pi Pico Wでバーサライタを作成② プログラム
ここでは、MicroPythonを用いて、Raspberry Pi Pico Wでバーサライタ用のプログラムを作成する。以下の記事の続きである。
シフトレジスタ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進数で表現することにする。すると等価な回路は以下の通り。
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命令を実行できるそうで、ぜひ使いこなしたい。
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_init
、sideset_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
を用いて、現在の角度が
- 白いラインの通過中に複数回読み取りが行われるので、一定時間(今回は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()を使うのが良いかもしれない。
動作確認
以下のコードで動作確認を行った。
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の実行中
続きはこちら
Discussion