4. SPI驱动开发实战:SPI控制器驱动框架、SPI设备驱动编写、SPI数据传输流程与API

SPI,串行外设接口,在嵌入式世界里太常见了。ADC、DAC、传感器、显示屏、Flash芯片……几乎每个板子上都能找到它的身影。今天我们就来聊聊SPI驱动开发的那些事。

我个人习惯把SPI驱动分成两块来看:控制器驱动设备驱动。前者是芯片厂商的事,后者才是我们嵌入式工程师的日常。不过,不理解控制器驱动,设备驱动也写不顺手。

4.1 SPI控制器驱动框架

Linux内核里,SPI控制器驱动遵循一套标准框架。说白了,就是内核帮你搭好了舞台,你只需要把硬件相关的部分填进去。

核心结构体是 struct spi_controller(老版本叫 spi_master)。每个SPI控制器对应一个这样的实例。你需要实现它的几个关键回调函数:

  • transfer_one_message:处理一条完整的SPI消息
  • transfer_one:处理一次SPI传输
  • setup:配置SPI模式、时钟、位宽等参数
  • prepare_message / unprepare_message:消息传输前后的准备和清理

嗯,这里要注意:transfer_one_message 是默认的消息处理函数。如果你不实现它,内核会用默认的,但效率嘛……你懂的。

核心要点:控制器驱动的本质,就是把硬件寄存器操作封装成内核框架要求的回调函数。DMA支持、FIFO管理、中断处理,这些都是加分项。

我在项目中遇到过一个问题:某款国产MCU的SPI控制器,硬件上支持16位数据模式,但驱动里只实现了8位。结果读传感器数据时,高位总是错的。后来加上了16位模式的支持,问题就解决了。所以,硬件能力 ≠ 驱动能力,你得自己补齐。

4.2 SPI设备驱动编写

这才是我们真正要写的代码。SPI设备驱动,就是让某个具体的SPI外设工作起来。

注册一个SPI设备驱动,你需要:

  1. 定义一个 struct spi_driver 结构体
  2. 实现 proberemove 函数
  3. module_spi_driver() 宏注册

来看一个典型的SPI设备驱动框架:

static int my_spi_device_probe(struct spi_device *spi)
{
    // 设置SPI模式、时钟频率、位宽
    spi->mode = SPI_MODE_0;
    spi->max_speed_hz = 1000000;
    spi->bits_per_word = 8;
    spi_setup(spi);

    // 分配设备私有数据
    // 注册字符设备或输入设备等
    return 0;
}

static int my_spi_device_remove(struct spi_device *spi)
{
    // 清理资源
    return 0;
}

static const struct spi_device_id my_spi_id[] = {
    { "my_spi_sensor", 0 },
    {}
};

static struct spi_driver my_spi_driver = {
    .driver = {
        .name = "my_spi_sensor",
        .of_match_table = of_match_ptr(my_spi_of_match),
    },
    .probe = my_spi_device_probe,
    .remove = my_spi_device_remove,
    .id_table = my_spi_id,
};

module_spi_driver(my_spi_driver);

你想想看,这个框架是不是很眼熟?和I2C、Platform驱动长得差不多。内核的驱动模型就是这样,一通百通。

个人经验:probe函数里一定要检查spi_setup的返回值。我曾经因为没检查,结果设备树里配的时钟频率不对,驱动却"成功"加载了,数据读出来全是乱的。排查了两天才发现是频率超了上限。

4.3 SPI数据传输流程

SPI数据传输,核心是 struct spi_messagestruct spi_transfer。一个message包含一个或多个transfer,每个transfer描述一次独立的收发操作。

传输流程是这样的:

  1. 分配并初始化 spi_message
  2. 分配并初始化 spi_transfer,设置tx_buf、rx_buf、len等
  3. spi_message_add_tail() 把transfer加到message里
  4. 调用 spi_sync()spi_async() 发起传输
  5. 等待传输完成(同步)或处理回调(异步)

同步传输最常用,简单粗暴:

struct spi_message msg;
struct spi_transfer xfer = {
    .tx_buf = tx_buffer,
    .rx_buf = rx_buffer,
    .len = 32,
    .cs_change = 0,
    .delay_usecs = 10,
};

spi_message_init(&msg);
spi_message_add_tail(&xfer, &msg);
spi_sync(spi_device, &msg);

为什么要有多个transfer?举个例子:你要读一个传感器的数据,通常需要先发一个命令字节,再读回数据。这两个步骤之间可能需要片选保持、需要延时。用两个transfer放在一个message里,就能保证片选不释放,连续完成。

避坑指南:我曾经在批量传输时,每个transfer都设了cs_change=1,结果每次传输完片选就释放了。有些外设受不了这个,数据就丢了。正确的做法是:同一个message内的transfer,只有最后一个才设cs_change=0(保持片选),或者根据外设手册来。

4.4 核心API详解

SPI子系统提供的API,常用的就这几个:

API 说明 使用场景
spi_sync() 同步传输,阻塞等待完成 大多数设备驱动,简单可靠
spi_async() 异步传输,注册回调 需要高吞吐、不阻塞主流程的场景
spi_write_then_read() 先写后读,封装好的便捷函数 传感器寄存器读写,省去手动构造message
spi_write() 只写不读 配置寄存器、发送命令
spi_read() 只读不写 读取数据缓冲区

我个人最常用的是 spi_write_then_read()。它内部帮你处理了message和transfer的构造,代码简洁很多。但要注意,它只适用于小数据量(通常不超过几百字节),因为内部用了栈上的缓冲区。

来看一个实际例子:读一个SPI温度传感器的温度值。

uint8_t cmd = 0x10;  // 读温度命令
uint8_t rx_data[2] = {0};

struct spi_message msg;
struct spi_transfer xfer[2] = {
    {
        .tx_buf = &cmd,
        .len = 1,
    },
    {
        .rx_buf = rx_data,
        .len = 2,
    },
};

spi_message_init(&msg);
spi_message_add_tail(&xfer[0], &msg);
spi_message_add_tail(&xfer[1], &msg);
spi_sync(spi_dev, &msg);

// 解析温度值
int16_t temp = (rx_data[0] << 8) | rx_data[1];

spi_write_then_read() 改写,就简洁多了:

uint8_t cmd = 0x10;
uint8_t rx_data[2] = {0};
spi_write_then_read(spi_dev, &cmd, 1, rx_data, 2);

嗯,是不是清爽很多?但记住,简洁不等于万能。如果你需要精细控制片选、延时、DMA等,还是得用message/transfer的方式。

4.5 性能优化小贴士

SPI传输的性能瓶颈,通常不在协议本身,而在软件开销上。我总结了几点:

  • 减少message构造次数:能一次传完的数据,别拆成多次。每次spi_sync都有上下文切换和锁的开销。
  • 使用DMA:如果控制器支持DMA,尽量用。CPU不用逐字节搬运,效率提升明显。
  • 预分配缓冲区:不要在传输路径上动态分配内存。用静态或预分配的DMA缓冲区。
  • 批量传输:对于大量数据,考虑用更大的transfer长度,减少传输次数。

核心观点:SPI驱动开发,说白了就是三件事——配参数、发数据、收数据。但要把这三件事做好,你得理解控制器、设备和数据流。框架是死的,硬件是活的,经验是慢慢攒的。

好了,SPI驱动开发的核心内容就这些。从控制器框架到设备驱动,从数据传输流程到API使用,每一步都有坑,但也都有解法。多写、多测、多看芯片手册,慢慢就熟了。

SPI驱动开发知识体系 Linux SPI 子系统 SPI 控制器驱动 SPI 设备驱动 核心结构体与回调 struct spi_controller transfer_one_message / transfer_one setup / prepare_message 设备驱动编写步骤 定义 spi_driver 结构体 实现 probe / remove 函数 module_spi_driver() 注册 数据传输:spi_message → spi_transfer → spi_sync/spi_async 核心API:spi_sync | spi_async | spi_write_then_read | spi_write | spi_read

写在最后:SPI驱动开发,说难不难,说简单也不简单。关键是理解数据流——从用户空间到内核,从内核到控制器,从控制器到外设,每一步都有它的规矩。把这些规矩摸透了,写驱动就跟搭积木一样。

专注资料整理