SPI技术系列 12 · SPI 设备驱动实战

📚 SPI 技术系列 · 持续更新中

本系列从推挽电气层一路打到 Linux spi 子系统,把"四根线"背后的电气/协议/控制器/驱动/实战全拧成一根线。

👉 想系统追更,戳这里查看完整专栏:

【SPI 技术系列】

👉 其他技术系列专栏,欢迎关注、交流、共同成长:

【I2C 技术系列】 【串口技术系列】【RISC-V 架构系列】【Linux内核系列】


本篇定位:协议、寄存器、框架都讲完了,本篇是落地------六大典型器件的完整驱动: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调试与排查]


✨ 感谢你的阅读 ✨

技术之路漫漫,分享是为了更好地交流。

如果本文的内容对你有启发,希望能得到你的支持:

👍 点赞 | ⭐ 收藏 | 👀 关注

如果你在调试过程中遇到了其他问题,

欢迎在 评论区 💬 留言,我们一起探讨。

关注我,一起交流底层开发的那些事儿!🚀


相关推荐
深圳市青牛科技实业有限公司1 小时前
GC4928|8‑28V 带霍尔三相 BLDC 栅极,OA 设备专用驱动芯片
单片机·嵌入式硬件
国科安芯1 小时前
开放指令集的空间之路:RISC-V微控制器在商业航天中的生态价值与自主性评述
嵌入式硬件·mcu·risc-v·低轨卫星·商业航天·抗辐射
Yiran_G1 小时前
从模组接口到平台解析:ZigBee通信正常但数据异常的逐级排查指南
嵌入式硬件·物联网·智能家居
inrichtt1 小时前
第十一课时:串口中断接收与回调函数实战解析
c语言·数据结构·stm32·单片机·嵌入式硬件
不悔哥1 小时前
ESP32上跑 Linux: 乐鑫给 ESP32-S31开放了官方 BSP
linux·嵌入式硬件·开源
潜创微科技1 小时前
CH592:集成 BLE 的青稞 RISC-V 无线 MCU
c语言·单片机·risc-v·无线投屏·低延迟·沁恒
Vegetable_xu1 小时前
高并发系统架构设计实践
系统架构
syagain_zsx1 小时前
WebSocket 教程:websocketpp 写 echo 服务端
linux·网络·c++·websocket·网络协议·工具库
国科安芯1 小时前
星载微控制器MCU选型与系统设计的证据化方法
大数据·数据库·单片机·嵌入式硬件·低轨卫星·商业航天·抗辐射