Linux内核与驱动:14.SPI子系统

        

        在现代嵌入式系统开发中,SPI(Serial Peripheral Interface)总线因其全双工、高速率的特性,成为了连接各类外设(如 CAN 控制器、OLED 屏幕、各类传感器)的绝对主力。

        然而,面对复杂的 SoC 内部资源和多任务的操作系统,如果让每一个外设驱动都直接去操作 CPU 物理底层的寄存器,代码将变得极度臃肿且难以移植。为了解决这个问题,Linux 内核引入了 SPI 子系统。它完美贯彻了“总线-设备-驱动”的分层隔离模型,将繁杂的硬件时序与纯粹的业务逻辑彻底解耦。

一个典型的 SPI 总线包含以下四根信号线:

信号线全称说明
SCLKSerial Clock由主设备产生的同步时钟信号
MOSIMaster Output, Slave Input主设备输出、从设备输入
MISOMaster Input, Slave Output主设备输入、从设备输出
CSChip Select(又称 SS)片选信号,由主设备控制,用于选中特定从设备

1.Linux SPI子系统分层架构

Linux SPI 子系统采用典型的三层架构,其设计哲学与 I2C 子系统相似——通过分层和抽象实现控制器驱动与外设驱动的解耦。这样做的优点是:同一个 SPI 外设可以无缝搭配不同厂商的 SPI 控制器,反之亦然。

我们这篇博客只关注SPI设备驱动层:

        SPI 设备驱动层是普通驱动开发者日常打交道最多的一层。它基于 SPI 总线设备驱动模型实现,spi_device 来自设备树(由 SPI 控制器驱动解析生成),spi_driver 则由开发者编写。

设备驱动层的具体工作包括:定义设备匹配信息、实现 probe/remove 函数、调用核心层 API 与硬件通信、注册更高层的内核子系统接口(如 IIO、input、MTD 等)。

2.SPI 子系统的核心数据结构

(1)struct spi_controller(struct spi_master)

spi_controller 用于描述一个物理 SPI 控制器(硬件上的 SPI 外设)。在新版内核中 spi_master 是它的别名。

struct spi_controller {
    struct device dev;
    struct list_head list;
    s16 bus_num;              // 总线编号,如 spi0 对应 bus_num=0
    u16 num_chipselect;       // 片选数量
    u16 mode_bits;            // 支持的模式掩码(CPOL/CPHA/CS_HIGH...)
    u32 max_speed_hz;         // 最大通信频率
    u32 min_speed_hz;         // 最小通信频率
    int (*transfer)(struct spi_device *spi, struct spi_message *mesg);
    int (*transfer_one)(struct spi_controller *ctlr,
                        struct spi_device *spi,
                        struct spi_transfer *transfer);
    // ...
};

通常由芯片厂商的 BSP 工程师维护,设备驱动开发者只需要通过上一层 API 间接使用它,一般不需要直接接触。

(2)struct spi_device

spi_device 代表挂载在 SPI 总线上的一个具体从设备,由内核解析设备树中的 SPI 子节点时自动创建。在设备驱动的 probe 函数中它会作为参数传入。

struct spi_device {
    struct device dev;
    struct spi_controller *controller;
    u32 max_speed_hz;     // 该设备的最大通信速率
    u8 chip_select;       // 片选号(CS0, CS1...)
    u8 bits_per_word;     // 字长(通常为 8)
    u16 mode;             // 工作模式(CPOL/CPHA/CS_HIGH 等)
    int irq;              // 中断号
    char modalias[SPI_NAME_SIZE];  // 驱动匹配名称
    // ...
};

开发者最需要关注的是其中的 mode、max_speed_hz 和 chip_select。设备树中的 spi-max-frequency 属性会填充到 max_speed_hz 字段,reg 属性则表示使用的是第几路 CS。

(3)struct spi_driver

spi_driver是驱动程序必须注册到系统中的桥梁,结构与标准的 platform_driver 相似。

static const struct of_device_id dac_of_match[] = {
    { .compatible = "mycompany,spi-dac" },
    { }
};
static struct spi_driver dac_driver = {
    .driver = {
        .name = "dac",
        .of_match_table = dac_of_match,
    },
    .probe = dac_probe,
    .remove = dac_remove,
};
module_spi_driver(dac_driver);

compatible 属性用于与设备树中的 SPI 设备节点进行匹配,匹配成功后 probe 函数就会被调用。

(4) spi_transfer 与 spi_message

这是 SPI 子系统中最贴近硬件传输机制的两个核心结构体。

最小搬运单元: spi_transfer

SPI 的硬件特性是全双工:你在发送数据的同时,硬件时钟也必然会“踩”回同等长度的数据。因此,每一个 spi_transfer 都包含了:

  • tx_buf:发送缓冲区(如果没有要发的数据,就发 dummy 字节)。
  • rx_buf:接收缓冲区(如果只发不收,可以忽略收到的数据)。
  • len:这一次传输的字节长度。

完整的业务会话: spi_message

一个 spi_message 是一个链表,它可以挂载多个 spi_transfer。 为什么要有 Message?核心原因是为了控制片选(CS)引脚的生命周期!

在一次完整的 spi_message 传输期间,SPI 的 CS 引脚会被持续拉低(激活状态)。 假设你需要向外设的 0X12 地址读取 2 个字节:

  • 你不能分为两次独立的传输(发地址拉低拉高一次,读数据拉低拉高一次),外设状态机会错乱。

  • 正确的做法: 组装两个 spi_transfer(一个装地址,一个装接收容器),将它们按顺序挂进同一个 spi_message 中。内核执行时,会拉低 CS -> 发地址 -> 读数据 -> 拉高 CS,一气呵成。

3. SPI 数据传输API

SPI 核心层为上层设备驱动提供了丰富的 API,全部位于 include/linux/spi/spi.h 中。

3.1简易读写函数

函数说明
spi_write(spi, buf, len)同步写入 len 字节数据
spi_read(spi, buf, len)同步读取 len 字节数据
spi_write_then_read(spi, txbuf, n_tx, rxbuf, n_rx)先写后读,适合少量数据

上述函数都是同步传输数据,调用的都是 spi_sync 。 

3.2 通用的消息传输函数

对于需要更精细控制的传输,可以自己构建 spi_transfer 和 spi_message,内核提供了两种完全不同的提交方式:spi_sync(同步) 和 spi_async(异步)。

1. spi_sync:同步阻塞传输(最常用)
spi_sync 是驱动开发中最常用的 API。顾名思义,它是“同步”的。

工作机制: 当你的驱动代码调用 spi_sync 时,当前执行这段代码的线程会立刻进入睡眠状态(Blocked)。它交出 CPU 的使用权,直到底层的 SPI 硬件把数据老老实实全部发完,并且接收完数据后,这个线程才会被内核唤醒,继续执行下一行代码。

优点:

代码逻辑极度清晰: 线性执行,就像平铺直叙的文章。函数只要返回了,就意味着数据绝对已经在 rx_buf 里准备好了,可以直接拿来用。

内存管理简单: 你的传输缓冲区(tx_buf / rx_buf)可以直接定义在栈上(局部变量),因为函数没结束前,栈内存绝对安全。

致命限制(使用禁忌):绝对不能在中断上下文(ISR / 自旋锁)中调用! 因为在 Linux 内核中,中断处理程序是不能睡眠的。如果违规调用,会导致系统直接死机崩溃(Kernel Panic)。

2.spi_async:异步非阻塞传输 
spi_async 专为高性能和特殊上下文设计。它是“异步”的,也是“非阻塞”的。

工作机制: 当调用 spi_async 时,内核的核心层只是把你的 spi_message 丢进一个待发送队列就立刻返回了。你的线程不会睡眠,而是会立刻执行下一行代码。

     那什么时候数据发完呢?你需要提前在 spi_message 里注册一个回调函数(msg.complete)。当底层硬件传输完毕触发中断时,内核会在中断或软中断上下文里自动调用你的回调函数。

4.通用SPI外设代码框架

假设我们要写mcp2515的驱动程序,已知mcp2515连接RK3568的SPI接口,我们首先要撰写设备树,设备树的撰写在上一节中已经写过了,在此不再赘述:Linux内核与驱动:GPIO设备树与SPI设备树的区别-CSDN博客

最简单的驱动框架如下:

5.对接用户空间

问:是不是几乎所有的驱动程序都需要在probe中写字符设备/块设备/网络设备?

答案是:绝对不是。

你之所以会有“几乎所有驱动都要注册这三类设备”的错觉,是因为作为应用层(C/C++)开发者,你平时能接触到的、能用来写业务逻辑的接口,全都是这三类设备。

实际上,在庞大的 Linux 内核源码中,有超过一半的驱动程序,在它们的 probe 函数里根本不注册字符设备、块设备或网络设备。

为了理解这一点,我们需要引入 Linux 内核中一个非常核心的思想:“服务对象(Customer)”的区别

Linux 中的设备驱动分为两大阵营:面向用户空间的驱动面向内核空间的驱动

所以,只有面向用户空间的驱动程序才需要写为字符设备/块设备/网络设备。

我们在上述的基础上,继续写mcp2515对用户空间的接口,创建字符设备:

//the name/compatible = "my-mcp2515"
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>

dev_t dev_num;
struct cdev mcp2515_dev;
struct class* mcp2515_class;
struct device* mcp2515_device;
int mcp2515_open (struct inode *, struct file *)
{
    return 0;
}
ssize_t mcp2515_read (struct file *, char __user *, size_t, loff_t *)
{
    return 0;
}
size_t mcp2515_write(struct file *, const char __user *, size_t, loff_t *)
{
    return 0;
}
int mcp2515_release (struct inode *, struct file *)
{
    return 0;
}
struct file_operations mcp2515_fops = {
    .open = mcp2515_open,
    .read = mcp2515_read,
    .write = mcp2515_write,
    .release = mcp2515_release,
};
int	mcp2515_probe(struct spi_device *spi)
{
    int ret;
    ret = alloc_chrdev_region(&dev_num,0,1,"mcp2515");
    if(ret < 0){
        printk("alloc dev_num failed\n");
        return -1;
    }
    cdev_init(&mcp2515_dev,&mcp2515_fops);
    mcp2515_dev.owner = THIS_MODULE;
    ret = cdev_add(&mcp2515_dev,dev_num,1);
    if(ret < 0)
    {
        printk("cdev_add failed\n");
        return -1;
    }
    mcp2515_class = class_create(THIS_MODULE,"spi_to_can");
    if(IS_ERR(mcp2515_class))
    {
        printk("class create failed\n");
        return PTR_ERR(mcp2515_class);
    }
    mcp2515_device = device_create(mcp2515_class,NULL,dev_num,NULL,"mcp2515");
    if(IS_ERR(mcp2515_device))
    {
        printk("class create failed\n");
        return PTR_ERR(mcp2515_device);
    }
    return 0;
}
int	mcp2515_remove(struct spi_device *spi)
{
    return 0;
}
const struct of_device_id mcp2515_of_match_table[] = {
    {.compatible = "my-mcp2515"},
    {}
};
struct spi_driver spi_mcp2515 = {
    .probe = mcp2515_probe,
    .remove = mcp2515_remove,
    .driver = {
        .name = "mcp2515",
        .owner = THIS_MODULE,
        .of_match_table = mcp2515_of_match_table,
    }
};
static int __init mcp2515_init(void)
{
    int ret;
    ret = spi_register_driver(&spi_mcp2515);
    if(ret < 0)
    {
        printk("spi_register_driver failed\n");
        return ret;
    }
    return 0;
}
static void __exit mcp2515_exit(void)
{
    device_destory(mcp2515_device);
    class_destory(mcp2515_class);
    cdev_del(&mcp2515_dev);
    unregister_chrdev_region(dev_num,1);

    spi_unregister_driver(&spi_mcp2515);
}
module_init(mcp2515_init);
module_exit(mcp2515_exit);
MODULE_LICENSE("GPL");

这样用户空间就可以通过 /dev/mcp2515 节点访问mcp2515了。

6.编写mcp2515驱动:复位函数

由mcp2515的手册可知,MCP2515在正常运行之前必须进行初始化,只有在配置模式下才能进行初始化,所以我们需要让mcp2515进入配置模式,在上电或复位时,器件会自动进入配置模式。

由表可知,向mcp2515发送指令 "1100 0000" 控制其复位。

所以我们撰写一个复位函数:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值