关于树莓派Pico里的PIO(Programmed I/O)编程,前面写过4篇文章:
以前的WS2812彩灯程序感觉像天书,根本看不懂,现在可以揭开它神秘的面纱了。
import machine
import utime
import array
import rp2
@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT,
autopull=True, pull_thresh=24)
def ws2812():
T1 = 2
T2 = 5
T3 = 3
wrap_target()
label("bitloop")
out(x, 1) .side(0) [T3 - 1]
jmp(not_x, "do_zero") .side(1) [T1 - 1]
jmp("bitloop") .side(1) [T2 - 1]
label("do_zero")
nop() .side(0) [T2 - 1]
wrap()
# 建立状态机,设置输出针的编号
sm = rp2.StateMachine(0, ws2812, freq=8_000_000, sideset_base=machine.Pin(15))
sm.active(1)
NUM_LEDS = 30
ar = array.array("I", [0 for _ in range(NUM_LEDS)])
while True:
for j in range(NUM_LEDS):
(r, g, b) = (0, 0, 0)
if(j%3 == 0): r = 11
if(j%3 == 1): g = 11
if(j%3 == 2): b = 11
ar[j] = g << 16 | r << 8 | b
sm.put(ar, 8)
utime.sleep_ms(20)
为了研究pioasm程序的汇编代码,我简化了主程序,只让30个灯依次显示红、绿、蓝三色。
先要了解WS2812特殊的信号机制,与平常的高电位为1、低电位为0的方式不一样,它的协议按高、低电位持续时间的长短来表示0和1,见下图:
如果高电平持续时间小于低电平持续时间,表示0;低电平持续时间长,表示1。持续时间也有严格的范围,以微秒来计。我查到的WS2812B中文手册与国外论坛里看到了指标参数有点差别。
主程序里pioasm状态机的频率为8M赫兹,这样一个汇编指令周期为1/8000000=0.125 us(微秒),看代码里的T1、T2和T3分别为2、5、3,也就对应着0.25 us、0.625 us和0.375 us。
这三个时间的含义如下图:
T0L=T2+T3=1.0us,在指标范围(0.58us ~ 1.6us)内,T1H=T1+T2=0.875,也在指标范围(0.58us ~ 1.6us)内。在T1时间段里永远是高电位,在T3段永远是低电位,T2时间内的电位对应着表示的数据:0或1。
现在可以看汇编代码了,out_shiftdir=rp2.PIO.SHIFT_LEFT 表示OSR里的数据向左移位,autopull=True, pull_thresh=24 表示自动从OSR里取数,而且只取24位(对应着RGB值)。
out(x, 1) 表示从OSR里移出1位数据,放入X寄存器里,后面的.side(0)设置低电位,[T3-1]补满T3指令周期。
jmp(not_x, “do_zero”)的意思是当X为0时跳到 do_zero 位置的代码,当X不为0时,继续执行下面的语句。
x不为0时,执行到 jmp(“bitloop”) 这一句,准备跳转到程序的最开始,跳转前捎带着完成 side(1),因为x不为0,所以side(1),逻辑正确,总延迟时间为T2。
x为0的时候,执行 nop().side(0) 指令,总延迟时间也是T2。
主程序用 array.array(“I”, list) 初始化一个数组,现代计算机的 int 类型一般都是4字节(32位)的整数,用来设置灯组的RGB值(24位)。
sm.put(ar, 8) 用于快速把数组中的所有数据送到状态机的FIFO队列里,第二个参数8表示数据先左移8位,再送到FIFO里。一组彩灯刷新周期里,24位的GRB值(注意顺序是:绿、红、蓝)要一个紧接着一个快速发出去,如果稍有延迟,会被认为是下一组彩灯刷新周期。
我看了一下WS2812B的规格参数,T1=T2=T3=0.375时,也符合各项指标要求,应该也是可以工作的,所以我把程序简化了一下,状态机仍工作在8M Hz频率下,运行完全没问题:
@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT,
autopull=True, pull_thresh=24)
def ws2812():
label("bitloop")
out(x, 1) .side(0) [2]
jmp(not_x, "do_zero") .side(1) [2]
jmp("bitloop") .side(1) [2]
label("do_zero")
nop() .side(0) [2]
如果让状态机工作在2.7M频率下,那么延迟参数也可以省了:
@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT,
autopull=True, pull_thresh=24)
def ws2812():
label("bitloop")
out(x, 1) .side(0)
jmp(not_x, "do_zero") .side(1)
jmp("bitloop") .side(1)
label("do_zero")
nop() .side(0)
sm = rp2.StateMachine(0, ws2812, freq=2_700_000, sideset_base=machine.Pin(15))
sm.active(1)
修改的代码在我的WS2812B工作正常,但不知道在旧款的2812上能否正常工作。