在这个附录中,我们将看到一些代码,它们看起来与我们在本书其余部分中处理的代码非常不同。那是因为我们必须在低水平上处理事情。大多数时候,MicroPython可以隐藏很多在微控制器上工作的复杂性。

当我们做这样的事情时:

print(“hello”)

..我们不必担心微控制器存储字母的方式,或它们被发送到串行终端的格式,或串行终端的时钟周期的数量。这些都是在后台处理的。然而,当我们谈到可编程输入和输出(PIO)时,我们需要在一个低得多的层次上处理事情。

我们将对PIO进行短暂的游览,并介绍一些高级主题,以便您了解正在发生的事情,并希望了解Pico上的PIO如何比其他微控制器上的选项提供一些真正的优势。然而,理解创建PIO程序所需的所有低级数据操作需要花时间来完全理解,所以如果它看起来有点不透明,不要担心。如果您对处理这种低级编程感兴趣,那么我们将为您提供入门知识,并为您指明继续您的旅程的正确方向。如果你对更高层次的工作更感兴趣,而宁愿把低层次的争吵留给其他人,我们将向你展示如何使用PIO程序。

在本书中,我们讨论了使用MicroPython控制Pico上的引脚的方法。我们可以使用专用的SPI和I2C控制器打开和关闭它们,获取输入,甚至发送数据。然而,如果我们想要连接一个不使用SPI或I2C通信的设备,该怎么办? 如果它有自己的特殊协议呢?

有几种方法可以做到这一点。在大多数MicroPython设备上,你需要做一个叫做“位敲打”的过程,在这个过程中你在MicroPython中实现协议。使用此功能,您可以按正确的顺序打开或关闭引脚以发送数据。

这样做有三个缺点。

  • 首先是它的速度很慢。MicroPython在某些方面做得非常好,但它的运行速度不如本地编译的代码快。
  • 第二,我们必须把这个和在微控制器上运行的其他代码混在一起。
  • 第三,一些时间关键的代码可能很难可靠地实现。快速协议可能需要事情在非常精确的时间发生,而使用MicroPython,我们可以非常精确,但如果你试图每秒传输兆比特,你需要事情每毫秒或可能每几百纳秒发生、这在MicroPython中很难可靠地实现。

Pico对此有一个解决方案:可编程I/O。有一些额外的、真正剥离的处理核心,可以运行简单的程序来控制IO引脚。你不能用MicroPython对这些核心进行编程,你必须为它们使用一种特殊的语言,但你可以从MicroPython中对它们进行编程。让我们来看一个例子:

from rp2 import PIO, StateMachine, asm_pio 
from machine import Pin
import utime
 
@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)
    utime.sleep(1) 
    sm1.active(0)
 
    sm2.active(1) 
    utime.sleep(1) 
    sm2.active(0)
 
    sm3.active(1) 
    utime.sleep(1) 
    sm3.active(0)

这里有三种方法,看起来都有点奇怪,但将板载LED设置为四分之一、一半和全亮度。它们看起来有点奇怪的原因是它们是用一种特殊的语言为Pico的PIO系统编写的。你大概可以猜到它们是做什么的,以一种类似于我们如何使用PWM的方式快速打开和关闭LED。指令:

  • set(pins,0) 关闭GPIO引脚
  • set(pins,1) 打开GPIO引脚。

这三个方法的上面都有一个描述符,告诉MicroPython将其视为PIO程序而不是普通方法。这些描述符还可以采用影响程序行为的参数。在这些情况下,我们使用set_init参数来告诉PIO GPIO引脚一开始应该是低还是高。

这些方法中的每一个都是运行在PIO状态机上的小程序,它们都是连续循环的。例如,ledhalfbrightness会不断地开启和关闭LED,这样它就会有一半的时间关闭,一半的时间打开。ledfullbrightness也会类似地循环,但因为唯一的指令是打开LED,这实际上不会改变任何东西。

这里有点不寻常的是ledquarterbrightness。每个PIO指令只需要运行一个时钟周期(可以通过设置频率来改变时钟周期的长度,稍后我们将看到)。但是,我们可以在一条指令后用方括号加一个介于1到31之间的数字,这告诉PIO状态机在运行下一条指令之前暂停这个时钟周期。在ledquarterbrightness中,两个set指令每个需要一个时钟周期,延迟需要两个时钟周期,所以整个循环需要四个时钟周期。在第一行中,set指令需要一个周期,延迟需要两个周期,所以GPIO管脚在这四个周期中的三个周期是关闭的。这使得LED的亮度只有平时的四分之一。

得到PIO程序后,需要将它加载到状态机中。因为我们有三个程序,所以需要将它们加载到三个状态机中(有8个可以使用,编号为0-7)。这是通过这样一行来完成的:

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

这里的参数是:

  • 状态机编号
  • 要加载的PIO程序
  • 频率(必须在2000到125000000之间)
  • 状态机操作的GPIO管脚

您将在其他程序中看到一些我们在这里不需要的附加参数。一旦创建了状态机,就可以使用带有1(启动)或0(停止)的active方法启动和停止状态机。在循环中,我们循环三种不同的状态机。

前面的示例有点做作,所以让我们通过一个实际示例来看看使用PIO的方法。WS2812B led(有时也被称为NeoPixels)是一种包含三个led(一个红、一个绿、一个蓝)和一个小微控制器的发光管。它们是由一根数据线控制的,该数据线有一个时间依赖协议,很难对数据进行实时控制。

让我们看看如何用PIO来控制它:

import array, utime
from machine import Pin
import rp2
from rp2 import PIO, StateMachine, asm_pio
 
# Configure the number of WS2812 LEDs. 
NUM_LEDS = 12
 
@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(0).
sm = StateMachine(0, ws2812, freq=8000000, sideset_base=Pin(0)) 
 
# Start the StateMachine, it will wait for data on its FIFO.
sm.active(1)

数据进入状态机有两个阶段。第一个是称为先进先出(FIFO)的存储器。这是我们的主Python程序发送数据的地方。第二个是输出移位寄存器(OSR),这是out()指令获取数据的地方。这两个是由pull指令连接的,从FIFO取数据,并把它放在OSR。然而,由于我们的程序设置autopull启用阈值为24,每次我们从OSR读取24位,它将从FIFO重新加载。

指令out(x,1)从OSR获取一位数据,并将其放入名为x的变量中(在PIO中只有两个可用变量:x和y)。

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

有一些jmp的意大利面条,主要是为了确保时间是一致的,因为循环必须采取完全相同的周期的每一次迭代,以保持协议的时间一致。

这里我们忽略的一个方面是.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) 
    utime.sleep_ms(10)
 
print("red")
for j in range(0, 255):
    for i in range(NUM_LEDS): 
        ar[i] = j<<8
    sm.put(ar,8) 
    utime.sleep_ms(10)
 
print("green")
for j in range(0, 255):
    for i in range(NUM_LEDS): 
        ar[i] = j<<16
    sm.put(ar,8) 
    utime.sleep_ms(10)
 
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) 
    utime.sleep_ms(10)

在这里,我们跟踪一个名为ar的数组,它保存了我们希望led拥有的数据(稍后我们将看看为什么我们这样创建数组)。数组中的每个数字都包含一盏灯上所有三种颜色的数据。格式有点奇怪,因为它是二进制的。使用PIO的一个问题是,您经常需要处理单个数据位。每个数字无论是1还是0,以及用这种方式构建的数字,十进制中的2(作为我们所有的正常数字)是10的二进制。以10为基数的3是二进制的11。二进制的8位中最大的数字是11111111,或以10为基数的255。我们不会在这里深入了解二进制,但如果你想了解更多,你可以尝试一下二进制英雄项目:hsmag.cc/binaryhero。

更让人困惑的是,我们实际上是在一个数字中存储了三个数字。这是因为在MicroPython中,整数存储为32位,但每个数字只需要8位。因为我们只需要24位,所以最后会有一些空闲空间,不过没关系。

前8位是蓝色值,后8位是红色,最后8位是绿色。8位存储的最大数字是255,所以每个LED有255个亮度级别。我们可以使用位移操作符«来实现这一点。这将在一个数字的末尾添加一定数量的0,所以如果我们想让LED处于红色、绿色和蓝色的一级亮度,我们从每个值为1开始,然后将它们移动适当的比特数。 对于绿色,我们有: 1 «16 = 10000000000000000

对于红色,我们有: 1 « 8 = 100000000

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

最右边的8位是蓝色的,后面的8位是红色的,最左边的8位是绿色的。 最后一行可能有点让人困惑:

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引脚改变为设定值或中断发生)。

WS2812B库

虽然试验WS2812B PIO程序很有用,但如果您想在实际项目中使用它,那么使用它可能更有用 一个汇集了所有信息的库。在hsmag.cc/pico-ws2812b有一个这样的例子。这让你创建一个对象来保存所有的LED颜色数据,然后使用set_pixel()和fill()等方法来改变数据。有关如何使用它的更多细节,请查看该存储库的示例文件夹。

尽管可能的指令数量很少,但可以实现广泛的通信协议。大多数指令是用来移动数据的 以某种形式。如果需要以任何特定的方式准备数据,比如操纵希望led的颜色,这应该在主MicroPython程序中完成,而不是在PIO程序中。 你可以在Pico Python SDK文档的Raspberry Pi Pico上找到更多关于如何使用这些,以及MicroPython中PIO的全部选项的信息,以及在RP2040数据手册中关于PIO如何工作的完整参考。这两个版本都可以在rptl.io/rp2040-get-started中找到。