1. SPI通信协议基础与核心原理
如果你在嵌入式领域摸爬滚打了一段时间,那么SPI(Serial Peripheral Interface)对你来说绝对不陌生。它就像电子设备之间说的一种“方言”,简单直接,效率极高。我最早接触SPI是在驱动一块Flash存储器时,当时被它那几根线就能实现高速数据交换的能力所吸引。与需要严格时序和地址寻址的I2C,或者需要起始位、停止位的UART不同,SPI的通信哲学更接近于“我说你听,同步进行”。它的核心是一个主设备(通常是你的MCU)和一个或多个从设备(如传感器、存储器、显示屏驱动芯片)。主设备完全掌控通信的节奏,通过产生时钟信号(SCLK)来同步数据的每一位传输。
这种主从架构决定了SPI的硬件连接至少需要四根线:SCLK(串行时钟)、MOSI(主出从入)、MISO(主入从出)和CS/SS(片选)。片选线是关键,主设备通过拉低对应从设备的片选线来“点名”它进行通信,其他未被选中的从设备则保持高阻态,互不干扰。这就好比在一个房间里,老师(主设备)点名某个学生(从设备)回答问题,其他学生保持安静。SPI是全双工的,这意味着数据可以同时在MOSI和MISO线上双向流动,在同一个时钟周期内完成数据的发送和接收,这是其高效率的源泉。
SPI协议本身没有严格的帧格式标准,其数据有效性由时钟极性(CPOL)和时钟相位(CPHA)共同定义,这构成了四种工作模式(Mode 0-3)。CPOL决定了SCLK在空闲时的电平(0为低电平,1为高电平),而CPHA决定了数据是在时钟的第一个边沿(CPHA=0)还是第二个边沿(CPHA=1)被采样。绝大多数器件的数据手册都会明确指定其支持的SPI模式。例如,很多Flash芯片工作在Mode 0(CPOL=0, CPHA=0)或Mode 3(CPOL=1, CPHA=1)。配置错误会导致数据采样错位,读回来的全是乱码,这是我早期调试时最常踩的坑之一。
2. MCSPI控制器架构与寄存器概览
当我们从理论走向实践,在具体的微控制器(比如TI的Sitara系列ARM处理器)上操作SPI时,我们打交道的不再是抽象的协议,而是一个具体的硬件模块:MCSPI(Multichannel Serial Peripheral Interface)。你可以把它想象成一个高度可编程的“SPI通信引擎”。它内部集成了时钟发生器、数据移位寄存器、FIFO缓冲区、中断和DMA控制器等一系列硬件单元。我们的任务,就是通过配置一系列内存映射的寄存器,来指挥这个引擎按照我们期望的方式工作。
输入材料中给出的寄存器列表,正是这个引擎的控制面板。乍一看密密麻麻的地址和缩写让人头大,但我们可以将其分门别类,理解其功能层次:
- 系统级控制与状态寄存器:这类寄存器管理整个SPI模块的全局行为。例如MCSPI_SYSCONFIG负责模块的软件复位、时钟门控和电源管理空闲模式;MCSPI_SYSSTATUS则用于查询复位是否完成等全局状态。MCSPI_MODULCTRL是模块的“总开关”,在这里你可以设置模块是工作在主模式(MS=0)还是从模式(MS=1),选择是单通道(SINGLE=1)还是多通道(SINGLE=0)主模式,甚至启用特殊的系统测试模式(SYSTEM_TEST)。
- 通道级配置寄存器:这是核心中的核心。对于每个独立的SPI通道(通常是0-3),都有一套独立的配置寄存器,如MCSPI_CHxCONF。在这里,你定义了该通道通信的所有细节:时钟分频比(CLKD)、字长(WL,支持4-32位!)、时钟极性与相位(POL, PHA)、传输模式(TRM, 收发、只收、只发)、是否启用Turbo模式(TURBO)以提升多字传输吞吐量,以及是否启用DMA请求(DMAR, DMAW)。每个通道都可以独立配置,互不影响,这为同时与多个不同特性的外设通信提供了可能。
- 数据缓冲区寄存器:即MCSPI_TXx和MCSPI_RXx。你要发送的数据写入TX寄存器,接收到的数据从RX寄存器读取。当启用FIFO功能时(通过MCSPI_CHxCONF的FFEW和FFER位),这些寄存器就变成了FIFO的访问端口,可以连续写入或读取多个数据字,大大减轻了CPU的负担。
- 中断与事件状态寄存器:这是实现高效、异步通信的关键。MCSPI_IRQSTATUS寄存器像一组状态指示灯,实时显示着“发送缓冲区空(TXx_EMPTY)”、“接收缓冲区满(RXx_FULL)”、“传输完成(EOW)”等事件是否发生。而MCSPI_IRQENABLE寄存器则是这些指示灯的开关,你可以选择关心哪些事件,并让它们在发生时触发CPU中断。例如,使能TX0_EMPTY_ENABLE后,每当通道0的发送FIFO为空(或几乎空),硬件就会产生一个中断,提醒CPU可以填充下一个数据了,从而实现“中断驱动”的流式传输。
- DMA相关寄存器:为了解放CPU,MCSPI提供了强大的DMA支持。MCSPI_XFERLEVEL寄存器可以设置FIFO触发DMA请求的水位线。当启用DMA时,数据搬运完全由DMA控制器完成,CPU只需设置好传输任务即可处理其他事务,这对于大数据块传输(如图像刷新、音频流)至关重要。
理解这个架构层次后,再去看那些具体的位域定义,就不会觉得是在看天书了。每一个配置位都对应着硬件逻辑电路的一个具体开关或参数。
3. 关键寄存器深度解析与配置策略
仅仅知道寄存器列表是不够的,我们必须深入几个最关键的寄存器,理解每个配置位背后的硬件行为,才能做出正确的选择。下面我结合自己的实战经验,对几个核心寄存器进行拆解。
3.1 通道配置寄存器(MCSPI_CHxCONF)——通信协议的蓝图
这个寄存器是设定SPI通信物理层参数的地方,其配置直接决定了波形。
- WL(位11-7) - 字长:这个字段定义了每个SPI数据帧包含多少位。它非常灵活,支持从4位到32位。这里有个关键点:字长设置必须与外设期望的完全一致。比如,一个12位的ADC,你可能需要设置WL=0x0B(11d, 对应12位)。如果设置成8位,你只会读到数据的前8位或后8位,导致数据解析错误。我曾调试过一个三轴加速度计,它要求以16位字长连续读取6个寄存器,如果字长设错,数据对齐就全乱了。
- POL(位1)与PHA(位0) - 时钟模式:这是SPI的“灵魂”。POL=0/1分别对应空闲时钟低电平/高电平;PHA=0/1分别对应在第一个/第二个时钟边沿采样数据。常见的Mode 0就是(POL=0, PHA=0),Mode 3是(POL=1, PHA=1)。务必与外设数据手册严格匹配。一个快速验证的方法是:用逻辑分析仪抓取波形,看数据线(MOSI/MISO)的变化是否发生在SCLK稳定的相位上,以及采样边沿是否正确。
- CLKD(位5-2)与CLKG(位29) - 时钟分频:它们共同决定SPI_SCLK的频率。公式大致为:
SCLK = Fclk / (预分频系数)。当CLKG=0时,分频系数是2的CLKD次幂(1, 2, 4, ..., 32768)。当CLKG=1时,分频系数是(EXTCLK << 4 | CLKD)+ 1,可以实现更精细的分频。选择频率时,首先要满足从设备的最大时钟频率要求,其次要考虑信号完整性。过高的频率在长导线或板子布局不佳时会导致数据错误。我通常从较低频率开始测试,稳定后再逐步提高。 - TRM(位13-12) - 传输模式:00=全双工收发,01=只接收,10=只发送。这个配置需要与外设的工作模式配合。例如,当你向一个只读的传感器发送读取命令时,实际上主设备是在“发送���,但从设备在“接收”命令并“发送”回数据。所以主设备通常配置为全双工(TRM=00),即使MOSI线上发送的是命令,MISO线上也能同时收到数据。
- TURBO(位19) - 加速模式:这是一个提升性能的利器。当启用Turbo模式且进行多字连续传输时,MCSPI会在字与字之间不释放片选(CS),从而节省了CS切换带来的时间开销。这仅适用于单次传输多个SPI字的场景,比如连续读取Flash的一个扇区。对于单字传输,开启Turbo反而可能因为额外的设置时间而降低效率。
3.2 中断控制寄存器(MCSPI_IRQENABLE/IRQSTATUS)——事件驱动的核心
轮询(Polling)方式简单,但CPU利用率低。中断方式高效,但配置稍复杂。这两个寄存器就是实现中断驱动的枢纽。
- TXx_EMPTY_ENABLE:发送缓冲区空中断使能。这是最常用的发送中断。当TX FIFO为空(或低于阈值)时触发,告知CPU可以写入下一个待发送数据了。在发送流中,你可以在中断服务程序(ISR)里检查是否还有数据要发,如果有就写入MCSPI_TXx,没有则关闭中断。
- RXx_FULL_ENABLE:接收缓冲区满中断使能。当RX FIFO达到满(或阈值)时触发,告知CPU可以读取数据了。在接收流中,你需要在ISR中及时读取MCSPI_RXx,防止数据溢出。
- EOWKE:字计数结束中断使能。当使用FIFO并设置了字计数器(MCSPI_XFERLEVEL[WCNT])时,传输完指定数量的字后会触发此中断。这对于已知长度的数据块传输非常方便,可以精确地在传输完成后进行后续处理。
- 操作流程:使能中断的典型步骤是:1)配置好通道并启动传输。2)在MCSPI_IRQENABLE中使能所需的中断源。3)在全局中断控制器中使能SPI中断向量。4)当硬件事件发生时,MCSPI_IRQSTATUS中对应的状态位会被置1。5)在ISR中,必须先读取IRQSTATUS的值来判断中断源,并进行相应处理(如填数据、读数据),然后向IRQSTATUS的对应位写1来清除该中断标志。这是一个关键细节,不清除标志位会导致中断持续触发。
3.3 传输流程控制与实操步骤
输入材料中给出了几种标准的传输流程表格,我们以“带字计数的收发流程(轮询法)”为例,将其转化为可操作的代码逻辑,并解释每一步的意图:
- 配置通道:根据你的外设要求,设置MCSPI_CHxCONF寄存器(时钟模式、频率、字长等)。这是通信的基石。
- (可选)配置字计数:如果你希望发送/接收特定数量的字,需要设置MCSPI_XFERLEVEL[WCNT]寄存器。这在发送固定长度命令或读取固定长度数据时非常有用。
- 启动通道:向MCSPI_CHxCTRL[0] EN位写1。这个操作会激活该通道的时钟和逻辑。
- 写入发送数据:将第一个要发送的数据字写入MCSPI_TXx寄存器。写入操作会启动时钟和传输过程。
- 轮询等待:在一个循环中,不断读取MCSPI_IRQSTATUS[17] EOW(字计数结束)或MCSPI_CHxSTAT中的状态位(如TXFFE发送FIFO空、EOT传输结束)。轮询间隔需要谨慎设计,太频繁浪费CPU,太慢可能错过状态。对于低速SPI,简单的
while循环即可;对于高速或实时性要求高的,建议使用中断。 - 读取接收数据:当状态指示接收完成(如RX_FULL或EOW),从MCSPI_RXx寄存器读取数据。注意:在全双工模式下,每写入一个发送字,理论上就会同时读回一个接收字,即使你并不关心从设备的回复。所以接收流程需要与发送流程配对。
- 停止通道:传输完成后,向MCSPI_CHxCTRL[0] EN位写0,关闭通道以节省功耗。
注意:在启用FIFO的情况下,步骤4和6可能会变成循环操作,即连续写入多个数据到TX FIFO,然后等待EOW中断或轮询EOW状态,最后再从RX FIFO中连续读出多个数据。
4. 从理论到实践:一个完整的SPI驱动传感器实例
让我们以一个具体的例子来串联所有知识:驱动一个常用的SPI接口数字温度传感器(例如TMP117)。假设我们使用MCU的SPI0通道,工作在Mode 0(CPOL=0, CPHA=0),时钟频率1MHz,传感器寄存器为16位。
4.1 初始化配置
首先,我们需要启用SPI模块的时钟(这部分依赖具体MCU的时钟树配置,此处略过)。然后进行寄存器初始化:
// 假设 MCSPI0 的基地址为 0x4803 0000 #define MCSPI0_BASE 0x48030000 #define MCSPI_CH0CONF (*(volatile uint32_t *)(MCSPI0_BASE + 0x12C)) #define MCSPI_MODULCTRL (*(volatile uint32_t *)(MCSPI0_BASE + 0x128)) #define MCSPI_CH0CTRL (*(volatile uint32_t *)(MCSPI0_BASE + 0x134)) void SPI_Init(void) { // 1. 软件复位模块 (可选,确保初始状态) // 设置 MCSPI_SYSCONFIG[1] SOFTRESET = 1 // 轮询 MCSPI_SYSSTATUS[0] RESETDONE 直到为1 // 2. 配置模块控制寄存器:主模式,单通道(因为我们只接一个传感器) MCSPI_MODULCTRL = (0 << 2) | // MS=0, 主模式 (0 << 0); // SINGLE=0, 假设我们使用多通道模式,但只使能一个通道 // 3. 配置通道0参数 uint32_t ch0_conf = 0; // POL=0, PHA=0 (Mode 0) ch0_conf &= ~(1 << 1); // 清除POL位 ch0_conf &= ~(1 << 0); // 清除PHA位 // 设置字长 WL = 16 bits -> 值应为 0xF (参见寄存器描述 WL=0xFh for 16-bit) ch0_conf |= (0xF << 7); // 设置时钟分频,假设输入时钟为48MHz,目标SCLK=1MHz,分频系数=48 // CLKG=0(2的幂次分频),最接近的是2^6=64分频(CLKD=6),得到0.75MHz,或2^5=32分频得1.5MHz。 // 我们选择32分频(CLKD=5)。注意:实际值需根据寄存器位域偏移计算,此处为示意。 ch0_conf |= (5 << 2); // 假设CLKD字段从bit2开始 // 传输模式:全双工收发 ch0_conf &= ~(3 << 12); // TRM=00 // 其他位保持默认,如不使用Turbo,不使用FIFO,DMA禁用等 MCSPI_CH0CONF = ch0_conf; // 4. (可选)配置中断,这里先禁用,使用轮询 // MCSPI_IRQENABLE = 0x0; }这段初始化代码的关键在于精确计算ch0_conf这个32位整数的值。你需要根据MCSPI_CHxCONF寄存器的位域定义,像拼图一样把每个参数放到正确的位置。使用位操作(|=用于设置,&=用于清除)是标准做法。务必查阅你的具体MCU参考手册,确认每个位的偏移量。
4.2 实现数据收发函数
接下来,我们实现一个基础的轮询式单字收发函数,用于读取温度传感器的寄存器。
#define MCSPI_TX0 (*(volatile uint32_t *)(MCSPI0_BASE + 0x138)) #define MCSPI_RX0 (*(volatile uint32_t *)(MCSPI0_BASE + 0x13C)) #define MCSPI_CH0STAT (*(volatile uint32_t *)(MCSPI0_BASE + 0x130)) #define MCSPI_IRQSTATUS (*(volatile uint32_t *)(MCSPI0_BASE + 0x118)) uint16_t SPI_Transfer(uint16_t data) { uint16_t received_data = 0; // 1. 确保通道已停止(安全操作) MCSPI_CH0CTRL &= ~(1 << 0); // 2. 启动通道 MCSPI_CH0CTRL |= (1 << 0); // 3. 写入要发送的数据(16位,注意对齐,通常写入低16位) MCSPI_TX0 = (uint32_t)data; // 4. 轮询等待传输结束。这里我们等待TX0_EMPTY和EOT标志。 // 注意:对于单字非FIFO传输,发送完成即代表接收也完成。 while (!(MCSPI_CH0STAT & ((1 << 3) | (1 << 2)))); // 等待TXFFE和EOT置位,具体位需查手册 // 或者使用IRQSTATUS的TX0_EMPTY位 // while (!(MCSPI_IRQSTATUS & (1 << 0))); // 5. 读取接收到的数据 received_data = (uint16_t)(MCSPI_RX0 & 0xFFFF); // 取低16位 // 6. 清除可能的中断标志(如果使用了中断使能) // MCSPI_IRQSTATUS = (1 << 0); // 写1清除TX0_EMPTY标志 // 7. 停止通道(对于单次传输,可以停止以省电;对于连续操作,可能保持开启) MCSPI_CH0CTRL &= ~(1 << 0); return received_data; }4.3 封装传感器读写操作
基于SPI_Transfer函数,我们可以封装符合传感器特定指令集的读写操作。假设TMP117的读寄存器命令格式为:16位指令,高8位为寄存器地址+读标志(例如0x90),低8位无关紧要;传感器会紧接着在下一个16位时钟周期返回寄存器的值。
#define TMP117_TEMP_RESULT_REG 0x00 #define TMP117_READ_CMD(reg) (0x8000 | ((reg & 0x7F) << 8)) // 构造读命令,最高位为1表示读 uint16_t TMP117_ReadReg(uint8_t reg_addr) { uint16_t cmd = TMP117_READ_CMD(reg_addr); uint16_t dummy = 0x0000; uint16_t received_val; // 第一次传输:发送读命令,同时会收到一个无意义的返回值(可能是上一个数据或全零) (void)SPI_Transfer(cmd); // 忽略第一次的返回值 // 第二次传输:发送任意数据(例如0x0000)以产生时钟,从而读取传感器返回的数据 received_val = SPI_Transfer(dummy); return received_val; } float TMP117_ReadTemperature(void) { uint16_t raw_temp = TMP117_ReadReg(TMP117_TEMP_RESULT_REG); // 根据TMP117数据手册将raw_temp转换为摄氏度 // 例如,TMP117是16位补码,精度为0.0078125°C/LSB float temperature = (int16_t)raw_temp * 0.0078125f; return temperature; }这个例子清晰地展示了SPI通信的“一问一答”模式。主设备先发送一个命令帧,紧接着再发起一次传输(发送任意数据)来提供时钟,从而读取从设备的响应帧。许多SPI传感器都采用这种“先写后读”的访问模式。
5. 高级应用:FIFO与DMA配置实战
当需要传输大量数据时,比如从SPI Flash读取固件,或者向SPI显示屏刷新一帧图像,逐个字节轮询的效率就太低了。这时就需要请出FIFO和DMA这两位“效率专家”。
5.1 FIFO功能配置与使用
FIFO(First In First Out)缓冲区允许你在SPI硬件传输的同时,提前写入多个待发送数据,或者暂存多个已接收数据,减少了CPU频繁干预的次数。
配置FIFO的核心步骤:
- 启用FIFO:在MCSPI_CHxCONF寄存器中,设置FFEW=1(启用发送FIFO)和/或FFER=1(启用接收FIFO)。注意:通常一个模块只有一个物理FIFO缓冲区,只能被一个通道独占使用。
- 设置传输水位线:通过MCSPI_XFERLEVEL寄存器设置AFL(Almost Full Level)和AEL(Almost Empty Level)。例如,设置AEL=2,意味着当发送FIFO中的数据量小于等于2个字时,会触发TXx_EMPTY事件(或中断)。这给了CPU一个提前量去填充数据,避免FIFO完全空掉导致传输暂停。
- 使用中断驱动:使能MCSPI_IRQENABLE中的TXx_EMPTY_ENABLE和RXx_FULL_ENABLE中断。
- 批量写入/读取:在中断服务程序中,检查FIFO状态,然后连续向MCSPI_TXx写入多个数据,或从MCSPI_RXx读取多个数据。你可以通过查询MCSPI_CHxSTAT寄存器中的TXFFE(发送FIFO空)、TXFFF(发送FIFO满)、RXFFE(接收FIFO空)、RXFFF(接收FIFO满)等位来了解FIFO的当前状态。
5.2 DMA配置实现零拷贝传输
DMA(直接内存访问)是终极的解放CPU方案。你只需要告诉DMA控制器源地址(内存)、目标地址(MCSPI_TXx寄存器)和数据量,它就会在后台自动搬运数据,完全不需要CPU参与。
配置SPI DMA传输的一般流程:
- SPI端配置:
- 在MCSPI_CHxCONF中使能DMA请求:设置DMAW=1(启用发送DMA请求)和/或DMAR=1(启用接收DMA请求)。
- 如果需要,在MCSPI_XFERLEVEL中设置DMA请求的水位线。
- 设置MCSPI_MODULCTRL[FDAA]=1如果DMA控制器要求256位对齐的地址(使用MCSPI_DAFTX/DAFRX寄存器)。
- DMA控制器配置(以MCU的DMA模块为例):
- 配置DMA通道的源地址(对于发送,是内存数组地址;对于接收,是MCSPI_RXx寄存器地址)。
- 配置DMA通道的目标地址(对于发送,是MCSPI_TXx寄存器地址;对于接收,是内存数组地址)。
- 配置传输数据量(元素个数和元素大小,需与SPI字长匹配)。
- 配置触发源:选择SPI的TX DMA请求或RX DMA请求作为触发信号。
- 配置传输模式:通常为外设到内存(接收)或内存到外设(发送)。
- 启用DMA通道。
- 启动传输:启动SPI通道(设置MCSPI_CHxCTRL[EN]=1),并写入第一个数据(对于发送DMA,有时需要手动写入第一个数据来启动时钟,后续由DMA自动填充;具体行为需查手册)。DMA会在FIFO达到水位线时自动请求搬运数据。
- 完成处理:可以配置DMA传输完成中断,在中断中处理接收到的数据或准备下一次发送。
重要心得:在调试DMA+SPI时,务必先用逻辑分析仪或示波器确认SPI时钟和数据波形是否正确。DMA配置错误可能导致数据错位、丢失或根本无波形。一个常见的错误是DMA传输的数据宽度(如字节)与SPI字长(如16位)不匹配,导致数据被拆散或合并。
6. 调试技巧与常见问题排查
即使理解了所有寄存器,实际调试中依然会遇到各种问题。下面是我总结的一些常见“坑点”和排查方法。
问题1:通信完全无反应,SCLK没有波形。
- 检查清单:
- 电源和时钟:确认SPI外设和MCU的SPI模块供电正常,且SPI模块的时钟源已使能。
- 引脚复用:确认MCU的SPI引脚(SCLK, MOSI, MISO, CS)已正确配置为SPI功能,而不是普通的GPIO。
- 片选信号:确认你操作的片选线(CS)是否正确拉低(或拉高,取决于MCSPI_CHxCONF[EPOL]的配置)。可以用万用表或示波器测量CS引脚电平。
- 通道使能:确认在传输前已设置MCSPI_CHxCTRL[EN] = 1。
- 主从模式:确认MCSPI_MODULCTRL[MS]设置正确,主设备应为0。
问题2:能抓到波形,但数据错误。
- 检查清单:
- 时钟模式(CPOL/CPHA):这是最常见的原因。用逻辑分析仪仔细比对SCLK和数据线(MOSI/MISO)的时序图,确认采样边沿和数据稳定区域是否符合外设要求。务必与数据手册的时序图逐位对比。
- 字长(WL):确认SPI字长设置与外设一致。8位设备设成了16位,会导致数据错位。
- 字节序(Endianness):对于大于8位的数据,MCU和SPI外设的字节序(大端/小端)可能不同。如果你发送
0x1234,对方可能理解为0x3412。需要根据数据手册调整发送数据的字节顺序。 - 时钟频率过高:降低MCSPI_CHxCONF[CLKD]的分频系数,尝试用更低的频率通信。信号完整性问题在高速下尤为突出。
- FIFO/DMA配置错误:如果使用了FIFO或DMA,检查水位线设置、DMA传输宽度和方向是否正确。尝试回归最基本的轮询单字传输进行测试。
问题3:中断无法触发,或进入中断后无法退出。
- 检查清单:
- 全局中断使能:确认CPU的全局中断已开启(如ARM Cortex-M的
__enable_irq())。 - NVIC配置:在中断控制器(如NVIC)中正确使能了SPI的中断向量,并设置了优先级。
- SPI中断使能:确认MCSPI_IRQENABLE中对应的位(如TX0_EMPTY_ENABLE)已被置1。
- 中断标志清除:这是最关键的步骤!必须在中断服务程序(ISR)中读取MCSPI_IRQSTATUS的值,并向产生中断的对应位写1来清除标志。只读不写,标志位会一直保持,导致中断持续触发,表现为程序卡死在ISR中。清除操作通常是
MCSPI_IRQSTATUS = (1 << bit_position);。 - 中断共享:如果多个中断源(如TX空和RX满)共用一个中断向量,需要在ISR中读取MCSPI_IRQSTATUS来判断是哪个事件触发的,并分别处理。
- 全局中断使能:确认CPU的全局中断已开启(如ARM Cortex-M的
问题4:使用DMA时数据丢失或错乱。
- 检查清单:
- 内存对齐:确保DMA源/目标缓冲区地址满足DMA控制器的对齐要求(通常是4字节或8字节对齐)。使用编译器指令(如
__attribute__((aligned(4))))来确保。 - 数据宽度与突发传输:确认DMA配置的数据项大小(byte, half-word, word)与SPI字长匹配。检查DMA的突发(Burst)大小设置是否合理。
- 缓冲区大小:确保DMA配置的传输数量与SPI实际要发送/接收的数据量一致。防止缓冲区溢出或不足。
- 缓存一致性:如果CPU有数据缓存(D-Cache),而DMA直接访问物理内存,可能会存在缓存一致性问题。对于DMA发送,在启动DMA前,需要确保待发送数据已从缓存写回内存(Clean操作)。对于DMA接收,在CPU读取DMA填充的内存数据前,可能需要无效化(Invalidate)对应缓存行。具体操作依赖芯片的缓存维护指令。
- 内存对齐:确保DMA源/目标缓冲区地址满足DMA控制器的对齐要求(通常是4字节或8字节对齐)。使用编译器指令(如
调试SPI,逻辑分析仪是你的最佳伙伴。它能直观地展示时钟、数据、片选线的每一个跳变,让你可以精确测量时序参数,并与数据手册对比。学会使用逻辑分析仪的协议解码功能(SPI解码),能直接将二进制波形翻译成十六进制数据,极大提升调试效率。
最后,寄存器配置虽然繁琐,但它是与硬件直接对话的方式。理解并熟练配置这些寄存器,意味着你真正掌握了SPI控制器,而不仅仅是调用某个驱动库的API。这份掌控力,在解决复杂问题、优化性能和进行底层调试时,是无价的。