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 上升沿采样 少数专用芯片
避坑指南: 我曾经在一个项目里,MCU和Flash的datasheet都写着Mode 0,结果实际通信就是不对。后来用示波器一抓,发现MCU默认配置是Mode 0,但Flash内部复位后默认是Mode 3。所以,一定要看芯片复位后的默认模式,别光看datasheet的推荐配置。

4.2 AUTOSAR SPI Handler/Driver配置

在AUTOSAR架构里,SPI驱动属于MCAL层,全称Spi Driver。它上面还有一层Spi Handler/Driver,负责管理多个SPI通道和作业。

我个人习惯把配置分成三个层次来理解:

  1. SpiDriver —— 最底层,直接操作寄存器,收发数据。
  2. SpiChannel —— 逻辑通道,绑定一个片选信号和一组参数(模式、波特率等)。
  3. SpiJob —— 一次传输任务,包含一个或多个Channel的连续收发序列。

配置时,你需要在EB tresos或Vector DaVinci中完成以下关键项:

配置项 说明 我的建议
SpiChannelId 通道ID,应用层通过它访问 用枚举命名,别用数字
SpiDataWidth 数据位宽,通常8bit或16bit Flash一般用8bit
SpiBaudrate 波特率,单位Hz 先设低,调通再拉高
SpiCsPolarity 片选极性,高/低有效 绝大多数是低有效
SpiClkPolarity 时钟极性CPOL 跟从机datasheet对齐
SpiClkPhase 时钟相位CPHA 同上
SpiDmaChannel 绑定的DMA通道 大数据量传输必配
小技巧: 配置波特率时,别忘了考虑PCB走线长度。我遇到过一块板子,SPI时钟设到20MHz就出错,降到10MHz就稳了。后来查出来是CS到Flash的走线绕了半块板子,信号反射严重。所以,高速SPI一定要看信号完整性。

4.3 片选信号与DMA配置

片选信号:别小看这根线

片选信号看似简单,其实坑不少。我总结了几条经验:

  • CS必须在传输前拉低,传输结束后拉高。 有些MCU的SPI外设会自动控制CS,有些需要GPIO手动控制。我个人更倾向用GPIO手动控制,灵活,不容易出问题。
  • CS拉低后要加延迟。 从机需要时间准备,尤其是Flash,从CS拉低到第一个时钟沿,通常需要几十纳秒的建立时间。
  • 多设备共用SPI总线时,CS之间要有死区时间。 一个设备释放总线后,另一个设备才能接管。否则两个设备同时驱动MISO,轻则数据错乱,重则烧引脚。
核心原则: 片选信号是SPI总线的“门禁”,门没开对,里面再好的配置也白搭。

DMA配置:解放CPU

SPI传输数据时,如果每个字节都用CPU中断处理,那CPU基本就废了。尤其是驱动Flash时,一页256字节,一读就是几十页,CPU根本忙不过来。

DMA(直接存储器访问)就是干这个的。你配置好源地址、目的地址、传输长度,DMA自己就把数据搬完了,搬完再给你一个中断。

配置DMA时,这几个参数最关键:

参数 说明 典型值
源地址 数据从哪来 SPI数据寄存器地址
目的地址 数据存到哪 内存缓冲区地址
传输宽度 每次传输多少位 8bit或16bit
传输计数 总共传多少个 256(一页)
循环模式 是否自动重载 一般关闭
中断使能 传输完成是否触发中断 使能
注意: 我曾经在一个项目里,DMA配置了循环模式忘了关,结果每次SPI传输完,DMA自动重新开始,把上一帧数据又发了一遍。Flash写操作被重复执行,数据全写花了。排查了两天才找到原因……所以,不用循环模式就关掉。

4.4 实战:驱动外部SPI Flash

好了,理论说完了,咱们来点真格的。以一颗常见的SPI Flash(比如Winbond W25Q64)为例,实现读写操作。

硬件连接

MCU与Flash的连接很简单:

  • SCLK → SCLK
  • MOSI → MOSI
  • MISO → MISO
  • CS → 任意GPIO(我习惯用PB0)

初始化流程

在AUTOSAR框架下,初始化由Spi_Init()完成。但底层逻辑是这样的:

  1. 配置GPIO:CS设为推挽输出,初始拉高。
  2. 配置SPI外设:模式0,波特率1MHz(先慢后快)。
  3. 配置DMA:绑定SPI的TX和RX通道。
  4. 调用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 {
    /* 检查硬件连接或配置 */
}
经验之谈: 我第一次调SPI Flash时,读ID总是返回0xFF。查了半天,发现是CS拉低后没等Flash准备好就发指令了。后来在CS拉低后加了10微秒延迟,问题解决。所以,别省那点延迟。

完整读写流程

写数据到Flash,需要三步:擦除→写使能→写入。读数据就简单了,直接发读指令加地址就行。

下面是一个写一页数据的流程:

  1. 发送扇区擦除指令(0x20 + 地址),等待忙标志清除。
  2. 发送写使能指令(0x06)。
  3. 发送页编程指令(0x02 + 地址 + 256字节数据)。
  4. 等待忙标志清除。

读数据就更直接了:

  1. 发送读数据指令(0x03 + 起始地址)。
  2. 连续接收数据,直到读完指定长度。
  3. 拉高CS结束传输。

4.5 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速索引:

SPI Driver与通信知识体系 SPI协议基础 AUTOSAR配置 片选与DMA 4种模式 CPOL/CPHA 全双工通信 多从机片选 SpiDriver SpiChannel SpiJob EB tresos配置 CS极性/时序 DMA源/目的 传输宽度/计数 中断使能 实战:驱动外部SPI Flash W25Q64 读写操作 初始化配置 读ID验证 擦除/写/读 核心:先调通SPI通信,再验证Flash ID,最后实现完整读写

4.6 常见问题与排查思路

最后,分享几个我实际项目中踩过的坑,希望能帮你少走弯路:

现象 可能原因 排查方法
读ID全为0xFF CS未拉低、时钟模式不匹配 示波器抓CS和SCLK
写数据后读回全为0xFF 未执行写使能、Flash处于保护状态 检查状态寄存器
数据偶尔错位 时钟太快、走线太长 降低波特率测试
DMA传输不完整 传输计数配置错误 检查DMA配置结构体
最后说一句: SPI Flash驱动是嵌入式开发的“基本功”,但也是“试金石”。能把Flash调通,说明你对SPI协议、MCU外设、DMA、AUTOSAR配置都有了扎实的理解。别急,慢慢来,调通了就是你的。