核心目标:从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 根信号线,所有设备共享时钟、数据收发线,通过独立片选区分设备,硬件连接如下:
表格
| 信号线 | 全称 | 方向(主机视角) | 核心作用 |
|---|---|---|---|
| SCLK | Serial Clock | 输出 | 串行时钟,由主机提供,控制通信速率与同步节拍 |
| MOSI | Master Out, Slave In | 输出 | 主机发送、从机接收,主机向从机传输数据 |
| MISO | Master In, Slave Out | 输入 | 主机接收、从机发送,从机向主机传输数据 |
| CS/SS | Chip Select / Slave Select | 输出 | 片选信号,低电平有效,每个从机对应一个独立 CS 引脚 |
核心特点:
- 全双工通信:SCLK 的每个时钟周期内,主机和从机同时发送 1 位数据、同时接收 1 位数据,收发同步完成,这是 SPI 与 I2C 最核心的区别(I2C 是半双工);
- 单主机多从机:一条 SPI 总线上只能有一个主机,但可挂载多个从机,每个从机有独立的 CS 引脚,主机通过拉低对应 CS 引脚选中目标设备,同一时间仅一个设备处于选中状态;
- 速率极高:标准 SPI 速率可达几十 MHz,高速 SPI 甚至可达上百 MHz,远高于 I2C 的 400kbps,适合大数据量传输(如 Flash 读写、屏幕刷新)。
1.2.2 SPI 四种通信模式(核心配置,通信失败高频原因)
SPI 的通信时序由时钟极性(CPOL) 和时钟相位(CPHA) 两个参数决定,组合形成 4 种标准通信模式,主机和从机必须使用相同的模式才能正常通信,这是 SPI 驱动开发的核心配置,也是通信失败的最常见原因。
表格
| SPI 模式 | CPOL(时钟极性) | CPHA(时钟相位) | 空闲时 SCLK 电平 | 数据采样边沿 |
|---|---|---|---|---|
| 模式 0 | 0 | 0 | 低电平 | 第 1 个时钟上升沿 |
| 模式 1 | 0 | 1 | 低电平 | 第 1 个时钟下降沿 |
| 模式 2 | 1 | 0 | 高电平 | 第 1 个时钟下降沿 |
| 模式 3 | 1 | 1 | 高电平 | 第 1 个时钟上升沿 |
嵌入式常用模式:绝大多数 SPI 外设(如 W25Qxx Flash、OLED 屏)均使用模式 0(CPOL=0, CPHA=0),驱动开发时需与外设手册严格匹配。
1.2.3 SPI 与 I2C 的核心对比
表格
| 对比维度 | SPI | I2C |
|---|---|---|
| 通信方式 | 全双工同步 | 半双工同步 |
| 信号线数量 | 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 控制器硬件差异,提供标准化全双工通信接口,统一管理总线与片选资源”**,具体价值如下:
- 硬件抽象,跨平台移植:内核实现了不同 MCU 的SPI 控制器驱动(如 spi-rockchip、spi-imx),屏蔽了底层寄存器差异,开发者编写 SPI 设备驱动时,只需调用标准化 API,驱动可跨平台移植;
- 自动管理片选与时序:内核 SPI 子系统自动控制片选引脚、生成符合配置的时钟时序,开发者无需手动操作片选和寄存器,大幅简化开发;
- 全双工传输封装:内核将 SPI 全双工传输封装为标准化 API,开发者只需填充发送和接收缓冲区,内核自动完成同步收发;
- 总线资源统一管理:内核统一管理 SPI 总线、片选引脚、时钟速率,避免多设备抢占总线、片选冲突,保证通信稳定性;
- 支持 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设备驱动层:开发者编写,实现外设通信协议,封装为字符设备暴露给用户态
-
SPI 控制器驱动层
- 核心作用:驱动硬件 SPI 控制器,实现底层的时钟生成、数据收发、片选控制,支持 DMA / 中断传输;
- 开发主体:芯片厂商(瑞芯微、NXP、ST)提供,已集成到内核,开发者只需在设备树中启用即可;
- 核心抽象:
struct spi_master,表示一个 SPI 控制器(一条 SPI 总线),包含总线号、时钟范围、片选数量、传输函数等。
-
SPI 核心层
- 核心作用:连接控制器层和设备驱动层,提供标准化的 SPI 传输 API,实现设备与驱动的自动匹配,管理总线互斥访问;
- 开发者视角:无需关注内部实现,只需调用其提供的传输、注册 API 即可。
-
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_hz、mode、chip_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 驱动的底层传输机制。
- 核心结构体
struct spi_transfer:描述一段传输,包含发送缓冲区、接收缓冲区、传输长度、速率等;struct spi_message:传输消息,由一个或多个spi_transfer组成,片选在整个 message 期间保持有效。
- 核心 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";
};
};
设备节点配置核心规范:
- 节点挂载:必须作为对应 SPI 控制器节点的子节点;
- 节点名格式:
设备名@片选号(如w25q64@0),数字为片选号,与reg属性一致; - compatible 属性:与驱动中
of_match_table的字符串完全一致,大小写敏感; - reg 属性:片选号,从 0 开始,对应
cs-gpios的索引; - spi-max-frequency:设备支持的最大时钟速率,单位 Hz,内核会自动选择不超过该值的最高速率;
- 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 引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 供电,禁止接 5V |
| GND | GND | 接地 |
| CLK | SPI1_SCLK | 串行时钟 |
| DI(MOSI) | SPI1_MOSI | 数据输入(主机发,从机收) |
| DO(MISO) | SPI1_MISO | 数据输出(主机收,从机发) |
| /CS | GPIO1_20 | 片选,低电平有效,对应 SPI1 的片选 0 |
| /WP、/HOLD | 3.3V | 写保护和挂起脚,拉高禁用 |
3.1.3 核心通信指令(驱动开发核心)
W25Qxx 系列 Flash 采用 “指令 + 地址 + 数据” 的通信格式,驱动开发必须严格遵循官方指令集,本次驱动实现 4 个核心功能对应的指令:
表格
| 功能 | 指令码 | 格式说明 |
|---|---|---|
| 读 JEDEC ID | 0x9F | 发 0x9F → 收 3 字节 ID(厂商 ID + 内存类型 + 容量) |
| 读数据 | 0x03 | 发 0x03 + 3 字节地址 → 连续收 N 字节数据(任意长度) |
| 页编程(写) | 0x02 | 先发写使能 → 发 0x02 + 3 字节地址 + 最多 256 字节数据 |
| 扇区擦除 | 0x20 | 先发写使能 → 发 0x20 + 3 字节扇区地址 → 等待擦除完成 |
| 写使能 | 0x06 | 发 0x06,写入 / 擦除前必须先发送 |
| 读状态寄存器 | 0x05 | 发 0x05 → 收 1 字节状态,BIT0=1 表示忙 |
关键注意:
- 所有写入、擦除操作前必须先发送写使能指令 0x06,否则操作无效;
- 页编程单次最多写 256 字节(一页),跨页需分多次写入;
- 写入和擦除操作需要时间,需循环读状态寄存器等待操作完成(BIT0 为 0 表示空闲)。
3.2 开发需求分析
实现 W25Q64 SPI Flash 字符设备驱动,满足嵌入式存储场景的核心需求:
- 基于设备树匹配,自动识别 W25Q64 并初始化;
- 封装为字符设备
/dev/w25q64,支持标准文件操作; - 实现核心功能:读 ID、读任意地址数据、页编程写入、扇区擦除;
- 自动处理写使能、忙等待、页边界,对上层透明;
- 完善的错误处理和资源释放,符合内核编码规范;
- 高可移植性,适配所有支持 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 代码核心亮点
- 协议分层清晰:底层 SPI 通信函数、业务逻辑函数、字符设备接口完全分离,可轻松移植到其他 SPI Flash(如 W25Q128、GD25Q 系列);
- 自动处理边界:写入操作自动分页处理,对上层应用透明,无需关心页大小;
- 完善的时序处理:写入 / 擦除前自动发写使能,操作后自动等待空闲,保证数据可靠;
- 标准文件接口:支持
read/write/seek/ioctl标准 POSIX 接口,应用可像操作普通文件一样操作 Flash; - 硬件验证机制: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 板卡端环境准备
- 确认设备树生效:重启板卡后,查看 SPI 总线设备:
bash
运行
输出ls /sys/bus/spi/devices/spi1.0表示 spi1 总线的第 0 个片选设备已创建。 - 确认 SPI 子系统已加载:
bash
运行
输出对应控制器驱动(如lsmod | grep spispi_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 日志,无设备文件。 排查:
- 检查驱动与设备树的
compatible字符串是否完全一致; - 检查 SPI 设备节点是否挂在正确的 SPI 控制器节点下;
- 确认 SPI 控制器节点
status = "okay",设备树已重新烧录生效。
5.2 读 ID 全为 0xFF/0x00,通信失败
现象:probe 时读 ID 为 0xFFFFFF 或 0x000000。 排查:
- 检查 SPI 模式(CPOL/CPHA)是否与外设一致,绝大多数 Flash 为模式 0;
- 检查硬件接线:SCLK/MOSI/MISO 是否接反,供电是否为 3.3V;
- 检查片选 GPIO 配置是否正确,电平是否为低有效;
- 降低
spi-max-frequency,速率过高也会导致通信失败。
5.3 读写数据错位 / 乱码
现象:能读到数据,但内容不对,存在位偏移。 排查:
- 确认 SPI 模式配置正确,CPOL/CPHA 与外设手册匹配;
- 检查
bits_per_word是否为 8 位; - 确认时钟速率未超过外设最大值。
5.4 写入 / 擦除无效
现象:写入后读回数据不变,擦除后仍为原数据。 排查:
- 检查写入 / 擦除前是否发送了写使能指令 0x06,这是最常见原因;
- 检查是否等待操作完成(读状态寄存器 BIT0),未等待就执行下一步会导致操作失效;
- 检查 WP 引脚是否拉高,WP 拉低会进入写保护状态。
5.5 片选不动作,多设备冲突
现象:多设备时选中一个设备另一个也响应,或片选始终为高 / 低。 排查:
- 检查设备树
cs-gpios配置是否正确,GPIO 编号、有效电平是否匹配; - 确认片选 GPIO 未被其他外设占用;
- 确认 SPI 控制器驱动支持 GPIO 片选,部分控制器仅支持硬件片选。

1244

被折叠的 条评论
为什么被折叠?



