第 10 篇 RK 平台安卓驱动实战 3:PWM 驱动开发,实现 LED 呼吸灯 + 电机调速

第 10 篇 RK 平台安卓驱动实战 3:PWM 驱动开发,实现 LED 呼吸灯 + 电机调速 目录开篇先搞懂PWM 到底是什么大白话定义核心概念小白必须懂常用场景的频率选择一、RK3568 PWM 控制器详解小白必记核心特点引脚复用配置二、实战前的硬件准备硬件清单硬件接线1LED 呼吸灯接线2直流电机调速接线三、第一步设备树配置1. 修改板级设备树文件核心属性讲解2. 编译烧录验证四、第二步PWM 内核驱动开发1. 核心 PWM API 讲解小白必懂2. 驱动代码编写3. 编译驱动烧录验证五、第三步HAL 层适配 安卓 App 开发1. HAL 层代码头文件my_pwm_hal.h实现文件my_pwm_hal.c2. 安卓 App 核心功能实现1LED 亮度调节2自动呼吸灯效果3电机调速六、小白 PWM 驱动必踩的坑提前规避结尾说两句大家好我是黒漂技术佬。前两篇我们搞定了 GPIO 和中断驱动实现了 LED 的亮灭和按键的实时响应。后台很多兄弟问“佬LED 只能亮和灭能不能实现渐变的呼吸灯效果还有我想控制电机的转速该怎么实现”答案就是PWM 驱动PWM 是嵌入式开发里极其常用的技术不仅能实现呼吸灯、电机调速还能做蜂鸣器音量调节、舵机角度控制、屏幕背光调节几乎所有需要模拟量控制的场景都能用到 PWM。今天这篇我就用大白话给你讲透 PWM 的核心原理手把手带你完成RK3568 平台 PWM 驱动的完整开发实现 LED 呼吸灯效果和直流电机调速并且打通安卓 App 调节 PWM 频率和占空比的全链路学完就能用在你的项目里。开篇先搞懂PWM 到底是什么大白话定义PWM 的全称是 Pulse Width Modulation脉冲宽度调制。说白了就是在一个固定的周期内通过控制高电平持续的时间脉冲宽度来改变输出电平的平均占空比从而实现模拟量的控制效果。举个最通俗的例子你家里的电灯开关只能开和关对应 GPIO 的高电平和低电平。如果你快速的反复开关这个灯1 秒钟内开关 1000 次你的眼睛根本看不到灯在闪烁只会觉得灯的亮度变了。如果 1 秒内灯亮的时间占 50%灭的时间占 50%你看到的就是半亮的状态亮的时间占 100%就是全亮亮的时间占 10%就是微亮。PWM 就是这个原理它通过芯片内部的 PWM 控制器输出固定频率的方波信号我们只需要调节高电平持续的时间占空比就能改变输出的平均电压从而实现呼吸灯、电机调速等效果。核心概念小白必须懂周期PeriodPWM 信号完成一个完整的高低电平循环所需要的时间单位是纳秒ns周期和频率互为倒数频率Frequency1 秒钟内 PWM 信号循环的次数单位是赫兹Hz频率 1 / 周期。比如周期是 1ms频率就是 1000Hz占空比Duty Cycle一个周期内高电平持续的时间占整个周期的比例用百分比表示。比如周期 1ms高电平持续 0.5ms占空比就是 50%分辨率PWM 占空比的调节精度比如 10 位分辨率就是把一个周期分成 1024 份占空比可以调节 1/1024 的精度。常用场景的频率选择不同的场景需要的 PWM 频率不一样给小白整理好了直接用就行表格应用场景推荐频率原因LED 呼吸灯 / 背光调节1kHz~10kHz频率高于人眼的视觉暂留频率不会看到闪烁直流电机调速10kHz~20kHz频率高于人耳的听觉范围不会听到电机的啸叫舵机控制50Hz20ms 周期舵机的标准协议固定 50Hz蜂鸣器2kHz~5kHz对应蜂鸣器的谐振频率声音最大一、RK3568 PWM 控制器详解小白必记RK3568 芯片内置了16 路 PWM 控制器分别是 PWM0~PWM15每一路都可以独立配置频率和占空比功能完全一样我们可以随便选一路来用。核心特点支持可编程的周期和占空比周期范围非常宽从几十纳秒到几十秒都可以配置支持 4 种工作模式我们入门只需要用默认的连续输出模式就行每一路 PWM 都可以配置引脚复用把 PWM 信号输出到对应的 GPIO 引脚上官方 SDK 里已经实现了完整的 PWM 子系统驱动我们不用从零写 PWM 控制器的驱动只需要调用内核提供的 PWM API就能实现 PWM 控制极大降低了开发难度。引脚复用配置RK3568 的每一路 PWM都对应了固定的 GPIO 引脚我们要使用 PWM 功能必须在设备树里把对应的引脚配置为 PWM 功能而不是普通 GPIO 功能。比如我们这次实战用的PWM2对应的引脚是 GPIO0_A2我们需要在设备树的 pinctrl 里把这个引脚配置为 PWM2 功能而不是 GPIO 功能。二、实战前的硬件准备我们这次的实战分为两个部分LED 呼吸灯用 PWM2 输出 PWM 信号控制 LED 灯的亮度实现渐变的呼吸灯效果安卓 App 可以调节呼吸频率和亮度直流电机调速用 PWM2 输出 PWM 信号通过 L298N 电机驱动模块控制直流电机的转速安卓 App 可以调节电机转速。硬件清单RK3568 开发板 1 块LED 灯 1 个1K 限流电阻 1 个直流减速电机 1 个3.3V~12V 都可以L298N 电机驱动模块 1 个必须用GPIO 和 PWM 引脚不能直接驱动电机电流不够电机配套电源 1 个根据电机的额定电压选杜邦线若干面包板 1 个。硬件接线1LED 呼吸灯接线表格RK3568 开发板引脚外接硬件接线说明GPIO0_A2PWM2LED 阳极长脚LED 阴极接 1K 限流电阻电阻另一端接 GNDGNDLED 的 GND共地2直流电机调速接线表格RK3568 开发板L298N 电机驱动模块说明GPIO0_A2PWM2ENAPWM 调速信号输入GPIO0_B0IN1电机正转控制引脚GPIO0_B1IN2电机反转控制引脚GNDGND开发板和驱动模块共地电机电源12V 输入电机的供电电源根据电机额定电压选电机输出端 OUT1、OUT2直流电机两端接电机小白红线警告绝对不能用开发板的 GPIO 引脚直接驱动电机电机的启动电流很大会直接烧芯片引脚必须用 L298N 这类电机驱动模块开发板、驱动模块、电机电源必须共地不然 PWM 信号会乱跳电机工作不正常电机电源的电压不要超过电机的额定电压不然会烧电机。三、第一步设备树配置我们需要在设备树里添加 PWM 设备节点配置引脚复用使能对应的 PWM 控制器。1. 修改板级设备树文件进入设备树目录打开你的开发板对应的.dts 文件bash运行cd ~/RK3568_Android11_SDK/kernel/arch/arm64/boot/dts/rockchip/ vim rk3568-firefly.dts在根节点里添加我们的 PWM 设备节点同时配置引脚复用dts/ { my_pwm: pwm_demo0 { compatible my-pwm,demo; status okay; pwms pwm2 0 1000000 0; // pwms属性说明pwm控制器 0 周期(ns) 极性 // 这里我们配置PWM2周期1000000ns1ms频率1000Hz正常极性 pinctrl-names default; pinctrl-0 pwm2_pins; }; // 引脚复用配置把GPIO0_A2配置为PWM2功能 pinctrl { pwm2 { pwm2_pins: pwm2-pins { rockchip,pins 0 RK_PA2 1 pcfg_pull_none; // 说明0 RK_PA2 1 ...1代表复用功能1也就是PWM2功能 }; }; }; // 使能PWM2控制器 pwm2 { status okay; }; };核心属性讲解pwms pwm2 0 1000000 0这是 PWM 设备节点的核心属性四个参数的含义pwm2使用的 PWM 控制器我们用的是 PWM2第二个参数0PWM 控制器的通道号RK3568 的 PWM 都是单通道固定填 0 就行第三个参数1000000PWM 的周期单位是纳秒ns1000000ns1ms对应频率 1000Hz适合 LED 呼吸灯第四个参数0PWM 的极性0 是正常极性占空比越高高电平时间越长1 是反极性引脚复用配置里的rockchip,pins 0 RK_PA2 1 pcfg_pull_none第三个参数1代表把 GPIO0_A2 配置为复用功能 1也就是 PWM2 功能每个引脚的复用功能编号可以在 RK3568 的芯片 datasheet 里查到。2. 编译烧录验证编译设备树打包 boot.img烧录到开发板重启验证设备树节点生效bash运行adb shell su ls /proc/device-tree/pwm_demo0能看到节点属性说明配置成功验证 PWM 控制器使能成功bash运行cat /sys/class/pwm/pwmchip2/npwm输出1说明 PWM2 控制器已经正常使能可以使用了。四、第二步PWM 内核驱动开发RK 官方已经实现了 PWM 控制器的底层驱动我们只需要调用内核提供的 PWM 子系统 API封装成字符设备驱动给上层 HAL 层调用就行。1. 核心 PWM API 讲解小白必懂Linux 内核 PWM 子系统给我们提供了一套标准的 API不用我们关心底层的寄存器操作直接调用就行表格API 函数作用devm_pwm_get()从设备树里获取 PWM 设备句柄推荐用带 devm 的函数驱动卸载的时候会自动释放资源pwm_config()配置 PWM 的周期和占空比单位都是纳秒nspwm_enable()使能 PWM 输出调用后 PWM 引脚就会输出配置好的方波pwm_disable()关闭 PWM 输出pwm_set_polarity()设置 PWM 的极性2. 驱动代码编写创建驱动文件bash运行cd ~/RK3568_Android11_SDK/kernel/drivers/char/my_drivers touch pwm_drv.c完整驱动代码全注释详解c运行#include linux/init.h #include linux/module.h #include linux/fs.h #include linux/cdev.h #include linux/uaccess.h #include linux/device.h #include linux/platform_device.h #include linux/pwm.h // 驱动信息声明 MODULE_LICENSE(GPL); MODULE_AUTHOR(黒漂技术佬); MODULE_DESCRIPTION(RK3568 Android PWM Driver); MODULE_VERSION(1.0); // 宏定义 #define DEVICE_NAME pwm_drv #define CLASS_NAME pwm_class // PWM控制命令 #define CMD_PWM_SET_CONFIG 0x2001 // 设置周期和占空比 #define CMD_PWM_ENABLE 0x2002 // 使能PWM输出 #define CMD_PWM_DISABLE 0x2003 // 关闭PWM输出 // PWM配置结构体和用户空间交互用 struct pwm_config { unsigned int period_ns; // 周期单位ns unsigned int duty_ns; // 占空比高电平时间单位ns }; // 全局变量 static dev_t pwm_devno; static struct cdev pwm_cdev; static struct class *pwm_class; static struct device *pwm_device; static struct pwm_device *pwm_dev; // PWM设备句柄 static int pwm_enabled 0; // PWM输出使能状态 // 字符设备核心函数 static int pwm_open(struct inode *inode, struct file *filp) { printk(【pwm_drv】设备被打开\n); return 0; } static long pwm_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct pwm_config config; int ret 0; switch (cmd) { case CMD_PWM_SET_CONFIG: // 从用户空间拷贝PWM配置参数 ret copy_from_user(config, (struct pwm_config __user *)arg, sizeof(struct pwm_config)); if (ret) { printk(【pwm_drv】配置参数拷贝失败\n); return -EFAULT; } // 校验参数占空比不能大于周期 if (config.duty_ns config.period_ns) { printk(【pwm_drv】参数错误占空比大于周期\n); return -EINVAL; } // 配置PWM的周期和占空比 ret pwm_config(pwm_dev, config.period_ns, config.duty_ns); if (ret) { printk(【pwm_drv】PWM配置失败\n); return ret; } printk(【pwm_drv】PWM配置成功周期%uns占空比%uns\n, config.period_ns, config.duty_ns); break; case CMD_PWM_ENABLE: // 使能PWM输出 if (!pwm_enabled) { ret pwm_enable(pwm_dev); if (ret 0) { pwm_enabled 1; printk(【pwm_drv】PWM输出使能\n); } else { printk(【pwm_drv】PWM使能失败\n); } } break; case CMD_PWM_DISABLE: // 关闭PWM输出 if (pwm_enabled) { pwm_disable(pwm_dev); pwm_enabled 0; printk(【pwm_drv】PWM输出关闭\n); } break; default: printk(【pwm_drv】无效命令\n); return -EINVAL; } return ret; } static int pwm_release(struct inode *inode, struct file *filp) { printk(【pwm_drv】设备被关闭\n); return 0; } // file_operations结构体 static const struct file_operations pwm_fops { .owner THIS_MODULE, .open pwm_open, .unlocked_ioctl pwm_ioctl, .release pwm_release, }; // platform驱动框架 static int pwm_drv_probe(struct platform_device *pdev) { int ret; printk(【pwm_drv】驱动和设备树匹配成功\n); // 1. 从设备树获取PWM设备句柄 pwm_dev devm_pwm_get(pdev-dev, NULL); if (IS_ERR(pwm_dev)) { ret PTR_ERR(pwm_dev); dev_err(pdev-dev, 获取PWM设备失败错误码%d\n, ret); return ret; } dev_info(pdev-dev, PWM设备获取成功\n); // 2. 注册字符设备 ret alloc_chrdev_region(pwm_devno, 0, 1, DEVICE_NAME); if (ret 0) { dev_err(pdev-dev, 设备号申请失败\n); return ret; } cdev_init(pwm_cdev, pwm_fops); pwm_cdev.owner THIS_MODULE; ret cdev_add(pwm_cdev, pwm_devno, 1); if (ret 0) { dev_err(pdev-dev, 字符设备注册失败\n); goto err_devno_free; } pwm_class class_create(THIS_MODULE, CLASS_NAME); if (IS_ERR(pwm_class)) { ret PTR_ERR(pwm_class); dev_err(pdev-dev, 设备类创建失败\n); goto err_cdev_del; } pwm_device device_create(pwm_class, NULL, pwm_devno, NULL, DEVICE_NAME); if (IS_ERR(pwm_device)) { ret PTR_ERR(pwm_device); dev_err(pwm-dev, 设备创建失败\n); goto err_class_destroy; } dev_info(pdev-dev, PWM驱动加载成功\n); return 0; // 错误处理 err_class_destroy: class_destroy(pwm_class); err_cdev_del: cdev_del(pwm_cdev); err_devno_free: unregister_chrdev_region(pwm_devno, 1); return ret; } static int pwm_drv_remove(struct platform_device *pdev) { printk(【pwm_drv】驱动开始卸载\n); // 关闭PWM输出 if (pwm_enabled) { pwm_disable(pwm_dev); } // 释放资源 device_destroy(pwm_class, pwm_devno); class_destroy(pwm_class); cdev_del(pwm_cdev); unregister_chrdev_region(pwm_devno, 1); dev_info(pdev-dev, PWM驱动卸载成功\n); return 0; } // 设备树匹配表 static const struct of_device_id pwm_drv_of_match[] { { .compatible my-pwm,demo }, { /* 结束 */ } }; MODULE_DEVICE_TABLE(of, pwm_drv_of_match); static struct platform_driver pwm_drv_driver { .probe pwm_drv_probe, .remove pwm_drv_remove, .driver { .name pwm_drv_driver, .of_match_table pwm_drv_of_match, }, }; // 驱动入口和出口 static int __init pwm_drv_init(void) { printk(【pwm_drv】PWM驱动开始加载\n); return platform_driver_register(pwm_drv_driver); } static void __exit pwm_drv_exit(void) { platform_driver_unregister(pwm_drv_driver); } module_init(pwm_drv_init); module_exit(pwm_drv_exit);3. 编译驱动烧录验证修改 Makefile添加 PWM 驱动的编译makefileobj-y hello_drv.o obj-y gpio_drv.o obj-y key_irq_drv.o obj-y pwm_drv.o编译内核打包 boot.img烧录到开发板重启验证驱动加载成功bash运行adb shell su dmesg | grep pwm_drv能看到「PWM 驱动加载成功」的日志说明驱动正常加载查看设备文件设置权限bash运行ls -l /dev/pwm_drv chmod 777 /dev/pwm_drv测试 PWM 功能我们用命令行测试设置 PWM 周期 1ms1000000ns占空比 50%500000ns使能输出bash运行# 编写一个简单的测试程序或者用ioctl工具测试 # 测试成功后LED会变成半亮状态说明PWM工作正常五、第三步HAL 层适配 安卓 App 开发和之前的流程一样我们完成 HAL 层适配、JNI 封装然后写一个安卓 App实现两个功能LED 呼吸灯App 里有一个滑动条调节 LED 的亮度还有一个按钮开启自动呼吸灯效果电机调速滑动条调节 PWM 占空比控制电机的转速按钮控制电机正转、反转、停止。1. HAL 层代码头文件my_pwm_hal.hc运行#ifndef MY_PWM_HAL_H #define MY_PWM_HAL_H #ifdef __cplusplus extern C { #endif struct pwm_config { unsigned int period_ns; unsigned int duty_ns; }; int pwm_set_config(struct pwm_config *config); int pwm_enable(void); int pwm_disable(void); #ifdef __cplusplus } #endif #endif实现文件my_pwm_hal.cc运行#include stdio.h #include stdlib.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include my_pwm_hal.h #define DEVICE_PATH /dev/pwm_drv #define CMD_PWM_SET_CONFIG 0x2001 #define CMD_PWM_ENABLE 0x2002 #define CMD_PWM_DISABLE 0x2003 static int fd -1; static int pwm_dev_init(void) { if (fd 0) { fd open(DEVICE_PATH, O_RDWR); if (fd 0) { printf(【pwm_hal】打开设备文件失败\n); return -1; } } return 0; } int pwm_set_config(struct pwm_config *config) { int ret; if (pwm_dev_init() 0) return -1; ret ioctl(fd, CMD_PWM_SET_CONFIG, config); if (ret 0) { printf(【pwm_hal】PWM配置失败\n); return -1; } return 0; } int pwm_enable(void) { int ret; if (pwm_dev_init() 0) return -1; ret ioctl(fd, CMD_PWM_ENABLE, 0); if (ret 0) { printf(【pwm_hal】PWM使能失败\n); return -1; } return 0; } int pwm_disable(void) { int ret; if (pwm_dev_init() 0) return -1; ret ioctl(fd, CMD_PWM_DISABLE, 0); if (ret 0) { printf(【pwm_hal】PWM关闭失败\n); return -1; } return 0; }2. 安卓 App 核心功能实现1LED 亮度调节通过 SeekBar 滑动条调节 PWM 的占空比0% 的时候 LED 熄灭100% 的时候全亮实现亮度调节java运行// 滑动条变化监听 seekBarBrightness.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { // 周期固定1ms1000000ns占空比progress% struct pwm_config config new struct pwm_config(); config.period_ns 1000000; config.duty_ns progress * 10000; // 0~100 → 0~1000000ns pwmJni.setConfig(config); } });2自动呼吸灯效果开启一个线程循环改变占空比从 0 到 100再从 100 到 0实现渐变的呼吸灯效果java运行new Thread(() - { int duty 0; int step 1; while (isBreathing) { struct pwm_config config new struct pwm_config(); config.period_ns 1000000; config.duty_ns duty * 10000; pwmJni.setConfig(config); duty step; if (duty 100) step -1; if (duty 0) step 1; try { Thread.sleep(20); // 20ms更新一次呼吸周期4秒 } catch (InterruptedException e) { e.printStackTrace(); } } }).start();3电机调速电机调速的原理和 LED 亮度调节完全一样只需要把 PWM 频率改成 10kHz周期 100000ns通过调节占空比改变电机的平均电压从而调节转速。六、小白 PWM 驱动必踩的坑提前规避坑 1PWM 引脚没有输出完全没反应90% 的情况是引脚复用配置错了没有把引脚配置为 PWM 功能还是 GPIO 功能。解决方法检查设备树里的 pinctrl 配置确认引脚的复用功能编号正确和芯片 datasheet 里的一致坑 2PWM 能输出但是频率和配置的不一样周期的单位是纳秒ns很多新手搞错了单位比如把 1ms 写成 1000ns实际频率就变成了 1MHz和预期的不一样。解决方法记住周期的单位是纳秒1ms1000000ns10kHz100000ns 周期坑 3LED 呼吸灯有闪烁电机有啸叫PWM 频率太低了低于人眼 / 人耳的感知范围。解决方法LED 呼吸灯把频率调到 1kHz 以上电机调速调到 10kHz 以上坑 4pwm_config () 调用失败占空比设置的比周期还大或者周期超出了 PWM 控制器的支持范围。解决方法校验参数保证占空比小于等于周期周期在合理范围内坑 5电机不转或者转速不正常没有用电机驱动模块直接用 GPIO 引脚驱动电机电流不够或者没有共地PWM 信号不正常。解决方法必须用 L298N 这类驱动模块保证开发板、驱动模块、电源共地。结尾说两句这篇文章我们彻底搞懂了 PWM 的核心原理完成了 RK 平台 PWM 驱动的完整开发实现了 LED 呼吸灯和电机调速的功能打通了安卓 App 调节 PWM 参数的全链路。PWM 是嵌入式开发里非常实用的技术学完这篇你就能应对绝大多数模拟量控制的场景了。下一篇我们进入串行总线驱动的第一部分I2C 设备驱动开发以 0.96 寸 OLED 屏为例教你怎么用 Linux I2C 子系统实现 I2C 设备的驱动开发并且打通安卓 App 控制屏幕显示的全链路。我是黒漂技术佬关注我带你零基础入门 RK 安卓驱动开发不踩坑。有任何 PWM 驱动的问题评论区留言我都会一一回复。