📚 SPI 技术系列 · 持续更新中
本系列从推挽电气层一路打到 Linux spi 子系统,把"四根线"背后的电气/协议/控制器/驱动/实战全拧成一根线。
👉 想系统追更,戳这里查看完整专栏:
👉 其他技术系列专栏,欢迎关注、交流、共同成长:
本篇定位:协议、寄存器、框架都讲完了,本篇是落地------六大典型器件的完整驱动:W25Q Flash、SD 卡 SPI、ILI9341 OLED、MCP3008 ADC、LIS3DH 加速度计、QSPI Flash + XIP 启动。每个给裸机 + Linux 代码,讲多设备共总线设计和 RTOS 集成。读完你能上手写 SPI 器件驱动。
实战是把前面 11 篇串起来器件驱动要用到:电气(01)/SI(02)/速度(03)/模式(04)/CS(05)/帧(06)/QSPI(07)/寄存器(08)/传输模式(09)/错误(10)/框架(11)。本篇每个器件都标注用到哪些知识点,帮你把碎片串成体系 。对照 【I2C 技术系列 12】I2C 设备驱动实战 看,理解器件驱动的共通模式。
目录
- [一、W25Q SPI Flash 驱动](#一、W25Q SPI Flash 驱动)
-
- [1.1 W25Q 特性](#1.1 W25Q 特性)
- [1.2 裸机驱动](#1.2 裸机驱动)
- [1.3 Linux 驱动](#1.3 Linux 驱动)
- [二、SD 卡 SPI 模式驱动](#二、SD 卡 SPI 模式驱动)
-
- [2.1 SD 卡 SPI 模式](#2.1 SD 卡 SPI 模式)
- [2.2 命令帧](#2.2 命令帧)
- [2.3 裸机驱动骨架](#2.3 裸机驱动骨架)
- [2.4 Linux 驱动](#2.4 Linux 驱动)
- [三、ILI9341 OLED 显示屏驱动](#三、ILI9341 OLED 显示屏驱动)
-
- [3.1 ILI9341 特性](#3.1 ILI9341 特性)
- [3.2 裸机驱动](#3.2 裸机驱动)
- [3.3 Linux 驸动](#3.3 Linux 驸动)
- [四、MCP3008 ADC 驱动](#四、MCP3008 ADC 驱动)
-
- [4.1 MCP3008 特性](#4.1 MCP3008 特性)
- [4.2 帧格式](#4.2 帧格式)
- [4.3 裸机驱动](#4.3 裸机驱动)
- [4.4 Linux 驱动](#4.4 Linux 驱动)
- [五、LIS3DH 加速度计驱动](#五、LIS3DH 加速度计驱动)
-
- [5.1 LIS3DH 特性](#5.1 LIS3DH 特性)
- [5.2 裸机驱动](#5.2 裸机驱动)
- [5.3 Linux 驱动](#5.3 Linux 驱动)
- [六、QSPI Flash + XIP 启动](#六、QSPI Flash + XIP 启动)
-
- [6.1 启动流程](#6.1 启动流程)
- [6.2 SPL 中配 QSPI XIP(裸机)](#6.2 SPL 中配 QSPI XIP(裸机))
- [6.3 Linux 下 XIP](#6.3 Linux 下 XIP)
- 七、多设备共总线设计
-
- [7.1 典型多设备总线](#7.1 典型多设备总线)
- [7.2 切换设备要重配](#7.2 切换设备要重配)
- [7.3 注意事项](#7.3 注意事项)
- [7.4 Linux 自动管理](#7.4 Linux 自动管理)
- [八、RTOS 集成](#八、RTOS 集成)
-
- [8.1 互斥访问](#8.1 互斥访问)
- [8.2 任务级缓冲](#8.2 任务级缓冲)
- [8.3 DMA + 信号量](#8.3 DMA + 信号量)
- [九、关键问题 Q&A](#九、关键问题 Q&A)
-
- [Q1:W25Q 页编程为什么不能跨页?](#Q1:W25Q 页编程为什么不能跨页?)
- [Q2:SD 卡 SPI 初始化为什么必须 400kHz 以下?](#Q2:SD 卡 SPI 初始化为什么必须 400kHz 以下?)
- [Q3:ILI9341 刷屏怎么最快?](#Q3:ILI9341 刷屏怎么最快?)
- [Q4:QSPI XIP 启动,中断向量在 Flash,中断响应慢怎么办?](#Q4:QSPI XIP 启动,中断向量在 Flash,中断响应慢怎么办?)
- [Q5:多设备共总线,Flash 50MHz 和 MCP3008 1MHz,切换开销?](#Q5:多设备共总线,Flash 50MHz 和 MCP3008 1MHz,切换开销?)
- [Q6:Linux 下 SPI 设备驱动,用现成子系统还是自己写?](#Q6:Linux 下 SPI 设备驱动,用现成子系统还是自己写?)
- 十、实战案例
-
- [案例 1:W25Q 读 ID 返回 0xFFFFFF](#案例 1:W25Q 读 ID 返回 0xFFFFFF)
- [案例 2:SD 卡初始化失败](#案例 2:SD 卡初始化失败)
- [案例 3:ILI9341 花屏](#案例 3:ILI9341 花屏)
- [案例 4:MCP3008 数值跳变](#案例 4:MCP3008 数值跳变)
- [案例 5:LIS3DH 读 WHO_AM_I 不是 0x33](#案例 5:LIS3DH 读 WHO_AM_I 不是 0x33)
- [案例 6:QSPI XIP 启动挂死](#案例 6:QSPI XIP 启动挂死)
- [案例 7:多设备切换首字节错](#案例 7:多设备切换首字节错)
- 十一、本篇小结与速查
一、W25Q SPI Flash 驱动
用到知识:Mode 0/3(\[04])、MSB(\[06])、命令帧(\[06])、burst(\[06])、写后轮询(\[06])、页编程不跨页(\[06])
1.1 W25Q 特性
- 容量:W25Q128(16MB)/W25Q256(32MB)
- 接口:标准 SPI / Dual / Quad / QPI
- 命令:0x03 读 / 0x02 页编程 / 0x20 扇区擦 / 0x06 写使能 / 0x05 读状态 / 0x9F 读 ID
- 页编程:一次 ≤ 256 字节,不跨页
- 写周期:页编程 5ms,扇区擦 50~400ms,块擦 1~2s
1.2 裸机驱动
c
// 基本收发
uint8_t spi_xfer(uint8_t d) {
while (!(SPI->SR & TXE));
SPI->DR = d;
while (!(SPI->SR & RXNE));
return SPI->DR;
}
// 读 ID
uint32_t flash_read_id(void) {
uint32_t id;
gpio_low(CS);
spi_xfer(0x9F);
id = spi_xfer(0xFF) << 16;
id |= spi_xfer(0xFF) << 8;
id |= spi_xfer(0xFF);
gpio_high(CS);
return id; // 期望 0xEF4018
}
// 写使能
void flash_write_enable(void) {
gpio_low(CS);
spi_xfer(0x06);
gpio_high(CS);
}
// 等待 BUSY 清
void flash_wait_busy(void) {
uint8_t s;
uint32_t to = 10000000;
do {
gpio_low(CS);
spi_xfer(0x05);
s = spi_xfer(0xFF);
gpio_high(CS);
} while ((s & 0x01) && --to); // BUSY 位
}
// 扇区擦除(4KB)
void flash_erase_sector(uint32_t addr) {
flash_write_enable();
gpio_low(CS);
spi_xfer(0x20);
spi_xfer(addr >> 16);
spi_xfer(addr >> 8);
spi_xfer(addr);
gpio_high(CS);
flash_wait_busy(); // 等擦完
}
// 页编程(不跨页)
int flash_page_program(uint32_t addr, uint8_t *data, int len) {
int page_left = 256 - (addr % 256);
if (len > page_left) len = page_left; // 截断到页内
flash_write_enable();
gpio_low(CS);
spi_xfer(0x02);
spi_xfer(addr >> 16);
spi_xfer(addr >> 8);
spi_xfer(addr);
for (int i = 0; i < len; i++) spi_xfer(data[i]);
gpio_high(CS);
flash_wait_busy();
return len;
}
// 任意长度写(自动分页)
int flash_write(uint32_t addr, uint8_t *data, int len) {
int written = 0;
while (len > 0) {
int n = flash_page_program(addr, data, len);
addr += n; data += n; len -= n; written += n;
}
return written;
}
// 读数据(burst + DMA)
void flash_read(uint32_t addr, uint8_t *buf, int len) {
gpio_low(CS);
spi_xfer(0x03);
spi_xfer(addr >> 16);
spi_xfer(addr >> 8);
spi_xfer(addr);
// 大块用 DMA(见 09 篇)
for (int i = 0; i < len; i++) buf[i] = spi_xfer(0xFF);
gpio_high(CS);
}
1.3 Linux 驱动
用 spi-nor 子系统,几乎不用写代码:
dts
&spi0 {
flash@0 {
compatible = "winbond,w25q128", "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <104000000>;
spi-tx-bus-width = <4>;
spi-rx-bus-width = <4>;
#address-cells = <1>;
#size-cells = <1>;
partition@0 {
label = "boot";
reg = <0 0x100000>;
};
partition@100000 {
label = "rootfs";
reg = <0x100000 0x700000>;
};
};
};
- spi-nor 自动探测 SFDP,挂 /dev/mtd0
- 用户空间用 mtd-utils(Flash_erase / nandwrite / dd)
二、SD 卡 SPI 模式驱动
用到知识:Mode 0(\[04])、MSB(\[06])、CRC(\[10])、初始化低速(\[03])
2.1 SD 卡 SPI 模式
- SD 卡支持 SPI 模式(慢但通用)
- 初始化必须 ≤ 400kHz
- 命令帧:6 字节(0x01 + CMD + arg + CRC + stop)
- 响应:R1/R2/R3(1~5 字节)
- 数据块:512 字节 + 2 字节 CRC
2.2 命令帧
字节0: 0x01(起始) | (CMD & 0x3F)
字节1-4: 32bit 参数(大端)
字节5: CRC(7bit) | 0x01(停止位)
2.3 裸机驱动骨架
c
uint8_t sd_cmd(uint8_t cmd, uint32_t arg, uint8_t crc) {
uint8_t r1;
spi_xfer(0x40 | (cmd & 0x3F));
spi_xfer(arg >> 24);
spi_xfer(arg >> 16);
spi_xfer(arg >> 8);
spi_xfer(arg);
spi_xfer((crc << 1) | 0x01);
// 等响应(最多 8 个 dummy)
for (int i = 0; i < 8; i++) {
r1 = spi_xfer(0xFF);
if (!(r1 & 0x80)) break; // MSB=0 是响应
}
return r1;
}
// 初始化(必须 ≤ 400kHz)
int sd_init(void) {
spi_set_clock(400000); // 低速
// 上电后发 74+ 个 dummy(10 字节 0xFF)
for (int i = 0; i < 10; i++) spi_xfer(0xFF);
gpio_low(CS);
sd_cmd(0, 0, 0x4A); // CMD0 GO_IDLE
sd_cmd(8, 0x1AA, 0x43); // CMD8 检测版本
// ACMD41 初始化(循环到 idle=0)
while (sd_cmd(55, 0, 0) & 0x01) {
sd_cmd(41, 0x40000000, 0); // HCS
}
spi_set_clock(25000000); // 提速
return 0;
}
// 读块(512 字节)
int sd_read_block(uint32_t block, uint8_t *buf) {
gpio_low(CS);
sd_cmd(17, block, 0); // CMD17 读单块
// 等数据 token(0xFE)
while (spi_xfer(0xFF) != 0xFE);
// 读 512 字节
for (int i = 0; i < 512; i++) buf[i] = spi_xfer(0xFF);
spi_xfer(0xFF); spi_xfer(0xFF); // CRC 丢弃
gpio_high(CS);
return 0;
}
2.4 Linux 驱动
dts
&spi0 {
mmc@0 {
compatible = "mmc-spi-slot";
reg = <0>;
spi-max-frequency = <25000000>;
voltage-ranges = <3300 3300>;
gpios = <&gpio0 7 0>; // CD 卡检测
interrupts-extended = <&gpio0 7 IRQ_TYPE_EDGE_BOTH>;
};
};
- mmc-spi-slot 驱动自动管,挂 /dev/mmcblk0
- 支持文件系统(FAT32/ext4)
三、ILI9341 OLED 显示屏驱动
用到知识:Mode 3(\[04])、D/C 引脚(\[06])、burst 刷屏(\[06])、DMA(\[09])
3.1 ILI9341 特性
- 240×320 RGB,262K 色
- SPI Mode 3(CPOL=1, CPHA=1)
- D/C 引脚:0=命令,1=数据
- 命令:0x2A 列地址 / 0x2B 行地址 / 0x2C 写显存 / 0x29 显示开 / 0x11 退出睡眠
- 刷屏:设窗口 → 连续写像素(16bit RGB565)
3.2 裸机驱动
c
#define DC_CMD 0
#define DC_DATA 1
void lcd_cmd(uint8_t c) {
gpio_set(DC, DC_CMD);
gpio_low(CS);
spi_xfer(c);
gpio_high(CS);
}
void lcd_data(uint8_t *d, int len) {
gpio_set(DC, DC_DATA);
gpio_low(CS);
// 大块用 DMA
for (int i = 0; i < len; i++) spi_xfer(d[i]);
gpio_high(CS);
}
// 初始化
void lcd_init(void) {
gpio_low(RESET); delay(10); gpio_high(RESET); delay(120);
lcd_cmd(0x11); // 退出睡眠
delay(120);
lcd_cmd(0x29); // 显示开
lcd_cmd(0x3A); lcd_data_byte(0x55); // 16bit 色
}
// 设窗口
void lcd_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
lcd_cmd(0x2A); // 列地址
lcd_data_byte(x0 >> 8); lcd_data_byte(x0);
lcd_data_byte(x1 >> 8); lcd_data_byte(x1);
lcd_cmd(0x2B); // 行地址
lcd_data_byte(y0 >> 8); lcd_data_byte(y0);
lcd_data_byte(y1 >> 8); lcd_data_byte(y1);
lcd_cmd(0x2C); // 写显存
}
// 刷屏(240×320,RGB565)
void lcd_flush(uint16_t *fb) {
lcd_set_window(0, 0, 239, 319);
gpio_set(DC, DC_DATA);
gpio_low(CS);
// 153600 字节,DMA 传(见 09 篇)
spi_dma_xfer((uint8_t*)fb, NULL, 240*320*2);
gpio_high(CS);
}
3.3 Linux 驸动
dts
&spi0 {
oled@1 {
compatible = "ilitek,ili9341";
reg = <1>;
spi-max-frequency = <40000000>;
spi-cpol; spi-cpha; // Mode 3
dc-gpios = <&gpio0 6 0>;
reset-gpios = <&gpio0 7 0>;
rotation = <90>;
bgr;
fps = <30>;
};
};
- drivers/staging/fbtft/ 或 drivers/gpu/drm/tiny/ 有驱动
- 挂 /dev/fb0,framebuffer 接口
四、MCP3008 ADC 驱动
用到知识:Mode 0(\[04])、LSB first(\[06])、字长非8位(\[06])
4.1 MCP3008 特性
- 8 通道 10bit ADC,SPI
- Mode 0,LSB first(注意!)
- 命令:start bit + single/diff + channel(3bit)
- 转换结果:10bit,2 字节返回
4.2 帧格式
发: 00000001 SGL/DIFF D2 D1 D0 xxxx
收: xxxx xxxx xxxx Null B9 B8 B7 B6 B5 B4 B3 B2 B1 B0
←── 10bit 结果 ──→
4.3 裸机驱动
c
uint16_t mcp3008_read(uint8_t ch) {
uint16_t val;
gpio_low(CS);
spi_xfer(0x01); // start bit
uint8_t high = spi_xfer(0x80 | (ch << 4)); // 配置
uint8_t low = spi_xfer(0x00); // 读低字节
gpio_high(CS);
val = ((high & 0x03) << 8) | low; // 10bit
return val;
}
4.4 Linux 驱动
dts
&spi0 {
adc@2 {
compatible = "microchip,mcp3008";
reg = <2>;
spi-max-frequency = <1000000>;
#io-channel-cells = <1>;
vref-supply = <&vref_3v3>;
};
};
- drivers/iio/adc/mcp320x.c 支持
- 通过 IIO 子系统,/sys/bus/iio/devices/
五、LIS3DH 加速度计驱动
用到知识:Mode 0(\[04])、寄存器读写帧(\[06])、多字节连读(\[06])
5.1 LIS3DH 特性
- 3 轴加速度,SPI/I2C 双接口
- SPI Mode 0,MSB first
- 寄存器帧:bit7=R/W,bit6=多字节
- 读:0x80|reg,写:0x00|reg
- 多字节:0xC0|reg(地址自增)
- WHO_AM_I=0x33,CTRL_REG1=0x20(唤醒)
5.2 裸机驱动
c
uint8_t lis3dh_read_reg(uint8_t reg) {
uint8_t v;
gpio_low(CS);
spi_xfer(0x80 | reg); // 读,单字节
v = spi_xfer(0xFF);
gpio_high(CS);
return v;
}
void lis3dh_write_reg(uint8_t reg, uint8_t v) {
gpio_low(CS);
spi_xfer(reg); // 写(bit7=0)
spi_xfer(v);
gpio_high(CS);
}
// 多字节连读(6 字节 XYZ)
void lis3dh_read_xyz(int16_t *x, int16_t *y, int16_t *z) {
uint8_t buf[6];
gpio_low(CS);
spi_xfer(0xC0 | 0x28); // 多字节读 OUT_X_L
for (int i = 0; i < 6; i++) buf[i] = spi_xfer(0xFF);
gpio_high(CS);
*x = (int16_t)(buf[0] | (buf[1] << 8)) >> 4; // 12bit 左对齐
*y = (int16_t)(buf[2] | (buf[3] << 8)) >> 4;
*z = (int16_t)(buf[4] | (buf[5] << 8)) >> 4;
}
void lis3dh_init(void) {
if (lis3dh_read_reg(0x0F) != 0x33) return; // WHO_AM_I
lis3dh_write_reg(0x20, 0x57); // CTRL_REG1: 100Hz, XYZ 使能
lis3dh_write_reg(0x23, 0x00); // CTRL_REG4: ±2g
}
5.3 Linux 驱动
dts
&spi0 {
accel@3 {
compatible = "st,lis3dh";
reg = <3>;
spi-max-frequency = <10000000>;
interrupt-parent = <&gpio0>;
interrupts = <8 IRQ_TYPE_EDGE_RISING>;
};
};
- drivers/iio/accel/st_accel_spi.c
- IIO 子系统,/sys/bus/iio/devices/
六、QSPI Flash + XIP 启动
用到知识:QSPI(\[07])、控制器寄存器(\[08])、XIP(\[07])
6.1 启动流程
1. ROM Bootloader → 加载 SPL(单阶段)到 RAM
2. SPL 初始化 QSPI 控制器 + 配 XIP
3. SPL 跳到 XIP 地址,执行 UBoot/Linux(直接从 Flash 取指)
6.2 SPL 中配 QSPI XIP(裸机)
c
void qspi_xip_init(void) {
// 1. 禁用控制器
QSPI->CR &= ~EN;
// 2. 配分频
QSPI->CR = (QSPI->CR & ~PRESCALER) | (3 << PRESCALER_LSB); // PCLK/4
// 3. 配设备(CSHT/CKMODE/FSIZE)
QSPI->DCR = (7 << CSHT_LSB) // CS 高 8 周期
| (0 << CKMODE_LSB) // Mode 0
| (23 << FSIZE_LSB); // 16MB Flash
// 4. 置 QE 位(发 0x06 + 0x01 写状态 bit9)
flash_write_enable();
flash_write_status(0x0200); // QE=1
// 5. 配 XIP 命令(Quad Fast Read 0x6B)
QSPI->CCR = (0x6B << INSTRUCTION_LSB) // 命令
| (1 << IMODE_LSB) // 命令 1 线
| (1 << ADMODE_LSB) // 地址 1 线
| (2 << ADSIZE_LSB) // 24bit 地址
| (3 << DMODE_LSB) // 数据 4 线
| (6 << DCYC_LSB) // 6 dummy
| (3 << FMODE_LSB); // XIP 内存映射
// 6. 使能
QSPI->CR |= EN;
// 7. 开 I-Cache(性能关键)
enable_icache();
// 8. 跳到 XIP 地址执行
void (*entry)(void) = (void*)0x08000000;
entry();
}
6.3 Linux 下 XIP
- 内核配置 CONFIG_SPI_XIP + 链接脚本(内核段映射到 Flash)
- 启动参数传 XIP 地址
- 实际多见 UBoot XIP,内核仍拷到 RAM
嵌入式视角:
平台启动:ROM → SPL(初始化 QSPI + XIP)→ UBoot XIP 执行 → 加载 Linux。SPL 中配 QSPI 控制器 + 置 QE + 配 CCR(XIP 命令)是 BSP 关键代码。配错启动不了。
七、多设备共总线设计
7.1 典型多设备总线
SPI0 控制器
├─ SCK/MOSI/MISO 共享
├─ CS0 → W25Q Flash (Mode 0/3, 50MHz)
├─ CS1 → ILI9341 OLED (Mode 3, 40MHz)
├─ CS2 → MCP3008 ADC (Mode 0, 1MHz)
└─ CS3 → LIS3DH 加速度计 (Mode 0, 10MHz)
7.2 切换设备要重配
c
void spi_select_device(uint8_t cs, uint8_t mode, uint32_t hz) {
// 1. 先抬所有 CS
gpio_high(CS0); gpio_high(CS1); gpio_high(CS2); gpio_high(CS3);
// 2. 重配控制器(模式/速度/位序)
spi_config(mode, hz, MSB_FIRST);
// 3. 拉低目标 CS
gpio_low(CS_BASE + cs);
}
7.3 注意事项
- 各设备模式不同:切换前重配控制器(开销 ~几 μs)
- 各设备速度不同:Flash 50MHz,MCP3008 1MHz,切换要改分频
- 共享 SCK/MOSI/MISO:负载电容累加,高速 SI 受影响(\[02])
- burst 期间不能切设备(会断帧)
7.4 Linux 自动管理
Linux SPI 框架自动管:每次 spi_sync 前根据 spi_device 配置重配控制器。
八、RTOS 集成
8.1 互斥访问
多任务访问 SPI,要互斥:
c
SemaphoreHandle_t spi_mutex;
void spi_init(void) {
spi_mutex = xSemaphoreCreateMutex();
}
int spi_device_xfer(uint8_t cs, uint8_t *tx, uint8_t *rx, int len) {
xSemaphoreTake(spi_mutex, portMAX_DELAY);
spi_select(cs);
int ret = spi_xfer(tx, rx, len);
spi_deselect(cs);
xSemaphoreGive(spi_mutex);
return ret;
}
8.2 任务级缓冲
ISR 不直接处理数据,给信号量,任务处理:
c
void spi_isr(void) {
BaseType_t hp = pdFALSE;
xSemaphoreGiveFromISR(spi_done_sem, &hp);
portYIELD_FROM_ISR(hp);
}
void spi_task(void *arg) {
while (1) {
xSemaphoreTake(spi_done_sem, portMAX_DELAY);
// 处理完成数据
process_rx_data();
}
}
8.3 DMA + 信号量
c
int spi_dma_xfer_rtos(uint8_t *tx, uint8_t *rx, int len) {
xSemaphoreTake(spi_mutex, portMAX_DELAY);
spi_dma_xfer_async(tx, rx, len); // 启动 DMA
xSemaphoreTake(spi_done_sem, portMAX_DELAY); // 等 DMA 完成
xSemaphoreGive(spi_mutex);
return len;
}
九、关键问题 Q&A
Q1:W25Q 页编程为什么不能跨页?
A :Flash 内部页缓冲 256 字节,一次页编程写一页。地址在页内回卷:从 addr 写超过页尾,多余字节回卷到页开头覆盖。软件分页:每次写截断到页尾,下次从新页开头写。
Q2:SD 卡 SPI 初始化为什么必须 400kHz 以下?
A :SD 卡上电后处于识别模式,最高 400kHz。识别完成(CMD41 返回 idle=0)才能提速到 25MHz+。初始化高速会失败(卡没就绪)。
Q3:ILI9341 刷屏怎么最快?
A :DMA + 16bit 字长。240×320×2 = 153600 字节,DMA 传(CPU 不参与),配 16bit 字长(一次 16bit,减少 SCK 数)。40MHz SPI 16bit,理论 10MB/s,刷屏 15ms,30fps 够。
Q4:QSPI XIP 启动,中断向量在 Flash,中断响应慢怎么办?
A :中断向量表和中断处理函数链接到 RAM 段(.ramfunc / .vectors),XIP 时中断从 RAM 取(快)。主程序 XIP 省 RAM,中断放 RAM 保实时。这是 UM680 内存优化的典型做法。
Q5:多设备共总线,Flash 50MHz 和 MCP3008 1MHz,切换开销?
A :每次切换重配分频寄存器(BAUDR),约几 μs。如果频繁切换(Flash 读 + ADC 采集交替),开销累积。优化:分时,一批 Flash 操作做完再做 ADC,减少切换。
Q6:Linux 下 SPI 设备驱动,用现成子系统还是自己写?
A:
- Flash:用 spi-nor(现成,SFDP 自动探测)
- 输入设备(触摸/加速度):用 IIO/input 子系统(现成)
- 显示:用 fbtft/drm(现成)
- 非标准器件:写 spi_driver(设备树 compatible 匹配)
- 调试:spidev(用户态,不用写驱动)
十、实战案例
案例 1:W25Q 读 ID 返回 0xFFFFFF
现象:W25Q 读 ID(0x9F)返回 0xFFFFFF。
分析:① CS 没拉低;② Mode 配错(要 Mode 0/3);③ 发 0x9F 后没发 dummy 读 3 字节;④ Flash 没上电。
解决:查 CS/模式/dummy/上电,读 ID 命令完整发 0x9F + 3 个 0xFF。
案例 2:SD 卡初始化失败
现象:SD 卡 SPI 初始化,CMD0 无响应。
分析:初始化速度过高(> 400kHz),卡没进识别模式。
解决:初始化强制 ≤ 400kHz,发 74+ dummy 时钟,再发 CMD0。
案例 3:ILI9341 花屏
现象:ILI9341 刷屏花,颜色错乱。
分析😄/C 引脚配错,命令被当数据写入显存。
解决:发命令前 D/C=0,发数据前 D/C=1。检查 Mode 3 配置。
案例 4:MCP3008 数值跳变
现象:MCP3008 读 ADC,数值不连续,跳变。
分析:MCP3008 是 LSB first,控制器配 MSB first,位序反。
解决:控制器改 LSB,或软件反转(reverse_bits)。
案例 5:LIS3DH 读 WHO_AM_I 不是 0x33
现象:LIS3DH 读 WHO_AM_I 返回 0xFF 或 0x00。
分析:① 读命令 bit7 没置(写成写);② Mode 错;③ 器件没上电。
解决:读命令 = 0x80 | reg(bit7=1),Mode 0,检查上电。
案例 6:QSPI XIP 启动挂死
现象:N300 平台 QSPI XIP 启动,跳转后挂死。
分析:QE 位没置,DQ2/DQ3 不是数据线,Quad 命令数据全错;或 DCYC dummy 不够,首字节错;或没开 I-Cache,执行极慢看似挂死。
解决:置 QE 位,DCYC=6(W25Q datasheet),开 I-Cache。
案例 7:多设备切换首字节错
现象:Flash(Mode 0)→ OLED(Mode 3)切换后,OLED 首字节错。
分析:切换没重配 CPOL/CPHA,OLED 用了 Flash 的 Mode 0。
解决:每次切换设备前重配控制器模式。
十一、本篇小结与速查
核心要点
- W25Q Flash:0x03读/0x02页编程(不跨页)/0x20扇区擦/0x06写使能/0x05轮询BUSY,写后等5ms+
- SD 卡 SPI:初始化≤400kHz,CMD0/8/55/41,读块 CMD17(512B+CRC)
- ILI9341 OLED:Mode 3,D/C 区分命令数据,设窗口+DMA刷屏(16bit RGB565)
- MCP3008 ADC:Mode 0,LSB first,start+配置+读10bit
- LIS3DH:Mode 0,寄存器帧(bit7=R/W,bit6=多字节),WHO_AM_I=0x33,CTRL_REG1唤醒
- QSPI XIP:置QE → 配CCR(0x6B/位宽/dummy)→ FMODE=3 → 开I-Cache → 跳转
- 多设备共总线:切换前重配模式/速度,burst期间不切
- RTOS:mutex互斥,ISR给信号量,任务处理,DMA+信号量等完成
- Linux:Flash用spi-nor,输入用IIO,显示用fbtft/drm,调试用spidev
器件速查表
| 器件 | 模式 | 速度 | 帧特点 | Linux 驱动 |
|---|---|---|---|---|
| W25Q Flash | 0/3 | 50-104MHz | 命令+地址+数据,QE位 | jedec,spi-nor |
| SD 卡 | 0 | 400k→25MHz | 6字节命令帧,512B块 | mmc-spi-slot |
| ILI9341 | 3 | 40MHz | D/C引脚,RGB565 | ilitek,ili9341 |
| MCP3008 | 0 | 1MHz | LSB first,10bit | microchip,mcp3008 |
| LIS3DH | 0 | 10MHz | 寄存器帧,bit7/6 | st,lis3dh |
W25Q 命令速查
| 命令 | 码 | 用途 |
|---|---|---|
| 读 ID | 0x9F | 0xEF4018 |
| 读 | 0x03 | 标准 |
| 快读 | 0x0B | 高速 |
| 写使能 | 0x06 | WEL |
| 页编程 | 0x02 | ≤256B 不跨页 |
| 扇区擦 | 0x20 | 4KB |
| 读状态 | 0x05 | BUSY |
| Quad Read | 0x6B | QSPI 4线 |
XIP 启动清单
- QSPI 控制器初始化(时钟/分频)
- 置 QE 位(0x06 + 0x01 写状态 bit9)
- 配 CCR(0x6B, IMODE/ADMODE=1线, DMODE=4线, DCYC=6)
- FMODE=3(XIP 内存映射)
- 开 I-Cache
- 链接脚本(代码段映射 Flash 地址)
- 中断向量/热点函数链接 RAM 段
延伸阅读
- \[06-SPI数据帧与位序] ------ 命令帧基础
- \[07-SPI双线四线八线-QSPI-Octal] ------ QSPI XIP
- \[08-SPI控制器寄存器与IP架构] ------ QSPI 寄存器
- \[09-SPI传输模式-轮询-中断-DMA] ------ DMA 刷屏
- \[10-SPI错误处理与恢复] ------ 写后校验
- \[11-SPI Linux驱动框架] ------ Linux 子系统
- \[12-I2C设备驱动实战] ------ 对比 I2C 器件
参考资料
- W25Q Datasheet
- SD Simplified Specification
- ILI9341 / MCP3008 / LIS3DH Datasheet
- Linux drivers/mtd/spi-nor/, drivers/iio/, drivers/gpu/drm/tiny/
- FreeRTOS SPI 驱动示例
SPI 技术系列 00· 总目录 · 上一篇:\[11-SPI Linux驱动框架] · 下一篇:\[13-SPI调试与排查]
✨ 感谢你的阅读 ✨
技术之路漫漫,分享是为了更好地交流。
如果本文的内容对你有启发,希望能得到你的支持:
👍 点赞 | ⭐ 收藏 | 👀 关注
如果你在调试过程中遇到了其他问题,
欢迎在 评论区 💬 留言,我们一起探讨。
关注我,一起交流底层开发的那些事儿!🚀