
简介面向PIC微控制器开发者的串行外设接口通信程序源码包专注解决微控制器与各类外设之间的高速同步数据交换问题适用于传感器采集、显示屏驱动、存储芯片读写等嵌入式场景。资源共1个文件为C语言源码文件RAR压缩包仅947B代码体量非常精简适合入门者通读和基础驱动的快速移植。目前已有127人学习可供需要理解底层协议实现或进行二次开发的单片机工程师参考。源码覆盖串行外设接口初始化、四种工作模式切换、时钟频率配置、从设备选择以及读写数据等核心函数并对时钟线、主出从入线、主入从出线和片选线四条信号线的时序关系作出清晰处理。通过阅读和修改这些代码读者可掌握主设备发起传输、从设备响应命令的完整流程也能够直接适配常见串行外设接口存储器件显示模块等外设从而显著缩短项目开发周期。1. 一个只有 SPI.c 的压缩包为什么还要看 PIC 的 MSSP拿到 SPI.rar 时里面只有一个 SPI.c很多人会直接把它拖进工程改好引脚就等着接 W25Q64、OLED 或 NRF24L01结果读回来的数据不是 0xFF 就是乱跳。SPI 虽然只有 SCK/MOSI/MISO/CS 四根线但在 PIC 上它藏在一个叫 MSSP 的模块里初始化顺序、CKP/CKE 组合、主模式时钟分频甚至 SS 引脚的方向都会影响通信结果。这个资源适合正在用 PIC16F88x 或类似型号做传感器采集、Flash 存储、显示屏驱动的工程师也适合原来用 STM32、现在不得不迁移到 PIC 的人。要真正用好这份代码先得把 MSSP 相关寄存器读明白否则 SPI.c 就只是一段“看起来能跑”的模板。2. PIC 的 MSSP 模块与 SPI 模式配置先看 CKP/CKE 再写初始化2.1 SPI 四种模式在 PIC 上的寄存器映射标准 SPI 协议由 CPOL 和 CPHA 两个位定位出 Mode 0 到 Mode 3。PIC 的 MSSP 没有直接用这两个名字而是用 SSPCON1 的 CKP 表示时钟极性用 SSPSTAT 的 CKE 表示数据在哪个边沿变化。很多人移植代码时习惯把 STM32 的 Mode 0/1/2/3 数值原封不动拿过来这在 PIC 上不成立因为 CKE 和 STM32 的 CPHA 位定义并不是一一对应。标准模式CKPSCK空闲电平CKE数据变化位置常见外设场景Mode 00空闲为低1空闲到有效边沿变化W25Q 系列 Flash、SSD1306 屏Mode 10空闲为低0有效到空闲边沿变化部分 ADC/DAC 芯片Mode 21空闲为高1空闲到有效边沿变化部分传感器模块Mode 31空闲为高0有效到空闲边沿变化部分 SD 卡、存储器注意CKE1 对应标准 CPHA0CKE0 对应标准 CPHA1。也就是说你在 STM32 上常用哪套模式到了 PIC 这边要先对着时序图核对不能只看数字。像 W25Q64 的手册会写支持 Mode 0 和 Mode 3但读 JEDEC ID 这种最底层的操作我一般固定用 Mode 0减少变量。SPI.c 里的SPI_SetMode如果写得太简单到了 PIC 上很容易漏掉 CKE 这个位。2.2 SPI_Init 的寄存器顺序先关 SSPEN再改 SSPSTAT初始化 MSSP 的关键不是把寄存器值算出来而是顺序要对。如果模块还开着就去改 CKP/CKESCK 上可能产生一个多余的脉冲从机会把这个毛刺当成命令起始位后面整包数据都错位。所以SPI_Init里第一步必须关闭 SSPEN。#define SPI_CLK_FOSC4 0b0010 #define SPI_CLK_FOSC16 0b0011 #define SPI_CLK_FOSC64 0b0100 void SPI_Init(unsigned char mode, unsigned char prescaler) { SSPEN 0; /* 1. 关闭MSSP避免配置中途产生SCK */ SSPCON1 0x00; /* 2. 清空SSPCON1输出保持空闲 */ SSPSTAT 0x00; /* 3. 清空SSPSTATSMP0、CKE0 */ switch (mode) /* 4. 按标准SPI模式设置CKP/CKE */ { case 0: SSPCON1bits.CKP 0; SSPSTATbits.CKE 1; break; case 1: SSPCON1bits.CKP 0; SSPSTATbits.CKE 0; break; case 2: SSPCON1bits.CKP 1; SSPSTATbits.CKE 1; break; case 3: SSPCON1bits.CKP 1; SSPSTATbits.CKE 0; break; } SSPCON1bits.SSPM prescaler; /* 5. 选择主模式时钟分频 */ SSPEN 1; /* 6. 使能MSSP开始产生SCK */ }这个函数把初始化收敛成一个入口mode只接受 0~3对应前面表格里的四种组合prescaler是 SSPM 字段的值不是实际分频倍数。经典 PIC16 的 SPI 主模式没有独立的波特率寄存器直接由 SSPM 选择/4、/16、/64例如SPI_Init(0, SPI_CLK_FOSC16)在 8MHz 晶振下得到 500kHz SCK。如果你用的 PIC 型号带 SPI 波特率发生器比如部分 PIC18F 系列那就改成SSPADD参与计算常见公式是Fosc / (4 * (SSPADD 1))效果等价于这里的分频选择。SMP 位我在这里设成 0也就是主机在数据输出中间采样。遇到 MISO 数据建立时间比较长的从设备再把 SMP 改成 1采样点会移到数据末尾通信余量会大一些。这个位很容易被 SPI.c 模板漏掉默认值也不全一致建议在外设调试不通时主动试一次。2.3 常见误区只改“分频值”不看实际 SCK很多人把初始化里的0b0011改成0b0010以为 SCK 翻倍就完事了忘了 PIC 内部振荡器本身的偏差可能到 ±1%~2%。用内部 OSC 时OSCCON即使设成 8MHz实际频率在不同温度下也会有波动低速 SPI 无所谓到了 2MHz 以上就可能出现偶发错位。外部晶振则要去烧录配置位里确认FOSC选的是 HS 而不是 LPLP 模式下晶振可能被当作低频钟表晶振实际频率只有几十 kHzSPI 通信照样能跑只是慢得让人误以为程序卡住。提示用 Pickit3 烧录时在配置位界面里先看一遍 FOSC 和 LVP。很多 PIC 板子默认开启 LVP会占用 PORTB 相关引脚如果那片引脚正好被当作 CS 或 MISO插拔烧录器后通信就变异常。调试时最常见的手段是拿示波器直接测 SCK 引脚数一数有没有 8 个脉冲、频率是否接近期望值。PIC16F88x 这类器件引脚复用很多MSSP 的 SCK/SDI/SDO 可能不是默认位置如果代码配置没问题但示波器上完全看不到 SCK优先查 APFCON 或对应型号的复用寄存器。3. 读写函数与片选策略SPI.c 里的核心不是读也不是写是字节交换3.1 SSPBUF 同时完成发送和接收SPI 是全双工主机每发一个字节从机通过 MISO 同时移出一个字节。所以真正底层的函数只有一个写SSPBUF启动一次传输等SSPIF置位后读回SSPBUF。所谓SPI_Read其实就是发一个空字节用产生的 8 个 SCK 时钟把从机数据移出来。unsigned char SPI_TransferByte(unsigned char dat) { SSPBUF dat; /* 写入SSPBUF即启动一次传输 */ while (!PIR1bits.SSPIF) /* 等待移位结束 */ ; PIR1bits.SSPIF 0; /* 必须软件清除中断标志 */ return SSPBUF; /* 返回MISO收到的数据 */ }逻辑说明dat是主机准备发送的字节如果只是读从机数据通常填 0x00 或 0xFF。SSPIF是 MSSP 的中断标志位位置随型号可能不同有的在PIR1有的在PIR2移植 SPI.c 时第一件事就是改这个标志位。等待循环里不要加超时处理调试时用示波器看 SCK 是否在循环里正常输出如果在等待前忘了清除SSPIF循环会直接跳过返回的上一次数据会造成误判。3.2 硬件片选还是软件片选PIC 的 SS 引脚经常是噪声源在 PIC 的 MSSP 中SS 引脚在从模式下是片选输入在主模式下可能被用于总线冲突检测。我建议单主机多从机场景全部使用软件片选也就是把任意空闲 GPIO 当作 CS 来拉低和拉高。原因很简单有些 PIC 型号在 SS 引脚被外部拉低时会自动切到从模式SCK 和 MOSI 方向反转表现为主机“发不出去”而且这种故障不固定可能只是某个引脚碰到了排针。片选方案优点缺点适用场景硬件 SS省一个 GPIO能检测总线冲突引脚固定模式切换有隐含风险多主总线软件片选任意 I/O时序完全可控方便支持多设备每包数据要自己控制电平单主多从最常用这和 Linux SPI 子系统把片选声明成cs-gpios是同一套思路SPI 控制器管时钟和数据片选是通信行为由通用 GPIO 完成。写在 SPI.c 里的SPI_SelectSlave如果只是把某个引脚置低必须和具体的SPI_TransferByte调用绑定在一起不能只拉低 CS 不拉高否则下一个外设会被错误选中。#define FLASH_CS LATCbits.LATC0 void Flash_Select(void) { FLASH_CS 0; } void Flash_Release(void) { FLASH_CS 1; __delay_us(1); /* 保证从机片选释放至少持续1us */ }注意Flash_Select和Flash_Release之间是一个完整事务中间不能被打断。很多人习惯在每次SPI_TransferByte前都拉低一次 CS如果从机对 CS 上升沿敏感会把一次读操作拆成了好几段无效事务。片选时序的正确做法是先准备好 D/C 或命令地址再拉低 CS连续传输所有字节最后拉高 CS。3.3 用 W25Q64 读 ID 把整套驱动跑通W25Q64 是验证 SPI 驱动最方便的芯片因为它有固定返回的 JEDEC ID不需要擦除写入就能确认通信链路。下面是常用命令命令字节码说明Read JEDEC ID0x9F连续读出 3 字节 IDRead Data0x033 字节地址后连续读Write Enable0x06写操作前必须发送Page Program0x02一次最多写 256 字节unsigned long W25Q_ReadJEDEC(void) { unsigned char b0, b1, b2; Flash_Select(); SPI_TransferByte(0x9F); /* JEDEC ID 命令 */ b0 SPI_TransferByte(0xFF); b1 SPI_TransferByte(0xFF); b2 SPI_TransferByte(0xFF); Flash_Release(); return ((unsigned long)b0 16) | ((unsigned long)b1 8) | b2; }逻辑说明0x9F 后面跟的三个空字节用来产生读时钟同时 MISO 上会依次移出 Manufacturer ID、Memory Type、Capacity。W25Q64 的返回值是 0xEF4017。如果读到 0xFFFFFF先看 CS 是否在整个事务期间保持低电平如果读到 0x000000查 MISO 是否接对、PIC 的 TRIS 方向是否把 SDI 设置成输入如果前两个字节对但第三个不对重点怀疑 SPI 模式不是 Mode 0。这里顺带说明PIC 的 MSSP 不会自动配置 TRIS 方向只开模块不设引脚方向是 SPI.c 最常见的使用错误。4. OLED、NRF24L01 与 Proteus 模拟外设的“非 SPI”引脚决定了能不能亮4.1 SSD1306 的 D/C 引脚命令和数据是两套总线行为SSD1306 这类 SPI OLED 不是纯 SPI 设备它多了一个 D/C 引脚用来区分当前字节是命令还是显示数据。SPI.c 只需要负责把字节发出去但 D/C 和 CS 的配合是由上层驱动决定的。很多白屏问题就是 D/C 电平在 CS 下降沿还没稳定从机采样时读到了错误的命令/数据标志。#define OLED_CS LATCbits.LATC1 #define OLED_DC LATCbits.LATC2 void OLED_WriteCmd(unsigned char cmd) { OLED_DC 0; /* 命令通道 */ OLED_CS 0; SPI_TransferByte(cmd); OLED_CS 1; } void OLED_WriteData(unsigned char dat) { OLED_DC 1; /* 数据通道 */ OLED_CS 0; SPI_TransferByte(dat); OLED_CS 1; }逻辑说明OLED_DC 0表示写命令OLED_DC 1表示写数据电平必须在 CS 拉低之前就位。SSD1306 的显存是 1024 字节128×64 逐位映射连续写显存时 D/C 保持高可以一次发送很多字节但初始化命令序列里每条命令之后要按手册延时比如关显示、设置对比度、开显示之间都不能连续猛发。4.2 NRF24L01 的 CE 和 CSNSPI 管配置CE 管发射NRF24L01 的 SPI 接口叫 CSN 而不是 CS功能上依然是片选。另一个控制脚 CE 则完全不属于 SPI它负责把配置好的数据包真正发出去。很多人在 NRF24L01 上反复读寄存器都正常但发送不出去就是因为 CE 时序没有配合好。#define RF_CE LATCbits.LATC3 #define RF_CSN LATCbits.LATC4 void RF_Transmit(unsigned char *buf, unsigned char len) { RF_CE 0; /* 先让模块回到空闲状态 */ RF_CSN 0; /* 打开SPI片选 */ SPI_TransferByte(0xA0); /* 写TX_PAYLOAD命令 */ while (len--) { SPI_TransferByte(*buf); } RF_CSN 1; /* 关闭片选SPI事务结束 */ RF_CE 1; /* 拉高CE触发发送 */ __delay_us(10); RF_CE 0; }逻辑说明CSN 只控制 SPI 总线访问CE 控制射频状态机。如果 CE 一直拉高模块可能处于接收模式写入 TX FIFO 后并不会立刻发送而是被状态机打断。这里把SPI_TransferByte封在 NRF 驱动外层SPI.c 本身不需要知道 NRF 的任何协议这是比较干净的分层方式。4.3 在 Proteus 里怎么查 SPI 时序Proteus 里模拟 SPI OLED 或 W25Q64 时不要只看最终能不能显示数据要把 SCK、MOSI、CS、D/C 四路信号拉出来看。步骤我一般这样做在 Proteus 中放置 PIC16F887 和 SSD1306 或 W25Q64 模型按实际电路连好 SCK、MOSI、MISO、CS、D/C。把 SPI.c 加入源文件编译生成 HEX 后加载到 PIC 模型。打开菜单里的 Logic Analyser添加 SCLK、SDO、CS 到观测窗口。全速运行查看 CS 拉低期间 SCK 是否有 8 的整数倍个脉冲MOSI 上第一个字节是否等于命令码。Proteus 对信号时序的模拟比真实硬件更理想化经常出现“模拟正常实机不正常”的情况。反过来也一样模拟器里因为模型简化可能忽略真实电路的负载电容和引脚上拉需求。看到模拟通过后再到实机上把 SCK 频率降一档往往是解决不稳定最直接的办法。5. 把 SPI.c 改造成带自检的驱动回环测试和示波器判据5.1 回环测试MOSI 和 MISO 短接调试阶段最值得做的事是回环测试把 MOSI 和 MISO 用一根跳线短接然后让 PIC 发送一个已知字节读回来必须完全相同。这个测试能一次性验证 SCK 是否产生、MISO 是否配置成输入、SSPBUF 读写逻辑是否正确。unsigned char SPI_LoopbackTest(void) { unsigned char tx 0xA5; unsigned char rx; SPI_Init(0, SPI_CLK_FOSC64); /* 模式0时钟降到Fosc/64 */ rx SPI_TransferByte(tx); return (rx tx); }如果返回 1说明主机的 MOSI 输出、MISO 输入和 MSSP 模块都正常。返回 0 时不要急着改 SPI.c先用万用表量 MOSI 和 MISO 是否真的短接。PIC 的 TRIS 方向寄存器不会因为开启 MSSP 而自动配置SDI 引脚需要手动设为输入SDO 设为输出这是最常见的失败原因。5.2 示波器判据先看空闲电平再看第一个跳变沿把示波器 CH1 接 SCKCH2 接 MOSI发送 0x55。0x55 的二进制是 01010101每一位都反转可以很清楚地看到数据变化和 SCK 边沿的相对位置。对于 Mode 0SCK 空闲为低MOSI 在 SCK 上升沿附近变化主机在下降沿采样如果看到 MOSI 在下降沿变化说明 CKE 设置反了。对于 Mode 2/3先看 SCK 空闲是不是高电平再确认数据和边沿的先后关系。如果一次SPI_TransferByte后 SCK 只有七个脉冲多半是 SSPEN 在传输过程中被意外关闭或者中断函数里动了 SSPCON1。使用 PIC16F88x 这类型号时还要检查 APFCON 或重映射寄存器SCK 可能被映射到非默认引脚示波器探针夹错位置代码怎么调都看不到波形。回环测试时在 MOSI 和 MISO 之间串一个 10kΩ 电阻既防误接短路也能模拟实际线路的驱动衰减读到 0xA5 说明从寄存器配置到引脚方向都对了。本文还有配套的精品资源点击获取