第1章:驱动框架基础:Linux设备驱动模型简介
各位同学,今天我们来聊聊驱动框架的基础。说实话,我刚入行那会儿,对Linux设备驱动模型也是一头雾水。那时候写驱动就是简单粗暴地注册一个字符设备,然后跟硬件直接打交道。后来项目越做越大,发现这种"野路子"根本撑不住——代码复用性差、调试困难、移植更是噩梦。
嗯,所以Linux内核团队搞出了这套设备驱动模型。说白了,就是把硬件和驱动的关系规范化、标准化。你想想看,一个SoC上可能有几十个外设,每个外设的驱动都自己搞一套注册方式,那不乱套了?
1.1 为什么需要设备驱动模型?
我遇到过不少刚入行的工程师,觉得设备驱动模型就是个"花架子",直接操作硬件寄存器多痛快。但实际项目里,这种想法会吃大亏。
设备驱动模型要解决的核心问题有三个:
- 解耦:把硬件设备和驱动程序分开管理
- 标准化:统一设备注册、发现、匹配的流程
- 可移植性:同样的驱动代码,换颗芯片也能用
举个例子,我在做一款智能手表项目时,需要同时支持多个厂家的加速度传感器。如果没有设备驱动模型,每个传感器驱动都得自己写一套probe、remove、suspend/resume逻辑。有了模型之后,只需要实现核心的读写函数,剩下的框架都帮你搞定了。
1.2 三大核心:platform bus、device、driver
Linux设备驱动模型里,最核心的就是这三个概念。我习惯把它们比作"相亲"的过程——device是"征婚者",driver是"应征者",bus就是那个"媒人"。
1.2.1 platform bus
platform bus是Linux内核中一种虚拟的总线。为什么叫"虚拟"?因为它不像I2C、SPI那样有物理总线。它专门用来管理那些"挂在CPU内存空间上"的设备——说白了,就是那些直接通过地址访问寄存器的外设。
我在项目中遇到过一种情况:一个UART控制器,在芯片手册里说它挂在APB总线上,但在Linux驱动里,它其实是通过platform bus来管理的。为什么?因为APB总线是芯片内部的物理总线,Linux不直接管这个,它用platform bus来抽象。
关键点:platform bus负责维护两个链表——device链表和driver链表。当有新的device或driver注册时,bus会遍历另一个链表,看看有没有匹配的。
1.2.2 device
device代表一个硬件设备。在设备树(Device Tree)流行的今天,device通常由设备树节点描述。比如:
uart1: serial@10001000 {
compatible = "vendor,uart-v1";
reg = <0x10001000 0x1000>;
interrupts = <33>;
clocks = <&clk_uart>;
};
这段设备树描述了一个UART设备,它的寄存器基地址是0x10001000,中断号是33。内核启动时,会解析设备树,为每个节点创建一个platform_device结构体。
我个人习惯在写驱动前,先把设备树仔细看一遍。因为很多坑都藏在设备树的属性里。比如reg属性的大小对不对?中断号有没有冲突?这些在设备树里就能发现。
1.2.3 driver
driver就是驱动本身。它包含了一组操作函数,比如probe、remove、suspend、resume。当driver和device匹配成功后,内核会调用driver的probe函数。
static struct platform_driver my_uart_driver = {
.probe = my_uart_probe,
.remove = my_uart_remove,
.driver = {
.name = "my_uart",
.of_match_table = my_uart_of_match,
},
};
module_platform_driver(my_uart_driver);
你看,这个驱动注册非常简单。module_platform_driver宏帮我们做了很多事情。但要注意,probe函数里才是真正的"重头戏"——申请资源、初始化硬件、注册字符设备等等。
我的经验:probe函数里一定要做错误处理。我曾经在一个项目中,probe函数里申请了三个资源,第二个申请失败了,结果第一个资源没释放,导致内存泄漏。后来我养成了习惯:每个资源申请后,如果后续失败,一定要goto到对应的错误处理标签。
1.3 驱动与硬件层的交互方式
驱动写好了,怎么跟硬件打交道?这里有几个常用的方式:
1.3.1 内存映射I/O(MMIO)
这是最常用的方式。通过ioremap把物理地址映射到内核虚拟地址空间,然后直接读写寄存器。
static int my_uart_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *base;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
// 读写寄存器
writel(0x01, base + UART_CTRL_REG);
val = readl(base + UART_STATUS_REG);
return 0;
}
这里有个细节:devm_ioremap_resource是"managed"版本,它会在驱动卸载时自动释放映射。我强烈建议用这种带devm_前缀的函数,能省去很多资源管理的麻烦。
1.3.2 中断处理
硬件事件来了,总不能一直轮询吧?中断是必须的。
static irqreturn_t my_uart_irq_handler(int irq, void *dev_id)
{
struct my_uart_dev *uart = dev_id;
u32 status;
status = readl(uart->base + UART_STATUS_REG);
if (status & RX_READY) {
// 处理接收数据
}
return IRQ_HANDLED;
}
static int my_uart_probe(struct platform_device *pdev)
{
int irq, ret;
irq = platform_get_irq(pdev, 0);
ret = devm_request_irq(&pdev->dev, irq, my_uart_irq_handler,
IRQF_TRIGGER_HIGH, "my_uart", uart);
// ...
}
避坑指南:我曾经在中断处理函数里调用了printk,结果导致系统死锁。记住,中断上下文里不能调用可能引起睡眠的函数。比如kmalloc(GFP_KERNEL)就不行,要用GFP_ATOMIC。
1.3.3 DMA传输
对于大数据量的传输,比如音频、视频,用CPU一个一个字节搬数据太浪费了。这时候要用DMA。
DMA的交互稍微复杂一些,需要配置DMA控制器、申请DMA缓冲区、设置传输描述符等。但好处是CPU可以去做别的事情,传输完成后DMA会触发中断通知你。
1.4 匹配机制:设备树与驱动如何"牵手"
前面说了,device和driver通过bus来匹配。那具体怎么匹配的呢?
核心就是设备树里的compatible属性和驱动里的of_match_table。比如:
// 设备树
compatible = "vendor,uart-v1";
// 驱动
static const struct of_device_id my_uart_of_match[] = {
{ .compatible = "vendor,uart-v1", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_uart_of_match);
当内核启动时,platform bus会遍历所有device,对每个device,遍历driver链表,比较compatible字符串。匹配上了,就调用probe。
这里有个小技巧:compatible字符串的命名规范是"厂商,设备名-版本"。比如"ti,am3359-uart"。这样能避免命名冲突。
1.5 知识体系总览
为了让大家更直观地理解,我画了一张图:
这张图把整个流程串起来了。从上到下看:Platform Bus负责匹配Device和Driver,匹配成功后,Driver通过MMIO、中断、DMA等方式与硬件交互。
1.6 小结
这一章我们讲了Linux设备驱动模型的三个核心概念:platform bus、device、driver。说白了,就是一套标准化的"相亲"流程。device描述硬件有什么资源,driver描述怎么操作这些资源,bus负责牵线搭桥。
驱动与硬件的交互方式主要有三种:MMIO直接读写寄存器、中断处理异步事件、DMA批量传输数据。这三种方式在实际项目中经常组合使用。
我个人觉得,理解这套模型的关键在于"抽象"二字。不要把它想得太复杂,它就是一套帮你管理硬件资源的框架。你只要按照它的规则来写驱动,剩下的框架帮你搞定。
给新手的建议:刚开始学驱动开发,不要急着看复杂的驱动代码。先找一个简单的GPIO驱动或者UART驱动,把probe函数里的每一行代码都搞清楚。我当时就是这么过来的,一个简单的LED驱动,反复看了三天,才真正理解了platform_driver的工作流程。
公众号:蓝海资料掘金营,微信deep3321