BPI-Pico-RP2040 开发板所搭载的RP2040微处理器,相对于其他微处理,最吸引人的一点就是提供了可编程 IO,简称PIO。
PIO的神奇之处在于,给PIO编程后,它可以自己运行来控制IO口,而不会再占用微处理器的资源,使得同时可以完成其他的逻辑,而相互之间可以完全不干扰。
在官方手册中,PIO单独用了一章,说明其重要性:
通过官方资料,可以了解到,RP2040提供了两个PIO控制块,每个PIO块中包含4个状态机,都可以通过程序进行控制:
要使用PIO,可以用官方提供的C-SDK来调用,也可以在MicroPython或者CircuitPython中调用。我在BPI-Pico-RP2040 开发板上使用的是circuitpython,所以下面的实例,都是在CircuitPython进行的。
一、环境配置
开始前,要先设置好CircuitPython开发环境。
我用的开发工具是Thonny,在Thonny中,可以直接给BPI-Pico-RP2040 开发板烧录CircuitPython固件。
按住开发板的BOOT按键,然后重新连接到电脑,就能在Thonny中进行CircuitPython的安装了:
然后在Thonny的插件管理中,安装adafruit-circuitpython-pioasm和adafruit_blinka:
再从Thonny的插件数据目录中,拷贝对应的库到CIRCUITPY U盘中lib目录下即可:
这样就准备好了PIO运行的环境,可以继续后面的操作了。
二、PIO点灯
因为是初探PIO的使用,而玩板子无不从点灯开始,所以这里也从点灯开始。
为了方便后续的操作,我把BPI-Pico-RP2040 开发板安装到一个扩展板上了:
从BPI-Pico-RP2040 开发板官方手册得知,BPI-Pico-RP2040 开发板板载了一个可以用户控制的LED,连接到了引脚GPIO-25,那我们就用一段PIO相关的程序,来点亮它。
import board
import rp2pio
import adafruit_pioasm
import time
# 定义PIO控制代码
led_blink = """
.program led_blink
loop:
pull
out pins, 1
jmp loop
"""
# 编译处理
led_blink_assembled = adafruit_pioasm.assemble(led_blink)
# 设置状态机
sm = rp2pio.StateMachine(
led_blink_assembled,
frequency=1000_000,
first_out_pin=board.GP25,
)
while True:
# 发送数据到状态机
sm.write(bytes((1,)))
time.sleep(1)
# 发送数据到状态机
sm.write(bytes((0,)))
time.sleep(1)
在BPI-Pico-RP2040 开发板上运行上述代码后,板载的LED就会闪烁起来了:
在上述代码中,led_blink定义的就是PIO控制代码。如果熟悉汇编的话,会发现,这和汇编非常相近。是的,你可以认为他就是RP2040的PIO专属的汇编指令。
设置好了PIO控制代码,再用adafruit_pioasm.assemble()进行编译处理,最后再使用rp2pio.StateMachine()设定好,对哪个GPIO端口启用PIO控制。
而主循环部分,则交替发送1和0到PIO,从而控制LED的亮灭。
三、PIO指令
上述代码中的PIO控制代码部分,我们详细看一下:
.program led_blink
loop:
pull
out pins, 1
jmp loop
控制代码,我们先声明要定义一段程序led_blink,然后设置了一个循环loop,是个死循环,没有任何延时,一直运行。放心,他只是在PIO控制块内运行,对开发板的微处理器没有任何影响。
然后,使用pull指令,把CircuitPython发送给PIO的数据取得,再使用out指令,发送1字节的数据输出给对应的GPIO口,实际上最终就是高低电平信号了。
因为我们在CircuitPython代码中,交替发送了1和0过来,所以,PIO也交替的让GPIO处于高电平和低电平的状态,最终就是LED闪烁了。
PIO的专用指令,一共有9条:
在这次初探中,使用到了如下的指令:
四、PIO控制亮度
说好了PIO可以自己运行来控制IO口,可上面的点灯还是需要Python程序来发送数据点亮,这不科学。
那下面,就用一段程序,来演示PIO自己的控制。
import board
import rp2pio
import adafruit_pioasm
import time
# 定义PIO控制代码
led_brightness = """
.program led_brightness
set pins, 0
set pins, 1
"""
# 编译处理
led_brightness_assembled = adafruit_pioasm.assemble(led_brightness)
while True:
# 设置状态机
sm = rp2pio.StateMachine(
led_brightness_assembled,
frequency=1000_000,
first_set_pin=board.GP25
)
time.sleep(1)
# 释
sm.deinit()
运行上述程序后,可能会发现,就是LED点亮了,其他没有啥变化。
这个时侯,你可以考虑一下,使用下面的PIO控制代码,看看有什么变化:
常亮:
.program led_brightness
set pins, 1
长灭:
.program led_brightness
set pins, 0
亮度:
.program led_brightness
set pins, 0 [8]
set pins, 1
最后一段控制代码内,可以尝试修改8
为其他值。
你会发现,能控制LED最亮、熄灭,以及不同的亮度。
在上述的PIO控制指令中,set pins, 0
表示设置低电平,set pins, 1
设置高电平,如果我们用下面的控制指令:
.program led_brightness
set pins, 0
set pins, 0
set pins, 0
set pins, 0
set pins, 0
set pins, 0
set pins, 0
set pins, 0
set pins, 0
set pins, 1
就会发现,LED比最亮的时候,按了不少。
因为是汇编指令,所以上面这段控制指令,其结果,就是连续9个执行周期,设置低电平,然后1个执行周期,设置高电平。
如果熟悉PWM的话,你马上知道,这不就是那个味----占空比吗。
是的,使用PIO,可以不用PWM,来控制占空比,从而控制LED的亮度。
上面的代码,相当于是10%的占空比。而一对set pins, 1
和一个set pins, 0
,则相当于是50%的占空比。可以用多行来设置不同的占空比。
但是写多行,太麻烦了,所以set pins, 0 [8]
来帮助了。后面[8]
表示执行完set pins, 0
后,再经过8个周期,才继续执行后面的指令,也就是相当于9:1,占空比还是10%。
这个部分,最开始的CircuitPython的程序,如果只是控制LED的亮度的话,那么可以不用写入到循环中。
然而,如果我们用两个不同亮度的PIO控制代码,来分别调用的话,就实现了不同亮度之间的变化了。大家可以自己试一试,该怎么实现。
五、呼吸灯
最后,再来一段纯PIO控制实现的呼吸灯,具体代码如下:
import board
import rp2pio
import adafruit_pioasm
import time
import array
# 定义PIO控制代码
led_breathe = """
.program led_breathe
start:
up:
set x, 31
loop11:
mov y, x
set pins, 0 [5]
loop12:
jmp y-- loop12
set pins, 1 [5]
jmp x-- loop11
down:
set x, 31
loop21:
mov y, x
set pins, 1 [5]
loop22:
jmp y-- loop22
set pins, 0 [5]
jmp x-- loop21
end:
jmp start
"""
# 编译处理
led_breathe_assembled = adafruit_pioasm.assemble(led_breathe)
# 设置状态机
sm = rp2pio.StateMachine(
led_breathe_assembled,
frequency=2_000,
first_set_pin=board.GP25,
wait_for_txstall=False,
)
while True:
time.sleep(1)
在上述代码中的PIO控制指令部分,有两个指令说明一下:
x--
、y--
,表示当其值不为0的时候跳转PIO控制指令部分,先用y值来控制set pins, 0
的执行次数,再用其来控制set pins, 1
的执行次数,从而实现了占空比的交替变化,使用x来控制每次变化执行的时长。
执行后,具体效果如下:
六、总结
因为只是初探,所以始于点灯,至于点灯。
RP2040的PIO所能发挥的功能,远远不止上述点灯,如果好好发掘,还会有很多意想不到的用处。
更多回帖