Skip to content

概述

PWM(Pulse Width Modulation,脉宽调制)通过调节输出方波的占空比控制平均电平,是 LED 调光、蜂鸣器、电机调速的基础。本教程使用官方示例 三通道 PWM 驱动 RGB LED:红、绿、蓝三路分别输出不同占空比的波形。

用大白话讲:PWM 就像快速开关灯的手:开的时间长、关的时间短,灯就显得亮;反过来就暗。这个「开的时间占总时间的比例」就是占空比(占空比 50% = 一半时间开、一半时间关)。因为切换速度快到肉眼反应不过来,灯看起来就是稳定的明暗。

本教程基于安信可官方 SDKAi-Thinker-Open/Ai-Thinker-WB2,版本 release_bl_iot_sdk_1.6.40)的官方示例 applications/peripherals/demo_pwm 编写,代码可在本地 SDK 中直接找到。

🎯本页目标通过硬件 PWM 在 IO14/IO17/IO3 三路输出不同频率与占空比的波形,驱动 RGB LED 显示不同颜色,掌握 PWM 通道配置方法。
🧰前置条件① Ai-WB2 开发板、RGB LED 模块(共阳或共阴,见开发板丝印)② 已按 [SDK 安装](../sdk/sdk_intro) 完成环境搭建。
🔗相关章节GPIO 输出见 [GPIO输出(点亮LED)](./gpio_led);定时器见 [TIMER 定时器](./timer)。

硬件接线

按官方示例接线(见 SDK applications/peripherals/demo_pwm/README.md):

Ai-WB2 引脚 RGB LED 引脚
IO14 Red(红)
IO17 Green(绿)
IO3 Blue(蓝)
3V3 VCC
GND GND

💡 示例将三路 GPIO 复用为 PWM 功能(复用 = 一根针脚可切换成不同功能,gpioFun = 8 就是指定 PWM 功能),同一引脚不能同时用于普通 GPIO 输出。

进入示例工程

打开终端,进入官方 demo_pwm 示例工程目录:

cd ~/Ai-Thinker-WB2/applications/peripherals/demo_pwm

说明:cd 是「进入目录」命令,这里进入 demo_pwm 示例工程目录;后续的 make 编译、make flash 烧录命令都必须先在这个目录里执行。

编写代码

打开 demo_pwm/main.c,本步完整代码已移至文末,见:

📜 完整代码 — 位于本页「完整代码」章节,默认折叠,点击展开,与官方示例(applications/peripherals/demo_pwm/demo_pwm/main.c)完全一致。

代码要点:

代码 作用
GLB_GPIO_Cfg_Type cfg[] 配置 GPIO 复用为 PWM 功能:gpioPin 指定引脚,gpioFun = 8 指定复用功能;引脚或功能号错则无波形输出
GLB_GPIO_Init(cfg + i) 初始化三路 GPIO 复用配置,不调用则引脚仍处于普通 GPIO 状态,不会出波形
PWM_Smart_Configure2(ch, clkDiv, period, threshold2) 配置 PWM 通道:频率 = 80MHz ÷ Divider ÷ Period占空比 = threshold2 ÷ Period;算错灯就不亮或亮度不对
PWM_Channel_Enable(ch) 使能 PWM 通道,开始输出;不使能引脚永远是低电平,灯不亮

三路 PWM 输出参数汇总:

通道 引脚 占空比 频率
2 IO14(红) 50%(1/2) 40MHz
3 IO17(绿) 75%(6/8) 5MHz
4 IO3(蓝) 10%(1/10) 1MHz
编译工程

在工程目录执行编译:

make -j8

说明:make 是「编译工程」命令,把代码变成开发板能运行的固件(程序文件);-j8 表示用 8 个核并行编译,速度更快。

编译成功后生成固件 build_out/demo_pwm.bin

烧录固件

开发板保持 USB 连接,确认串口设备号后执行烧录:

make flash p=/dev/ttyUSB0 b=921600

说明:make flash 是「烧录」命令,把编译好的固件写进开发板芯片。p= 后面是串口设备号(改成你电脑上实际的串口,可用 ls /dev/ttyUSB* 查看),b= 是烧录波特率(传输速度)。

⏳ 烧录过程中按提示长按开发板 EN 键进入下载模式,等待进度条完成即烧录成功。

运行验证

烧录完成后开发板自动重启运行,RGB LED 三色灯珠以各自占空比点亮,整体呈混合色;用逻辑分析仪可观察到三路不同频率、不同占空比的方波(见官方示例 img/logic_analyzer.jpg)。

img

💡 将某通道 threshold2 改为 period 的 1/2 即为 50% 占空比;改为 0 则恒为低电平。修改后重新 make -j8 && make flash 即可验证。

看到三色灯珠同时点亮、亮度不同(呈混合色)即为成功;如果某个灯不亮或颜色不对,说明还没成功,对照文末「常见问题与踩坑提示」排查。

代码执行流程

例程从启动到运行的完整流程如下(图中的循环箭头表示反复执行):


本文 API 汇总

GLB_GPIO_Init(cfg)

把指定引脚复用为 PWM 输出等外设功能(本教程配置三路 GPIO 复用为 PWM 功能)。

参数

  • cfgGLB_GPIO_Cfg_Type 结构体指针,必填。关键字段:gpioPin(引脚号,可选值:GPIO_PIN_0~GPIO_PIN_22,需为 PWM 支持的引脚)、gpioFun(复用功能号,可选值:GPIO_FUN_PWM0~GPIO_FUN_PWM5,本教程 8)、gpioMode(方向模式,本教程 GPIO_MODE_OUTPUT)、pullType(上下拉,本教程 GPIO_PULL_DOWN

返回值:无

PWM_Channel_Enable(ch)

打开 PWM 通道,开始输出配置好的波形。

参数

  • ch:PWM 通道号,可选值:PWM_CH0~PWM_CH5(需与 GPIO 复用功能号对应,如 PWM_CH0GPIO_FUN_PWM0

返回值:无

PWM_Channel_Disable(ch)

停止 PWM 输出(引脚回到静态电平)。

参数

  • ch:PWM 通道号,可选值:PWM_CH0~PWM_CH5

返回值:无

PWM_Smart_Configure2(ch, clkDiv, period, threshold2)

设置分频、周期与阈值,确定输出频率和占空比(示例自定义封装,源自官方 PWM_Smart_Configure)。

参数

  • ch:PWM 通道号,可选值:PWM_CH0~PWM_CH5
  • clkDiv:时钟分频系数,可选值:1~255频率 = 80MHz ÷ Divider ÷ Period
  • period:周期计数(决定频率),可选值:1~0xFFFF,如 4000(占空比分母)
  • threshold2:阈值 2(决定占空比),可选值:0~period占空比 = threshold2 ÷ period,如 2000(50% 占空比)

返回值:无


完整代码

以下为 demo_pwm/main.c 完整源码,与官方示例(applications/peripherals/demo_pwm/demo_pwm/main.c)完全一致:

📜 点击展开 demo_pwm/main.c 完整代码
c
#include <FreeRTOS.h>
#include <task.h>
#include <blog.h>
#include <bl602.h>
#include <bl602_gpio.h>
#include <bl602_glb.h>
#include <bl_pwm.h>

#define PWM_Get_Channel_Reg(ch)  (PWM_BASE+PWM_CHANNEL_OFFSET+(ch)*0x20)
#define PWM_STOP_TIMEOUT_COUNT          (160*1000)

/// @brief Copy from PWM_Smart_Configure. Use Bus Clock instead of External Crystal Clock for PWM Timer
/// @param ch PWM Channel
/// @param clkDiv PWM clock divider
/// @param period PWM period
/// @param threshold2 
/// @return 
BL_Err_Type PWM_Smart_Configure2(PWM_CH_ID_Type ch, uint16_t clkDiv, uint16_t period,uint16_t threshold2)
{
    uint32_t tmpVal;
    uint32_t timeoutCnt = PWM_STOP_TIMEOUT_COUNT;
    /* Get channel register */
    uint32_t PWMx = PWM_Get_Channel_Reg(ch);
        
    tmpVal = BL_RD_REG(PWMx, PWM_CONFIG);
    // if(BL_GET_REG_BITS_VAL(tmpVal, PWM_REG_CLK_SEL) != PWM_CLK_XCLK){
    if(BL_GET_REG_BITS_VAL(tmpVal, PWM_REG_CLK_SEL) != PWM_CLK_BCLK){
        BL_WR_REG(PWMx, PWM_CONFIG, BL_SET_REG_BIT(tmpVal, PWM_STOP_EN));
        while(!BL_IS_REG_BIT_SET(BL_RD_REG(PWMx, PWM_CONFIG), PWM_STS_TOP)){
            timeoutCnt--;
            if(timeoutCnt == 0){
                return TIMEOUT;
            }
        }
        // tmpVal = BL_SET_REG_BITS_VAL(tmpVal, PWM_REG_CLK_SEL, PWM_CLK_XCLK);
        tmpVal = BL_SET_REG_BITS_VAL(tmpVal, PWM_REG_CLK_SEL, PWM_CLK_BCLK);
    }
    tmpVal = BL_SET_REG_BITS_VAL(tmpVal, PWM_OUT_INV, PWM_POL_NORMAL);
    tmpVal = BL_SET_REG_BITS_VAL(tmpVal, PWM_STOP_MODE, PWM_STOP_GRACEFUL);
    BL_WR_REG(PWMx, PWM_CONFIG, tmpVal);

    /* Config pwm division */
    BL_WR_REG(PWMx, PWM_CLKDIV, clkDiv);

    /* Config pwm period and duty */
    BL_WR_REG(PWMx, PWM_PERIOD, period);
    BL_WR_REG(PWMx, PWM_THRE1, 0);
    BL_WR_REG(PWMx, PWM_THRE2, threshold2);
    
    return SUCCESS;
}

int main(void)
{
    GLB_GPIO_Cfg_Type cfg[3] = {
        {
            .drive = 0,
            .smtCtrl = 1,
            .gpioMode = GPIO_MODE_OUTPUT,
            .pullType = GPIO_PULL_DOWN,
            .gpioPin = 14,  /// red
            .gpioFun = 8,
        },
        {
            .drive = 0,
            .smtCtrl = 1,
            .gpioMode = GPIO_MODE_OUTPUT,
            .pullType = GPIO_PULL_DOWN,
            .gpioPin = 17,  /// green
            .gpioFun = 8,
        },
        {
            .drive = 0,
            .smtCtrl = 1,
            .gpioMode = GPIO_MODE_OUTPUT,
            .pullType = GPIO_PULL_DOWN,
            .gpioPin = 3,  /// blue
            .gpioFun = 8,
        },
    };

    for (int i = 0; i < 3; i++) {
        GLB_GPIO_Init(cfg + i);

        PWM_CH_ID_Type ch = cfg[i].gpioPin % PWM_CH_MAX;
        PWM_Channel_Disable(ch);
    }

    /// PWM Frequency = 80 Hz / Divider / Period

    /// set pwm channel 2 for 50% duty, 40MHz
    /// Divider = 1
    /// Period = 2
    PWM_Smart_Configure2(2, 1, 2, 1);
    PWM_Channel_Enable(2);

    /// set pwm channel 3 for 75% duty, 5MHz
    /// Divider = 2
    /// Period = 8
    PWM_Smart_Configure2(3, 2, 8, 6);
    PWM_Channel_Enable(3);

    /// set pwm channel 3 for 10% duty, 1MHz
    /// Divider = 8
    /// Period = 10
    PWM_Smart_Configure2(4, 8, 10, 1);
    PWM_Channel_Enable(4);

    for (;;) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }

    return 0;
}

常见问题与踩坑提示

⚠️ RGB LED 不亮或颜色异常
原因:RGB LED 模块引脚顺序与示例不符,或共阳/共阴类型不匹配
解决:对照模块丝印确认 R/G/B 引脚分别接 IO14/IO17/IO3;共阳 LED 需将 PWM_OUT_INV 反相或将 3V3/GND 对调

⚠️ 同一引脚先做 GPIO 后配 PWM 不生效
原因:引脚复用功能未切换干净
解决:重新上电或复位后再烧录;确认 gpioFun = 8 为 PWM 复用功能号(见 SDK 引脚复用表)

⚠️ 占空比计算公式容易记反
原因threshold2Period 的角色混淆
解决:占空比 = threshold2 / Period,例如 Period=10、threshold2=1 即 10% 占空比

⚠️ 烧录一直卡住等待,进度条不动
原因:未进入下载模式,或数据线只能充电不能传数据
解决:烧录时按提示长按 EN 键进入下载模式;换一根能传数据的 Type-C 数据线后重试

⚠️ 找不到串口设备或提示无权限
原因:Linux 下 /dev/ttyUSB0 不存在或权限不足,Windows 下未安装 USB 转串口驱动
解决:Linux 用 ls /dev/ttyUSB* 确认设备号,权限不足执行 sudo usermod -aG dialout $USER 后重新登录;Windows 在设备管理器安装驱动并确认 COM 口号

⚠️ 执行 make 报找不到 Makefile
原因:在错误的目录执行了编译命令(必须在示例工程目录内)
解决:先执行 cd ~/Ai-Thinker-WB2/applications/peripherals/demo_pwm 进入工程目录,再执行 make -j8

运行自检

RGB LED 三色同时点亮且亮度不同(混合色),或逻辑分析仪观察到三路不同频率方波,即 PWM 功能验证通过。

遇到问题?

如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

Released under the MIT License. Build Time 2026-09-11 14:52:23