3、SPI设备驱动实战:编写SPI字符设备驱动、使用spidev用户空间接口、SPI Flash驱动案例分析
SPI总线驱动,说白了就是让CPU和那些挂在SPI总线上的外设能正常说话。我做了这么多年驱动,SPI设备驱动这块踩过的坑最多。今天咱们就把它彻底讲透。
3.1 SPI设备驱动的整体架构
先看一张图,把SPI设备驱动的层次结构理清楚。你想想看,一个SPI设备要正常工作,需要哪几层?
这张图我画了很多遍。每次给团队新人讲SPI驱动,我都会先让他们看这个层次。最底层是控制器驱动,跟具体芯片绑定;中间是SPI核心层,Linux内核已经帮你写好了;上面才是我们要写的设备驱动。
3.2 编写SPI字符设备驱动
先说说最直接的方式——写一个字符设备驱动。为什么选字符设备?因为SPI设备本质上就是顺序读写,跟字符设备天然匹配。
3.2.1 驱动框架搭建
一个完整的SPI字符设备驱动,核心就这几步:
- 注册SPI设备驱动:用
spi_register_driver()告诉内核我能管哪些设备 - 分配主次设备号:用
alloc_chrdev_region()或者静态指定 - 初始化字符设备:用
cdev_init()绑定 file_operations - 实现读写接口:在
open/read/write/ioctl里操作SPI - 注销清理:模块卸载时别忘了释放资源
核心要点:SPI字符设备驱动的灵魂在 file_operations 结构体。你实现的每一个回调函数,都是用户空间和硬件之间的桥梁。
3.2.2 关键代码实现
我直接贴一段我项目里用过的框架代码。注意看注释,这些都是血泪教训。
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define SPI_DEVICE_NAME "my_spi_dev"
#define SPI_CLASS_NAME "spi_char"
#define DEVICE_MINOR_NUM 0
static struct spi_device *g_spi;
static struct class *spi_class;
static struct cdev spi_cdev;
static dev_t dev_num;
/* 打开设备 */
static int spi_open(struct inode *inode, struct file *filp)
{
filp->private_data = g_spi;
return 0;
}
/* 读取数据:从SPI设备读回len字节 */
static ssize_t spi_read(struct file *filp, char __user *buf,
size_t len, loff_t *off)
{
struct spi_device *spi = filp->private_data;
u8 *rx_buf;
int ret;
rx_buf = kmalloc(len, GFP_KERNEL);
if (!rx_buf)
return -ENOMEM;
/* 发送全0来读取从设备数据 */
ret = spi_write_then_read(spi, NULL, 0, rx_buf, len);
if (ret < 0) {
kfree(rx_buf);
return ret;
}
if (copy_to_user(buf, rx_buf, len)) {
kfree(rx_buf);
return -EFAULT;
}
kfree(rx_buf);
return len;
}
/* 写入数据:向SPI设备发送len字节 */
static ssize_t spi_write(struct file *filp, const char __user *buf,
size_t len, loff_t *off)
{
struct spi_device *spi = filp->private_data;
u8 *tx_buf;
int ret;
tx_buf = memdup_user(buf, len);
if (IS_ERR(tx_buf))
return PTR_ERR(tx_buf);
ret = spi_write(spi, tx_buf, len);
kfree(tx_buf);
return ret < 0 ? ret : len;
}
/* 控制接口:配置SPI模式、速度、位数等 */
static long spi_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
struct spi_device *spi = filp-private_data;
u32 tmp;
switch (cmd) {
case SPI_IOC_RD_MODE:
tmp = spi->mode;
if (copy_to_user((u32 __user *)arg, &tmp, sizeof(tmp)))
return -EFAULT;
break;
case SPI_IOC_WR_MODE:
if (copy_from_user(&tmp, (u32 __user *)arg, sizeof(tmp)))
return -EFAULT;
spi->mode = tmp & SPI_MODE_X_MASK;
break;
/* 其他命令:SPI_IOC_RD_LSB_FIRST, SPI_IOC_WR_MAX_SPEED_HZ 等 */
default:
return -ENOTTY;
}
return 0;
}
static struct file_operations spi_fops = {
.owner = THIS_MODULE,
.open = spi_open,
.read = spi_read,
.write = spi_write,
.unlocked_ioctl = spi_ioctl,
};
/* SPI设备探测回调 */
static int spi_probe(struct spi_device *spi)
{
int ret;
g_spi = spi;
/* 分配设备号 */
ret = alloc_chrdev_region(&dev_num, DEVICE_MINOR_NUM, 1, SPI_DEVICE_NAME);
if (ret < 0)
return ret;
/* 初始化字符设备 */
cdev_init(&spi_cdev, &spi_fops);
spi_cdev.owner = THIS_MODULE;
ret = cdev_add(&spi_cdev, dev_num, 1);
if (ret)
goto err_cdev;
/* 创建设备节点 */
spi_class = class_create(THIS_MODULE, SPI_CLASS_NAME);
if (IS_ERR(spi_class)) {
ret = PTR_ERR(spi_class);
goto err_class;
}
device_create(spi_class, NULL, dev_num, NULL, SPI_DEVICE_NAME);
dev_info(&spi->dev, "SPI char device registered, major=%d\n",
MAJOR(dev_num));
return 0;
err_class:
cdev_del(&spi_cdev);
err_cdev:
unregister_chrdev_region(dev_num, 1);
return ret;
}
static int spi_remove(struct spi_device *spi)
{
device_destroy(spi_class, dev_num);
class_destroy(spi_class);
cdev_del(&spi_cdev);
unregister_chrdev_region(dev_num, 1);
dev_info(&spi->dev, "SPI char device removed\n");
return 0;
}
static struct spi_driver my_spi_driver = {
.driver = {
.name = SPI_DEVICE_NAME,
.owner = THIS_MODULE,
},
.probe = spi_probe,
.remove = spi_remove,
};
module_spi_driver(my_spi_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("SPI Character Device Driver Example");
我的经验:spi_write_then_read() 这个函数很好用,但要注意它内部会做两次传输。如果你对时序要求极高,建议用 spi_message + spi_transfer 自己组装。我曾经在一个ADC采集项目里,就因为用了 spi_write_then_read 导致采样率上不去,换成消息队列后直接翻倍。
3.3 使用spidev用户空间接口
说实话,很多时候我们不需要自己写内核驱动。Linux内核已经提供了一个通用的SPI用户空间接口——spidev。你想想看,如果只是读个传感器、写个LCD,何必折腾内核模块?
3.3.1 spidev是什么
spidev 是内核自带的一个SPI设备驱动,它把SPI总线操作暴露成 /dev/spidevX.Y 设备节点。用户空间程序可以直接通过 open/read/write/ioctl 操作SPI设备。
| 设备节点 | 含义 |
|---|---|
/dev/spidev0.0 |
SPI控制器0,片选0 |
/dev/spidev0.1 |
SPI控制器0,片选1 |
/dev/spidev1.0 |
SPI控制器1,片选0 |
3.3.2 用户空间编程示例
下面是一个用spidev读写SPI Flash ID的C程序。代码不长,但包含了所有关键步骤。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
#define SPI_DEVICE "/dev/spidev0.0"
#define SPI_SPEED 1000000 /* 1MHz */
static void dump_buf(const char *name, const uint8_t *buf, size_t len)
{
printf("%s: ", name);
for (size_t i = 0; i < len; i++)
printf("%02x ", buf[i]);
printf("\n");
}
int main(void)
{
int fd;
int ret;
uint8_t mode = SPI_MODE_0;
uint8_t bits = 8;
uint32_t speed = SPI_SPEED;
/* 1. 打开设备 */
fd = open(SPI_DEVICE, O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
/* 2. 配置SPI模式 */
ret = ioctl(fd, SPI_IOC_WR_MODE, &mode);
if (ret < 0) { perror("WR_MODE"); close(fd); return 1; }
/* 3. 配置位数 */
ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
if (ret < 0) { perror("WR_BITS"); close(fd); return 1; }
/* 4. 配置速度 */
ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
if (ret < 0) { perror("WR_SPEED"); close(fd); return 1; }
printf("SPI configured: mode=%d, bits=%d, speed=%d Hz\n",
mode, bits, speed);
/* 5. 发送读ID命令(0x9F) */
uint8_t tx[] = {0x9F, 0x00, 0x00, 0x00};
uint8_t rx[4] = {0};
struct spi_ioc_transfer tr = {
.tx_buf = (unsigned long)tx,
.rx_buf = (unsigned long)rx,
.len = sizeof(tx),
.speed_hz = speed,
.delay_usecs = 0,
.bits_per_word = bits,
.cs_change = 0,
};
ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
if (ret < 0) {
perror("SPI_IOC_MESSAGE");
close(fd);
return 1;
}
dump_buf("TX", tx, sizeof(tx));
dump_buf("RX", rx, sizeof(rx));
printf("Flash ID: 0x%02x%02x%02x\n", rx[1], rx[2], rx[3]);
close(fd);
return 0;
}
注意:使用spidev时,spi_ioc_transfer 结构体里的 tx_buf 和 rx_buf 是用户空间地址,内核会自动帮你做拷贝。但 len 不能超过内核的缓冲区限制,默认一般是4096字节。我曾经在传输大块数据时踩过这个坑,返回 EMSGSIZE 错误,查了半天才发现是长度超了。
3.4 SPI Flash驱动案例分析
好了,前面都是热身。现在咱们来啃一个硬骨头——SPI Flash驱动。为什么选它?因为SPI Flash太常见了,几乎所有嵌入式板子上都有。而且它包含了SPI设备驱动的所有典型要素:命令协议、状态轮询、页编程、擦除操作。
3.4.1 SPI Flash的协议基础
SPI Flash的协议其实很简单。你想想看,它就是一个用SPI接口访问的存储芯片。常用的命令就那么几个:
| 命令 | 操作码 | 说明 |
|---|---|---|
| READ ID | 0x9F | 读取厂商ID和设备ID |
| READ | 0x03 | 读取数据,后面跟3字节地址 |
| WRITE ENABLE | 0x06 | 写使能,写操作前必须发 |
| PAGE PROGRAM | 0x02 | 页编程,一次最多256字节 |
| SECTOR ERASE | 0x20 | 扇区擦除,4KB为单位 |
| READ STATUS | 0x05 | 读取状态寄存器,检查忙标志 |
3.4.2 驱动实现要点
写SPI Flash驱动,最核心的就是处理好 写操作的状态轮询。为什么?因为Flash写入和擦除都需要时间,你必须等它忙完才能发下一个命令。
我直接说关键代码片段:
/* 等待Flash空闲 */
static int spi_flash_wait_ready(struct spi_device *spi)
{
uint8_t cmd = 0x05; /* READ STATUS */
uint8_t status;
int ret, timeout = 1000;
do {
ret = spi_write_then_read(spi, &cmd, 1, &status, 1);
if (ret < 0)
return ret;
if (!(status & 0x01)) /* BUSY位为0 */
return 0;
udelay(10);
} while (--timeout);
return -ETIMEDOUT;
}
/* 页编程:一次写最多256字节 */
static int spi_flash_page_program(struct spi_device *spi,
uint32_t addr,
const uint8_t *data,
size_t len)
{
uint8_t cmd[4];
int ret;
if (len > 256)
return -EINVAL;
/* 1. 写使能 */
cmd[0] = 0x06;
ret = spi_write(spi, cmd, 1);
if (ret < 0) return ret;
/* 2. 发送页编程命令 + 地址 + 数据 */
cmd[0] = 0x02;
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
/* 这里用spi_message组装多段传输 */
struct spi_transfer t[2];
struct spi_message msg;
memset(t, 0, sizeof(t));
t[0].tx_buf = cmd;
t[0].len = 4;
t[1].tx_buf = data;
t[1].len = len;
spi_message_init(&msg);
spi_message_add_tail(&t[0], &msg);
spi_message_add_tail(&t[1], &msg);
ret = spi_sync(spi, &msg);
if (ret < 0) return ret;
/* 3. 等待写完成 */
return spi_flash_wait_ready(spi);
}
关键点:spi_message 可以组装多个 spi_transfer,它们会在一次片选有效期间连续完成。这对于Flash操作至关重要——命令、地址、数据必须在一次CS拉低期间发送完,中间不能断。
3.4.3 实际项目中的坑
我记得有一次做工业数据记录仪,用的Winbond W25Q64。板子跑起来后,偶尔会出现数据写错的情况。查了三天,最后发现是SPI时钟频率太高了。
那个Flash芯片标称支持104MHz,但实际在板子上,因为走线长、信号质量差,跑到50MHz以上就开始丢数据。后来我把频率降到30MHz,问题就再没出现过。
避坑指南:
- SPI Flash的时钟频率不要只看datasheet,要根据实际板子测试
- 写操作前一定要发WRITE ENABLE(0x06),否则写不进去
- 擦除操作很慢(扇区擦除一般几百毫秒),状态轮询的超时时间要设够
- 页编程不能跨页,地址必须对齐到256字节边界
3.5 三种方式的对比与选择
最后做个总结。三种SPI设备驱动方式,各有各的适用场景。
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 自定义字符驱动 | 完全可控,性能最优 | 开发工作量大,需要维护内核模块 | 对时序/性能要求极高的设备 |
| spidev用户空间 | 开发快,调试方便,无需编译内核 | 每次传输有上下文切换开销 | 传感器、LCD、低速外设 |
| SPI Flash框架驱动 | 复用内核MTD子系统,支持文件系统 | 依赖内核配置,灵活性稍差 | 存储类设备,需要挂载文件系统 |
我个人习惯是:原型验证用spidev,产品定型后如果性能瓶颈明显再改字符驱动。至于SPI Flash,直接用内核的 m25p80 驱动就好,除非你有特殊需求,否则别自己造轮子。
嗯,SPI设备驱动这块就讲这么多。代码都在上面了,建议你拿块开发板实际跑一遍。只有亲手调过SPI时序,才能真正理解那些配置参数的意义。