可以同时使用 C++ SDK 和 MicroPython 进行 PIO 编程。未来的计划是新增更多的 API,以便利用预先编写好的 PIO 程序菜单轻松地创建新的 UART、PWM 通道等,但就目前而言,开发人员需参照示例代码自行完成这些操作。
在深入探讨 PIO 汇编语言之前,先花些时间来看一个虽小但完整的应用程序,该程序:
- 将一个程序加载到PIO的指令存储器中;
- 设置一个PIO状态机来运行该程序;
- 在状态机运行起来后与其进行交互。
这个示例的主要组成部分如下:
- 一个PIO程序;
- 一个用C语言编写的软件,用于运行整个程序流程;
- 一个CMake文件,用于描述如何将上述两者组合成一个程序镜像,以便加载到基于RP系列微控制器的开发板上。
文中的代码可以在
pico-examples
中的pio/hello_pio
示例中找到。
PIO 程序
示例程序使用 PIO 汇编语言编写。
以下展示了 hello_pio/hello.pio
的第 8-16 行代码:
8 .program hello
9
10 ; Repeatedly get one word of data from the TX FIFO, stalling when the FIFO is
11 ; empty. Write the least significant bit to the OUT pin group.
12
13 loop:
14 pull
15 out pins, 1
16 jmp loop
pull
指令从发送 FIFO1 缓冲区取出一个数据项,并将其放入输出移位寄存器(OSR)。数据每次以一个字(32 位)的单位从 FIFO 移至 OSR。OSR 能够使用输出指令将这些数据每次按一位或多位向外移出,传送到其他目的地。
这里的 out
指令从 FIFO 中取出一位,并将该位数据写入某些引脚。
jmp
指令会跳回到 loop:
这个标签处,这样程序就会无限循环下去。所以,总结一下这个程序的功能:反复从一个先入先出队列中取出一个数据项,从该数据项中取出一位,并将其写入一个引脚。
.pio
文件还包含一个辅助函数,用于设置一个 PIO 状态机,以确保该程序能正确执行。
以下展示了 hello_pio/hello.pio
的第 19-34 行代码:
19 static inline void hello_program_init(PIO pio, uint sm, uint offset, uint pin) {
20 pio_sm_config c = hello_program_get_default_config(offset);
21
22 // Map the state machine's OUT pin group to one pin, namely the `pin`
23 // parameter to this function.
24 sm_config_set_out_pins(&c, pin, 1);
25 // Set this pin's GPIO function (connect PIO to the pad)
26 pio_gpio_init(pio, pin);
27 // Set the pin direction to output at the PIO
28 pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
29
30 // Load our configuration, and jump to the start of the program
31 pio_sm_init(pio, sm, offset, &c);
32 // Set the state machine running
33 pio_sm_set_enabled(pio, sm, true);
34 }
函数中,主要设置的对象是 GPIO,程序将数据输出到 GPIO。有三点需要考虑:
- 需要告知状态机要将数据输出到哪个或哪些 GPIO。在不同情况下,不同的指令会使用四个不同的引脚组;在这里,由于我们只是使用了
out
指令,所以我们使用的是输出引脚组。 - 还需要告知 GPIO,PIO 正在控制它(GPIO功能选择)。
- 如果仅将该引脚用于输出,需要确保 PIO 将输出使能线拉高。PIO 可以通过
out pindirs
这样的指令以编程的方式将该线拉高或拉低,实例程序中,在程序启动之前就已经将其设置好了。
C 程序
还需要做一些软件设置,才能让 PIO 正常工作起来。hello.pio
会被自动转换成一个头文件,其中包含汇编好的PIO程序二进制文件、在该文件中包含的任何辅助函数以及一些关于该程序的有用信息。将该头文件命名为 hello.pio.h
并包含到 C 程序源代码中。
以下展示了 hello_pio/hello.c
的 C 代码:
/**
* Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/pio.h"
// Our assembled program:
#include "hello.pio.h"
// This example uses the default led pin
// You can change this by defining HELLO_PIO_LED_PIN to use a different gpio
#if !defined HELLO_PIO_LED_PIN && defined PICO_DEFAULT_LED_PIN
#define HELLO_PIO_LED_PIN PICO_DEFAULT_LED_PIN
#endif
// Check the pin is compatible with the platform
#if HELLO_PIO_LED_PIN >= NUM_BANK0_GPIOS
#error Attempting to use a pin>=32 on a platform that does not support it
#endif
int main() {
#ifndef HELLO_PIO_LED_PIN
#warning pio/hello_pio example requires a board with a regular LED
#else
PIO pio;
uint sm;
uint offset;
setup_default_uart();
// This will find a free pio and state machine for our program and load it for us
// We use pio_claim_free_sm_and_add_program_for_gpio_range so we can address gpios >= 32 if needed and supported by the hardware
bool success = pio_claim_free_sm_and_add_program_for_gpio_range(&hello_program, &pio, &sm, &offset, HELLO_PIO_LED_PIN, 1, true);
hard_assert(success);
// Configure it to run our program, and start it, using the
// helper function we included in our .pio file.
printf("Using gpio %d\n", HELLO_PIO_LED_PIN);
hello_program_init(pio, sm, offset, HELLO_PIO_LED_PIN);
// The state machine is now running. Any value we push to its TX FIFO will
// appear on the LED pin.
// press a key to exit
while (getchar_timeout_us(0) == PICO_ERROR_TIMEOUT) {
// Blink
pio_sm_put_blocking(pio, sm, 1);
sleep_ms(500);
// Blonk
pio_sm_put_blocking(pio, sm, 0);
sleep_ms(500);
}
// This will free resources and unload our program
pio_remove_program_and_unclaim_sm(&hello_program, pio, sm, offset);
#endif
}
RP2040 有两个 PIO 模块,每个模块都带有 4 个状态机(RP2350芯片有 3 个 PIO 模块,每个模块也带有 4 个状态机)。每个 PIO 模块都有一个可被该模块内 4 个状态机访问的、具有322个存储槽的指令存储器。在状态机运行之前,需要将程序加载到这个指令存储器中。pio_add_program()
函数会在给定的 PIO 的指令存储器中为程序找到空闲空间,并将其加载进去。
程序加载完毕后,就可以找一个空闲的状态机来运行加载的程序。至此和同时启动多个状态机运行同一个程序。同样地,如果所有程序都能一次性装入指令存储器,也可以指示每个状态机分别同时运行不同的程序。
示例代码对这个状态机进行配置,使其能够将数据输出到 Pico 系列设备上的 LED 灯上。
设备通电后,状态机开始在自主运行,随即进入停滞状态,等待 TX FIFO 中的数据。处理器可以使用 pio_sm_put_blocking()
函数将数据直接推入状态机的 TX FIFO 中。(之所以命名为 _blocking
,是因为当 TX FIFO 被装满时,这个函数会使处理器处于停滞状态。)当写入1时会点亮LED灯,写入0时则会关闭LED灯。
CMake 文件
如果光有 .pio
和 .c
文件,我们还无法把他们构建成二进制文件,可以通过 CMake 文件来进行构建配置,构建成功后就可以将输出程序文件加载到 Pico系列设备或其他基于 RP 系列微控制器的电路板上运行了。
以下展示了 hello_pio/CMakeLists.txt
的 CMake 代码:
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
)
# Pass cmake -DHELLO_PIO_LED_PIN=x, where x is the pin you want to use
if(HELLO_PIO_LED_PIN)
target_compile_definitions(hello_pio PRIVATE
HELLO_PIO_LED_PIN=${HELLO_PIO_LED_PIN}
)
endif()
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 语言头文件,以便在 C 程序中使用。target_sources()
:列出hello_pio
程序的源代码文件。示例中,源代码文件只有一个 C 文件。target_link_libraries()
:配置程序依赖 PIO 硬件 API,这样就可以在 C 文件中调用诸如pio_add_program()
的函数。pico_add_extra_outputs()
:默认情况下,执行完构建后,只会输出一个.elf
文件。使用该命令可以输出其他格式,如.uf2
文件,可以直接通过USB
连接将程序拖放到 Pico 系列设备上。
SDK 环境配置可以参考博文树莓派(Raspberry Pi)Pico 2 C_C++开发环境配置(Docker+SDK),程序运行教程可以参考博文树莓派(Raspberry Pi)Pico 2 启动运行,通过如下的命令编译构建程序。
$ mkdir build
$ cd build
$ cmake ..
$ make hello_pio
链接
先入先出队列(FIFOs)是在硬件中实现的数据队列。每个状态机在其与系统总线之间都有两个先入先出队列,分别用于数据传出(发送,TX)和传入(接收,RX)芯片。它们的名称(先入先出)来源:数据在队列输出端出现的顺序与其在输入端呈现的顺序相同。 ↩︎
虽然存储槽数量不多,但是足够满足日常使用,因为 PIO 指令集被设计的非常紧凑。如
pico-examples
中的pio/uart_tx
示例所示,一个完全可用的 UART 发送程序可以用四条指令来实现。此外,状态机还有多种从其他源(比如直接从先入先出队列(FIFOs))执行指令的方式,可以在 RP2350 的数据手册中详细了解到这些内容。 ↩︎