一、前言
随着驱动开发逐渐深入,仅仅掌握 GPIO 和中断还不够。实际项目中,驱动可能需要周期性检查设备状态、延迟执行某些操作,或者通过统一的设备模型与硬件进行匹配。
Linux 内核提供了定时器机制和多种设备驱动框架。其中,platform 总线常用于描述片上设备,I²C 子系统则为通过 I²C 总线连接的传感器、扩展芯片等设备提供了统一的访问方式。
本文介绍内核定时器、platform 驱动模型和 I²C 驱动的基本结构,并简要说明 IIO 与 USB 子系统的用途。
二、Linux 内核定时器
2.1 定时器的作用
内核定时器用于在指定时间到达后触发回调函数。它适用于延迟处理、周期性检查以及部分简单的状态监测任务。
例如,在按键驱动中,可以在检测到电平变化后延迟一段时间再次读取状态,以辅助实现按键消抖。
需要注意,定时器回调通常运行在不能睡眠的上下文中,因此不应在回调中执行可能睡眠的操作。
2.2 jiffies 与 HZ
Linux 内核使用 jiffies 记录系统启动后累计的时钟节拍数,而 HZ 表示每秒的时钟节拍数。
在相关计算中,可以通过 msecs_to_jiffies() 将毫秒转换成内核节拍数。例如:
unsigned long delay;
delay = msecs_to_jiffies(100);
该代码用于计算约 100 毫秒对应的节拍数。具体精度取决于内核配置和定时器机制,不应把普通内核定时器当成高精度实时定时器。
2.3 较早内核中的定时器接口
在 Linux 4.1.15 等较早内核中,常见的定时器接口包括:
init_timer(&timer);
timer.function = timer_callback;
timer.data = (unsigned long)dev;
timer.expires = jiffies + msecs_to_jiffies(100);
add_timer(&timer);
其中:
init_timer():初始化定时器。
timer.function:指定回调函数。
timer.data:向回调传递参数。
timer.expires:指定到期时间。
add_timer():将定时器加入内核定时器系统。
修改已经启动的定时器时间时,可以使用 mod_timer();删除定时器时,可以使用 del_timer() 或适当的同步删除接口。
以上接口适用于相应旧版本内核。较新的 Linux 内核已采用不同的定时器初始化方式,开发时应以目标内核头文件和文档为准。
2.4 定时器回调示例
static void timer_callback(unsigned long data)
{
/* 执行简短且不能睡眠的操作 */
}
在使用该接口的内核版本中,回调函数参数类型与上述旧接口相对应。
如果要实现周期性任务,通常需要在回调中重新设置定时器,或者将任务交给工作队列执行。对于需要睡眠或较复杂的处理,不能直接放在定时器回调中完成。
三、platform 总线与驱动模型
3.1 为什么需要 platform 总线
在嵌入式 SoC 中,很多设备直接集成在芯片内部,例如 GPIO 控制器、UART 控制器和定时器控制器。这些设备不一定通过 PCI 或 USB 等外部总线连接。
Linux 使用 platform 设备模型来管理这类设备,并通过设备与驱动的匹配关系完成驱动绑定。
在设备树体系中,设备节点通常由内核转换为相应的设备对象,platform 驱动再根据匹配结果执行初始化。
3.2 platform 驱动的基本结构
一个典型的 platform 驱动包含:
struct platform_driver:描述驱动。
probe():设备与驱动匹配后执行初始化。
remove():设备移除时执行资源清理。
of_match_table:描述设备树匹配规则。
示例:
static const struct of_device_id demo_of_match\[\] = {
{ .compatible = "demo,device" },
{ }
};
static struct platform_driver demo_driver = {
.probe = demo_probe,
.remove = demo_remove,
.driver = {
.name = "demo_device",
.of_match_table = demo_of_match,
},
};
module_platform_driver(demo_driver);
这里的 demo_probe() 和 demo_remove() 需要由驱动开发者实现。示例只展示基本结构,并不构成可以直接操作硬件的完整驱动。
3.3 probe 函数的主要任务
probe() 是 platform 驱动初始化设备的关键位置,通常需要完成:
1.获取设备树和设备资源。
2.申请 GPIO、中断或其他硬件资源。
3.初始化设备状态和驱动私有数据。
4.注册字符设备、misc 设备或其他用户接口。
5.完成后续硬件初始化。
如果中间某一步失败,应当及时释放已经成功申请的资源,避免重复注册或资源泄漏。
3.4 设备与驱动分离
采用 platform 驱动模型后,硬件资源描述与驱动控制逻辑可以分开维护。
设备树主要描述硬件的存在及其资源,驱动则负责如何初始化和操作设备。当硬件连接方式发生变化时,如果驱动逻辑仍然适用,通常可以通过调整设备树完成适配,而不必将所有硬件信息重新写进驱动代码。
四、I²C 驱动基础
4.1 什么是 I²C
I²C 是一种常见的串行通信总线,通常使用 SDA 数据线和 SCL 时钟线连接控制器与外设。
嵌入式设备中的温度传感器、EEPROM、时钟芯片和部分扩展 IO 芯片,都可能通过 I²C 与处理器通信。
Linux 内核提供了 I²C 子系统,用于统一管理控制器、总线设备和对应驱动。
4.2 I²C 子系统中的主要对象
|---------------|--------------------|
| 对象 | 作用 |
| i2c_adapter | 表示 I²C 控制器或适配器 |
| i2c_client | 表示挂接在总线上的具体 I²C 设备 |
| i2c_driver | 表示用于操作某类 I²C 设备的驱动 |
| i2c_algorithm | 描述底层 I²C 传输实现 |
通常情况下,I²C 控制器驱动负责实现底层传输能力,而具体设备驱动负责与传感器或其他芯片进行通信。
4.3 I²C 设备树描述
I²C 设备通常放在对应 I²C 控制器节点的子节点中。例如:
&i2c1 {
status = "okay";
sensor@48 {
compatible = "demo,sensor";
reg = <0x48>;
status = "okay";
};
};
这里的 0x48 是示例设备地址,实际地址应根据具体芯片的地址配置确定。
compatible 用于匹配驱动,reg 在此类 I²C 子节点中通常表示设备的 I²C 地址。不要将这个地址直接当作处理器的内存映射寄存器地址。
4.4 I²C 驱动的基本结构
I²C 设备驱动通常需要:
1.编写设备树匹配表。
2.实现 probe() 和设备移除处理。
3.在初始化过程中获取设备信息。
4.通过 I²C 子系统提供的接口读写设备寄存器。
5.根据具体芯片协议解析数据。
具体的读写方式取决于芯片的数据手册。有些设备需要先写入寄存器地址,再读取数据;另一些设备则采用不同的通信时序。
因此,编写 I²C 驱动之前,应先阅读目标芯片的数据手册,确认寄存器地址、数据格式和通信要求。
五、IIO 子系统简介
IIO(Industrial I/O)是 Linux 内核中用于管理多种传感器和数据采集设备的子系统。
例如,部分加速度计、陀螺仪、光照传感器和 ADC 设备,可以使用 IIO 框架向内核和用户空间提供测量数据。
相较于为每一种传感器设计完全独立的接口,IIO 提供了一套较统一的设备模型和数据访问方式。
不过,并非所有传感器都必须使用 IIO。实际选择应根据设备类型、内核已有驱动以及项目需求决定。
六、USB 子系统简介
USB 是常见的外部设备连接方式。Linux USB 子系统负责管理 USB 总线、设备枚举、接口以及驱动匹配。
在开发 USB 设备驱动时,需要了解设备描述符、接口描述符、端点以及数据传输方式等基本概念。
与 I²C 驱动不同,USB 设备的识别与匹配通常依赖 USB 设备或接口的相关标识信息。具体驱动结构和传输接口,应根据设备类型与目标内核版本确定。
本文只对 USB 子系统进行基础介绍,后续若需要开发具体 USB 设备驱动,还应单独学习 USB 的枚举流程和数据传输机制。
七、驱动调试思路
7.1 platform 驱动没有进入 probe
重点检查:
驱动是否成功注册。
设备树节点是否启用。
compatible 字符串是否一致。
实际启动使用的 DTB 是否包含设备节点。
相关设备是否成功创建。
7.2 I²C 设备无法通信
可以从以下方向排查:
I²C 控制器是否启用。
SDA、SCL 引脚复用和电气配置是否正确。
设备地址是否正确。
外设是否供电。
上拉电阻和硬件连线是否正常。
通信时序和寄存器访问方式是否符合芯片手册。
如果系统提供了 i2c-tools,还可以使用相应工具辅助检查总线设备,但使用前应确认目标系统已经安装工具,并注意不要随意扫描或写入正在工作的设备。
7.3 定时器行为异常
应检查定时器是否正确初始化、到期时间是否合理,以及回调函数中是否执行了不允许的操作。
八、总结
内核定时器、platform 总线和 I²C 子系统分别解决了不同的问题。
定时器适用于延迟触发和简单的周期性任务;platform 驱动模型用于管理许多片上设备,并通过设备与驱动匹配完成初始化;I²C 子系统则为总线上的外设提供统一的驱动框架。
IIO 和 USB 也属于 Linux 设备开发中常见的知识领域,但其内部机制和适用场景各不相同,应根据项目需求进一步学习。
学习驱动开发时,建议先掌握设备树与 GPIO,再理解中断和同步机制,最后学习 platform、I²C 等设备框架。这样更容易建立完整的 Linux 驱动知识体系。