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

通过判定标准:

  1. 加载模块后LED呈现明显的呼吸渐变效果;
  2. 写入不同频率值后,LED闪烁频率明显变化(可用手机相机慢动作或示波器测量验证);
  3. 写入不同占空比值后,LED亮度明显变化(占空比越大越亮);
  4. 卸载模块(rmmod gpio_pwm)后LED熄灭,无内核报错。

常见问题

  • GPIO占用冲突:确认所选GPIO未被其他驱动占用,可通过/sys/kernel/debug/gpio查看。
  • 频率上限:hrtimer软件翻转受中断响应时间限制,建议频率不超过50kHz,否则波形失真。
  • 呼吸速度调节:修改pwm_timer_handler中current_duty的增减步长(当前为1),步长越大呼吸越快。