☰
RV1106平台LED驱动开发:从分层架构到设备树实战
2026/10/21 12:22:22 网站建设 项目流程

最近在调试 RV1106 平台,正好要写一个 LED 驱动。说实话,LED 驱动在整个嵌入式 Linux 开发里算是入门级的活,但入门不等于没坑。尤其是这类芯片平台,如果你一开始没把整体架构想清楚,后面加按键、加呼吸灯、加网络状态指示,代码会越写越乱。这篇就是系列的第一篇,先把整体架构讲透,再往下走细节。

文章适合刚接触 RV1106 平台、想快速上手内核驱动开发的开发者,也适合想在自己的项目里把 LED 驱动从“能用”做到“好维护”的人。我会从架构设计的角度,把这个驱动拆成几层,说清楚每一层为什么这么划分、数据是怎么流的、代码该往哪里放、后面怎么扩展。

1. 整体架构设计:一个 LED 驱动为什么也值得认真分层

1.1 你以为的 LED 驱动其实只有三行代码

很多人写 LED 驱动,第一反应是:申请一个 GPIO,然后写高写低,完事。比如用gpio_direction_output(42, 1)点亮,用gpio_set_value(42, 0)熄灭。实际做产品时,需求远远不是“点亮”和“熄灭”这么简单。

至少要考虑这些场景:

  • 系统启动后默认处于什么状态(亮、灭、还是保持上次状态)
  • 开机时闪烁一次,表示启动成功
  • 系统运行中,LED 作为状态指示灯,比如有网络时快闪、无网络时慢闪
  • 要和设备树绑定,不同板子用不同引脚,不能把引脚写死在代码里
  • 应用层需要通过标准接口控制,比如echo 255 > /sys/class/leds/xxx/brightness
  • 后续可能要支持呼吸灯效果,那就不再是简单 GPIO 翻转了

如果一上来就把这些逻辑全堆在一个驱动文件里,代码会变成一个几百行的“大杂烩”。后面换引脚、换控制方式、加新功能,都会非常痛苦。

所以我给这个项目定的第一条规则是:先分层,再写码。

1.2 整套驱动的分层思路

我把整个 LED 控制功能拆成三层:

  1. 应用层:用户态程序、脚本、服务,通过标准接口控制 LED,不用关心底层是 GPIO 还是 I2C 还是 PWM。
  2. 内核驱动层:负责和硬件直接打交道,向上提供统一接口。这里面又分两块:
    • 设备树解析:读取当前板子的引脚配置、默认状态、触发方式
    • LED class 驱动:注册成标准 LED 设备,通过/sys/class/leds/暴露接口
  3. 硬件层:芯片的 GPIO 控制器、时钟、中断等底层资源,由内核 GPIO 子系统管理。

为什么这样分层?最核心的原因是隔离变化。硬件引脚可能会改,驱动程序可能要适配不同产品,应用软件最好不要跟着改。有了 LED class 这个标准接口,应用层只需要写echo 1 > /sys/class/leds/board_led/brightness就能控制,完全不关心底层是不是换了引脚。

这个思路和写业务系统是一样的:接口稳定,实现随便换。

1.3 技术选型:用 LED class 还是自己写字符设备

这是写 LED 驱动第一个要做的决定。有两条路:

方案优点缺点
LED class(推荐)内核自带,接口统一,自带 sysfs 节点,支持触发器(trigger)很多细节被框架“包好”了,可能让人忽略底层机制
杂项字符设备完全自定义,想怎么控制都行,适合特殊需求要自己实现 open/read/ioctl,应用层接口不标准,还要处理并发、权限、错误码,代码量大

除非你的 LED 有特别复杂的私有协议,比如通过某个串行总线、有特定时序,否则都用 LED class。

RV1106 平台跑的是标准 Linux 内核,LED class 是现成支持的。你只要实现led_classdev结构体里的 ops,框架就会帮你把 sysfs 里的 brightness、trigger、max_brightness 等属性全部建立好。用户空间操作这些属性文件,内核框架会调用你写的 ops。

后续如果想支持呼吸灯,还可以用led_classdev的brightness_set_blocking配合 PWM,或者用pattern触发器,都不用动应用层的逻辑。所以这个选型是稳的。

2. RV1106 平台驱动开发的基础认知

2.1 RV1106 到底是什么平台

RV1106 是一款面向 IPC(网络摄像头)、视觉识别等场景的 SoC 芯片,芯片本身集成了 CPU、ISP、视频编解码模块、各种外设控制器。开发这种平台,要注意的事情和普通 ARM 开发板差不多,但有几个特点:

  • 芯片的 SDK 里通常会带一套完整的内核和设备树源码,外设的引脚复用(pinmux)、时钟、电源域一般都需要在设备树里配置。
  • 很多外设的控制方式和传统单片机不一样,GPIO 的资源申请必须通过内核的gpiod接口,不能直接操作物理地址。
  • 板级差异很大,同一颗芯片在不同板子上,LED 可能接在 GPIO0、GPIO1,也可能是低电平点亮。这些差异都应该用设备树来描述。

所以在 RV1106 上写 LED 驱动,最重要的一个习惯是:不要写死任何引脚号和寄存器地址。所有的硬件参数尽量从设备树拿。

2.2 GPIO 控制 LED 的原理要点

LED 本质上就是一个二极管,给它两端加合适的电压,它就会亮。在芯片平台上,板子设计通常把 LED 接到某个 GPIO 引脚上,另一端接电源或地。

驱动里要做的事情就两件:

  1. 把 GPIO 配置成输出模式
  2. 往寄存器里写 0 或 1,让引脚输出高电平或低电平

这里最容易踩的就是“极性”。有的板子 GPIO 输出高电平 LED 亮,有的 GPIO 输出低电平 LED 亮。写驱动时必须支持设备树里的active-low属性。

在旧的内核接口里,你可能习惯用gpio_xxx函数,带一个gpio_request的 int 号。但在新内核和 RV1106 这类新 SDK 里,推荐直接用gpiod_xxx接口,它和设备树结合得更紧密,能自动处理active-low、GPIO_ACTIVE_LOWflag 等细节。

比如:

struct gpio_desc *led_gpio; struct device_node *np = dev->of_node; led_gpio = devm_gpiod_get(dev, "led", GPIOD_OUT_LOW); if (IS_ERR(led_gpio)) { return PTR_ERR(led_gpio); } gpiod_set_value(led_gpio, 1); // 点亮 gpiod_set_value(led_gpio, 0); // 熄灭

devm_gpiod_get里的第二个参数是“属性名后缀”,配合设备树的-gpios后缀属性来用。比如设备树里有led-gpios = <&gpio0 15 GPIO_ACTIVE_LOW>;,驱动里就调devm_gpiod_get(dev, "led", ...)。框架会自动找到led-gpios,并处理好极性,你在代码里gpiod_set_value(desc, 1)永远表示“语义上的打开”,而不是“物理上的拉高”。这是非常省心的设计。

2.3 设备树在驱动里的角色

设备树是 Linux 用来描述硬件配置的机制。驱动通过“compatible”属性和设备树里的节点匹配。当匹配成功,内核会调用驱动的probe函数,并把节点的struct device_node *传给你。

LED 驱动通常会定义一个这样的节点:

led_status: led-status { compatible = "rv1106-led"; label = "status_led"; led-gpios = <&gpio0 15 GPIO_ACTIVE_LOW>; default-state = "off"; };

驱动里用of_property_read_string读 label,用devm_gpiod_get读 led-gpios。设备树的作用就是把这些硬件参数从代码里解耦出去。以后换了引脚,只要改 DTS 重新编译设备树,不用重新编译内核。

这里要提醒一点:设备树不是写完了就自动生效的。在 RV1106 的 SDK 里,设备树会被编译成 dtb,烧写到设备的分区里。修改 DTS 之后要重新编译整个设备树,或者用 fastboot 单独烧写 dtb 分区分区名一般是resource或dtb,不同 SDK 不一样,最好先查一下 SDK 文档。如果只改了设备树,别忘了在驱动加载前确认 dtb 已经更新。

3. 整体架构实现:模块划分与数据流

3.1 代码文件怎么组织

很多新手写驱动,把所有代码放在一个.c文件里,功能能跑,但维护起来很头疼。我的习惯是:以“可扩展”为前提来组织文件。对于这个 LED 驱动,我把它分成下面几个文件:

rv1106-led/ ├── rv1106_led.c // 驱动主文件:probe、remove、led ops 注册 ├── rv1106_led.h // 结构体、宏定义,私有数据 ├── rv1106_led_platform.c // 平台相关的配置、引脚初始化(如果以后要支持复杂时序,可以拆开) ├── Kconfig // 内核配置项 └── Makefile // 编译规则

对于一个简单的 LED 驱动,其实rv1106_led.c和rv1106_led.h就够了,rv1106_led_platform.c可以后面需要的时候再加。但是把“驱动核心逻辑”和“平台硬件适配”分开的思路一定要有,这样万一以后要在一个驱动里支持多个 GPIO 控制多个 LED,或者还要支持 PWM 模式,大家不会互相打架。

3.2 驱动对象的核心数据结构

在设计架构阶段,首先要定义一个结构体来承载“每个 LED 设备的所有实例信息”。

建议这样定义:

struct rv1106_led_dev { struct led_classdev led_cdev; struct gpio_desc *gpio; struct device *dev; bool active_low; int default_state; };

led_classdev是内核 LED 框架要求你必须嵌入的结构体,它不能单独存在。通过container_of,在 ops 回调里可以从传入的led_classdev*反推出你自己的rv1106_led_dev*,然后拿到 GPIO 描述符和其他信息。这个模式在内核驱动里非常常见,你要习惯。

私有数据结构的设计就是整个驱动的灵魂。提前把所有必要字段想好,probe里做的就是“把这个结构体填满”,后面所有的 ops 都是围绕它转,架构自然清晰。

3.3 从用户层到底层的数据流

把数据流画一遍,你会对架构有更深的理解。

当用户在终端敲下:

echo 1 > /sys/class/leds/status_led/brightness

内核里实际发生的事情是这样的:

  1. VFS 层处理文件写入,找到对应的 sysfs 文件对应的 inode。
  2. sysfs 的 store 回调被触发,这个回调由 LED 框架注册。
  3. LED 框架把写入的字符串转换成brightness值,然后调用led_cdev->brightness_set或led_cdev->brightness_set_blocking指针指向的函数。
  4. 你的驱动 ops 里,根据 brightness 值调用gpiod_set_value_cansleep(gpio, brightness ? 1 : 0)。
  5. GPIO 子系统把值设置到芯片 PWM/GPIO 控制器寄存器,物理引脚电平变化,LED 亮或灭。

也就是说,作为驱动作者,你真正需要实现的回调函数只有:

static void rv1106_led_set_brightness(struct led_classdev *led_cdev, enum led_brightness brightness) { struct rv1106_led_dev *priv = container_of(led_cdev, struct rv1106_led_dev, led_cdev); gpiod_set_value_cansleep(priv->gpio, brightness ? 1 : 0); }

其它 sysfs 节点、触发器逻辑,LED 框架全都帮你做好了。这也是我推荐用 LED class 的原因:看似少写了很多代码,实际上是框架帮你把复杂度和安全问题都解决了。

3.4 触发器和多 LED 的架构扩展

如果只是点亮和熄灭,上面足够了。但实际项目里,LED 往往要配合“事件”使用。比如:

  • 网络断开时闪烁
  • 有数据读写时闪烁
  • 开机启动时先亮 1 秒,然后熄灭

Linux 的 LED 框架里有“触发器(trigger)”机制。最简单的做法是在驱动里注册一个 trigger,比如heartbeat、netdev、timer,用户可以在 sysfs 里随时切换。

架构上,如果你一开始就把 ops 分离出来,后续加 trigger 只是新增一个文件或者配置项的事,不需要改核心点亮逻辑。

举个例子,在设备树节点里可以加:

linux,default-trigger = "heartbeat";

内核会自动把板级 LED 绑定到系统心跳闪烁上。不过这个需要内核配置开启相关的 trigger(CONFIG_LEDS_TRIGGER_HEARTBEAT)。在 RV1106 的 SDK 里,多数 trigger 默认是开着的,但你要确认一下。

要是你要实现多个 LED,比如一个电源灯、一个状态灯,架构上建议就是“一个 probe 匹配多个子节点”,在 probe 里遍历所有 compatible = "rv1106-led" 的子节点,为每个节点分配一个struct rv1106_led_dev,分别注册到 LED 框架。这样的好处是所有 LED 驱动在同一模块里,代码复用率高,管理集中。

4. 核心代码骨架与实现要点

4.1 模块入口和出口

直接给出我实现的模块入口文件骨架。

#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of_device.h> #include <linux/leds.h> #include <linux/gpio/consumer.h> #include <linux/of_gpio.h> static struct platform_driver rv1106_led_driver = { .probe = rv1106_led_probe, .remove = rv1106_led_remove, .driver = { .name = "rv1106-led", .of_match_table = rv1106_led_of_match, }, }; module_platform_driver(rv1106_led_driver); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Friendly Engineer"); MODULE_DESCRIPTION("RV1106 LED driver");

写驱动第一个要注意的细节:模块加载宏。用module_platform_driver自动生成了module_init和module_exit,不用自己手动写了。如果你用module_init(led_init),那也要相应地用module_exit(led_exit),千万别搞混,不然打印错误信息可能显示符号找不到。

of_match_table是设备树匹配的关键。定义如下:

static const struct of_device_id rv1106_led_of_match[] = { { .compatible = "rv1106-led", }, { } }; MODULE_DEVICE_TABLE(of, rv1106_led_of_match);

编译的时候,MODULE_DEVICE_TABLE会把设备 ID 表生成加载时需要的模块符号。如果你只写了设备树节点却不写这个表,insmod之后驱动可能一直不 probe,这是新手最常见的坑。

4.2 probe 函数怎么组织

probe是整个驱动的“构造函数”,它要做的事不外乎是:

  1. 解析设备树节点
  2. 申请并填充priv结构体
  3. 获取 GPIO
  4. 注册 LED class 设备
  5. 在 sysfs 里创建初始状态

下面是一个完整度较高的实现:

static const struct of_device_id rv1106_led_of_match[] = { { .compatible = "rv1106-led", }, { } }; MODULE_DEVICE_TABLE(of, rv1106_led_of_match); static int rv1106_led_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct device_node *np = dev->of_node; struct rv1106_led_dev *led; const char *label; const char *default_state; int ret; led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); if (!led) return -ENOMEM; led->dev = dev; /* 读 label,如果没写则用 node 名字 */ ret = of_property_read_string(np, "label", &label); if (ret) label = np->name; /* 获取 GPIO,这个函数会自动处理命名规则和极性 */ led->gpio = devm_gpiod_get(dev, "led", GPIOD_ASIS); if (IS_ERR(led->gpio)) { dev_err(dev, "failed to get led gpio: %ld\n", PTR_ERR(led->gpio)); return PTR_ERR(led->gpio); } /* 设置初始方向 */ ret = gpiod_direction_output(led->gpio, 0); if (ret) return ret; /* 读默认状态 */ default_state = "off"; of_property_read_string(np, "default-state", &default_state); if (!strcmp(default_state, "on")) { gpiod_set_value_cansleep(led->gpio, 1); led->led_cdev.brightness = 1; } else { gpiod_set_value_cansleep(led->gpio, 0); led->led_cdev.brightness = 0; } /* 填充 led_classdev */ led->led_cdev.name = label; led->led_cdev.brightness_set = rv1106_led_set_brightness; led->led_cdev.max_brightness = 1; led->led_cdev.flags = LED_CORE_SUSPENDRESUME; /* 注册到内核 LED 框架 */ ret = devm_led_classdev_register(dev, &led->led_cdev); if (ret) { dev_err(dev, "failed to register led classdev: %d\n", ret); return ret; } dev_info(dev, "rv1106 led driver registered: %s\n", label); return 0; }

这里面有几个很关键的细节。

第一,devm_gpiod_get的第五个参数GPIOD_ASIS表示不改变引脚方向,只拿描述符。然后自己调用gpiod_direction_output设置方向。这样更可控,不会因为某个 SDK 版本里devm_gpiod_get自动设置方向和预想不一致。如果你确定要用,可以用GPIOD_OUT_LOW或GPIOD_OUT_HIGH,它会自动设置方向。

第二,label用在 LED 名字上,确保不能为空。设备树里面的label如果没写,我回退到节点名。因为后面 sysfs 路径会用到它,空指针直接崩。

第三,max_brightness = 1表示纯开关模式,只有亮/灭两个状态。如果你以后要亮度调节,比如 PWM 调光,就需要把max_brightness设成 255,并且brightness_set里根据输入值做 PWM 占空比映射。

第四,flags = LED_CORE_SUSPENDRESUME这一个标志的作用是告诉框架,系统 suspend/resume 的时候驱动会自动保存和恢复亮度。如果不需要,可以不加。

4.3 remove 函数和释放资源

remove函数负责卸载时的清理。由于大量使用了devm_资源管理,其实很多资源不用手动释放。但要注意两点:GPIO 的电平状态和 sysfs 节点。

static void rv1106_led_remove(struct platform_device *pdev) { struct rv1106_led_dev *led = platform_get_drvdata(pdev); /* 把 LED 关掉,避免卸载后引脚还是高电平导致灯常亮 */ gpiod_set_value_cansleep(led->gpio, 0); /* devm_ 管理的资源自动释放,不需要你手动 gpio_free 或 kfree */ }

platform_get_drvdata能拿到私有数据的默认机制,前提是在 probe 里调用过platform_set_drvdata(pdev, led)。别漏了这句话。

不过实际上,devm_led_classdev_register会自动把led_classdev和设备的生命周期绑定,卸载的时候框架会清理 sysfs 目录。所以remove里大部分情况下只需要处理“引脚状态恢复”这一类副作用。

4.4 设备树的坑:-gpios还是-gpio

这是很多刚接触设备树的人会迷糊的地方。devm_gpiod_get在查找设备树属性时,会自动把所有形如xxx-gpios的属性作为候选人,然后根据conindex匹配参数。如果你写的是led-gpios = <...>,在驱动里就传"led"。如果你的需求很特殊,非要用一个单数属性名led-gpio,那你需要知道,旧的of_get_named_gpio函数是支持任意属性名的,但使用gpiod接口时框架只会查找复数形式。所以设备树里统一写成led-gpios,别写成led-gpio,否则它拿不到描述符。

还有一点:如果你的 DTS 里引脚被别的驱动占用了,比如被复用成 I2C 或者 UART,devm_gpiod_get会返回-EBUSY,这个看起来是“拿不到 GPIO”,实际上可能是 pinmux 冲突。查这种问题别傻傻盯着驱动,先去查 DTS 里有没有别的节点复用了同一个gpio。

5. 调试技巧与常见问题排查

5.1 驱动不 probe 怎么办

这是 LED 驱动写完后第一个会碰到的状况。你insmod全加载了,但probe就是不执行。

先按这个顺序排查:

检查项具体操作
of_match_table是否匹配在of_device_id里的compatible必须和设备树节点里的完全一致,包括大小写
设备树节点是否编译进 dtb打开生成的 dtb,搜索节点名字,或者用fdtdump确认
设备树分区是否烧写成功启动 log 里看有没有Warning: unable to open an initial console之类,或者确认启动加载的 dtb 分区
模块是否被正确加载insmod后 `dmesg

最常用的排查方式是在 probe 函数入口加一个dev_info(dev, "probe enter\n");。如果你看到函数进入了,那就说明前面的匹配没问题。如果没进入,优先检查compatible和设备树的对应关系。

另外,有些 SDK 在启动时不会自动加载模块,所以即使写好了驱动模块,也别忘了用insmod或把模块名加到启动脚本里。如果你用的是内核内建方式(CONFIG_LEDS_RV1106=y),那驱动会随内核启动自动匹配并加载。

5.2 引脚电平不对,LED 亮灭反了

这是最典型的极性坑。板子设计可能是“低电平点亮”。如果你的驱动里简单地用gpiod_set_value(desc, 0)熄灭,默认的思维是“0 表示灭”,但在低点亮板子上,“0”反而是亮。

解决这个问题的方法,是永远不要自己在驱动代码里判断反正信号。应该这样做:

  1. 设备树里老老实实写GPIO_ACTIVE_LOW或GPIO_ACTIVE_HIGH。
  2. 驱动代码里统一用逻辑值1表示点亮,0表示熄灭。
  3. 让 GPIO 子系统的devm_gpiod_get自动根据 flags 转换电平。

比如板子实际是低电平点亮,设备树写:

led-gpios = <&gpio0 15 GPIO_ACTIVE_LOW>;

驱动里写:

gpiod_set_value_cansleep(led->gpio, 1); // 点亮 gpiod_set_value_cansleep(led->gpio, 0); // 熄灭

GPIO 子系统会在中间做一层语义转换,它在内部看到GPIO_ACTIVE_LOW,当你的逻辑值是 1,它会输出真正的低电平。

如果你自己手动读引脚状态或者测试,别用gpio_get_value看原始寄存器值来判断亮没亮,那样很容易被低电平搞晕。要看实际现象,不要看寄存器值。

5.3 卸载模块的时候 kernel 报错或者 panic

卸载模块常见问题有两个:

  1. sysfs 还在被应用层引用。比如应用层打开了/sys/class/leds/xxx/brightness这个文件,或者在某个 shell 脚本里循环读它,这时卸载模块会导致释放的内存被 uevent 访问,比较常见的是led_classdev回调到空指针。

  2. remove 顺序错乱。注册 LED class 设备的函数是devm_led_classdev_register,驱动程序模块卸载的时候,framework 会自动注销,但是前提是你的remove函数不能先手动去释放 GPIO 描述符并且内核正在用led_classdev操作中断——所以 remove 里尽可能少做事,除了熄灭 LED。其余交给devm的统一收尾。

如果一定要手动注销,led_classdev_unregister要放在gpio_free之前。

static int rv1106_led_remove(struct platform_device *pdev) { struct rv1106_led_dev *led = platform_get_drvdata(pdev); /* 先注销 LED 设备,确保 sysfs 不再可访问 */ led_classdev_unregister(&led->led_cdev); /* 再熄灭灯 */ gpiod_set_value_cansleep(led->gpio, 0); return 0; }

但你说你要用devm_led_classdev_register吗?那记得在remove里就不要调led_classdev_unregister。这是两条不同的资源管理路径,你选一个用到底,千万别混着写。混用会导致重复注销,内核直接 warning。

5.4 怎么快速验证驱动是否正常

调试 LED 驱动,我最常用这几个命令:

# 查看 LED 设备是否存在 ls /sys/class/leds/ # 手动点亮 echo 1 > /sys/class/leds/status_led/brightness # 手动熄灭 echo 0 > /sys/class/leds/status_led/brightness # 设置默认触发器(如果支持) echo heartbeat > /sys/class/leds/status_led/trigger

如果ls /sys/class/leds/已经能看到设备名,但echo没反应,大概率是brightness_set回调没有正确设置,或者 GPIO 描述符拿错引脚下。可以检查 dmesg 里驱动打印的任何错误。

另外,有一种很常见的迷惑现象:写echo 1亮灯,但是再写echo 0灭灯没反应。这时检查brightness的值是否被框架缓存了,如果brightness_set里使用了brightness变量但没做 0/1 映射,比如直接把 brightness 传给了gpiod_set_value,而gpiod_set_value期待非 0 即 1,那其实是没问题的。但如果你的max_brightness设了 255,而板子只支持开关,你需要自己判断brightness > 0才点亮,否则关掉,否则在中间值的地方行为会很奇怪。

6. 实测中积累的几个经验

这个 LED 驱动虽然简单,我在调试过程里还是踩了几个有意思的坑,分享出来你可能也会遇到。

第一个坑是“代码里明明写了gpiod_set_value(desc, 1),LED 就是一直灭”。后来发现设备树节点里写的是GPIO_ACTIVE_HIGH,而板子实际是低电平亮。我把它改成了GPIO_ACTIVE_LOW,驱动一行没动,灯就正常了。所以遇到这种问题,优先怀疑设备树极性,不要怀疑代码。

第二个坑是设备树里我同时写了老朋友gpios属性和新风格led-gpios,结果驱动有时能匹配,有时不能。原因是当一个节点同时有多个 gpio 相关属性时,devm_gpiod_get会按属性名字的地址、配置顺序去选,容易产生歧义。最佳实践是只保留一个属性。

第三个坑是模块卸载后,系统提示led_classdev相关的 workqueue 还在跑。我检查后发现是没有设置.flags里LED_CORE_SUSPENDRESUME导致的系统 suspend/resume 行为不一致。后来我在驱动初始化时把flags补充完整,这个报警就没了。

总的来说,写 RV1106 的 LED 驱动,不要因为功能简单就忽略架构。先用 LED class 把标准接口搭起来,用设备树管理硬件差异,用私有结构体串起 ops 和设备资源,后面无论扩展呼吸灯、状态触发还是多 LED 支持,都会非常从容。这一篇先把整体框架立住了,下一篇我会继续拆解具体的设备树配置细节、怎么配合 sysfs 测试,以及如果要加 PWM 呼吸灯效果,驱动部分要改动哪几个点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询