找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2942

积分

0

好友

383

主题
发表于 昨天 05:54 | 查看: 2| 回复: 0

在嵌入式系统里,SPI(Serial Peripheral Interface)协议如同一条高速通道,连接着各种外设,实现高效的数据交互。它凭借简单易用、传输速率快的优点,在连接传感器、Flash、显示屏等场景中广泛应用,是嵌入式开发中不可或缺的一环。

而掌握Linux系统下的SPI驱动开发,则是嵌入式工程师释放系统潜能、实现复杂外设功能落地的关键技能。无论是智能家居的传感器联动,还是工业控制中的设备通信,背后都离不开稳定可靠的SPI驱动支撑。

Linux SPI驱动架构流程图

一、SPI 基础

想把SPI驱动开发好,首先得把协议基础打牢。SPI的核心逻辑并不复杂,重点掌握总线结构、工作模式和传输特性,就能避开大部分入门级的坑。

1.1 SPI 总线结构与信号定义

SPI采用主从架构,由一个主设备(如MCU)和一个或多个从设备(如SPI Flash)组成。主设备负责生成时钟、选择从设备,主导整个通信节奏,从设备则被动配合完成数据收发。它们通过四根核心信号线连接,每根线的作用都非常明确:

SPI通信架构示意图

  • SCK(Serial Clock):时钟线,由主设备产生,是整个通信的“节拍器”。其频率决定了数据传输的速率,主从设备依据SCK的脉冲同步数据的发送和接收。
  • MOSI(Master Output Slave Input):主发从收线。主设备通过这条“高速公路”,将数据一位一位地发送给从设备。
  • MISO(Master Input Slave Output):主收从发线。与MOSI相反,是从设备向主设备反馈数据的“回程路”。
  • CS(Chip Select):片选线,也称为SS(Slave Select)。主设备通过拉低某个从设备对应的CS线来“选中”它。通常低电平有效,未被选中的从设备处于高阻态。需注意,部分特殊芯片可能存在MOSI与MISO引脚复用的情况。

1.2 SPI 四种工作模式

SPI的四种工作模式,本质是时钟极性(CPOL)和时钟相位(CPHA)的四种组合。这两个参数直接决定了数据采样的时序,主从设备必须配置相同,否则数据传输必定错乱。

SPI通信时序图

先明确两个核心参数的定义:

  • CPOL(时钟极性):决定时钟线空闲时的电平。CPOL=0时,SCK空闲为低电平;CPOL=1时,SCK空闲为高电平。
  • CPHA(时钟相位):决定数据采样的时钟边沿。CPHA=0时,在时钟的第一个边沿采样;CPHA=1时,在时钟的第二个边沿采样。

由此组合出四种工作模式:

  • Mode 0:CPOL = 0,CPHA = 0。空闲时SCK为低电平,数据在时钟第一个边沿(上升沿)采样。
  • Mode 1:CPOL = 0,CPHA = 1。空闲时SCK为低电平,数据在时钟第二个边沿(下降沿)采样。
  • Mode 2:CPOL = 1,CPHA = 0。空闲时SCK为高电平,数据在时钟第一个边沿(下降沿)采样。
  • Mode 3:CPOL = 1,CPHA = 1。空闲时SCK为高电平,数据在时钟第二个边沿(上升沿)采样。

在实际开发中,务必确保主从设备模式一致。例如,很多SPI Flash默认工作在Mode 3,那么主设备的驱动也必须配置为Mode 3。

1.3 SPI 传输核心特性

SPI协议拥有全双工同步传输两大优势。全双工允许主从设备同时收发数据,就像两人可以同时通话,大大提升了效率。同步传输则依靠SCK时钟同步双方操作,保证了数据传输的准确性和稳定性,减少了错误。

当然,SPI也有其局限性:它没有内置的应答和流控制机制,主设备无法确认从设备是否成功接收数据,在高速或可靠性要求极高的场景中需要上层协议弥补。

为了提高效率,SPI支持FIFO(先进先出缓冲区)和DMA(直接内存访问)两种传输方式。DMA允许外设直接访问系统内存,无需CPU干预,能显著减轻CPU负担并提升大数据量传输的速度,在对时序要求严格的场景(如红外数据传输)中尤为有用。

二、Linux SPI 驱动架构

Linux的SPI驱动采用了经典的分层设计,核心思想是“解耦”——将硬件操作、核心逻辑、设备适配分离,从而提升驱动的可移植性和复用性。整个架构分为三层,各司其职。

2.1 Linux SPI 驱动三层架构解析

三层架构从上到下依次为:SPI设备驱动层SPI核心层SPI控制器驱动层(也称主机驱动层)。理解每层的职责,是上手开发的关键。

  • SPI核心层:承上启下的桥梁。它向上为设备驱动提供统一接口,向下屏蔽不同物理总线控制器的差异。它就像系统的“管理员”和“翻译官”,负责驱动的注册注销管理,并制定统一的交互规范,使得设备驱动无需关心底层硬件的具体实现。
  • SPI控制器驱动层:直接操作硬件的“管家”。它负责初始化具体的SPI控制器硬件,配置工作模式、时钟频率,管理DMA和中断等。由于可能有多个设备请求传输,它还需要对请求进行队列管理,保证先进先出。
  • SPI设备驱动层:外设的“专属管家”。它针对具体的SPI外设(如Flash、传感器)进行开发,负责实现设备的初始化、参数配置以及数据传输等功能。它通过调用SPI核心层提供的接口,与底层控制器交互,完成与外设的数据交换。

2.2 关键数据结构

Linux SPI驱动的核心是三个紧密关联的数据结构:spi_masterspi_devicespi_driver。理清它们的定义和关系,就掌握了驱动的骨架。

SPI核心数据结构关系图

1. struct spi_master
描述SPI主控制器,是控制器驱动层的核心。

struct spi_master {
    struct device dev;
    struct list_head list;
    s16 bus_num;
    u16 num_chipselect;
    int (*setup)(struct spi_device *spi);
    int (*transfer)(struct spi_device *spi, struct spi_message *mesg);
    void (*cleanup)(struct spi_device *spi);
    // 其他成员...
};
  • dev: 用于纳入Linux设备模型管理。
  • bus_num: 总线编号,用于区分不同的SPI总线。
  • num_chipselect: 支持的片选信号数量,决定能连接多少从设备。
  • setup: 函数指针,用于设置SPI设备参数(模式、频率等)。
  • transfer: 核心函数指针,实现具体的数据传输。
  • cleanup: 函数指针,用于设备注销时的资源清理。

2. struct spi_device
描述具体的SPI外设,记录了设备的通信参数。

struct spi_device {
    struct device dev;
    struct spi_master *master;
    u32 max_speed_hz;
    u8 chip_select;
    u8 mode;
    u8 bits_per_word;
    int irq;
    void *controller_state;
    void *controller_data;
    char modalias[SPI_NAME_SIZE];
    // 其他成员...
};
  • master: 指向所连接的SPI主控制器,建立关联。
  • max_speed_hz: 设备支持的最大时钟频率。
  • chip_select: 设备使用的片选信号编号。
  • mode: SPI工作模式(如SPI_MODE_0)。
  • bits_per_word: 每次传输的数据位宽(如8位、16位)。
  • modalias: 设备别名,用于驱动匹配。

3. struct spi_driver
描述SPI设备驱动,是设备驱动层的核心。

struct spi_driver {
    const struct spi_device_id *id_table;
    int (*probe)(struct spi_device *spi);
    int (*remove)(struct spi_device *spi);
    void (*shutdown)(struct spi_device *spi);
    int (*suspend)(struct spi_device *spi, pm_message_t mesg);
    int (*resume)(struct spi_device *spi);
    struct device_driver driver;
};
  • id_table: 驱动支持的设备ID表,用于匹配。
  • probe: 关键函数,设备与驱动匹配成功后调用,用于初始化。
  • remove: 驱动移除时调用,用于释放资源。
  • driver: 包含驱动名称等基本信息,纳入设备模型。

三者关系:spi_device通过master指针关联spi_masterspi_driver通过probe函数与匹配的spi_device绑定,从而建立完整的驱动链路。

三、Linux SPI 驱动开发实战

理论储备充足后,我们进入实战环节。从设备树配置到驱动编写,再到应用层测试,一步步掌握开发流程。

3.1 设备树配置:SPI 设备的“身份信息”

在现代基于Linux的嵌入式开发中,设备树(Device Tree)是描述硬件配置的标准方式。我们需要在其中准确描述SPI设备的连接和参数。

首先,确保SPI控制器节点已启用。以ZYNQ平台为例:

&spi0 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&spi0_pins>;
};

status = "okay"表示启用该控制器,pinctrl-0指定了引脚复用配置。

接着,在控制器节点下添加具体的SPI外设子节点。以连接一个W25Q128 SPI Flash为例:

&spi0 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&spi0_pins>;

    flash@0 {
        compatible = "winbond,w25q128";
        spi-max-frequency = <10000000>;
        reg = <0>;
        spi-mode = <3>;
        #address-cells = <1>;
        #size-cells = <1>;
    };
};

关键属性解析:

  • compatible:驱动匹配的关键,必须与驱动中的ID表对应。
  • spi-max-frequency:设置最大通信频率(10MHz)。
  • reg:指定片选号(CS0)。
  • spi-mode:设置工作模式为3(CPOL=1, CPHA=1)。
  • #address-cells#size-cells:用于定义子节点地址和大小的表示方式。

3.2 SPI 设备驱动编写:从注册到数据传输

设备树配置好后,开始编写驱动代码。主要步骤包括:定义驱动结构体、实现probe/remove函数、完成数据传输逻辑。

1. 驱动框架与注册
首先定义一个spi_driver结构体,并实现模块的初始化与退出函数。

#include <linux/module.h>
#include <linux/spi/spi.h>

static const struct spi_device_id spi_device_ids[] = {
    {"winbond,w25q128", 0},
    {}
};
MODULE_DEVICE_TABLE(spi, spi_device_ids);

static struct spi_driver spi_device_driver = {
   .driver = {
       .name = "spi_w25q128_driver",
       .owner = THIS_MODULE,
    },
   .id_table = spi_device_ids,
   .probe = spi_device_probe,
   .remove = spi_device_remove,
};

static int __init spi_device_driver_init(void){
    return spi_register_driver(&spi_device_driver);
}

static void __exit spi_device_driver_exit(void){
    spi_unregister_driver(&spi_device_driver);
}

module_init(spi_device_driver_init);
module_exit(spi_device_driver_exit);
MODULE_LICENSE("GPL");

2. probe函数实现
probe函数在驱动与设备匹配成功后调用,负责设备的初始化。

static int spi_device_probe(struct spi_device *spi) {
    int ret;
    // 分配设备私有数据结构体
    struct spi_device_private *priv = devm_kzalloc(&spi->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;
    spi_set_drvdata(spi, priv);

    // 获取并配置设备参数
    priv->max_speed_hz = spi->max_speed_hz;
    priv->mode = spi->mode;
    priv->bits_per_word = spi->bits_per_word;

    // 配置通信模式与速率(覆盖设备树设置或进行微调)
    spi->mode = SPI_MODE_3;
    spi->max_speed_hz = 10000000;
    ret = spi_setup(spi);
    if (ret) {
        dev_err(&spi->dev, "spi setup failed: %d\n", ret);
        return ret;
    }

    dev_info(&spi->dev, "SPI device %s probed successfully\n", spi->modalias);
    return 0;
}

3. 同步数据传输
Linux内核使用spi_transferspi_message结构体来组织数据传输。以下是一个同步读取的示例:

static ssize_t spi_device_read(struct file *filp, char __user *buf, size_t count, loff_t *off){
    struct spi_device *spi = filp->private_data;
    struct spi_device_private *priv = spi_get_drvdata(spi);

    // 构建SPI传输消息结构体
    struct spi_transfer tr = {
       .tx_buf = NULL, // 本次传输只读,不发送数据
       .rx_buf = priv->rx_buffer,
       .len = count,
    };
    struct spi_message msg;
    spi_message_init(&msg);
    spi_message_add_tail(&tr, &msg);

    int ret = spi_sync(spi, &msg);
    if (ret) {
        dev_err(&spi->dev, "SPI read failed: %d\n", ret);
        return ret;
    }

    // 将读取到的数据从内核缓冲区复制到用户空间
    ret = copy_to_user(buf, priv->rx_buffer, count);
    if (ret) {
        dev_err(&spi->dev, "copy to user failed: %d\n", ret);
        return -EFAULT;
    }
    return count;
}

4. 多transfer合并
有时需要将多个传输合并为一个原子操作(如先发命令再读数据),可以使用一个spi_message链接多个spi_transfer

static int spi_device_multi_transfer(struct spi_device *spi, const void *tx_buf1, size_t len1, const void *tx_buf2, size_t len2){
    struct spi_transfer tr1 = {
       .tx_buf = tx_buf1,
       .rx_buf = NULL,
       .len = len1,
    };
    struct spi_transfer tr2 = {
       .tx_buf = tx_buf2,
       .rx_buf = NULL,
       .len = len2,
    };
    struct spi_message msg;
    spi_message_init(&msg);
    spi_message_add_tail(&tr1, &msg);
    spi_message_add_tail(&tr2, &msg);

    return spi_sync(spi, &msg);
}

3.3 应用层测试

驱动加载后,需要在应用层验证功能。Linux提供了spidev驱动,可以在用户空间直接操作SPI设备,方便测试。

基本流程:打开设备节点→配置参数→执行读写→关闭节点。以下是一个简单的C语言测试程序:

#include <stdio.h>
#include <fcntl.h>
#include <linux/spi/spidev.h>
#include <sys/ioctl.h>

#define SPI_DEVICE "/dev/spidev0.0" // 设备节点(总线0,片选0)

int main(){
    int fd, ret;
    u8 tx_buf[] = {0x03, 0x00, 0x00, 0x00};  // 读取指令+地址(W25Q128)
    u8 rx_buf[10] = {0};                     // 接收缓冲区

    // 1. 打开SPI设备节点
    fd = open(SPI_DEVICE, O_RDWR);
    if (fd < 0) {
        perror("打开设备失败");
        return -1;
    }

    // 2. 配置SPI参数(模式、位宽、频率)
    u8 mode = SPI_MODE_3;
    u8 bits = 8;
    u32 speed = 10000000;

    // 配置工作模式
    ret = ioctl(fd, SPI_IOC_WR_MODE, &mode);
    if (ret < 0) { perror("配置模式失败"); goto err; }
    // 配置数据位宽
    ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
    if (ret < 0) { perror("配置位宽失败"); goto err; }
    // 配置传输频率
    ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
    if (ret < 0) { perror("配置频率失败"); goto err; }

    // 3. 执行读写传输
    struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long)tx_buf,
        .rx_buf = (unsigned long)rx_buf,
        .len = sizeof(tx_buf),
        .speed_hz = speed,
        .bits_per_word = bits,
    };

    ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
    if (ret < 0) {
        perror("传输失败");
        goto err;
    }

    // 打印接收结果
    printf("读取数据:");
    for (int i = 0; i < sizeof(rx_buf); i++) {
        printf("%02x ", rx_buf[i]);
    }
    printf("\n");

err:
    close(fd);
    return ret;
}

将此代码交叉编译后,在开发板上运行,若能正确读取SPI Flash数据,则证明驱动工作正常。

四、SPI 驱动调试排坑宝典

SPI驱动开发中,通信失败是常事。建议遵循“先硬件,后软件”的原则进行排查。

4.1 硬件层面调试

硬件问题是导致通信失败的首要原因。使用示波器观察波形是最直接有效的方法。重点检查四根信号线:

  1. SCK:测量频率是否与配置值相符。如果频率不对,检查时钟源和分频配置。
  2. CS:确认通信期间CS信号是否被正确拉低。若未拉低,检查连线或驱动中的片选配置。
  3. MOSI/MISO:观察数据波形是否正常,是否与SCK同步。波形异常可能是引脚虚焊、短路或工作模式不匹配。
  4. 其他隐患
    • 检查引脚连接和复用配置是否正确。
    • 确保芯片供电电压正常、稳定。曾有案例因某路电源未供电导致通信异常。
    • 核对总线上的上拉/下拉电阻配置,确保信号空闲电平稳定。

4.2 软件层面排坑

硬件无误后,重点检查软件配置。

  1. 核对设备树:确保compatiblespi-max-frequencyreg(片选号)、spi-mode等属性与硬件原理图及芯片手册完全一致。
  2. 检查驱动参数:确认驱动中设置的频率、模式、位宽等参数与设备树及设备要求匹配。
  3. 控制器驱动适配:某些平台的原生SPI控制器驱动在处理多transfer时可能有bug。如果发现多段传输数据丢失,尝试将其合并为一个spi_transfer(如3.2节所示)往往能解决问题。

4.3 典型问题解决方案

  • 读数据全为0:可能是MISO引脚被硬件设置为高阻态。需查阅芯片手册,确认是否需要配置特定引脚来使能MISO功能。
  • 写有应答,读无数据:在确认信号波形正常后,应重点检查芯片的每一路电源是否都已正确供电。
  • 数据高低位顺序错误:检查驱动中关于数据位序(MSB/LSB first)的配置,确保与通信对方(从设备)的约定一致。

五、Linux SPI 驱动典型应用场景

5.1 嵌入式传感器数据采集

SPI驱动在温度、压力、加速度等传感器采集中应用广泛。例如,MAX6675温度传感器使用SPI接口。驱动需根据其手册配置为Mode 0,并通过spi_transfer发送指令、接收温度数据。在工业自动化和智能家居中,这种实时、可靠的数据采集是系统感知环境的基础。

5.2 SPI 存储设备驱动开发

SPI Flash、EEPROM等存储设备常用作数据或程序存储。其驱动需要实现完整的擦除、读写、查忙等操作。对于固件升级等大数据量操作,结合DMA传输可以极大提升效率,缩短升级时间,增强系统可靠性。这要求开发者不仅要熟悉SPI协议,还要了解具体存储芯片的命令集。

5.3 SPI 屏幕驱动适配

在RK3568/RK3588等嵌入式平台上,适配SPI屏幕(如ST7789等)是常见需求。适配工作包括:在设备树中正确配置SPI控制器引脚和屏幕参数;在驱动中控制显示时序,将帧缓冲区的图像数据通过SPI总线持续发送到屏幕驱动IC;处理好引脚复用功能配置。这涉及对Linux帧缓冲(Framebuffer)子系统和SPI子系统的综合理解。

通过对Linux SPI驱动从基础到架构、从开发到调试、从理论到应用的全方位解析,我们希望为你构建起清晰的知识脉络。驱动开发是一门需要结合计算机体系结构知识、内核原理与硬件理解的实践性技能。多动手、多调试、多查阅文档和社区资料,是提升的最佳路径。如果你在实践中有更多心得或疑问,欢迎在云栈社区与广大开发者交流探讨。




上一篇:Java轻量级流程引擎Easy Work:6种流程编排与JSON配置实战
下一篇:ChatGPT数据安全警报:CISA代理局长上传“仅供官方使用”敏感文件
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-9 00:52 , Processed in 0.306962 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表