ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Linux SPI设备驱动开发实战:从框架理解到代码实现

Linux SPI设备驱动开发实战:从框架理解到代码实现 1. 项目概述从零到一理解Linux SPI驱动的骨架搞嵌入式Linux开发SPI总线驱动是绕不开的一道坎。无论是驱动一块OLED屏幕、读写一个Flash芯片还是与各种传感器通信SPI都因其全双工、高速、简单的特性被广泛使用。但很多朋友包括当年的我在初次接触Linux下的SPI驱动时都会感到一头雾水内核源码里drivers/spi/目录下文件那么多框架看起来层层叠叠到底从哪里入手写一个驱动是要自己实现spi_master还是直接用spi_deviceprobe函数里到底该干嘛这个实验的目的就是亲手剥开Linux SPI驱动框架的“洋葱”从最核心的模型开始一步步构建一个能真实工作的SPI设备驱动。我们不满足于仅仅调用spidev或者spi-bitbang这些现成的方案而是要深入理解struct spi_driver、struct spi_device以及它们如何通过SPI核心层与主机控制器驱动交互。通过这个实验你将掌握如何在Linux内核中为一个具体的SPI从设备比如一颗SPI Flash或一个ADC芯片编写符合规范的驱动程序并理解数据是如何从用户空间通过层层关卡最终变成SCLK上的脉冲的。实验适合已经了解Linux字符设备驱动基础、对SPI协议时序有一定概念的开发者。如果你还在用单片机那套直接怼寄存器的方式思考那么这次实验将带你切换到操作系统驱动开发的思维模式——一种以框架为中心、以协议为契约的模式。2. SPI驱动框架深度拆解核心与边界在动手写代码之前必须把Linux内核中SPI子系统的架构图在脑子里画清楚。这能让你明白自己写的代码处于哪个层次该调用哪些API以及最重要的——哪些事情不该你管。2.1 三层架构主机、核心与设备Linux的SPI驱动清晰地分为三层这种分离使得驱动开发各司其职耦合度低。SPI主机控制器驱动Master Controller Driver这是最底层直接操作SoC内部的SPI控制器硬件。它负责产生SCLK、管理CS片选线、实现数据移位等硬件操作。像spi-bcm2835树莓派、spi-imxi.MX系列都属于这一类。你的设备驱动通常不需要关心这一层除非你在做板级移植或芯片原厂驱动开发。SPI核心层SPI Core这是中枢神经系统位于drivers/spi/spi.c。它提供了核心数据结构和API如spi_register_driver,spi_sync_transfer实现了主机控制器驱动和设备驱动之间的适配与路由。核心层负责将设备驱动的传输请求翻译成主机控制器驱动能理解的硬件操作序列。理解核心层提供的API是编写设备驱动的关键。SPI设备驱动SPI Device Driver这就是我们本次实验要编写的部分。它针对具体的SPI从设备如AT25系列Flash、MPU-9250传感器等实现该设备特定的功能逻辑。它通过SPI核心层提供的接口与硬件通信向上则通常提供文件操作接口file_operations或其它内核子系统接口如IIO、MTD供用户空间访问。注意千万不要试图在你的设备驱动里直接去操作GPIO模拟SPI时序除非极特殊情况或者直接调用主机控制器的寄存器。这破坏了框架的层次会导致驱动无法移植、不稳定并且可能破坏其它正在使用同一SPI总线的设备。你的世界应该从struct spi_device开始。2.2 关键数据结构spi_device与spi_driver驱动是围绕数据结构展开的。SPI设备驱动有两个核心结构体。struct spi_device代表一个“已连接”的SPI从设备。它包含了这个设备的“静态”配置信息这些信息通常在设备树Device Tree或板级文件中定义并由内核在系统启动时解析、创建max_speed_hz该设备支持的最大通信频率。modeSPI模式决定时钟极性和相位CPOL, CPHA即SPI_MODE_0到SPI_MODE_3。bits_per_word每个数据字的位数通常是8。chip_select该设备使用的是哪个片选线CS。controller指向它所连接的SPI主机控制器struct spi_controller的指针。master旧版API中的名称与controller同义。struct spi_driver这就是你编写的设备驱动本身。它类似于platform_driver或i2c_driver是一个“模板”或“适配器”用来匹配和管理spi_device。driver内嵌的device_driver结构需要设置.name成员。probe当内核发现一个spi_device与本驱动匹配时调用的初始化函数。这是你驱动的主入口。remove设备移除或驱动卸载时的清理函数。id_table可选的SPI设备ID匹配表用于通过名称匹配。它们的关系系统启动时根据设备树信息创建若干个spi_device。当你的spi_driver被注册时内核会遍历所有spi_device进行匹配优先通过设备树compatible属性其次通过id_table。匹配成功后就调用你的probe函数并将对应的spi_device结构体指针传递给你。从此这个spi_device就成了你驱动与具体硬件对话的“通行证”。3. 实验环境搭建与基础驱动骨架我们假设实验平台是一块常见的ARM开发板如树莓派、i.MX6UL等其SPI主机控制器驱动已由内核默认支持并启用。我们要做的是添加一个虚拟的或真实的SPI从设备并为它编写驱动。3.1 硬件与内核配置首先确保内核配置已开启SPI子系统和支持你的主机控制器。# 在内核源码目录下 make menuconfig确保以下选项被启用y或mDevice Drivers - SPI support(CONFIG_SPI)你的具体主机控制器例如Device Drivers - SPI support - BCM2835 SPI controller(CONFIG_SPI_BCM2835)为了调试可以启用Device Drivers - SPI support - User mode SPI device driver(CONFIG_SPI_SPIDEV)但我们的实验驱动会替代它。如果你的设备是通过设备树描述的需要在设备树源文件.dts或.dtsi中添加节点。例如假设我们在SPI0总线的CS0上连接了一个虚拟的“实验芯片”spi0 { status okay; #address-cells 1; #size-cells 0; my_spi_device: my_spi_device0 { compatible vendor,experimental-chip; // 这是驱动匹配的关键字 reg 0; // 对应CS0 spi-max-frequency 10000000; // 10MHz spi-cpol; // 根据设备需要设置模式 spi-cpha; // 其他设备特定属性比如中断引脚等 // interrupt-parent gpio; // interrupts 17 IRQ_TYPE_EDGE_RISING; }; };编译并更新设备树后重启系统。你可以在/sys/bus/spi/devices/下看到对应的设备节点如spi0.0。3.2 驱动骨架代码实现现在我们来创建最基础的驱动骨架文件my_spi_driver.c。#include linux/module.h #include linux/spi/spi.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h #define DRIVER_NAME my_spi_drv #define DEVICE_NAME my_spi_dev #define N_DEVICES 1 // 自定义设备结构体用于封装驱动实例的私有数据 struct my_spi_data { struct spi_device *spi; struct cdev cdev; dev_t devno; struct class *cls; struct device *dev; // 可以添加缓冲区、锁、等待队列等私有数据 }; // 1. 实现probe函数 static int my_spi_probe(struct spi_device *spi) { struct my_spi_data *data; int ret; printk(KERN_INFO DRIVER_NAME : Probing device on SPI bus %d, CS %d, max speed %d Hz\n, spi-master-bus_num, spi-chip_select, spi-max_speed_hz); // 分配并初始化私有数据结构 data devm_kzalloc(spi-dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >struct spi_transfer { const void *tx_buf; // 发送缓冲区指针 void *rx_buf; // 接收缓冲区指针 unsigned len; // 传输长度字节数 // ... 其他如速度、延迟等高级字段 };一次SPI传输spi_message可以包含多个spi_transfer它们会背靠背执行片选线在整个message期间保持有效。这对于需要发送命令字后紧接收数据的设备非常有用。让我们实现一个简单的“读写”函数作为驱动的核心引擎static int my_spi_read_write(struct my_spi_data *data, const u8 *tx_buf, u8 *rx_buf, size_t len) { struct spi_device *spi >u8 cmd 0x9F; // 假设是读ID命令 u8 id_buf[3]; struct spi_transfer t[] { { .tx_buf cmd, .len 1, }, { .rx_buf id_buf, .len sizeof(id_buf), }, }; spi_message_init(m); spi_message_add_tail(t[0], m); spi_message_add_tail(t[1], m); status spi_sync(spi, m);4.2 完善驱动匹配表与文件操作现在我们需要补全驱动使其能被设备树正确匹配并实现基本的文件操作让用户空间程序可以通过read/write/ioctl与设备交互。首先定义设备树匹配表#ifdef CONFIG_OF static const struct of_device_id my_spi_of_match[] { { .compatible vendor,experimental-chip }, // 必须与设备树完全一致 {}, }; MODULE_DEVICE_TABLE(of, my_spi_of_match); #endif static struct spi_driver my_spi_driver { .driver { .name DRIVER_NAME, .owner THIS_MODULE, .of_match_table of_match_ptr(my_spi_of_match), // 关联匹配表 }, .probe my_spi_probe, .remove my_spi_remove, };of_match_ptr宏在未启用设备树时会安全地处理成NULL。接下来定义文件操作集my_spi_fops。这里实现一个简单的ioctl接口用于执行一次SPI交换同时收发#include linux/ioctl.h #define MY_SPI_IOC_MAGIC k #define MY_SPI_IOC_RW _IOWR(MY_SPI_IOC_MAGIC, 1, struct my_spi_ioc_transfer) // 定义ioctl命令 struct my_spi_ioc_transfer { u32 len; u8 *tx_buf; u8 *rx_buf; }; static long my_spi_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct my_spi_data *data filp-private_data; struct my_spi_ioc_transfer usr_transfer; u8 *tx_buf NULL, *rx_buf NULL; int ret 0; if (_IOC_TYPE(cmd) ! MY_SPI_IOC_MAGIC) return -ENOTTY; switch (cmd) { case MY_SPI_IOC_RW: // 1. 从用户空间拷贝控制结构体 if (copy_from_user(usr_transfer, (void __user *)arg, sizeof(usr_transfer))) return -EFAULT; // 2. 安全检查长度 if (usr_transfer.len 4096) // 设置一个合理的上限 return -EINVAL; // 3. 分配内核缓冲区 tx_buf kmalloc(usr_transfer.len, GFP_KERNEL); rx_buf kmalloc(usr_transfer.len, GFP_KERNEL); if (!tx_buf || !rx_buf) { ret -ENOMEM; goto out; } // 4. 拷贝用户空间的发送数据到内核缓冲区 if (usr_transfer.tx_buf) { if (copy_from_user(tx_buf, (void __user *)usr_transfer.tx_buf, usr_transfer.len)) { ret -EFAULT; goto out; } } else { memset(tx_buf, 0, usr_transfer.len); } // 5. 执行SPI传输 ret my_spi_read_write(data, tx_buf, rx_buf, usr_transfer.len); if (ret) goto out; // 6. 将接收到的数据拷贝回用户空间 if (usr_transfer.rx_buf) { if (copy_to_user((void __user *)usr_transfer.rx_buf, rx_buf, usr_transfer.len)) { ret -EFAULT; goto out; } } break; default: return -ENOTTY; } out: kfree(tx_buf); kfree(rx_buf); return ret; } static int my_spi_open(struct inode *inode, struct file *filp) { struct my_spi_data *data container_of(inode-i_cdev, struct my_spi_data, cdev); filp-private_data data; return 0; } static const struct file_operations my_spi_fops { .owner THIS_MODULE, .open my_spi_open, .unlocked_ioctl my_spi_ioctl, // 可以根据需要实现.read, .write, .llseek等 };至此一个功能完整的、可通过ioctl进行SPI数据传输的驱动就完成了。用户空间程序可以打开/dev/my_spi_dev0然后使用自定义的ioctl命令来发起读写操作。5. 调试、问题排查与性能考量驱动写好了但第一次就能完美运行的概率很低。掌握调试和排查方法至关重要。5.1 调试手段与信息获取printk与dev_*系列函数最基本的调试工具。在probe、remove、传输函数中加入dev_info(),dev_dbg(),dev_err()。dev_dbg()需要定义DEBUG宏或动态启用。#define DEBUG // 在文件开头定义以启用dev_dbg dev_dbg(spi-dev, Transfer: len%zu, tx%*ph, rx%*ph\n, len, (int)len, tx_buf, (int)len, rx_buf);Sysfs与DebugfsSPI总线信息/sys/bus/spi/devices/下查看所有SPI设备/sys/bus/spi/drivers/查看驱动。设备树节点/proc/device-tree/可以查看解析后的设备树。Debugfs如果内核配置了CONFIG_DEBUG_FS可以挂载debugfs并在/sys/kernel/debug/spi/下查看更详细的SPI控制器和设备状态。逻辑分析仪或示波器这是硬件调试的终极武器。抓取SCLK、MOSI、MISO、CS波形可以直观验证时序是否符合SPI模式CPOL, CPHA。数据位是否正确有无错位。片选信号在传输前后是否正确拉低和拉高。通信速度是否与设置一致。5.2 常见问题与解决方案速查表问题现象可能原因排查步骤与解决方案probe函数根本不被调用1. 设备树节点compatible与驱动不匹配。2. 设备树节点未正确启用status “okay”。3. SPI总线控制器驱动未加载或未启用。4. 设备树未成功编译加载。1. 检查dmesg看是否有设备树解析错误。2. 核对/sys/bus/spi/devices/下是否有你的设备节点如spi0.0。3. 检查/sys/bus/spi/drivers/下你的驱动是否注册成功。4. 使用of_find_compatible_node等API在驱动初始化时主动查找节点打印信息。SPI传输失败返回-EIO等错误1. SPI模式mode设置错误。2. 通信频率max_speed_hz超过设备或控制器支持范围。3. 硬件连接问题线接错、没接上拉电阻。4. 片选CS线冲突或控制方式不对。1. 用示波器检查波形确认CPOL/CPHA。2. 降低频率尝试如先降到100kHz。3. 检查硬件连接确认电压电平匹配。4. 确认是硬件片选还是GPIO模拟片选。设备树中可配置spi-cs-high等属性。数据传输内容错误1. 字节序Endianness问题。2. 数据位长度bits_per_word不匹配比如设备是16位驱动设为8位。3. 传输时序中的延迟需求未满足。1. 核对设备数据手册确认多字节数据的传输顺序MSB/LSB。2. 设置spi-bits_per_word或在spi_transfer中设置.bits_per_word。3. 在spi_transfer中设置.delay_usecs来增加片选释放或传输间的延迟。驱动加载后系统不稳定或其它SPI设备异常1. 驱动probe/remove中资源申请释放不对等造成内存泄漏或引用计数问题。2. 错误地操作了共享的SPI总线状态。3. 中断处理不当如果使用了中断。1. 确保所有devm_分配的资源无需手动释放非devm_分配的资源在remove中必须释放。2. 除非绝对必要不要在设备驱动中调用spi_setup()频繁修改总线参数这会影响同总线其他设备。3. 使用devm_request_irq管理中断并在remove中确保中断已释放。用户空间ioctl返回-EFAULT用户空间指针非法或拷贝失败。1. 仔细检查copy_from_user和copy_to_user的返回值。2. 对用户传入的长度参数进行严格的边界检查防止缓冲区溢出。3. 使用access_ok()函数验证用户空间指针的合法性。5.3 性能优化与高级话题当基础功能稳定后可以考虑优化DMA传输对于大数据量传输使用DMA可以极大降低CPU占用。这通常需要在spi_transfer中设置.tx_dma和.rx_dma字段并依赖主机控制器驱动对DMA的支持。你的设备驱动需要分配DMA缓冲区dma_alloc_coherent。线程化IRQ与工作队列如果SPI设备通过中断通知数据就绪在中断处理函数中执行长时间的SPI传输是不合适的。应该将传输任务推送到工作队列workqueue或内核线程kthread中异步执行。使用内核子系统如果你的设备是Flash应集成到MTD子系统中如果是传感器考虑IIOIndustrial I/O子系统如果是触摸屏可能是输入子系统。这能提供更统一、强大的用户空间接口和框架支持。电源管理实现spi_driver的pm_ops支持系统的休眠唤醒Suspend/Resume。6. 从实验到实践连接真实硬件为了将实验落到实处我们以一颗常见的SPI Flash芯片如Winbond W25Q128为例简述如何调整驱动。设备树根据芯片手册确认最大频率比如104MHz设置正确的SPI模式W25Q128通常支持Mode 0和Mode 3。在设备树中完善节点属性。experimental_flash: flash0 { compatible winbond,w25q128, vendor,experimental-chip; // 可以添加具体型号 reg 0; spi-max-frequency 50000000; /* 保守起见先设50MHz */ spi-cpol; spi-cpha; };驱动适配在驱动中需要实现芯片特定的命令序列。例如实现读ID、读数据、页编程、扇区擦除等操作。这些操作都通过组合不同的spi_transfer来完成。static int flash_read_id(struct my_spi_data *data, u8 *id_buf) { u8 cmd 0x9F; // W25Q128的读ID命令 struct spi_transfer t[] { { .tx_buf cmd, .len 1, }, { .rx_buf id_buf, .len 3, }, // 制造商ID内存类型容量ID }; // ... 使用spi_sync执行 }然后可以将这些操作映射到文件操作的read、write或者更具体的ioctl命令上。测试编写一个简单的用户空间测试程序打开设备文件发送读ID命令验证返回的数据是否与手册一致Winbond的制造商ID通常是0xEF。通过这样一个完整的循环——从框架理解、骨架搭建、核心实现、调试排错到适配真实硬件你对Linux SPI设备驱动的开发就有了一个扎实的、可复现的认知基础。记住驱动开发的精髓在于“遵循框架契约”和“细心处理边界”剩下的就是根据具体设备的数据手册耐心地实现每一个功能命令。
返回列表