正点原子 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 一步搞定,是简单字符设备驱动的标准快捷方式。