树莓派 Pico 之可编程 IO(PIO)

在这篇教程中,我们看到的代码看起来与其他的部分的代码非常不同。那是因为我们大多数时候不得不在 MCU 的底层处理事情。大多数时候,MicroPython 可以隐藏很多在微控制器上工作的复杂性。
当我们 print(“hello”) 的时候,我们不必担心微控制器存储字母的方式,或它们被发送到串行终端的格式,或串行终端所接受的时钟周期的数量。这些都是在后台处理的。然而,当我们进入可编程输入和输出时(PIO),我们需要在更低的层次上处理这些逻辑。

我们将简要介绍 PIO,并介绍一些高级主题,以便你了解正在发生的事情,并希望了解 Pico 上的 PIO 相对于其他微控制器是如何突显优势的。但是,理解创建 PIO 程序所需的所有低级数据操作需要花时间来完全理解,所以如果它看起来有点不透明也不用担心。如果你对处理这种低级编程感兴趣,那么我们将帮助你入门。如果你更感兴趣的是在更高的层次上工作,而宁愿把低层次的争论留给其他人,我们将向你展示如何使用 PIO 程序。

数据输入和数据输出

树莓派 Pico 不仅支持 SPI 和 I2C 控制器发送数据。它还有自己的特殊协议:可编程 IO。让我们看一个例子:

from rp2 import PIO, StateMachine, asm_pio
from machine import Pin
import time

@asm_pio(set_init=PIO.OUT_LOW)
def led_quarter_brightness():
	set(pins, 0)[2]
	set(pins, 1)

@asm_pio(set_init=PIO.OUT_LOW)
def led_half_brightness():
	set(pins, 0)
	set(pins, 1)

@asm_pio(set_init=PIO.OUT_HIGH)
def led_full_brightness():
	set(pins, 1)

sm1 = StateMachine(1, led_quarter_brightness, freq=10000, set_base=Pin(25))
sm2 = StateMachine(2, led_half_brightness, freq=10000, set_base=Pin(25))
sm3 = StateMachine(3, led_full_brightness, freq=10000, set_base=Pin(25))

while(True):
	sm1.active(1)
	time.sleep(1)
	sm1.active(0)
	sm2.active(1)
	time.sleep(1)
	sm2.active(0)
	sm3.active(1)
	time.sleep(1)
	sm3.active(0)

这些方法实际上是运行在 PIO 状态机上的小程序,且在不断循环。例如,led_half_brightness() 会不断地打开和关闭 LED,这样 LED 就会有一半的时间是关闭的,一半的时间是打开的。led_full_brightness() 将类似地循环,但由于惟一的指令是打开 LED,这实际上并没有改变任何东西。

这里稍微有点不寻常的是 led_quarter_brightness()。每个 PIO 指令只需要运行一个时钟周期(可以通过设置频率来更改时钟周期的长度,稍后我们将看到)。但是,我们可以在一条指令之后用方括号添加一个 1 到 31 之间的数字,来告诉 PIO 状态机在运行下一条指令之前暂停这个时钟周期。

然后,在 led_quarter_brightness() 中,两个 set 指令每个使用一个时钟周期,延迟使用两个时钟周期,因此总循环使用四个时钟周期。在第一行中,set 指令需要一个周期,延迟需要两个周期,所以 GPIO 管脚在这四个周期中的三个是关闭的。这使得 LED 的亮度达到了持续亮灯的四分之一。

一旦设定了 PIO 程序,Pico 就需要将其加载到状态机中。因为我们有三个程序,所以需要将它们加载到三个状态机中(有 8 个状态机可以使用,编号为 0-7)。这可以用下面一行来实现:

sm1 = StateMachine(1, led_quarter_brightness, freq=10000, set_base=Pin(25))

参数如下:
– 状态机器编号
– PIO 程序加载
– 频率(必须在 2000 到 125000000 之间)
– 状态机器操纵的 GPIO 引脚

还有一些额外的参数,你会在其他程序中看到,我们这里不需要。 一旦创建了状态机,就可以使用 active 方法启动和停止状态机,1 表示启动,0 表示停止。

在我们的循环中,我们循环三个不同的状态机。

一个真实的例子

让我们通过一个实际示例来看看使用 PIO 的方法。WS2812B LED 灯条是一种包含三个 LED(一个红色、一个绿色、一个蓝色)和一个小型微控制器的灯组。它们由一根数据线控制,带有计时相关协议。
LED 的接线很简单,可能有一个插座,可以把头部电线推进去,或者你可能需要自己焊接它们。

你需要注意的是从 Pico 上的 5V 引脚获得的功率是有限的,如果无限扩展这个灯条,则需要额外给灯条供电。

现在我们已经把灯条和 Pico 连接好了,让我们看看如何用 PIO 来控制它:

import array, time
from machine import Pin
import rp2
from rp2 import PIO, StateMachine, asm_pio

# Configure the number of WS2812 LEDs.
NUM_LEDS = 10

@asm_pio(sideset_init=PIO.OUT_LOW, out_shiftdir=PIO.SHIFT_LEFT, autopull=True, pull_thresh=24)
def ws2812():
	T1 = 2
	T2 = 5
	T3 = 3
	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]

# Create the StateMachine with the ws2812 program, outputting on Pin(22).
sm = StateMachine(0, ws2812, freq=8000000, sideset_base=Pin(0))

# Start the StateMachine, it will wait for data on its FIFO.
sm.active(1)

它的基本逻辑是每秒发送 800,000 位数据(注意频率是 8000000,程序的每个周期是 10 个时钟周期)。每一位数据都是一个脉冲——一个短脉冲表示 0,一个长脉冲表示 1。这个程序和我们之前的 程序之间的一个很大的区别是,MicroPython 需要能够向这个程序发送数据 PIO 的程序。

数据进入状态机有两个阶段。第一个是称为先进先出(FIFO)的内存。这是我们的主 Python 程序发送数据到的地方。第二个是输出移位寄存器(OSR)。这就是 out() 指令获取数据的地方。两者通过拉指令连接,拉指令从 FIFO 获取数据并将其放在 OSR 中。然而,由于我们的程序设置了启用 autopull 的阈值为 24,所以每次我们从 OSR 读取 24 位时,它将从 FIFO 重新加载指令 out(x,1) 从 OSR 中获取一位数据,并将其放入名为 x 的变量中(PIO 中只有两个可用变量:x 和 y)。

jmp 指令告诉代码直接移动到特定的标签,但是它可以有一个条件。指令 jmp(not_x,”do_zero”) 告诉代码,如果 x 的值为 0(或者,在逻辑术语中,如果 not_x 为真,并且 not_x 是 x 的 反面——在 pio 级别中,0 为假,任何其他数字为真),则移动到 do_zero。

这里我们一直忽略的一个方面是 .side() 位。它们与 set() 类似,但它们与另一条指令同时发生。 这意味着 out(x,1) 发生时,.side(0) 设置侧集引脚的值为 0。

哇,对于这么小的一个程序来说,这实在是太多了。现在我们已经激活了它,让我们看看如何使用它。下面的代码需要在程序中位于上述代码之下,以便将数据发送到 PIO 程序。

# Display a pattern on the LEDs via an array of LED RGB values.
ar = array.array("I", [0 for _ in range(NUM_LEDS)])

print("blue")
for j in range(0, 255):
	for i in range(NUM_LEDS):
		ar[i] = j
	sm.put(ar,8)
	time.sleep_ms(100)

print("red")
for j in range(0, 255):
	for i in range(NUM_LEDS):
		ar[i] = j<<8
	sm.put(ar,8)
	time.sleep_ms(100)

print("green")

for j in range(0, 255):
	for i in range(NUM_LEDS):
		ar[i] = j<<16
	sm.put(ar,8)
	time.sleep_ms(100)

print("white")
for j in range(0, 255):
	for i in range(NUM_LEDS):
		ar[i]=j<<16+j<<8+j
	sm.put(ar,8)
	time.sleep_ms(100)

在这里,我们跟踪一个名为 ar 的数组,它保存了我们希望 LED 拥有的数据。数组中的每个数字都包含了一盏灯上所有三种颜色的数据。格式有点奇怪,因为它是二进制的。使用 PIO 的一个问题是,你经常需要处理单个数据位。每一位数据 都是 1 或 0,数字可以通过这种方式建立,所以以 10 为基数的 2 就是二进制的 10。以 10 为基数的 3 在二进制中等于 11。二进制数的 8 位中最大的数是 11111111,或者以 10 为基数的 255。我们不会在这里深入讨论二进制。让人更困惑的是,我们实际上把三个数字存储在一个数字中。这是因为在 MicroPython 中,整数存储在 32 位,但每个数字只需要 8 位。最后还有一点空闲空间,因为我们只需要 24 位,不过没关系。

前八位是蓝色,后八位是红色,最后八位是绿色。8 位最多可以存储 255 个数字,所以每个 LED 都有 255 个亮度级别。我们可以使用移位运算符 << 来实现这一点。这将在一个数字的末尾加上一 定数量的 0,所以如果我们想让 LED 的红色、绿色和蓝色亮度达到 1 级,我们将每个值都设为 1,然后将它们移动到合适的位数。对于绿色,我们有:

1 << 16 = 10000000000000000

对于红色,我们有:

1 << 8 = 100000000

对于蓝色,我们根本不需要移位位,所以我们只有 1。如果我们把所有这些加在一起, 我们得到以下(如果我们把前面的位加起来,得到一个 24 位的数):

000000010000000100000001

最右边的八位是蓝色的,接下来的八位是红色的,最左边的八位是绿色的。最后一点可能看起来有点令人困惑的是这行:

ar = array.array("I", [0 for _ in range(NUM_LEDS)])

这创建了一个数组,第一个值是 I,然后每个 LED 都是 0。在开头有一个I的原因是它告诉 MicroPython 我们使用的是一系列 32 位的值。但是,对于每个值,我们只需要将 24 位发送给 PIO,所以我们告诉 put 命令删除 8 位:

sm.put(ar,8)

相关说明

PIO 状态机使用的语言非常简洁,所以只有少量的指令。除了我们已经看过的,你还可以使用:
in():移动 1 到 32 位到状态机)与out()类似,但相反)。
push():将数据发送到连接状态机和主存的内存中 MicroPython 程序。
pull():从连接状态机和主存的内存块中获取数据 MicroPython 程序。这里我们没有使用它,因为通过在程序中包含 autopull=True,当我们使用 out() 时,会自动发生这种情况。
mov():在两个位置之间移动数据(例如 x 和 y 变量)。
irq():控制中断。如果你需要触发一个特定的东西以在程序的 MicroPython 端运行,就可以使用这些。
wait():暂停直到发生一些事情(例如 IO pin 更改了一个设定值或中断发生)。

虽然只有少量的指令,但可以实现大量的通信协议。大多数指令都是用于以某种形式移动数据。如果你需要以任何特定的方式准备数据,例如操纵你希望 LED 的颜色,这应该在你的主 MicroPython 程序中完成,而不是在 PIO 程序中。

你还可以:
查看系列教程中的其他文章「树莓派 Pico 上手指南(在 Pico 上使用 MicroPython)」
购买本教程所用到的 Pico 上手套件



坐沙发

发表评论

你的邮件地址不会公开


*