GD32 TIMER — TIMER1 PWM 输出

GD32 GD32F30x_Firmware_Library V3.0.3 TIMER TIMER1 PWM 输出 示例教学

TIMER — TIMER1 PWM 输出

固件库: GD32F30x_Firmware_Library V3.0.3
芯片: GD32F30x(以 GD32F307 评估板为例)
源文件: GD32F30x_Firmware_Library_V3.0.3/Examples/TIMER/TIMER1_pwmout/main.c


功能简介

本示例用 TIMER1 的 3 个通道(CH0/CH1/CH2)同时输出 3 路 PWM(脉冲宽度调制) 波形,每路的频率相同、占空比不同

  • CH0(PA0):占空比 25%
  • CH1(PA1):占空比 50%
  • CH2(PA2):占空比 75%

PWM 是嵌入式里最常用、也最好玩的功能之一——LED 呼吸灯、电机调速、舵机转角、蜂鸣器音调,本质上都是"调占空比 / 调频率"。本例不依赖任何中断,配置好定时器后它就在后台自动、持续地翻转引脚,CPU 可以干别的(这里只是 while(1) 空转)。

PWM 是什么直观感受? 想象一个电灯以很快的频率(比如 60Hz 以上,人眼看不见闪烁)在"亮—灭—亮—灭"。亮的时间占比就是"占空比"。占空比 25% 看起来暗,75% 看起来亮——这就是 LED 调光的原理。


硬件准备

1. 开发板

  • GD32F30x 评估板(本例无 LED/串口,纯定时器输出)。

2. 观察 PWM 的两种方式

  • 示波器 / 逻辑分析仪:直接测量 PA0 / PA1 / PA2 三路波形,最直观。
  • 用 LED + 限流电阻:把 PA0~PA2 各串一个 220Ω 电阻接 LED 到地。占空比 25% 的灯明显比 75% 的暗。注意 TIMER1 是 3.3V 电平,LED 别直接接大电流。
  • 万用表测电压:直流档测 PA0,读数 ≈ 3.3V × 占空比(25% → 约 0.83V,50% → 约 1.65V,75% → 约 2.48V)。这也是"PWM 当 DAC 用"的思路。

3. 引脚

通道 引脚 复用功能
CH0 PA0 TIMER1_CH0
CH1 PA1 TIMER1_CH1
CH2 PA2 TIMER1_CH2

完整代码(带中文注释)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include "gd32f30x.h"
#include <stdio.h>
#include "gd32f307c_eval.h"

void gpio_config(void);
void timer_config(void);

/*!
    \brief      GPIO 配置:把 PA0/PA1/PA2 设为 TIMER1 的复用输出
*/
void gpio_config(void)
{
    rcu_periph_clock_enable(RCU_GPIOA);
    rcu_periph_clock_enable(RCU_AF);   /* 用复用功能必须开 AF 时钟 */

    /* PA0/PA1/PA2 作为 TIMER1 的 CH0/CH1/CH2,配成 复用推挽输出 */
    gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0);
    gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_1);
    gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_2);
}

/*!
    \brief      TIMER1 配置:输出 3 路不同占空比的 PWM
    计算过程:
      TIMER1CLK = SystemCoreClock / 120 = 120MHz / 120 = 1MHz
      周期 = (ARR+1) / 1MHz = 16000 / 1MHz = 16ms  → 频率约 62.5Hz
      CH0 占空比 = 4000 / 16000 = 25%
      CH1 占空比 = 8000 / 16000 = 50%
      CH2 占空比 = 12000 / 16000 = 75%
*/
void timer_config(void)
{
    timer_oc_parameter_struct timer_ocintpara;
    timer_parameter_struct    timer_initpara;

    rcu_periph_clock_enable(RCU_TIMER1);

    timer_deinit(TIMER1);   /* 先复位,防止残留配置干扰 */

    /* ---- 定时器基础参数 ---- */
    timer_initpara.prescaler         = 119;            /* 预分频:120MHz/(119+1)=1MHz */
    timer_initpara.alignedmode       = TIMER_COUNTER_EDGE;   /* 边沿对齐(非中央对齐) */
    timer_initpara.counterdirection  = TIMER_COUNTER_UP;     /* 向上计数 0→ARR */
    timer_initpara.period            = 15999;          /* 自动重装载值 ARR:0~15999 */
    timer_initpara.clockdivision     = TIMER_CKDIV_DIV1;     /* 时钟不分频(与死区无关) */
    timer_initpara.repetitioncounter = 0;             /* 重复计数(高级定时器才用) */
    timer_init(TIMER1, &timer_initpara);

    /* ---- 三通道共用的输出参数 ---- */
    timer_ocintpara.outputstate  = TIMER_CCX_ENABLE;     /* 通道输出使能 */
    timer_ocintpara.outputnstate = TIMER_CCXN_DISABLE;  /* 互补通道不用 */
    timer_ocintpara.ocpolarity   = TIMER_OC_POLARITY_HIGH;    /* 有效电平=高 */
    timer_ocintpara.ocnpolarity  = TIMER_OCN_POLARITY_HIGH;
    timer_ocintpara.ocidlestate  = TIMER_OC_IDLE_STATE_LOW;   /* 空闲时输出低 */
    timer_ocintpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
    timer_channel_output_config(TIMER1, TIMER_CH_0, &timer_ocintpara);
    timer_channel_output_config(TIMER1, TIMER_CH_1, &timer_ocintpara);
    timer_channel_output_config(TIMER1, TIMER_CH_2, &timer_ocintpara);

    /* ---- CH0:PWM 模式0,比较值 3999 → 占空比约 25% ---- */
    timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_0, 3999);
    timer_channel_output_mode_config(TIMER1, TIMER_CH_0, TIMER_OC_MODE_PWM0);
    timer_channel_output_shadow_config(TIMER1, TIMER_CH_0, TIMER_OC_SHADOW_DISABLE);

    /* ---- CH1:PWM 模式0,比较值 7999 → 占空比约 50% ---- */
    timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_1, 7999);
    timer_channel_output_mode_config(TIMER1, TIMER_CH_1, TIMER_OC_MODE_PWM0);
    timer_channel_output_shadow_config(TIMER1, TIMER_CH_1, TIMER_OC_SHADOW_DISABLE);

    /* ---- CH2:PWM 模式0,比较值 11999 → 占空比约 75% ---- */
    timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_2, 11999);
    timer_channel_output_mode_config(TIMER1, TIMER_CH_2, TIMER_OC_MODE_PWM0);
    timer_channel_output_shadow_config(TIMER1, TIMER_CH_2, TIMER_OC_SHADOW_DISABLE);

    /* 开启自动重装载影子寄存器(更新 ARR 时不产生毛刺) */
    timer_auto_reload_shadow_enable(TIMER1);
    /* 启动定时器,PWM 开始输出 */
    timer_enable(TIMER1);
}

int main(void)
{
    gpio_config();
    timer_config();

    while (1) {
        /* 定时器在后台自动翻转引脚,CPU 这里空转即可 */
    }
}

代码分段讲解

1. GPIO:PWM 引脚是"复用功能"

1
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0);  // PA0 = TIMER1_CH0

PWM 波形不是软件一位位翻转出来的,而是定时器硬件直接驱动引脚产生的。所以引脚必须配成 GPIO_MODE_AF_PP(复用推挽),并且必须开 RCU_AF 时钟。如果忘了开 AF,引脚不会输出 PWM。

2. 定时器的"三件套":PSC、ARR、CCR

PWM 的两个关键量——频率占空比——由这三者决定:

参数 代码 含义 决定什么
预分频 PSC prescaler = 119 计数时钟 = 120MHz/120 = 1MHz 时间基准
重装载 ARR period = 15999 一个周期计 16000 次 → 16ms 频率(约 62.5Hz)
比较值 CCR pulse = 3999/7999/11999 计到该值时翻转 占空比

占空比公式(PWM 模式 0、高极性):

1
2
3
4
占空比 ≈ CCR / (ARR + 1)
CH0: 3999 / 16000 ≈ 25%
CH1: 7999 / 16000 ≈ 50%
CH2: 11999 / 16000 ≈ 75%

(代码注释里写 4000/16000 是取整近似,实际 CCR=3999,细节不重要,理解"比例"即可。)

为什么 TIMER1CLK 要除以 120 而不是直接用 120MHz? 直接用 120MHz 计数,16000 次只有 133 微秒,频率高达 7.5kHz,对 LED 调光没问题,但本例想演示"慢一点、好测"。分频到 1MHz 后频率约 62.5Hz,既方便示波器观察,也接近 LED 调光的常用频段。想得到别的频率,改 PSC 或 ARR 即可。

3. 通道输出配置:使能 + 极性

1
2
3
timer_ocintpara.outputstate  = TIMER_CCX_ENABLE;       // 通道输出打开
timer_ocintpara.ocpolarity   = TIMER_OC_POLARITY_HIGH; // 有效电平为高
timer_channel_output_config(TIMER1, TIMER_CH_x, &timer_ocintpara);
  • outputstate = ENABLE:告诉定时器"这个通道要往引脚上输出波形"。
  • ocpolarity = HIGH:把"有效电平"定义成高电平。配合 PWM0 模式,效果是"计数 < CCR 时输出低,≥ CCR 时输出高"——也就是高电平占 CCR/ARR,正弦地,占空比 = CCR/ARR。
  • 互补通道(outputnstate/ocnpolarity)只在高级定时器 TIMER0 驱动半桥电机时才用,本例关掉。

4. 每通道:比较值 + 模式 + 影子

1
2
3
timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_0, 3999);  // 设 CCR
timer_channel_output_mode_config(TIMER1, TIMER_CH_0, TIMER_OC_MODE_PWM0);  // 设 PWM0
timer_channel_output_shadow_config(TIMER1, TIMER_CH_0, TIMER_OC_SHADOW_DISABLE); // 影子

三行缺一不可:

  1. 设比较值:决定占空比的核心数字。
  2. 设模式为 PWM0:让通道进入"比较值到就翻转"的 PWM 行为。
  3. 影子寄存器:这里 DISABLE 表示 CCR 立即生效。若开启影子(ENABLE),修改 CCR 会在下次更新事件才生效,可避免中途改值导致波形断裂。本例静态输出,关不关都行。

5. 启动:影子 ARR + 使能

1
2
timer_auto_reload_shadow_enable(TIMER1);  // ARR 用影子寄存器
timer_enable(TIMER1);                    // 启动!PWM 开始输出
  • timer_auto_reload_shadow_enable:让 ARR 也在"更新事件"时才更新,避免在计数中途改周期导致波形毛刺(本例虽不改 ARR,但这是好习惯)。
  • timer_enable 才是真正的"发令枪"——调用之后,硬件计数器开始跑,引脚上立刻出现 PWM 波形。配置再多,不 enable 就没输出。

实验现象

  1. 下载程序并复位后,PA0 / PA1 / PA2 立即开始输出 PWM(无需任何触发)。
  2. 用示波器看:
    • 三路频率相同(约 62.5Hz,周期约 16ms)。
    • 占空比分别为 25% / 50% / 75%,高电平宽度依次翻倍。
  3. 若用 LED 接这三路:亮度明显呈 暗 → 中 → 亮 三档。
  4. 若用万用表直流档:电压约为 0.83V / 1.65V / 2.48V(3.3V × 占空比)。

常见问题

Q1:下载后引脚没有任何波形?
按顺序排查:① 是否 timer_enable(TIMER1) 被调用(没启动当然没输出);② 引脚是否配成 GPIO_MODE_AF_PP 且开了 RCU_AF;③ 是否开了 RCU_TIMER1 时钟;④ 通道 outputstate 是否为 TIMER_CCX_ENABLE

Q2:频率/占空比和预期不符?
先确认系统时钟:GD32F30x 评估板通常是 120MHz,例程按此算 1MHz 计数时钟。若你的板子系统时钟不同(如 108MHz),TIMER1CLK = SystemCoreClock / 120 的分母就要相应改。频率 = 1MHz / (ARR+1),占空比 = CCR/(ARR+1),用公式反推即可。

Q3:想改频率怎么办?
频率 = 计数时钟 / (ARR+1)。改 period(ARR)最直接;要同时保住占空比比例,就按同比例改三路的 pulse 值。注意 ARR 越大频率越低。

Q4:想调占空比怎么办?
改对应通道的 timer_channel_output_pulse_value_config(..., CCR)。CCR 越大占空比越高。运行时也可以反复调用这个函数动态调(比如做呼吸灯)。

Q5:PWM 模式 0 和模式 1 有什么区别?
PWM0:计数 < CCR 输出无效(低),≥ CCR 输出有效(高);PWM1 反过来。再叠加 ocpolarity 的极性翻转,最终波形可上可下。本例用 PWM0+高极性,结果是"高电平占比 = CCR/ARR"。记不住就实际用示波器看一眼。

Q6:为什么 PA0~PA2 能同时输出?一个定时器不是只有一个计数器吗?
对!TIMER1 只有一个计数器在跑,但它有多个比较通道(CH0~CH3),每个通道有独立的 CCR。计数器是大家共用的,每个通道拿同一个计数值和自己的 CCR 比,于是同一频率下能输出不同占空比的多路 PWM。这正是"用一个定时器驱动多个舵机/多个 LED"的原理。


小结

本示例把 PWM 的"三件套"讲得干干净净:

  1. 开时钟RCU_GPIOA + RCU_AF(复用必须)+ RCU_TIMER1
  2. 配 GPIO:PWM 引脚设 GPIO_MODE_AF_PP
  3. 配定时器PSC 定时间基准、ARR 定周期(频率)、计数方向=向上
  4. 配通道:使能输出、设极性、设 PWM0 模式、设 CCR(占空比)。
  5. 启动timer_auto_reload_shadow_enable + timer_enable,硬件自动输出。

记住公式 频率 = 计数时钟/(ARR+1),占空比 = CCR/(ARR+1),你就能随心所欲地产生任意 PWM。


拓展应用

掌握 PWM 后,可以做一堆好玩的项目:

  • LED 呼吸灯:在 while(1) 里用循环不断改 pulse 值(从 0 渐增到 ARR 再渐减),占空比平滑变化,LED 就会"呼吸"。
  • 电机调速:把 PWM 接到直流电机驱动模块(如 L298N、TB6612)的使能端,占空比越大转速越高。
  • 舵机控制:舵机靠 50Hz(周期 20ms)PWM 的脉宽定位,1.5ms 居中、0.52.5ms 对应 0180°。改 ARR/PSC 得到 50Hz,再用 CCR 调脉宽即可。
  • 蜂鸣器/音频:无源蜂鸣器用不同频率的 PWM 发出不同音调,可演奏简单音乐。
  • 伪 DAC:低通滤波 PWM 得到平滑电压,用占空比当"模拟量输出"(精度有限但省 DAC 外设)。
  • 多路独立 PWM:TIMER1 的 4 个通道可各自不同占空比,同时驱动 4 个舵机或 4 路 LED 调光。
  • 进阶:需要精确测频率/脉宽时,看同目录 TIMER2_inputcapture;需要电机死区/互补输出看 TIMER0 系列。

想理解"比较值怎么自动改出呼吸灯/波形",建议基于本例把 while(1) 改成动态调整 pulse 的小循环试试。

世界是你们
使用 Hugo 构建
主题 StackJimmy 设计