第 54 天:嵌入式 Linux 核心进阶 ——SPI 总线设备驱动开发

核心目标:从SPI 总线硬件原理Linux SPI 子系统架构出发,拆解嵌入式高速串行总线 ——SPI 的设备驱动开发流程,掌握SPI 子系统核心组件、设备树配置、SPI 全双工数据读写 API三大核心技能,通过W25Q64 SPI Nor Flash经典实战案例,实现从设备树配置→驱动代码编写→交叉编译→应用读写测试的端到端落地,解决 “SPI 通信失败、数据错位、Flash 擦写异常、多设备片选冲突” 等嵌入式开发核心痛点,完成从I2C 低速总线SPI 高速总线的驱动进阶,为后续 LCD、ADC、以太网等高速外设驱动开发奠定基础。

一、本质追问:SPI 总线与 Linux SPI 子系统的核心价值

嵌入式系统中,串行总线分为低速总线(I2C、UART,kbps 级)和高速总线(SPI,MHz 级),其中SPI 总线凭借全双工通信、传输速率高、协议简单的特性,成为嵌入式中大数据量高速传输的首选总线 ——SPI Nor Flash、OLED/LCD 显示屏、高速 ADC、以太网芯片等外设均采用 SPI 接口,掌握 SPI 驱动开发是嵌入式底层开发从 “入门” 到 “进阶” 的核心标志。

1.1 裸机 SPI 开发的致命痛点

在裸机开发中,开发者需要直接操作 SPI 控制器寄存器实现通信(配置时钟极性 / 相位、控制片选、收发数据),这种方式在简单场景下可行,但在 Linux 系统中存在无法解决的问题:

  • 硬件耦合度极高:不同 MCU 的 SPI 控制器寄存器差异巨大,裸机 SPI 代码无法跨平台移植,更换芯片需重写所有通信逻辑;
  • 手动控制片选:SPI 每个设备都有独立的片选引脚,裸机开发需手动控制片选高低电平,多设备时极易出现片选冲突、数据错乱;
  • 全双工处理繁琐:SPI 是全双工通信,发送和接收同时进行,裸机需手动处理收发同步,代码繁琐且易出错;
  • 无速率自适应:不同外设支持的 SPI 速率不同,裸机需针对每个外设单独配置时钟,扩展性差;
  • 无法复用内核资源:裸机 SPI 无法利用内核的 DMA、中断、缓存机制,大数据量传输时 CPU 占用率极高,实时性差。

1.2 SPI 总线核心特性与硬件原理

SPI(Serial Peripheral Interface,串行外设接口)是摩托罗拉推出的高速全双工同步串行总线,专为短距离、高速的芯片间通信设计,其硬件特性是驱动开发的基础。

1.2.1 SPI 总线四线制硬件结构

标准 SPI 采用4 根信号线,所有设备共享时钟、数据收发线,通过独立片选区分设备,硬件连接如下:

表格

信号线全称方向(主机视角)核心作用
SCLKSerial Clock输出串行时钟,由主机提供,控制通信速率与同步节拍
MOSIMaster Out, Slave In输出主机发送、从机接收,主机向从机传输数据
MISOMaster In, Slave Out输入主机接收、从机发送,从机向主机传输数据
CS/SSChip Select / Slave Select输出片选信号,低电平有效,每个从机对应一个独立 CS 引脚

核心特点

  1. 全双工通信:SCLK 的每个时钟周期内,主机和从机同时发送 1 位数据、同时接收 1 位数据,收发同步完成,这是 SPI 与 I2C 最核心的区别(I2C 是半双工);
  2. 单主机多从机:一条 SPI 总线上只能有一个主机,但可挂载多个从机,每个从机有独立的 CS 引脚,主机通过拉低对应 CS 引脚选中目标设备,同一时间仅一个设备处于选中状态;
  3. 速率极高:标准 SPI 速率可达几十 MHz,高速 SPI 甚至可达上百 MHz,远高于 I2C 的 400kbps,适合大数据量传输(如 Flash 读写、屏幕刷新)。
1.2.2 SPI 四种通信模式(核心配置,通信失败高频原因)

SPI 的通信时序由时钟极性(CPOL)时钟相位(CPHA) 两个参数决定,组合形成 4 种标准通信模式,主机和从机必须使用相同的模式才能正常通信,这是 SPI 驱动开发的核心配置,也是通信失败的最常见原因。

表格

SPI 模式CPOL(时钟极性)CPHA(时钟相位)空闲时 SCLK 电平数据采样边沿
模式 000低电平第 1 个时钟上升沿
模式 101低电平第 1 个时钟下降沿
模式 210高电平第 1 个时钟下降沿
模式 311高电平第 1 个时钟上升沿

嵌入式常用模式:绝大多数 SPI 外设(如 W25Qxx Flash、OLED 屏)均使用模式 0(CPOL=0, CPHA=0),驱动开发时需与外设手册严格匹配。

1.2.3 SPI 与 I2C 的核心对比

表格

对比维度SPII2C
通信方式全双工同步半双工同步
信号线数量4 线(SCLK/MOSI/MISO/CS),多设备需增加 CS 线2 线(SDA/SCL),多设备无需额外线
传输速率高速(几十~上百 MHz)低速(100kbps~400kbps)
设备寻址独立片选引脚,无地址7/10 位从机地址,软件寻址
适用场景大数据量高速传输(Flash、屏幕、ADC)低速控制类外设(传感器、RTC、EEPROM)
协议复杂度简单,无应答、无仲裁复杂,有应答、仲裁、校验

核心结论:SPI 主打高速、大数据量,I2C 主打简洁、多设备、控制类,嵌入式开发中根据外设特性选择总线。

1.3 Linux SPI 子系统的核心使命

Linux 内核设计了标准化、分层化、可扩展的 SPI 子系统,其核心使命是 **“屏蔽 SPI 控制器硬件差异,提供标准化全双工通信接口,统一管理总线与片选资源”**,具体价值如下:

  1. 硬件抽象,跨平台移植:内核实现了不同 MCU 的SPI 控制器驱动(如 spi-rockchip、spi-imx),屏蔽了底层寄存器差异,开发者编写 SPI 设备驱动时,只需调用标准化 API,驱动可跨平台移植;
  2. 自动管理片选与时序:内核 SPI 子系统自动控制片选引脚、生成符合配置的时钟时序,开发者无需手动操作片选和寄存器,大幅简化开发;
  3. 全双工传输封装:内核将 SPI 全双工传输封装为标准化 API,开发者只需填充发送和接收缓冲区,内核自动完成同步收发;
  4. 总线资源统一管理:内核统一管理 SPI 总线、片选引脚、时钟速率,避免多设备抢占总线、片选冲突,保证通信稳定性;
  5. 支持 DMA 与中断:高端 SPI 控制器支持 DMA 传输,内核 SPI 子系统封装了 DMA 接口,大数据量传输时可实现零 CPU 占用,提升实时性。

1.4 SPI 驱动开发方式与学习优先级

和 I2C 驱动一致,现代嵌入式 Linux 的 SPI 驱动均采用设备树匹配方式,这是主流开发模式,也是本次教学的核心。

表格

开发方式核心特点匹配原理适用场景学习优先级
设备树匹配(现代)设备树配置 SPI 设备信息,驱动通过 compatible 匹配驱动 compatible 与设备树 compatible 一致则匹配Linux 3.10+,所有主流嵌入式板卡★★★★★
传统板级方式代码中手动注册 SPI 设备,指定总线号、片选、速率驱动根据总线号和片选匹配设备老旧内核、无设备树的定制板卡★★

核心关联:SPI 设备驱动 = Linux SPI 子系统 API(实现高速全双工通信) + 字符设备驱动框架(暴露用户态接口),第 52 天的字符设备知识、第 53 天的总线驱动开发思路可完全复用,仅需掌握 SPI 专属的 API 和配置。

二、内核基础:Linux SPI 子系统核心架构与必备 API

Linux SPI 子系统采用分层架构,与 I2C 子系统设计思路高度一致,开发者只需关注核心层提供的标准化 API,无需关心底层控制器实现。本节拆解 SPI 子系统的三层架构、核心组件,并整理嵌入式开发必备的 SPI 数据读写 API、设备树配置规范。

2.1 Linux SPI 子系统三层架构

SPI 子系统从下到上分为三层,各层职责明确、解耦性强,开发者的工作集中在最上层的设备驱动层。

plaintext

硬件层:SPI控制器(MCU) + SPI外设(Flash/OLED/ADC)
↓
SPI控制器驱动层:spi_master驱动(芯片厂商提供),操作SPI寄存器,实现底层时序和片选控制
↓
SPI核心层:子系统核心,提供标准化API,完成设备与驱动匹配,管理总线资源
↓
SPI设备驱动层:开发者编写,实现外设通信协议,封装为字符设备暴露给用户态
  1. SPI 控制器驱动层

    • 核心作用:驱动硬件 SPI 控制器,实现底层的时钟生成、数据收发、片选控制,支持 DMA / 中断传输;
    • 开发主体:芯片厂商(瑞芯微、NXP、ST)提供,已集成到内核,开发者只需在设备树中启用即可;
    • 核心抽象:struct spi_master,表示一个 SPI 控制器(一条 SPI 总线),包含总线号、时钟范围、片选数量、传输函数等。
  2. SPI 核心层

    • 核心作用:连接控制器层和设备驱动层,提供标准化的 SPI 传输 API,实现设备与驱动的自动匹配,管理总线互斥访问;
    • 开发者视角:无需关注内部实现,只需调用其提供的传输、注册 API 即可。
  3. SPI 设备驱动层

    • 核心作用:针对具体 SPI 外设(如 W25Q64)实现通信协议,封装为字符设备接口供用户态调用;
    • 开发主体:嵌入式开发者,也是本次教学的核心;
    • 核心抽象:struct spi_device(SPI 外设的内核抽象)、struct spi_driver(SPI 驱动的内核抽象)。

2.2 SPI 子系统核心组件

SPI 驱动开发围绕三大核心结构体展开,理解其作用和关联是编写驱动的基础。

2.2.1 struct spi_master:SPI 控制器(总线)抽象

表示一条 SPI 总线,由控制器驱动初始化,包含总线号、最大时钟速率、片选数量、DMA 支持等信息,开发者无需手动操作,内核自动管理。

c

运行

struct spi_master {
    struct device dev;
    s16 bus_num;              // 总线号(如spi0对应bus_num=0)
    u16 num_chipselect;       // 支持的片选数量
    u32 min_speed_hz;         // 最小时钟速率
    u32 max_speed_hz;         // 最大时钟速率
    // 底层传输函数、DMA配置等内核内部字段
};

查看方式:通过ls /sys/bus/spi/devices/可查看系统中的 SPI 总线,如spi0.0表示 spi0 总线的第 0 个片选设备。

2.2.2 struct spi_device:SPI 设备(从机)抽象

表示总线上的一个 SPI 从机设备,由设备树自动创建,包含设备的片选号、时钟速率、SPI 模式、所属控制器等核心信息,是 SPI 数据传输 API 的核心参数。

c

运行

struct spi_device {
    struct device dev;
    struct spi_master *master; // 所属SPI控制器
    u32 max_speed_hz;          // 设备支持的最大时钟速率
    u8 chip_select;            // 片选号(对应控制器的第几个片选)
    u8 mode;                   // SPI模式(CPOL/CPHA组合,如SPI_MODE_0)
    u8 bits_per_word;          // 数据位宽,通常为8位
    const char *modalias;      // 设备别名,用于驱动匹配
};

核心字段max_speed_hzmodechip_select均由设备树配置,驱动中可直接读取使用,内核会自动根据这些参数配置 SPI 控制器。

2.2.3 struct spi_driver:SPI 设备驱动核心结构体

开发者编写 SPI 驱动的核心结构体,包含设备树匹配表、探测 / 移除函数,驱动注册后,内核会根据 compatible 属性与spi_device自动匹配,匹配成功调用probe函数初始化设备。

c

运行

struct spi_driver {
    const struct spi_device_id *id_table; // 传统匹配表
    int (*probe)(struct spi_device *spi); // 设备探测函数(核心)
    int (*remove)(struct spi_device *spi); // 设备移除函数
    void (*shutdown)(struct spi_device *spi);
    const struct of_device_id *of_match_table; // 设备树匹配表(核心)
    struct device_driver driver; // 内核设备模型驱动结构体
};

核心字段

  • of_match_table:设备树匹配表,compatible 字符串需与设备树完全一致;
  • probe:匹配成功后自动调用,完成字符设备注册、外设初始化等;
  • remove:驱动卸载 / 设备移除时调用,释放所有资源。

2.3 嵌入式 SPI 驱动开发必备 API

SPI 驱动 API 分为三类:驱动注册注销 API数据传输核心 API字符设备封装 API(复用字符设备知识),其中数据传输 API是 SPI 全双工通信的核心。

2.3.1 SPI 驱动注册注销 API

struct spi_driver注册到内核 SPI 子系统,自动完成设备匹配,推荐使用快捷宏简化代码。

c

运行

// 注册SPI驱动
int spi_register_driver(struct spi_driver *sdrv);
// 注销SPI驱动
void spi_unregister_driver(struct spi_driver *sdrv);
// 快捷宏:替代module_init/module_exit,自动注册注销
module_spi_driver(driver);

使用示例

c

运行

static struct spi_driver w25q64_driver = {
    .driver = {
        .name = "w25q64",
        .of_match_table = w25q64_of_match,
    },
    .probe = w25q64_probe,
    .remove = w25q64_remove,
};
module_spi_driver(w25q64_driver);
2.3.2 SPI 数据传输核心 API(嵌入式高频使用)

Linux SPI 子系统提供了简化 API高级 API两类传输接口,满足不同复杂度的传输需求,所有 API 均自动控制片选和时序,开发者无需手动操作硬件。

1. 简化 API:适用于简单的半双工传输(常用)

适合普通的读写操作,内核自动封装全双工逻辑,使用简单,嵌入式开发中 80% 的场景都可使用。

c

运行

// 向SPI设备发送数据(只写不读,半双工)
static inline int spi_write(struct spi_device *spi, const void *buf, size_t len);
// 从SPI设备读取数据(只读不写,半双工,发送端自动填充0)
static inline int spi_read(struct spi_device *spi, void *buf, size_t len);
// 先写后读:先发送len_write字节,再读取len_read字节(最常用,如读寄存器、读Flash)
int spi_write_then_read(struct spi_device *spi, const void *txbuf, unsigned n_tx, void *rxbuf, unsigned n_rx);

返回值:成功返回 0,失败返回负的 errno 码。 适用场景spi_write_then_read是最常用的 API,适合 “发指令→读数据” 的场景(如读传感器 ID、读 Flash 数据),自动处理全双工同步。

2. 高级 API:spi_message + spi_transfer(全双工 / 多段传输)

适用于复杂的全双工传输、多段连续传输、大数据量 DMA 传输,灵活性最高,是 SPI 驱动的底层传输机制。

  1. 核心结构体
    • struct spi_transfer:描述一段传输,包含发送缓冲区、接收缓冲区、传输长度、速率等;
    • struct spi_message:传输消息,由一个或多个spi_transfer组成,片选在整个 message 期间保持有效。
  2. 核心 API

c

运行

// 初始化spi_message
void spi_message_init(struct spi_message *m);
// 向spi_message中添加一个spi_transfer
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m);
// 同步执行SPI传输(阻塞等待完成)
int spi_sync(struct spi_device *spi, struct spi_message *message);

使用示例(全双工传输)

c

运行

struct spi_transfer t = {
    .tx_buf = tx_buf,  // 发送缓冲区
    .rx_buf = rx_buf,  // 接收缓冲区
    .len = data_len,   // 传输长度
    .speed_hz = 10000000, // 本次传输速率(可选)
};
struct spi_message msg;
spi_message_init(&msg);
spi_message_add_tail(&t, &msg);
int ret = spi_sync(spi, &msg); // 执行同步传输

嵌入式注意:大数据量传输(如整扇区 Flash 读写)推荐使用spi_sync+spi_transfer,可开启 DMA 传输,大幅降低 CPU 占用。

2.3.3 字符设备封装 API(完全复用)

SPI 驱动最终需向用户态暴露标准化文件接口,完全复用第 52 天的字符设备框架:

  • 设备号管理:alloc_chrdev_region/unregister_chrdev_region
  • 字符设备注册:cdev_init/cdev_add/cdev_del
  • 设备文件创建:class_create/device_create
  • 内核 - 用户态拷贝:copy_from_user/copy_to_user
  • 内存管理:kzalloc/kfree

2.4 SPI 设备树配置规范(嵌入式核心)

SPI 设备的总线归属、片选号、速率、模式均在设备树中配置,内核根据配置自动创建spi_device,配置错误是驱动匹配失败、通信异常的首要原因。

2.4.1 SPI 控制器节点配置(启用总线)

芯片厂商已提供 SPI 控制器节点,开发者只需启用并配置引脚即可,以 RK3328 的 SPI1 为例:

dts

&spi1 {
    status = "okay";          // 启用SPI1总线
    pinctrl-names = "default";
    pinctrl-0 = <&spi1m0_clk &spi1m0_bus>; // SPI引脚配置
    num-cs = <2>;             // 支持2个片选
    cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>, // 片选0:GPIO1_20,低电平有效
               <&gpio1 21 GPIO_ACTIVE_LOW>; // 片选1:GPIO1_21,低电平有效
};

核心字段

  • status = "okay":启用 SPI 控制器;
  • cs-gpios:片选 GPIO 列表,每个片选对应一个 GPIO,配置为低电平有效(SPI 片选默认低有效);
  • pinctrl-0:SPI 引脚复用配置,需确保 SCLK/MOSI/MISO 引脚复用为 SPI 功能。
2.4.2 SPI 设备节点配置(挂载外设到总线)

在启用的 SPI 控制器节点下添加设备子节点,配置 compatible、片选号、速率、模式等,内核自动创建对应的spi_device

dts

&spi1 {
    status = "okay";
    cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;

    // W25Q64 Flash设备节点
    w25q64@0 { // 格式:设备名@片选号
        compatible = "winbond,w25q64"; // 兼容字符串,与驱动一致
        reg = <0>;                     // 片选号,对应cs-gpios的第0个
        spi-max-frequency = <50000000>; // 最大时钟速率50MHz
        spi-cpol = <0>;                // 时钟极性0(CPOL=0)
        spi-cpha = <0>;                // 时钟相位0(CPHA=0),即模式0
        status = "okay";
    };
};

设备节点配置核心规范

  1. 节点挂载:必须作为对应 SPI 控制器节点的子节点;
  2. 节点名格式设备名@片选号(如w25q64@0),数字为片选号,与reg属性一致;
  3. compatible 属性:与驱动中of_match_table的字符串完全一致,大小写敏感;
  4. reg 属性:片选号,从 0 开始,对应cs-gpios的索引;
  5. spi-max-frequency:设备支持的最大时钟速率,单位 Hz,内核会自动选择不超过该值的最高速率;
  6. spi-cpol/spi-cpha:配置 SPI 模式,两个属性都不写默认模式 0;写spi-cpol表示 CPOL=1,写spi-cpha表示 CPHA=1。

三、实战开发:W25Q64 SPI Nor Flash 驱动

本节以嵌入式最通用的W25Q64JV SPI Nor Flash为例,实现完整的 SPI 设备驱动。W25Q64 容量 8MB,广泛用于固件存储、参数存储、日志存储,是嵌入式项目的标配外设,其驱动开发具有极强的代表性和实用性。

3.1 硬件分析:W25Q64 SPI Flash

3.1.1 核心硬件特性
  • 容量:8MB(64Mbit),页大小 256 字节,扇区 4KB,块 64KB;
  • 接口:标准 SPI 四线制,支持模式 0/3,最高时钟速率 80MHz;
  • 指令集:标准 SPI 指令集,包含读 ID、读数据、页编程、扇区擦除、全片擦除等;
  • 耐久性:约 10 万次擦写,数据保存 20 年。
3.1.2 硬件连接

W25Q64 与嵌入式板卡 SPI 总线连接如下:

表格

W25Q64 引脚板卡 SPI 引脚说明
VCC3.3V供电,禁止接 5V
GNDGND接地
CLKSPI1_SCLK串行时钟
DI(MOSI)SPI1_MOSI数据输入(主机发,从机收)
DO(MISO)SPI1_MISO数据输出(主机收,从机发)
/CSGPIO1_20片选,低电平有效,对应 SPI1 的片选 0
/WP、/HOLD3.3V写保护和挂起脚,拉高禁用
3.1.3 核心通信指令(驱动开发核心)

W25Qxx 系列 Flash 采用 “指令 + 地址 + 数据” 的通信格式,驱动开发必须严格遵循官方指令集,本次驱动实现 4 个核心功能对应的指令:

表格

功能指令码格式说明
读 JEDEC ID0x9F发 0x9F → 收 3 字节 ID(厂商 ID + 内存类型 + 容量)
读数据0x03发 0x03 + 3 字节地址 → 连续收 N 字节数据(任意长度)
页编程(写)0x02先发写使能 → 发 0x02 + 3 字节地址 + 最多 256 字节数据
扇区擦除0x20先发写使能 → 发 0x20 + 3 字节扇区地址 → 等待擦除完成
写使能0x06发 0x06,写入 / 擦除前必须先发送
读状态寄存器0x05发 0x05 → 收 1 字节状态,BIT0=1 表示忙

关键注意

  1. 所有写入、擦除操作前必须先发送写使能指令 0x06,否则操作无效;
  2. 页编程单次最多写 256 字节(一页),跨页需分多次写入;
  3. 写入和擦除操作需要时间,需循环读状态寄存器等待操作完成(BIT0 为 0 表示空闲)。

3.2 开发需求分析

实现 W25Q64 SPI Flash 字符设备驱动,满足嵌入式存储场景的核心需求:

  1. 基于设备树匹配,自动识别 W25Q64 并初始化;
  2. 封装为字符设备/dev/w25q64,支持标准文件操作;
  3. 实现核心功能:读 ID、读任意地址数据、页编程写入、扇区擦除;
  4. 自动处理写使能、忙等待、页边界,对上层透明;
  5. 完善的错误处理和资源释放,符合内核编码规范;
  6. 高可移植性,适配所有支持 SPI 子系统的嵌入式板卡。

3.3 设备树配置(以 RK3328 为例)

按照 2.4 节规范,在设备树中启用 SPI1 并添加 W25Q64 设备节点:

dts

&spi1 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&spi1m0_clk &spi1m0_bus>;
    cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>; // 片选0:GPIO1_20

    w25q64@0 {
        compatible = "winbond,w25q64";
        reg = <0>;                     // 片选0
        spi-max-frequency = <50000000>; // 50MHz
        spi-cpol = <0>;
        spi-cpha = <0>;                // SPI模式0
        status = "okay";
    };
};

配置完成后重新编译并烧录设备树,重启板卡。

3.4 完整 W25Q64 驱动代码编写(带详细注释)

驱动代码分为底层通信函数、字符设备接口、SPI 驱动框架三部分,遵循内核编码规范,带完善的错误回滚,可直接使用。

3.4.1 驱动代码:w25q64_drv.c

c

运行

/*************************************************************************
*  嵌入式Linux W25Q64 SPI Nor Flash 设备驱动
*  核心:SPI子系统API + 字符设备驱动框架 + 设备树匹配
*  功能:read读Flash、write写Flash、ioctl擦除/读ID
*  设备树:compatible = "winbond,w25q64",reg = <0>
*************************************************************************/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/kzalloc.h>
#include <linux/of.h>
#include <linux/err.h>
#include <linux/delay.h>
#include <linux/ioctl.h>

/************************** 1. 宏定义:驱动与W25Q64配置 **************************/
// 字符设备配置
#define W25Q_DEV_NAME    "w25q64"
#define W25Q_DEV_CNT     1
// Flash参数
#define W25Q_PAGE_SIZE    256       // 页大小256字节
#define W25Q_SECTOR_SIZE  4096      // 扇区大小4KB
#define W25Q_TOTAL_SIZE   (8*1024*1024) // 总容量8MB
// W25Q指令集
#define W25Q_CMD_WRITE_ENABLE  0x06  // 写使能
#define W25Q_CMD_READ_STATUS   0x05  // 读状态寄存器
#define W25Q_CMD_READ_DATA     0x03  // 读数据
#define W25Q_CMD_PAGE_PROG     0x02  // 页编程
#define W25Q_CMD_SECTOR_ERASE  0x20  // 扇区擦除
#define W25Q_CMD_JEDEC_ID      0x9F  // 读JEDEC ID
// IOCTL命令
#define W25Q_IOCTL_READ_ID     _IOR('W', 0x01, u32)   // 读ID
#define W25Q_IOCTL_ERASE_SECTOR _IOW('W', 0x02, u32)  // 按地址擦除扇区

/************************** 2. 全局变量 **************************/
static dev_t w25q_dev_num;
static struct cdev w25q_cdev;
static struct class *w25q_class;
static struct spi_device *w25q_spi; // SPI设备句柄

/************************** 3. W25Q底层通信函数 **************************/
/**
 * @brief  等待Flash操作完成(读状态寄存器,BIT0=0为空闲)
 */
static int w25q_wait_ready(void)
{
    u8 cmd = W25Q_CMD_READ_STATUS;
    u8 status;
    int ret;
    int timeout = 1000; // 超时1秒

    do {
        // 发读状态指令,读1字节状态
        ret = spi_write_then_read(w25q_spi, &cmd, 1, &status, 1);
        if (ret < 0)
            return ret;
        if (!(status & 0x01)) // BIT0=0,空闲
            return 0;
        udelay(100); // 等待100us再查
    } while (timeout--);

    printk(KERN_ERR "W25Q wait ready timeout\n");
    return -ETIMEDOUT;
}

/**
 * @brief  发送写使能指令
 */
static int w25q_write_enable(void)
{
    u8 cmd = W25Q_CMD_WRITE_ENABLE;
    int ret = spi_write(w25q_spi, &cmd, 1);
    if (ret < 0)
        printk(KERN_ERR "W25Q write enable failed: %d\n", ret);
    return ret;
}

/**
 * @brief  读取JEDEC ID(3字节:厂商+类型+容量)
 * @param  id: 输出3字节ID,存在u32的低3字节
 */
static int w25q_read_jedec_id(u32 *id)
{
    u8 cmd = W25Q_CMD_JEDEC_ID;
    u8 buf[3];
    int ret;

    ret = spi_write_then_read(w25q_spi, &cmd, 1, buf, 3);
    if (ret < 0) {
        printk(KERN_ERR "W25Q read ID failed: %d\n", ret);
        return ret;
    }
    *id = (buf[0] << 16) | (buf[1] << 8) | buf[2];
    printk(KERN_INFO "W25Q JEDEC ID: 0x%06X\n", *id);
    return 0;
}

/**
 * @brief  从指定地址读取Flash数据
 * @param  addr: 24位起始地址
 * @param  buf: 接收数据缓冲区
 * @param  len: 读取长度
 */
static int w25q_read_data(u32 addr, u8 *buf, u32 len)
{
    u8 tx_buf[4]; // 指令+3字节地址
    int ret;

    if (addr + len > W25Q_TOTAL_SIZE)
        return -EINVAL;

    // 构造发送数据:1字节指令 + 3字节地址(高字节在前)
    tx_buf[0] = W25Q_CMD_READ_DATA;
    tx_buf[1] = (addr >> 16) & 0xFF;
    tx_buf[2] = (addr >> 8) & 0xFF;
    tx_buf[3] = addr & 0xFF;

    // 先发指令地址,再读数据
    ret = spi_write_then_read(w25q_spi, tx_buf, 4, buf, len);
    if (ret < 0)
        printk(KERN_ERR "W25Q read data failed: %d\n", ret);
    return ret;
}

/**
 * @brief  页编程:向指定地址写入一页(最多256字节,不跨页)
 * @param  addr: 起始地址(必须页对齐)
 * @param  buf: 写入数据
 * @param  len: 写入长度(<=256)
 */
static int w25q_page_program(u32 addr, const u8 *buf, u32 len)
{
    u8 *tx_buf;
    int ret;

    if (len > W25Q_PAGE_SIZE)
        return -EINVAL;

    // 分配发送缓冲区:1字节指令 + 3字节地址 + len字节数据
    tx_buf = kzalloc(4 + len, GFP_KERNEL);
    if (!tx_buf)
        return -ENOMEM;

    // 构造发送数据
    tx_buf[0] = W25Q_CMD_PAGE_PROG;
    tx_buf[1] = (addr >> 16) & 0xFF;
    tx_buf[2] = (addr >> 8) & 0xFF;
    tx_buf[3] = addr & 0xFF;
    memcpy(tx_buf + 4, buf, len);

    // 1. 写使能
    ret = w25q_write_enable();
    if (ret < 0)
        goto out;

    // 2. 发送页编程指令+地址+数据
    ret = spi_write(w25q_spi, tx_buf, 4 + len);
    if (ret < 0)
        goto out;

    // 3. 等待写入完成
    ret = w25q_wait_ready();

out:
    kfree(tx_buf);
    return ret;
}

/**
 * @brief  擦除指定地址所在的扇区(4KB)
 * @param  addr: 扇区内任意地址
 */
static int w25q_erase_sector(u32 addr)
{
    u8 tx_buf[4];
    int ret;

    if (addr >= W25Q_TOTAL_SIZE)
        return -EINVAL;

    // 构造指令+扇区地址
    tx_buf[0] = W25Q_CMD_SECTOR_ERASE;
    tx_buf[1] = (addr >> 16) & 0xFF;
    tx_buf[2] = (addr >> 8) & 0xFF;
    tx_buf[3] = addr & 0xFF;

    // 1. 写使能
    ret = w25q_write_enable();
    if (ret < 0)
        return ret;

    // 2. 发送扇区擦除指令
    ret = spi_write(w25q_spi, tx_buf, 4);
    if (ret < 0)
        return ret;

    // 3. 等待擦除完成
    return w25q_wait_ready();
}

/************************** 4. 字符设备接口实现 **************************/
static int w25q_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "W25Q device opened\n");
    return 0;
}

static int w25q_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "W25Q device closed\n");
    return 0;
}

/**
 * @brief  read:从当前偏移读取Flash数据
 */
static ssize_t w25q_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
    u32 addr = *pos;
    u8 *kernel_buf;
    int ret;

    if (addr >= W25Q_TOTAL_SIZE)
        return 0; // 读到末尾
    if (addr + count > W25Q_TOTAL_SIZE)
        count = W25Q_TOTAL_SIZE - addr;

    // 分配内核缓冲区
    kernel_buf = kzalloc(count, GFP_KERNEL);
    if (!kernel_buf)
        return -ENOMEM;

    // 读取Flash数据
    ret = w25q_read_data(addr, kernel_buf, count);
    if (ret < 0)
        goto out;

    // 拷贝到用户态
    if (copy_to_user(buf, kernel_buf, count)) {
        ret = -EFAULT;
        goto out;
    }

    // 更新文件偏移
    *pos += count;
    ret = count;

out:
    kfree(kernel_buf);
    return ret;
}

/**
 * @brief  write:向当前偏移写入Flash数据(自动分页处理)
 */
static ssize_t w25q_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
    u32 addr = *pos;
    u8 *kernel_buf;
    u32 written = 0;
    int ret;

    if (addr >= W25Q_TOTAL_SIZE)
        return -ENOSPC;
    if (addr + count > W25Q_TOTAL_SIZE)
        count = W25Q_TOTAL_SIZE - addr;

    kernel_buf = kzalloc(count, GFP_KERNEL);
    if (!kernel_buf)
        return -ENOMEM;

    // 从用户态拷贝数据
    if (copy_from_user(kernel_buf, buf, count)) {
        ret = -EFAULT;
        goto out;
    }

    // 分页写入,处理页边界
    while (written < count) {
        u32 page_off = addr % W25Q_PAGE_SIZE;
        u32 chunk = W25Q_PAGE_SIZE - page_off; // 当前页剩余可写字节数
        if (chunk > count - written)
            chunk = count - written;

        ret = w25q_page_program(addr, kernel_buf + written, chunk);
        if (ret < 0)
            goto out;

        addr += chunk;
        written += chunk;
    }

    *pos += written;
    ret = written;

out:
    kfree(kernel_buf);
    return ret;
}

/**
 * @brief  ioctl:控制命令(读ID、擦除扇区)
 */
static long w25q_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    u32 val;
    int ret = 0;

    switch (cmd) {
        case W25Q_IOCTL_READ_ID:
            ret = w25q_read_jedec_id(&val);
            if (ret < 0)
                return ret;
            if (copy_to_user((u32 __user *)arg, &val, sizeof(val)))
                return -EFAULT;
            break;
        case W25Q_IOCTL_ERASE_SECTOR:
            if (copy_from_user(&val, (u32 __user *)arg, sizeof(val)))
                return -EFAULT;
            ret = w25q_erase_sector(val);
            break;
        default:
            return -EINVAL;
    }
    return ret;
}

/************************** 5. file_operations初始化 **************************/
static const struct file_operations w25q_fops = {
    .owner = THIS_MODULE,
    .open = w25q_open,
    .release = w25q_release,
    .read = w25q_read,
    .write = w25q_write,
    .unlocked_ioctl = w25q_ioctl,
    .llseek = generic_file_llseek, // 支持文件偏移定位
};

/************************** 6. SPI驱动核心:匹配表+probe/remove **************************/
static const struct of_device_id w25q_of_match[] = {
    { .compatible = "winbond,w25q64" },
    { /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, w25q_of_match);

static int w25q_probe(struct spi_device *spi)
{
    u32 id;
    int ret;

    printk(KERN_INFO "W25Q probe: bus=%d, cs=%d, max_speed=%dHz\n",
           spi->master->bus_num, spi->chip_select, spi->max_speed_hz);
    w25q_spi = spi;

    // 读取JEDEC ID,验证硬件
    ret = w25q_read_jedec_id(&id);
    if (ret < 0) {
        printk(KERN_ERR "W25Q probe failed: cannot read ID\n");
        return ret;
    }

    // 1. 申请字符设备号
    ret = alloc_chrdev_region(&w25q_dev_num, 0, W25Q_DEV_CNT, W25Q_DEV_NAME);
    if (ret < 0)
        goto err_alloc;

    // 2. 注册字符设备
    cdev_init(&w25q_cdev, &w25q_fops);
    w25q_cdev.owner = THIS_MODULE;
    ret = cdev_add(&w25q_cdev, w25q_dev_num, W25Q_DEV_CNT);
    if (ret < 0)
        goto err_cdev;

    // 3. 创建设备类和设备文件
    w25q_class = class_create(THIS_MODULE, W25Q_DEV_NAME "_class");
    if (IS_ERR(w25q_class)) {
        ret = PTR_ERR(w25q_class);
        goto err_class;
    }
    device_create(w25q_class, NULL, w25q_dev_num, NULL, W25Q_DEV_NAME);

    printk(KERN_INFO "W25Q driver init success: /dev/%s\n", W25Q_DEV_NAME);
    return 0;

err_class:
    cdev_del(&w25q_cdev);
err_cdev:
    unregister_chrdev_region(w25q_dev_num, W25Q_DEV_CNT);
err_alloc:
    return ret;
}

static int w25q_remove(struct spi_device *spi)
{
    device_destroy(w25q_class, w25q_dev_num);
    class_destroy(w25q_class);
    cdev_del(&w25q_cdev);
    unregister_chrdev_region(w25q_dev_num, W25Q_DEV_CNT);
    w25q_spi = NULL;
    printk(KERN_INFO "W25Q driver remove success\n");
    return 0;
}

/************************** 7. SPI驱动注册与模块信息 **************************/
static struct spi_driver w25q64_driver = {
    .driver = {
        .name = W25Q_DEV_NAME,
        .of_match_table = w25q_of_match,
        .owner = THIS_MODULE,
    },
    .probe = w25q_probe,
    .remove = w25q_remove,
};
module_spi_driver(w25q64_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("W25Q64 SPI Nor Flash Driver for Embedded Linux");
MODULE_AUTHOR("Embedded Developer");
MODULE_VERSION("V1.0");
3.4.2 代码核心亮点
  1. 协议分层清晰:底层 SPI 通信函数、业务逻辑函数、字符设备接口完全分离,可轻松移植到其他 SPI Flash(如 W25Q128、GD25Q 系列);
  2. 自动处理边界:写入操作自动分页处理,对上层应用透明,无需关心页大小;
  3. 完善的时序处理:写入 / 擦除前自动发写使能,操作后自动等待空闲,保证数据可靠;
  4. 标准文件接口:支持read/write/seek/ioctl标准 POSIX 接口,应用可像操作普通文件一样操作 Flash;
  5. 硬件验证机制:probe 时读取 JEDEC ID 验证硬件是否正常,提前发现硬件问题。

3.5 Makefile 编写

复用通用驱动 Makefile,仅修改模块名,适配交叉编译:

makefile

obj-m += w25q64_drv.o

KERNELDIR ?= /home/developer/linux-5.10.61
CROSS_COMPILE ?= arm-linux-gnueabihf-
ARCH ?= arm

PWD := $(shell pwd)

all:
	@echo "Compiling W25Q64 SPI driver for $(ARCH)..."
	$(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean
	rm -rf .*.cmd *.mod.c *.o *.ko .tmp_versions modules.order Module.symvers

install:
	scp w25q64_drv.ko root@192.168.1.100:/opt/drv/

执行make即可交叉编译出 ARM 架构的w25q64_drv.ko

四、驱动加载测试与应用开发

4.1 板卡端环境准备

  1. 确认设备树生效:重启板卡后,查看 SPI 总线设备:

    bash

    运行

    ls /sys/bus/spi/devices/
    
    输出spi1.0表示 spi1 总线的第 0 个片选设备已创建。
  2. 确认 SPI 子系统已加载

    bash

    运行

    lsmod | grep spi
    
    输出对应控制器驱动(如spi_rockchip)表示正常。

4.2 驱动加载与匹配验证

bash

运行

# 加载驱动
insmod w25q64_drv.ko
# 查看内核日志
dmesg | grep W25Q

成功标志:内核日志输出 probe 信息和 JEDEC ID(W25Q64 为 0xEF4017):

plaintext

[1234.567890] W25Q probe: bus=1, cs=0, max_speed=50000000Hz
[1234.567901] W25Q JEDEC ID: 0xEF4017
[1234.567912] W25Q driver init success: /dev/w25q64

查看设备文件:

bash

运行

ls -l /dev/w25q64

4.3 驱动功能测试

4.3.1 读 ID 测试

编写简单的 ioctl 测试程序,或使用内核日志验证。

4.3.2 读写数据测试

bash

运行

# 从Flash地址0读取16字节,以十六进制显示
dd if=/dev/w25q64 bs=16 count=1 2>/dev/null | hexdump -C

擦除 + 写入 + 读回验证

bash

运行

# 1. 擦除第0扇区(地址0)
# (可通过ioctl测试程序实现,此处先以驱动验证为主)
# 2. 写入测试数据
echo "Hello SPI Flash!" > /tmp/test.bin
dd if=/tmp/test.bin of=/dev/w25q64 bs=16 count=1 conv=notrunc
# 3. 读回验证
dd if=/dev/w25q64 bs=16 count=1 2>/dev/null

输出Hello SPI Flash!表示读写功能正常。

4.4 用户态测试应用开发

c

运行

// w25q_test.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>

#define W25Q_DEV "/dev/w25q64"
#define W25Q_IOCTL_READ_ID     _IOR('W', 0x01, unsigned int)
#define W25Q_IOCTL_ERASE_SECTOR _IOW('W', 0x02, unsigned int)

int main()
{
    int fd = open(W25Q_DEV, O_RDWR);
    if (fd < 0) {
        perror("open failed");
        return -1;
    }

    // 1. 读ID
    unsigned int id;
    ioctl(fd, W25Q_IOCTL_READ_ID, &id);
    printf("W25Q JEDEC ID: 0x%06X\n", id);

    // 2. 擦除第0扇区
    unsigned int addr = 0;
    printf("Erasing sector at 0x%X...\n", addr);
    ioctl(fd, W25Q_IOCTL_ERASE_SECTOR, &addr);

    // 3. 写入数据
    char wbuf[] = "Embedded Linux SPI Flash Test";
    lseek(fd, 0, SEEK_SET);
    write(fd, wbuf, strlen(wbuf));
    printf("Write: %s\n", wbuf);

    // 4. 读回数据
    char rbuf[64] = {0};
    lseek(fd, 0, SEEK_SET);
    read(fd, rbuf, sizeof(rbuf));
    printf("Read back: %s\n", rbuf);

    close(fd);
    return 0;
}

交叉编译后运行,可完整验证 Flash 的擦、写、读功能。

五、SPI 驱动开发常见问题排查

5.1 驱动匹配失败,probe 不执行

现象:加载驱动后无 probe 日志,无设备文件。 排查

  1. 检查驱动与设备树的compatible字符串是否完全一致;
  2. 检查 SPI 设备节点是否挂在正确的 SPI 控制器节点下;
  3. 确认 SPI 控制器节点status = "okay",设备树已重新烧录生效。

5.2 读 ID 全为 0xFF/0x00,通信失败

现象:probe 时读 ID 为 0xFFFFFF 或 0x000000。 排查

  1. 检查 SPI 模式(CPOL/CPHA)是否与外设一致,绝大多数 Flash 为模式 0;
  2. 检查硬件接线:SCLK/MOSI/MISO 是否接反,供电是否为 3.3V;
  3. 检查片选 GPIO 配置是否正确,电平是否为低有效;
  4. 降低spi-max-frequency,速率过高也会导致通信失败。

5.3 读写数据错位 / 乱码

现象:能读到数据,但内容不对,存在位偏移。 排查

  1. 确认 SPI 模式配置正确,CPOL/CPHA 与外设手册匹配;
  2. 检查bits_per_word是否为 8 位;
  3. 确认时钟速率未超过外设最大值。

5.4 写入 / 擦除无效

现象:写入后读回数据不变,擦除后仍为原数据。 排查

  1. 检查写入 / 擦除前是否发送了写使能指令 0x06,这是最常见原因;
  2. 检查是否等待操作完成(读状态寄存器 BIT0),未等待就执行下一步会导致操作失效;
  3. 检查 WP 引脚是否拉高,WP 拉低会进入写保护状态。

5.5 片选不动作,多设备冲突

现象:多设备时选中一个设备另一个也响应,或片选始终为高 / 低。 排查

  1. 检查设备树cs-gpios配置是否正确,GPIO 编号、有效电平是否匹配;
  2. 确认片选 GPIO 未被其他外设占用;
  3. 确认 SPI 控制器驱动支持 GPIO 片选,部分控制器仅支持硬件片选。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值