3. PWM控制LED亮度

PWM控制LED亮度 学习卡片

任务目标

在嵌入式Linux环境中,通过PWM(脉冲宽度调制)控制LED亮度,实现从暗到亮再到暗的平滑呼吸灯效果。本案例直接操作硬件寄存器,不依赖Linux内核PWM子系统,适用于快速验证和教学演示。

硬件与前置条件

  • 开发板:i.MX6ULL(或同系列NXP i.MX处理器)
  • LED连接至GPIO1_IO08(对应PWM1输出引脚)
  • 交叉编译工具链已安装,目标板可运行Linux

核心实现

i.MX6ULL的PWM1控制器基地址为0x020BC000。通过配置PWM周期寄存器(PWMPR)和占空比寄存器(PWMSAR),即可控制输出波形的占空比,从而调节LED亮度。以下代码直接映射物理地址并操作寄存器,实现完整的呼吸灯效果:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdint.h>

// PWM1寄存器偏移(i.MX6ULL参考手册)
#define PWM_BASE    0x020BC000
#define PWMCR_OFF   0x00    // 控制寄存器
#define PWMSR_OFF   0x04    // 状态寄存器
#define PWMPR_OFF   0x08    // 周期寄存器
#define PWMSAR_OFF  0x0C    // 占空比寄存器

// PWMCR关键位定义
#define PWMCR_EN     (1 << 0)   // PWM使能
#define PWMCR_CLKSRC (2 << 2)   // 时钟源=IPG (66MHz)
#define PWMCR_PRESCALER(x) ((x) << 4)  // 预分频
#define PWMCR_POUTC   (1 << 9)  // 输出极性:高电平有效

// 映射物理地址到用户空间
static volatile uint32_t *pwm_regs;

static int pwm_init(void) {
    int fd = open("/dev/mem", O_RDWR | O_SYNC);
    if (fd < 0) {
        perror("open /dev/mem");
        return -1;
    }
    pwm_regs = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
                    MAP_SHARED, fd, PWM_BASE);
    close(fd);
    if (pwm_regs == MAP_FAILED) {
        perror("mmap");
        return -1;
    }
    return 0;
}

static void pwm_set_freq_duty(uint32_t freq_hz, uint32_t duty_percent) {
    uint32_t period, duty;
    uint32_t pwmcr_val;

    // 时钟源66MHz,预分频64 → PWM时钟≈1.03MHz
    // 周期 = PWM时钟频率 / 期望频率
    period = 1031250 / freq_hz;
    if (period > 0xFFFF) period = 0xFFFF;
    if (period < 2) period = 2;

    // 占空比 = 周期 × 百分比 / 100
    duty = period * duty_percent / 100;
    if (duty == 0) duty = 1;  // 避免完全关闭

    // 先关闭PWM再修改寄存器
    pwm_regs[PWMCR_OFF / 4] &= ~PWMCR_EN;

    pwm_regs[PWMPR_OFF / 4] = period - 1;
    pwm_regs[PWMSAR_OFF / 4] = duty;

    // 配置并启动:预分频64,输出高电平有效
    pwmcr_val = PWMCR_EN | PWMCR_CLKSRC | PWMCR_PRESCALER(6) | PWMCR_POUTC;
    pwm_regs[PWMCR_OFF / 4] = pwmcr_val;
}

int main(void) {
    int i;

    if (pwm_init() != 0) {
        return -1;
    }

    printf("呼吸灯启动,按Ctrl+C退出\n");

    // 呼吸灯循环:频率1kHz,占空比从1%递增到99%再递减
    while (1) {
        // 渐亮
        for (i = 1; i <= 99; i += 2) {
            pwm_set_freq_duty(1000, i);
            usleep(15000);  // 15ms
        }
        // 渐暗
        for (i = 99; i >= 1; i -= 2) {
            pwm_set_freq_duty(1000, i);
            usleep(15000);
        }
    }

    return 0;
}

编译与运行

# 交叉编译(以arm-linux-gnueabihf为例)
arm-linux-gnueabihf-gcc -o pwm_led pwm_led.c

# 拷贝到目标板并运行(需要root权限)
./pwm_led

验证方法

  1. 程序运行后,观察LED亮度应呈现从暗到亮、再从亮到暗的周期性平滑变化,每个完整周期约3秒。
  2. 使用示波器或万用表测量GPIO1_IO08引脚,应看到频率约1kHz、占空比周期性变化的方波信号。
  3. 按Ctrl+C终止程序后,LED应停止发光(PWM输出关闭)。

关键说明

  • 预分频值设为6(即2^6=64分频),实际PWM时钟为66MHz/64≈1.03MHz,周期寄存器值对应约1kHz输出。
  • 若LED亮度变化不明显,可检查LED是否连接至正确的PWM输出引脚,以及是否串联了合适的限流电阻(建议100Ω~330Ω)。
  • 本代码直接操作物理地址,仅适用于教学验证;实际产品开发应使用内核PWM子系统或设备树配置。