platform总线,驱动,虚拟设备(实例2)--led闪烁灯(附代码)


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 关于私有结构指针的获取

如果把私有结构指针设置成全局变量,则在使用时就很方便,哪个函数都可访问.

不过不是推荐做法, 安全性不好. 这里的安全性指的是不推荐全局变量. 应该用其它方式.

  1. virt_led_remove 函数中, 它利用了参数pdev, pdev->dev 中的一个指针来保留私有指针.
    故可以用platform_get_drvdata(pdev); 来获取到这个指针.
  2. 在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位置,

以后你访问这个设备号时,就可获得这个地址.

  1. 在read,write 函数中,用filp参数拿到私有地址

    struct led_drv_ctx *ctx = filp->private_data;

    因为在led_open 中,filp->private_data 已经被赋值

  2. 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, 实验成功

相关推荐
嵌入式老牛2 天前
第一章 SiC MOSFET器件特性(二)
驱动·电力电子·sic
慢慢向上的蜗牛4 天前
Atlas300I推理卡驱动适配Linux 6.12+内核
linux·c++·人工智能·华为·驱动·底层开发·ascend
Terasic友晶科技18 天前
答疑解惑 | DE25-Nano开发板串口在访问FPGA端外设LED时卡死,无任何反应
fpga开发·串口·led·de25-nano
So_shine21 天前
stm32f103汇编-1:LED点灯
汇编·stm32·单片机·led
Terasic友晶科技1 个月前
答疑解惑 | DE25-Nano开发板Uboot阶段与FPGA外设交互失败
fpga开发·led·uboot·de25-nano·terasic
嵌入式×边缘AI:打怪升级日志1 个月前
使用文件 I/O 操作硬件 —— 从 LED 到温湿度传感器
qt·led·温湿度传感器
老师用之于民1 个月前
【DAY38】嵌入式 Linux 系统移植与驱动开发入门指南
驱动
嵌入式老牛1 个月前
SST专题3-1 基于光分路器的MMC分布式控制系统架构
分布式·架构·驱动·光纤·sst
中达瑞和-高光谱·多光谱1 个月前
光谱成像技术赋能LED灯珠品质检测:中达瑞和引领工业检测新标准
led·高光谱·多光谱·高光谱相机·分选