4. GPIO模拟PWM输出

任务目标

在瑞芯微RK3588平台上,通过高精度定时器(hrtimer)在GPIO上模拟PWM波形,实现LED亮度渐变效果。本案例不依赖硬件PWM控制器,而是利用内核定时器中断翻转GPIO电平,通过调节占空比控制LED平均电流,从而改变亮度。

环境与依赖

  • 硬件:RK3588开发板、LED(阳极接GPIO,阴极串330Ω电阻接地)
  • 内核:Linux 5.10+(含CONFIG_HIGH_RES_TIMERS)
  • GPIO编号:以RK3588的GPIO0_B2为例(对应Linux GPIO编号 gpiochip0 base + 10,实际需按板卡dts换算)

核心实现

驱动使用hrtimer创建周期定时器,在回调函数中翻转GPIO电平。通过sysfs节点动态调整占空比(0~100%),实现亮度渐变。关键点:hrtimer回调运行在原子上下文,GPIO操作需使用原子版本API(gpiod_set_value可安全用于原子上下文)。

#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/kobject.h>

#define PWM_PERIOD_NS 20000  // 20us周期,50kHz频率

static struct gpio_desc *pwm_gpio;
static struct hrtimer pwm_timer;
static int duty_cycle = 50;  // 默认占空比50%
static int gpio_value = 0;

static enum hrtimer_restart pwm_timer_handler(struct hrtimer *timer)
{
    ktime_t next_period;

    // 翻转GPIO电平
    gpio_value = !gpio_value;
    gpiod_set_value(pwm_gpio, gpio_value);

    // 根据占空比设置下一次定时时间
    if (gpio_value) {
        // 高电平持续时间 = 占空比 * 周期
        next_period = ktime_set(0, PWM_PERIOD_NS * duty_cycle / 100);
    } else {
        // 低电平持续时间 = (1 - 占空比) * 周期
        next_period = ktime_set(0, PWM_PERIOD_NS * (100 - duty_cycle) / 100);
    }

    hrtimer_forward_now(timer, next_period);
    return HRTIMER_RESTART;
}

static ssize_t duty_store(struct device *dev, struct device_attribute *attr,
                          const char *buf, size_t count)
{
    int value;
    if (kstrtoint(buf, 10, &value) < 0 || value < 0 || value > 100)
        return -EINVAL;
    duty_cycle = value;
    return count;
}

static ssize_t duty_show(struct device *dev, struct device_attribute *attr,
                         char *buf)
{
    return sprintf(buf, "%d\n", duty_cycle);
}

static DEVICE_ATTR(duty, 0644, duty_show, duty_store);

static int __init pwm_sim_init(void)
{
    int ret;

    // 获取GPIO(需在设备树中配置gpio-hog或使用gpiod_get)
    pwm_gpio = gpiod_get(NULL, "pwm-sim", GPIOD_OUT_LOW);
    if (IS_ERR(pwm_gpio)) {
        pr_err("Failed to get GPIO\n");
        return PTR_ERR(pwm_gpio);
    }

    // 初始化hrtimer
    hrtimer_init(&pwm_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
    pwm_timer.function = pwm_timer_handler;

    // 创建sysfs属性
    ret = device_create_file(&pwm_gpio->dev, &dev_attr_duty);
    if (ret) {
        gpiod_put(pwm_gpio);
        return ret;
    }

    // 启动定时器
    hrtimer_start(&pwm_timer, ktime_set(0, PWM_PERIOD_NS), HRTIMER_MODE_REL);
    pr_info("GPIO PWM simulator started, period=%dns, duty=%d%%\n",
            PWM_PERIOD_NS, duty_cycle);
    return 0;
}

static void __exit pwm_sim_exit(void)
{
    hrtimer_cancel(&pwm_timer);
    gpiod_set_value(pwm_gpio, 0);
    device_remove_file(&pwm_gpio->dev, &dev_attr_duty);
    gpiod_put(pwm_gpio);
    pr_info("GPIO PWM simulator stopped\n");
}

module_init(pwm_sim_init);
module_exit(pwm_sim_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("RK3588 GPIO PWM Simulator using hrtimer");

设备树配置

在板级dts中为GPIO添加别名,确保驱动能通过gpiod_get获取对应引脚:

&pinctrl {
    pwm_sim_pin: pwm-sim-pin {
        rockchip,pins = <0 RK_PB2 RK_FUNC_GPIO&nbsp;&nbsp;pinctrl_pull_down>;
    };
};

&gpio0 {
    pwm-sim-gpio {
        compatible = "gpio-led";
        pinctrl-names = "default";
        pinctrl-0 = <&pwm_sim_pin>;
        gpios = <&gpio0 RK_PB2 GPIO_ACTIVE_HIGH>;
        label = "pwm-sim";
    };
};

编译与加载

  1. 将驱动源码放入内核drivers/misc/目录,并添加Makefile条目:obj-m += pwm_sim.o
  2. 编译模块:make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- M=drivers/misc modules
  3. 加载模块:insmod pwm_sim.ko
  4. 确认加载:dmesg | tail 应看到启动日志

验证方法

  1. 静态验证:cat /sys/devices/platform/pwm-sim-gpio/duty 应输出当前占空比(默认50)
  2. 动态验证:写入不同占空比观察LED亮度变化:
    echo 20 > /sys/devices/platform/pwm-sim-gpio/duty   # 变暗
    echo 80 > /sys/devices/platform/pwm-sim-gpio/duty   # 变亮
  3. 波形验证(可选):用示波器或逻辑分析仪测量GPIO引脚,应看到频率约50kHz、占空比随写入值变化的方波
  4. 渐变测试:循环写入0→100→0的占空比序列,观察LED亮度平滑变化

通过标准:LED亮度随duty值单调变化,且无闪烁;dmesg无错误日志;卸载模块后GPIO恢复低电平。

注意事项

  • hrtimer回调中禁止调用可能导致睡眠的函数(如printk需谨慎),本实现仅做GPIO翻转和定时器重排,符合原子上下文要求
  • GPIO编号需根据实际板卡dts换算,不同型号RK3588核心板引脚偏移可能不同
  • 若需更高频率(如100kHz以上),需减小PWM_PERIOD_NS并确认hrtimer精度足够