Platform + 设备树 LED 驱动开发
2025/9/24大约 5 分钟
Platform + 设备树 LED 驱动开发
一、驱动框架的整体思路
倒推法:从 module_init() 出发,一步步推导出需要定义哪些结构体、实现哪些函数。
module_init(leddriver_init)
↓
platform_driver_register(&led_driver)
↓
需要定义 struct platform_driver led_driver
↓
led_driver 包含:.probe / .remove / .driver.of_match_table
↓
需要定义 of_device_id 匹配表 → 需要设备树节点
↓
probe 执行 → 注册字符设备 → 创建 /dev/xxx二、核心数据结构
先定义设备结构体,用于在驱动各函数间传递数据。
struct leddev_dev {
dev_t devid; /* 设备号(主+次) */
struct cdev cdev; /* 字符设备对象 */
struct class *class; /* 设备类(/sys/class/xxx) */
struct device *device; /* 设备对象(/dev/xxx) */
int major; /* 主设备号 */
struct device_node *node; /* 设备树节点指针(标准写法可省略) */
int led0; /* GPIO 编号 */
};
static struct leddev_dev leddev; /* 全局设备实例 */三、驱动入口与出口
module_init(leddriver_init);
module_exit(leddriver_exit);
static int __init leddriver_init(void)
{
return platform_driver_register(&led_driver);
}
static void __exit leddriver_exit(void)
{
platform_driver_unregister(&led_driver);
}四、platform_driver 结构体
这是 Platform 驱动的核心骨架。
static struct platform_driver led_driver = {
.driver = {
.name = "my-imx6ull-led", /* 驱动名称(/sys/bus/platform/drivers/ 下显示) */
.of_match_table = led_of_match, /* 设备树匹配表 */
},
.probe = led_probe, /* 匹配成功后执行 */
.remove = led_remove, /* 驱动卸载时执行 */
};注意:
.name可以任意起名,不影响设备树匹配(匹配靠.of_match_table)。
五、of_match_table —— 设备树匹配表
static const struct of_device_id led_of_match[] = {
{ .compatible = "my_first-led" }, /* 必须与设备树中的 compatible 完全一致 */
{ /* Sentinel */ }, /* 空哨兵,表示结束 */
};
MODULE_DEVICE_TABLE(of, led_of_match); /* 可选:导出匹配表信息 */
//将 led_of_match 数组里的 compatible 字符串提取出来,放到内核模块的一个特殊段(__mod_of_device_table)中。设备上电,设备树加载时,看见它,compatible匹配成功后会自动执行insmod加载操作,然后触发
//init() —>probe()函数。相当于自动加载设备树节点示例:
gpioled {
compatible = "my_first-led";
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};⚠️ compatible 字符串必须一字不差,多一个空格、大小写不同都会导致匹配失败。
六、probe —— 驱动的“心脏”
当 compatible 匹配成功后,probe 函数会被调用。它负责:
- 注册字符设备(分配设备号、添加 cdev)
- 创建
/sys/class/目录和/dev/设备节点 - 从设备树获取硬件资源(GPIO 编号)
- 初始化 GPIO
static int led_probe(struct platform_device *dev)
{
printk("led driver and device was matched!\r\n");
/* ---------- 1. 设备号 ---------- */
if (leddev.major) {
leddev.devid = MKDEV(leddev.major, 0);
register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);
} else {
alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME);
leddev.major = MAJOR(leddev.devid);
}
/* ---------- 2. 注册字符设备 ---------- */
cdev_init(&leddev.cdev, &led_fops);
cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);
/* ---------- 3. 创建类(/sys/class/xxx) ---------- */
leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);
if (IS_ERR(leddev.class)) {
return PTR_ERR(leddev.class);
}
/* ---------- 4. 创建设备节点(/dev/xxx) ---------- */
leddev.device = device_create(leddev.class, NULL,
leddev.devid, NULL, LEDDEV_NAME);
if (IS_ERR(leddev.device)) {
return PTR_ERR(leddev.device);
}
/* ---------- 5. 获取 GPIO(两种写法) ---------- */
/* 写法一:标准写法(推荐)—— 直接用 dev 传进来的 of_node */
leddev.led0 = of_get_named_gpio(dev->dev.of_node, "led-gpio", 0);
/* 写法二:传统写法(不推荐)—— 硬编码路径查找 */
// leddev.node = of_find_node_by_path("/gpioled");
// leddev.led0 = of_get_named_gpio(leddev.node, "led-gpio", 0);
if (leddev.led0 < 0) {
printk("can't get led-gpio\r\n");
return -EINVAL;
}
/* ---------- 6. 申请并初始化 GPIO ---------- */
gpio_request(leddev.led0, "led0");
gpio_direction_output(leddev.led0, 1); /* 默认高电平(LED 灭) */
return 0;
}关键区别
| 写法 | 优点 | 缺点 |
|---|---|---|
new dev->dev.of_node | 由内核自动传入,无硬编码路径 | 无 |
old of_find_node_by_path("/xxx") | 简单直观 | 路径硬编码,换节点名需改驱动 |
七、file_operations —— 应用层接口
probe 中注册的 led_fops 提供了用户态操作入口。
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
};write 函数实现
static ssize_t led_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *off)
{
unsigned char databuf[2];
unsigned char ledstat;
if (copy_from_user(databuf, buf, cnt)) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0];
if (ledstat == LEDON) {
gpio_set_value(leddev.led0, 0); /* 低电平亮 */
} else if (ledstat == LEDOFF) {
gpio_set_value(leddev.led0, 1); /* 高电平灭 */
} else {
printk("Invalid value: %d\n", ledstat);
}
return cnt; /* ⚠️ 必须返回实际写入字节数,否则应用层 read() 会返回 0 认为 EOF */
}应用层使用
echo -n 1 > /dev/xxx时,写入的是 ASCII 字符'1'(0x31),驱动判断的是数值1,因此需要通过printf "\x01" > /dev/xxx或应用程序写入二进制值。
八、remove —— 资源回收
static int led_remove(struct platform_device *dev)
{
gpio_set_value(leddev.led0, 1); /* 关灯 */
gpio_free(leddev.led0); /* 释放 GPIO */
cdev_del(&leddev.cdev); /* 删除字符设备 */
unregister_chrdev_region(leddev.devid, LEDDEV_CNT); /* 注销设备号 */
device_destroy(leddev.class, leddev.devid); /* 销毁设备节点 */
class_destroy(leddev.class); /* 销毁类 */
return 0;
}九、完整驱动文件结构
┌─────────────────────────────────────────┐
│ 头文件区 │
├─────────────────────────────────────────┤
│ 宏定义(设备名、LEDON/LEDOFF 等) │
├─────────────────────────────────────────┤
│ 设备结构体 + 全局实例 │
├─────────────────────────────────────────┤
│ led0_switch() / led_open() / led_write() │
├─────────────────────────────────────────┤
│ led_probe() / led_remove() │
├─────────────────────────────────────────┤
│ of_device_id 匹配表 │
├─────────────────────────────────────────┤
│ platform_driver 结构体 │
├─────────────────────────────────────────┤
│ module_init() / module_exit() │
├─────────────────────────────────────────┤
│ MODULE_LICENSE / MODULE_AUTHOR │
└─────────────────────────────────────────┘十、关键目录概念速查
| 概念 | 位置 | 说明 |
|---|---|---|
/sys/bus/platform/devices/ | 设备树解析生成 | 代表硬件设备,上电即存在 |
/sys/bus/platform/drivers/ | platform_driver_register() 生成 | 代表驱动,insmod 后出现,probe不执行也会出现 |
/dev/xxx | probe()->device_create() 生成 | 应用层通过 open/read/write 访问 |
/sys/class/xxx/ | probe()->class_create() 生成 | 设备类信息,方便应用层查找 |
十一、常见问题总结
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
dmesg 没有 matched! | compatible 不匹配 | 检查设备树和驱动中的字符串是否完全一致 |
/dev/xxx 不存在 | probe 执行失败 | 检查 dmesg 错误日志(如 GPIO 获取失败) |
echo 1 > /dev/xxx 无效 | 写入的是 ASCII 字符 | 应用层用 printf "\x01" 或 write 写入二进制值 |
| 灯不亮但驱动加载成功 | GPIO 引脚冲突或硬件连接问题 | 检查 /sys/kernel/debug/gpio 确认 GPIO 是否被占用 |

