## 可编程IO 在这个附录中,我们将看到一些代码,它们看起来与我们在本书其余部分中处理的代码非常不同。那是因为我们必须在低水平上处理事情。大多数时候,MicroPython可以隐藏很多在微控制器上工作的复杂性。 当我们做这样的事情时: print(“hello”) ..我们不必担心微控制器存储字母的方式,或它们被发送到串行终端的格式,或串行终端的时钟周期的数量。这些都是在后台处理的。然而,当我们谈到可编程输入和输出(PIO)时,我们需要在一个低得多的层次上处理事情。 我们将对PIO进行短暂的游览,并介绍一些高级主题,以便您了解正在发生的事情,并希望了解Pico上的PIO如何比其他微控制器上的选项提供一些真正的优势。然而,理解创建PIO程序所需的所有低级数据操作需要花时间来完全理解,所以如果它看起来有点不透明,不要担心。如果您对处理这种低级编程感兴趣,那么我们将为您提供入门知识,并为您指明继续您的旅程的正确方向。如果你对更高层次的工作更感兴趣,而宁愿把低层次的争吵留给其他人,我们将向你展示如何使用PIO程序。 ### 1. 数据输入和数据输出 在本书中,我们讨论了使用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状态机上的小程序,它们都是连续循环的。例如,led_half_brightness会不断地开启和关闭LED,这样它就会有一半的时间关闭,一半的时间打开。led_full_brightness也会类似地循环,但因为唯一的指令是打开LED,这实际上不会改变任何东西。 这里有点不寻常的是led_quarter_brightness。每个PIO指令只需要运行一个时钟周期(可以通过设置频率来改变时钟周期的长度,稍后我们将看到)。但是,我们可以在一条指令后用方括号加一个介于1到31之间的数字,这告诉PIO状态机在运行下一条指令之前暂停这个时钟周期。在led_quarter_brightness中,两个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方法启动和停止状态机。在循环中,我们循环三种不同的状态机。 ### 2. 一个真实的例子 前面的示例有点做作,所以让我们通过一个实际示例来看看使用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(not_x, "do_zero")告诉代码,如果x的值为0(或者,用逻辑术语来说,如果not_x为真,而not_x与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) ### 3. 所有的指令 用于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中找到。