3. GPIO模拟PWM
任务目标
在RK3588平台上,通过内核GPIO字符设备接口与高精度定时器(hrtimer),在任意空闲GPIO上模拟PWM输出,实现LED呼吸灯效果,并允许通过命令行参数实时调整频率与占空比。
实现原理
内核hrtimer提供纳秒级定时回调。在回调中翻转GPIO电平并重新计算下一次翻转时间,即可在GPIO上产生方波。通过控制高电平持续时间(占空比)并周期性改变该值,即可实现呼吸灯效果。本案例使用Linux内核模块方式实现,直接操作GPIO子系统API,不依赖硬件PWM控制器。
完整代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/gpio/consumer.h>
#include <linux/hrtimer.h>
#include <linux/ktime.h>
#include <linux/device.h>
#include <linux/sysfs.h>
#define DEFAULT_GPIO GPIO_NUM(3, 1) // 根据实际板卡修改,此处为示例
#define DEFAULT_FREQ 1000 // 1kHz
#define DEFAULT_DUTY 50 // 50%
static struct gpio_desc *pwm_gpio;
static struct hrtimer pwm_timer;
static int freq = DEFAULT_FREQ;
static int duty = DEFAULT_DUTY;
static int breath_en = 1;
static int breath_step = 1;
static int current_duty = 0;
static ktime_t period_ns;
static enum hrtimer_restart pwm_timer_handler(struct hrtimer *timer)
{
static bool level = false;
ktime_t next_switch;
int high_ns, low_ns;
// 呼吸模式:周期性增减占空比
if (breath_en) {
current_duty += breath_step;
if (current_duty >= 95) {
current_duty = 95;
breath_step = -1;
} else if (current_duty <= 5) {
current_duty = 5;
breath_step = 1;
}
duty = current_duty;
}
high_ns = ktime_to_ns(period_ns) * duty / 100;
low_ns = ktime_to_ns(period_ns) - high_ns;
if (level) {
gpiod_set_value(pwm_gpio, 0);
next_switch = ktime_set(0, low_ns);
level = false;
} else {
gpiod_set_value(pwm_gpio, 1);
next_switch = ktime_set(0, high_ns);
level = true;
}
hrtimer_forward_now(timer, next_switch);
return HRTIMER_RESTART;
}
static ssize_t freq_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
int val;
if (kstrtoint(buf, 10, &val) || val < 1 || val > 100000)
return -EINVAL;
freq = val;
period_ns = ktime_set(0, 1000000000 / freq);
return count;
}
static ssize_t freq_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
return sprintf(buf, "%d\n", freq);
}
static ssize_t duty_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
int val;
if (kstrtoint(buf, 10, &val) || val < 0 || val > 100)
return -EINVAL;
duty = val;
breath_en = 0; // 手动设置时关闭呼吸模式
return count;
}
static ssize_t duty_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
return sprintf(buf, "%d\n", duty);
}
static ssize_t breath_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
int val;
if (kstrtoint(buf, 10, &val))
return -EINVAL;
breath_en = val ? 1 : 0;
if (breath_en)
current_duty = 5;
return count;
}
static ssize_t breath_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
return sprintf(buf, "%d\n", breath_en);
}
static DEVICE_ATTR_RW(freq);
static DEVICE_ATTR_RW(duty);
static DEVICE_ATTR_RW(breath);
static struct attribute *pwm_attrs[] = {
&dev_attr_freq.attr,
&dev_attr_duty.attr,
&dev_attr_breath.attr,
NULL,
};
ATTRIBUTE_GROUPS(pwm_attrs);
static struct class *pwm_class;
static struct device *pwm_device;
static int __init gpio_pwm_init(void)
{
int ret;
pwm_gpio = gpiod_get(NULL, NULL, GPIOD_OUT_LOW);
if (IS_ERR(pwm_gpio)) {
// 若未通过DT获取,则直接使用GPIO编号
pwm_gpio = gpio_to_desc(DEFAULT_GPIO);
if (!pwm_gpio) {
pr_err("Failed to get GPIO\n");
return -ENODEV;
}
ret = gpiod_direction_output(pwm_gpio, 0);
if (ret) {
pr_err("Failed to set GPIO direction\n");
return ret;
}
}
period_ns = ktime_set(0, 1000000000 / DEFAULT_FREQ);
current_duty = 5;
hrtimer_init(&pwm_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
pwm_timer.function = pwm_timer_handler;
hrtimer_start(&pwm_timer, period_ns, HRTIMER_MODE_REL);
pwm_class = class_create(THIS_MODULE, "gpio_pwm");
if (IS_ERR(pwm_class)) {
pr_err("Failed to create class\n");
hrtimer_cancel(&pwm_timer);
return PTR_ERR(pwm_class);
}
pwm_device = device_create(pwm_class, NULL, MKDEV(0, 0), NULL, "pwm_ctrl");
if (IS_ERR(pwm_device)) {
pr_err("Failed to create device\n");
class_destroy(pwm_class);
hrtimer_cancel(&pwm_timer);
return PTR_ERR(pwm_device);
}
ret = sysfs_create_groups(&pwm_device->kobj, pwm_groups);
if (ret) {
pr_err("Failed to create sysfs files\n");
device_destroy(pwm_class, MKDEV(0, 0));
class_destroy(pwm_class);
hrtimer_cancel(&pwm_timer);
return ret;
}
pr_info("GPIO PWM module loaded. GPIO=%d, freq=%dHz, duty=%d%%\n",
desc_to_gpio(pwm_gpio), DEFAULT_FREQ, DEFAULT_DUTY);
return 0;
}
static void __exit gpio_pwm_exit(void)
{
hrtimer_cancel(&pwm_timer);
gpiod_set_value(pwm_gpio, 0);
sysfs_remove_groups(&pwm_device->kobj, pwm_groups);
device_destroy(pwm_class, MKDEV(0, 0));
class_destroy(pwm_class);
gpiod_put(pwm_gpio);
pr_info("GPIO PWM module unloaded\n");
}
module_init(gpio_pwm_init);
module_exit(gpio_pwm_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("RK3588 Driver Lab");
MODULE_DESCRIPTION("GPIO Software PWM with hrtimer");
编译与加载
# 内核模块Makefile
obj-m := gpio_pwm.o
KDIR := /path/to/kernel/build/dir # 替换为实际内核源码路径
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
编译后执行:insmod gpio_pwm.ko,查看内核日志确认加载成功。
验证方法
将LED(串联330Ω电阻)连接至目标GPIO与GND之间。加载模块后,LED应自动开始呼吸效果(亮度从暗到亮再变暗,周期约2秒)。通过sysfs接口可实时调整参数:
# 查看当前频率
cat /sys/class/gpio_pwm/pwm_ctrl/freq
# 设置固定频率2kHz,占空比30%(自动关闭呼吸模式)
echo 2000 > /sys/class/gpio_pwm/pwm_ctrl/freq
echo 30 > /sys/class/gpio_pwm/pwm_ctrl/duty
# 重新开启呼吸模式
echo 1 > /sys/class/gpio_pwm/pwm_ctrl/breath
通过判定标准:
- 加载模块后LED呈现明显的呼吸渐变效果;
- 写入不同频率值后,LED闪烁频率明显变化(可用手机相机慢动作或示波器测量验证);
- 写入不同占空比值后,LED亮度明显变化(占空比越大越亮);
- 卸载模块(
rmmod gpio_pwm)后LED熄灭,无内核报错。
常见问题
- GPIO占用冲突:确认所选GPIO未被其他驱动占用,可通过
/sys/kernel/debug/gpio查看。 - 频率上限:hrtimer软件翻转受中断响应时间限制,建议频率不超过50kHz,否则波形失真。
- 呼吸速度调节:修改
pwm_timer_handler中current_duty的增减步长(当前为1),步长越大呼吸越快。