Linux内核与驱动:14.SPI子系统
在现代嵌入式系统开发中SPISerial 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 子系统的核心数据结构1struct spi_controllerstruct spi_masterspi_controller 用于描述一个物理 SPI 控制器硬件上的 SPI 外设。在新版内核中 spi_master 是它的别名。struct spi_controller { struct device dev; struct list_head list; s16 bus_num; // 总线编号如 spi0 对应 bus_num0 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 间接使用它一般不需要直接接触。2struct spi_device这是写完设备树Linux内核找到这个设备树之后自己生成的这个spi从设备的描述不需要我们自己定义。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。3struct spi_driverspi_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 函数就会被调用。module_spi_driver()本质上等价于module_init(...) spi_register_driver(bmi088_gyro_driver); module_exit(...) spi_unregister_driver(bmi088_gyro_driver);(4) spi_transfer 与 spi_message这是 SPI 子系统中最贴近硬件传输机制的两个核心结构体。最小搬运单元: spi_transferSPI 的硬件特性是全双工你在发送数据的同时硬件时钟也必然会“踩”回同等长度的数据。因此每一个 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 数据传输APISPI 核心层为上层设备驱动提供了丰富的 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 控制其复位。所以我们撰写一个复位函数

相关新闻