Linux字符设备驱动开发(五):PWM调光——实现LED亮度控制与呼吸灯效果
前言在上一篇文章中我们通过GPIO子系统点亮和熄灭开发板上的LED但这只是最简单的开关控制。如果想让LED像手机呼吸灯一样平滑地明暗变化就需要用到一个更强大的外设——PWM脉宽调制。本文将利用PWM子系统实现LED的亮度调节从零编写一个可设置占空比的字符设备驱动并通过一个Shell脚本在应用层实现流畅的呼吸灯效果。你将掌握PWM的基本原理与Linux PWM子系统设备树中PWM节点的配置pwm_apply_state等新版API的使用如何在驱动中根据用户输入动态调整占空比应用层脚本实现呼吸灯一、PWM子系统简介1.1 什么是PWMPWMPulse Width Modulation脉宽调制是一种对模拟信号电平进行数字编码的方法。它由**周期Period和占空比Duty Cycle**两个核心参数决定周期 T 高电平时间 低电平时间 占空比 高电平时间 / 周期 × 100%通过调整占空比可以等效地改变输出电压的平均值从而控制LED的亮度、电机的转速等。1.2 Linux PWM子系统Linux内核提供了统一的PWM框架包含两个层次PWM控制器驱动芯片原厂实现操作硬件寄存器向上层提供标准接口。PWM消费者接口我们写驱动时使用通过linux/pwm.h中的API申请和使用PWM通道。常用API新版函数作用pwm_get(dev, con_id)从设备树获取PWM设备描述符pwm_put(pwm)释放PWM设备pwm_init_state(pwm, state)用设备树参数初始化一个PWM状态pwm_apply_state(pwm, state)应用新的PWM状态周期、占空比、极性等旧版API如pwm_config()、pwm_enable()在较新内核中已标记为废弃推荐统一使用pwm_apply_state()。二、设计思路本文以韦东山课程常用的i.MX6ULL开发板为例使用PWM3外设控制一个LED假设LED连接在PWM3的输出引脚上。驱动设计如下在设备树中添加一个pwm-led节点引用pwm3控制器。驱动采用platform_driver架构与设备树节点匹配。在probe中获取PWM设备设置默认周期为50000ns20kHz初始占空比为0LED熄灭。通过/dev/pwmled设备节点向用户空间提供接口写入0~100的百分比数字设置占空比读取时返回当前百分比。应用层通过Shell脚本循环调整占空比产生呼吸灯效果。三、设备树修改3.1 确认PWM引脚查看原理图找到接有LED的PWM引脚。以i.MX6ULL开发板为例PWM3通常用于LCD背光但如果LED硬件连接到了PWM3引脚就可以使用。假设LED连接在PWM3输出引脚上对应GPIO为GPIO1_IO04或GPIO4_IO01等取决于具体板子。设备树中需确保该引脚未被其他功能占用。3.2 添加PWM LED设备树节点在板级设备树文件中添加如下节点确保文件头部包含dt-bindings/pwm/pwm.h以使用PWM_POLARITY_INVERTED等宏/ { pwm_led { compatible yourname,pwm-led; pwms pwm3 0 50000; /* 使用pwm3通道0周期50000ns */ status okay; }; };属性说明compatible与驱动中的of_match_table匹配。pwms指定PWM控制器、通道号和默认周期单位纳秒。50000ns 20kHz这是人眼不闪烁的合适频率。注意某些旧设备树可能还需要显式配置引脚复用pinctrl这里假设内核已默认配置好。如果LED不亮请检查引脚复用设置。重新编译设备树并替换后重启开发板。四、驱动代码实现新建pwmled_drv.c完整代码如下。所有错误处理路径均严格编写并使用了新版pwm_apply_state()。/* * pwmled_drv.c * PWM LED 字符设备驱动。 * 使用 platform_driver 匹配设备树节点通过 pwm_apply_state() 控制占空比。 * 设备节点 /dev/pwmled写入 0-100 百分比控制亮度读取返回当前百分比。 * 作者[你的ID] * 适配内核Linux 5.x (4.x 亦可) * 参考开发板i.MX6ULL韦东山课程配套板 */#includelinux/module.h#includelinux/fs.h#includelinux/cdev.h#includelinux/device.h#includelinux/uaccess.h#includelinux/platform_device.h#includelinux/pwm.h/* PWM API */#includelinux/of.h#defineDEVICE_NAMEpwmled#defineCLASS_NAMEpwmled_class/* 默认周期 50000ns (20kHz)占空比范围 0 ~ period */#definePWM_PERIOD_NS50000staticdev_tdev_num;staticstructcdevmy_cdev;staticstructclass*my_class;staticstructdevice*my_device;staticstructpwm_device*led_pwm;/* PWM设备描述符 */staticu32 current_duty_ns;/* 当前占空比纳秒 *//* 打开设备 */staticintpwmled_open(structinode*inode,structfile*file){pr_info(pwmled: device opened\n);return0;}/* 关闭设备 */staticintpwmled_release(structinode*inode,structfile*file){pr_info(pwmled: device closed\n);return0;}/* 读取当前占空比百分比0 - 100 */staticssize_tpwmled_read(structfile*file,char__user*buf,size_tcount,loff_t*f_pos){charkbuf[8];intpercent;intlen;percent(current_duty_ns*100)/PWM_PERIOD_NS;lensnprintf(kbuf,sizeof(kbuf),%d\n,percent);if(*f_poslen)return0;/* EOF */if(copy_to_user(buf,kbuf,len))return-EFAULT;*f_poslen;returnlen;}/* 写入占空比百分比接收 0 ~ 100 的数字字符串 */staticssize_tpwmled_write(structfile*file,constchar__user*buf,size_tcount,loff_t*f_pos){charkbuf[8]{0};unsignedlongpercent;intret;structpwm_statestate;if(count7)count7;/* 最多允许输入 100 换行 4字节7已足够 */if(copy_from_user(kbuf,buf,count))return-EFAULT;retkstrtoul(kbuf,0,percent);if(ret0){pr_warn(pwmled: invalid input, not a number\n);return-EINVAL;}if(percent100){pr_warn(pwmled: percentage out of range (0-100)\n);return-EINVAL;}/* 计算占空比纳秒值 */current_duty_ns(PWM_PERIOD_NS*percent)/100;/* 获取当前PWM状态修改占空比并应用 */pwm_get_state(led_pwm,state);state.duty_cyclecurrent_duty_ns;state.enabled(current_duty_ns0);/* 占空比为0时关闭PWM输出 */retpwm_apply_state(led_pwm,state);if(ret){pr_err(pwmled: pwm_apply_state failed\n);return-EFAULT;}pr_info(pwmled: set duty %lu%% (%u ns)\n,percent,current_duty_ns);returncount;}staticstructfile_operationspwmled_fops{.ownerTHIS_MODULE,.openpwmled_open,.releasepwmled_release,.readpwmled_read,.writepwmled_write,};/* ---------------- platform_driver 部分 ---------------- */staticintpwmled_probe(structplatform_device*pdev){intret;structdevice*devpdev-dev;structpwm_statestate;pr_info(pwmled: probe called\n);/* 1. 获取PWM设备 */led_pwmpwm_get(dev,NULL);if(IS_ERR(led_pwm)){pr_err(pwmled: failed to get pwm device\n);returnPTR_ERR(led_pwm);}/* 2. 初始化PWM状态周期固定占空比0LED灭 */pwm_init_state(led_pwm,state);state.periodPWM_PERIOD_NS;state.duty_cycle0;state.enabledtrue;/* 使能PWM输出占空比0时LED熄灭 */retpwm_apply_state(led_pwm,state);if(ret){pr_err(pwmled: pwm_apply_state failed\n);pwm_put(led_pwm);returnret;}current_duty_ns0;/* 3. 动态分配设备号 */retalloc_chrdev_region(dev_num,0,1,DEVICE_NAME);if(ret0){pr_err(pwmled: alloc_chrdev_region failed\n);gotoerr_alloc;}/* 4. 初始化cdev */cdev_init(my_cdev,pwmled_fops);my_cdev.ownerTHIS_MODULE;retcdev_add(my_cdev,dev_num,1);if(ret){pr_err(pwmled: cdev_add failed\n);gotoerr_cdev_add;}/* 5. 创建class和设备节点 */my_classclass_create(THIS_MODULE,CLASS_NAME);if(IS_ERR(my_class)){pr_err(pwmled: class_create failed\n);retPTR_ERR(my_class);gotoerr_class_create;}my_devicedevice_create(my_class,dev,dev_num,NULL,DEVICE_NAME);if(IS_ERR(my_device)){pr_err(pwmled: device_create failed\n);retPTR_ERR(my_device);gotoerr_device_create;}pr_info(pwmled: /dev/%s created, period%u ns\n,DEVICE_NAME,PWM_PERIOD_NS);return0;err_device_create:class_destroy(my_class);err_class_create:cdev_del(my_cdev);err_cdev_add:unregister_chrdev_region(dev_num,1);err_alloc:/* 安全关闭PWM获取当前状态禁用后再释放 */pwm_get_state(led_pwm,state);state.enabledfalse;pwm_apply_state(led_pwm,state);pwm_put(led_pwm);returnret;}staticintpwmled_remove(structplatform_device*pdev){structpwm_statestate;/* 关闭PWM输出并释放资源 */pwm_get_state(led_pwm,state);state.enabledfalse;pwm_apply_state(led_pwm,state);device_destroy(my_class,dev_num);class_destroy(my_class);cdev_del(my_cdev);unregister_chrdev_region(dev_num,1);pwm_put(led_pwm);pr_info(pwmled: module unloaded\n);return0;}/* 设备树匹配表 */staticconststructof_device_idpwmled_of_match[]{{.compatibleyourname,pwm-led},{}};MODULE_DEVICE_TABLE(of,pwmled_of_match);staticstructplatform_driverpwmled_driver{.probepwmled_probe,.removepwmled_remove,.driver{.namepwmled,.ownerTHIS_MODULE,.of_match_tablepwmled_of_match,},};module_platform_driver(pwmled_driver);MODULE_LICENSE(GPL);MODULE_AUTHOR(Your Name);MODULE_DESCRIPTION(A PWM LED character device driver);MODULE_VERSION(1.0);代码关键点pwm_get(dev, NULL)获取设备树pwms属性指定的PWM资源。pwm_init_state() 手动修改state.period和state.duty_cycle后调用pwm_apply_state()这是标准的初始化流程。写函数中将百分比转换为纳秒占空比并仅在占空比大于0时使能输出节能。错误路径使用goto进行完整清理并确保PWM输出关闭关闭时先通过pwm_get_state获取当前状态再禁用避免未初始化字段。五、Makefile# Makefile for pwmled KERNEL_DIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) obj-m : pwmled_drv.o all: make -C $(KERNEL_DIR) M$(PWD) modules clean: make -C $(KERNEL_DIR) M$(PWD) clean交叉编译时设置ARCH和CROSS_COMPILE环境变量同前几篇文章。六、测试与验证6.1 加载驱动将pwmled_drv.ko拷贝到开发板执行insmod pwmled_drv.ko查看日志dmesg|tail# pwmled: probe called# pwmled: /dev/pwmled created, period50000 ns6.2 确认设备节点ls-l/dev/pwmled# crw------- 1 root root 238, 0 ...chmod666/dev/pwmled6.3 手动控制亮度echo0/dev/pwmled# 熄灭echo25/dev/pwmled# 25% 亮度echo50/dev/pwmled# 半亮echo100/dev/pwmled# 最亮观察LED亮度变化。读取当前百分比cat/dev/pwmled# 输出当前设置值例如 506.4 呼吸灯脚本编写breath.sh通过循环平滑改变占空比来实现呼吸效果。#!/bin/bashDEV/dev/pwmledSTEP2# 每次变化的百分比步长INTERVAL0.02# 每次变化的间隔秒echoStarting breath effect on$DEV...whiletrue;do# 从0渐亮到100for((i0;i100;iSTEP));doecho$i$DEVsleep$INTERVALdone# 从100渐暗到0for((i100;i0;i-STEP));doecho$i$DEVsleep$INTERVALdonedone赋予执行权限并运行chmodx breath.sh ./breath.sh你将看到LED像呼吸一样平滑地亮灭。按CtrlC停止脚本后可以手动设置占空比。如果开发板的sleep命令不支持小数例如某些busybox版本可将sleep $INTERVAL替换为usleep 2000020000微秒 0.02秒。6.5 卸载驱动rmmod pwmled_drvLED自动熄灭/dev/pwmled消失。七、常见问题排查insmod报错Unknown symbol pwm_apply_state内核可能未开启PWM子系统或API版本过旧。检查内核配置CONFIG_PWMy或尝试使用旧版APIpwm_configpwm_enable。LED不亮但驱动加载成功确认设备树中的PWM通道与实际硬件连接一致。检查引脚复用pinctrl是否已配置为PWM功能。用示波器测量PWM引脚是否有波形输出。占空比设置与亮度不对应检查LED的硬件极性若LED为共阳连接则有效电平为低需要在设备树pwms属性中设置第三参数为PWM_POLARITY_INVERTED如pwm3 0 50000 PWM_POLARITY_INVERTED并在驱动中设置state.polarity PWM_POLARITY_INVERTED。呼吸灯闪烁不连贯Shell脚本中的sleep精度有限若间隔过长会导致可见的阶梯变化。可改用C语言程序调用usleep()或在内核空间使用定时器直接实现平滑渐变但增加驱动复杂度。八、总结与下篇预告本文通过PWM子系统实现了LED亮度的动态调节并配合简单脚本完成了经典的呼吸灯效果。从GPIO的数字开关到PWM的模拟控制我们的硬件操控能力又上了一个台阶。下篇预告在实际项目中驱动往往需要同时处理多种外设并传递更丰富的数据。下一篇我们将探索I2C子系统驱动一个常见的I2C传感器如MPU6050或AT24C02 EEPROM学习如何在内核中与I2C设备通信并暴露数据给用户空间。敬请期待如果本文对你有帮助欢迎点赞、收藏、关注。有任何技术疑问欢迎在评论区留言交流