【嵌入式 – GD32开发实战指南(ARM版)】第1部分 基础篇 – 第8章 PWM输出

开发环境:
MDK:Keil 5.30
开发板:GD32F207I-EVAL
MCU:GD32F207IK

8.1 PWM输出的工作原理

脉冲宽度调制(PWM),是英文“Pulse Width Modulation” 的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。简单一点,就是对脉冲宽度的控制。

GD32 的定时器除了 TIMER5 和 6(基本定时器)。其他的定时器都可以用来产生 PWM 输出。

每个定时器有四个通道,每一个通道都有一个捕获比较寄存器,,将寄存器值和计数器值比较,通过比较结果输出高低电平,便可以实现脉冲宽度调制模式(PWM信号)。

在上一节,讲解了定时器的相关寄存器即基本原理,本节将不再赘述。下面谈谈如何使用定时器的寄存器进行PWM输出的。若配置脉冲计数器TIMERx_CNT为向上计数,而重载寄存器TIMERx_CAR配置为N,即TIMERx_CNT的当前计数值数值X在CK_TIMER时钟源的驱动下不断累加,当TIMERx_CNT的数值X大于N时,会重置TIMERx_CNT数值为0重新计数。而在TIMERx_CNT计数的同时,TIMERx_CNT的计数值X会与比较寄存器TIMERx_CHxCV预先存储了的数值A进行比较,当脉冲计数器TIMERx_CNT的数值X小于比较寄存器TIMERx_CHxCV的值A时,输出高电平(或低电平),相反地,当脉冲计数器的数值X大于或等于比较寄存器的值A时,输出低电平(或高电平)。如此循环,得到的输出脉冲周期就为重载寄存器TIMERx_CAR存储的数值(N+1)乘以触发脉冲的时钟周期,其脉冲宽度则为比较寄存器TIMERx_CHxCV的值A乘以触发脉冲的时钟周期,即输出PWM的占空比为A/(N+1)。

估计很多初学者看了上面的一段话都很蒙圈,没关系,下面以向上计数模式为例进行讲解。

OI8CgH.png

在PWM输出模式下,除了CNT(计数器当前值)、CAR(自动重装载值)之外,还多了一个值CHxCV(捕获/比较寄存器值)。当CNT小于CHxCV时,CHxCV通道输出低电平;当CNT等于或大于CHxCV时,CHxCV通道输出高电平。因此得到PWM的一个周期如下:

1.定时器从0开始向上计数;
2.当0-t1段,定时器计数器CNT值小于CHxCV值,输出低电平;
3.t1-t2段,定时器计数器CNT值大于CHxCV值,输出高电平;
4.当CNT值达到CAR时,定时器溢出,重新向上计数…循环此过程。

至此一个PWM周期完成。针对PWM重点关注两个寄存器,CAR寄存器确定PWM频率,CHxCV寄存器确定占空比。

上文提到了PWM的输出模式,下面讲解PWM的工作模式:

  • PWM模式1(向上计数) :计数器从0计数加到自动重装载值(CAR),然后重新从0开始计数,并且产生一个计数器溢出事。
  • PWM模式2(向下计数) :计数器从自动重装载值(CAR)减到0,然后重新从重装载值(CAR)开始递减,并且产生一个计数器溢出事件。

这里我们仅利用 TIMER2产生多路 PWM 输出。如果要产生多路输出,大家可以根据我们的代码稍作修改即可。具体不同定时器对应引脚在对应芯片数据手册的引脚说明(pin description) 中查看。

[ps] 本文以F1系列为例进行讲解,GD不同系列其定时器个数不同

8.2 PWM输出的寄存器描述

同样,我们首先通过对 PWM 相关的寄存器进行讲解,大家了解了定时器 TIMER2的 PWM原理之后,我们再讲解怎么使用库函数产生 PWM 输出。

要使 GD32 的通用定时器 TIMERx 产生 PWM 输出,除了上一章介绍的寄存器外,我们还会用到 3 个寄存器,来控制 PWM 的。这三个寄存器分别是:通道控制寄存器(TIMERx_CHCTL0/1)、通道控制寄存器(TIMERx_CHCTL2)、捕获/比较寄存器(TIMERx_CHxCV)。接下来我们简单介绍一下这三个寄存器。

首先是通道控制寄存器(TIMERx_CHCTL0/1),该寄存器总共有2个,TIMERx_CHCTL0和TIMERx_CHCTL1。TIMERx_CHCTL0控制 CH1 和 2,而TIMERx_CHCTL1 控制 CH3 和 4。该寄存器的各位描述如下图。

OI8u8g.md.png

OI8lKs.md.png

该寄存器的有些位在不同模式下,功能不一样,所以在上图中,我们把寄存器分了2层,上面一层对应输出而下面的则对应输入。关于该寄存器的详细说明,请参考《GD32F20x User Manual》。这里我们需要说明的是模式设置位CH0COMCTL,此部分由 3 位组成。总共可以配置成 7 种模式,我们使用的是 PWM 模式,所以这 3 位必须设置为 110/111。这两种PWM 模式的区别就是输出电平的极性相反。
接下来,我们介绍通道控制寄存器(TIMERx_CHCTL2),该寄存器控制着各个输入输出通道的开关。该寄存器的各位描述如下图。

OI81rn.md.png

OI8Dq1.md.png

该寄存器比较简单, 我们这里只用到了CHxEN位,该位是输入/捕获 2 输出使能位,要想PWM 从 IO 口输出,这个位必须设置为 1,所以我们需要设置该位为 1。

最后,我们介绍一下捕获/比较寄存器(TIMERx_CHxCV),该寄存器总共有 4 个,对应 4 个输通道 CH0~3。因为这 4 个寄存器都差不多,我们仅以TIMERx_CH0CV为例介绍,该寄存器的各位描述如下图。

OI8ya6.md.png

在输出模式下,该寄存器的值与 CNT 的值比较,根据比较结果产生相应动作。利用这点,我们通过修改这个寄存器的值,就可以控制 PWM 的输出脉宽了。

假如我们要利用 TIMER2的 CH1 输出 PWM 来控制 DS0 的亮度,但是 TIMER2_CH1默认是接在 PA7上面的,这就可以通过重映射功能,把 TIMER2_CH1映射到 PB5 上。

GD32 的重映射控制是由复用重映射和调试 IO 配置寄存器控制的,该寄存器的各位描述如上图。我们这里用到的是 TIMER2的重映射,从上图可以看出,TIMER2_REMAP 是由[11:10]这 2 个位控制的。TIMER2_REMAP[1:0]重映射控制表如下表。

OI3HgJ.md.png

默认条件下,TIMER2_REMAP[1:0]为 00,是没有重映射的,所以 TIMER2_CH0~TIMER2_CH3 分别是接在 PA6、 PA7、 PB0 和 PB1 上的,而我们想让 TIMER2_CH1 映射到 PB5 上, 则需要设置TIMER2_REMAP[1:0]=10,即部分重映射,这里需要注意,此时TIMER2_CH0 也被映射到 PB4 上了。

TIMER定时器的四路通道CHx_O输出PWM。

OI86IK.md.png

8.3 PWM输出实现

8.3.1 PWM代码分析

本章要实现通过TIMER2实现四路方波的输出,以TIMER2_CH0 输出 PWM 为例进行讲解。下面我们介绍通过库函数来配置该功能的步骤。

首先要提到的是,PWM 相关的函数设置在库函数文件gd32f20x_timer.h和gd32f20x_timer.c文件中。

1) 开启 TIMER2 时钟以及GPIO的时钟,配置 PA6为复用输出。

要使用 TIMER2,我们必须先开启 TIMER2的时钟,这点相信大家看了这么多代码,应该明白了。库函数使能 TIMER2及PA6时钟的方法是:

/* enable the GPIOA clock */
rcu_periph_clock_enable(RCU_GPIOA);
//Enable TIMER2 clock
rcu_periph_clock_enable(RCU_TIMER2);

库函数设置 AFIO 时钟的方法是:

/* 开启复用功能时钟 */
rcu_periph_clock_enable(RCU_AF); 

2) 初始化 TIMER2,设置 TIMER2的 CAR 和 PSC。
在开启了 TIMER2 的时钟之后,我们要设置 CAR 和 PSC 两个寄存器的值来控制输出 PWM 的周期。这在库函数是通过timer_init()函数实现的,在上一节定时器中断章节我们已经有讲解,这里就不详细讲解,调用的格式为:

/* TIMER2 configuration */
timer_init_struct.prescaler         = 0;
timer_init_struct.alignedmode       = TIMER_COUNTER_EDGE;
timer_init_struct.counterdirection  = TIMER_COUNTER_UP;
timer_init_struct.period            = 999;
timer_init_struct.clockdivision     = TIMER_CKDIV_DIV1;
timer_init_struct.repetitioncounter = 0;
timer_init(TIMER2, &timer_init_struct); 

3) 设置 TIMER2_CH0的 PWM 模式,使能 TIMER2的 CH0输出。
接下来,我们要设置 TIMER2_CH0为 PWM 模式(默认是冻结的),在库函数中,PWM通道设置是通过函数 timer_channel_output_config()来设置的,我们直接来看看结构体 timer_oc_parameter_struct的定义:

/* channel output parameter structure definitions */
typedef struct {
    uint16_t outputstate;                       /*!< channel output state */
    uint16_t outputnstate;                      /*!< channel complementary output state */
    uint16_t ocpolarity;                        /*!< channel output polarity */
    uint16_t ocnpolarity;                       /*!< channel complementary output polarity */
    uint16_t ocidlestate;                       /*!< idle state of channel output */
    uint16_t ocnidlestate;                      /*!< idle state of channel complementary output */
} timer_oc_parameter_struct;

该结构体主要配置通道的状态,极性等,还需要设置占空比等配置,不同的通道需要分别设置。

/* PWM Mode configuration: Channel0 */
timer_channel_output_config(TIMER2, TIMER_CH_0, &timer_oc_init_struct);

/* 通道2占空比设置 */
timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_0, CH0CV_Val);
/* PWM模式0 */
timer_channel_output_mode_config(TIMER2,TIMER_CH_0,TIMER_OC_MODE_PWM0);
/* 不使用输出比较影子寄存器 */
timer_channel_output_shadow_config(TIMER2,TIMER_CH_0,TIMER_OC_SHADOW_DISABLE);

4) 使能 TIM3。
我们需要使能 TIMER2。使能 TIMER2的方法前面已经讲解过:

timer_enable(TIMER2);

最后看下主函数代码:

/*
    brief      main function
    param[in]  none
    param[out] none
    retval     none
*/
int main(void)
{
    //systick init
    sysTick_init();

    /* configure the TIMER peripheral */
    timer2_init();

    while(1) 
    {

    }
}

是不是很简单,这里进行了PWM初始化,最核心的就是timer2_init()函数,其代码如下:

/*
    brief      configure the TIMER peripheral
    param[in]  none
    param[out] none
    retval     none
  */
void timer2_init(void)
{
    /* TIMER1 configuration: generate PWM signals with different duty cycles:
       TIMER1CLK = SystemCoreClock / 120 = 1MHz */
    /* 定义一个定时器初始化结构体 */
    timer_parameter_struct timer_init_struct;
    /* 定义一个定时器输出比较参数结构体*/
    timer_oc_parameter_struct timer_oc_init_struct;

    /* PWM信号电平跳变值 */
    uint16_t CH0CV_Val = 500;
    uint16_t CH1CV_Val = 375;
    uint16_t CH2CV_Val = 250;
    uint16_t CH3CV_Val = 125;

    /* ----------------------------------------------------------------------- 
    TIMER2 Channel0 duty cycle = (TIMER2_CH0CV/ TIMER2_CAR+1)* 100% = 50%
    TIMER2 Channel1 duty cycle = (TIMER2_CH1CV/ TIMER2_CAR+1)* 100% = 37.5%
    TIMER2 Channel2 duty cycle = (TIMER2_CH2CV/ TIMER2_CAR+1)* 100% = 25%
    TIMER2 Channel3 duty cycle = (TIMER2_CH3CV/ TIMER2_CAR+1)* 100% = 12.5%
    ----------------------------------------------------------------------- */
    // gpio init
    timer_gpio_init();

    //Enable TIMER2 clock
    rcu_periph_clock_enable(RCU_TIMER2);
    /* 开启复用功能时钟 */
    rcu_periph_clock_enable(RCU_AF);

    timer_deinit(TIMER2);

    /* TIMER2 configuration */
    timer_init_struct.prescaler         = 0;
    timer_init_struct.alignedmode       = TIMER_COUNTER_EDGE;
    timer_init_struct.counterdirection  = TIMER_COUNTER_UP;
    timer_init_struct.period            = 999;
    timer_init_struct.clockdivision     = TIMER_CKDIV_DIV1;
    timer_init_struct.repetitioncounter = 0;
    timer_init(TIMER2, &timer_init_struct);

    /* PWM初始化 */
    timer_oc_init_struct.outputstate  = TIMER_CCX_ENABLE;    /* 通道使能 */
    timer_oc_init_struct.outputnstate = TIMER_CCXN_DISABLE;   /* 通道互补输出使能(定时器2无效) */
    timer_oc_init_struct.ocpolarity   = TIMER_OC_POLARITY_HIGH; /* 通道极性 */
    timer_oc_init_struct.ocnpolarity  = TIMER_OCN_POLARITY_HIGH;/* 互补通道极性(定时器2无效)*/
    timer_oc_init_struct.ocidlestate  = TIMER_OC_IDLE_STATE_LOW;/* 通道空闲状态输出(定时器2无效)*/
    timer_oc_init_struct.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;/*互补通道空闲状态输出(定时器2无效) */

    /* PWM Mode configuration: Channel0 */
    timer_channel_output_config(TIMER2, TIMER_CH_0, &timer_oc_init_struct);

    /* 通道2占空比设置 */
    timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_0, CH0CV_Val);
    /* PWM模式0 */
    timer_channel_output_mode_config(TIMER2,TIMER_CH_0,TIMER_OC_MODE_PWM0);
    /* 不使用输出比较影子寄存器 */
    timer_channel_output_shadow_config(TIMER2,TIMER_CH_0,TIMER_OC_SHADOW_DISABLE);

    /* PWM Mode configuration: Channel1 */
    timer_channel_output_config(TIMER2, TIMER_CH_1, &timer_oc_init_struct);

    /* 通道2占空比设置 */
    timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_1, CH1CV_Val);
    /* PWM模式0 */
    timer_channel_output_mode_config(TIMER2,TIMER_CH_1,TIMER_OC_MODE_PWM0);
    /* 不使用输出比较影子寄存器 */
    timer_channel_output_shadow_config(TIMER2,TIMER_CH_1,TIMER_OC_SHADOW_DISABLE);

    /* PWM Mode configuration: Channel2 */
    timer_channel_output_config(TIMER2, TIMER_CH_2, &timer_oc_init_struct);

    /* 通道2占空比设置 */
    timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_2, CH2CV_Val);
    /* PWM模式0 */
    timer_channel_output_mode_config(TIMER2,TIMER_CH_2,TIMER_OC_MODE_PWM0);
    /* 不使用输出比较影子寄存器 */
    timer_channel_output_shadow_config(TIMER2,TIMER_CH_2,TIMER_OC_SHADOW_DISABLE);

    /* PWM Mode configuration: Channel3 */
    timer_channel_output_config(TIMER2, TIMER_CH_3, &timer_oc_init_struct);

    /* 通道2占空比设置 */
    timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_3, CH3CV_Val);
    /* PWM模式0 */
    timer_channel_output_mode_config(TIMER2,TIMER_CH_3,TIMER_OC_MODE_PWM0);
    /* 不使用输出比较影子寄存器 */
    timer_channel_output_shadow_config(TIMER2,TIMER_CH_3,TIMER_OC_SHADOW_DISABLE);

    /* 自动重装载影子比较器使能 */
    timer_auto_reload_shadow_enable(TIMER2);

    /* TIMER2 enable */
    timer_enable(TIMER2);
}

8.3.2 PWM周期、占空比分析

根据前面的参数配置,我们可以算出PWM的输出周期:
PWM=1/(Tclk/(psc+1)) * (arr+1)

这里我们 arr=999 psc=0 Tclk=120Mhz ,
PWM=1/(120Mhz/(1)) * (999+1)=1/120ms

因此PWM的输出频率120KHz,周期是8.3us。

PWM的占空比为:
Dutycycle=(CHxCV/ CAR+1) * 100%

PWM自动重装值为999,四个通道的跳变值分别为500,375,250,125。因此,TIM3的四个通道的占空比分别为50%,37.5%,25%,12.5%。

8.4 PWM输出的实验现象

在前面我们输出了TIM3 的通道 1(PA6)、2(PA7)、3(PB0)、4(PB1)不同占空比的 PWM 信号。接下来就看看PWM的输出,PWM 信号可以通过示波器看到,下面笔者就是用逻辑分析仪查看波形。
首先笔者使用的逻辑分析仪是Kingst LA5016,当然啦,其他的也可以,关于逻辑分析仪的相关使用笔者这里就不介绍了,可以查看官方资料。

首先将通道 1(PA6)、2(PA7)、3(PB0)、4(PB1)分别接到逻辑分析仪的CH0 – CH3,然后下载程序到板子中,打开Kingst VIS,然后进行采样。

OI84sA.md.png

我们就可以看到不同通道的实际周期,占空比等信息。

OI8TdP.md.png

从上图可以看到,实际测量的频率和占空比和理论是相符的。


欢迎访问我的网站

BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书
BruceOu的知乎


资源获取方式

1.关注公众号[嵌入式实验楼]
2.在公众号回复关键词[GD32开发实战指南]获取资料提取码

Related posts

2 Thoughts to “【嵌入式 – GD32开发实战指南(ARM版)】第1部分 基础篇 – 第8章 PWM输出”

  1. yap

    为什么要开启 了gpio、timer的时钟, 还要开启复用时钟?

Leave a Comment