
1. 项目概述如果你在嵌入式开发中用过I2C可能会觉得它虽然省线但速度上总有点“温吞水”尤其是在驱动TFT屏幕或者高速ADC时那种等待数据的感觉并不好受。这时SPISerial Peripheral Interface就该登场了。它是一种简单、高速、全双工的同步串行通信协议没有复杂的地址机制全靠主设备“点名”片选信号和“打拍子”时钟信号来指挥数据流动。我最近在调试一个基于Freescale现NXPMC1323x系列无线MCU的项目其中用SPI连接了一个高精度的温湿度传感器和一个Flash存储器。在啃官方手册和实际调试的过程中我发现很多教程只讲“怎么配”很少深挖“为什么这么配”尤其是面对CPOL、CPHA、双缓冲这些概念时一知半解很容易在时序上栽跟头。这篇文章我就结合MC1323x的SPI模块从最基础的通信原理讲起一直深入到每个寄存器的比特位该如何设置并分享一些从示波器波形里“抠”出来的调试经验。无论你是刚开始接触SPI还是想深入理解其内部机制以便更好地驾驭它希望这篇近万字的解析能成为你手边实用的参考。2. SPI通信协议核心原理深度拆解SPI协议的精髓在于其极简的硬件设计和极高的灵活性。它不像UART需要事先约定波特率也不像I2C需要应答机制其通信的节奏完全由主设备产生的时钟SCLK控制。2.1 四线制与主从架构一个最基本的SPI总线包含四根信号线SCLK (Serial Clock) 时钟信号由主设备产生并输出给所有从设备。它是数据收发的节拍器。MOSI (Master Out Slave In) 主设备数据输出、从设备数据输入线。MISO (Master In Slave Out) 主设备数据输入、从设备数据输出线。SS/CS (Slave Select / Chip Select) 片选信号低电平有效。主设备通过拉低对应从设备的SS线来“选中”它通知其准备通信。这里有一个关键点MOSI和MISO是从主设备视角命名的。对于从设备而言MOSI是它的输入MISO是它的输出。在一个一主多从的系统中SCLK、MOSI、MISO通常是所有设备共享的总线而每个从设备都有自己独立的SS线。主设备通过控制不同的SS线来选择与哪个从设备对话从而避免了总线冲突。注意 有些器件为了节省引脚会支持三线制半双工模式即共用一根数据线进行双向传输。MC1323x也通过SPC0位支持这种“单线双向”模式但这通常以牺牲通信简便性为代价需要更复杂的软件控制。2.2 时钟极性(CPOL)与时钟相位(CPHA)时序的灵魂这是SPI最难理解也最容易出错的地方。不同的外设芯片对时钟和数据沿的关系要求可能不同SPI协议通过CPOL和CPHA这两个参数的组合提供了四种时钟模式Mode 0, 1, 2, 3来适配它们。时钟极性 CPOL (Clock Polarity) 它决定了SCLK线在空闲状态即两次传输之间SS为高时的电平。CPOL 0 SCLK空闲时为低电平。CPOL 1 SCLK空闲时为高电平。 你可以把它想象成时钟信号的“初始状态”。它并不直接影响数据采样点但决定了第一个时钟边沿是上升沿还是下降沿。时钟相位 CPHA (Clock Phase) 它决定了数据是在时钟的哪个边沿被采样捕获以及在哪个边沿被改变输出。CPHA 0 数据在第一个时钟边沿即SCLK的第一个跳变沿被采样在第二个边沿改变。CPHA 1 数据在第二个时钟边沿被采样在第一个边沿改变。这里的“第一个”和“第二个”边沿是相对于一个时钟周期而言的。结合CPOL就产生了四种模式模式CPOLCPHASCLK空闲电平数据采样边沿数据改变边沿常见应用Mode 000低电平第一个上升沿下降沿很多传感器如BMP280、Flash如W25QxxMode 101低电平下降沿第一个上升沿Mode 210高电平第一个下降沿上升沿Mode 311高电平上升沿第一个下降沿某些RFID芯片、SD卡SPI模式如何记忆和选择我的经验是不必死记硬背。拿到一个外设的数据手册直接去时序图里找两个关键信息1. SCLK空闲时的电平对应CPOL。2. 数据线MOSI/MISO上的数据是在SCLK的哪个边沿稳定的即采样边沿对应CPHA。绝大多数情况下芯片手册会明确写明“SPI Mode 0”或“CPOL0, CPHA0”。主设备和从设备的CPOL、CPHA设置必须完全一致否则数据会错位这是调试SPI通信的首要检查点。2.3 数据传输机制移位与双缓冲SPI的数据传输是同步且全双工的。主从设备内部各有一个移位寄存器。当主设备启动传输向数据寄存器写入数据后在SCLK的驱动下主设备的移位寄存器数据通过MOSI线一位一位地移出同时从设备的数据也通过MISO线一位一位地移入主设备的移位寄存器。8个时钟周期后两个设备完成了一个字节的交换。为了提高效率防止数据覆盖SPI模块通常设计了双缓冲机制。以MC1323x为例发送缓冲器 (Transmit Data Buffer) 你写入SPI1D寄存器的数据首先放在这里。发送移位寄存器 (Transmit Shift Register) 当移位寄存器空闲时发送缓冲器中的数据会自动加载到这里并开始串行移出。接收移位寄存器 (Receive Shift Register) 从MISO线串行移入的数据暂存于此。接收缓冲器 (Receive Data Buffer) 一个字节接收完成后数据从接收移位寄存器自动转移到此处供CPU读取。关键标志位SPTEF (SPI Transmit Buffer Empty Flag) 发送缓冲器空标志。为1时表示你可以安全地写入下一个要发送的字节到SPI1D而不会覆盖尚未送出的数据。SPRF (SPI Read Buffer Full Flag) 接收缓冲器满标志。为1时表示有一个接收到的字节已经躺在SPI1D寄存器里等你读取。这个双缓冲结构允许你在当前字节正在传输的同时准备下一个要发送的字节查询SPTEF并在上一个字节接收完成后、下一个字节传输结束前读取数据查询SPRF从而实现接近理论带宽的连续流式传输。3. MC1323x SPI模块寄存器详解与配置实战理解了原理我们来看如何在MC1323x上具体操作。MC1323x的SPI模块在手册中记为SPI1提供了5个8位寄存器进行控制。3.1 核心控制寄存器SPI1C1 与 SPI1C2SPI1C1是主控制寄存器大部分关键配置都在这里。位名称功能描述复位值配置要点7SPIESPI接收/模式故障中断使能01使能当SPRF或MODF置1时产生中断。0禁用需软件查询。6SPESPI系统使能0总开关。1开启SPI模块。任何配置改动前应先将其清零。5SPTIESPI发送中断使能01使能当SPTEF置1时产生中断。适用于DMA或高效连续发送。4MSTR主/从模式选择0核心配置。1主模式0从模式。3CPOL时钟极性00SCLK空闲低1SCLK空闲高。2CPHA时钟相位10数据在第一个边沿样1数据在第二个边沿采样。复位后默认为1(Mode 1)需根据外设调整。1SSOE从选择输出使能0与MODFEN配合决定主模式下SS引脚功能。0LSBFE低位先发送00先发送最高位(MSB)1先发送最低位(LSB)。绝大多数器件使用MSB First。SPI1C2用于控制一些高级功能。位名称功能描述复位值配置要点4MODFEN模式故障功能使能0主模式下与SSOE配合定义SS引脚功能见下表。3BIDIROE双向模式输出使能0仅在SPC01单线双向模式时有效。1使能输出驱动。1SPISWAI等待模式下SPI停止0低功耗相关。1MCU进入Wait模式时SPI时钟停止。0SPC0SPI引脚控制000标准四线模式1单线双向模式使用MOSI或MISO单线通信。MODFEN和SSOE的配合主模式下这是配置SS引脚行为的关键很多初学者会困惑。MODFENSSOE主模式下SS引脚功能00通用I/O口。SPI不控制该引脚你需要用软件控制一个GPIO来作为手动片选。01通用I/O口。同上。10模式故障输入。此时SS引脚作为输入用于检测多主冲突见后文故障排查。11自动SS输出。SPI硬件自动管理SS引脚在数据传输开始时拉低结束后拉高。最常用。实操心得 对于大多数单一主设备的应用我推荐设置MODFEN1, SSOE1启用自动SS输出。这样硬件会自动管理片选时序非常可靠。如果你需要手动控制片选例如在一次片选期间进行多次数据传输则设置MODFEN0, SSOE0并将SS对应的GPIO配置为输出由软件控制。3.2 波特率生成与SPI1BR寄存器SPI的通信速率由主设备的SCLK决定。MC1323x的SPI波特率由总线时钟BUSCLK经过两级分频得到计算公式为SPI Bit Rate BUSCLK / (Prescaler * Divisor)SPI1BR寄存器控制这两级分频器SPPR[2:0] (Bits 6:4) 预分频器Prescaler分频系数可选1, 2, 3, ..., 8。SPR[2:0] (Bits 2:0) 波特率分频器Divisor分频系数可选2, 4, 8, ..., 256。例如假设BUSCLK 4 MHz设置SPPR2分频比3SPR4分频比32则SPI波特率 4,000,000 / (3 * 32) ≈ 41.67 kHz。配置建议 初始调试时建议设置一个较低的波特率如100-500 kHz确保通信稳定。待逻辑正确后再逐步提高速率并观察波形质量。过高的速率可能导致信号边沿变差通信失败。3.3 状态与数据寄存器SPI1S 与 SPI1DSPI1S是状态寄存器只读用于查询SPI模块的当前状态。位名称功能描述7SPRF接收缓冲器满标志。1表示SPI1D中有新数据可读。读取SPI1S该位为1后再读SPI1D可清除此标志。5SPTEF发送缓冲器空标志。1表示可以向SPI1D写入新数据以启动/继续传输。读取SPI1S该位为1后再写SPI1D可清除此标志。4MODF模式故障标志。主模式下当MODFEN1且SSOE0时如果SS引脚被拉低可能有多主冲突此位置1。SPI1D是数据寄存器读写同一地址但物理上是两个不同的缓冲器。写入SPI1D 数据进入发送缓冲器。在主模式下如果移位寄存器空闲此操作会启动一次传输。读取SPI1D 返回的是接收缓冲器中的数据。这里有一个极其重要的顺序要求也是新手最容易踩的坑启动发送前必须先检查SPTEF是否为1发送缓冲空。通常做法是while(!(SPI1S 0x20));。读取接收数据前必须先检查SPRF是否为1接收缓冲满。通常做法是while(!(SPI1S 0x80));。清除SPTEF标志的正确顺序是先读SPI1S再写SPI1D。清除SPRF标志的正确顺序是先读SPI1S再读SPI1D。不遵循这个顺序可能会导致标志位无法正确清除或写入/读取操作被忽略。4. MC1323x SPI驱动实现与数据收发流程下面我将以一个具体的例子来展示如何初始化SPI主设备并实现一个字节的发送和接收函数。假设我们需要以Mode 0 (CPOL0, CPHA0)与一个外设通信波特率约1MHz使用自动SS输出。4.1 SPI主模式初始化代码解析/** * brief 初始化SPI1为主机模式 * param None * retval None */ void SPI1_Master_Init(void) { // 1. 首先禁用SPI模块以便安全配置寄存器 SPI1C1 ~SPI_C1_SPE_MASK; // 2. 配置SPI控制寄存器1 (SPI1C1) // SPI_C1_SPE_MASK (0x40): 位6SPI使能位先保持为0 // SPI_C1_SPTIE_MASK (0x20): 位5发送中断使能我们先用查询方式故禁用(0) // SPI_C1_MSTR_MASK (0x10): 位4主模式选择置1 // SPI_C1_CPOL_MASK (0x08): 位3CPOL0 (空闲低电平) // SPI_C1_CPHA_MASK (0x04): 位2CPHA0 (第一个边沿采样) // SPI_C1_SSOE_MASK (0x02): 位1SS输出使能置1配合MODFEN实现自动SS // SPI_C1_LSBFE_MASK (0x01): 位0MSB先发送置0 SPI1C1 SPI_C1_MSTR_MASK | SPI_C1_SSOE_MASK; // CPOL和CPHA复位值已是0这里显式写出是为了代码清晰实际可省略。 // 3. 配置SPI控制寄存器2 (SPI1C2) // SPI_C2_MODFEN_MASK (0x10): 位4模式故障使能置1与SSOE1共同启用自动SS输出 // 其他位保持0标准双线模式等待模式下时钟继续运行 SPI1C2 SPI_C2_MODFEN_MASK; // 4. 配置SPI波特率寄存器 (SPI1BR) // 假设总线时钟BUSCLK 8MHz目标波特率 1MHz // 计算分频系数 8MHz / 1MHz 8 // 我们可以选择 Prescaler2 (分频比4), Divisor2 (分频比2) - 总系数4*28 // SPI_BR_SPPR_MASK(0x70): SPPR[2:0]位于位6:4设置SPPR2 (010) // SPI_BR_SPR_MASK (0x07): SPR[2:0]位于位2:0设置SPR2 (010) SPI1BR (2 4) | (2 0); // 即 0x22 // 5. 最后使能SPI模块 SPI1C1 | SPI_C1_SPE_MASK; }这段初始化代码有几个关键点先关闭后配置 修改SPI配置前先清除SPE位是一个好习惯可以避免配置过程中产生意外的时钟边沿。CPOL和CPHA 根据外设要求设置为Mode 0。务必与外设手册核对。自动SS 通过MODFEN1和SSOE1让硬件自动管理SS引脚。此时对应的GPIO引脚通常是PTD0或类似会自动被SPI模块接管在数据传输期间输出低电平。波特率计算 分频系数不一定是整数应选择最接近目标值的组合。过高的分频系数低速更稳定过低高速则对PCB布线和负载更敏感。4.2 基于查询方式的数据收发函数在简单应用或初始化阶段查询方式Polling足够使用。/** * brief 通过SPI1发送并接收一个字节查询方式 * param txData: 要发送的字节 * retval 接收到的字节 */ uint8_t SPI1_TransferByte(uint8_t txData) { // 1. 等待发送缓冲区为空SPTEF 1 while(!(SPI1S SPI_S_SPTEF_MASK)); // SPI_S_SPTEF_MASK 通常为 0x20 // 2. 将数据写入数据寄存器启动传输 SPI1D txData; // 3. 等待接收完成SPRF 1 while(!(SPI1S SPI_S_SPRF_MASK)); // SPI_S_SPRF_MASK 通常为 0x80 // 4. 读取接收到的数据此操作也会清除SPRF标志 return SPI1D; } /** * brief 通过SPI1发送多个字节查询方式 * param pTxData: 发送数据缓冲区指针 * param pRxData: 接收数据缓冲区指针可为NULL如果只发送不关心接收 * param size: 数据大小 * retval None */ void SPI1_TransferBlock(uint8_t *pTxData, uint8_t *pRxData, uint16_t size) { for(uint16_t i 0; i size; i) { uint8_t rx SPI1_TransferByte(pTxData[i]); if(pRxData ! NULL) { pRxData[i] rx; } } }为什么发送和接收可以写在一个函数里这正是SPI全双工特性的体现。主设备在发出一个字节的同时从设备也在通过MISO线发回一个字节。SPI1_TransferByte函数在发出txData的同时也等待着从设备返回的数据。即使你只想发送命令不关心返回也必须读取SPI1D来清除SPRF标志否则会导致后续通信卡死。4.3 基于中断方式的高效传输当需要传输大量数据或不想让CPU空等时中断方式是更好的选择。这里展示一个利用发送中断进行连续发送的思路框架。volatile uint8_t spi_tx_buffer[256]; volatile uint8_t spi_rx_buffer[256]; volatile uint16_t spi_tx_index 0; volatile uint16_t spi_tx_count 0; volatile bool spi_transfer_complete false; /** * brief SPI中断服务例程 */ void SPI1_IRQHandler(void) { // 检查中断源发送缓冲区空中断 if((SPI1S SPI_S_SPTEF_MASK) (SPI1C1 SPI_C1_SPTIE_MASK)) { if(spi_tx_index spi_tx_count) { // 还有数据要发送写入下一个字节 SPI1D spi_tx_buffer[spi_tx_index]; // 同时可以在这里读取上一个字节接收到的数据如果有用 // spi_rx_buffer[spi_tx_index-1] SPI1D; // 注意此时读的是上一个字节接收完成的数据 // 更严谨的做法是在SPRF中断中读取或使用DMA。 } else { // 所有数据发送完毕禁用发送中断 SPI1C1 ~SPI_C1_SPTIE_MASK; spi_transfer_complete true; } } // 还可以处理接收完成中断(SPRF)和模式故障中断(MODF) // ... } /** * brief 启动一次基于中断的SPI块传输 */ void SPI1_StartTransferIT(uint8_t *pData, uint16_t size) { // 1. 复制数据到发送缓冲区需考虑临界区保护 // memcpy((void*)spi_tx_buffer, pData, size); spi_tx_count size; spi_tx_index 0; spi_transfer_complete false; // 2. 确保SPI已使能且发送缓冲区为空 while(!(SPI1S SPI_S_SPTEF_MASK)); // 3. 写入第一个字节启动传输链 SPI1D spi_tx_buffer[spi_tx_index]; // 4. 使能SPI发送中断 SPI1C1 | SPI_C1_SPTIE_MASK; }注意事项 中断方式中数据的读取时机需要仔细设计。因为SPTEF中断发生在可以发送下一个数据时而此时上一个数据可能刚移入移位寄存器还未完全接收。因此在SPTEF中断中直接读取SPI1D得到的是更早之前接收的数据。对于需要精确对应收发的场景最好同时使能SPRF中断并在SPRF中断中读取数据。或者直接使用DMA控制器来搬运SPI数据这是处理高速流数据的最佳方案。5. 高级功能与故障排查实录5.1 模式故障(MODF)与多主冲突防护模式故障是SPI主设备的一种保护机制。当MODFEN1且SSOE0时主设备的SS引脚被配置为输入。如果这个引脚被意外拉低例如另一个设备错误地试图将它作为从设备选中MODF标志位会被置1并且SPI模块会自动将自己切换为从模式同时禁用MOSI、MISO、SCLK的输出驱动器防止总线冲突。如何恢复读取SPI1S寄存器此时MODF位为1。向SPI1C1寄存器写入任何值通常读回后再写回即可。这一步会清除MODF标志。重新将MSTR位设置为1切换回主模式。在单一主设备的系统中为了避免误触发通常我们设置MODFEN1且SSOE1使SS引脚为自动输出这样就避免了它被外部拉低的风险。5.2 接收溢出与数据丢失预防接收溢出是另一个常见问题。如前所述SPI接收是双缓冲的。如果CPU没有及时读取SPI1D即SPRF标志为1时未读走数据而下一个字节的传输已经完成新数据将无处存放导致接收溢出新数据丢失。MC1323x的SPI模块没有硬件溢出标志这意味着软件必须保证读取速度跟上传输速度。避坑技巧查询方式 在while循环等待SPRF时确保没有其他高优先级中断长时间阻塞CPU。中断方式 使能SPRF中断并在中断服务程序中第一时间读取SPI1D。中断优先级应设置得足够高。DMA方式 这是最可靠的方案。将SPI的接收数据寄存器SPI1D配置为DMA的源地址让DMA自动将数据搬运到内存缓冲区彻底解放CPU也绝不会溢出。5.3 示波器调试实战看懂SPI波形理论千万条波形第一条。调试SPI一个数字示波器是必不可少的。连接探头到SCLK、MOSI、MISO和SS线设置触发为SS下降沿。检查基本时序SS线是否在8个SCLK周期内保持低电平传输结束后是否拉高SCLK的空闲电平是否符合CPOL设置数据是在SCLK的哪个边沿稳定采样边沿是否符合CPHA设置重点看MISO线因为它是从设备响应的最能反映时序匹配情况。检查数据内容MOSI上发送的数据是否正确与你代码中写入的数据对比MISO上返回的数据是否符合预期与外设数据手册对比是MSB先传还是LSB先传LSBFE位常见异常波形与原因MISO线一直是高阻态或固定电平 从设备未被正确选中SS线问题、从设备供电或初始化问题、MISO引脚配置错误应为输入。SCLK没有输出 主设备SPI未使能SPE0、主模式未设置MSTR0、时钟源配置错误。数据错位 CPOL或CPHA设置与从设备不匹配。这是最常见的原因。只能发送一次数据第二次卡住 极大概率是SPTEF或SPRF标志清除顺序不对或者没有及时读取接收数据导致软件“死等”。我曾在调试一个SPI Flash时发现写入命令后读回的数据全是0xFF。用示波器抓取波形发现SCLK、MOSI、SS都正常但MISO线始终为高电平。排查后发现是Flash芯片的/HOLD和/WP引脚未上拉导致芯片进入写保护状态不输出数据。这个教训告诉我除了SPI四根线外设的其他控制引脚状态也必须检查。5.4 低功耗模式下的SPI行为MC1323x作为无线MCU低功耗至关重要。其SPI模块在不同低功耗模式下的行为如下Wait模式 CPU时钟停止但外设总线时钟BUSCLK可能仍在运行取决于SPISWAI位。如果SPISWAI0SPI时钟继续运行这意味着SPI从设备仍可响应外部主设备的访问并产生中断唤醒MCU。如果SPI作为主设备由于CPU停止无法发起传输。Stop模式 所有时钟停止SPI模块完全关闭。从Stop模式唤醒后SPI模块会处于复位状态需要重新初始化。因此在设计低功耗应用时如果SPI从设备需要随时唤醒主机则需配置为从模式并确保在进入Wait模式前SPISWAI0。同时唤醒后的中断服务程序需要妥善处理可能接收到的半截数据。