SPI子系统:从协议时序到设备驱动开发的完整实战
SPI是嵌入式里仅次于I2C的高速串行总线,Flash、显示屏、ADC、IMU全靠它,和I2C不同,SPI是全双工、没有应答机制,速度快但线多,写SPI驱动搞不清四种模式、片选逻辑、传输API,出来的就是一堆乱码和超时。本文基于Linux 6.8内核,把SPI协议、子系统架构、设备树、驱动开发、传输API、控制器驱动、调试方法串一遍。
一、SPI协议基础
四线制全双工
SPI典型四根线:
| 信号线 | 方向 | 作用 |
|---|---|---|
| SCLK | 主→从 | 时钟信号,主机驱动 |
| MOSI | 主→从 | 主出从入,主机发送数据 |
| MISO | 从→主 | 主入从出,从机发送数据 |
| CS(SS) | 主→从 | 片选,低电平选中从机 |
四线制是典型配置,实际MISO可以不用(只写不读的设备),CS也可以用GPIO软件控制。但SCLK和MOSI必须有。
全双工是SPI和I2C最大的区别:发送和接收同时进行,每个时钟周期MOSI和MISO各传一位。这意味着读操作必须同时写(写dummy字节),写操作必须同时读(丢弃接收数据)。
四种模式:CPOL/CPHA组合
SPI没有统一标准,不同器件的时钟极性和相位可能不同。CPOL决定空闲时SCLK电平,CPHA决定采样边沿。
| 模式 | CPOL | CPHA | 空闲SCLK | 采样边沿 | 典型器件 |
|:-----|:-----|:-----|:---------|:---------|:---------|
| Mode 0 | 0 | 0 | 低电平 | 上升沿采样 | W25Q Flash、多数传感器 |
| Mode 1 | 0 | 1 | 低电平 | 下降沿采样 | 部分ADC |
| Mode 2 | 1 | 0 | 高电平 | 下降沿采样 | 少数器件 |
| Mode 3 | 1 | 1 | 高电平 | 上升沿采样 | SD卡(SPI模式)、部分显示 |
Mode 0用得最多,90%的SPI器件都是Mode 0。主从双方必须配置相同的模式,否则数据全乱。
传输速率
SPI没有协议层面的速率限制,速率取决于时钟频率:
| 场景 | 典型频率 | 说明 |
|---|---|---|
| 低速传感器 | 1~5MHz | IMU、ADC等 |
| SPI Flash | 20~80MHz | W25Q系列,高速读操作 |
| SPI显示 | 10~50MHz | TFT LCD、OLED |
| 极限速率 | 100MHz+ | 短走线、低电容负载 |
实际速率受走线长度、负载电容、信号完整性限制,板子上走线超过10cm还想50MHz,基本不现实。
二、SPI子系统架构
Linux SPI子系统分三层,和I2C一样:
各层职责
SPI设备驱动*(我们写的部分)。
- 实现spi_driver的probe和remove
- 通过spi_sync、spi_async、spi_write等API与从机通信
- 通过of_device_id匹配设备树中的设备节。
SPI核心*(内核提供)。
- 管理spi_driver和spi_device的注册与匹配
- 向上提供统一的spi_sync/spi_async等数据传输API
- 向下调用控制器驱动的transfer_one
- spi_message和spi_transfer的组装与调度
SPI控制器驱动层(SoC厂商提供):
- 操作控制器硬件寄存器,产生SCLK/MOSI/CS信号
- 实现
transfer_one或transfer_one_message回调 - 处理DMA、中断、CS控制等底层细。
6.8变更:
spi_master已改名为spi_controller。旧代码里看到的spi_master在新内核里等价于spi_controller,spi_alloc_master对应spi_alloc_host。写新驱动直接用新名字。
驱动开发者绝大多数时候只写设备驱动层,控制器驱动由SoC厂商已经写好。
三、设备树配置
SPI设备要在设备树里声明,内核才能创建spi_device并匹配驱动。
SPI控制器节。
SoC的设备树通常已经定义好了SPI控制器节点:
/* SoC级设备树(厂商提供) */
spi0: spi@fe610000 {
compatible = "rockchip,rk3568-spi";
reg = <0x0 0xfe610000 0x0 0x1000>;
clocks = <&cru CLK_SPI0>;
interrupts = <GIC_SPI 52 IRQ_TYPE_LEVEL_HIGH>;
#address-cells = <1>;
#size-cells = <0>;
status = "disabled";
};
SPI设备子节。
在板级设备树中使能控制器并添加从设备。
/* 板级设备*/
&spi0 {
status = "okay";
max-frequency = <50000000>; /* 控制器最50MHz */
/* W25Q32 SPI Flash,片选 */
flash@0 {
compatible = "jedec,spi-nor";
reg = <0>; /* 片选号,对应CS0 */
spi-max-frequency = <50000000>; /* 设备最50MHz */
spi-cpol; /* CPOL=1 */
spi-cpha; /* CPHA=1,Mode 3 */
};
/* ILI9341显示控制器,片选 */
display@1 {
compatible = "ilitek,ili9341";
reg = <1>; /* 片选号,对应CS1 */
spi-max-frequency = <20000000>; /* 设备最50MHz */
spi-cs-high; /* CS高电平有效(罕见)*/
rotation = <90>;
};
};
关键字段说明。
| 字段 | 说明 |
|:-----|:-----|
| reg | 片选编号,0=CS00=CS1,依此类推 |
| spi-max-frequency | 设备支持的最大时钟频率,Hz |
| spi-cpol | 存在则CPOL=1,不存在则CPOL=0 |
| spi-cpha | 存在则CPHA=1,不存在则CPHA=0 |
| spi-cs-high | 存在则CS高电平有效,默认低电平有效 |
| spi-3wire | 存在则三线制模式(MOSI/MISO合并 |
完整DTS示例
/* spi-board.dtsi - SPI Flash + 显示控制*/
&spi0 {
status = "okay";
max-frequency = <50000000>;
pinctrl-names = "default";
pinctrl-0 = <&spi0m0_pins>;
/* W25Q64 SPI NOR Flash,片。,Mode 0 */
flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <80000000>;
/* Mode 0: 不设spi-cpol和spi-cpha */
};
/* ST7789V TFT显示,片。,Mode 0 */
display@1 {
compatible = "sitronix,st7789v";
reg = <1>;
spi-max-frequency = <30000000>;
dc-gpios = <&gpio4 22 GPIO_ACTIVE_HIGH>; /* 数据/命令选择 */
reset-gpios = <&gpio4 23 GPIO_ACTIVE_LOW>; /* 复位引脚 */
rotation = <90>;
width = <240>;
height = <320>;
};
};
dc-gpios是SPI显示控制器特有的,用来区分数据字节和命令字节(SPI协议本身没有这个信号,靠额外GPIO实现)。reset-gpios控制芯片硬件复位。
四、SPI设备驱动开销
spi_driver结构建
#include <linux/spi/spi.h>
#include <linux/module.h>
/* 设备私有数据 */
struct w25q_data {
struct spi_device *spi;
struct mutex lock;
u8 cmd_buf[4]; /* 命令缓冲*/
};
spi_device_id + MODULE_DEVICE_TABLE
SPI驱动有两种匹配方式,和I2C一样都要写。
/* 方式1:spi_device_id - 传统非设备树匹配 */
static const struct spi_device_id w25q_id[] = {
{ "w25q32", 0 },
{ "w25q64", 1 },
{ "w25q128", 2 },
{ }
};
MODULE_DEVICE_TABLE(spi, w25q_id);
/* 方式2:of_device_id - 设备树匹配(优先*/
static const struct of_device_id w25q_of_match[] = {
{ .compatible = "jedec,spi-nor" },
{ }
};
MODULE_DEVICE_TABLE(of, w25q_of_match);
MODULE_DEVICE_TABLE宏把匹配表导出到模块信息,modprobe根据它自动加载对应驱动。
probe/remove
static int w25q_probe(struct spi_device *spi)
{
struct w25q_data *data;
/* 检查控制器是否支持所需模式 */
if (spi->mode & SPI_MODE_0)
dev_info(&spi->dev, "using SPI Mode 0\n");
data = devm_kzalloc(&spi->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->spi = spi;
mutex_init(&data->lock);
spi_set_drvdata(spi, data);
dev_info(&spi->dev, "W25Q SPI Flash probed\n");
return 0;
}
static void w25q_remove(struct spi_device *spi)
{
/* devm资源自动释放,无需手动清理 */
dev_info(&spi->dev, "W25Q SPI Flash removed\n");
}
完整代码示例:SPI Flash读取ID
// w25q_id.c - W25Q SPI Flash读取JEDEC ID(Linux 6.8。#include <linux/spi/spi.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/delay.h>
#define W25Q_CMD_JEDEC_ID 0x9F
struct w25q_data {
struct spi_device *spi;
struct mutex lock;
};
/* 读取JEDEC ID:厂商+类型+容量 */
static int w25q_read_jedec_id(struct w25q_data *data, u8 *id)
{
struct spi_device *spi = data->spi;
int ret;
mutex_lock(&data->lock);
/*
* spi_write_then_read:先字节命令,再字节ID
* 这是SPI Flash读ID最常用的方。 */
ret = spi_write_then_read(spi, &W25Q_CMD_JEDEC_ID, 1, id, 3);
mutex_unlock(&data->lock);
return ret;
}
/* 读取状态寄存器1 */
static int w25q_read_sr1(struct w25q_data *data)
{
u8 cmd = 0x05; /* RDSR命令 */
u8 val;
int ret;
ret = spi_write_then_read(data->spi, &cmd, 1, &val, 1);
if (ret < 0)
return ret;
return val;
}
static int w25q_probe(struct spi_device *spi)
{
struct w25q_data *data;
u8 id[3];
int ret, sr1;
/* 设置SPI模式(如果设备树没配,代码里也可以改*/
spi->mode = SPI_MODE_0;
spi->max_speed_hz = 50000000; /* 50MHz */
ret = spi_setup(spi);
if (ret < 0) {
dev_err(&spi->dev, "spi_setup failed: %d\n", ret);
return ret;
}
data = devm_kzalloc(&spi->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->spi = spi;
mutex_init(&data->lock);
spi_set_drvdata(spi, data);
/* 读取JEDEC ID */
ret = w25q_read_jedec_id(data, id);
if (ret < 0) {
dev_err(&spi->dev, "read JEDEC ID failed: %d\n", ret);
return ret;
}
dev_info(&spi->dev, "JEDEC ID: %02x %02x %02x\n",
id[0], id[1], id[2]);
/* Winbond: id[0]=0xEF, id[1]=0x40, id[2]=0x16(W25Q32) */
/* 读状态寄存器验证 */
sr1 = w25q_read_sr1(data);
if (sr1 < 0) {
dev_err(&spi->dev, "read SR1 failed: %d\n", sr1);
return sr1;
}
dev_info(&spi->dev, "SR1=0x%02x, BUSY=%d, WEL=%d\n",
sr1, sr1 & 0x01, (sr1 >> 1) & 0x01);
return 0;
}
static void w25q_remove(struct spi_device *spi)
{
dev_info(&spi->dev, "W25Q removed\n");
}
static const struct spi_device_id w25q_id[] = {
{ "w25q32", 0 },
{ "w25q64", 1 },
{ "w25q128", 2 },
{ }
};
MODULE_DEVICE_TABLE(spi, w25q_id);
static const struct of_device_id w25q_of_match[] = {
{ .compatible = "jedec,spi-nor" },
{ }
};
MODULE_DEVICE_TABLE(of, w25q_of_match);
static struct spi_driver w25q_driver = {
.driver = {
.name = "w25q",
.of_match_table = w25q_of_match,
},
.probe = w25q_probe,
.remove = w25q_remove,
.id_table = w25q_id,
};
module_spi_driver(w25q_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("linuxros");
MODULE_DESCRIPTION("W25Q SPI Flash ID reader driver");
module_spi_driver宏展开后自动注册/注销驱动,不用手写module_init和module_exit。
五、SPI传输API
SPI传输的核心概念是spi_message和spi_transfer。一个message包含一个或多个transfer,同一个message里的transfer之间CS保持低电平不释放。
spi_transfer结构建
struct spi_transfer {
const void *tx_buf; /* 发送缓冲区 */
void *rx_buf; /* 接收缓冲*/
unsigned len; /* 传输字节数(tx和rx相同*/
unsigned speed_hz; /* 本次传输时钟频率0=使用默认 */
u16 bits_per_word; /* 每字比特数,0=使用默认8bit */
u16 delay_usecs; /* 传输后延迟(微秒*/
u8 cs_change; /* 传输后释放CS0=释放0=保持*/
/* ... */
};
SPI是全双工,tx_buf和rx_buf同时工作。只写时rx_buf设NULL,只读时tx_buf设NULL(控制器会自动发0xFF)。
spi_message + spi_transfer_list_add_tail + spi_sync/spi_async
/* 组装一次完整传输:写命令+读数据*/
static int w25q_read_data(struct w25q_data *data,
u32 addr, u8 *buf, int len)
{
struct spi_device *spi = data->spi;
struct spi_message msg;
struct spi_transfer xfer_cmd, xfer_data;
u8 cmd[4]; /* 命令+24位地址 */
int ret;
/* 构造读命令:0x03 + 3字节地址 */
cmd[0] = 0x03;
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
spi_message_init(&msg);
/* 第一段:发送命令+地址,CS保持 */
memset(&xfer_cmd, 0, sizeof(xfer_cmd));
xfer_cmd.tx_buf = cmd;
xfer_cmd.len = 4;
xfer_cmd.cs_change = 0; /* CS保持低电*/
spi_message_add_tail(&xfer_cmd, &msg);
/* 第二段:读取数据,CS释放 */
memset(&xfer_data, 0, sizeof(xfer_data));
xfer_data.rx_buf = buf;
xfer_data.len = len;
xfer_data.cs_change = 1; /* 传输完释放CS */
spi_message_add_tail(&xfer_data, &msg);
/* 同步传输,阻塞等待完*/
ret = spi_sync(spi, &msg);
if (ret < 0)
dev_err(&spi->dev, "read failed: %d\n", ret);
return ret;
}
spi_sync是同步版本,阻塞等待传输完成。spi_async是异步版本,传输完成后调用完成回调:
static void w25q_async_complete(void *context)
{
struct w25q_data *data = context;
/* 传输完成,处理接收数*/
dev_dbg(&data->spi->dev, "async transfer complete\n");
complete(&data->done); /* 通知等待*/
}
/* 异步传输 */
static int w25q_read_async(struct w25q_data *data,
u32 addr, u8 *buf, int len)
{
struct spi_message msg;
struct spi_transfer xfer;
u8 cmd[4];
cmd[0] = 0x03;
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
spi_message_init(&msg);
msg.complete = w25q_async_complete;
msg.context = data;
memset(&xfer, 0, sizeof(xfer));
xfer.tx_buf = cmd;
xfer.rx_buf = buf;
xfer.len = 4 + len;
spi_message_add_tail(&xfer, &msg);
return spi_async(data->spi, &msg);
}
异步传输适合大数据量场景,避免长时间阻塞当前线程。但大部分驱动用spi_sync就够了。
spi_write / spi_read / spi_write_then_read:简化API
内核封装了几个快捷函数,内部都是组装spi_message再调spi_sync。
/* 纯写:发送buf的len字节 */
int ret = spi_write(spi, buf, len);
/* 纯读:接收len字节到buf */
int ret = spi_read(spi, buf, len);
/* 先写后读:发n_tx字节,再读n_rx字节 */
int ret = spi_write_then_read(spi, tx_buf, n_tx, rx_buf, n_rx);
spi_write_then_read是最常用的,读寄存器、读ID这类"发命令+读结果的操作一行搞定。它内部会做CS保持——写和读之间CS不释放。
传输方式对比
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
spi_sync + 手动组装 |
多段传输、CS控制 | 最灵活,精确控制CS | 代码多 |
spi_async |
大数据量、非阻塞 | 不阻塞调用线程 | 需要完成回调,复杂 |
spi_write |
纯写操作 | 一行代码 | 只能写 |
spi_read |
纯读操作 | 一行代码 | 只能读,发dummy字节 |
spi_write_then_read |
发命令读结果 | 最常用,简单 | 写和读之间不能插延迟 |
实际开发中80%的场景用spi_write_then_read搞定,剩下20%用spi_sync手动组装。
六、SPI控制器驱动
spi_controller + spi_controller_mem_ops
6.8内核中,SPI控制器用spi_controller描述(旧名spi_master),核心回调是transfer_one。
struct spi_controller {
struct device dev;
struct list_head list;
u16 bus_num; /* 总线编号 */
u16 num_chipselect; /* 片选数*/
u32 mode_bits; /* 支持的模式位掩码 */
u32 min_speed_hz;
u32 max_speed_hz;
u16 bits_per_word_mask;
/* 核心回调 */
int (*transfer_one)(struct spi_controller *ctlr,
struct spi_device *spi,
struct spi_transfer *xfer);
void (*handle_err)(struct spi_controller *ctlr,
struct spi_message *msg);
/* SPI Memory操作集.8新接口) */
const struct spi_controller_mem_ops *mem_ops;
/* ... */
};
/* SPI Memory操作,用于SPI NOR/NAND */
struct spi_controller_mem_ops {
int (*exec_op)(struct spi_controller *ctlr,
const struct spi_mem_op *op);
const char *(*get_name)(struct spi_controller *ctlr);
int (*dirmap_create)(struct spi_controller *ctlr,
const struct spi_mem_dirmap_info *info);
void (*dirmap_destroy)(struct spi_controller *ctlr,
struct spi_mem_dirmap_desc *desc);
ssize_t (*dirmap_read)(struct spi_controller *ctlr,
const struct spi_mem_dirmap_desc *desc,
u64 offs, size_t len, void *buf);
ssize_t (*dirmap_write)(struct spi_controller *ctlr,
const struct spi_mem_dirmap_desc *desc,
u64 offs, size_t len, const void *buf);
};
spi_controller_mem_ops6.8为SPI Memory(NOR/NAND Flash)新增的优化接口。传统的spi_transfer方式对Flash操作效率低——每次读都要组装message。mem_ops直接执行Flash操作命令,支持直接映射(dirmap),让Flash读取走DMA,省掉了message组装的开销。
何时需要写控制器驱动
*绝大多数情况不需要 SoC厂商已经在主线内核或BSP里提供了控制器驱动:
| SoC | 控制器驱动文件 | compatible |
|---|---|---|
| RK3568 | drivers/spi/spi-rockchip.c |
rockchip,rk3568-spi |
| i.MX6 | drivers/spi/spi-imx.c |
fsl,imx6q-ecspi |
| STM32MP | drivers/spi/spi-stm32.c |
st,stm32h7-spi |
| Allwinner | drivers/spi/spi-sun6i.c |
allwinner,sun8i-h3-spi |
需要自己写控制器驱动的场景:- 自研FPGA实现SPI控制。- 使用冷门SoC,主线内核没有支。- 需要优化Flash读取性能(实现mem_ops的dirmap。
七、SPI vs I2C对比
| 维度 | SPI | I2C |
|---|---|---|
| 信号线数 | 4线(SCLK/MOSI/MISO/CS | 2线(SDA/SCL |
| 通信方式 | 全双工 | 半双工 |
| 传输速率 | 几十MHz | 100kbps~5Mbps |
| 寻址方式 | 硬件片选(CS线) | 软件地址。/10位) |
| 多从 | 每个从机一根CS | 共享总线,地址区分 |
| 应答机制 | ACK/NACK | |
| 传输距离 | 短(<30cm | 短(<1m |
| 功能 | 较高 | 较低 |
| 硬件复杂 | 线多,布线麻 | 线少,布线简 |
| 适用场景 | Flash、显示、高速ADC | 传感器、EEPROM、PMIC |
选型原则:要速度上SPI,要省线用I2C。一个板子上通常两种都有——Flash和屏走SPI,传感器和电源芯片走I2C。
八、调试技。
spi-tools
SPI版的i2c-tools,用于用户空间调试SPI通信。
# 安装
sudo apt install spi-tools
# 查看SPI总线信息
spi-config --device /dev/spidev0.0 --list-devices
# 配置SPI参数
spi-config --device /dev/spidev0.0 \
--mode 0 \
--speed 1000000 \
--bits-per-word 8
# 发送数据并接收
spi-transfer --device /dev/spidev0.0 \
--speed 1000000 \
--mode 0 \
--tx-data 9F,00,00,00 \
--rx-size 3
# 返回Flash的JEDEC ID
逻辑分析仪抓。
SPI调试比I2C更依赖逻辑分析仪——因为SPI没有应答机制,数据错了不会NACK告诉你,只能看波形:
- Saleae Logic / sigrok:抓取SCLK/MOSI/MISO/CS信号,自动解码SPI协议
- *关注。 - CS是否在传输前拉低、传输后拉高
- SCLK空闲电平是否和CPOL配置一。 - 数据采样边沿是否正确(CPHA=0上升沿采样,CPHA=1下降沿采样)
- MOSI数据是否和预期一。 - 时钟频率是否在设备支持范围内
- 两次传输之间CS是否意外释放
spidev用户空间驱动
内核提供了spidev驱动,让用户空间程序直接操作SPI设备。
/* 设备树中启用spidev */
&spi0 {
status = "okay";
spidev@0 {
compatible = "rohm,dh2228fv"; /* spidev兼容字符号*/
reg = <0>;
spi-max-frequency = <10000000>;
};
};
用户空间C代码。
// spidev_test.c - 用户空间SPI读写
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
int main(void)
{
int fd = open("/dev/spidev0.0", O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
/* 配置SPI参数 */
unsigned char mode = SPI_MODE_0;
unsigned int speed = 10000000; /* 10MHz */
unsigned char bits = 8;
ioctl(fd, SPI_IOC_WR_MODE, &mode);
ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
/* 全双工传输:发JEDEC ID命令,读3字节 */
unsigned char tx[] = { 0x9F, 0x00, 0x00, 0x00 };
unsigned char rx[4] = { 0 };
struct spi_ioc_transfer xfer = {
.tx_buf = (unsigned long)tx,
.rx_buf = (unsigned long)rx,
.len = 4,
.speed_hz = speed,
.bits_per_word = bits,
};
ioctl(fd, SPI_IOC_MESSAGE(1), &xfer);
printf("JEDEC ID: %02x %02x %02x\n", rx[1], rx[2], rx[3]);
close(fd);
return 0;
}
spidev适合快速验证硬件通信,不适合生产环境——没有设备模型、没有电源管理、没有并发控制。正式产品还是得写内核驱动。
九、常见问题
Q1:SPI通信数据错位。
数据整体偏移了几位,读出来的值和预期差一个bit位置,这是模式配错:
- CPOL/CPHA不匹配——最常见的原因。对照器件手册确认模式,设备树里
spi-cpol和spi-cpha要和器件一致。2. MSB/LSB顺序反了**——SPI默认MSB first,少数器件是LSB first,设备树加spi-lsb-first - CS时序不对——有些器件要求CS拉低后等一段时间再发时钟,用
spi_transfer.delay_usecs加延。
# 查看当前SPI设备配置
cat /sys/bus/spi/devices/spi0.0/modalias
cat /sys/bus/spi/devices/spi0.0/max_speed_hz
Q2:spi_sync返回-ETIMEDOUT。
超时说明控制器发了时钟但没完成传输:
- *CS未正确拉——检查CS引脚是否被其他驱动占用,或者GPIO配置不对
- 时钟频率过高——降频试试,有些器件标称50MHz实际只能稳定50MHz
- 控制器驱动bug——DMA地址映射错误、中断丢失等,查dmesg
- 走线太长——信号完整性差,高频时钟变形,降频或缩短走。
# 查看SPI控制器状态cat /sys/class/spi_master/spi0/statistics/*
Q3:SPI Flash写入失败。
SPI Flash写入有特殊时序要求,不是直接写就行的。
1. 没发写使能命令——Flash每次写操作前必须发WREN(0x06)命令,否则写入被忽略。2. 写入时Busy位未等待——写操作需要时间(典型1~5ms),必须轮询状态寄存器的BUSY位。3. 跨页写入**——Flash按页写(256字节/页),跨页地址会回卷到页首,数据被覆盖
/* Flash写入正确流程 */
static int w25q_write_enable(struct w25q_data *data)
{
u8 cmd = 0x06; /* WREN */
return spi_write(data->spi, &cmd, 1);
}
static int w25q_wait_busy(struct w25q_data *data)
{
int sr1;
int retries = 100;
do {
sr1 = w25q_read_sr1(data);
if (sr1 < 0)
return sr1;
if (!(sr1 & 0x01)) /* BUSY位清*/
return 0;
usleep_range(100, 200);
} while (retries-- > 0);
return -ETIMEDOUT;
}
static int w25q_write_page(struct w25q_data *data,
u32 addr, const u8 *buf, int len)
{
int ret;
/* 1. 写使*/
ret = w25q_write_enable(data);
if (ret < 0)
return ret;
/* 2. 发页编程命令+数据 */
/* ... spi_sync组装PP命令(0x02)+地址+数据 ... */
/* 3. 等待写入完成 */
return w25q_wait_busy(data);
}
十、总结
SPI驱动开发速查看
| 环节 | 关键字 |
|---|---|
| 协议 | 四线全双工,CPOL/CPHA四种模式,Mode 0最常用 |
| 子系统架 | 设备驱动→核心层→控制器驱动,三层解 |
| 设备 | reg填片选号,spi-max-frequency设速率,spi-cpol/spi-cpha配模块 |
| 驱动注册 | spi_driver+of_device_id+spi_device_id,module_spi_driver |
| 数据传输 | 优先spi_write_then_read,复杂时序用spi_sync手动组装 |
| 控制器驱动 | SoC厂商提供。.8用spi_controller替代spi_master |
| 调试 | spi-tools读写验证,逻辑分析仪看波形,spidev用户空间快速测 |
| 常见故障 | 数据错位→CPOL/CPHA配错,超时→CS/频率问题,Flash写失败→缺WREN |
本文首发于linuxros.cn,转载请注明出处。