树莓派pico--PIO入门

PIO是什么?

(Programmable I/O)PIO是为RP2040设计的一个新硬件。它允许你在基于RP2040的设备上新创建或添加硬件接口。比如你可以“添加4个uart”,或“输出DPI视频”,甚至“打算与某个在某宝上买的串口设备通讯,但找不到其他支持这个硬件的设备”,这些都可以通过PIO解决。

背景

与其他数字硬件组件的接口是困难的。它经常发生在非常高的频率(由于需要传输的数据量),并且有非常精确的时间要求。

PIO硬件使用PIO

RP2040上的PIO子系统允许你为所谓的PIO状态机编写小型、简单的程序,RP2040在两个PIO实例中有八个分裂的状态机。状态机负责设置和读取一个或多个gpio,将数据缓冲到处理器或从处理器(或RP2040的超高速DMA子系统),并在需要数据或注意时通过IRQ或轮询通知处理器。
这些程序的运行周期精度最高可达系统时钟速度(或程序时钟可以划分给不活泼的协议去慢慢的运行)。
PIO状态机比RP2040上的通用Cortex-M0+处理器要紧凑得多。事实上,它们在尺寸(因此成本)上与标准SPI外设相似,例如RP2040上也发现了PL022 SPI,因为它们的大部分面积都花在了所有串行外设共同的组件上,如fifo、移位寄存器和时钟分频器。由于指令集小而规则,所以无需花费太多的硅来解码指令。没有必要因为将一个状态机仅用于一个I/O任务而感到内疚,因为有8个状态机!
尽管如此,当涉及到I/O时,PIO状态机在一个周期内比Cortex-M0+完成了更多的工作:例如,在一个周期内采样GPIO值,切换时钟信号并推入FIFO。缺点是PIO状态机不能远程运行通用软件。正如我们将看到的,编写PIO状态机对于以前编写过汇编代码的人来说是非常熟悉的,对于没有编写过汇编代码的人来说,小指令集应该很快就能上手。
对于简单的硬件协议-如PWM或双工SPI -一个单一的PIO状态机可以独自处理实现硬件接口的任务。对于更复杂的协议,如SDIO或DPI视频,你可能最终会使用两到三个。
不要试图在RP2040上“篡改”一个协议,而是使用PIO代替。对于重复从GPIOs读写数据的任何东西来说都是这样,但对于旨在传输数据的任何东西来说肯定都是这样。

开始使用PIO

可以在c++ SDK中或直接从MicroPython中编写PIO程序。

第一个PIO程序

在深入了解PIO汇编语言的所有细节之前,我们应该花点时间看看一个小而完整的应用程序:

  1. 将程序加载到PIO的指令存储器中
  2. 设置一个PIO状态机来运行程序
  3. 在状态机运行时与状态机进行交互

清单:

  • 一个PIO程序
  • 一些用C语言编写的软件,可以运行整个程序
  • 一个CMake文件,描述如何将这两者组合成一个程序映像,以便加载到基于rp2040的开发板上

下面是一个PIO程序列表。它是用PIO汇编语言编写的。

 .program hello
 
 ; Repeatedly get one word of data from the TX FIFO, stalling when the FIFO is
 ; empty. Write the least significant bit to the OUT pin group.

 loop:
     pull
 out pins, 1
 jmp loop

pull指令从发送FIFO缓冲区取一个数据项,并把它放在输出移位寄存器(OSR)中。数据每次从FIFO移动一个字(32位)到OSR。OSR能够使用out指令将数据一次移出一个或多个位,移到更远的目的地。(tip: fifo是在硬件上实现的数据队列。每个状态机有两个fifo,在状态机和系统总线之间,用于数据从(TX)发送到(RX)芯片。)

out指令从我们刚刚从FIFO拉出的数据中取一位,并将数据写入一些引脚。我们稍后将看到如何确定这些是哪些引脚。

jmp指令返回到loop: label,这样程序就会无限地重复。所以,总结一下这个程序的功能:重复地从FIFO中取一个数据项,从这个数据项中取一个位,并把它写入一个引脚。
.pio文件还包含了一个帮助函数,用于设置PIO状态机,以便正确执行这个程序:

static inline void hello_program_init(PIO pio, uint sm, uint offset, uint pin) {
     
    pio_sm_config c = hello_program_get_default_config(offset);

    // Map the state machine's OUT pin group to one pin, namely the `pin`
    // parameter to this function.
    sm_config_set_out_pins(&c, pin, 1);
    // Set this pin's GPIO function (connect PIO to the pad)
    pio_gpio_init(pio, pin);
    // Set the pin direction to output at the PIO
    pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);

    // Load our configuration, and jump to the start of the program
    pio_sm_init(pio, sm, offset, &c);
    // Set the state machine running
    pio_sm_set_enabled(pio, sm, true);
}

这里要设置的主要内容是我们打算将数据输出到的GPIO。这里有三件事需要考虑:

  1. 需要告诉状态机要向哪个或哪个GPIO输出。有四个不同的引脚组,它们被不同的指令在不同的情况下使用;这里我们用的是out引脚组,因为我们用的是out指令。
  2. GPIO还需要被告知PIO正在控制它(GPIO function select)
  3. 如果我们只使用输出引脚,我们需要确保PIO驱动输出使能行高。PIO可以通过编程的方式来驱动这一行,例如使用out pindirs指令,但是在这里我们是在启动程序之前进行设置的。

PIO在被正确配置之前不会做任何事情,所以我们需要一些软件来做这些。我们刚刚看到的hello.pio将自动转换(稍后我们将看到如何转换)为一个头文件,其中包含汇编好的pio程序二进制文件、文件中包含的任何辅助函数以及有关该程序的一些有用信息。在c程序中我们引用的头文件叫hello.pio.h。

/**
* Copyright (c) 2020 Raspberry Pi (Trading) Ltd. 3*
* SPDX-License-Identifier: BSD-3-Clause
*/

#include "pico/stdlib.h"
#include "hardware/pio.h"
// Our assembled program:
#include "hello.pio.h"

int main() {
     
	// Choose which PIO instance to use (there are two instances)
    PIO pio = pio0;

	// Our assembled program needs to be loaded into this PIO's instruction
	// memory. This SDK function will find a location (offset) in the
	// instruction memory where there is enough space for our program. We need
	// to remember this location!
    uint offset = pio_add_program(pio, &hello_program);

	// Find a free state machine on our chosen PIO (erroring if there are
	// none). Configure it to run our program, and start it, using the
	// helper function we included in our .pio file.
    uint sm = pio_claim_unused_sm(pio, true);
    hello_program_init(pio, sm, offset, PICO_DEFAULT_LED_PIN);

	// The state machine is now running. Any value we push to its TX FIFO will
	// appear on the LED pin.
    while (true) {
     
		// Blink
		pio_sm_put_blocking(pio, sm, 1);
		sleep_ms(500);
		// Blonk
		pio_sm_put_blocking(pio, sm, 0);
		sleep_ms(500);
    }
}  

上面说到,RP2040有两个PIO块,每个PIO块有四个状态机。每个PIO块都有一个32槽的指令内存,这对块中的4个状态机都是可见的。在任何状态机运行程序之前,我们需要将程序加载到这个指令内存中。函数pio_add_program()在给定的PIO指令内存中为我们的程序找到空闲空间,并加载它。
指令有32条,这听起来可能不是很多,但是一旦充分了解了PIO指令集的特性,它就会变得非常密集。一个完全可用的UART传输程序可以用4条指令实现,如pico-examples中的pio/uart_tx示例所示。状态机也有两种方式来执行来自其他来源的指令——比如直接来自FIFOs——在RP2040Datasheet中可以阅读到有关这些指令的所有信息。
一旦程序被加载,我们就找到一个空闲的状态机并告诉它运行我们的程序。我们可以命令多个状态机来运行同一个程序。同样,我们可以指示每个状态机运行不同的程序,只要它们都能同时放入指令内存。
上面的构建的状态机输出数据到PICO上的LED上,可以很明显的看到。
此时,状态机是自动运行的。状态机将立即停止,因为它正在等待TX FIFO中的数据,而我们还没有提供任何数据。处理器可以使用pio_sm_put_blocking()函数将数据直接推入状态机的TX FIFO。(_blocking,因为当TX FIFO满的时候,这个函数会让处理器停止。)写入1会打开LED,写入0会关闭LED
下面写cmake文件
现在有两个文本文件,名称以,pio和.c结尾,但它们并没有给我们带来太多好处。一个CMake文件描述了这些是如何构建到一个二进制文件,适合加载到你的Raspberry Pi Pico或其他基于rp2040的板。

add_executable(hello_pio)

pico_generate_pio_header(hello_pio ${
     CMAKE_CURRENT_LIST_DIR}/hello.pio)

target_sources(hello_pio PRIVATE hello.c)

target_link_libraries(hello_pio PRIVATE
        pico_stdlib
        hardware_pio
        )

pico_add_extra_outputs(hello_pio)

# add url via pico_set_program_url
example_auto_set_url(hello_pio)
  • add_executable(): 声明我们正在构建一个名为hello_pio的程序
  • pico_generate_pio_header(): 声明我们有一个PIO程序,hello.pio,我们希望将其构建到C头文件中 与我们的程序一起使用
  • target_sources():列出hello_pio程序的源代码文件。在本例中,只有一个C文件。
  • target_link_libraries():确保我们的程序是使用PIO硬件API构建的,这样我们就可以调用类似的函数 在C文件中的pio_add_program()。
  • pico_add_extra_outputs():默认情况下,我们只获得一个.elf文件作为我们的应用程序的构建输出。在这里,我们声明我们也想要额外的构建格式,比如一个.uf2文件,可以直接拖放到USB连接的树莓派Pico上。

然后通过以下命令来构建程序

mkdir build
cd build
cmake ..
make

你可能感兴趣的:(树莓派,pico,树莓派)