内容目录:
一、可编程IO与PIO汇编器
二、RP2040 MCU的PIO、State Machine与pioasm
三、Pico开发板MicroPython嵌入pioasm汇编混合编程示例
一、可编程IO与PIO汇编器
树莓派Pico开发板所使用的微控制器(MCU)型号是RP2040双核MCU,它提供了可编程IO(PIO, Programmed I/O)。
许多开发板都有针对特定通信方式设置好的GPIO引脚,如I2C、SPI、UART等,但是如果想增加此类引脚的数量,甚至使用一些更特殊的通信协议如I2S、VGA等,可能我们只能使用软件来进行模拟,即用主控制器用软件控制某些引脚位来模拟一些特殊的通信协议接口,也叫[Bit Banging]。然而,每种通信协议的通信速率不同,采用软件模拟会占用MCU资源,并不是十分高效。为解决此问题,RP2040 提供了两组PIO,相当于独立的Mini MCU,我们可以采用称为PIO汇编器(PIO Assembler, pioasm)的组合语言来设定GPIO引脚的功能。
可编程IO引脚并非新概念,它在FPGA、CPLD中广泛使用,树莓派Pico开发板也引入了类似的可编程IO引脚概念,并可采用MicroPython与pioasm汇编组合的方式进行编程。
二、RP2040 MCU的PIO、State Machine与pioasm
RP2040 有两个PIO,理论上,每个PIO可以存储为32条pioasm 指令,并且能分成四个状态机(State Machine0~ State Machine3),见图1所示。
图1
这些State Machine共用同一個PIO的指令,但能彼此独立操作,甚至能以不同的速度运行,它们也是我们操作PIO的主要使用对象,图2是其中一个State Machine结构示意图。
图2
从图2可知,State Machine内部主要包含以下部件:
(1) 32-bit In Shift Register(ISR)和 Out Shift Register(OSR):32位输入移位寄存器和输出移位寄存器;
(2) 32-bit X/Y Scratch Register: 32位X/Y暂存器;
(3) Program Counter (PC): 指向pioasm的程序(位置)计数器;
(4) Clock Divider : 时钟除法器,设置为常数,State Machine的执行时钟频率为MCU时钟频率除以此常数。
Clock Divider 的取值范围1~65536,增減最小单位为1/256。若Pico使用预设的 125 MHz时钟,则State Machine可执行的最低时钟频率为125000000/65536 = 1908Hz。
图3
State Machine与外界的联络借助于两个FIFO总线相连的FIFO Input及FIFO Output(4 x 32 bits),它们分別用来与外界接收或发送数据。从外界接收的输入数据存放在ISR,而要发送的数据则存放到OSR。X和Y则是允许用户使用的暂存器(图3,参考文献[1])。
通过此架构并采用 pioasm来读写暂存器中的数据,State Machine 可以视情況用 1 或 0 设定或读取引脚位的高/低电平,以达到预期的使用目标。
基于状态机的9条pioasm汇编指令如下(参考文献[1]):
(1)in: Shifts 1 word of 32 bits at a time into the ISR from another location
(2)out: Shifts 1 word of 32 bits from the OSR to another location
(3)push: Sends data to the RX (input) FIFO
(4)pull: Gets data from the TX (output) FIFO
(5)mov: Moves data from one location to another
(6)irq: Sets or clears interrupt flag
(7)set: Writes data to destination
(8)wait: Pauses until a defined action happens
(9)jmp: Jumps to a different point in the code
三、MicroPython嵌入pioasm汇编混合编程示例
下面通过板上LED发光示例来说明MicroPython组合pioasm汇编指令编程的基本应用。
程序示例:MicroPython组合pioasm汇编控制树莓派Pico板上LED间断闪亮。
程序清单如下:
# Filename: main.pyimport rp2from machine import Pin@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)def on_Board_LED_Blink():wrap_target()set(pins, 1) [31]nop()[31]nop()[31]nop()[31]nop()[31]set(pins, 0) [31]nop()[31]nop()[31]nop()[31]nop()[31]wrap()sm = rp2.StateMachine(0, on_Board_LED_Blink, freq=2000, set_base=Pin(25))sm.active(1)
图4
Pico板上LED使用的是GPIO 25口线。可用Thonny解释器编辑本程序,将Pico开发板连接到PC机,运行程序(见图4),可以看到Pico板上LED间断闪亮。关于使用Thonny编程器编写MicroPython程序控制板上LED闪亮,可参见参考文献[2]。
本程序可以分成两个部分:
(1)首先是定义一个on_Board_LED_Blink()函数,并在前面加上 rp2.asm_pio() 装饰器:
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
装饰器可以替函数套上额外的功能,这里就是为函数on_Board_LED_Blink()套上额外的功能。关于装饰器,有些 Python书籍中有专门介绍,此略。这里on_Board_LED_Blink()函数的內容就是我們要编写的pioasm汇编程序;MicroPython 将它包装成Python函数的形式。装饰器有一个参数set_init。
(2)接下来是用rp2.StateMachine方法建立一个状态机对象,并输入一些基本参数:
sm = rp2.StateMachine(0, blink, freq=2000, set_base=Pin(25))
这里,第一個参数id表示State Machine编号(0~7),第2个参数是包含pioasm程序的Python函数,接着是其他一系列参数,取决于应用需要,本程序只设定了State Machine的执行速度(2000 Hz)以及set指令的base pin 或GPIO起始引脚位(GPIO 25)。
最后在主程序中通过State Machine对象的active方法启动状态机,以使pioasm汇编程序发挥作用:
sm.active(1)
若在程序中需要关闭状态机,则使用sm.active(0)方法。
下面对本例程序中的pioasm汇编程序进行说明:
wrap
首先,pioasm 程序的首尾格式为:
wrap_target()
…
wrap() # 跳转到wrap_target()
当pioasm执行到wrap时,它将自动跳回到wrap_target的位置,构成一个无限循环。当然这不是唯一的循环写法,我们也可以按以下方式设定一个label和使用jmp 指令实现类似的功能:
label(‘loop’)
…
jmp(‘loop’) # 跳转到’loop’
这二者的差別在于,jmp(jump)指令执行时需要用到State Machine的1个周期(1 cycle)处理时间,而wrap 无需花费任何时间。当然jmp也可依条件決定是否要跳转到特定的label。
由于本程序设置State Machine以2000Hz速度执行,因此1个周期的时间就是1/2000 = 0.0005秒(0.5 ms)。
set
set 用來設置GPIO引脚位的逻辑电平:
set(pins, 1) # 设置高电平
set(pins, 0) # 设置低电平
pins是pioasm自动解释的一个名称,代表set指令要写入的引脚位,它可通过rp2.StateMachine()的set_base参数指定,这里就是本例中的GPIO 25。当然, set也可以同时设置多个引脚,这就是pins 表示成复数的原因。
这里提醒注意的是,asm_pio装饰器有一个set_init参数:
set_init=rp2.PIO.OUT_LOW
其含义是由set_base参数指定的GPIO引脚位,其初始状态将设置为低电平。
nop与delay
本程序中有好几行nop(),它是no operation空操作的意思。nop其实表示以下指令:
mov(y, y)
mov(move)传送指令是把一个来源数据传送到另一个地方,这里的来源跟目的地都是Y暂存器,那就什么事情也不会发生,它仅仅只是多延迟1个周期的时间。
我们注意到,所有的set和nop 后都会跟一个中括号数字:
set(pins, 1) [31]
nop() [31]
该数字就是delay延时,也就是要进一步延时的cycle周期数量,这里中括号与前面的指令相隔多少个空格没做统一要求,多少个空格都可以。
在 Python中,中括号是列表、字典等容器取值时的语法,Pico的MicroPython语法巧妙地用它重现了pioasm的指令风格。
[31]表示将额外延时31个周期数(31 cycles),而指令本身要占1个周期数(1 cycle)。因此,本程序除了wrap_target/wrap之外,每一行set和nop的执行时间都是32个周期(32 cycles)。整个程序重复一轮的总时间是32x10=320cycles,而320 x 0.5ms= 160ms(毫秒)。
由于pioasm程序平均分成了两部分,也就是LED的点亮或熄灭,因此LED将每80毫秒点亮或熄灭一次:
wrap_target()# GPIO 25引脚设置为高电平并等待80毫秒,板上LED灯点亮set(pins, 1) [31]nop()[31]nop()[31]nop()[31]nop()[31]#GPIO 25引脚设置为低电平并等待80毫秒, 板上LED灯熄灭set(pins, 0) [31]nop()[31]nop()[31]nop()[31]nop()[31]# 重复pioasm汇编程序wrap()
本示例程序相当于下面的控制板上LED点亮/熄灭的MicroPython程序:
from machine import Pinimport timeled = Pin(25, Pin.OUT)while True:led.value(1) # led.on()time.sleep_ms(80)led.value(0) # led.off()time.sleep_ms(80)
from machine import Pin
import time
led = Pin(25, Pin.OUT)
while True:
led.value(1) # LED ON
time.sleep_ms(80)
led.value(0) # LED OFF
time.sleep_ms(80)
80 毫秒延时时间比较短暂,我们可增加等待的cycle周期数或者进一步降低Pico开发板中的RP2040 MCU的时钟周期,这样能适当延长LED间断闪亮的时间间隔。但是,由于pioasm汇编程序的指令数量有限,理论上最多只能达到32条指令,因此要想达到几百毫秒甚至数秒级的更大延时,可采用循环计数等策略实现之。
参考文献:
[1] Programmable I/O with Raspberry Pi Pico.
/blog//01/25/programmable-io-with-raspberry-pi-pico/
[2] 博主CSDN博文.Raspberry Pi Pico实践系列2-基于Thonny和MicroPython的树莓派Pico板上LED控制编程实践
发布日期:07月17日