1. SPI通信协议深度解析:从基础原理到实战配置
在嵌入式系统开发中,设备间的通信是构建复杂功能的基础。面对I2C、UART、SPI等众多协议,SPI以其简单、高速和全双工的特性,成为连接闪存、传感器、显示屏等外设的首选方案。今天,我们不谈枯燥的理论,直接从一位嵌入式老兵的视角,拆解SPI的运作内核,并以经典的MMC2107微控制器为例,手把手带你完成从寄存器配置到实战调试的全过程。无论你是刚接触SPI的新手,还是想深入理解其硬件实现的老鸟,这篇文章都将为你提供可直接“抄作业”的配置方法和避坑指南。
SPI,全称Serial Peripheral Interface,即串行外设接口。它的核心设计哲学是“极简主义”——没有复杂的地址寻址,没有起始/停止位,仅依靠四根线(有时甚至三根)就能实现主从设备间的高速数据交换。这种设计带来的直接好处是硬件开销小、通信速率高(轻松达到数十MHz),非常适合对实时性要求高的场景。但“简单”的另一面是灵活性带来的配置复杂性,时钟极性、相位、主从模式等选项,稍有不慎就会导致通信失败。接下来,我们将深入其原理,并聚焦于MMC2107这款MCU的SPI模块,看看如何通过操控寄存器,让这套精密的通信机制为你所用。
2. SPI核心原理与工作模式拆解
2.1 四线制与全双工通信的本质
SPI通信围绕四根信号线展开,理解每根线的角色是第一步:
- SCK (Serial Clock):时钟信号线,由主设备产生,是所有数据收发的节拍器。它的频率和波形直接决定了通信速率和数据采样的时刻。
- MOSI (Master Out Slave In):主设备输出、从设备输入的数据线。主设备通过这根线向从设备发送指令或数据。
- MISO (Master In Slave Out):主设备输入、从设备输出的数据线。从设备通过这根线向主设备返回数据或状态。
- SS/CS (Slave Select / Chip Select):从设备选择线,低电平有效。这是SPI支持多从设备的关键。主设备通过拉低对应从设备的SS线,通知它“准备好,我要和你通话了”。同一时刻,只能有一个从设备的SS线被拉低。
全双工通信是SPI的一大优势。在SCK的每个时钟周期内,主设备通过MOSI送出一位数据,同时通过MISO读入一位数据。你可以把它想象成两个人面对面同时说话和倾听,数据在两条线上并行流动,效率远高于半双工或单工方式。MMC2107的SPI模块内部有一个8位移位寄存器,主从设备的移位寄存器通过MOSI和MISO线首尾相连,在SCK驱动下形成一个虚拟的16位环形移位寄存器,数据就在这个环中循环移动,完成交换。
2.2 时钟极性(CPOL)与时钟相位(CPHA):通信的“握手暗号”
这是SPI配置中最容易出错的地方。CPOL和CPHA共同定义了数据在时钟的哪个边沿被采样(捕获)和哪个边沿被更新(输出)。
- CPOL (Clock Polarity):时钟极性。它决定了SCK线在空闲状态(无数据传输时)的电平。
- CPOL = 0:SCK空闲时为低电平。
- CPOL = 1:SCK空闲时为高电平。
- CPHA (Clock Phase):时钟相位。它决定了数据是在时钟的第一个边沿还是第二个边沿被采样。
- CPHA = 0:数据在时钟的第一个边沿(对于CPOL=0是上升沿,对于CPOL=1是下降沿)被采样,在下一个边沿更新。
- CPHA = 1:数据在时钟的第二个边沿被采样,在第一个边沿更新。
这两者组合出四种模式,通常被称为Mode 0, 1, 2, 3:
- Mode 0: CPOL=0, CPHA=0。SCK空闲低,数据在SCK上升沿采样,下降沿更新。
- Mode 1: CPOL=0, CPHA=1。SCK空闲低,数据在SCK下降沿采样,上升沿更新。
- Mode 2: CPOL=1, CPHA=0。SCK空闲高,数据在SCK下降沿采样,上升沿更新。
- Mode 3: CPOL=1, CPHA=1。SCK空闲高,数据在SCK上升沿采样,下降沿更新。
> 关键经验:主从设备的CPOL和CPHA设置必须完全一致!这是通信建立的前提。通常需要查阅从设备(如传感器、Flash芯片)的数据手册来确定其支持的SPI模式。MMC2107的SPICR1寄存器中的CPOL和CPHA位就是用来配置这两个参数的。
2.3 主从模式与多设备连接策略
SPI采用主从架构。主设备(Master)掌控SCK时钟的产生和通信的发起,从设备(Slave)被动响应。
- 主模式 (Master Mode):在MMC2107中,通过设置SPICR1寄存器的MSTR位为1来启用。主设备负责生成SCK,控制MOSI输出,并监听MISO输入。只有主设备能主动发起一次传输(通过写入数据寄存器)。
- 从模式 (Slave Mode):通过清除MSTR位(设为0)来启用。从设备的SCK和MOSI为输入,MISO为输出(仅在SS为低时有效)。从设备完全由主设备的时钟和片选信号驱动。
连接多个从设备时,通常有两种拓扑结构:
- 独立片选(最常用):主设备为每个从设备提供独立的SS线。通信时,只拉低目标从设备的SS线,其他保持高电平。这种方式软件控制简单,互不干扰,但需要占用更多主设备GPIO。
- 菊花链(Daisy-Chain):所有从设备的MOSI和MISO依次串联,主设备只连接第一个和最后一个。数据像接力棒一样在链中传递。这种方式节省GPIO,但所有从设备会同时收到数据,软件协议需要设计地址来区分,且链中任一设备故障可能影响后续通信。MMC2107的SPI模块本身支持标准的独立片选模式。
3. MMC2107 SPI模块寄存器配置详解
理解了原理,我们进入实战环节——配置MMC2107的SPI寄存器。手册内容虽然详尽,但直接看容易懵。我将结合常见使用场景,为你解读关键寄存器每个位的实际意义和配置方法。
3.1 控制寄存器1 (SPICR1) – 设定通信基础
地址0x00CB_0000。这是SPI的核心控制寄存器,决定了SPI的基本工作模式。
| 位 | 名称 | 功能描述 | 常用配置与解析 |
|---|---|---|---|
| 7 | SPIE | SPI中断使能。1=使能SPIF和MODF标志中断。 | 中断 or 轮询?对于低速或非实时任务,可以禁用中断(SPIE=0),采用轮询SPIF标志的方式。对于高速或需要及时响应的场景,建议使能中断(SPIE=1),避免主程序阻塞等待。 |
| 6 | SPE | SPI系统使能。1=启用SPI,相关引脚(SS, SCK, MOSI, MISO)专用于SPI功能。 | 上电第一步:在配置完其他参数后,最后再将此位置1以启用SPI模块。顺序错误可能导致意外输出。 |
| 5 | SWOM | 线或模式。1=SPI端口引脚[3:0]输出为开漏模式。 | 主要用于多主设备总线竞争场景。开漏输出需要外部上拉电阻,可以实现“线与”逻辑,避免多个设备同时驱动总线造成冲突。在单一主设备系统中,通常设为0(CMOS推挽输出),驱动能力强。 |
| 4 | MSTR | 主从模式选择。1=主模式,0=从模式。 | 根据你的硬件设计确定。MCU作为控制器时通常设为主模式(MSTR=1)。 |
| 3 | CPOL | 时钟极性。1=SCK空闲高,0=SCK空闲低。 | 必须与从设备匹配!参考从设备手册确定。 |
| 2 | CPHA | 时钟相位。1=数据在第一个SCK边沿捕获,0=数据在第二个SCK边沿捕获。 | 必须与从设备匹配!与CPOL共同决定SPI模式。 |
| 1 | SSOE | 从选择输出使能。 | 此位与数据方向寄存器SPIDDR的DDRSP3位共同决定主模式下SS引脚的功能,详见下文表格。 |
| 0 | LSBFE | 低位优先使能。1=数据先传输最低位(LSB),0=先传输最高位(MSB)。 | 必须与从设备匹配!大多数器件采用MSB first(LSBFE=0),但有些(如某些音频芯片)可能采用LSB first,务必查证。 |
关于SS引脚配置(主模式下): SSOE位和DDRSP3位的组合决定了主设备SS引脚的角色,这是一个容易混淆的点。
| DDRSP3 | SSOE | 主模式下SS引脚功能 | 应用场景 |
|---|---|---|---|
| 0 | 0 | 模式故障输入 | 多主系统。用于检测总线冲突。当另一个主设备试图驱动总线时,会拉低此引脚,触发MODF错误。 |
| 0 | 1 | 通用输入 | 将SS引脚用作普通GPIO输入,SPI功能不使用此引脚。此时需用另一个GPIO手动控制从设备的片选。 |
| 1 | 0 | 通用输出 | 将SS引脚用作普通GPIO输出,可用于手动控制从设备片选,但无法自动控制。 |
| 1 | 1 | 从选择输出 | 最常用。SPI模块自动管理SS输出。每次数据传输开始时,SS自动拉低;传输结束后,SS自动拉高。适用于单一从设备或需要自动片选控制的场景。 |
实操心得:在单一主控、单一从设备的典型应用中,最省心的配置是
MSTR=1,SSOE=1,DDRSP3=1,让硬件自动管理SS信号。如果你需要驱动多个从设备,则通常将SSOE设为0,DDRSP3设为1,把SS引脚当作普通GPIO输出,然后用自己的代码分别控制多个GPIO来充当不同从设备的片选信号。
3.2 控制寄存器2 (SPICR2) 与波特率寄存器 (SPIBR)
SPICR2 (地址0x00CB_0001)主要控制一些高级功能:
- SPISDOZ位:决定CPU进入Doze(打盹)低功耗模式时,SPI时钟是否停止。如果SPI需要在低功耗模式下维持通信(如定时读取传感器),则需清除此位(设为0)。
- SPC0位:用于启用双向引脚模式。在**正常模式(SPC0=0)下,引脚功能为标准定义(MISO, MOSI)。在双向模式(SPC0=1)**下,引脚功能可能发生变化(例如MOSI变为MOMI),这用于特殊的单线数据总线应用。除非数据手册明确要求,否则保持SPC0=0(正常模式)。
SPIBR (地址0x00CB_0002)用于设置SPI通信的波特率(即SCK频率)。波特率由模块输入时钟(例如33MHz)经过一个分频器得到。分频系数由SPPR[6:4](预分频)和SPR[2:0](分频)共同决定,计算公式大致为:SCK频率 = 模块时钟频率 / (预分频系数 * 分频系数)。
手册中的表格列出了所有组合下的分频系数和最终波特率。例如,模块时钟33MHz,若SPPR[6:4] = 000,SPR[2:0] = 001,则分频系数为4,SCK频率为 33MHz / 4 = 8.25 MHz。
配置技巧:选择波特率时,需考虑两个因素:1)从设备支持的最高SCK频率,不能超过此限制;2)信号完整性。过高的速率在长导线或面包板上可能导致通信错误。建议从较低速率(如1MHz以下)开始测试,稳定后再逐步提高。切记:不要在SPI传输过程中修改SPIBR寄存器!这可能导致SCK出现毛刺或频率突变,引发通信错误。
3.3 状态寄存器 (SPISR) 与数据寄存器 (SPIDR)
SPISR (地址0x00CB_0003)是了解SPI模块工作状态和错误的窗口。
- SPIF (位7):SPI中断标志。这是最常用的标志。当一次8位数据传输完成,接收到的数据从移位寄存器转移到SPIDR时,此位被硬件置1。如果SPIE位使能了中断,则会触发中断。清除SPIF标志的标准操作是:先读取SPISR(此时SPIF=1),然后再访问SPIDR(读或写)。
- WCOL (位6):写冲突标志。当SPI传输正在进行时(数据正在移位),如果软件试图向SPIDR写入新数据,此位会被置1,且这次写入操作无效。这通常是由于程序没有检查SPIF标志或FIFO状态就盲目写数据造成的。清除方法同SPIF。
- MODF (位4):模式故障标志。仅在主模式下有意义。当SS引脚被配置为模式故障输入(DDRSP3=0, SSOE=0)且被外部拉低时,此位置1。这表明可能有另一个主设备正在试图控制总线,发生了冲突。发生模式故障时,硬件会自动清除SPE和MSTR位,并关闭SPI输出驱动器,防止总线冲突加剧。清除方法是先读SPISR,再写SPICR1。
SPIDR (地址0x00CB_0005)是一个特殊的寄存器,它既是发送数据缓冲区,也是接收数据缓冲区。
- 写入SPIDR:在主机模式下,向此寄存器写入一个字节,如果移位寄存器空闲,则会启动一次SPI传输。务必在写入前确保SPIF标志为1(表示上次传输完成)或WCOL标志为0。
- 读取SPIDR:读取此寄存器得到的是上次传输完成后从从设备接收到的数据。必须在SPIF标志置1后、下一次传输结束前读取,否则数据可能被覆盖。
核心操作流程(轮询方式):
- 检查SPIF标志是否为1(或等待其置1),表示传输就绪。
- 读取SPIDR,获取上次传输收到的数据(如果需要)。
- 向SPIDR写入要发送的新数据,启动下一次传输。
- 重复步骤1-3。
4. MMC2107 SPI模块完整配置与驱动实现
理论铺垫足够,现在我们来动手实现一个具体的SPI驱动实例。假设我们要用MMC2107作为主设备,以Mode 0 (CPOL=0, CPHA=0)、波特率1MHz,驱动一个SPI Flash芯片(如W25Q128)。
4.1 硬件连接与初始化代码
首先,确保硬件连接正确:
- MMC2107 MOSI -> Flash MOSI (DI)
- MMC2107 MISO -> Flash MISO (DO)
- MMC2107 SCK -> Flash SCK (CLK)
- MMC2107 GPIOx (如PTA0) -> Flash CS# (片选,因为我们可能控制多个设备,不直接用SSOE自动输出)
以下是初始化代码,包含详细的注释:
/** * @brief 初始化MMC2107的SPI1模块为主机,Mode 0, 1MHz * @note 假设使用SPI1模块,SS引脚用普通GPIO(PTA0)手动控制 */ void SPI1_Master_Init(void) { // 1. 首先配置相关引脚的复用功能(假设SPI1引脚在Port C的0-3位) // 具体复用功能配置需参考MMC2107的Pin Assignment章节,此处为示例 // PORTC_PCR0 = PORT_PCR_MUX(2); // PC0 复用为 SPI1_SCK // PORTC_PCR1 = PORT_PCR_MUX(2); // PC1 复用为 SPI1_MOSI // PORTC_PCR2 = PORT_PCR_MUX(2); // PC2 复用为 SPI1_MISO // PORTA_PCR0 = PORT_PCR_MUX(1); // PA0 复用为 GPIO,用作Flash片选 // 2. 配置片选引脚PA0为GPIO输出,并初始化为高电平(不选中) // GPIOA_PDDR |= (1 << 0); // 设置PA0为输出 // GPIOA_PSOR = (1 << 0); // 输出高电平 // 3. 配置SPI控制寄存器1 (SPICR1) // 地址: 0x00CB_0000 (假设SPI1基址为0x00CB_0000) volatile uint8_t *spicr1 = (volatile uint8_t *)0x00CB0000; // 先停止SPI,安全配置 *spicr1 = 0x00; // 配置: SPIE=0(轮询), SPE=0(先禁用), SWOM=0(CMOS), MSTR=1(主机), // CPOL=0(空闲低), CPHA=0(第一个边沿采样), SSOE=0(SS作GPIO), LSBFE=0(MSB先) *spicr1 = (0 << 7) | (0 << 6) | (0 << 5) | (1 << 4) | (0 << 3) | (0 << 2) | (0 << 1) | (0 << 0); // 等价于 *spicr1 = 0x10; (仅MSTR位为1) // 4. 配置SPI控制寄存器2 (SPICR2) volatile uint8_t *spicr2 = (volatile uint8_t *)0x00CB0001; *spicr2 = 0x00; // SPISDOZ=0(Doze模式SPI仍工作), SPC0=0(正常引脚模式) // 5. 配置SPI波特率寄存器 (SPIBR) // 目标: 1MHz。假设总线时钟为33MHz,分频系数需33。 // 查表17-5: SPPR[6:4]=001 (预分频系数2), SPR[2:0]=100 (分频系数16) // 总系数 = 2 * 16 = 32 -> 33MHz/32 ≈ 1.03MHz (接近1MHz) volatile uint8_t *spibr = (volatile uint8_t *)0x00CB0002; // SPPR[6:4] = 001, SPR[2:0] = 100 *spibr = (0 << 7) | (0 << 6) | (0 << 5) | (1 << 4) | (0 << 3) | (1 << 2) | (0 << 1) | (0 << 0); // 等价于 *spibr = 0x24; // 6. 配置SPI端口数据方向寄存器 (SPIDDR) // 使能MOSI, SCK, SS(作为GPIO)为输出,MISO为输入 volatile uint8_t *spiddr = (volatile uint8_t *)0x00CB0008; // DDRSP3(SS)=1(输出), DDRSP2(SCK)=1(输出), DDRSP1(MOSI)=1(输出), DDRSP0(MISO)=0(输入) *spiddr = (0 << 7) | (0 << 6) | (0 << 5) | (0 << 4) | (1 << 3) | (1 << 2) | (1 << 1) | (0 << 0); // 等价于 *spiddr = 0x0E; // 7. 最后,使能SPI模块 *spicr1 |= (1 << 6); // 设置SPE位为1 }4.2 基础数据传输函数实现
有了初始化,接下来实现最核心的字节发送/接收函数。我们采用轮询(Polling)方式,简单可靠。
/** * @brief 通过SPI1发送并接收一个字节(全双工) * @param txData: 要发送的字节 * @retval 接收到的字节 */ uint8_t SPI1_TransferByte(uint8_t txData) { volatile uint8_t *spisr = (volatile uint8_t *)0x00CB0003; volatile uint8_t *spidr = (volatile uint8_t *)0x00CB0005; // 等待上一次传输完成(SPIF标志置位) while( !(*spisr & 0x80) ) { // 可选:加入超时机制,防止死循环 } // 清除SPIF标志:先读状态寄存器,再访问数据寄存器 (void)*spisr; // 读状态寄存器,忽略返回值 // 写入数据,启动新的传输 *spidr = txData; // 再次等待本次传输完成 while( !(*spisr & 0x80) ) { // 等待 } // 清除SPIF标志并读取接收到的数据 (void)*spisr; // 读状态寄存器 return *spidr; // 读数据寄存器,返回接收到的数据 } /** * @brief 设置Flash片选引脚(PA0)电平 * @param state: 0=低电平(选中), 1=高电平(取消选中) */ void Flash_CS_Set(uint8_t state) { if(state) { // GPIOA_PSOR = (1 << 0); // 输出高 } else { // GPIOA_PCOR = (1 << 0); // 输出低 } } /** * @brief 向SPI Flash发送命令并读取数据(示例:读器件ID) * @note 以W25Q128的Read JEDEC ID命令(0x9F)为例 */ uint32_t SPI_Flash_ReadID(void) { uint32_t id = 0; uint8_t *id_ptr = (uint8_t*)&id; Flash_CS_Set(0); // 拉低片选,选中Flash SPI1_TransferByte(0x9F); // 发送读ID命令 id_ptr[0] = SPI1_TransferByte(0xFF); // 读制造商ID (Winbond: 0xEF) id_ptr[1] = SPI1_TransferByte(0xFF); // 读存储器类型 id_ptr[2] = SPI1_TransferByte(0xFF); // 读容量 Flash_CS_Set(1); // 拉高片选,释放Flash // 注意:MMC2107是小端序,但SPI传输是MSB先出。 // 我们按顺序接收了3个字节,存储在id的低24位。 // 为了便于阅读,可以调整字节顺序。 return ((uint32_t)id_ptr[0] << 16) | ((uint32_t)id_ptr[1] << 8) | id_ptr[2]; }4.3 中断驱动方式实现
对于需要高效处理或后台传输的场景,中断方式是更好的选择。以下是中断服务例程(ISR)的简化示例:
volatile uint8_t spi_tx_buffer[32]; volatile uint8_t spi_rx_buffer[32]; volatile uint8_t spi_tx_index = 0; volatile uint8_t spi_rx_index = 0; volatile uint8_t spi_transfer_len = 0; volatile bool spi_busy = false; /** * @brief SPI1中断服务程序 */ void SPI1_IRQHandler(void) { volatile uint8_t *spisr = (volatile uint8_t *)0x00CB0003; volatile uint8_t *spidr = (volatile uint8_t *)0x00CB0005; // 检查是否是SPIF中断(也可能有MODF,这里简化处理) if(*spisr & 0x80) { // SPIF标志位 // 清除标志 (void)*spisr; // 读取接收到的数据 spi_rx_buffer[spi_rx_index++] = *spidr; if(spi_tx_index < spi_transfer_len) { // 还有数据要发送,写入下一个字节 *spidr = spi_tx_buffer[spi_tx_index++]; } else { // 所有数据发送完毕 // 可以在这里设置完成标志,通知主程序 spi_busy = false; // 可选:禁用SPI中断或进入空闲状态 } } // 如果需要处理模式故障(MODF),可以在这里添加判断 } /** * @brief 启动一次中断驱动的SPI传输 * @param tx_data: 发送数据指针 * @param rx_data: 接收数据缓冲区指针 * @param len: 传输长度 */ void SPI1_Start_Transfer_IT(const uint8_t *tx_data, uint8_t *rx_data, uint8_t len) { if(spi_busy || len == 0) return; spi_busy = true; spi_transfer_len = len; spi_tx_index = 0; spi_rx_index = 0; // 复制发送数据到内部缓冲区(实际项目可能用DMA) for(uint8_t i=0; i<len; i++) { spi_tx_buffer[i] = tx_data[i]; } // 配置用户接收缓冲区指针(ISR完成后需要拷贝数据) // 此处简化,假设使用全局缓冲区 // 使能SPI中断 (设置SPICR1的SPIE位) volatile uint8_t *spicr1 = (volatile uint8_t *)0x00CB0000; *spicr1 |= (1 << 7); // 设置SPIE=1 // 手动启动第一次传输:写入第一个字节 volatile uint8_t *spidr = (volatile uint8_t *)0x00CB0005; *spidr = spi_tx_buffer[spi_tx_index++]; }中断使用要点:在中断服务程序中,操作要快,避免长时间占用。对于大数据量传输,考虑使用DMA配合SPI,可以极大减轻CPU负担。MMC2107的SPI模块是否支持DMA需查阅其具体型号的参考手册。
5. 常见问题排查与调试技巧实录
即使按照手册配置,SPI通信也常会遇到问题。下面是我在多年调试中总结的一些典型故障现象和排查思路,希望能帮你快速定位问题。
5.1 问题排查速查表
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 完全无通信,SCK无波形 | 1. SPI模块未使能(SPE=0)。 2. 主从模式设置错误(MSTR位)。 3. 引脚复用功能未正确配置。 4. 时钟或电源问题。 | 1. 检查SPICR1的SPE位是否为1。 2. 确认MSTR位设置正确(主设备为1)。 3. 使用逻辑分析仪或示波器检查SCK、MOSI引脚是否有输出。检查MCU的IOMUX配置,确保引脚已映射到SPI功能。 4. 检查MCU和从设备供电是否正常,时钟是否起振。 |
| SCK有波形,但MOSI无数据或数据错误 | 1. CPOL/CPHA模式不匹配。 2. 数据位顺序(LSBFE)不匹配。 3. 片选信号(SS)问题。 4. 从设备未准备好或损坏。 | 1.这是最高频问题!用示波器同时抓取SCK和MOSI信号,对照从设备手册的时序图,检查数据采样边沿是否正确。调整CPOL和CPHA。 2. 检查LSBFE位设置,与从设备要求保持一致。 3. 确认从设备的片选引脚是否被正确拉低。如果使用SSOE自动控制,检查SSOE和DDRSP3配置;如果使用GPIO手动控制,检查代码逻辑。 4. 尝试降低SCK频率。确认从设备电源、复位信号正常。 |
| 能发送,但接收数据全为0xFF或固定值 | 1. MISO线连接错误或断路。 2. 从设备输出使能问题(SS信号)。 3. 主设备MISO引脚配置为输出。 4. 从设备本身需要特定命令才会输出数据。 | 1. 检查硬件连接,确保MISO线连通。用示波器看MISO引脚在传输期间是否有波形变化。 2. 确认从设备的片选(CS)在整个传输期间保持低电平。有些设备在CS变高后会释放MISO总线。 3. 检查主设备SPIDDR寄存器,确保MISO对应的DDRSP0位为0(输入)。 4. 许多SPI设备(如Flash、传感器)需要先发送正确的读命令,才会在后续时钟周期输出数据。确认你发送的命令序列符合从设备协议。 |
| 通信不稳定,偶尔出错 | 1. SCK速率过高,信号质量差。 2. 总线负载过重,多个从设备并联。 3. 中断或更高优先级任务打断了SPI时序。 4. 电源噪声。 | 1. 降低SPIBR的波特率设置。检查PCB布线,SCK和MOSI/MISO线应尽量短,并行走线,避免过长或靠近干扰源。 2. 在总线上增加缓冲器(如74HC245),或检查从设备MISO是否为高阻态时漏电。 3. 在关键的SPI传输序列(如Flash写使能、页编程)中,临时关闭全局中断。 4. 在电源引脚增加去耦电容(如100nF陶瓷电容紧贴芯片)。 |
| 模式故障(MODF标志置位) | 1. 多主系统中,另一个主设备拉低了SS线。 2. SS引脚配置为模式故障输入,但被意外拉低(如干扰)。 3. 硬件上SS引脚对地短路。 | 1. 检查多主总线仲裁逻辑。 2. 如果不使用多主模式,建议将SS引脚配置为通用输出(DDRSP3=1, SSOE=0)或从选择输出(DDRSP3=1, SSOE=1),避免误触发。 3. 检查硬件电路。清除MODF标志的方法是先读SPISR,再写SPICR1。 |
5.2 调试工具与技巧
- 逻辑分析仪是你的最佳伙伴:一个支持SPI协议解码的逻辑分析仪(如Saleae)能直观显示SCK、MOSI、MISO、CS上的波形和数据,一眼就能看出时钟极性、相位、数据位是否正确,是调试SPI的终极利器。
- 示波器看细节:当通信不稳定时,用示波器观察信号边沿是否陡峭,是否有过冲、振铃或毛刺。过长的导线或过高的速率都会导致信号畸变。
- 软件模拟SPI:当硬件SPI调不通时,可以先用GPIO模拟SPI时序(Bit-banging)来验证从设备是否正常、命令序列是否正确。这能排除硬件SPI配置复杂性的干扰。
- 分步测试法:
- 第一步:只初始化GPIO,手动翻转SCK和MOSI引脚,用逻辑分析仪看是否有预期波形,验证硬件连接和引脚控制。
- 第二步:配置SPI,但先不接从设备,用逻辑分析仪观察主设备自发自收(Loopback)是否正常。MMC2107支持环回模式(LOOPS位),可用于自测试。
- 第三步:连接从设备,先进行最简单的操作,如读器件ID(JEDEC ID),这通常不需要复杂的初始化。
- 注意电源和地:确保主从设备共地。数字电路的噪声会通过电源传导,良好的电源去耦(每个芯片的VCC附近加0.1uF电容)至关重要。
5.3 MMC2107 SPI环回模式(Loopback)配置
环回模式是调试驱动程序的强大工具,它让SPI自己发送给自己,无需外部硬件即可验证SPI核心功能是否正常。MMC2107的SCI模块有明确的环回和单线操作模式,但SPI模块本身的标准文档未强调此模式。不过,我们可以通过短接MOSI和MISO引脚在物理上实现环回。
软件环回思路:将MOSI和MISO引脚在PCB或面包板上用跳线短接。这样,主设备发送的数据会通过导线立刻被自己接收回来。
- 硬件上短接MCU的MOSI和MISO引脚。
- 正常配置SPI为主模式。
- 发送一个已知数据(如0xAA)。
- 检查接收到的数据是否与发送的一致。
如果环回测试成功,说明MCU的SPI模块配置、引脚驱动和基本时序是正确的,问题可能出在从设备端或主从匹配上。如果失败,则需集中排查MCU自身的配置和代码。