关于树莓派Pico里的PIO(Programmable I/O),我已经写了两篇文章:
PIO虽然只有9条指令,功能非常有限,但代码并不容易看懂,更不容易编写了。
官方给出的用PIO实现的PWM函数,理解起来并不容易:
@asm_pio(sideset_init=PIO.OUT_LOW)
def pwm_prog():
pull(noblock) .side(0)
mov(x, osr) # Keep most recent pull data stashed in X, for recycling by noblock
mov(y, isr) # ISR must be preloaded with PWM count max
label("pwmloop")
jmp(x_not_y, "skip")
nop() .side(1)
label("skip")
jmp(y_dec, "pwmloop")
代码中大量的新知识点造成了理解障碍:
- sideset
- pull
- noblock
- OSR
- ISR
我先用控制指令周期的办法写了一个丑陋的程序:
import rp2
import utime
from machine import Pin
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def pwm_10():
set(pins, 1) [9]
set(pins, 0) [19]
nop() [19]
nop() [19]
nop() [19]
nop() [9]
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def pwm_20():
set(pins, 1) [19]
set(pins, 0) [19]
nop() [19]
nop() [19]
nop() [19]
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def pwm_30():
set(pins, 1) [19]
nop() [9]
set(pins, 0) [19]
nop() [19]
nop() [19]
nop() [9]
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def pwm_40():
set(pins, 1) [19]
nop() [19]
set(pins, 0) [19]
nop() [19]
nop() [19]
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def pwm_50():
set(pins, 1) [19]
nop() [19]
nop() [9]
set(pins, 0) [19]
nop() [19]
nop() [9]
sm = rp2.StateMachine(0, pwm_10, freq=20000, set_base=Pin(25))
sm.active(1)
utime.sleep(1)
sm.active(0)
sm = rp2.StateMachine(0, pwm_20, freq=20000, set_base=Pin(25))
sm.active(1)
utime.sleep(1)
sm.active(0)
sm = rp2.StateMachine(0, pwm_30, freq=20000, set_base=Pin(25))
sm.active(1)
utime.sleep(1)
sm.active(0)
sm = rp2.StateMachine(0, pwm_40, freq=20000, set_base=Pin(25))
sm.active(1)
utime.sleep(1)
sm.active(0)
sm = rp2.StateMachine(0, pwm_50, freq=20000, set_base=Pin(25))
sm.active(1)
utime.sleep(1)
sm.active(0)
程序里用指令的延迟控制PWM的占空比为10%,20%,30%,40%和50%的情况,让小灯每隔1秒变亮一点。这代码肯定无法复用。
接下来,我借鉴官方的PWM程序,改写了我的程序:
import rp2
import utime
import machine
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def pwm2():
warp_target()
set(pins, 0)
set(x, 0)
set(y, 9)
label("pwmloop")
jmp(x_not_y, "skip")
set(pins, 1)
label("skip")
jmp(y_dec, "pwmloop")
wrap()
sm = rp2.StateMachine(0, pwm2, freq=2000000, set_base=machine.Pin(25))
sm.active(1)
由于PIO汇编语言的限制,goto指令让代码难以阅读,转换为结构化编程语言,大概是下面这样:
while True:
set(pins, 0)
x = 0
y = 9
while y >= 0:
if(x == y):
set(pins, 1)
y -= 1
我的本意是想得到PWM占空比为1/10(10%)的效果,仔细计算了一下指令周期,好像并不准确。画了下面这个图,总指令周期为24,亮的时候有2个周期,占空比应该是2/24。
由于PIO汇编指令的限制,这里的y最大为31,改变x的值,就可以获得不同的占空比。
现在用sideset改写一下:
import rp2
import utime
import machine
@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW)
def pwm2():
wrap_target()
set(x, 0) .side(0)
set(y, 9)
label("pwmloop")
jmp(x_not_y, "skip")
nop() .side(1)
label("skip")
jmp(y_dec, "pwmloop")
wrap()
sm = rp2.StateMachine(0, pwm2, freq=2000, sideset_base=machine.Pin(25))
sm.active(1)
一开始不理解这个sideset的意思,后面感觉它翻译为“捎带着”可能更容易理解,用set(pins, 0)是占一个指令周期的,而side(0)可以跟着一条指令的后面,捎带着设置针脚的电位,而不占用额外的指令周期,当然也节省下来宝贵的32条指令存储空间。
这里要谈到PIO指令的内部编码了,每个指令用16位二进制编码,见下图:
Delay/sideset共同占用5位。使用了sideset之后,delay最大就不是31了,我上面的程序使用了一个sideset,delay最大只能设置成7了,现在还不明白为什么delay最大不是15?
上面的程序节省了一条指令周期,占空比应该是2/23。
现在的程序仍有2个主要缺点,x值写死了,最大只能为31,y值也是一样,最大为31,更精细地控制PWM是不行的。现在需要OSR和ISR寄存器上场了,看官方的图,我把OSR,ISR和pull()标上了:
再来看官方程序,主程序里给状态机的FIFO队列里添加数据是用sm.put()函数,然后pull()函数将数据存放到OSR里,mov(isr, osr)意思就是 OSR 里的数据再放到 ISR 里,经过这一番初始化,ISR寄存器里就保存了一个初始值(本例为65535)。
@asm_pio(sideset_init=PIO.OUT_LOW)
def pwm_prog():
pull(noblock) .side(0)
mov(x, osr) # Keep most recent pull data stashed in X, for recycling by noblock
mov(y, isr) # ISR must be preloaded with PWM count max
label("pwmloop")
jmp(x_not_y, "skip")
nop() .side(1)
label("skip")
jmp(y_dec, "pwmloop")
pwm_sm = StateMachine(0, pwm_prog, freq=10_000_000, sideset_base=Pin(25))
max_count = 65535
pwm_sm.put(max_count)
pwm_sm.exec("pull()")
pwm_sm.exec("mov(isr, osr)")
pwm_sm.active(1)
函数pwm_prog 的意思大概是这样:
(1)pull(noblock) .side(0)
从FIFO队列里取一个32位整数,放到OSR寄存器里,noblock的意思就是不阻塞,如果FIFO里没数据,程序继续向下执行。同时side(0)捎带着把sideset引脚置0,本例的sideset是25针,在sideset_base里设置。
(2)mov(x, osr)
OSR里的数保存到x寄存器里,没有用set()函数,x值的上限可以是65535。
(3)move(y, isr)
还记得isr里已经初始化了max_count(本例为65535),再把它保存到y寄存器里。
(4)后面的循环体,前面已经解释过了
现在不断向FIFO里保存0~65535的数,实现占空比的平缓增加和降低,来实现小灯的渐亮和渐暗。
while True:
for i in range(255):
pwm_sm.put(i*i)
sleep(0.001)
for i in reversed(range(255)):
pwm_sm.put(i*i)
sleep(0.001)
推荐阅读:
树莓派Pico开发系列文章