十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Linux驱动:SPI子系统

Linux驱动:SPI子系统 Linux SPI驱动框架层次如上图除开硬件和用户态应用程序由上到下分成3层设备驱动层: spi框架使用者核心层spi框架搭建者控制器驱动层 spi框架适配者drivers/spi/spi.c spi-mem.c spi-mux.cinclude/linux/spi/spi.hspi.c一方面对SPI子系统进行初始化工作注册spi bus注册spi_master class同时提供spi设备驱动对spi总线进行操作的API。另一方面SPI子系统对spi控制器层提供注册控制器的api和回调操作函数。spi.h包含了spi核心层的一些重要数据结构struct spi_master; struct spi_transfer; struct spi_message以及一些实现比较简单的函数等。spi-gpio.cSPI GPIO框架SPI子系统提供了一个名为spi-gpio的框架可使用GPIO引脚模拟SPI总线gpio模拟spi代码在drivers/spi/spi-gpio.c中。这个框架允许将GPIO引脚配置为SPI总线的时钟、片选、输入和输出信号并提供了对应的接口函数供驱动程序使用。spi-bitbangspi-bitbang是Linux内核中提供的一个通用框架用于在没有硬件SPI控制器或需要灵活控制SPI时序和配置的系统中模拟SPI总线的通信。代码在spi-bitbang.c中核心层的作用:对上层的使用者也就是SPI设备驱动提供标准的spi收发API以及设备注册函数。 对底下的适配者也就是控制器驱动层提供注册控制器接口并提供一些需要控制器驱动实现的回调函数。SPI驱动匹配流程首先使用spi_register_driver函数向SPI总线注册一个驱动driver:static struct spi_driver icm20608_driver {.probe icm20608_probe,.remove icm20608_remove,.id_table icm20608_id,.driver {.name icm20608,.owner THIS_MODULE,.of_match_table of_match_ptr(icm20608_of_match),},};spi_register_driver(icm20608_driver);spi_register_driver进行以下执行逻辑spi_register_driver(icm20608_driver);-driver_register(sdrv-driver);-bus_add_driver(drv);// 将驱动添加到总线-driver_attach(drv); //自动探测并绑定设备核心操作-bus_for_each_dev(drv-bus, NULL, drv, __driver_attach);-while ((dev next_device(i)) !error) //遍历循环 从迭代器中获取下一个设备error fn(dev, data); //调用用户提供的回调函数也就是__driver_attach-driver_match_device(drv, dev)// 匹配检查 调用总线的 match 函数-drv-bus-match(dev, drv)//也就是spi_bus_type中的spi_match_device-spi_match_device(struct device *dev, struct device_driver *drv)这里进行四种匹配方式具体实现如下框在这里执行__driver_attach中的driver_probe_device-driver_probe_device(drv, dev);-really_probe(dev, drv);// 执行实际的探测核心操作 // 优先调用总线的 probe 函数if (dev-bus-probe) {// 对于 SPI 总线调用 spi_drv_probe()// 对于 I2C 总线调用 i2c_device_probe()// 对于 Platform 总线调用 platform_drv_probe()ret dev-bus-probe(dev);-probe(to_spi_device(dev));-to_spi_device(dev)-container_of(dev, struct spi_device, dev)// container_of 计算方式// (char *)dev - offsetof(struct spi_device, dev)// 即成员地址 - 成员在结构体中的偏移量// 得到的是 spi_device 结构体的起始地址在这里执行自己定义的icm20608_driver中的icm20608_probe函数-module_add_driver(drv-owner, drv);建立驱动与模块的关联并在 sysfs 中创建符号链接/** * spi_match_device - SPI 总线的设备和驱动匹配函数 * dev: 要匹配的设备SPI 设备 * drv: 要匹配的驱动SPI 驱动 * * 返回值 * 1 → 匹配成功 * 0 → 匹配失败 * * 这是 SPI 总线类型的 match 回调函数在 driver_match_device() 中被调用 * 它按照优先级顺序尝试多种匹配方式来找到合适的驱动 */ static int spi_match_device(struct device *dev, struct device_driver *drv) { const struct spi_device *spi to_spi_device(dev); const struct spi_driver *sdrv to_spi_driver(drv); /* Attempt an OF style match */ // 设备树示例 // spi1 { // icm206080 { // compatible invensense,icm20608; ← 匹配这个 // reg 0; // spi-max-frequency 10000000; // }; if (of_driver_match_device(dev, drv)) // 设备树匹配最高优先级 return 1; /* Then try ACPI */ // acpi_driver_match_device() 检查 ACPI 表匹配 // 主要用于 x86 和 ARM 服务器平台嵌入式 Linux 较少使用 if (acpi_driver_match_device(dev, drv)) //ACPI 匹配 return 1; // 检查驱动是否定义了 id_table // id_table 是传统 SPI 驱动的匹配方式主要用于非设备树的平台 if (sdrv-id_table) return !!spi_match_id(sdrv-id_table, spi); //SPI ID 表匹配 // 如果所有方式都失败了尝试简单的字符串比较 // spi-modaliasSPI 设备的模块别名 // 通常从设备树或板级代码中获取 // 例如icm20608 // drv-name驱动的名称 // 在 struct device_driver 中定义 // 例如.name icm20608 return strcmp(spi-modalias, drv-name) 0; //名称匹配最后的后备方案 }我使用的是of_driver_match_device(dev, drv)设备树匹配最高优先级of_driver_match_device(dev, drv)-of_match_device(drv-of_match_table, dev)-of_match_node(matches, dev-of_node)-__of_match_node(matches, node);-__of_device_is_compatible(node, matches-compatible,matches-type, matches-name);-of_compat_cmp(cp, compat, strlen(compat)#define of_compat_cmp(s1, s2, l) strcasecmp((s1), (s2))到这里进行最后的比较strcasecmp//不区分大小写的字符串比较函数 int strcasecmp(const char *s1, const char *s2) { int c1, c2; do { c1 tolower(*s1); //将大写字母转换为小写字母其他字符保持不变。 c2 tolower(*s2); //将大写字母转换为小写字母其他字符保持不变。 } while (c1 c2 c1 ! 0); return c1 - c2; }比较完成后返回到上面的spi_match_device函数中最终返回到bus_add_driver函数中的driver_attach(drv); //自动探测并绑定设备核心操作后面然后继续执行module_add_driver建立驱动与模块的关联并在 sysfs 中创建符号链接完成注册接下来执行icm20608_probe函数具体实现如下static int icm20608_probe(struct spi_device *spi) { int ret; struct icm20608_data *data; uint8_t test_id; pr_info(ICM-20608G: probe started\n); pr_info(SPI bus%d, chip_select%d, max_speed%d\n, spi-master-bus_num, spi-chip_select, spi-max_speed_hz); if (!spi-dev.of_node) { pr_err(No device tree node found\n); return -EINVAL; } // 测试SPI通信 pr_info(Testing SPI communication...\n); ret icm20608_read_reg(spi, ICM20608_WHO_AM_I, test_id); if (ret 0) { pr_err(SPI communication test failed: %d\n, ret); return ret; } pr_info(WHO_AM_I 0x%02X\n, test_id); // 分配设备数据结构 data devm_kzalloc(spi-dev, sizeof(*data), GFP_KERNEL); if (!data) { pr_err(Failed to allocate memory\n); return -ENOMEM; } >// SPI 读写函数 static int icm20608_read_reg(struct spi_device *spi, uint8_t reg, uint8_t *val) { uint8_t tx_buf[2]; uint8_t rx_buf[2]; struct spi_transfer t; struct spi_message m; int ret; tx_buf[0] reg | 0x80;发送的第一个字节要读或者要写的寄存器的地址 这里需要说明一下这里发送的是表示读还是写的最高位和剩下的7位寄存器地址位这是由icm20608规定的而不是SPI tx_buf[1] 0xFF;发送一个空字节SPI中读就是写、写就是读 memset(t, 0, sizeof(t)); t.tx_buf tx_buf; t.rx_buf rx_buf; t.len 2; spi_message_init(m); //初始化消息 spi_message_add_tail(t, m);//添加传输到消息 ret spi_sync(spi, m); if (ret 0) { pr_err(SPI read failed: %d\n, ret); return ret; } *val rx_buf[1]; return 0; }SPI的读写操作读就是写、写就是读可以将SOC的SPI的数据寄存器和传感器的数据寄存器理解为两个环形移位寄存器SPI 硬件本质上是两个环形移位寄存器关键- 主设备和从设备的移位寄存器构成一个环形电路- 每个 SCLK 时钟周期数据在两个移位寄存器之间流动- 发送数据会挤出接收数据对于最关键的以下三句spi_message_init(m);spi_message_add_tail(t, m);ret spi_sync(spi, m);spi_message_init(m)- 初始化消息// include/linux/spi/spi.h static inline void spi_message_init(struct spi_message *m) { memset(m, 0, sizeof(*m)); // ① 清零所有字段 INIT_LIST_HEAD(m-transfers); // ② 初始化传输链表头指向自己 }执行后的内存状态struct spi_message m (初始化后)spi_message_add_tail(t, m)- 添加传输到消息// include/linux/spi/spi.h static inline void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m) { list_add_tail(t-transfer_list, m-transfers); }执行后的内存状态struct spi_message m (添加传输后)链表结构m.transfers ←→ t.transfer_list↑ ↑prev/next prev/next这样就构成了一个双向循环链表消息通过这个链表管理所有传输。ret spi_sync(spi, m)- 执行同步传输这是最核心的一行我们来深入看spi_sync的实现// drivers/spi/spi.c int spi_sync(struct spi_device *spi, struct spi_message *message) { DECLARE_COMPLETION_ONSTACK(done); // ① 创建完成信号 int ret; // ② 关联设备 message-spi spi; // ③ 设置完成回调 message-complete spi_complete; message-context done; // ④ 发起异步传输 ret spi_async(spi, message); if (ret) return ret; // ⑤ 等待传输完成 wait_for_completion(done); // ⑥ 返回传输状态 return message-status; }完整的执行流程spi_sync(spi, m)│├─ 1. 创建完成信号 (done)│├─ 2. m.spi spi ← 告诉内核要操作哪个设备│├─ 3. m.complete spi_complete│ m.context done ← 设置完成回调│├─ 4. spi_async(spi, m) ← 发起异步传输│ ││ ├─ __spi_validate() // 验证参数│ ├─ __spi_map_msg() // 映射 DMA 缓冲区│ ├─ __spi_queued_transfer() // 加入队列│ └─ spi_pump_messages() // 实际硬件传输│ ││ ├─ 设置 CS片选│ ├─ 发送第一个字节│ ├─ 接收第一个字节│ ├─ 发送第二个字节│ ├─ 接收第二个字节│ ├─ 释放 CS│ └─ m.complete(m.context) // 调用完成回调│├─ 5. wait_for_completion(done) ← 等待完成│ ↑│ └─ spi_complete() 唤醒│└─ 6. return m.status ← 返回传输结果 为什么需要消息和传输两层答一个消息包含多个传输// 场景读取传感器数据命令 地址 数据struct spi_transfer t1 {.tx_buf cmd, // 发送命令.len 1,.cs_change 0, // 保持片选};struct spi_transfer t2 {.tx_buf addr, // 发送地址.len 1,.cs_change 0, // 保持片选};struct spi_transfer t3 {.rx_buf data, // 接收数据.len 10,.cs_change 1, // 完成后释放片选};spi_message_init(m);spi_message_add_tail(t1, m);spi_message_add_tail(t2, m);spi_message_add_tail(t3, m);// 执行后// CS 在整个消息期间保持有效// 三个传输连续执行中间不断开片选
返回列表