简介:SPI是一种应用广泛的同步串行通信协议,常用于微控制器与外围设备之间的高速数据交换;这是一份面向PIC微控制器的SPI通信示例程序包,用C语言编写,主要解决PIC主机与各类SPI从设备之间的数据通信问题。对于需要驱动Flash存储器、传感器、显示屏等外设,或正在学习PIC内部MSSP模块SPI模式的开发者,可直接参考使用。资源包仅包含1个C源文件,整体压缩后约947B,代码体量虽小,却围绕SCK、MOSI、MISO、SS四线机制,覆盖了接口初始化、参数配置与数据收发核心流程,并给出了清晰的函数划分。程序实现了SPI接口初始化、四种工作模式(模式0~3)切换、时钟频率设定、从设备片选控制以及单字节读写等关键功能,开发者对照实际硬件调整引脚映射与时序参数,即可移植到Proteus仿真或实物开发板中,用于连接Flash、传感器、显示屏等常见SPI外设,也可辅助排查SCK/MOSI/MISO/SS信号线上的时序问题,节省从零编写底层驱动的时间。该资源目前已有127人学习下载,适合正在研究PIC单片机SPI协议、调试通信时序,或需要一套最小可运行代码的开发者参考。
1. 从"SPI.rar_PIC_PIC spi_spi pic"说起:PIC 与 SPI 的真正距离
SPI.rar_PIC_PIC spi_spi pic这类压缩包名,在各大单片机论坛和网盘里并不少见。文件名把芯片型号、接口名、工程关键词全部堆在一起,解压后往往是一堆.c、.h文件和 Proteus 仿真工程。对刚接触 PIC 的开发者来说,难题不在于 "SPI 是什么",而在于:PIC 的数据手册怎么看、MSSP 模块怎么配、为什么别人贴的代码在自己板子上读回来全是0xFF。STM32 有 CubeMX 图形化配置,PIC 虽然有 MCC 代码生成器,但你拿到的旧工程或同事发来的源码却未必带着完整配置,最后还是得回到寄存器级别。这篇文章就顺着 PIC 的 SPI 外设展开:模块结构、寄存器映射、XC8 驱动代码、W25Q64 读 ID 实战,最后收在软件模拟 SPI 和片选管理的技巧上。适合手里有一块 PIC16/18 系列开发板、想驱动 Flash、OLED、传感器之类 SPI 从设备的工程师。
2. PIC 的 SPI 外设:从 MSSP 模块到寄存器映射
2.1 SPI 物理层:四线、主从和极相位的概念先对齐
SPI 是同步串行接口,PIC 的 MSSP(Master Synchronous Serial Port)模块承担了它的大部分硬件工作。四根线分别是 SCK(时钟)、SDO(主出从入)、SDI(主入从出)、SS(片选,从模式时使用)。主设备产生时钟,数据在时钟边沿移位,从设备靠片选信号决定是否参与总线。相比 I2C,SPI 没有应答机制,也没有地址帧,主设备想和哪个从设备通信,就把哪个从设备的片选拉低。这个特性决定了 SPI 的速度可以做得比较高,但也意味着片选管理、时钟极性和相位必须由程序员自己控制好。
时钟极性和相位对应 SPI 的四种模式,PIC 的数据手册里用 CKP(时钟极性)和 CKE(时钟边沿)两个位来描述,外部从设备的数据手册则用 CPOL/CPHA 描述。绝大多数 SPI Flash 和 OLED 驱动芯片默认工作在 Mode 0,即 CPOL=0、CPHA=0,时钟空闲为低,数据在上升沿采样。在 PIC 的 MSSP 上,Mode 0 对应 CKP=0、CKE=1。这里有一个常见误区:有人把 CKP 和 CKE 当成独立的开关随意尝试,却不理解它们在时间轴上对应哪个边沿,结果波形看起来有数据,但读回来全错位。
Mode 0: 空闲低电平,上升沿采样,下降沿切换数据 Mode 3: 空闲高电平,上升沿采样,下降沿切换数据(注意极性相反)与 I2C 相比,SPI 没有标准定义的总线速率分级,SCK 上限完全由主设备分频和从设备最高时钟参数共同决定。PIC16F877A 的 MSSP 在 20 MHz 主频下最高可以跑出 5 MHz 的 SCK(Fosc/4),但实际使用中要考虑从设备的最高时钟、PCB 走线长度和 MISO 建立时间。建议第一版驱动先跑低频,如 Fosc/64,逻辑分析仪确认波形没问题后再往上加速。
2.2 把 MSSP 拆开:SSPCON1、SSPSTAT、SSPBUF 的职责划分
PIC16F877A 这类经典芯片的 MSSP 模块,核心寄存器只有三个:SSPCON1 负责模式选择和使能,SSPSTAT 负责状态位和时钟边沿配置,SSPBUF 是数据缓冲。注意,这里没有独立的波特率寄存器,SPI 主模式的时钟分频直接由 SSPCON1 里的 SSPM 位决定,SSPADD 是 I2C 模式才用的,不少初学 PIC 的人会在 SSPADD 上浪费时间。
SSPCON1 的各 bit 分布如下:
- Bit 7 WCOL:写冲突标志,发送过程中又写 SSPBUF 时置 1,需要软件清除
- Bit 6 SSPOV:接收溢出标志,SSPBUF 未读又收到新数据时置 1
- Bit 5 SSPEN:MSSP 模块使能,置 1 后 SCK/SDO/SDI 才被模块接管
- Bit 4 CKP:时钟极性,0 为空闲低电平,1 为空闲高电平
- Bit 3:0 SSPM:模式选择,SPI 主模式时选
0100(Fosc/64)或0000(Fosc/4)
SSPSTAT 的关键位是 CKE(时钟边沿选择)、SMP(采样点选择)和 BF(缓冲满标志)。CKE 决定了数据在时钟的哪个边沿变化,SMP 决定在哪个位置采样输入数据。这两个位配合 CKP 一起,才能拼出完整的 Mode 0 到 Mode 3。
SSPBUF 是一个双重缓冲寄存器。写入 SSPBUF 时,数据被送入内部的移位寄存器,同时硬件开始产生 SCK;接收完成后,移位寄存器里的数据被搬运到 SSPBUF。所以说,一次SSPBUF = data;加一次while(!SSPSTATbits.BF);加一次result = SSPBUF;就完成了一字节的全双工交换。这里要特别注意,读 SSPBUF 会清除 BF 标志,所以轮询和读取的顺序不能颠倒。
// XC8 中常见的寄存器写法,PIC16F877A SSPCON1 = 0b00100010; // SPI 主模式, Fosc/64, CKP=0, SSPEN=1 SSPSTAT = 0b01000000; // CKE=1, SMP=0,对应 Mode 0这段配置的含义是:SSPM 位为0010,SCK 频率为主时钟的 1/64;CKP 为 0,SCK 空闲为低;CKE 为 1,数据在 SCK 上升沿采样。这是驱动 W25Q64、OLED、nRF24L01 这类从设备时最常用的起点配置。
2.3 时钟频率怎么算:Fosc/4 到 Fosc/64 的选择逻辑
PIC16 系列 SPI 主模式的时钟来源是系统时钟分频,各档位由 SSPM 的低四位决定。常见配置如下表:
| SSPM 值 | 分频 | 4 MHz 主频下 SCK | 20 MHz 主频下 SCK |
|---|---|---|---|
| 0000 | Fosc/4 | 1 MHz | 5 MHz |
| 0001 | Fosc/16 | 250 kHz | 1.25 MHz |
| 0010 | Fosc/64 | 62.5 kHz | 312.5 kHz |
| 0011 | 定时器2输出/2 | 取决于 TMR2 | 取决于 TMR2 |
选择分频档位时,先看从设备数据手册里"Maximum SCK Frequency"一项,W25Q64 在标准 SPI 模式最多支持 50 MHz 时钟,远不是瓶颈;真正限制的是引脚驱动能力、飞线的信号质量、以及你的示波器/逻辑分析仪能不能看清波形。开发期用 Fosc/64 最稳妥,跑通逻辑后再逐步提升,比如把 SSPM 改成0001或0000。
有一种特殊情况值得提:某些从设备要求 SCK 低于某个值,比如早期的射频模块或者长线传输场景,这种情况下 Fosc/64 仍然太快,那就只能用软件模拟 SPI 或降低主频。片上硬件 SPI 的效率优势在这种场景会被极限拉低,反而 bit-bang 更灵活,这也是后面第 5 章单独讲软模拟的原因。
3. 用 XC8 写一个可复用的 PIC SPI 驱动
3.1 引脚规划:公共引脚、重映射和上下拉的预处理
PIC16F877A 的 MSSP 引脚是固定的:RC3 对应 SCK,RC4 对应 SDI,RC5 对应 SDO。这三个脚的 TRIS 位必须设置正确,方向反了,数据要么出不去,要么读不到。RC3(SCK)设为输出,RC5(SDO)设为输出,RC4(SDI)设为输入。片选脚建议自己选一个普通 GPIO,比如 RC0,不要用 MSSP 的 SS 脚。
#define SCK RC3 #define SDO RC5 #define SDI RC4 #define CS RC0 TRISC3 = 0; // SCK 输出 TRISC5 = 0; // SDO 输出 TRISC4 = 1; // SDI 输入 TRISC0 = 0; // 片选输出 CS = 1; // 默认不选中从设备在 Proteus 仿真里也要注意,RC4 同时是 I2C 的 SDA 引脚,仿真模型中如果 I2C 外设处在激活状态,可能影响 SDI 的电平读取。这在真实芯片上不会发生,但仿真时却是常见坑点。SDO 引脚在空闲状态下如果从设备没有驱动 MISO,读到的电平不确定,处理办法是在 PCB 上给 SDI 加一个 10k 上拉到 VCC,这样悬空时读到的是高电平,至少不会出现随机跳变。对 W25Q64 这种从设备来说,未选中时 MISO 是高阻态,上拉很有必要。
3.2 最小驱动代码:初始化、单字节收发、片选控制
下面这段代码可以在 XC8 编译器下直接使用,接口兼容 8 位 PIC 的 MSSP 模块。spi_init()放在主程序开始时调用,spi_xfer()每次完成一字节的全双工交换,读指令时传0x00或其他任意占位字节。
void spi_init(void) { TRISC3 = 0; TRISC5 = 0; TRISC4 = 1; TRISC0 = 0; CS = 1; SSPCON1 = 0b00100010; // SPI 主模式, Fosc/64, CKP=0, SSPEN=1 SSPSTAT = 0b01000000; // CKE=1, SMP=0 (Mode 0) } unsigned char spi_xfer(unsigned char data) { SSPBUF = data; // 写入要发送的数据,触发 SCK 移位 while (!SSPSTATbits.BF); // 等待接收缓冲满 return SSPBUF; // 读取接收到的数据,同时清 BF 标志 }这段代码的逻辑是:写SSPBUF之后,硬件自动把数据一位一位移出 SDO,同时从 SDI 移入数据。BF 位置 1 代表接收完成,此时SSPBUF里就是从设备回复的字节。读操作本身会清除 BF 位,所以下一次循环不会读到旧数据。注意每次spi_xfer()的返回值都必须读走,哪怕你不关心它,否则溢出标志 SSPOV 会置位,后续通信数据就乱了。
实际操作中,片选拉低应该在发送第一个字节之前,拉高要在读完全部字节之后,中间不能松开。常见的错误是在循环里每次调用spi_xfer()都重新拉低拉高 CS,这样从设备会被反复复位,读出来的数据自然不对。
// 读 W25Q64 JEDEC ID 的调用方式 CS = 0; spi_xfer(0x9F); id_byte1 = spi_xfer(0x00); id_byte2 = spi_xfer(0x00); id_byte3 = spi_xfer(0x00); CS = 1;3.3 波特率与模式选择对照表:默认 Mode 0 怎么读
MSSP 的边沿配置可以通过 CKP 和 CKE 自由组合,但并不是所有组合都对应外部通用的模式编号。我这里给出一张实用对照表,方便你直接查询和验证:
| 外部模式 | CPOL | CPHA | PIC 建议配置(CKP, CKE) | 典型从设备 |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | CKP=0, CKE=1 | W25Q64, OLED, nRF24L01 |
| Mode 1 | 0 | 1 | CKP=0, CKE=0 | 部分 ADC 芯片 |
| Mode 2 | 1 | 0 | CKP=1, CKE=1 | 部分传感器 |
| Mode 3 | 1 | 1 | CKP=1, CKE=0 | 部分 Flash 的 QPI 模式周边 |
如果从设备数据手册只写了 CPOL/CPHA,没有直接给 Mode 号,就按这个表换算成 PIC 的 CKP/CKE。要注意的是,老版本 Microchip 数据手册对 CKE 的描述出现过表述不清的情况,不同系列芯片(PIC16 和 PIC18)在极相位的对应关系上是一致的,但如果你用的是 PIC24/dsPIC,寄存器名和位定义都变了,必须重新查手册。
调试的时候如果波形看起来对但数据错乱,不要只盯着 SCK 频率,把 CKP 和 CKE 的四种组合各试一遍,配合逻辑分析仪看哪组能解出正确的字节。这个排查法看似笨,但效率比反复读手册高得多,因为有时候从设备的时序图和文字描述并不完全一致,实测才是最终标准。
4. 实战:SPI 读写 W25Q64,验证驱动是否成立
4.1 W25Q64 的命令协议如何和 PIC 驱动对接
W25Q64 是华邦 8 MB 串行 Flash,SPI 接口时序简单、价格便宜,是验证 SPI 驱动的理想从设备。它识别自己是靠 JEDEC ID,发送0x9F命令后,从设备会回复 3 个字节:厂商 ID、容量 ID、版本 ID。W25Q64 的典型回复是0xEF 0x40 0x17,其中0x40表示容量为 8 MB,0x17是设备版本。GD25Q64 等国产兼容芯片会回复0xC8开头,说明总线通信是通的,只是厂商不同。
除了 JEDEC ID,最常用的命令是读状态寄存器0x05,它返回 1 个字节,Bit 0 是 BUSY 位。写入操作前要等 BUSY 清零才能进行,这是 Flash 编程的基本握手。写使能命令0x06必须在每次页编程或擦除前发送,否则命令会被忽略,这个细节在处理写入时会反复碰到。
4.2 完整例程:读 JEDEC ID 与读状态寄存器
下面的函数把读 JEDEC ID 封装好,返回一个uint32_t,低 24 位依次是厂商、容量、版本 ID。因为 PIC16 的unsigned long是 32 位,可以直接放。
unsigned long w25q64_read_jedec_id(void) { unsigned long id = 0; CS = 0; spi_xfer(0x9F); // 发送 JEDEC ID 读命令 id = (unsigned long)spi_xfer(0x00) << 16; id |= (unsigned long)spi_xfer(0x00) << 8; id |= (unsigned long)spi_xfer(0x00); CS = 1; return id; } unsigned char w25q64_read_status(void) { unsigned char status; CS = 0; spi_xfer(0x05); // 读状态寄存器命令 status = spi_xfer(0x00); // 占位发送,接收状态字节 CS = 1; return status; }函数里把0x00作为占位字节发送,是因为 SPI 是全双工协议,每发送一个字节就会收到一个字节。即使你只关心从设备发来的数据,也必须输出时钟,硬件才会移位。CS = 0在命令发送前执行,CS = 1在整个事务完成后才拉高,中途不能释放,否则 W25Q64 会把当前指令当作完成而终止。
// 主程序验证 unsigned long jedec; jedec = w25q64_read_jedec_id(); if ((jedec & 0xFFFFFF) == 0xEF4017) { // W25Q64 识别成功 } else { // 检查硬件连接或极相配置 }4.3 出现全 0xFF 时先查这三个地方
SPI 调试最典型的故障现象就是读回来的数据全是0xFF,这说明总线上一半以上的环节出了问题。按优先级排查,第一是片选:用万用表量 CS 引脚电压,确认在事务期间确实被拉低。很多人会把片选误接到 MISO 的邻近引脚,或者飞线接触不良,导致从设备从未被选中,此时 MISO 一直处于高阻态,被上拉拉高,读回0xFF就顺理成章了。
第二是时钟极性和相位不匹配。W25Q64 支持 Mode 0 和 Mode 3,但如果你把 CKP 配成了 1,而数据手册里默认是 Mode 0,从设备会对边沿判断错位,解码出的数据就可能是0xFF。这种问题看波形最容易发现,SCK 空闲电平应该为低,逻辑分析仪上一眼就能看出来。
第三是 SDI 引脚方向。TRISC4 如果是输出模式,PIC 内部的输出驱动器会强制驱动引脚,外部从设备根本拉不动它,数据就进不来。这个问题常见于复用别人工程时顺带把引脚配置一起拷过来,偏偏没留意方向位的赋值。把这三处逐一核对,百分之八十的0xFF问题都能解决,剩下的才需要怀疑芯片损坏或焊接异常。
5. 进阶:软件模拟 SPI、多从机片选与调试技巧
5.1 什么时候需要软件模拟 SPI,bit-bang 最小实现
硬件 SPI 不是万能药,碰到这两种情况就得用软件模拟:一是 MSSP 被 I2C 占用(PIC16 的 MSSP 在同一时刻只能选一种模式),二是需要把 SCK 拉到硬件外设不支持的低频。用 GPIO 逐位翻转,就是常见的 bit-bang 方式。PIC 引脚翻转速度不快,但配合内部 4/8 MHz 时钟驱动 W25Q64 这类设备绰绰有余。
void spi_soft_init(void) { SCK = 0; // 空闲低电平,对应 Mode 0 SDO = 0; CS = 1; } unsigned char spi_soft_xfer(unsigned char tx) { unsigned char i, rx = 0; for (i = 0; i < 8; i++) { SDO = (tx & 0x80) ? 1 : 0; tx <<= 1; SCK = 1; // 上升沿,从设备采样 rx <<= 1; if (SDI) rx |= 0x01; // 读取 MISO SCK = 0; // 拉低准备下一次上升沿 } return rx; }代码中每一个 SCK 高电平都模拟了硬件 SPI 的采样边沿,时序逻辑上和 MSSP 的 Mode 0 完全一致。注意这个写法是"先置数据再抬时钟",顺序不能反。软件模拟 SPI 的波特率由每条语句的执行时间决定,无法精确用公式计算,但这在低频场景下反而成了优势:不用配分频,直接把延时循环加在SCK = 1和SCK = 0之间就行,适配长线或慢速从设备非常灵活。
5.2 多从机与硬件片选/软件片选的取舍
PIC 的 MSSP 主模式下,SS 引脚默认不作为片选输出使用。很多开发者第一次接触 PIC 时,会下意识把 SS 脚接从设备的片选,结果发现控制不了,这就是没搞清"硬件片选"和"软件片选"的区别。
| 片选方式 | 引脚占用 | 切换速度 | 适用场景 |
|---|---|---|---|
| 硬件片选(仅从模式) | 1 引脚由 MSSP 控制 | 由硬件自动控制 | 单主单从,且从设备 SS 必须接 MCU |
| 软件片选(GPIO 手动拉) | 每从机 1 个 GPIO | 靠代码控制,速度偏慢 | 多从机,或从机片选极性特殊 |
PIC 主模式本身没有硬件片选自动拉低机制,所以最常见的做法就是软件片选,每个从设备一个 GPIO。管理多个 Flash、OLED 和传感器时,写一个spi_select(uint8_t cs_pin)和spi_deselect()会比逐个拉电平更可靠。另外要注意,如果某个从设备的片选是高电平有效,控制逻辑要反过来,这类芯片在 SPI 总线里不太常见,但遇到时直接套用低有效逻辑就会一直处于未选中状态。
5.3 验证:逻辑分析仪抓时序与频率调整
硬件 SPI 驱动写完,不要急着接真实从设备,先用一个 8 通道逻辑分析仪抓一下 CS、SCK、SDO、SDI 四条线的时序。在屏幕上确认三个特征:CS 拉低期间,SCK 有完整 8 个脉冲;SCK 空闲电平为低(Mode 0 时);SDO 的数据在 SCK 上升沿之前已稳定。任意一条不满足,都能定位到对应的寄存器配置或引脚设置。
逻辑分析仪的 SPI 解码器通常会要求你指定 CPOL/CPHA 和位序,这里填 Mode 0 和 MSB First。如果解码结果和发送的数据一致,硬件 SPI 通路基本就没问题了。之后再接上 W25Q64 验证 JEDEC ID。这个过程比直接闷头调从设备快得多,因为你能区分问题是出在 PIC 侧还是从设备侧。
最后一个实用技巧:当怀疑 SCK 太快导致 MISO 采样点不对时,不一定要降主频,可以只把 SSPCON1 的分频从 Fosc/4 改成 Fosc/16 或 Fosc/64,再比较逻辑分析仪的解码结果。多数情况下,问题会从"波形乱跳"变成"数据完全正确",这比逐条改 CKE/CKP 组合更快。
本文还有配套的精品资源,点击获取