3、Linux内核与设备驱动基础:内核模块编程、字符设备驱动框架、platform总线与设备树、GPIO与中断驱动开发
说实话,很多做嵌入式Linux的朋友,一开始都被驱动开发给吓住了。我当年也是,看着那堆内核代码,心里直打鼓。但后来我发现,只要你把几个核心概念吃透了,驱动开发其实就是个搭积木的活儿。
这一章,咱们就聊聊驱动开发里最基础、也最常用的几个东西。内核模块怎么编?字符设备怎么搞?设备树和platform总线是干嘛的?还有GPIO和中断,这些在气象数据采集里天天都要打交道。
3.1 内核模块编程——驱动的最小单元
Linux内核是个庞然大物,但好在它支持模块化。说白了,就是你可以把一段代码编译成一个.ko文件,然后动态地加载到内核里,或者卸载掉。这比每次改个驱动都要重新编译整个内核,可方便太多了。
我习惯把内核模块比作「乐高积木」。你写一个模块,就是造一块积木,然后插到内核这个「大底板」上。模块之间可以互相调用,但要注意符号的导出。
一个最简单的内核模块,长这样:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_driver_init(void)
{
printk(KERN_INFO "Hello, 气象数据采集模块加载!\n");
return 0;
}
static void __exit my_driver_exit(void)
{
printk(KERN_INFO "模块卸载,再见!\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("你的名字");
MODULE_DESCRIPTION("一个简单的气象数据采集驱动模块");
你看,结构很清晰。init函数在模块加载时执行,exit函数在卸载时执行。这里有个坑,我刚开始写的时候经常犯——printk的日志级别。如果你在终端看不到输出,多半是级别设低了,试试KERN_ALERT。
printk,确认模块确实加载成功了。这招虽然土,但管用。
3.2 字符设备驱动框架——让应用层和硬件对话
气象数据采集,说白了就是读传感器数据。传感器在Linux里,通常被抽象成一个字符设备。你想想看,应用层程序通过open、read、write这些标准接口,就能操作硬件,多优雅。
字符设备驱动的核心,是file_operations结构体。它就像一张函数表,告诉内核:当用户程序调用read时,该执行驱动里的哪个函数。
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_release,
};
我记得有一次做温湿度传感器驱动,用户态程序读到的数据总是乱码。查了半天,发现是my_read函数里,没有把数据从内核空间拷贝到用户空间。嗯,这里要注意,copy_to_user这个函数不能少。
注册字符设备的流程,我总结了三步:
- 分配设备号——用
alloc_chrdev_region或者register_chrdev_region - 初始化cdev——用
cdev_init把file_operations挂上去 - 添加设备——用
cdev_add告诉内核
alloc_chrdev_region让内核动态分配,省心又安全。
3.3 platform总线与设备树——驱动和设备怎么配对
你想想看,以前写驱动,硬件地址、中断号都硬编码在代码里。换个板子,就得改代码重新编译。这多麻烦。
设备树(Device Tree)的出现,就是为了解决这个问题。它把硬件信息从驱动代码里剥离出来,放到一个.dts文件里。驱动只需要说「我要用哪个设备」,剩下的由内核去匹配。
platform总线,就是设备和驱动之间的「红娘」。它负责把设备树里描述的硬件,和对应的驱动绑在一起。
一个典型的设备树节点,比如我们的气压传感器:
bmp280@76 {
compatible = "bosch,bmp280";
reg = <0x76>;
};
驱动这边,只需要声明自己支持哪个compatible:
static const struct of_device_id bmp280_of_match[] = {
{ .compatible = "bosch,bmp280" },
{ /* 结束标记 */ }
};
MODULE_DEVICE_TABLE(of, bmp280_of_match);
static struct platform_driver bmp280_driver = {
.probe = bmp280_probe,
.remove = bmp280_remove,
.driver = {
.name = "bmp280",
.of_match_table = bmp280_of_match,
},
};
module_platform_driver(bmp280_driver);
当内核启动时,它会遍历设备树,找到compatible为"bosch,bmp280"的节点,然后调用bmp280_probe函数。在probe里,你可以拿到设备的寄存器地址、中断号等信息。
3.4 GPIO与中断驱动开发——响应外部事件
气象数据采集里,很多传感器都有数据就绪引脚。比如,当气压传感器采集到新数据时,它会拉高一个GPIO引脚。这时候,驱动就需要通过中断来响应,而不是傻傻地轮询。
GPIO的操作,在Linux里已经封装得很好了。你可以用gpio_request申请一个引脚,用gpio_direction_input设置方向,用gpio_get_value读取电平。
中断的注册,稍微复杂一点。你需要先拿到中断号,然后注册中断处理函数:
static irqreturn_t bmp280_irq_handler(int irq, void *dev_id)
{
struct bmp280_dev *bmp280 = (struct bmp280_dev *)dev_id;
// 读取数据
bmp280_read_data(bmp280);
// 唤醒等待队列,通知应用层
wake_up_interruptible(&bmp280->wq);
return IRQ_HANDLED;
}
static int bmp280_probe(struct platform_device *pdev)
{
// ... 其他初始化代码 ...
int irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(&pdev->dev, "获取中断号失败\n");
return irq;
}
ret = request_irq(irq, bmp280_irq_handler,
IRQF_TRIGGER_RISING,
"bmp280", bmp280);
if (ret) {
dev_err(&pdev->dev, "注册中断失败\n");
return ret;
}
return 0;
}
这里有个细节,我特别想强调——中断上下文不能睡眠。你在中断处理函数里,不能用kmalloc带GFP_KERNEL标志,也不能用mutex_lock。因为中断处理时,调度器是停掉的。
tasklet或者workqueue把活儿推到下半部去干。
说到GPIO,还有一个常用的功能——输出控制。比如控制加热器、风扇,或者LED指示灯。用gpio_direction_output设置方向,然后用gpio_set_value设置电平,就这么简单。
好了,这一章的内容就这些。内核模块、字符设备、platform总线、设备树、GPIO和中断,这几个概念你吃透了,嵌入式Linux驱动开发的大门就算打开了。下一章,咱们就真刀真枪地写一个完整的气象数据采集驱动。