3、SPI设备驱动实战:编写SPI字符设备驱动、使用spidev用户空间接口、SPI Flash驱动案例分析

SPI总线驱动,说白了就是让CPU和那些挂在SPI总线上的外设能正常说话。我做了这么多年驱动,SPI设备驱动这块踩过的坑最多。今天咱们就把它彻底讲透。

3.1 SPI设备驱动的整体架构

先看一张图,把SPI设备驱动的层次结构理清楚。你想想看,一个SPI设备要正常工作,需要哪几层?

SPI设备驱动软件层次结构 用户空间应用程序 用户空间接口:/dev/spidevX.Y 或 自定义字符设备 内核空间:SPI设备驱动(字符驱动或框架驱动) SPI核心层(spi.c / spi-dev.c) SPI控制器驱动(硬件相关,如i.MX6、STM32 SPI) SPI总线硬件(SCLK/MOSI/MISO/CS)

这张图我画了很多遍。每次给团队新人讲SPI驱动,我都会先让他们看这个层次。最底层是控制器驱动,跟具体芯片绑定;中间是SPI核心层,Linux内核已经帮你写好了;上面才是我们要写的设备驱动。

3.2 编写SPI字符设备驱动

先说说最直接的方式——写一个字符设备驱动。为什么选字符设备?因为SPI设备本质上就是顺序读写,跟字符设备天然匹配。

3.2.1 驱动框架搭建

一个完整的SPI字符设备驱动,核心就这几步:

  1. 注册SPI设备驱动:用 spi_register_driver() 告诉内核我能管哪些设备
  2. 分配主次设备号:用 alloc_chrdev_region() 或者静态指定
  3. 初始化字符设备:用 cdev_init() 绑定 file_operations
  4. 实现读写接口:在 open/read/write/ioctl 里操作SPI
  5. 注销清理:模块卸载时别忘了释放资源

核心要点: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时序,才能真正理解那些配置参数的意义。