树莓派46/100- 再探Pico里的PIO(Programmable I/O),看懂PWM里的汇编代码

关于树莓派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开发系列文章

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

申龙斌

撸代码来深夜,来杯咖啡钱

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值