RP2040中有2个相同的PIO块, 每个PIO块都有专用的连接到总线结构,GPIO和中断控制器.单个PIO块的示意图如图所示.
Pico的PIO Block的功能框图(1/2)
PIO是一种通用的硬件接口,它可以支持多种IO标准.包括实现以下功能:
PIO和处理器一样,都是可以编程的,有两个PIO块,每个块有四个状态机,可以独立执行顺序程序来操作GPIO和传输数据.与通用处理器不同的是,PIO状态机对IO的专业化程度很高(highly specialised),它注重确定性、精确的时序, 并与固定功能硬件紧密结合,每个状态机都配备有以下内容:
以类似软件的方式使状态机可编程,而不是像CPLD那样的完全可配置的逻辑结构,可以在相同的成本和功率范围内提供更多的硬件接口,这也为那些希望通过直接编程而不是使用PIO库中的预制接口来利用PIO的全部灵活性的人提供了一个更熟悉的编程模型和更简单的工具流程.
PIO具有很高的性能以及灵活性,这得益于每个状态机内部精心挑选的一组固定功能硬件.在输出DPI时,当使用48MHz系统时钟运行时,PIO可以在活动扫描线期间维持360Mb/s的速度.在这个例子中,一个状态机负责处理帧/扫描线时序和生成像素时钟,而另一个状态机负责处理像素数据,并解包运行长度编码的扫描线.
状态机的输入和输出最多可以映射到32个GPIO(RP2040限制为30个GPIO),所有状态机都可以独立地同时访问任何GPIO.例如,标准UART代码允许TX/RX/CTS/RTS成为任意四个GPIO.I2C允许SDA/SCL也是如此.可用的自由度取决于给定的PIO程序究竟如何选择使用PIO的引脚映射资源,但至少,一个接口可以自由地选择一些数量的GPIO。
四个状态机从一个共享指令存储器中执行,系统软件将程序加载到这个存储器中,配置状态机和IO映射,然后设置状态机运行.PIO程序的来源多种多样:
从这一点上看.状态机一般是自主的,系统软件通过DMA/IRQ和控制寄存器进行交互,与RP2040上的其它外设一样,对于比较复杂的接口,PIO提供了一套小而灵活的基元,使系统软件可以更多地亲自动手处理状态机控制流程。
PIO状态机执行短小的二进制程序.
在PIO库中可以找到UART/SPI/I2C等常用接口的程序,所以在很多情况下,不需要编写PIO程序.但是,PIO在直接编程时就灵活多了,它支持各种各样的接口,这些接口可能是设计者没有预料到的.
Pico的PIO FIFO(1/2)
下面是一个PIO程序集的例子:
.program squarewave set pindirs, 1 ; Set pin to output again: set pins, 1 [1] ; Drive pin high and then delay for one cycle set pins, 0 ; Drive pin low jmp again ; Set PC to label `again`
PIO汇编器包含在SDK中,名为pioasm.该程序处理一个PIO汇编输入文本文件,其中可能包含多个程序,并写出组装好的程序供使用.对于SDK来说,这些组装好的程序是以C头的形式发出的,其中包含const数组.
在每一个系统时钟周期,每个状态机都会获取/解码并执行一条指令.每条指令恰好需要一个周期,除非它明确地停顿(如WAIT指令),指令还可以在下一条指令执行前插入最多31个周期的延迟,以帮助编写周期精确的程序.
比如pio例子中的squarewave,输出一个12.5MHz的方波(如果系统时钟频率为125 MHz).
主程序可以向指令缓存(32个插槽)写入数据,指令缓存是仅写入的,4个状态机都可以看到.
for (int i = 0; i < count_of(squarewave_program_instructions); ++i) pio->instr_mem[i] = squarewave_program_instructions[i];
状态机自身可以运行得和系统一样快,也可以进行分频,现在让状态机0进行2.5分频.
pio->sm[0].clkdiv = (uint32_t) (2.5f * (1 << 16));
上面的代码片段是一个完整的代码例子的一部分,它驱动一个12.5 MHz的方波从GPIO0输出.我们还可以使用WAIT PIN指令来延缓状态机的执行一段时间,或者使用JMP PIN指令来对引脚的状态进行分支,因此控制流可以根据引脚的状态而变化.
系统可以通过CTRL寄存器随时启动和停止每个状态机,多个状态机可以同时启动,PIO的确定性意味着它们可以保持完美的同步,下方代码就是配置IO方法和启动状态机,实际上我们无需了解这么深入,因为每个SDK给封装了很好的函数,这里目的是为了知晓基本的工作流程.
pio->sm[0].pinctrl = (1 << PIO_SM0_PINCTRL_SET_COUNT_LSB) | (0 << PIO_SM0_PINCTRL_SET_BASE_LSB); gpio_set_function(0, GPIO_FUNC_PIO0); hw_set_bits(&pio->ctrl, 1 << (PIO_CTRL_SM_ENABLE_LSB + 0));9
在PIO内部只有少数的几个寄存器,面向我们编程使用的只有OSR/ISR(输出/输入移位),Scratch(分为X寄存器和Y寄存器).
可以通过PULL显式地把数据从TX FIFO存入OSR,然后使用OUT命令取出数据,可以一次性取出32位,也可以取出1-32位.同理,ISR也是一样,只是PULL改成PUSH,OUT改成IN.
而Scratch是普通寄存器,除了IN/OUT之外,还可以SET和MOV.
Pico的PIO输出移位寄存器(1/2)
Pico的PIO输入移位寄存器
接下来了解下PIO的核心功能.
Side-set 是一种允许状态机改变最多5个引脚的电平或方向的功能,与当前指令同时进行.
一个需要这样做的例子是快速SPI接口,在这里,时钟转换(切换1→0或0→1)必须与数据转换同时进行,即一个新的数据位从OSR转移到GPIO,在这种情况下,一个带Side-set的OUT就可以同时实现这两个功能.(注意是同时设置)
这使得接口的时序更加精确,减少了整个程序的大小(因为不需要单独的SET指令来翻转时钟引脚),也提高了SPI可以运行的最大频率.
Side-set也使得GPIO映射更加灵活,因为它的映射与SET无关.示例中I2C代码允许SDA和SCL被映射到任意两个任意引脚,.通常情况下,SCL切换来同步数据传输,SDA包含被移出的数据位.然而,一些特殊的I2C时序,如开始和停止线条件,需要一个固定的模式在SDA以及SCL上驱动.I2C用来实现这一目的的映射如下:
Side-set -> SCL OUT -> SDA SET -> SDA
具体代码解释(需要在之前声明哪个引脚是Side-set引脚):
.side_set 1 ;声明了1个Bit的Side-set out x, 1 side 0 ; 保持SCK无效状态(主命令:移位读X寄存器1个Bit) mov pins, x side 1 [1] ; 设置SCK(主命令:把上一次[不是上一句]读到的Bit电平挪到引脚,并延迟一个周期.) in pins, 1 side 0 ; 拉低SCK(主命令:把当前引脚输入移入X寄存器)
其他更多的关键字,在后续的代码中再来描述.
Pico的PIO FIFO映射
每个PIO有4个“状态机”。这些状态机的运行方式就像微小的、非常有限的处理器,能够运行共享指令内存中的指令。指令存储器最多可以容纳32条指令。然而,每个状态机都可以从内存中的任何地方提取指令。
例如,您可以创建4个独立的程序,其中有8条指令,每个指令分别在状态机上运行。或者,您可以有一个带有16条指令的程序,该程序在所有4个状态机上运行(其他16条指令槽不做任何事情)。
每个状态机都可以访问2个fifo,您可以使用它们向主处理器(Arm Cortex-M0+)发送和接收数据。缺省情况下,一个FIFO用于传出数据,另一个FIFO用于传入数据。但是,如果您希望将缓冲区大小增加一倍,则可以将它们设置为同时用于传出或同时用于传入。
此外,每个状态机都可以访问由8个中断标志组成的共享银行。它们有多种用途,比如同步状态机或通知CPU某些数据已准备好使用。
最后,每个状态机可以控制RP2040的32个GPIO管脚中的任何一个。然而,状态机要控制一组引脚,必须将这些引脚分组到一个连续的集合中。
每个状态机(请记住:在RP2040中总共有8个状态机——每个PIO中有4个)由一组寄存器和一些控制逻辑组成。
“控制逻辑”就像CPU,但它缺少一个非常重要的组件:算术逻辑单元(ALU)。结果,状态机无法执行大多数数学函数,除了用于计数的非常基本的“增量”函数(例如for循环)。
时钟分频器(clock Div)用于划分RP2040上的主系统时钟。最大时钟速度(不超频)是133 MHz。Raspberry Pi Pico默认运行在125 MHz。时钟分配器允许我们以大约2khz到133mhz的频率运行每个状态机(假设系统时钟是133mhz)。 您还将发现两个用于从fifo发送和接收数据的寄存器,以及用于保存临时数据的scratch (X, Y)寄存器。每个寄存器是32位宽的。
程序计数器(PC)决定状态机下一步应该从共享指令内存中的什么地方读取。每个状态机都有自己的PC,这允许它们彼此独立地运行独立的程序(只要所有程序组合在一起时,单个PIO实例占用的指令不超过32条)。
PIO汇编语言由9条指令组成:JMP、WAIT、IN、OUT、PUSH、PULL、MOV、IRQ和SET。 我们不会在这里全部介绍。我建议阅读RP2040数据表的3.4节。 我们将从简单的SET指令开始,该指令用于独立于主CPU切换一个引脚。我们还将使用一个“NOP”命令(没有操作)。没有NOP命令——PIO库有一个“NOP”的包装器,它将“寄存器Y的内容MOV到寄存器Y”(什么也不完成)。 我们将使用作为PIO汇编语言包装器的rp2 MicroPython模块。它为我们处理大量配置,编写汇编程序看起来很像编写MicroPython函数。注意,默认情况下,rp2随Pico MicroPython固件一起提供,所以我们不需要做任何特殊的安装(除了上传我们在第1部分中所做的MicroPython .uf2固件)。
用PIO闪灯的例子
在一个新的thony文件中,输入以下代码:
import machine import utime import rp2 # Blink state machine program. Blinks LED at 10 Hz (with freq=2000) # 2000 Hz / (20 cycles per instruction * 10 instructions) = 10 Hz # Single pin (base pin) starts at output and logic low @rp2.asm_pio(set_init=rp2.PIO.OUT_LOW) def blink(): wrap_target() set(pins, 1) [19] nop() [19] nop() [19] nop() [19] nop() [19] set(pins, 0) [19] nop() [19] nop() [19] nop() [19] nop() [19] wrap() # Init state machine with "blink" program # (state machine 0, running at 2kHz, base pin is GP25 (LED)) sm = rp2.StateMachine(0, blink, freq=2000, set_base=machine.Pin(25)) # Continually start and stop state machine while True: print("Starting state machine...") sm.active(1) utime.sleep(1) print("Stopping state machine...") sm.active(0) utime.sleep(1)
PIO程序包含在blink()函数中。注意,要在PIO中使用它,必须使用@rp2.asmpio装饰器。在装饰器的参数中,我们设置了引脚方向和FIFO方向。 我们可以在每个状态机中使用一个wrap()和wraptarget()函数。这允许我们在没有JMP指令的情况下自动循环。
set()函数是set指令的包装器。第一个参数是目标,我们有一些选项,如“pin”、“X寄存器”、“Y寄存器”等等。“引脚”是指将指定的GPIO设置为给定值。第二个参数是值。所以,我们用1来设置引脚高,用0来设置引脚低。
每个指令都有一个时钟周期(在除法器之后)。我们最多可以将状态机延迟31个额外周期。我们用函数调用后的括号来实现这一点。所以,set()需要1个周期,我们延迟19个周期,这整行总共需要20个周期。
然后,我们在80个周期(4个nop()命令,每个命令有额外的19个周期延迟)中不做任何操作。
所以,我们设置一个管脚高,等待总共100个周期。然后我们重复这一过程,引脚低电平100个周期。
如果我们设置时钟分频器,使状态机工作在2khz,这意味着我们将以2khz/200 = 10hz的频率闪烁LED。
在程序的主要部分中,我们从rp2模块创建了一个StateMachine对象。第一个参数是我们希望使用的状态机。状态机0到3在PIO 0,状态机4到7在PIO 1。因此,您必须为这个参数选择一个0到7之间的数字。接下来,我们设置所需的频率(对于Pico,在2000到125000000之间),然后是我们希望使用的引脚。
注意,我们可以将多个引脚分配给一个状态机,但它们必须是连续的。当声明StateMachine对象时,我们选择基脚(例如pin 25)。在装饰器中,我们将使用元组来设置管脚的方向。例如(基脚设置为25),setinit=(rp2.PIO.OUTLOW, rp2.PIO.OUT_HIGH)会设置引脚25输出默认的低值,引脚26输出默认的高值。
最后,我们可以使用sm.active(1)和sm.active(0)启动和停止状态机。
如果您运行该程序,您应该看到LED快速闪烁(10hz),但它将每秒钟启动和停止闪烁(正如我们告诉状态机启动和停止)。
使用PIO还有很多我们没有涉及的内容(例如side-setting)。如果您希望了解关于PIO如何工作的更多细节,我鼓励您阅读RP2040数据表。 然而,这并不能阻止我们使用别人的代码!Raspberry Pi在GitHub repo中有许多很棒的PIO示例。让我们使用PWM示例来创建我们自己的MicroPython库。注意,下面的示例来自该存储库。 在一个新的Thonny文件中,输入以下代码:
from machine import Pin from rp2 import PIO, StateMachine, asm_pio @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") class PIOPWM: def __init__(self, sm_id, pin, max_count, count_freq): self._sm = StateMachine(sm_id, pwm_prog, freq=2 * count_freq, sideset_base=Pin(pin)) # Use exec() to load max count into ISR self._sm.put(max_count) self._sm.exec("pull()") self._sm.exec("mov(isr, osr)") self._sm.active(1) self._max_count = max_count def set(self, value): # Minimum value is -1 (completely turn off), 0 actually still produces narrow pulse value = max(value, -1) value = min(value, self._max_count) self._sm.put(value)
保存到你Pico的/lib目录下:piopwm.py 创建一个新的文件:
import utime from piopwm import PIOPWM # Create PIOPWM object used to fade LED # (state machine 0, pin 25, max PWM count of 65535, PWM freq of 10 MHz) pwm = PIOPWM(0, 25, max_count=(256 ** 2) - 1, count_freq=10_000_000) # Loop forever while True: # Fade LED in for i in range(256): pwm.set(i ** 2) utime.sleep(0.01) # Fade LED out for i in reversed(range(256)): pwm.set(i ** 2) utime.sleep(0.01)
在这里,我们导入在PIOPWM模块中创建的PIOPWM类。我们可以使用它来启动一个PIO实例,为我们处理脉冲宽度调制(PWM) ! 我们在引脚25上设置了最大计数65535和状态机频率10mhz。然后我们通过pwm.set()函数增加亮度来淡出板载LED,并通过降低亮度来淡出。 将代码保存为Pico的顶级目录中的main.py。运行它,你应该看到LED慢慢淡入淡出。