【数莓派】使用 PIO 状态机编程

一、说明

        在微控制器编程中,与其他硬件的接口可能非常简单或非常具有挑战性。如果其他硬件(例如传感器)支持I2C、SPI或UART等标准总线系统,则只需将它们连接起来并通过实施的总线系统读取/写入数据即可。如果需要连接其他硬件,则被迫实现精确的定时信号,使用多个引脚发送和接收数据,并解释信号。

        您可以使用普通 C 对这些时序考虑因素进行编程,但这意味着要非常仔细地编程,因为您与处理器时钟周期相关联,并且需要了解每行代码的时序影响。

        为了应对这一挑战,Raspberry Pico有一个独特的硬件扩展:PIO,可编程输入/输出的缩写。PIO 实现为 4 个独立的状态机。每个状态机都与FIFO队列连接,以便与主程序交换数据。除了队列之外,stata机器还可以DMS和访问所有GPIO,但不能访问其他硬件或协议。

        笔克社区使用PIO输出音效或视频,连接到专有LCD系统,或连接其他需要非常特定协议的硬件。

        为了帮助您开始使用 PIO,本文简要介绍:了解硬件部分的基本知识,了解 PIO 程序的外观以及它如何与 C 主程序交互,最后深入了解 PIO 编程语言。

这篇文章最初出现在我的博客上。

二、为什么需要PIO?

当您想要与无法连接到USB,I2C,SPI或UART板载支持的协议的硬件接口时,您被迫编写非常时间有限的代码来读取和写入GPIO。但是,当您要连接的外部硬件需要非常低速的数据传输时,您需要处理中断或等待周期较长。

官方 C SDK 指南明确指出,将 IRQ 用于比主进程慢 1000 倍的协议变得不切实际,因为您将注定 CPU 在大部分时间等待。或者,在频谱的另一端,您可能有一个具有高周期的硬件,并且您正在迫使微控制器永远不会错过任何一个时钟周期。这两个挑战基本上迫使您陷入相同的境地:您的所有 CPU 资源都将花费在处理或等待仅使用单个外部硬件上。您不能将笔克用于其他任何事情。

PIO子系统为这个问题引入了一种新的解决方案。从表面上看,它类似于现场可编程门阵列(FGPA),通过提供用于构建复杂逻辑的编程环境。但是你不是用软件设计集成电路,然后需要编写与这种状态交互的微控制器软件。相反,您可以直接编程多达 4 个不同的状态机。每个状态机都可以自由访问 GPIO 引脚以读取和写入数据,它可以缓冲来自处理器或其他 DMA 的数据,并通过中断或轮询通知处理器其计算结果。

三、PIO 示例程序:闪烁的 LED

        让我们定义一个简单的PIO启动程序,该程序将使LED闪烁。我们需要定义两个文件:一个 PIO 文件,它保存类似汇编程序的代码,以及一个带有函数的普通 C 文件。main

        让我们先看看 PIO 文件。PIO 文件由两部分组成:定义 PIO 指令的部分,以及包含用于向程序公开 PIO 程序的函数的部分。基本布局如下:programc-sdkmain

.program hello
...% c-sdk {
...
%} 

四、PIO 程序

        PIO程序本身实际上是用汇编程序编写的,确切地说,是汇编程序语句的一个子集。要打开和关闭 LED,以下程序就足够了:

.program helloset pindirs, 1loop:
  set pins, 1 [31]
  set pins, 0 [31]
  jmp loop 

让我们逐行剖析该程序。

  • 第 1 行:该语句开始声明 PIO 程序。它需要一个标识符,该标识符将在编译和链接过程中使用。program
  • 第 3 行:指令是多用途语句。这条线意味着我们将所有配置的设置引脚设置为输出SET
  • 第 5 行:此声明是一个自由格式的标签,用于对较大程序的各个部分进行分组。loop
  • 第 6 行:将配置的 LED 引脚设置为输出高值,总共 32 个时钟周期。每个 PIO 语句在 1 个时钟周期内执行,额外的 5Bit 值可用于等待额外的周期。
  • 第 7 行:将配置的 LED 引脚设置为输出低值,总共 32 个时钟周期。
  • 第 8 行:我们回到之前定义的标签。JMPloop

五、C-SDK 绑定

        若要运行此程序,还需要定义 C-SDK 绑定。本质上,绑定是 PIO 程序内部的一个函数。在编译过程中,编译器将拾取它,编译器会输出一个头文件,您可以将其集成到主程序中。

        添加以下代码 — 本文稍后将提供详细说明。

% c-sdk {
static inline void hello_program_init(PIO pio, uint sm, uint offset, uint pin) {
  // 1. Define a config object
  pio_sm_config config = hello_program_get_default_config(offset);
  // 2. Set and initialize the output pins
  sm_config_set_set_pins(&config, pin, 1);
  // 3. Apply the configuration & activate the State Machine
  pio_sm_init(pio, sm, offset, &config);
  pio_sm_set_enabled(pio, sm, true);
}
%}

六、主程序

        最后,我们将所有内容添加到主程序文件中。

#include <stdio.h>
#include <stdbool.h>
#include <pico/stdlib.h>
#include <hardware/pio.h>
#include <hello.pio.h>#define LED_BUILTIN 25;int main() {
  stdio_init_all();  PIO pio = pio0;
  uint state_machine_id = 0;
  uint offset = pio_add_program(pio, &hello_program);  hello_program_init(pio, state_machine_id, offset, LED_BUILTIN, 1);  while(1) {
    //do nothing
  }
} 

        在这里,我们看到以下详细信息:

  • 第 4 行:要使用 PIO 状态机,我们需要包含这个特殊的标头
  • 第 5 行:此语句包括在编译过程中组装的 PIO 程序。它将公开前面定义的函数,并将指向程序的指针定义为 。注意命名约定!hello_program_inithello_program
  • 第 12 行:Pico 有两个不同的状态机总线,我们需要将状态机定义为属于其中一个。
  • 第 13 行:我们定义状态机的 id(4 位值)
  • 第 14 行:此语句分配将保存状态机代码的动态内存。它返回一个内存偏移值,我们将传递给状态机初始化
  • 第 16 行:我们初始化并启动程序

七、PIO 技术细节

         看完示例后,让我们深入了解技术细节。

7.1 PIO 组件

        笔克提供两个 PIO 块,每个块中有 4 个状态机。每个状态机提供以下组件。

  • TX FIFO/RX FIFO:从/向主程序接收或发送 32 位值
  • 输入移位寄存器 (ISR)/输出移位寄存器 (OSR):这些寄存器保存易失性数据,以便在状态机和主程序之间直接交换。
  • 暂存寄存器:标记和 ,这些 32 位寄存器允许您存储状态机所需的任何其他数据。xy
  • 可配置时钟分频器:Pico的时钟周期为133MHz,可按16位值调节至2000Hz
  • 灵活的 GPIO 映射:Pico 的核心在于能够访问 GPIO 引脚,每个状态机都可以使用四组不同的 GPIO(输入、输出、设置、侧集)
  • DMA 访问:直接访问内存,无需主处理器参与
  • IRQ标志:可以设置或清除8个全局标志,每个状态机和主程序都可以立即访问中断

7.2 PIO 汇编语言

        要对 PIO 进行编程,您需要使用汇编语言的特殊方言。在示例程序中,我们已经看到了如何将逻辑电平应用于引脚以及如何定义一个简单的循环。汇编语言中只有 9 个命令,还有一些用于代码结构的附加语句。我将简要介绍所有这些指令,但有关所有指令的完整定义,请参阅官方文档的第 3.3.2 节。

        由于该语言非常压缩,因此多个语句执行多种功能。特别是如何正确使用 GPIO 引脚可能会很棘手。因此,我将语句分组为不同类型的函数。

7.3 程序结构

        若要从一般意义上构建程序,可以使用以下命令。

  • .program NAME- 程序的名称,以及在编译过程中将生成的头文件的名称,以便您访问主程序中的状态机
  • .define NAME VALUE- 与 C 程序类似,您可以定义状态机中可见的顶级常量
  • LABEL:- 标签是相关语句的语法分组。您可以定义任何标签,然后跳回该标签
  • ; COMMENT- 分号后面的任何内容都是注释
  • .wrap_target和 - 重复运行 PIO 程序的一部分的说明.wrap
  • .word- 将原始的 16 位值作为指令存储在程序中(每个 PIO 语句都是一个 16 位值)
  • .side_set COUNT (opt)- 此指令还配置了该程序的 SIDE 引脚。COUNT 值是从指令中减少的位数,opt 值确定 PIO 程序中的语句是可选的还是必需的。使用此声明时,可以将其他命令附加到所有表达式,例如,将一个位从 移动到 FIFO RX,并将 SIDE 引脚设置为逻辑电平 LOW。sideout x, 1 side 0OSR

在移位寄存器内移动数据

  • in SOURCE count- 将数据转移到 ISR 中,其中 SOURCE 可以是 、 或 ,计数为XYOSRISR0...32
  • out DESTINATION count- 将数据从 OSR 转移到目的地 , ,XYISR
  • mov DESTINATION, SOURCE- 将数据从源(、或)移动到目标(、或)移动XYOSRISRXYOSRISR)
  • set DESTIANTION, data- 将 5 位数据值写入 DESTIANTION (,XY)

在移位寄存器和主程序之间移动数据

  • pull- 将数据从TX FIFO加载到OSR
  • push- 将数据从 ISR 推送到 RX FIFO,然后清除 ISR
  • irq INDEX op- 将 IRQ 编号修改为清除 () 或设置(indexop=0op=1)

将数据写入 GPIO 引脚

设置引脚

  • set PINDIRS, 1- 将配置的SET引脚定义为输出引脚
  • set PINS, value- 将高电平 () 或低电平 () 写入 SET 引脚value=1value=1

输出引脚

  • mov PINS, SOURCE- 从源 (、、、) 写入输出引脚 (、 或XYOSRISRXYOSRISR)

从 GPIO 引脚读取数据

设置引脚

  • set PINDIRS, 0- 将配置的SET引脚定义为输入引脚

输入引脚

  • mov DESTINATION, PINS- 从 IN 引脚写入目标 (、、、 和 OUT)XYOSRISRPINS)

条件语句

  • jmp CONDITION LABEL- 当以下类型为真时转到LABELCONDITION
  • !(X|Y|OSRE)- 当 、 为空时为 trueXYOSR
  • X-- | Y--)- 当暂存寄存器为空时为 true,否则递减暂存寄存器。
  • PIN- 当跳跃引脚为逻辑电平高电平时为真
  • wait POLARITY TYPE NUMBER- 延迟进一步处理,直到极性与 ..
  • pin NUMBER- 输入引脚
  • gpio NUMBER- 绝对编号的GPIO
  • irq NUMBER- IRQ 编号(如果极性为 1,则清除 IRQ 编号)
  • nop- 什么都不做

7.4 PIO 配置

PIO 程序是高度可配置的。pico中的c-sdk部分定义了一个包装器函数,该函数将由Pico汇编程序编译。此函数可从主程序访问,并且可以接收任何参数。

您可以在此功能中配置令人眼花缭乱的方面 — 以下列表简要介绍了所有选项。

  • 定义输入引脚、输出引脚和侧引脚
  • 为指令定义一个特殊的引脚JMP
  • 初始化输入引脚的方向
  • 配置输入和输出移位寄存器的移位方向、自动加载和位大小(最多 32 位)
  • 将输入移位寄存器配置为附加输出移位寄存器,反之亦然
  • 时钟分频器应用于默认的 133Mhz 时钟时间(16Bit 值),因此您可以将 PIO 时钟周期缩小到 2000Hz,例如 0,492ms。

为了在使用 PIO 时存在所有配置选项,我喜欢使用以下模板。按照这个模板,我只需配置我需要适应的内容,或删除不需要的内容。

static inline void __program_init(PIO pio, uint sm, uint offset, uint in_pin, uint in_pin_count, uint out_pin, uint out_pin_count, float frequency) {
  // 1. Define a config object
  pio_sm_config config = __program_get_default_config(offset);
  // 2. Set and initialize the input pins
  sm_config_set_in_pins(&config, in_pin);
  pio_sm_set_consecutive_pindirs(pio, sm, in_pin, in_pin_count, 1);
  pio_gpio_init(pio, in_pin);
  // 3. Set and initialize the output pins
  sm_config_set_out_pins(&config, out_pin, out_pin_count);
  pio_sm_set_consecutive_pindirs(pio, sm, out_pin, out_pin_count, 0);
  // 4. Set clock divider
  if (frequency < 2000) {
    frequency = 2000;
  }
  float clock_divider = (float) clock_get_hz(clk_sys) / frequency * 1000;
  sm_config_set_clkdiv(&config, clock_divider);
  // 5. Configure input shift register
  // args: BOOL right_shift, BOOL auto_push, 1..32 push_threshold
  sm_config_set_in_shift(&config, true, false, 32);
  // 6. Configure output shift register
  // args: BOOL right_shift, BOOL auto_push, 1..32 push_threshold
  sm_config_set_out_shift(&config, true, false, 32);
  // 7. Join the ISR & OSR
  // PIO_FIFO_JOIN_NONE = 0, PIO_FIFO_JOIN_TX = 1, PIO_FIFO_JOIN_RX = 2
  sm_config_set_fifo_join(&config, PIO_FIFO_JOIN_NONE);
  // 8. Apply the configuration
  pio_sm_init(pio, sm, offset, &config);
  // 9. Activate the State Machine
  pio_sm_set_enabled(pio, sm, true);
}

八、结论

        PIO是树莓Pico的可编程输入/输出状态机,是一种连接任何硬件的新颖解决方案。与其将 CPU 周期浪费在空闲等待时间上,或者恰恰相反,始终从 PIN 读取和写入,状态机可以完成与任何硬件交互的繁重工作。它们可以配置为从2000HZ运行到133Mhz,可以自由访问所有GPIO引脚,可以在每个时钟周期读取和写入这些引脚。使用简化的类似汇编器的语言,您可以对这些状态机进行编程,以遵守特定的时序约束并与主程序交换位数据。本文展示了 PIO 的工作原理,列出了组件及其所有编程语言语句。最后,我们看到了状态机的许多配置选项。您最多可以调用 8 个状态机来与主程序一起工作 — 您的用例是什么?

猜你喜欢

转载自blog.csdn.net/gongdiwudu/article/details/131912987