树莓派48/100 - 深入研究WS2812彩灯PIO汇编程序

关于树莓派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,见下图:
树莓派48/100 - 深入研究WS2812彩灯PIO汇编程序
如果高电平持续时间小于低电平持续时间,表示0;低电平持续时间长,表示1。持续时间也有严格的范围,以微秒来计。我查到的WS2812B中文手册与国外论坛里看到了指标参数有点差别。

树莓派48/100 - 深入研究WS2812彩灯PIO汇编程序
主程序里pioasm状态机的频率为8M赫兹,这样一个汇编指令周期为1/8000000=0.125 us(微秒),看代码里的T1、T2和T3分别为2、5、3,也就对应着0.25 us、0.625 us和0.375 us。

这三个时间的含义如下图:

树莓派48/100 - 深入研究WS2812彩灯PIO汇编程序
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值(注意顺序是:绿、红、蓝)要一个紧接着一个快速发出去,如果稍有延迟,会被认为是下一组彩灯刷新周期。

树莓派48/100 - 深入研究WS2812彩灯PIO汇编程序
我看了一下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上能否正常工作。

上一篇:英语作业四级(考前一)


下一篇:2022年前端面试集