4、SPI Driver与通信:从协议到Flash实战
SPI,说白了就是嵌入式世界里最常用的板级通信总线之一。我做了这么多年项目,几乎每个板子上都有SPI设备——Flash、传感器、显示屏、ADC……你想想看,要是连SPI都搞不定,那基本告别汽车电子了。
这一章,咱们就把它彻底吃透。从协议基础到AUTOSAR配置,再到驱动外部SPI Flash的完整实战,一步到位。
4.1 SPI协议基础:四根线搞定一切
SPI全称Serial Peripheral Interface,同步串行外设接口。它用四根线实现全双工通信:
| 信号 | 方向 | 说明 |
|---|---|---|
| SCLK | Master → Slave | 串行时钟,由主机产生 |
| MOSI | Master → Slave | 主机输出,从机输入 |
| MISO | Slave → Master | 从机输出,主机输入 |
| CS | Master → Slave | 片选信号,低电平有效 |
这里有个关键点:SPI没有从机地址,靠片选信号区分设备。你拉低哪个CS,就访问哪个从机。我刚开始做项目时,有次把CS极性搞反了,结果两个Flash同时响应,数据全乱套……嗯,从那以后我再也不敢轻视片选配置。
时钟极性与相位
SPI有四种工作模式,由CPOL(时钟极性)和CPHA(时钟相位)决定:
| 模式 | CPOL | CPHA | 采样沿 | 常用场景 |
|---|---|---|---|---|
| 0 | 0 | 0 | 上升沿采样 | 多数Flash、SD卡 |
| 1 | 0 | 1 | 下降沿采样 | 某些ADC |
| 2 | 1 | 0 | 下降沿采样 | 部分传感器 |
| 3 | 1 | 1 | 上升沿采样 | 少数专用芯片 |
4.2 AUTOSAR SPI Handler/Driver配置
在AUTOSAR架构里,SPI驱动属于MCAL层,全称Spi Driver。它上面还有一层Spi Handler/Driver,负责管理多个SPI通道和作业。
我个人习惯把配置分成三个层次来理解:
- SpiDriver —— 最底层,直接操作寄存器,收发数据。
- SpiChannel —— 逻辑通道,绑定一个片选信号和一组参数(模式、波特率等)。
- SpiJob —— 一次传输任务,包含一个或多个Channel的连续收发序列。
配置时,你需要在EB tresos或Vector DaVinci中完成以下关键项:
| 配置项 | 说明 | 我的建议 |
|---|---|---|
| SpiChannelId | 通道ID,应用层通过它访问 | 用枚举命名,别用数字 |
| SpiDataWidth | 数据位宽,通常8bit或16bit | Flash一般用8bit |
| SpiBaudrate | 波特率,单位Hz | 先设低,调通再拉高 |
| SpiCsPolarity | 片选极性,高/低有效 | 绝大多数是低有效 |
| SpiClkPolarity | 时钟极性CPOL | 跟从机datasheet对齐 |
| SpiClkPhase | 时钟相位CPHA | 同上 |
| SpiDmaChannel | 绑定的DMA通道 | 大数据量传输必配 |
4.3 片选信号与DMA配置
片选信号:别小看这根线
片选信号看似简单,其实坑不少。我总结了几条经验:
- CS必须在传输前拉低,传输结束后拉高。 有些MCU的SPI外设会自动控制CS,有些需要GPIO手动控制。我个人更倾向用GPIO手动控制,灵活,不容易出问题。
- CS拉低后要加延迟。 从机需要时间准备,尤其是Flash,从CS拉低到第一个时钟沿,通常需要几十纳秒的建立时间。
- 多设备共用SPI总线时,CS之间要有死区时间。 一个设备释放总线后,另一个设备才能接管。否则两个设备同时驱动MISO,轻则数据错乱,重则烧引脚。
DMA配置:解放CPU
SPI传输数据时,如果每个字节都用CPU中断处理,那CPU基本就废了。尤其是驱动Flash时,一页256字节,一读就是几十页,CPU根本忙不过来。
DMA(直接存储器访问)就是干这个的。你配置好源地址、目的地址、传输长度,DMA自己就把数据搬完了,搬完再给你一个中断。
配置DMA时,这几个参数最关键:
| 参数 | 说明 | 典型值 |
|---|---|---|
| 源地址 | 数据从哪来 | SPI数据寄存器地址 |
| 目的地址 | 数据存到哪 | 内存缓冲区地址 |
| 传输宽度 | 每次传输多少位 | 8bit或16bit |
| 传输计数 | 总共传多少个 | 256(一页) |
| 循环模式 | 是否自动重载 | 一般关闭 |
| 中断使能 | 传输完成是否触发中断 | 使能 |
4.4 实战:驱动外部SPI Flash
好了,理论说完了,咱们来点真格的。以一颗常见的SPI Flash(比如Winbond W25Q64)为例,实现读写操作。
硬件连接
MCU与Flash的连接很简单:
- SCLK → SCLK
- MOSI → MOSI
- MISO → MISO
- CS → 任意GPIO(我习惯用PB0)
初始化流程
在AUTOSAR框架下,初始化由Spi_Init()完成。但底层逻辑是这样的:
- 配置GPIO:CS设为推挽输出,初始拉高。
- 配置SPI外设:模式0,波特率1MHz(先慢后快)。
- 配置DMA:绑定SPI的TX和RX通道。
- 调用Spi_Init(),传入配置结构体。
Flash操作命令
W25Q64的指令集很标准,常用的就这几个:
| 指令 | 代码 | 说明 |
|---|---|---|
| Write Enable | 0x06 | 写使能,每次写操作前必须发 |
| Read Status | 0x05 | 读状态寄存器,检查是否忙 |
| Page Program | 0x02 | 写一页(256字节) |
| Read Data | 0x03 | 读数据,地址连续 |
| Sector Erase | 0x20 | 擦除一个扇区(4KB) |
代码示例:读Flash ID
这是最基础的验证代码,能读到ID就说明SPI通信通了:
/* 读Flash ID指令:0x9F */
uint8_t cmd = 0x9F;
uint8_t rxBuf[3] = {0};
/* 拉低CS */
Spi_WriteChannel(CS_CHANNEL, 0);
/* 发送指令,接收3字节ID */
Spi_SyncTransmit(SPI_CH_FLASH, &cmd, 1);
Spi_SyncReceive(SPI_CH_FLASH, rxBuf, 3);
/* 拉高CS */
Spi_WriteChannel(CS_CHANNEL, 1);
/* 检查ID:W25Q64的ID应为0xEF4017 */
if (rxBuf[0] == 0xEF && rxBuf[1] == 0x40 && rxBuf[2] == 0x17) {
/* SPI通信正常 */
} else {
/* 检查硬件连接或配置 */
}
完整读写流程
写数据到Flash,需要三步:擦除→写使能→写入。读数据就简单了,直接发读指令加地址就行。
下面是一个写一页数据的流程:
- 发送扇区擦除指令(0x20 + 地址),等待忙标志清除。
- 发送写使能指令(0x06)。
- 发送页编程指令(0x02 + 地址 + 256字节数据)。
- 等待忙标志清除。
读数据就更直接了:
- 发送读数据指令(0x03 + 起始地址)。
- 连续接收数据,直到读完指定长度。
- 拉高CS结束传输。
4.5 知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速索引:
4.6 常见问题与排查思路
最后,分享几个我实际项目中踩过的坑,希望能帮你少走弯路:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读ID全为0xFF | CS未拉低、时钟模式不匹配 | 示波器抓CS和SCLK |
| 写数据后读回全为0xFF | 未执行写使能、Flash处于保护状态 | 检查状态寄存器 |
| 数据偶尔错位 | 时钟太快、走线太长 | 降低波特率测试 |
| DMA传输不完整 | 传输计数配置错误 | 检查DMA配置结构体 |