RP-series PIO(六)-PIO 使用入门之 PIO 和 DMA
PIO 和 DMA(一个逻辑分析器)
前面的文章介绍过如何从处理器直接向 PIO 写入数据,这往往会使处理器处于空转状态,来等待 FIFO 缓冲区有空间来进行数据传输,导致处理器得不到充分利用,同时也限制了能达到的总数据吞吐量。
RP系列微控制器配备有直接内存访问单元(DMA),用于不通过 CPU 传输数据。通过适当编程,DMA 可以在无监管的情况下对长序列数据进行传输。每个系统时钟最多可向一个 PIO 状态机输出或输入一个字的数据,从技术上来讲,这样的数据传输带宽其实非常大。该带宽由所有状态机共享,但可以编程让一个状态机占用全部带宽。
本文分析 logic_analyser
示例,利用 PIO 对 RP 系列微控制器自身的一些引脚进行采样,并以全速捕获这些引脚上正在发生情况的逻辑轨迹。
void logic_analyser_init(PIO pio, uint sm, uint pin_base, uint pin_count, float div) {
// Load a program to capture n pins. This is just a single `in pins, n`
// instruction with a wrap.
uint16_t capture_prog_instr = pio_encode_in(pio_pins, pin_count);
struct pio_program capture_prog = {
.instructions = &capture_prog_instr,
.length = 1,
.origin = -1
};
uint offset = pio_add_program(pio, &capture_prog);
// Configure state machine to loop over this `in` instruction forever,
// with autopush enabled.
pio_sm_config c = pio_get_default_sm_config();
sm_config_set_in_pins(&c, pin_base);
sm_config_set_wrap(&c, offset, offset);
sm_config_set_clkdiv(&c, div);
// Note that we may push at a < 32 bit threshold if pin_count does not
// divide 32. We are using shift-to-right, so the sample data ends up
// left-justified in the FIFO in this case, with some zeroes at the LSBs.
sm_config_set_in_shift(&c, true, true, bits_packed_per_word(pin_count));
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_RX);
pio_sm_init(pio, sm, offset, &c);
}
示例程序仅由一条 in pins, <pin_count>
指令构成,程序中启用了程序循环和自动拉取功能。由于要移位的数据量只有在运行时才能确定,而且程序很短,示例程序采用(使用“pio_encode_”系列函数)动态生成的方式,不需要通过PIO汇编器( pioasm
)来处理程序。程序被封装在一个数据结构中,该数据结构说明了程序的大小和程序加载位置,起始地址( origin
)设置为 -1,意思是“无需指定”。
-
输入移位寄存器
-
输入移位寄存器(ISR)是输出移位寄存器(OSR)的镜像。通常,数据在状态机中按以下两种方向之一流动:
系统 → TX FIFO 缓冲区 → OSR → 引脚
,或者引脚 → ISR → RX FIFO 缓冲区 → 系统
。in
指令可将数据移入 ISR。 - 如果不需要 ISR 的移位功能,可以将 ISR 当作第三个暂存寄存器来使用。它的大小为32位,与 X、Y 寄存器以及 OSR 相同。详细信息可查阅 《RP2350数据手册》。
将程序加载到选定的 PIO 中,然后在选定的状态机上配置输入引脚映射,以便 in pins
(输入引脚)指令能够访问程序所关注的引脚。对于 in pins
指令,开发人员只需配置基准引脚即可,基准引脚作为 in pins
指令采样的最低有效位引脚。要采样的引脚数量由 in pins
指令的位数参数决定 —— 它将从我们指定的基准引脚开始对 n 个引脚进行采样,并将它们移入 ISR。
-
引脚组(映射)
-
之前的文章中提到过,有四个引脚组需要进行配置,以便将状态机的内部数据总线连接到它所操控的 GPIO 上。一个状态机可同时访问一个引脚组内的所有引脚,而且引脚组之间可以相互重叠。前文的示例中介绍了 out、 side-set 和 in 引脚组,第四个引脚为 set 引脚组。 out 引脚组指的是那些会受到从 OSR 移出数据影响的引脚,使用
out pins
或out pindirs
指令时,每次最多可处理32位数据。 -
set 引脚组与
set pins
和set pindirs
指令配合使用,每次最多可处理5位数据,其数据是直接编码在指令当中的。它对于切换控制信号很有用。side-set
引脚组与set
引脚组类似,但它会与另一个指令同时运行。注意:mov pin
指令会根据数据传输方向使用输入 in 引脚组或 out 引脚组。
配置时钟分频器可以降低状态机的执行速度:时钟除数为 n 意味着每 n 个系统时钟周期执行 1 条指令。SDK 的默认系统时钟频率为 125MHz。
sm_config_set_in_shif
函数将移位方向设置为向右,启用自动推送功能,并将自动推送阈值设置为32。状态机时刻关注移入 ISR 的数据总量,一旦达到或超过总计 32(或你所配置的任意数字)的移位计数, ISR 的内容连同来自 in
操作的新数据就会直接进入 RX FIFO 缓冲区。与此同时,ISR 会被清零。
sm_config_set_fifo_join
函数用于操作 FIFO 缓冲区,以便 DMA 能够获得更高的吞吐量。如果我们想要在每个时钟周期对每个引脚进行采样,那将会占用大量带宽!我们已经完成了对状态机应如何配置的描述,所以接下来使用 pio_sm_init
函数将配置加载到状态机中,并使状态机进入一个初始的干净状态。
-
先入先出合并(FIFO Joining)
- 每个状态机在都配备有两个 FIFO 缓冲区:发送先入先出(TX FIFO)缓冲区用于在数据传出系统时对其进行缓冲,而接收先入先出(RX FIFO)缓冲区则对传入的数据执行相同的操作。每个 FIFO 缓冲区都有四个数据槽位,每个槽位可容纳 32 位的数据。通常情况下,希望 FIFO 缓冲区尽可能深一些,这样在外设的时序关键操作与可能非常繁忙或具有较高访问延迟的系统组件的数据传输之间就会有更多的空闲时间。不过,这会带来较高的硬件成本。
- 如果只使用两个 FIFO (发送或接收)中的一个,那么一个状态机可以整合其资源,为单个 FIFO 缓冲区提供双倍的深度。 《RP2350数据手册》中有更详细的内容,包括这种机制在底层实际上是如何运作的。
示例程序中的状态机已准备好对一些引脚进行采样了。来看一下如何将 DMA 与状态机连接起来,以及如何告知状态机一旦检测到某些触发条件就开始采样。
void logic_analyser_arm(PIO pio, uint sm, uint dma_chan, uint32_t *capture_buf, size_t capture_size_words,
uint trigger_pin, bool trigger_level) {
pio_sm_set_enabled(pio, sm, false);
// Need to clear _input shift counter_, as well as FIFO, because there may be
// partial ISR contents left over from a previous run. sm_restart does this.
pio_sm_clear_fifos(pio, sm);
pio_sm_restart(pio, sm);
dma_channel_config c = dma_channel_get_default_config(dma_chan);
channel_config_set_read_increment(&c, false);
channel_config_set_write_increment(&c, true);
channel_config_set_dreq(&c, pio_get_dreq(pio, sm, false));
dma_channel_configure(dma_chan, &c,
capture_buf, // Destination pointer
&pio->rxf[sm], // Source pointer
capture_size_words, // Number of transfers
true // Start immediately
);
pio_sm_exec(pio, sm, pio_encode_wait_gpio(trigger_level, trigger_pin));
pio_sm_set_enabled(pio, sm, true);
}
我们希望直接 DMA 从我们 PIO 状态机的 RX FIFO 缓冲区进行读取操作,这样每次 DMA 读取时的地址都是相同的。而另一方面,写入地址在每次 DMA 传输后都应该递增,以便随着数据的传入, DMA 能逐渐填满我们的捕获缓冲区。还需要指定一个数据请求信号(DREQ),以便 DMA 能以合适的速率传输数据。
-
数据请求信号(Data request signals)
- DMA 能够以极快的速度传输数据,而且几乎无一例外,这个速度会比任何 PIO 程序实际所需的速度快得多。 MA 会基于与状态机的数据请求握手来调整自身的传输节奏,所以只要选择了正确的数据请求(DREQ)信号,就不用担心它会造成 FIFO 缓冲区上溢或下溢的问题。状态机与 DMA 相互配合,告知它何时 TX FIFO 缓冲区中有可用空间,或者 RX FIFO 缓冲区中有可用的数据。
需要为 DMA 通道提供初始读取地址、初始写入地址以及要执行的读取/写入操作的总次数(而非字节总数)。接着会立即启动 DMA 通道——从这一刻起, DMA 就准备就绪了,等待状态机生成数据。一旦 RX FIFO 缓冲区中有了数据,DMA 就会迅速抓取数据,并将其传送到系统内存中的捕获缓冲区。
就目前情况而言,状态机一旦启用,就会立即进入一个执行 in
指令的单周期循环。由于可用于捕获的系统内存相当有限,所以状态机最好在等待某个触发条件后再开始采样。具体来说,使用 wait pin
(等待引脚)指令来暂停状态机,直到某个引脚变为高电平或低电平,而且我们再次使用 pio_encode_
系列函数之一来动态编码这条指令。
pio_sm_exec
函数会告知状态机立即执行给定的某个指令。这条指令永远不会被写入指令存储器,如果该指令暂停执行(在这种情况下就会如此 ——“wait”指令的作用就是暂停),那么状态机将会锁存该指令,直至其执行完成。由于状态机因 wait
指令而暂停,在启用它时,不会马上被大量数据淹没。
此时,一切都已准备就绪,只等来自选定 GPIO 的触发信号了。这将引发以下一系列事件:
wait
指令将会解除。- 在下一个周期,状态机将开始从程序存储器中执行
in
指令。 - 一旦 RX FIFO 缓冲区中有了数据,DMA 就会开始传输数据。
- 一旦 DMA 传输完所要求的数据量,它就会自动停止。
-
状态机执行功能(State Machine EXEC Functionality)
-
到目前为止,的状态机都是从指令存储器中执行指令,但还有其他选择。其中一个是
SMx_INSTR
寄存器(由pio_sm_exec()
函数使用):状态机将立即执行写入此处的任何内容,如有必要,会暂时中断它正在运行的当前程序。这对于从系统端深入探究状态机内部情况以及进行初始设置很有用。 -
另外两个选项(它们使用相同的底层硬件)分别是
out exec
(从流经 OSR 的数据中移出一条指令并执行它)和mov exec
(执行存储在例如暂存寄存器中的一条指令)。除了让人眼前一亮之外,如果希望状态机在输出流中的某个特定点执行一些由数据定义的操作,那么这些功能确实很有用。
示例代码提供了这样一个巧妙的函数,用于在终端中将捕获到的逻辑轨迹以 ASCII 艺术字的形式显示出来。
logic_analyser.c 第 89-108 行代码:
void print_capture_buf(const uint32_t *buf, uint pin_base, uint pin_count, uint32_t n_samples) {
// Display the capture buffer in text form, like this:
// 00: __--__--__--__--__--__--
// 01: ____----____----____----
printf("Capture:\n");
// Each FIFO record may be only partially filled with bits, depending on
// whether pin_count is a factor of 32.
uint record_size_bits = bits_packed_per_word(pin_count);
for (uint pin = 0; pin < pin_count; ++pin) {
printf("%02d: ", pin + pin_base);
for (uint32_t sample = 0; sample < n_samples; ++sample) {
uint bit_index = pin + sample * pin_count;
uint word_index = bit_index / record_size_bits;
// Data is left-justified in each FIFO entry, hence the (32 - record_size_bits) offset
uint word_mask = 1u << (bit_index % record_size_bits + 32 - record_size_bits);
printf(buf[word_index] & word_mask ? "-" : "_");
}
printf("\n");
}
}
现在,我们已经具备了让 RP 系列微控制器在运行其他程序的同时捕获自身引脚逻辑轨迹所需的一切条件。在这里,我们正在设置一个脉宽调制(PWM)切片,使其通过两个 GPIO 以大约15MHz 的频率输出,并将我们全新的逻辑分析仪连接到相同的两个 GPIO 上。
logic_analyser.c 第 110-159 行代码:
int main() {
stdio_init_all();
printf("PIO logic analyser example\n");
// We're going to capture into a u32 buffer, for best DMA efficiency. Need
// to be careful of rounding in case the number of pins being sampled
// isn't a power of 2.
uint total_sample_bits = CAPTURE_N_SAMPLES * CAPTURE_PIN_COUNT;
total_sample_bits += bits_packed_per_word(CAPTURE_PIN_COUNT) - 1;
uint buf_size_words = total_sample_bits / bits_packed_per_word(CAPTURE_PIN_COUNT);
uint32_t *capture_buf = malloc(buf_size_words * sizeof(uint32_t));
hard_assert(capture_buf);
// Grant high bus priority to the DMA, so it can shove the processors out
// of the way. This should only be needed if you are pushing things up to
// >16bits/clk here, i.e. if you need to saturate the bus completely.
bus_ctrl_hw->priority = BUSCTRL_BUS_PRIORITY_DMA_W_BITS | BUSCTRL_BUS_PRIORITY_DMA_R_BITS;
PIO pio = pio0;
uint sm = 0;
uint dma_chan = 0;
logic_analyser_init(pio, sm, CAPTURE_PIN_BASE, CAPTURE_PIN_COUNT, 1.f);
printf("Arming trigger\n");
logic_analyser_arm(pio, sm, dma_chan, capture_buf, buf_size_words, CAPTURE_PIN_BASE, true);
printf("Starting PWM example\n");
// PWM example: -----------------------------------------------------------
gpio_set_function(CAPTURE_PIN_BASE, GPIO_FUNC_PWM);
gpio_set_function(CAPTURE_PIN_BASE + 1, GPIO_FUNC_PWM);
// Topmost value of 3: count from 0 to 3 and then wrap, so period is 4 cycles
pwm_hw->slice[0].top = 3;
// Divide frequency by two to slow things down a little
pwm_hw->slice[0].div = 4 << PWM_CH0_DIV_INT_LSB;
// Set channel A to be high for 1 cycle each period (duty cycle 1/4) and
// channel B for 3 cycles (duty cycle 3/4)
pwm_hw->slice[0].cc =
(1 << PWM_CH0_CC_A_LSB) |
(3 << PWM_CH0_CC_B_LSB);
// Enable this PWM slice
pwm_hw->slice[0].csr = PWM_CH0_CSR_EN_BITS;
// ------------------------------------------------------------------------
// The logic analyser should have started capturing as soon as it saw the
// first transition. Wait until the last sample comes in from the DMA.
dma_channel_wait_for_finish_blocking(dma_chan);
print_capture_buf(capture_buf, CAPTURE_PIN_BASE, CAPTURE_PIN_COUNT, CAPTURE_N_SAMPLES);
}
该程序的输出看起来如下所示:
Starting PWM example
Capture:
16: ----____________----____________----____________----____________----_______
17: ------------____------------____------------____------------____-----------