嵌入式学习linux内核驱动8——IIC设备驱动和lm75-dht11
一、中断处理机制
中断基础概念
- 介绍了在设备树中定义中断的方法,包括指定中断所属的组和编号。
- 解释了中断触发方式的默认值设置,指出该值在后续的request_irq调用中会被重新设定。
中断申请与控制
- 讲解了中断的申请、释放以及禁用(disable_irq)和使能(enable_irq)操作。
- 强调了中断服务程序中可能实现的功能复杂度不同,简单操作可直接在顶半部完成。
底半部处理机制
- 说明了当顶半部处理任务较复杂时,需使用底半部来执行耗时操作。
- 介绍了底半部的三种实现方式:软中断、tasklet和工作队列,并比较了它们的特点。
软中断与工作队列对比
- 指出软中断本质上是软件模拟的中断,具有较高优先级但不能休眠或阻塞。
- 工作队列则像普通线程一样受操作系统调度,没有软中断的限制,但实时性较差。
实际应用考量
- 通过ADC中断的例子说明了根据数据读取频率和实时性要求选择合适的底半部机制。
- 强调实际选择取决于具体应用场景和经验判断,而非固定的时间标准。
二、同步机制与锁的应用
互斥锁与信号量的问题
- 分析了互斥锁和信号量可能导致线程阻塞并被调度出去的问题。
- 指出这种机制在持有时间短的情况下效率低下,因为调度开销可能超过等待时间。
自旋锁的特点与适用场景
- 介绍了自旋锁作为一种忙等待锁,不会导致线程被调度出去。
- 适用于中断上下文等不能被调度的环境,以及持有时间极短的临界区保护。
锁的选择策略
- 提出自旋锁可以在进程上下文中使用,但需根据代码执行时间长短决定是否采用。
- 较长的临界区应使用互斥锁,而非常短暂的操作适合使用自旋锁以减少调度开销。
原子变量的作用
- 说明原子变量用于实现单个变量的原子性操作,避免竞态条件。
- 指出自旋锁的实现通常依赖于原子变量操作。
原子变量 互斥锁 信号量 自旋锁 读写锁原子变量:内核中实现单个整形变量原子操作加减的一种变量互斥锁和信号量:仅能在进程上下文中使用自旋锁:忙等待锁,当锁被其他线程持有时,自旋锁不被调度,原地等待,可以在中断上下文中使用spinlock_t spinlock;读写锁:读和读之间不互斥。读和写之间、写和写之间互斥
三、I²C设备驱动开发
I²C总线架构
- 解释了I²C总线驱动作为平台无关的部分,由操作系统提供,负责处理基本通信时序。
- 设备驱动需要基于此总线驱动来实现特定设备的功能。
用户空间I²C访问
- 展示了如何在用户空间通过/dev/i2c-X设备节点直接进行I²C通信。
- 使用ioctl设置从设备地址,然后进行读写操作,演示了与LM75温度传感器的交互。
内核空间设备驱动
- 说明了编写内核级I²C设备驱动的必要性,以便为应用程序提供更简单的接口。
- 驱动程序封装底层细节,使应用层可以直接读取解析后的数据。
驱动模型与匹配机制
- 描述了I²C驱动模型中的client和driver结构,以及它们之间的匹配过程。
- 匹配可通过名称或ID table完成,probe函数接收client参数以获取适配器和地址信息。
数据传输流程
- 详细说明了驱动内部如何构造i2c_msg结构体并调用i2c_transfer完成数据收发。
- 举例说明了读取LM75寄存器值的具体步骤:先写入寄存器地址,再读取数据。
四、驱动开发实践建议
开发方法论
- 建议先在用户空间验证通信逻辑,确保正确后再移植到内核驱动中。
- 这种分步开发方式有助于快速定位问题,降低调试难度。
资源管理与配置
- 强调设备树(DTS)在现代Linux系统中的重要性,用于描述硬件资源配置。
- I²C设备信息如总线索引和从机地址应在设备树中定义,驱动程序从中获取。
子系统依赖关系
- 指出某些驱动的实现依赖于其他子系统的存在,形成父子依赖关系。
- 在配置和编译时必须确保所有相关模块都被正确包含,否则功能无法正常工作。
五、I2C子系统架构与分层思想
分离与分层设计原则
- 设备与驱动实现了分离设计,提高了系统的灵活性和可维护性。
- 采用分层架构,将I2C总线驱动与设备驱动分离,形成清晰的层次结构。
- 核心层负责管理适配器并为设备驱动提供接口服务,实现匹配机制。
I2C系统组件关系
- 总线驱动层包含adapter组件,与底层硬件接口一一对应。
- 设备驱动层通过核心层调用总线驱动提供的传输函数进行数据收发。
- adapter包含master_xfer算法,用于控制底层硬件完成数据传输操作。
协议栈类比理解
- I2C分层结构类似于网络协议栈,具有自底向上逐层负责的特点。
- 各层之间通过明确定义的接口进行交互,上层依赖下层提供的服务。
- 这种栈式结构便于功能模块化,支持多种设备在统一框架下工作。
📋 架构总体说明
这是一个分层+总线式的混合驱动模型,体现了Linux设备驱动的分离思想。
🎯 各层次详细解析
第一层:用户空间 (User Space)
user app - 用户应用程序
open("/dev/i2c-0")
- 直接打开I2C适配器设备文件
open(...)
- 也可能通过具体设备节点打开第二层:I2C设备驱动层 (I2C Device Driver)
这层包含具体的设备驱动:
I2c dev drv - I2C设备文件驱动
提供
/dev/i2c-0
等设备节点实现标准的文件操作接口
lm75_driver - LM75温度传感器驱动
at24c08_driver - AT24C08 EEPROM驱动
lm75_client - LM75的I2C客户端数据结构
at24c08_client - AT24C08的I2C客户端数据结构
第三层:I2C核心层 & 总线驱动层 (I2C Core & Bus Driver)
这是I2C子系统的核心:
I2c_bus drv - I2C总线驱动
I2c 0_client - I2C总线本身的客户端表示
adapter0, adapter1 - I2C适配器(控制器)
algo - I2C算法操作集
master_xfer
- 最关键的传输函数,实现具体的I2C时序第四层:硬件层 (Hardware)
I2C 0 - 物理I2C控制器硬件
连接实际的LM75、AT24C08等物理芯片
🔄 数据传输流程分析
场景1:用户直接控制I2C总线
text
用户app → I2c dev drv → I2c_bus drv → adapter0 → I2C 0硬件↓ open("/dev/i2c-0") → ioctl(I2C_RDWR) → master_xfer() → 产生I2C波形场景2:通过具体设备驱动
text
用户app → lm75_driver → I2c_bus drv → adapter0 → I2C 0硬件↓ read("温度数据") → i2c_transfer() → master_xfer() → 读取LM75寄存器
💡 关键设计思想
1. 分离设计
设备驱动:只关心设备功能(如LM75的温度转换)
总线驱动:只关心I2C时序和协议
核心层:负责两者的匹配和协作
2. 客户端模型
每个I2C设备在内核中都有一个
i2c_client
结构,包含:
设备地址
所属适配器
设备驱动指针
3. 适配器抽象
不同的I2C控制器硬件通过
i2c_adapter
抽象:
统一的接口(
master_xfer
)硬件差异在算法层(
algo
)实现
🛠️ 实际开发中的应用
编写I2C设备驱动时:
// 1. 定义设备驱动 static struct i2c_driver lm75_driver = {.driver.name = "lm75",.probe = lm75_probe,.id_table = lm75_ids, };// 2. 在probe函数中获取client static int lm75_probe(struct i2c_client *client) {// 使用i2c_smbus_read_byte_data()等函数访问设备 }直接用户空间访问时:
c
int fd = open("/dev/i2c-0", O_RDWR); ioctl(fd, I2C_SLAVE, 0x48); // 设置LM75地址 i2c_smbus_read_byte_data(fd, TEMP_REGISTER);
✅ 总结
这个架构图体现了Linux驱动设计的精髓:
层次化:用户空间→设备驱动→核心层→总线驱动→硬件
分离化:设备功能与总线协议分离
标准化:统一的接口和操作集
可扩展:易于添加新的I2C设备和控制器
这种设计使得:
设备驱动开发者无需关心具体硬件控制器
控制器驱动开发者无需了解具体设备功能
用户可以选择直接控制或通过设备驱动访问
六、设备驱动开发实践
LM75温度传感器驱动实现
- 驱动程序基于platform驱动模板修改,替换为I2C驱动注册方式。
- 使用i2c_add_driver宏注册驱动,简化了驱动注册流程。
- probe函数中保存client指针,供read等接口函数后续使用。
设备树配置要点
- 在i2c1节点下添加lm75子节点,配置compatible属性和从机地址。
- compatible属性建议包含厂商信息(如ti,lm75),避免命名冲突。
- 可以配置多个compatible值,支持同一驱动适配不同厂商的同类设备。
应用程序接口设计
- 实现标准的open、read、write、close文件操作接口。
- read函数通过i2c_transfer调用底层adapter完成数据收发。
- 使用container_of宏根据成员地址计算结构体首地址,是内核常用技巧。
七、DHT11传感器驱动开发
单总线通信协议分析
- DHT11采用单总线半双工通信,主机先发送启动信号。
- 启动信号由主机拉低至少18ms,然后拉高20-40μs组成。
- 传感器响应信号为80μs低电平后接80μs高电平。
数据传输时序特征
- 每个数据位以50μs低电平开始,区分0和1的关键在于高电平持续时间。
- 数据0表示为26-28μs高电平,数据1表示为70μs高电平。
- 一次完整数据传输包含40位:湿度16位、温度16位和8位校验和。
驱动实现关键技术
- sleep与delay的区别:sleep会引发进程调度,可用于进程上下文;delay不调度,可用于中断上下文。
- get_bit函数通过35μs延时判断高低电平,若仍为高则为1,否则为0。
- read_data函数循环40次获取所有数据位,并按字节重组数据。
调试问题与解决方案
- 初始实现遇到wait_response超时问题,可能因引脚选择不当或上拉电阻不足。
- 尝试更换GPIO引脚,调整电气属性配置(如下拉改为上拉)。
- 建议优先选择已有外部上拉电阻的按键引脚,确保信号质量。