TI AM263P MCSPI传输序列与FIFO模式深度解析与实战
1. 项目概述与MCSPI核心价值
搞嵌入式开发,尤其是用到TI的Sitara系列或者类似的高性能MCU,SPI通信绝对是绕不开的坎。你可能用它来读取高精度ADC的数据,驱动一块TFT屏幕,或者与一个复杂的FPGA进行数据交换。但当你翻开TI那动辄几千页的技术参考手册(TRM),看到里面密密麻麻的寄存器描述和时序流程图时,是不是经常感到一阵头大?特别是关于如何正确地启动一次传输、如何优雅地处理收发中断、以及如何利用DMA和FIFO来解放CPU,这些细节往往决定了你项目的稳定性和性能上限。
今天,我们就以TI AM263P微控制器中的MCSPI(多通道串行外设接口)模块为例,抛开那些晦涩的官方术语,从一个一线工程师的视角,彻底拆解它的传输序列(Transfer Sequence)和FIFO模式的配置逻辑。SPI本身是一个简单的同步串行协议,但MCSPI控制器把它变得异常强大和复杂——它支持多通道独立或并发操作,每种传输模式(全双工、只发、只收)配合中断或DMA,再叠加上FIFO缓冲,衍生出十几种不同的配置流程。手册里的表格和流程图虽然准确,但更像是一张张“处方”,告诉你步骤ABCD,却很少解释“为什么是A而不是B”。我的目标就是把这些“处方”背后的病理和药理讲清楚,让你不仅能照着做,更能理解每一步的意图,从而在遇到奇葩外设或苛刻时序要求时,能自己开出正确的“药方”。
2. MCSPI传输序列的核心逻辑与设计思路
在深入代码之前,我们必须先建立几个核心认知,这是理解后续所有操作的基础。
2.1 MCSPI的三种基本角色与两种数据传输机制
MCSPI模块可以在三种模式下工作:控制器(Controller)模式、外设(Peripheral)模式,以及一个特殊的控制器Turbo模式。在绝大多数情况下,我们的MCU作为主机,使用的是控制器模式。外设模式通常用于MCU之间通过SPI互联,或者模拟一个SPI从设备。Turbo模式则是控制器模式的一个变种,针对某些特定的高吞吐量场景进行了优化。
无论哪种模式,数据搬移的核心机制只有两种:中断(IRQ)和直接内存访问(DMA)。这是所有现代MCU外设的通用设计哲学:
- 中断模式:适合数据量小、传输不频繁的场景。每次发送缓冲空(TX_EMPTY)或接收缓冲满(RX_FULL)时,产生一个中断,CPU跳转到中断服务程序(ISR)中,手动读写数据寄存器。这种方式编程直观,但频繁的中断会消耗大量CPU资源。
- DMA模式:适合大数据量、连续传输的场景。你只需要配置好源地址、目标地址和数据长度,DMA控制器就会在后台自动完成数据在内存和SPI数据寄存器之间的搬运,仅在传输开始和结束时通知CPU。这能极大减轻CPU负担,实现高效的数据流。
手册中所有复杂的序列,本质上都是在描述这两种机制在不同角色、不同方向(收/发)下的组合拳法。
2.2 理解“传输结束(End of Transfer)”的多样性
这是最容易让人困惑的地方。一次SPI传输什么时候算结束?答案并不是唯一的,它取决于你的传输模式和数据量(N)。
手册中的Table 13-36(传输结束序列总结表)是这个概念的核心。我们来解读一下:
- 对于全双工(Transmit-and-Receive)模式:无论用中断还是DMA,传输结束的条件都很直观——发送了N个字,同时也接收了N个字。因为SPI时钟同时驱动收发,收发数量必然相等。
- 对于只发(Transmit-Only)模式:结束条件是发送了N个字。但这里有个关键点,在中断模式下,你需要等待一个额外的
EOT(传输结束)状态位变为1,以确保最后一个bit已经成功移出。 - 对于只收(Receive-Only)模式:这是最复杂的,也是Turbo模式存在的主要原因。
- 控制器普通模式:要接收N个字,你必须先手动触发N次发送(因为SPI时钟需要由主机发出),实际只关心收到的N个字。所以DMA传输大小是N-1(最后一个字通过中断处理)。
- 控制器Turbo模式:针对需要连续接收的场景做了优化,其DMA传输大小变为N-2,内部状态机更复杂,旨在减少中断次数,提升连续流接收效率。
- 外设模式:作为从机,时钟由外部主机提供,因此只需要安静地接收N个字即可,逻辑最简单。
理解这张表,你就掌握了MCSPI传输状态机的“宪法”。后续所有具体的步骤,都是这部“宪法”在不同场景下的“实施细则”。
2.3 关键软件变量与状态机
手册在描述流程时,引入了几个关键的“软件变量”,如write_count,read_count,last_transfer等。务必注意,这些变量不是MCSPI硬件寄存器,而是需要你在软件中自己维护的状态标志!它们的作用是让主循环(Main Process)和中断服务程序(ISR)之间能够同步,知道当前传输进行到了哪一步。
write_count/read_count:分别记录已发送和已接收的数据字(word)数量。last_transfer:一个标志,用于告诉主循环“最后一个数据字已经提交给硬件,可以等待最终结束了”。channel_enable:主要用于Turbo接收模式,控制内部状态。last_request:在无FIFO的只收模式中,标记最后一次接收请求。
你的软件状态机必须紧密配合硬件状态机。硬件通过状态位(如TXx_EMPTY,RXx_FULL,EOT)告诉你“现在可以做什么”,而你的软件变量则记录“我们已经做了什么”。两者结合,才能准确无误地走完整个传输流程。
3. 无FIFO模式下的传输序列详解与实操
我们先从最简单的无FIFO模式开始。在这种模式下,SPI的发送(TX)和接收(RX)缓冲区通常只有一个字(word)的深度。这意味着数据必须“即产即销”,对时序的要求非常严格。
3.1 通用传输序列框架
无论何种具体模式,一个完整的传输都遵循一个通用框架,如手册Table 13-35所示。我们可以将其转化为更易懂的伪代码和操作意图:
// 伪代码:通用传输主流程 1. 清除通道中断状态 (MCSPI_IRQSTATUS[channel i bits] = 0b1111) - **意图**:避免残留的中断标志误触发。 2. 使能所需中断 (MCSPI_IRQENABLE = 0x...) - **意图**:告诉MCSPI,当TX空或RX满时,请通知我(CPU或DMA)。 3. 配置通道参数 (MCSPI_CHCONF_x = 0x...) - **意图**:设置时钟极性/相位(CPOL/CPHA)、波特率、字长、模式(控制器/外设)等。这是SPI通信的基石,配置错误会导致数据错位。 4. 启动通道 (MCSPI_CHCTRL_x[0] EN = 1) - **意图**:拉高片选信号(对于控制器模式),并启动SPI时钟引擎。 5. 等待第一次写请求(TX空或DMA写请求) - **意图**:SPI引擎就绪,可以接收第一个要发送的数据了。对于只收模式,这一步也需要写入一个“哑元”(dummy)数据来产生时钟。 6. 向发送寄存器写入数据 (MCSPI_TX_x = data) - **意图**:将数据压入发送移位寄存器。 7. 等待传输结束的主机事件 - **意图**:这是核心循环,根据不同的模式(全双工、只发、只收)和机制(中断、DMA),等待`write_count`/`read_count`达到N,或`last_transfer`等标志置位。这个“等待”在非阻塞程序中可能是查询状态位,在阻塞程序中可能是等待信号量。 8. 停止通道 (MCSPI_CHCTRL_x[0] EN = 0) - **意图**:结束��输,拉低片选信号。3.2 全双工(Transmit-and-Receive)中断模式实操解析
假设我们要用通道0,以中断方式收发100个字的数据。以下是基于手册Table 13-37和13-38的详细实现和注释:
// 全局状态变量(软件维护) volatile uint32_t g_write_count = 0; volatile uint32_t g_read_count = 0; uint32_t g_tx_buffer[100]; uint32_t g_rx_buffer[100]; // 主函数中的传输启动流程 void SPI_StartTransceive(void) { // 1. 清除通道0的中断状态位 HW_WR_REG32(MCSPI_BASE + MCSPI_IRQSTATUS, (1 << 0)); // 假设bit0对应通道0 // 2. 使能通道0的TX空和RX满中断 HW_WR_REG32(MCSPI_BASE + MCSPI_IRQENABLE, (1 << 8) | (1 << 2)); // 假设TX0_EMPTY_EN在bit8, RX0_FULL_EN在bit2 // 3. 配置通道0:主模式,CPOL=0, CPHA=0, 16位数据,使能中断 uint32_t chconf = 0; chconf |= (0x0 << 12); // 选择SPIEN线(主模式) chconf |= (0x0 << 8); // CLKG位,时钟格式 chconf |= (0x1 << 7); // 使能中断请求 // ... 其他配置位 HW_WR_REG32(MCSPI_BASE + MCSPI_CHCONF0, chconf); // 4. 启动通道0 HW_WR_REG32(MCSPI_BASE + MCSPI_CHCTRL0, 0x1); // EN位写1 // 5. 主循环等待传输完成 while(!(g_write_count == 100 && g_read_count == 100)) { // 这里可以执行其他低优先级任务,或者使用RTOS的等待事件/信号量 // 实际项目中,强烈建议使用信号量而非忙等待 } // 8. 传输完成,停止通道 HW_WR_REG32(MCSPI_BASE + MCSPI_CHCTRL0, 0x0); // EN位写0 // 可选:关闭中断使能 HW_WR_REG32(MCSPI_BASE + MCSPI_IRQENABLE, 0x0); } // 中断服务程序 (ISR) void MCSPI_IRQHandler(void) { // 读取中断状态,判断中断源 uint32_t irq_status = HW_RD_REG32(MCSPI_BASE + MCSPI_IRQSTATUS); // 处理通道0中断 if (irq_status & (1 << 0)) { // 假设bit0是通道0中断标志 // 清除通道0的状态位(非常重要!) HW_WR_REG32(MCSPI_BASE + MCSPI_IRQSTATUS, (1 << 0)); uint32_t ch_stat = HW_RD_REG32(MCSPI_BASE + MCSPI_CHSTAT0); // 读取通道状态 // 情况A:发送缓冲区空,且还有数据要发 if ((ch_stat & (1 << 8)) && (g_write_count < 100)) { // 假设TX_EMPTY在bit8 HW_WR_REG32(MCSPI_BASE + MCSPI_TX0, g_tx_buffer[g_write_count]); g_write_count++; } // 情况B:接收缓冲区满,且还有数据要收 if ((ch_stat & (1 << 2)) && (g_read_count < 100)) { // 假设RX_FULL在bit2 g_rx_buffer[g_read_count] = HW_RD_REG32(MCSPI_BASE + MCSPI_RX0); g_read_count++; } } }关键实操心得:
- 中断清除顺序:一定要先读取
MCSPI_IRQSTATUS,再写入特定位来清除。有些MCU架构要求通过“写1清0”,有些则要求“读后自动清除”或“写入特定值清除”。务必查阅数据手册的精确描述,错误的清除方式可能导致中断丢失或死锁。- 状态判断:在ISR中,不能只依赖全局中断状态寄存器
MCSPI_IRQSTATUS来判断是TX空还是RX满,必须结合通道状态寄存器MCSPI_CHSTATx的TXx_EMPTY和RXx_FULL位。因为一个通道中断可能由多种事件触发。- 变量原子性:
g_write_count和g_read_count在ISR和主循环中被并发访问,必须声明为volatile,并且在32位以上架构中,如果主循环进行非原子性的读取比较(如g_write_count == 100),可能存在风险。在临界代码段可以考虑使用简单的关中断或原子操作来保护。
3.3 只收(Receive-Only)模式与DMA配置的陷阱
只收模式是配置的“重灾区”。我们以控制器普通模式、DMA读取为例(对应手册Table 13-45和13-46)。关键点在于:要接收N个字,DMA只需要配置搬运N-1个字。
为什么?因为SPI是同步接口。作为主机,每产生一个时钟周期,既会发送一位数据,也会接收一位数据。因此,要接收数据,你必须同时发送数据(通常是无效的dummy数据,如0x00或0xFF)。DMA可以自动完成接收数据的搬运,但产生发送时钟的“触发源”需要被管理。
流程精讲:
- 初始化:
write_count = 0,read_count = 0,last_transfer = FALSE。 - 启动通道:这会拉高片选,并开始产生SPI时钟吗?不,此时时钟是停滞的,因为发送缓冲区是空的,没有数据可以移出。
- 关键的第一步:你需要手动写入第一个数据到
MCSPI_TXx寄存器。这个写入动作,才真正触发了第一个SPI时钟周期,并开始接收第一个数据字。这个操作通常在主程序中,在启动通道后立即进行。 - DMA的工作:你配置DMA从
MCSPI_RXx寄存器自动搬运数据到内存。当DMA完成了N-1次搬运后(即read_count = N-1),它会停止请求。 - 最后的收尾:此时,第N个数据字已经通过SPI线接收,并停留在
MCSPI_RXx寄存器中,等待读取。RXx_FULL中断会产生。在对应的ISR里,你检查到read_count == N-1,便设置last_transfer = TRUE。主程序检测到这个标志后,手动读取最后一次MCSPI_RXx,然后停止通道。
// 伪代码片段:只收DMA模式的核心逻辑 // 主程序 start_channel(); write_dummy_data_to_TX(); // 手动写入第一个dummy数据,启动时钟 wait_until(read_count == N-1); // 等待DMA完成N-1次搬运 disable_DMA_read_request(); // 关闭DMA请求,防止后续干扰 wait_until(last_transfer == TRUE); // 等待ISR发出最后一个字就绪的信号 stop_channel(); read_final_data_from_RX(); // 手动读取第N个字 // ISR if (RXx_FULL interrupt) { if (read_count == N-1) { // DMA已经搬完了N-1个,这是最后一个字 last_transfer = TRUE; // 注意:这里并不读取寄存器!最后一个字由主程序读取。 } else { // 正常情况下,DMA会处理,ISR不应该进入这个分支。如果进入,说明DMA可能未正确配置。 } }避坑指南:
- DMA大小配置:这是最容易出错的地方。对于“控制器普通只收模式”,DMA传输大小(Transfer Size)必须设置为N-1,而不是N。如果设成N,DMA会尝试搬运第N个数据,但此时可能因为时序问题导致数据不完整或冲突。
- 第一个时钟的触发:务必记住,在只收模式下,启动通道(EN=1)并不会自动开始产生时钟。你必须先写入一个数据到TX寄存器,才能“撬动”整个传输流程。这个数据的内容取决于你的从设备,通常是无意义的dummy数据。
- Turbo模式的区别:当需要高速连续接收时,使用Turbo模式。它的DMA大小是N-2,内部有一个更复杂的双缓冲区机制来平滑数据流,减少中断延迟的影响。除非你对吞吐量有极致要求,否则先从普通模式开始调试。
4. FIFO模式:提升吞吐量的利器
当数据量较大或频率较高时,无FIFO模式下的频繁中断会成为系统瓶颈。MCSPI的FIFO模式就是为了解决这个问题而生的。它为每个通道(注意:多通道下,同一时间只能有一个通道使用FIFO)配备了缓冲区,可以一次性写入或读取多个数据字,从而大幅减少中断或DMA请求的次数。
4.1 FIFO模式的核心配置与启用
启用FIFO模式不是简单地设置一个开关,它涉及几个关键寄存器:
MCSPI_CHCONFx[28] FFER(FIFO Enable Read):使能接收FIFO。MCSPI_CHCONFx[27] FFEW(FIFO Enable Write):使能发送FIFO。MCSPI_XFERLEVEL:这是FIFO模式的“大脑”。它不单独属于某个通道,而是全局设置FIFO的触发水位线(Watermark)。你需要配置:AEL:接收FIFO的“几乎空”级别。当RX FIFO中的数据量小于或等于此值时,可能触发中断或DMA请求(取决于配置)。AFL:发送FIFO的“几乎满”级别。当TX FIFO中剩余空间小于或等于此值时,可能触发中断或DMA请求。
配置流程:
// 1. 检查并确保其他通道的FIFO未被启用(重要!) // 2. 配置传输水位线。例如,设置当TX FIFO至少有4个空位时触发写请求,当RX FIFO至少有4个数据时触发读请求。 HW_WR_REG32(MCSPI_BASE + MCSPI_XFERLEVEL, (3 << 8) | (3 << 0)); // AFL=3, AEL=3 (级别是0-indexed,3代表第4个元素) // 3. 配置通道控制寄存器,使能发送和/或接收FIFO uint32_t chconf = HW_RD_REG32(MCSPI_BASE + MCSPI_CHCONF0); chconf |= (1 << 28); // 使能接收FIFO (FFER) chconf |= (1 << 27); // 使能发送FIFO (FFEW) HW_WR_REG32(MCSPI_BASE + MCSPI_CHCONF0, chconf);4.2 带字计数(Word Count)与不带字计数的流程差异
FIFO模式的传输序列分为两大类:带字计数和不带字计数。这是理解FIFO流程图(Figure 13-42 至 13-46)的关键。
- 带字计数(Word Count):你确切地知道要传输多少个数据字(N)。流程相对规整。传输的结束通常由EOW(End of Word Count)中断来标志。当发送或接收的字数达到预设值时,硬件产生EOW中断,通知主机传输完成。
- 不带字计数(Word Count):用于流式传输,数据长度未知或连续。传输的结束需要由主机主动判断。通常的判断逻辑是:发送FIFO为空(
TXFFE)且传输结束(EOT)状态有效,同时结合last_request等软件标志。
以FIFO模式全双工带字计数(Figure 13-42)为例,其软件逻辑如下:
- 主程序启动通道。
- 等待EOW中断。
- 在EOW中断服务程序中,检查
read_count是否为0。如果不为0,说明RX FIFO中还有数据,需要全部读出。 - 在FIFO请求例程(被中断或DMA触发)中:
- 如果
TXx_EMPTY(TX FIFO有空位),则判断剩余待发送数据量(write_count)。 - 如果剩余数据量大于等于一次FIFO请求的大小(
write_request_size),则一次性写入write_request_size个字到TX FIFO。 - 如果剩余数据量不足一次FIFO请求,则只写入最后
last_write_request_size个字。 - 对接收侧同理,当
RXx_FULL(RX FIFO有数据)时,根据read_count判断是批量读取还是读取最后一部分数据。
- 如果
write_request_size和read_request_size是什么?它们不是直接配置的寄存器,而是由你根据MCSPI_XFERLEVEL设置的水位线和FIFO深度(通常是64字节)在软件中计算出的一个优化值。例如,如果FIFO深度是16个字,AFL设为12,那么write_request_size可能就是4(16-12),意味着当TX FIFO有空位时,你最好一次性写入4个字来填满它,以达到最高的总线效率。
4.3 FIFO模式下的DMA配置技巧
在FIFO模式下使用DMA,效能提升最为显著。配置时需要注意:
- 突发传输(Burst):将DMA的源/目标突发大小(Burst Size)设置为与FIFO水位线相匹配的值。例如,如果
write_request_size是4,则将DMA的发送突发大小也设为4,这样一次DMA请求就能搬移4个字,与FIFO的物理特性对齐,效率最高。 - 流控制:在无FIFO模式下,DMA请求直接对应单个数据寄存器的空/满。在FIFO模式下,DMA请求对应的是FIFO的水位线事件。你需要正确配置
MCSPI_CHCONFx中的DMAW(DMA写使能)和DMAR(DMA读使能)位,并确保MCSPI_XFERLEVEL的水位线设置能及时触发DMA。 - 内存对齐:确保DMA搬运的内存缓冲区地址与数据宽度(如32位)对齐,可以避免不必要的性能损失或甚至硬件错误。
5. 常见问题排查与调试经验实录
即使理解了所有流程,实际调试MCSPI时依然会遇到各种问题。下面是我在多个项目中总结的“踩坑”记录和排查思路。
5.1 问题速查表
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 完全无数据收发 | 1. 时钟或片选引脚未正确映射/初始化。 2. 通道未启动( MCSPI_CHCTRLx[0] EN不为1)。3. 控制器/外设模式配置错误。 | 1. 检查PinMux配置,用示波器测量SCLK和CSn引脚是否有信号。 2. 读取 MCSPI_CHCTRLx寄存器确认EN位已置1。3. 确认 MCSPI_CHCONFx中CLKG、CLKD等位配置与从设备期望的CPOL/CPHA一致。 |
| 只能发送,不能接收 | 1. 只收模式下,未写入第一个TX数据来启动时钟。 2. 接收中断或DMA未使能。 3. 数据位宽不匹配(如配置为8位,但按32位读取)。 | 1. 在只收模式启动通道后,立即向MCSPI_TXx写入一个dummy值。2. 检查 MCSPI_IRQENABLE中RX_FULL中断是否使能,或MCSPI_CHCONFx中DMAR位是否置1。3. 核对 MCSPI_CHCONFx中的WL(字长)设置,并确保软件读写的数据类型与之匹配。 |
| 数据错位(如0xAA收到0x55) | 时钟相位(CPHA)或极性(CPOL)错误。 | 这是SPI最经典的问题。用示波器同时捕获SCLK、MOSI、MISO和CSn信号,与从设备的数据手册时序图严格比对。调整MCSPI_CHCONFx的CLKG和CLKD位。通常CPHA=0表示在时钟的第一个边沿采样,CPHA=1则表示在第二个边沿采样。 |
| DMA传输少一个或多个字 | DMA传输大小配置错误,特别是只收模式。 | 回顾第3.3节。对于“控制器普通只收模式”,DMA传输大小应设为N-1。检查DMA配置寄存器的传输计数(Transfer Size)字段。 |
| FIFO模式下传输不稳定或卡死 | 1. 多个通道同时使能了FIFO。 2. MCSPI_XFERLEVEL水位线设置不合理。3. 带/不带字计数的结束判断逻辑混淆。 | 1.确保全局只有一个通道的FFER或FFEW为1。在启用前,遍历检查所有通道的MCSPI_CHCONFx寄存器。2. 水位线不宜设得过低或过高。对于发送,AFL通常设为FIFO深度的一半到四分之三;对于接收,AEL设为四分之一到一半。先从中间值开始调试。 3. 明确本次传输是否有明确长度。有则用EOW中断结束;无则需轮询 TXFFE和EOT位,并配合软件标志。 |
| 中断频繁触发,CPU负载高 | 1. 无FIFO模式下传输大量数据。 2. FIFO水位线设置过高(AEL太小或AFL太大)。 | 1. 对于大数据量传输,务必使用DMA或FIFO模式。 2. 调整 MCSPI_XFERLEVEL。提高AEL值(让RX FIFO攒更多数据再中断),降低AFL值(让TX FIFO空位更多时再中断)。目标是减少中断频率,但避免FIFO上溢或下溢。 |
| 从设备响应慢,首字节丢失 | 片选(CSn)激活到第一个时钟沿的时间太短。 | 检查MCSPI模块是否支持配置片选建立时间(CS Setup Time)和保持时间(CS Hold Time)。在MCSPI_CHCONFx或相关寄存器中寻找相关配置位(如TCS、CSE等),适当增加建立时间。 |
5.2 调试方法与心得
- 寄存器诊断法:当通信异常时,不要急于修改代码。首先,编写一个简单的寄存器打印函数,将
MCSPI_CHCONFx、MCSPI_CHCTRLx、MCSPI_CHSTATx、MCSPI_IRQSTATUS等关键寄存器的值以十六进制打印出来。与手册复位值或你的配置预期逐位比对,往往能快速发现配置错误。 - 示波器/逻辑分析仪是终极武器:软件层面的任何推测,都不如用示波器看一眼波形来得直接。重点观察:
- CSn信号:是否在传输开始时拉低,结束时拉高?是否有毛刺?
- SCLK时钟:频率是否正确?空闲电平(CPOL)和采样边沿(CPHA)是否符合预期?
- MOSI/MISO数据:数据相对时钟边沿的位置是否正确?发送的数据是否与软件写入
MCSPI_TXx的值一致?接收的数据波形是否与从设备输出一致?
- 简化测试法:如果复杂传输不通,就退回到最简单的模式:无FIFO、无DMA、无中断,使用轮询(Polling)模式(手册13.1.3.5.2.1.9节)进行单字节收发测试。先让最基本的通信跑通,然后再逐步叠加中断、DMA、FIFO等高级功能。轮询模式的代码虽然效率低,但逻辑简单,是验证硬件连接和基础配置的绝佳方式。
- 利用芯片的Loopback功能:大多数MCSPI控制器都支持内部回环测试模式(通常在
MCSPI_CHCONFx寄存器中有一个LB位)。启用该模式后,发送的数据会直接被内部接收,无需外部硬件。这是验证控制器本身软件配置和驱动程序是否正确的最快方法。先通过内部回环测试,再连接外部设备,可以明确区分是软件问题还是硬件/时序问题。
6. 从理论到实践:一个完整的FIFO+DMA传输示例
最后,我们整合以上所有知识点,勾勒一个使用FIFO+DMA进行全双工大数据块传输的软件框架。假设我们需要通过MCSPI通道1,与一个SPI Flash芯片交互,连续读写4KB数据。
// 宏定义与全局变量 #define SPI_CHANNEL_1_CONF_REG (MCSPI_BASE + MCSPI_CHCONF1) #define SPI_CHANNEL_1_CTRL_REG (MCSPI_BASE + MCSPI_CHCTRL1) #define SPI_CHANNEL_1_STAT_REG (MCSPI_BASE + MCSPI_CHSTAT1) #define SPI_CHANNEL_1_TX_REG (MCSPI_BASE + MCSPI_TX1) #define SPI_CHANNEL_1_RX_REG (MCSPI_BASE + MCSPI_RX1) #define FIFO_DEPTH_WORDS 16 // 假设FIFO深度为16个字(64字节) #define TX_FIFO_AFULL_LEVEL 12 // TX FIFO几乎满水位线(当空位<=4时触发) #define RX_FIFO_AEMPTY_LEVEL 4 // RX FIFO几乎空水位线(当有数据>=4时触发) #define TRANSFER_SIZE_WORDS 1024 // 4KB / 4 bytes per word volatile bool g_spi_transfer_complete = false; uint32_t g_tx_data[TRANSFER_SIZE_WORDS]; uint32_t g_rx_data[TRANSFER_SIZE_WORDS]; // DMA完成回调函数 void dma_transfer_complete_callback(void) { // 设置传输完成标志,或释放信号量 g_spi_transfer_complete = true; } // 主配置与传输函数 void SPI_FIFO_DMA_Transceive(void) { // --- 第1步:检查并独占FIFO资源 --- // 遍历所有其他通道,确保其MCSPI_CHCONFx[28]和[27]位为0 // ... // --- 第2步:配置FIFO全局水位线 --- // AFL = TX_FIFO_AFULL_LEVEL, AEL = RX_FIFO_AEMPTY_LEVEL uint32_t xfer_level = (TX_FIFO_AFULL_LEVEL << 8) | (RX_FIFO_AEMPTY_LEVEL << 0); HW_WR_REG32(MCSPI_BASE + MCSPI_XFERLEVEL, xfer_level); // --- 第3步:配置MCSPI通道1,使能FIFO --- uint32_t chconf = 0; // ... 配置时钟、模式、字长等 ... chconf |= (1 << 28); // 使能接收FIFO (FFER) chconf |= (1 << 27); // 使能发送FIFO (FFEW) chconf |= (1 << 15); // 使能DMA读请求 (DMAR) chconf |= (1 << 14); // 使能DMA写请求 (DMAW) HW_WR_REG32(SPI_CHANNEL_1_CONF_REG, chconf); // --- 第4步:配置DMA控制器 --- // 配置DMA通道0用于发送(内存 -> MCSPI_TX1) // 源地址: g_tx_data // 目标地址: SPI_CHANNEL_1_TX_REG // 传输大小: TRANSFER_SIZE_WORDS // 源突发大小: (FIFO_DEPTH_WORDS - TX_FIFO_AFULL_LEVEL) = 4 // 目标突发大小: 同上 // 使能传输完成中断,回调函数设为dma_transfer_complete_callback // 配置DMA通道1用于接收(MCSPI_RX1 -> 内存) // 源地址: SPI_CHANNEL_1_RX_REG // 目标地址: g_rx_data // 传输大小: TRANSFER_SIZE_WORDS // 突发大小: RX_FIFO_AEMPTY_LEVEL = 4 // 注意:全双工FIFO模式,DMA大小直接设为N,无需N-1。 // ... // --- 第5步:启动传输 --- // 清除中断状态(可选,DMA模式下可能不依赖CPU中断) HW_WR_REG32(MCSPI_BASE + MCSPI_IRQSTATUS, (1 << 1)); // 清除通道1状态 // 启动DMA通道(先启动接收DMA,再启动发送DMA,顺序可能影响初始触发) // ... // 最后,启动MCSPI通道 HW_WR_REG32(SPI_CHANNEL_1_CTRL_REG, 0x1); // --- 第6步:等待传输完成 --- while(!g_spi_transfer_complete) { // 进入低功耗模式或执行其他任务 __WFI(); // 等待中断 } // --- 第7步:清理 --- HW_WR_REG32(SPI_CHANNEL_1_CTRL_REG, 0x0); // 停止通道 // 关闭DMA,清除FIFO使能位等 // ... } // 中断服务程序(如果需要处理非DMA错误) void MCSPI_IRQHandler(void) { uint32_t irq_status = HW_RD_REG32(MCSPI_BASE + MCSPI_IRQSTATUS); // 处理错误中断,如溢出错误、超时错误等 if (irq_status & (1 << 1)) { // 通道1中断 uint32_t ch_stat = HW_RD_REG32(SPI_CHANNEL_1_STAT_REG); if (ch_stat & (1 << 4)) { // 假设bit4是接收溢出错误位 // 处理错误:记录日志,重置FIFO,可能需要重新初始化通道 // HW_WR_REG32(SPI_CHANNEL_1_CONF_REG, 0x0); // 先禁用 // ... 清理FIFO ... // HW_WR_REG32(SPI_CHANNEL_1_CONF_REG, original_conf); // 重新配置 } HW_WR_REG32(MCSPI_BASE + MCSPI_IRQSTATUS, (1 << 1)); // 清除中断 } }这个示例展示了将FIFO与DMA结合的高效编程模型。核心思想是:让硬件(MCSPI+FIFO+DMA)自动管理数据流,CPU仅负责初始化和最终处理。通过精心设置FIFO水位线,可以确保DMA请求在数据量“恰到好处”时触发,从而实现总线带宽的饱和利用,同时将CPU干预降到最低。
调试这样的系统时,建议先屏蔽DMA,在FIFO中断模式下验证数据流是否正确,然后再启用DMA。同时,密切关注DMA和MCSPI的错误中断标志位,它们是你发现潜在数据溢出或配置不当的第一线索。
