实战:编写第一个UIO驱动
好,咱们直接动手。这一章我会带你从零搭建一个完整的UIO驱动——从设备树配置、内核模块代码,到用户态程序框架。说白了,就是让你亲眼看看用户态驱动到底怎么跑起来的。
我个人习惯把UIO驱动的开发分成三步走:设备树里先占个坑,内核模块负责把硬件暴露给用户态,最后用户态程序接管一切。每一步都不复杂,但细节容易翻车。我当年第一次写UIO驱动时,就因为在设备树里少配了一个中断属性,折腾了整整一个下午。
设备树配置:给硬件一个身份
设备树(Device Tree)是Linux内核用来描述硬件拓扑的机制。对于UIO来说,我们需要在设备树里声明一个设备节点,告诉内核:这块硬件归UIO管。
来看一个实际的例子。假设我们有一块简单的GPIO控制器,映射在0x43C00000地址上,占用4KB空间,使用中断号42。
// uio-gpio.dtsi
/ {
uio_gpio: uio-gpio@43c00000 {
compatible = "generic-uio";
reg = <0x43c00000 0x1000>;
interrupts = <0 42 4>;
interrupt-parent = <&gic>;
};
};
这里有几个关键点:
- compatible:必须设为"generic-uio"。这是UIO框架的标准匹配字符串,内核看到它就知道要用UIO驱动来绑定。
- reg:描述物理地址和大小。0x43C00000是起始地址,0x1000是4KB空间。
- interrupts:中断描述。第一个0表示SPI中断类型,42是中断号,4是触发方式(高电平触发)。
注意:我曾经在项目里把interrupt-parent写成了中断控制器的别名,结果内核死活不认。后来发现必须用完整的引用路径,比如<&gic>。这个坑踩过一次就记住了。
如果你用的是设备树覆盖(Device Tree Overlay),写法也类似:
// uio-gpio-overlay.dts
/dts-v1/;
/plugin/;
/ {
fragment@0 {
target = <&amba>;
__overlay__ {
uio_gpio: uio-gpio@43c00000 {
compatible = "generic-uio";
reg = <0x43c00000 0x1000>;
interrupts = <0 42 4>;
interrupt-parent = <&gic>;
};
};
};
};
内核模块代码:UIO驱动的骨架
设备树配好了,接下来写内核模块。UIO内核模块的核心工作就两件:注册UIO设备和实现中断处理。其他事情——比如内存映射、文件操作——UIO框架都帮你搞定了。
来看一个最小化的UIO驱动代码:
// uio_gpio.c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/uio_driver.h>
#include <linux/interrupt.h>
static struct uio_info gpio_uio_info = {
.name = "uio_gpio",
.version = "1.0",
.irq = UIO_IRQ_CUSTOM, // 使用自定义中断处理
};
static irqreturn_t gpio_uio_handler(int irq, struct uio_info *dev_info)
{
// 这里处理硬件中断
// 实际项目中需要读取硬件状态寄存器确认中断源
printk(KERN_INFO "UIO GPIO interrupt received\n");
return IRQ_HANDLED;
}
static int gpio_uio_probe(struct platform_device *pdev)
{
struct resource *res;
int ret;
// 获取设备树中的内存资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "No memory resource\n");
return -ENODEV;
}
// 设置UIO内存映射信息
gpio_uio_info.mem[0].addr = res->start;
gpio_uio_info.mem[0].size = resource_size(res);
gpio_uio_info.mem[0].memtype = UIO_MEM_PHYS;
// 设置中断处理函数
gpio_uio_info.handler = gpio_uio_handler;
gpio_uio_info.irq = platform_get_irq(pdev, 0);
// 注册UIO设备
ret = uio_register_device(&pdev->dev, &gpio_uio_info);
if (ret) {
dev_err(&pdev->dev, "UIO registration failed\n");
return ret;
}
platform_set_drvdata(pdev, &gpio_uio_info);
dev_info(&pdev->dev, "UIO GPIO driver probed\n");
return 0;
}
static int gpio_uio_remove(struct platform_device *pdev)
{
struct uio_info *info = platform_get_drvdata(pdev);
uio_unregister_device(info);
dev_info(&pdev->dev, "UIO GPIO driver removed\n");
return 0;
}
static const struct of_device_id gpio_uio_of_match[] = {
{ .compatible = "generic-uio" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, gpio_uio_of_match);
static struct platform_driver gpio_uio_driver = {
.probe = gpio_uio_probe,
.remove = gpio_uio_remove,
.driver = {
.name = "uio_gpio",
.of_match_table = gpio_uio_of_match,
},
};
module_platform_driver(gpio_uio_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("UIO GPIO Driver Example");
这段代码里,我特别想强调几个地方:
- UIO_IRQ_CUSTOM:告诉UIO框架,中断处理由我们自己实现。如果你不需要中断,可以设为UIO_IRQ_NONE。
- mem[0]:UIO支持最多5个内存区域。对于大多数简单设备,一个就够了。
- platform_get_irq:从设备树中读取中断号。注意返回值检查,我见过有人忘了检查,结果中断号是负数还照用不误。
小技巧:调试UIO内核模块时,可以用modprobe uio_gpio加载,然后用ls /dev/uio*查看生成的设备节点。如果没看到,多半是设备树匹配出了问题。
用户态程序框架:接管硬件控制权
内核模块写好了,设备节点也生成了。现在轮到用户态程序登场。用户态程序要做的事情很直接:打开设备文件、映射内存、处理中断、操作硬件。
来看一个完整的用户态程序框架:
// uio_app.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <errno.h>
#define UIO_DEVICE "/dev/uio0"
#define MAP_SIZE 0x1000 // 4KB
int main(int argc, char *argv[])
{
int fd;
void *map_base;
volatile uint32_t *reg_base;
int irq_count;
// 1. 打开UIO设备
fd = open(UIO_DEVICE, O_RDWR);
if (fd < 0) {
fprintf(stderr, "Failed to open %s: %s\n",
UIO_DEVICE, strerror(errno));
return -1;
}
// 2. 映射硬件寄存器到用户空间
map_base = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (map_base == MAP_FAILED) {
fprintf(stderr, "mmap failed: %s\n", strerror(errno));
close(fd);
return -1;
}
reg_base = (volatile uint32_t *)map_base;
printf("Hardware registers mapped at %p\n", map_base);
// 3. 读取硬件寄存器(示例:读取ID寄存器)
uint32_t id = reg_base[0];
printf("Hardware ID: 0x%08x\n", id);
// 4. 等待中断
printf("Waiting for interrupt...\n");
if (read(fd, &irq_count, sizeof(irq_count)) < 0) {
fprintf(stderr, "read failed: %s\n", strerror(errno));
} else {
printf("Interrupt received! Count: %d\n", irq_count);
}
// 5. 清理
munmap(map_base, MAP_SIZE);
close(fd);
return 0;
}
这个程序虽然简单,但包含了UIO用户态编程的所有核心步骤:
- 打开设备:/dev/uio0是第一个UIO设备,如果有多个,依次是uio1、uio2...
- 内存映射:mmap把物理地址映射到用户空间,之后就可以直接读写寄存器了。
- 中断处理:对UIO设备文件执行read操作,会阻塞等待中断。中断发生后,read返回中断次数。
核心要点:UIO的中断机制很巧妙。用户态程序通过read等待中断,内核在中断发生时唤醒用户态进程。这样用户态程序就能以事件驱动的方式响应硬件,而不是轮询。
编译与测试
最后,咱们看看怎么编译和测试这套东西。
内核模块的Makefile:
obj-m += uio_gpio.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
用户态程序的编译就简单了:
gcc -o uio_app uio_app.c
测试步骤:
- 加载内核模块:
sudo insmod uio_gpio.ko - 检查设备节点:
ls -l /dev/uio0 - 运行用户态程序:
sudo ./uio_app - 触发硬件中断(比如按个按钮),观察程序输出
注意:访问UIO设备需要root权限,或者把用户加入uio组。我建议在开发阶段直接用sudo,但生产环境一定要做好权限控制。
好了,第一个UIO驱动就这么写完了。你想想看,整个过程其实就三步:设备树声明、内核模块注册、用户态程序映射。没有复杂的字符设备驱动开发,没有繁琐的ioctl接口设计。UIO把最麻烦的部分都封装好了,让我们能专注于硬件逻辑本身。
下一章,我们会深入UIO的中断处理机制,看看用户态程序如何高效响应硬件事件。到时候我会分享一个我在工业控制项目中遇到的中断丢失问题,以及怎么用UIO的特性来解决它。