正点原子 I.MX6ULL MINI 开发板学习笔记:自动创建 /dev 节点与 sysfs 属性

分享
设备模型三步走

上一篇的 LED 驱动还要手动 mknod 才能用。这篇在同一驱动上加两步:用 class_create + device_create 让内核自动生成 /dev 节点,再挂一个 sysfs 属性文件,用 cat/echo 就能看灯、控灯。代码来自实际工程 code/2026/20260803/04_led_sys,已验证可用。

1. 完成什么实验?

在 LED 字符设备驱动基础上:insmod 后自动生成 /dev/myled0(不再手动 mknod);同时在 /sys/class/LED/myled0/ 下挂出 led_status 属性文件,cat 读灯状态、echo 控制亮灭。

2. 上一篇哪里不方便?

手动 mknod 有两个问题:一是主设备号是动态分配的,每次加载可能不同,得先 cat /proc/devices 查;二是卸载后节点残留,容易误用。Linux 的设备模型就是来解决这个的——驱动里注册 class 和 device,内核通过热插拔机制通知用户态的 mdev(busybox 提供)自动创建/删除 /dev 节点。

3. 设备模型:class 和 device

  • class_create(THIS_MODULE, "LED"):创建一个设备类,出现目录 /sys/class/LED/。失败时返回错误码指针,要用 IS_ERR/PTR_ERR 判断。
  • device_create(pclass, NULL, dev_no, NULL, "myled0"):在类下创建一个设备,内核发 uevent,mdev 收到后自动生成 /dev/myled0。
  • 卸载时反序销毁:device_destroy(pclass, dev_no) → class_destroy(pclass),/dev 节点随之消失。

4. sysfs 属性文件

sysfs 是内核对象导出到用户态的文件系统。给设备挂一个属性文件,读它触发驱动的 show 回调、写它触发 store 回调,相当于给驱动开了第二组不需要写应用程序的调试接口。属性用 __ATTR 宏定义:

static ssize_t my_show(struct device *dev, struct device_attribute *attr, char *buf);
static ssize_t my_store(struct device *dev, struct device_attribute *attr,
                        const char *buf, size_t count);

/* 定义属性:/sys/class/LED/myled0/led_status,权限 0664,
 * 读触发 my_show,写触发 my_store */
static const struct device_attribute attr =
    __ATTR(led_status, 0664, my_show, my_store);
/* cat led_status 时调用:把状态字符串写进 buf,返回字节数 */
static ssize_t my_show(struct device *dev, struct device_attribute *attr, char *buf)
{
    u32 v = readl(pdata);
    int sw = (v >> 3) & 1;      /* 1=灭(高电平),0=亮(低电平) */

    if (1 == sw)
        strcpy(buf, "LED_OFF\n");
    else
        strcpy(buf, "LED_ON\n");
    return strlen(buf);
}

/* echo 0 > led_status 亮灯,echo 1 > led_status 灭灯 */
static ssize_t my_store(struct device *dev, struct device_attribute *attr,
                        const char *buf, size_t count)
{
    u32 v = readl(pdata);
    if (0 == strcmp(buf, "0\n"))          /* echo 自带换行符,要一起比较 */
    {
        v &= ~(1 << 3);                  /* 清 bit3 输出低电平:点亮 */
        writel(v, pdata);
    }
    else if (0 == strcmp(buf, "1\n"))
    {
        v |= (1 << 3);
        writel(v, pdata);
    }
    return count;   /* 惯例:成功必须返回 count,返回 0 会被视为写入失败 */
}

init 函数尾部新增的三步(前面申请设备号、ioremap 等和上一篇完全相同):

/* 创建设备类:生成 /sys/class/LED/,失败返回错误码指针(IS_ERR 判断) */
pclass = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(pclass))
{
    ret = PTR_ERR(pclass);
    goto fail_unmap_pdata;
}

/* 在类下创建设备:mdev 据此自动生成 /dev/myled0,无需手动 mknod */
pdevice = device_create(pclass, NULL, dev_no, NULL, DEVICE_NODE);
if (IS_ERR(pdevice))
{
    ret = PTR_ERR(pdevice);
    goto fail_class;
}

/* 挂出属性文件 /sys/class/LED/myled0/led_status,也要检查返回值 */
ret = device_create_file(pdevice, &attr);
if (ret)
    goto fail_device;

return 0;

/* 倒序释放链:跳到哪个标签就释放到哪一步,然后自然下落 */
fail_device:
    device_destroy(pclass, dev_no);
fail_class:
    class_destroy(pclass);
fail_unmap_pdata:
    iounmap(pdata);
/* ... 继续下落:iounmap 其余寄存器 → cdev_del → 释放设备号 ... */

exit 里严格反序释放:

device_remove_file(pdevice, &attr);  /* 移除 sysfs 属性文件 */
device_destroy(pclass, dev_no);      /* 销毁设备,/dev/myled0 随之消失 */
class_destroy(pclass);               /* 销毁设备类 /sys/class/led */
iounmap(pmux);  iounmap(ppad);  iounmap(pdir);  iounmap(pdata);
cdev_del(&mycdev);
unregister_chrdev_region(dev_no, 10);

5. 关键点说明

  • 谁建的 /dev 节点:是用户态的 mdev(busybox 的热插拔守护机制),内核只负责发 uevent。rootfs 的 rcS 里 echo /sbin/mdev > /proc/sys/kernel/hotplug 和 mdev -s 就是干这个的——系统移植篇埋的伏笔在这里用上了。
  • echo 的换行符:echo 0 实际写入的是 "0\n",所以比较时要带上换行;更稳妥的写法是内核提供的 sysfs_streq()。
  • store 的返回值:写回调成功必须返回 count,这是 sysfs 的惯例,返回 0 会让 shell 报写入失败。
  • 两套接口并存:/dev/myled0 的 read/write 和 sysfs 的 show/store 互不冲突,应用层用哪套都行。
  • goto 错误处理:更新后的工程代码里,class_create / device_create / device_create_file 任何一步失败都会跳到倒序释放链,把已申请的资源放干净再返回错误码。
  • 大小写:类名是 "LED",sysfs 区分大小写,所以实际路径是 /sys/class/LED/myled0/。

6. 实验现象

# insmod drv_led.ko 之后,不需要 mknod:
ls /dev/myled0                        # 节点已自动存在
ls /sys/class/LED/myled0/             # 能看到 led_status 属性文件

cat /sys/class/LED/myled0/led_status  # 输出 LED_OFF
echo 0 > /sys/class/LED/myled0/led_status   # 灯亮
cat /sys/class/LED/myled0/led_status  # 输出 LED_ON
echo 1 > /sys/class/LED/myled0/led_status   # 灯灭

总结

问题答案
自动建 /dev 节点靠什么?class_create + device_create + mdev 热插拔
sysfs 属性怎么定义?__ATTR(名字, 权限, show, store) + device_create_file
cat 属性文件触发什么?驱动的 show 回调
echo 写属性触发什么?驱动的 store 回调,成功返回 count
卸载顺序?属性文件 → 设备 → 类 → iounmap → cdev_del → 释放设备号

class_create + device_create 比手动 mknod 进了一步,但申请设备号、cdev、class、device 这一串还是繁琐。下一篇介绍 misc 设备:misc_register 一步搞定,是简单字符设备驱动的标准快捷方式。

继续阅读

嵌入式开发面试知识手册

嵌入式开发工程师面试知识手册 适用对象:零基础起步、目标是通过嵌入式软件/驱动/BSP 方向面试的读者 编写依据:赵哲简历全部知识点 + 智能车 AGV 控制平台完整项目代码(i.MX6ULL Linux 端 + STM32F407 固件)+ Linux 驱动练习代码包 + embedded-agent 嵌入式开发智能体仓库 + 真实面试手撕代码与追问记录 使用方法:每篇按"通俗解释 → 原理细节 → 命令/API → 代码实例 → 项目结合 → 记忆口诀 → 面试问答"组织;口诀用于快速记忆,问答用于自测。第9篇是讲项目的演练场,第10篇是手撕代码实战题库。 总目录 * 第1篇 C语言与数据结构 * 1.1 编译链接全过程 * 1.2 数据类型与内存

作者:azhezhezhe

正点原子 I.MX6ULL MINI 开发板学习笔记:Linux 驱动架构详解——编译、树内树外与运行时效果

这是 Linux 驱动小系列的总结篇。前三篇把 LED 驱动从内核模块写到 misc 设备,属于“会用”;这篇把背后的架构讲透:驱动到底怎么编译、内核树内和树外两种方式的区别、以及驱动里每个操作在系统跑起来之后到底产生了什么效果。内容整理自我的学习笔记(note.pdf)和实际工程 code/2026/20260803。 1. 驱动代码到底怎么编译的? 不管哪种方式,最终都靠内核的 kbuild 系统。核心就三个文件的关系: * Kconfig:定义 menuconfig 里的选项(如 CONFIG_MYMODULE),三态 y/m/n——y 编进内核,m 编成模块,n 不编; * .config:menuconfig 的选择结果,保存在源码顶层; * Makefile:

作者:azhezhezhe

正点原子 I.MX6ULL MINI 开发板学习笔记:misc 设备——字符设备驱动的快捷方式

上两篇分别用手动 mknod 和 class_create+device_create 注册字符设备,调用链一长串。这篇用 misc(混杂)设备 一步搞定。代码来自实际工程 code/2026/20260803/05_misc_led,已验证可用。 1. 完成什么实验? 用 misc_register 重写 LED 驱动:一步完成设备号分配、字符设备注册、/dev/myled0 节点创建,同时保留上一篇的 sysfs 属性 led_status。 2. 什么是 misc 设备? misc(混杂)设备是 Linux 给“不好归类的小字符设备”

作者:azhezhezhe