4、SPI总线驱动开发:SPI核心层API、SPI控制器驱动、SPI设备驱动编写

SPI总线,说白了就是四根线搞定一切通信。SCLK、MOSI、MISO、CS,就这四根。我刚开始做机顶盒驱动时,总觉得SPI比I2C简单多了——没有地址、没有应答、没有复杂的协议栈。但后来发现,越简单的东西,坑往往越深

这一章,我们重点讲三块内容:SPI核心层APISPI控制器驱动SPI设备驱动。这三块搞明白了,你在机顶盒上接什么SPI Flash、SPI触摸屏、SPI射频模块,基本都能手到擒来。

4.1 SPI核心层API:你绕不开的基础设施

Linux内核的SPI子系统,设计得挺巧妙的。它把控制器驱动设备驱动完全解耦了。中间这一层,就是SPI核心层。核心层提供了一堆API,供上下两层调用。

我个人习惯,先看数据结构,再看操作函数。数据结构是骨架,函数是血肉。

4.1.1 核心数据结构

你写SPI驱动,天天要跟这几个结构体打交道:

结构体 作用 我常用的字段
struct spi_master 描述一个SPI控制器 bus_numnum_chipselecttransfer_one_message
struct spi_device 描述一个SPI从设备 chip_selectmax_speed_hzmode
struct spi_message 一次SPI传输的完整描述 transfers链表、complete回调
struct spi_transfer 一次SPI传输中的一个片段 tx_bufrx_buflenspeed_hz

嗯,这里要注意:spi_master在较新内核里改名叫spi_controller了。但老内核还是spi_master。我建议你写驱动时,直接看内核版本对应的头文件,别凭记忆写。

4.1.2 核心API速查

核心层提供的API,我按用途分了三类:

  • 控制器注册/注销spi_register_master()spi_unregister_master()
  • 设备注册/注销spi_new_device()spi_alloc_device()spi_add_device()
  • 数据传输spi_sync()spi_async()spi_write_then_read()

我遇到过不少新手,上来就用spi_sync(),结果在中断上下文里调用了,直接内核panic。记住:spi_sync()是阻塞的,只能在进程上下文用。中断里要用spi_async()

核心原则:SPI核心层帮你管理了总线编号、片选映射、时钟频率协商。你写驱动时,不要自己去操作GPIO模拟片选,除非你明确知道自己在干什么。

4.2 SPI控制器驱动:让硬件动起来

控制器驱动,说白了就是让SoC里的SPI硬件模块能正常工作。机顶盒里常见的SPI控制器,有DesignWare DW_apb_ssiFreescale DSPIRockchip SPI等。但不管哪种,套路都差不多。

4.2.1 控制器驱动的骨架

写一个SPI控制器驱动,你需要实现这几个回调:

static int spi_master_probe(struct platform_device *pdev)
{
    struct spi_master *master;
    struct my_spi_data *spi_data;

    // 1. 分配spi_master
    master = spi_alloc_master(&pdev->dev, sizeof(*spi_data));
    if (!master)
        return -ENOMEM;

    // 2. 设置master的硬件参数
    master->bus_num = pdev->id;  // 总线编号
    master->num_chipselect = 1;  // 支持几个片选
    master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH;

    // 3. 实现传输回调
    master->transfer_one_message = my_transfer_one_message;
    master->setup = my_setup;

    // 4. 注册master
    spi_register_master(master);

    return 0;
}

这里有个坑,我踩过好几次。spi_alloc_master()的第二个参数是私有数据的大小。这个私有数据会紧跟在spi_master结构体后面分配。用spi_master_get_devdata()来获取。千万别自己单独malloc一个结构体,然后挂在master的某个字段上——那样生命周期管理会乱套。

4.2.2 传输回调的实现

transfer_one_message是控制器驱动的核心。它负责把spi_message里的所有spi_transfer逐个发出去。

我曾经在一个机顶盒项目里,遇到SPI传输偶尔丢数据的问题。查了两天,最后发现是FIFO阈值设置不对。硬件FIFO深度是16字节,我设的阈值是8,结果发送时FIFO空了,时钟还在跑,就收到了垃圾数据。

避坑指南:我曾经在写SPI控制器驱动时,忽略了spi_transfer里的bits_per_word字段。默认是8位,但有个SPI Flash需要16位模式。结果读出来的ID全是错的。后来加上master->bits_per_word_mask = SPI_BPW_MASK(8) | SPI_BPW_MASK(16);才搞定。

4.3 SPI设备驱动:跟外设打交道

设备驱动,就是具体跟某个SPI外设通信的代码。比如SPI Flash、SPI触摸屏、SPI音频Codec。这部分工作,说白了就是拼凑spi_message,然后发出去。

4.3.1 设备驱动的注册

SPI设备驱动通过spi_driver结构体注册:

static struct spi_driver my_spi_device_driver = {
    .driver = {
        .name = "my_spi_device",
        .of_match_table = my_spi_of_match,
    },
    .probe = my_spi_probe,
    .remove = my_spi_remove,
    .id_table = my_spi_id_table,
};

module_spi_driver(my_spi_device_driver);

嗯,这里要注意:id_tableof_match_table二选一就行。现在机顶盒基本都是设备树了,所以of_match_table用得更多。

4.3.2 数据传输的几种姿势

我总结了三种常用的传输方式:

  1. 同步传输spi_sync(),简单粗暴,但会阻塞。适合在进程上下文用。
  2. 异步传输spi_async(),不阻塞,通过回调通知完成。适合在中断上下文用。
  3. 写后读spi_write_then_read(),专门为SPI Flash这类设备设计的——先发命令,再读数据。

举个例子,读一个SPI Flash的ID:

static int read_spi_flash_id(struct spi_device *spi)
{
    u8 cmd = 0x9F;  // 读ID命令
    u8 id[3];
    int ret;

    // 方式一:用spi_write_then_read
    ret = spi_write_then_read(spi, &cmd, 1, id, 3);
    if (ret < 0)
        dev_err(&spi->dev, "read id failed\n");

    // 方式二:手动构造spi_message
    struct spi_message msg;
    struct spi_transfer t = {
        .tx_buf = &cmd,
        .rx_buf = id,
        .len = 4,  // 先发1字节命令,再收3字节数据
    };
    spi_message_init(&msg);
    spi_message_add_tail(&t, &msg);
    ret = spi_sync(spi, &msg);

    return (id[0] << 16) | (id[1] << 8) | id[2];
}

我个人更推荐spi_write_then_read(),因为它内部帮你处理了片选信号的保持问题。你想想看,如果自己构造message,片选在命令发完后就释放了,再读数据时片选又拉低一次——有些SPI Flash会认为这是新的命令序列,直接罢工。

4.3.3 设备树配置

机顶盒里,SPI设备的信息通常来自设备树。一个典型的SPI设备节点长这样:

&spi0 {
    status = "okay";
    cs-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;

    flash@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;
        spi-max-frequency = <50000000>;
        spi-cpol;
        spi-cpha;
    };
};

这里有个细节:cs-gpios是控制器级别的属性,指定了片选用的GPIO。而reg = <0>表示这个设备使用第0个片选。我见过有人把这两个搞混,结果片选信号死活拉不下来。

经验之谈:调试SPI设备驱动时,先用逻辑分析仪抓波形。看片选有没有拉低、时钟有没有输出、数据线有没有电平变化。我80%的SPI问题,都是靠逻辑分析仪定位的。别一上来就怀疑代码逻辑。

4.4 本章小结

SPI总线驱动开发,其实就三件事:

  • 核心层API:记住那几个结构体和函数,别在中断里调spi_sync()
  • 控制器驱动:实现transfer_one_message回调,注意FIFO阈值和位宽设置。
  • 设备驱动:拼好spi_message,用spi_write_then_read()简化操作。

下一章,我们会讲I2C总线驱动开发。I2C比SPI多了一个地址和应答机制,但核心思路是一样的——理解核心层,写好控制器和设备驱动

嗯,今天就到这里。有什么问题,欢迎交流。