author: hjjdebug
date: 2026年 05月 11日 星期一 17:04:05 CST
descrip: platform总线,驱动,虚拟设备(实例2)--led闪烁灯(附代码)
文章目录
- [1. 先看虚拟设备文件virt_lec_dev.c](#1. 先看虚拟设备文件virt_lec_dev.c)
-
- [1.1 附代码: virt_led_dev.c](#1.1 附代码: virt_led_dev.c)
- [2. 再看驱动文件,这是真驱动.](#2. 再看驱动文件,这是真驱动.)
-
- [2.1 cdev 名称的问题.](#2.1 cdev 名称的问题.)
- [2.2 platfrom_get_drvdata 问题](#2.2 platfrom_get_drvdata 问题)
- [2.3 关于私有结构指针的获取](#2.3 关于私有结构指针的获取)
- [2.4 关于led_id_table](#2.4 关于led_id_table)
- [2.5 附代码: led_drv.c](#2.5 附代码: led_drv.c)
- [3. 实验.](#3. 实验.)
目的,巩固platform总线,驱动,设备编程架构. 让led灯闪烁
这是一个及其简单的设备,相当于只有一个I/O 脚
在基础架构上
添加了内核定时器timer_list 的使用示例.
添加了完整的cdev接口. 可操控led 的闪烁与停止
echo "stop" \|sudo tee /dev/myled
echo "start" |sudo tee /dev/myled
1. 先看虚拟设备文件virt_lec_dev.c
它需要定义一个platform_device 对象, 这里用了动态内存分配的方法
输入参数,给个设备名称就够了,这里叫"myled"
g_virt_led_pdev = platform_device_alloc(VIRT_LED_PLAT_NAME, -1);
然后,我们为这个设备分配私有数据, 只有一个bool值就够了
struct virt_led_dev_ctx {
// LED 1bit 状态保持寄存器
bool led_state_bit;
};
//内存分配直接分配,不要挂靠dev
struct virt_led_dev_ctx *hw;
hw = devm_kzalloc(&g_virt_led_pdev->dev, sizeof(*hw), GFP_KERNEL);
这个函数是不能调用的,否则当与驱动binding时,出现错误
驱动会有resource present before probe 错误,拒绝binding
正确写法如代码中所示, 直接分配,不要挂靠dev
hw = kzalloc(sizeof(*hw), GFP_KERNEL);
//为数据付初值
hw->led_state_bit = false;
// 把私有数据绑定到platform设备,归设备所有
platform_set_drvdata(g_virt_led_pdev, hw);
// 添加设备到总线
platform_device_add(g_virt_led_pdev);
比platform_device_register 一步注册全局变量更细致一些.
以为那么多的驱动代码, 操作的就是这1bit 数据, 功能虽然少了点,但框架很重要!
1.1 附代码: virt_led_dev.c
cpp
$ cat virt_led_dev.c
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/platform_device.h>
// 设备匹配名,要和驱动严格一致
// 此处的设备名是 /sys/bus/platform/devices/<设备名链接>
// 指向 /sys/devices/platfom/devices/<设备名>
#define DEVICE_NAME "myled"
// 核心:虚拟设备硬件结构体,状态位属于设备本身
struct virt_led_dev_ctx {
// LED 1bit 状态保持寄存器
bool led_state_bit;
};
static struct platform_device *g_pdev_obj;
static int __init virt_led_dev_init(void)
{
//这是platform_device_register 的另一种写法,分步写法. 动态分配内存,挂载私有数据,添加到系统
// 1 分配虚拟platform设备
g_pdev_obj = platform_device_alloc(DEVICE_NAME, -1);
if (!g_pdev_obj) {
pr_err("platform_device_alloc failed\n");
return -ENOMEM;
}
// 2 为设备分配私有硬件资源(含LED状态位)
// 不能用devm_kzalloc 来分配内存,与驱动binding 时,驱动会有resource present before probe 错误,拒绝binding
struct virt_led_dev_ctx *hw;
hw = kzalloc(sizeof(*hw), GFP_KERNEL);
if (!hw) {
platform_device_put(g_pdev_obj);
return -ENOMEM;
}
// LED状态位初始默认熄灭
hw->led_state_bit = false;
// 把私有数据绑定到platform设备,归设备所有
platform_set_drvdata(g_pdev_obj, hw); //意思是 dev->driver_data = data
// 添加设备到总线
int ret = platform_device_add(g_pdev_obj);
if (ret) {
pr_err("platform_device_add failed, ret=%d\n", ret);
platform_device_put(g_pdev_obj);
return ret;
}
pr_info("virt-led-dev: 虚拟设备创建完成,自带LED状态寄存器\n");
return 0;
}
static void __exit virt_led_dev_exit(void)
{
platform_device_del(g_pdev_obj); //从系统中删除设备
platform_device_put(g_pdev_obj); //设备释放资源
pr_info("virt-led-dev: 设备模块卸载\n");
}
module_init(virt_led_dev_init);
module_exit(virt_led_dev_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Virt LED Platform Device · 持有LED状态位");
2. 再看驱动文件,这是真驱动.
框架还是一样,注册平台驱动,驱动名叫"myled", 不要长于20个字符,驱动有限制
id_table 匹配再x86_64上内核默认没有开启,所以还是靠驱动名和设备名匹配来binding.
驱动和设备匹配上会调用probe 函数.
碰到: Resources present before probing, 拒绝probe 的问题
原因: 虚拟设备中,分配内存时挂靠到了dev设备, 应该用独立内存分配. 则可执行到probe函数
2.1 cdev 名称的问题.
alloc_chrdev_region(&devno, 0,子设备个数,"设备名称");
这里的设备名称是/proc/devices 下显示的名称
class_create(THIS_MODULE,"类名称")
类名称是/sys/class/<目录名称>
device_create(类对象指针,NULL,devno,NULL,"设备节点名称");
设备节点名称是/dev/下创建的设备节点名
代码中都已经说明
2.2 platfrom_get_drvdata 问题
在probe函数中, 首次调用 platform_get_drvdata(pdev)
得到的是设备册设置的指针
struct led_dev_ctx *hw = platform_get_drvdata(pdev);
在执行probe 时, 我们分配驱动私有上下文,保留了硬件册的指针.
在probe 结尾,我们重设了drvdata,指向了我们的私有上下文
platform_set_drvdata(pdev, ctx);
这相当于修改了原来设备册设置的指针, 只所以要改它,是因为remove时(驱动卸载会调用)
要拿到这个私有指针,除非你用全局设置,否则需要保留到pdev中
2.3 关于私有结构指针的获取
如果把私有结构指针设置成全局变量,则在使用时就很方便,哪个函数都可访问.
不过不是推荐做法, 安全性不好. 这里的安全性指的是不推荐全局变量. 应该用其它方式.
- virt_led_remove 函数中, 它利用了参数pdev, pdev->dev 中的一个指针来保留私有指针.
故可以用platform_get_drvdata(pdev); 来获取到这个指针. - 在led_open 函数中,
它利用了参数inode, inode->i_cdev 指向的是led_drv_ctx 中的 cdev地址,指针往上偏移一点,
就能计算出ctx 地址. 代码中用container_of 宏来简化计算
把拿到的私有地址,存入了filp->private_data 中
3 为啥inode->i_cdev 就与私有指针挂钩了?
因为你调用过
cdev_add(&ctx->cdev, ctx->devno, 1);
内核知道了设备号及你设置的cdev 地址, 内核创建文件节点时,把这个地址放入inode->i_cdev位置,
以后你访问这个设备号时,就可获得这个地址.
-
在read,write 函数中,用filp参数拿到私有地址
struct led_drv_ctx *ctx = filp->private_data;
因为在led_open 中,filp->private_data 已经被赋值
-
blink_timer_fn 怎样获取私有地址? 利用参数t
它用了from_timer 宏
struct led_drv_ctx *ctx = from_timer(ctx, t, blink_timer);
与container_of 宏类似(其实就是container_of宏)
t 是结构成员变量blink_timer 的地址,当然可推算出结构体变量的起始地址
参数t 是何时赋值的? timer_setup 函数传入的.
timer_setup(&ctx->blink_timer, blink_timer_fn, 0);
2.4 关于led_id_table
设置.id_table 表,本意是可通过id_table来匹配设备和驱动.
但ubuntu 系统默认未开启id_table 匹配功能, 所以实际是个摆设,还得靠驱动名和设备名匹配来binding
但arm 是默认开启的,可由id来匹配, 想要x86_64用id匹配,需重配置内核.
如此则没有疑问了. 下面给出代码:
2.5 附代码: led_drv.c
cpp
$cat led_drv.c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/timer.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/mod_devicetable.h>
#pragma GCC diagnostic ignored "-Wunused-result"
#define BLINK_INTERVAL 500
//此处的驱动名,是/sys/bus/platorm/drivers/<驱动名>,且必需要与虚拟设备名一致
#define DRIVER_NAME "myled"
//此处的设备名是/proc/devices 下的名称, 设备节点名,是/dev/<设备节点名>
#define DEV_NAME "myled"
#define DEV_NODE_NAME DEV_NAME
// 和设备模块完全一致的硬件结构体声明, 持有硬件的一个指针
struct led_dev_ctx {
bool led_out_bit;
};
// 驱动自己的私有控制结构体,驱动自己持有
struct led_drv_ctx {
struct device *dev; //持有内核的struct device 指针
dev_t devno;
struct cdev cdev; //字符设备对象
struct class *cls; //一个类指针
struct timer_list blink_timer; //内核定时器实例
// 持有指向设备硬件的指针,不拷贝状态
struct led_dev_ctx *hw_ref;
};
// 定时器:只翻转【设备侧的1bit寄存器】
static void blink_timer_fn(struct timer_list *t)
{
struct led_drv_ctx *ctx = from_timer(ctx, t, blink_timer);
// 操作的永远是设备的状态位
ctx->hw_ref->led_out_bit = !ctx->hw_ref->led_out_bit;
pr_info("[驱动] 输出设备LED状态 = %d\n", ctx->hw_ref->led_out_bit);
//重启内核定时器
mod_timer(&ctx->blink_timer, jiffies + msecs_to_jiffies(BLINK_INTERVAL));
}
// 文件操作:读写设备寄存器
static ssize_t led_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
struct led_drv_ctx *ctx = filp->private_data;
char stat = ctx->hw_ref->led_out_bit ? '1' : '0';
copy_to_user(buf, &stat, 1);
return 1;
}
static ssize_t led_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
struct led_drv_ctx *ctx = filp->private_data;
char _buf[32];
copy_from_user(_buf, buf, 32);
if (strncmp(_buf,"stop",4)==0) //防止stop 后跟回车之类,故用strncmp
{
ctx->hw_ref->led_out_bit = 0;
del_timer(&ctx->blink_timer); //从列表中摘除定时器.
}
else if (strncmp(_buf,"start",5)==0)
{
mod_timer(&ctx->blink_timer, jiffies + msecs_to_jiffies(BLINK_INTERVAL));
}
return 1;
}
static int led_open(struct inode *inode, struct file *filp)
{
//在结构led_drv_ctx 中,知道了cdev地址,找结构开始地址
struct led_drv_ctx *ctx = container_of(inode->i_cdev, struct led_drv_ctx, cdev);
filp->private_data = ctx;
return 0;
}
static const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
};
static void virt_led_dev_release(struct device *dev)
{
// 空合法实现,内核只要知道有release就不会报警告
}
static int virt_led_probe(struct platform_device *pdev)
{
pr_info("in virt_led_probe.\n") ;
// 1. 拿到设备侧绑定的硬件状态
struct led_dev_ctx *hw = platform_get_drvdata(pdev);
if (!hw) return -ENODEV;
// 2. 分配驱动自己的上下文,绝不污染设备drvdata
struct led_drv_ctx *ctx = devm_kzalloc(&pdev->dev, sizeof(*ctx), GFP_KERNEL);
if (!ctx) return -ENOMEM;
// 保存设备硬件指针,读写都会用该指针
ctx->hw_ref = hw;
// 3. 注册字符设备(设备号自动分配),输出设备号, 这是/proc/devices下显示的名字
int ret = alloc_chrdev_region(&ctx->devno, 0, 1, DEV_NAME);
if (ret) return ret;
cdev_init(&ctx->cdev, &led_fops);
cdev_add(&ctx->cdev, ctx->devno, 1);
// 4. 创建/dev/节点, 免去了手工创造节点的麻烦
// 设备是按类划分的,这里是新加了一个类别,类名是/sys/class/<类名>/
ctx->cls = class_create(THIS_MODULE, DEV_NAME "_class");
//在/dev下创建节点,输入参数是设备名DEV_NAME,设备号, 还有cls, 返回struct device
//访问该节点,则可以访问到cdev 关联的 led_fops 操作
ctx->dev = device_create(ctx->cls, NULL, ctx->devno, NULL, DEV_NODE_NAME);
// 关键:给设备绑定release函数,消除警告
ctx->dev->release = virt_led_dev_release;
// 5. 启动闪烁定时器
timer_setup(&ctx->blink_timer, blink_timer_fn, 0);
// jiffier 开机到现在的滴答数,滴答是HZ的倒数,HZ是每秒钟中断的次数,通常是100,或250或1000
mod_timer(&ctx->blink_timer, jiffies + msecs_to_jiffies(BLINK_INTERVAL));
// 设置自己的私有数据,让remove 可以取到,注意,实际上是改掉了原来的drvdata.
platform_set_drvdata(pdev, ctx);
dev_info(&pdev->dev, "✅ Probe成功,驱动绑定设备LED寄存器\n");
return 0;
}
static int virt_led_remove(struct platform_device *pdev)
{
// 这里拿的是驱动自己的ctx, 跟probe 时拿到的设备册指针不同,因为probe结尾已经用platform_set_drvdata修改了
struct led_drv_ctx *ctx = platform_get_drvdata(pdev);
// 1. 首先销毁定时器,杜绝野指针回调(最重要)
del_timer_sync(&ctx->blink_timer);
// 2. 销毁 /dev/ 设备节点
device_destroy(ctx->cls, ctx->devno);
// 3. 销毁设备class
class_destroy(ctx->cls);
// 4. 删除字符设备
cdev_del(&ctx->cdev);
// 5. 注销设备号
unregister_chrdev_region(ctx->devno, 1);
dev_info(&pdev->dev, "✅ Remove成功,驱动卸载\n");
return 0;
}
static const struct platform_device_id led_id_table[] = {
{ DRIVER_NAME },
{ }
};
MODULE_DEVICE_TABLE(platform, led_id_table);
static struct platform_driver led_plat_driver = {
.probe = virt_led_probe,
.remove = virt_led_remove,
.id_table = led_id_table,
.driver = {
// .name = "virt-led-driver", 由于id_table在x86上不能成功,x86_64默认未开启
// 所以还需要用 .name 匹配
.name = DRIVER_NAME,
},
};
static int __init drv_init(void)
{
int ret = platform_driver_register(&led_plat_driver);
pr_info("led platform driver registered. ret:%d\n",ret);
return ret;
}
static void __exit drv_exit(void)
{
platform_driver_unregister(&led_plat_driver);
pr_info("led platform driver unregisterd.\n");
}
module_init(drv_init);
module_exit(drv_exit);
MODULE_LICENSE("GPL");
3. 实验.
$ sudo install led_drv.ko
$ sudo install virt_led_dev.ko
$ dmesg
35057.003421\] led platform driver registered. ret:0 \[35074.409409\] in virt_led_probe. \[35074.409488\] myled myled: ✅ Probe成功,驱动绑定设备LED寄存器 \[35074.409521\] virt-led-dev: 虚拟设备创建完成,自带LED状态寄存器 \[35074.930415\] \[驱动\] 输出设备LED状态 = 1 \[35075.442414\] \[驱动\] 输出设备LED状态 = 0 \[35075.954413\] \[驱动\] 输出设备LED状态 = 1 停止 $ echo "stop" \|sudo tee /dev/myled 再启动 $ echo "start" \|sudo tee /dev/myled 查看 dmesg, 实验成功