RP2040 PICOSDK食用指南——PIO2简单的例子

.pio_version 0 // 仅需要PIO版本0

.program hello

; 从TX FIFO循环读取一个字的数据,当FIFO为空时暂停。
; 将最低有效位写入OUT引脚组。

loop:
    pull         ; 从TX FIFO获取数据字
    out pins, 1  ; 将数据的最低位输出到引脚
    jmp loop     ; 跳回循环开始处

% c-sdk {
// 初始化hello PIO程序的C函数
static inline void hello_program_init(PIO pio, uint sm, uint offset, uint pin) {
 
    pio_sm_config c = hello_program_get_default_config(offset); // 获取默认配置

    sm_config_set_out_pins(&c, pin, 1); // 配置OUT引脚组,将状态机的输出映射到指定引脚
    
    pio_gpio_init(pio, pin); // 初始化GPIO引脚功能(将PIO连接到引脚)
    
    pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true); // 设置PIO引脚方向为输出

    pio_sm_init(pio, sm, offset, &c); // 加载配置并跳转到程序开始处
    
    pio_sm_set_enabled(pio, sm, true); // 启用状态机
}
%}

loop:这个loop是标签,并非关键字

也就是第一个指令是pull ,他的意思就是前往TX FIFO取出所有内容并将其放入输出移位寄存器中,但是这个寄存器只有5位吧

隐式的含义是pull block,如果TX中有什么就抓住放入输出移位寄存器中

如果txo中没有任何内容,则暂停。不要继续下去,只需等待此指令,直到用户将某些内容放入该TX FIFO

将等待该拉取指令知道C程序实际将某些内容放入TX FIFO中

那么如果txo中有东西需要获取,他就会执行在pull block操作时所执行的操作,他会

如果我们确实没有拉出任何区块并且txo中没有任何东西,它就不会停止,他会转到x暂存寄存器,取出那里的任何内容

并将其放入输出移位寄存器,这也说明哪里有一些默认数据或者其他东西

但是在输入移位寄存器和输出移位寄存器上有这些移位计数器,用于计算已移入或移出的位数。我们可以设置一个阈值位数,如果我们执行PULL操作(如果为空)拉取指令就会直接通过他,我们不会做任何事情,除非我们已经计算出已移出的阈值位数,在这种情况下我们获取TX5中的内容并将其拉入输出移位寄存器

但是在例子中并没有使用这些

如果我们将任何数据放入TX FIFO 它获取信息并将其放入输出移位寄存器中。

我们程序中的下一条指令是输出指令,输出指令会获取输出移位寄存器中的所有内容并将其移动到指定的目的地

这里的意思是请获取输出移位寄存器的内容并将其移动到这个目的地,这里的引脚和最后一个参数告诉他,我们希望将输出移位寄存器中的多少位移动引脚上

因为是输出移位寄存器,所以会将其移动一位,获取被抛出的位并将其提供给引脚,

哪一个引脚?因此他会将其清空,因此如果您用32个1和0来填充它,俺么在完成后,如果您要做32个这样的输出引脚,那么它将充满全零

您可以进行的一种配置时,如果您希望偏移发生在最不重要的方向或者最重要的方向上,你可以选择这里的最后一条指令是无条件跳转。因此,这是一个无条件跳回循环程序标签的过程,此时我们将坐下来等待TXFIFO中出现其他内容然后继续

然后在下面你会发现一个初始化这个PIO的C程序这个程序不一定非得存在于这个PIO文件中

这是PI使用的一个约定,因为他将与PIO及其初始化配置相关的所有内容整合到这个地方。好的因此我们创建一个C函数,我们将调用它

来初始化该PIO程序。因此我们将这个C函数称为hello_program_init

作为参数您可以看到它采用一个名为PIO的PIO对象。

您记得有两个PIO块,对吗,就像有两个SPI/I2C通道一样

下一个参数是一个整数,它表示我们希望用于该PIO程序中PIO块的状态机。记得每个PIO块有四个

所以我们必须告诉他我们希望将那个PIO程序加载到状态机0 1 2或3上,下一个是偏移量

我们应该把这个程序放到指令储存器的什么位置上,我们稍后在查看C程序时会看到,我们可以自己指定这一点,或者可以使用SDK函数

这些函数允许我们说,请给我一个您未使用的状态机,请计算一个合适的偏移量,然后告诉我它的偏移量应该是多少,然后我就不用计算他了

这往往是这些程序的运行方式,但是如果你愿意的话,你可以把它放在特定的地方。

一个例子:也许您可以指定偏移量的一个例子是,默认情况下这些状态机默认从下往上加载到指令寄存器中,假设您在整个PIO BLOCK上有一个单指令PIO程序时候,您可以说只需给我一个偏移量即可加载他,它会把它放进去。他会将其加载到底部的指令内存地址31处。有时,如果您使用程序计数器玩游戏,那么让他从零偏移会很方便,对吗,因此这是一个例子,也许您可以对它进行一些定义

最后一个参数是一个代表引脚的整数,当我们说从输出移位寄存器中获取数据,并将其移动到引脚时,我们将其配置为我们正在谈论的引脚。

那么我们应该怎么做呢

我们要做的第一件事是创建一个PIO状态机配置类型的对象,您还记得当我们讨论DMA通道时,SDK具有我们正在使用的这些DMA通道配置对象。

非常类似的,SDK具有这些PIO状态机配置对象,如果你仔细观察,就会发现这些实际上只是所有代表PIO通道所有配置的字段结构

OK 我们创建一个该类型的对象,然后调用一个由编译器根据我们指定的PIO程序名称自动生成的函数

因此他会生成一个名为hello program get default config的函数

我们使用偏移地址调用它,他将使用所有默认配置初始化该PIO状态机对象,但其余部分只是将这些默认值修改为任何我们想要的值

好的我们得到了所有默认的PIO配置然后我们对他们进行一些改动。

举例来说,我们需要配置的事情之一是当我们调用一个输出指令时,我们制定来自该输出指令的数据的目标为引脚,我们说的是哪个引脚或那些引脚

因为他可能是配置发生的任何一个GPIO,我们调用状态机配置设置输出引脚作为参数,我们给他一个指向我们的状态机控制对象配置对象的指针

第二个参数是我们作为参数传入的引脚。因此,假设我们传入25个连接到LED的GPIO,那么25就会出现在那里

其中一个询问我从您指定的引脚开始要配置多少个引脚作为输出脚,因此如果我们在那里放置2,并将25作为参数传递

这将表示,我将把引脚25和26作为输出引脚,这件有一个约束条件

如果您将一条指令一个输出指令,一个设置指令吗,一个移动指令来控制多个引脚,那么这些引脚必须是连续的

当我们查看VGA驱动的时候也是,你会发现RGB引脚是连续的,就是这个原因

然后我们在该引脚上将PIO称为GPIO这是我们告诉GPIO你不会受到处理器控制也不会受到SPI/I2C的控制

你将被PIO控制

然后我们设置该引脚的引脚方向,因此我们传入的参数是这个PIOBLOCK,该PIO BLOCK上的状态机

此引脚1表示输出,如果我们在这里输入零则表示输入。

我们初始化PIO ,所以你会看到我们已经传递了PIO BNLOCK

我们正在讨论提供给我们的指令内存中的偏移量。我们将刚刚设置的PIO配置与该状态机关联了起来

所以所有这些东西都是我们对抽象的PIO配置对象进行配置。此行是我们将这些配置与我们特定PIO程序关联起来的地方。

然后这里的最后一条指令是我们启用PIO并启动它。

#include <stdio.h>

#include "pico/stdlib.h"
#include "hardware/pio.h"
// 我们汇编后的PIO程序:
#include "hello.pio.h"

// 本示例使用默认LED引脚
// 您可以通过定义HELLO_PIO_LED_PIN来使用不同的GPIO引脚
#if !defined HELLO_PIO_LED_PIN && defined PICO_DEFAULT_LED_PIN
#define HELLO_PIO_LED_PIN PICO_DEFAULT_LED_PIN
#endif

// 检查引脚是否与平台兼容
#if HELLO_PIO_LED_PIN >= NUM_BANK0_GPIOS
#error 尝试在不支持的平台上使用编号>=32的引脚
#endif

int main() {
#ifndef HELLO_PIO_LED_PIN
#warning pio/hello_pio示例需要一块带有标准LED的开发板
#else
    PIO pio;        // PIO控制器指针
    uint sm;        // 状态机编号
    uint offset;    // 程序加载偏移量

    // 初始化默认UART用于调试输出
    setup_default_uart();

    // 查找空闲的PIO和状态机,加载我们的程序
    // 使用pio_claim_free_sm_and_add_program_for_gpio_range以便在需要时支持编号>=32的GPIO,我们将其指向我们的PIO程序。我们将它指向我们的PIO类型对象
    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);  // 断言操作成功

    // 配置并启动状态机,使用我们在.pio文件中定义的辅助函数
    printf("使用GPIO %d\n", HELLO_PIO_LED_PIN);
    hello_program_init(pio, sm, offset, HELLO_PIO_LED_PIN);

    // 状态机现在已运行。任何推送到其TX FIFO的值都会出现在LED引脚上
    // 按任意键退出循环
    while (getchar_timeout_us(0) == PICO_ERROR_TIMEOUT) {
        // 点亮LED
        pio_sm_put_blocking(pio, sm, 1);
        sleep_ms(500);
        // 熄灭LED
        pio_sm_put_blocking(pio, sm, 0);
        sleep_ms(500);
    }

    // 释放资源并卸载程序
    pio_remove_program_and_unclaim_sm(&hello_program, pio, sm, offset);
#endif
}
  1. PIO 状态机:PIO 是 Pico 芯片的可编程外设,可以执行自定义指令来控制 GPIO 引脚。
  2. TX FIFO:每个状态机有一个 4 字深的 TX FIFO,用于存储待发送的数据。
  3. 阻塞特性:如果 FIFO 已满,pio_sm_put_blocking 会暂停程序,直到 FIFO 有空间(即状态机从 FIFO 读取数据后)。

如果您不希望程序阻塞,可以使用非阻塞版本:

bool pio_sm_put(PIO pio, uint sm, uint32_t data);

这个函数会尝试写入数据并返回成功或失败(FIFO 已满时返回 false),但不会等待。

以下函数pio声明自由状态机并为GPIO范围添加程序。我们的状态机,对象,我们的um int (将保存我们的偏移量),我们想要使用的引脚以及最后两个参数

让我在PIO0/1/2上给你一个空闲的PIO块

你刚刚提供给我的PIO程序在指令储存其中的合适位置。所以这一切都是免费给我们的。

这个正是我们刚刚编写的加载PIO程序的地方。因此这里的交互代码将会弹出一个到TX50中我们的PIO状态机将在该PULL至零上停滞,在该指令出现时他会抓住他,并将其移出到恰好连接到LED的引脚上,因此引脚将转动,LED将亮起。这些指令每个周期执行一次

因此,我们将其弹出到TX FIFO中。下一个时钟周期出现在引脚上。在接下来的时钟周期中,我们执行跳转回拉,接下来的那个,我们就坐在那里等待。

因此PIO project绝对比我们更胜一筹,它彻底打败了我们的C程序

 

posted @ 2025-07-08 18:58  mcwhirr  阅读(238)  评论(0)    收藏  举报