浅浅的撅一下这个TIMER(二):定时器发生PWM

浅浅的撅一下这个TIMER(二):定时器发生PWM

上回我们利用定时器完成了基本的定时操作,进行了周期性的中断调用。这回我们用它来完成定时器的另一个常见操作——PWM波的发生。

本文还是以GD32E230所配的TIMER13(通用L2)为例。
(还是因为它配置最寒碜(TIMER13:为什么受伤的总是我?))

旧事重提

GD32E23x的通用L2定时器结构 来自GD32E23x_User_Manual_Rev1.5_CN.pdf
没错,又是这个老图。
也没错,今天我们还是只需要其中的一部分功能,因此这个图又可以砍成这样:
被砍完了的定时器结构
根据上次我们所讲的,定时器输入时钟先经过预分频,然后作为计数器的计数时钟进行计数。在最常用的向上计数模式中,每次时钟脉冲会令计数器的值增加1。一旦计数器的值达到自动重载值(溢出事件),定时器就会将计数器的值重置为0,并重新开始计数。

定时器的PWM模式运行初探

那计数器和定时器发生PWM又有什么关系呢?
不妨考虑一下,定时器处在计数运行时,溢出事件的发生是固定周期性的,也就是说溢出事件发生的频率是固定的。
那么根据“从特殊到一般、从简单到复杂”的探索思维,设想一个最简化的情况,如果我们需要发生一个1kHz的对称方波信号(即占空比为50%),那么一个方波的周期即为1ms,也即高电平和低电平各占0.5ms。换句话说,电平翻转的频率是2kHz(一个方波周期内有两次翻转)。如果我们将定时器的溢出事件周期配置为2kHz,并开启对应的溢出中断,那么该中断函数就会被以2kHz的频率调用。如果我们在该函数内翻转某个IO输出电平,则在该IO上即会发生频率为2kHz的电平翻转,也就是产生了1kHz的方波信号。
图形示意如下:(每条青色竖线代表一次溢出中断)
电平翻转与波形产生1
如果我们需要产生一个1kHz、占空比为10%的PWM方波信号呢?那么我们可以类比上面的过程(实际上是将一个波形的周期等分为2份),将一个波形的周期时间等分为10份,在第1份的时间(0.1ms)内令其为高电平,而在其余9份的时间(0.9ms)内令其为低电平:
电平翻转与波形产生2
也就是在第1份时间的开始处将IO输出电平置为高,在第2份时间的开始处将IO输出电平置为低。为了在中断内确定“当前是第几份时间开始处的中断”,我们可以另外设置一个状态变量以记录,在每次中断内+1,若超过0-9的范围即归0。
可以看出,如果我们将一个周期的时间等分的份数越多,我们就可以产生越高分辨率的PWM方波信号。PWM占空比的最小分辨率与周期(T)、周期等分数(N)显然有以下关系:
$$
Resolution_{min}( \% )=(\frac{1}{N})*100 \%
$$

$$
Resolution_{min}=(\frac{T}{N})
$$

然后呢?

上面的方式有一个缺点:定时器的中断会反复打断CPU的运行,在中断内计数、判断的过程均会占用CPU时间。并且在进行一些对时序有高要求的操作时,若关中断会造成PWM信号错误,若不关中断又会造成该操作受到影响。
能不能想想办法,解决这个问题?
答案是有办法的——通道输出比较功能。
GD32E23x的通用L2定时器通道道输出比较原理 来自GD32E23x_User_Manual_Rev1.5_CN.pdf
上面我们提到了“计数量”、“周期等分份数”和“第几份时间处电平翻转”的概念。在通道输出比较功能中,其对应关系如下:

  • 计数量 对应 定时器计数值
  • 周期等分份数 对应 定时器自动重载值
  • 第几份时间处电平翻转 对应 定时器输出比较值CHxCV

当定时器向上计数时,一旦计数值等于CHxCV的值,输出比较控制器就会产生准备信号OxCPRE的置位、清除或翻转(一般使用的PWM模式0是清除),并可以按照CHxPCHxE的设置而控制对应输出通道的电平。
是不是像我们刚刚所说的“第几份时间处电平翻转”?
实际上,PWM模式就是输出比较功能的一个特例用法,它在定时器每个计数周期(也就是溢出事件的周期)内只发生一次计数值与CHxCV的值比较成功(即相等),并在此处采取控制电平的操作。以较常用的PWM模式0为例,其在每个计数周期开始时将OxCPRE置为有效,并在比较成功时将其置为无效。
GD32E23x的通用L2定时器PWM时序图 来自GD32E23x_User_Manual_Rev1.5_CN.pdf

如何根据需求计算定时器的各项参数呢?

我们采取逆向计算的思维。
首先确定所需要的PWM信号频率$f_{PWM}$和PWM分辨率点数$N$(即一共有多少级分辨率)。于是得到以下数据:
$$
f_{PSC\_CLK}=f_{PWM}*N
$$

$$
自动重载值CAR=N-1
$$
然后根据TIMER_CK的时钟频率$f_{TIMER\_CK}$计算得到一个合适的PSC值:
$$
f_{TIMER\_CK}=f_{PSC\_CLK}*(PSC+1)
$$
需要注意的是,$PSC$的取值范围是有限制的,其必须处于$[0,2^{B}-1]$的区间内(B为该定时器的PSC寄存器宽度位数,也必须是一个整数。若计算得到的$PSC$为负数,说明该定时器的时钟频率过低,无法同时满足PWM信号频率与分辨率点数要求,需要提高时钟频率,或是降低频率/分辨率点数要求;若计算得到的$PSC$为小数,可就近约取一整数,并按照应用需求适当调节PWM信号频率$f_{PWM}$或PWM分辨率点数$N$。

示例代码

以下代码整理自“GD32E23x_Firmware_Library_V1.1.4/Examples/TIMER/TIMER2_pwmout”。
示例在PB1引脚上输出频率为1kHz、占空比为11.4%的PWM方波。

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
#include "gd32e23x.h"

// GPIO初始化
void gpio_config(void)
{
// 开GPIOB时钟
rcu_periph_clock_enable(RCU_GPIOB);

// 配置PB1的输出模式,并复用为TIMER13_CH0功能(AF0)
gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_1);
gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,GPIO_PIN_1);
gpio_af_set(GPIOB, GPIO_AF_0, GPIO_PIN_1);
}

// 定时器初始化
void timer_config(void)
{
// 建立配置用结构体
timer_oc_parameter_struct timer_ocinitpara;
timer_parameter_struct timer_initpara;

// 开启TIMER13时钟
rcu_periph_clock_enable(RCU_TIMER13);
// 清除TIMER13配置
timer_deinit(TIMER13);
// 初始化配置用结构体
timer_struct_para_init(&timer_initpara);
// 开始配置
// 以下配置基于SYSCLK = 72MHz情况。
timer_initpara.prescaler = 71; // 预分频72,得到PSC_CLK=1MHz
timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
timer_initpara.counterdirection = TIMER_COUNTER_UP;
timer_initpara.period = 999; // 自动重载计数器设为999,即每计数1000次产生一次向上溢出。1MHz计数1000次,即向上溢出事件发生的周期是1kHz,也即PWM频率
timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
timer_initpara.repetitioncounter = 0;
timer_init(TIMER13, &timer_initpara);

// 初始化通道输出比较配置用结构体
timer_channel_output_struct_para_init(&timer_ocinitpara);
// 开始配置
timer_ocinitpara.outputstate = TIMER_CCX_ENABLE; // 使能正相通道
timer_ocinitpara.outputnstate = TIMER_CCXN_DISABLE; // 禁用反相通道(TIMER13它也没有反相通道)
timer_ocinitpara.ocpolarity = TIMER_OC_POLARITY_HIGH; // 设置正相通道高电平有效
timer_ocinitpara.ocnpolarity = TIMER_OCN_POLARITY_HIGH;
timer_ocinitpara.ocidlestate = TIMER_OC_IDLE_STATE_LOW; // 设置正相通道低电平为空闲电平
timer_ocinitpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
timer_channel_output_config(TIMER13, TIMER_CH_0, &timer_ocinitpara);

// 设置通道0输出比较值为114,即占空比为114/(999+1)*100%=11.4%
timer_channel_output_pulse_value_config(TIMER13, TIMER_CH_0, 114);
// 设置通道0为PWM模式0
timer_channel_output_mode_config(TIMER13, TIMER_CH_0, TIMER_OC_MODE_PWM0);
// 禁用通道0的输出比较值影子寄存器,这样可以在运行中实时改变输出比较值,在当前计数周期就会生效
timer_channel_output_shadow_config(TIMER13, TIMER_CH_0, TIMER_OC_SHADOW_DISABLE);

// 启用定时器自动重载值影子寄存器
timer_auto_reload_shadow_enable(TIMER13);
}

int main(void)
{
// 初始化GPIO
gpio_config();
// 初始化定时器
timer_config();
// 启动TIMER13
timer_enable(TIMER13);

// 什么都不再做,主循环中死等
while(1){;}
return 0;
}

浅浅的撅一下这个TIMER(二):定时器发生PWM

https://wuxiproj.mzy7.cn/posts/9dd62baf.html

作者

MZY7

发布于

2023-09-15

更新于

2024-12-07

许可协议

CC BY-NC-SA 4.0

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×