news 2026/3/11 18:46:37

基于platform的字符设备驱动设计实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于platform的字符设备驱动设计实践

从零构建一个基于 platform 的字符设备驱动:不只是“Hello World”

你有没有遇到过这种情况——在写一个嵌入式 Linux 驱动时,直接把硬件地址写死在代码里?比如:

#define MY_ADC_BASE 0x12000000

然后用ioremap(MY_ADC_BASE, SZ_4K)映射寄存器。看似能跑,但一旦换了块板子、改了地址,就得重新编译驱动……更别提多个 SoC 平台共用同一份驱动的噩梦。

这正是现代 Linux 内核引入platform总线机制要解决的问题:让驱动不再“绑定”硬件细节

今天我们就来手把手实现一个完整的、基于platform的字符设备驱动。这不是简单的 “Hello World”,而是一个贴近真实项目的工程实践模板,涵盖设备树匹配、资源管理、字符设备注册和安全释放等关键环节。


为什么需要 platform 驱动?

在传统的 PCI 或 USB 设备中,设备可以“自报家门”:插上就能被系统发现并分配资源。但大多数嵌入式 SoC 上的外设(如 ADC、PWM 控制器)是焊死在芯片里的,它们没有即插即用能力。

Linux 内核怎么办?搞了个“虚拟总线”——platform_bus_type,它不对应任何物理总线,而是用来承载那些“无法自我识别”的平台级设备。

于是就有了两个核心角色:
-platform_device:描述一个静态存在的设备(由设备树或板级代码创建)
-platform_driver:提供对该设备的操作逻辑

两者通过名字或.compatible字段自动匹配,实现“解耦”。

📌 关键洞察:
platform框架的本质,是把硬件资源配置权交给系统(通常是设备树),驱动只负责“使用”。这种设计极大提升了可移植性和模块化程度。


核心结构解析:device 和 driver 如何配对?

platform_device—— 硬件信息的容器

这个结构体并不需要你在驱动里手动定义。它通常来自:
- 旧方式:板级 C 文件中的静态数组(已淘汰)
- 新方式:设备树节点自动转换而来

例如,我们有一个内存映射的 ADC 控制器:

// myboard.dts myadc: adc@12000000 { compatible = "mycompany,mychardev"; reg = <0x12000000 0x1000>; // 寄存器范围:1KB interrupts = <GIC_SPI 10 IRQ_TYPE_LEVEL_HIGH>; // 使用 SPI 中断,号为 10 clocks = <&clkc 12>; // 依赖某个时钟源 };

当内核启动时,of_platform_default_populate()会扫描所有未匹配的节点,并为每个带有compatible属性的节点生成一个platform_device实例。

你可以把它理解为:“这是我的设备说明书,请找对应的司机来开。”


platform_driver—— 真正干活的人

这才是我们要写的部分。它的骨架长这样:

static struct platform_driver my_platform_driver = { .probe = my_platform_probe, .remove = my_platform_remove, .driver = { .name = "mychardev", .of_match_table = of_match_ptr(my_platform_dt_ids), .owner = THIS_MODULE, }, };

重点看.of_match_table,它是连接设备树的关键桥梁:

static const struct of_device_id my_platform_dt_ids[] = { { .compatible = "mycompany,mychardev" }, { } /* 必须以空项结尾 */ };

只要设备树里的compatible和这里的字符串一致,内核就会调用.probe函数。

✅ 小贴士:推荐始终使用设备树匹配而非.name匹配,因为前者更精确、更灵活,也符合主流开发规范。


字符设备怎么接入?四步走策略

现在问题来了:如何在一个platform_driver里注册一个用户态可用的/dev/mychardev

答案是在probe()函数中完成以下四个步骤:

  1. 获取设备资源
  2. 映射寄存器空间
  3. 注册字符设备
  4. 创建设备文件节点

我们逐个拆解。


第一步:从 device 获取资源

不要硬编码地址!要用标准 API 动态获取:

static int my_platform_probe(struct platform_device *pdev) { struct resource *res; /* 获取内存资源 */ res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(&pdev->dev, "No memory resource\n"); return -ENXIO; } /* 获取中断资源 */ int irq = platform_get_irq(pdev, 0); if (irq < 0) return irq; dev_info(&pdev->dev, "Got mem: %pa, irq: %d\n", &res->start, irq); ... }

platform_get_resource()是通用接口,支持 MEM、IRQ、DMA 等多种类型。第二个参数是索引,适用于多组同类资源的情况。


第二步:安全映射寄存器(强烈建议用 devm_*)

传统做法是ioremap()+ 手动iounmap(),但容易漏掉释放导致内存泄漏。

更好的方式是使用managed resource API(devm_系列),它们会在驱动卸载或 probe 失败时自动清理:

void __iomem *base; base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(base)) return PTR_ERR(base);

你看,连错误处理都简洁了。而且即使后面某一步出错,也不用手动回滚iounmap—— 内核已经帮你记住了。


第三步:动态注册字符设备

字符设备的核心是struct cdevfile_operations。我们先定义操作函数集:

static ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *offset) { dev_info(file->f_inode->i_private, "read invoked\n"); return 0; } static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { dev_info(file->f_inode->i_private, "ioctl cmd=%u\n", cmd); return 0; } static const struct file_operations fops = { .owner = THIS_MODULE, .read = my_read, .write = my_write, .unlocked_ioctl = my_ioctl, .open = my_open, .release = my_release, };

接着动态申请设备号并注册:

static dev_t dev_num; /* 主次设备号 */ static struct class *my_class; /* 设备类 */ static struct cdev my_cdev; /* 字符设备 */ int ret = alloc_chrdev_region(&dev_num, 0, 1, "mychardev"); if (ret < 0) { dev_err(&pdev->dev, "Failed to allocate device number\n"); return ret; }

为什么不推荐register_chrdev_region()?因为它要求你知道主设备号,而很多号已被占用或保留,容易冲突。动态分配才是现代驱动的标配。


第四步:自动生成 /dev 节点

以前要手动mknod,现在全靠 udev 规则配合class_createdevice_create自动搞定:

/* 创建设备类 */ my_class = class_create(THIS_MODULE, "myclass"); if (IS_ERR(my_class)) { ret = PTR_ERR(my_class); goto fail_class; } /* 在 /sys/class/myclass 下创建设备,并自动生成 /dev/mychardev */ struct device *dev = device_create(my_class, NULL, dev_num, NULL, "mychardev"); if (IS_ERR(dev)) { ret = PTR_ERR(dev); goto fail_device; }

这样一来,模块一加载,/dev/mychardev就出现了,完全无需用户干预。


完整驱动框架整合

下面是我们整合后的完整驱动代码(精简版):

#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/io.h> #define DEVICE_NAME "mychardev" #define CLASS_NAME "myclass" static dev_t dev_num; static struct cdev my_cdev; static struct class *my_class; static const struct of_device_id my_platform_dt_ids[] = { { .compatible = "mycompany,mychardev" }, { } /* NULL terminator */ }; MODULE_DEVICE_TABLE(of, my_platform_dt_ids); /* 文件操作函数略... */ static int my_platform_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; int ret; /* --- 获取资源 --- */ res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; /* --- 映射寄存器 --- */ base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(base)) return PTR_ERR(base); /* --- 分配设备号 --- */ ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME); if (ret < 0) { dev_err(&pdev->dev, "alloc_chrdev_region failed\n"); return ret; } /* --- 注册 cdev --- */ cdev_init(&my_cdev, &fops); my_cdev.owner = THIS_MODULE; ret = cdev_add(&my_cdev, dev_num, 1); if (ret < 0) { unregister_chrdev_region(dev_num, 1); return ret; } /* --- 创建设备类与节点 --- */ my_class = class_create(THIS_MODULE, CLASS_NAME); if (IS_ERR(my_class)) { cdev_del(&my_cdev); unregister_chrdev_region(dev_num, 1); return PTR_ERR(my_class); } if (IS_ERR(device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME))) { class_destroy(my_class); cdev_del(&my_cdev); unregister_chrdev_region(dev_num, 1); return -ENODEV; } /* 保存私有数据(可选) */ platform_set_drvdata(pdev, base); dev_info(&pdev->dev, "Driver probed successfully, major=%d\n", MAJOR(dev_num)); return 0; } static int my_platform_remove(struct platform_device *pdev) { device_destroy(my_class, dev_num); class_destroy(my_class); cdev_del(&my_cdev); unregister_chrdev_region(dev_num, 1); dev_info(&pdev->dev, "Driver removed\n"); return 0; } static struct platform_driver my_platform_driver = { .probe = my_platform_probe, .remove = my_platform_remove, .driver = { .name = "mychardev", .of_match_table = of_match_ptr(my_platform_dt_ids), .owner = THIS_MODULE, }, }; module_platform_driver(my_platform_driver); MODULE_AUTHOR("Engineer X"); MODULE_DESCRIPTION("A real-world platform character device driver"); MODULE_LICENSE("GPL");

工程最佳实践清单

写驱动不是跑通就行,还要考虑健壮性、可维护性和兼容性。以下是我在实际项目中总结的几条铁律:

项目推荐做法
资源申请全部使用devm_*系列(devm_kmalloc,devm_request_irq,devm_ioremap_resource
错误处理使用goto统一跳转到 cleanup 标签,避免重复释放
日志输出使用dev_info/dev_err替代printk,自带设备上下文
并发控制多进程访问时加mutexsemaphore
设备树检查添加MODULE_DEVICE_TABLE(of, ...),确保 depmod 正常工作
模块加载使用module_platform_driver()宏,省去 init/exit 函数

举个例子,如果你用了中断:

ret = devm_request_threaded_irq(&pdev->dev, irq, my_interrupt_handler, my_thread_fn, IRQF_ONESHOT, "mychardev", pdev);

不仅不用手动释放,还能保证在 probe 失败时自动注销。


常见坑点与避坑秘籍

❌ 坑一:忘记 MODULE_DEVICE_TABLE

现象:驱动加载时报No such device or address,明明 compatible 对得上。

原因:没有导出设备表,modprobe 找不到匹配项。

✅ 解法:加上这一行!

MODULE_DEVICE_TABLE(of, my_platform_dt_ids);

❌ 坑二:cdev_del 放错了位置

现象:模块卸载后再次加载失败,提示“Device or resource busy”。

原因:cdev_add成功但后续步骤失败(如 device_create),却没有调用cdev_del

✅ 解法:严格按照申请顺序反向释放,且每一步都要判断是否成功再释放。


❌ 坑三:open 函数返回负数却没清零 file->private_data

后果:后续 read/write 可能访问非法指针。

✅ 解法:在 open 失败路径上务必确保不会留下脏状态。


这套模式能用在哪?

这套基于platform+ 字符设备的设计范式,广泛应用于各种专用控制器场景:

  • 自定义 ADC/DAC 模块
  • FPGA 扩展接口(如 PCIe endpoint 上的逻辑块)
  • 特殊通信协议处理器
  • 工业 I/O 板卡
  • 音频采集前端
  • 功率监控单元

只要你面对的是“内存映射寄存器 + 固定资源”的硬件模型,这套方案就是首选。


结语:掌握这套思维,你就掌握了内核驱动的“普通话”

很多人觉得 Linux 驱动难学,其实是没抓住主线。

platform驱动 + 设备树 + 字符设备注册,构成了现代嵌入式 Linux 驱动开发的“黄金三角”。掌握了它,你就不只是会抄 demo,而是真正理解了内核的设备模型思想。

下一步你可以尝试:
- 把这个驱动改成支持多个次设备号(minor)
- 加入 miscdevice 简化注册流程
- 引入 DMAengine 进行高效数据搬运
- 结合 input subsystem 上报事件

但无论走多远,起点都是今天这一课。

如果你正在调试自己的 platform 驱动,欢迎留言交流具体问题,我们一起踩坑、填坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/11 5:46:07

2025最新!继续教育8个AI论文工具测评:写论文不再难

2025最新&#xff01;继续教育8个AI论文工具测评&#xff1a;写论文不再难 2025年继续教育AI论文工具测评&#xff1a;为何需要这份榜单&#xff1f; 在当前学术环境日益严格的背景下&#xff0c;继续教育群体在撰写论文时面临诸多挑战&#xff0c;包括时间紧张、文献检索困难、…

作者头像 李华
网站建设 2026/3/9 10:47:05

SankeyMATIC:零代码打造专业级流程图的终极指南

SankeyMATIC&#xff1a;零代码打造专业级流程图的终极指南 【免费下载链接】sankeymatic Make Beautiful Flow Diagrams 项目地址: https://gitcode.com/gh_mirrors/sa/sankeymatic 还在为复杂的数据流向图而头疼吗&#xff1f;SankeyMATIC让流程图制作变得像写购物清单…

作者头像 李华
网站建设 2026/3/9 22:12:09

效率倍增!Windows、Mac、Linux 三大系统常用快捷键终极指南

目录效率倍增&#xff01;Windows、Mac、Linux 三大系统常用快捷键终极指南1. 预备知识&#xff1a;认识你的键盘修饰键核心修饰键对照表2. 第一章&#xff1a;文档编辑与通用操作&#xff08;必修&#xff09;复制、粘贴与撤销3. 第二章&#xff1a;系统控制与窗口管理窗口切换…

作者头像 李华
网站建设 2026/3/9 17:04:30

5分钟搞定MobileNetV2模型部署?这份保姆级教程让你零基础上手

5分钟搞定MobileNetV2模型部署&#xff1f;这份保姆级教程让你零基础上手 【免费下载链接】models A collection of pre-trained, state-of-the-art models in the ONNX format 项目地址: https://gitcode.com/gh_mirrors/model/models 还在为模型部署的复杂流程头疼吗…

作者头像 李华
网站建设 2026/3/4 1:22:24

Real-ESRGAN终极指南:5分钟掌握AI图像超分辨率技术

Real-ESRGAN终极指南&#xff1a;5分钟掌握AI图像超分辨率技术 【免费下载链接】Real-ESRGAN Real-ESRGAN aims at developing Practical Algorithms for General Image/Video Restoration. 项目地址: https://gitcode.com/gh_mirrors/real/Real-ESRGAN 还在为模糊的老照…

作者头像 李华
网站建设 2026/3/4 1:22:27

如何快速构建LinkedIn数据采集系统:Python爬虫的完整指南

如何快速构建LinkedIn数据采集系统&#xff1a;Python爬虫的完整指南 【免费下载链接】linkedin_scraper A library that scrapes Linkedin for user data 项目地址: https://gitcode.com/gh_mirrors/li/linkedin_scraper LinkedIn作为全球最大的职业社交平台&#xff0…

作者头像 李华