前言


通过本次的学习,了解定时器的分类和配置,高级定时器,通用定时器等不同等级的定时器拥有的功能,学习定时器的输入捕获与输出比较功能(PWM)脉冲宽度调制,来控制电机等外设。


 定义

定时器的分类

在GD32这款单片机中不同的单片机拥有的功能数不同的,其中高级定时器的功能最多,通用定时其次,基本定时的功能最少。


高级定时器简介

高级定时器(TIMER0 和 TIMER7)是四通道定时器,支持输入捕获和输出比较。可以产生 PWM信号控制电机和电源管理。高级定时器含有一个 16 位无符号计数器。

高级定时器是可编程的,可以被用来计数,其外部事件可以驱动其他定时器。高级定时器包含了一个死区时间插入模块,非常适合电机控制。

定时器和定时器之间是相互独立,但是它们的计数器可以被同步在一起形成一个更大的定时器。

高级定时器的主要特征

 总通道数: 4;

 计数器宽度: 16位;

 时钟源可选:内部时钟,内部触发,外部输入,外部触发;

 多种计数模式:向上计数,向下计数和中央计数;

 正交编码器接口:被用来追踪运动和分辨旋转方向和位置;

 霍尔传感器接口:用来做三相电机控制;

 可编程的预分频器: 16位,运行时可以被改变;

 每个通道可配置:输入捕获模式,输出比较模式,可编程的PWM模式,单脉冲模式;

 可编程的死区时间;

 自动重装载功能;

 可编程的计数器重复功能;

 中止输入功能;

 中断输出和DMA请求:更新事件,触发事件,比较/捕获事件和中止事件;

 多个定时器的菊链使得一个定时器可以同时启动多个定时器;

 定时器的同步允许被选择的定时器在同一个时钟周期开始计数;

 定时器主/从模式控制器。


预分频的概念

预分频器可以将定时器的时钟(TIMER_CK)频率按 1 到 65536 之间的任意值分频,分频后的时钟 PSC_CLK 驱动计数器计数。分频系数受预分频寄存器 TIMERx_PSC 控制,这个控制寄存器带有缓冲器,它能够在运行时被改变。新的预分频器的参数在下一次更新事件到来时被采用。(简单来说就是GD32这款MCU的时钟频率默认是120MHz,经过一分频之后的值是60MHz,经过2分频后的时钟频率是30MHz),预分频器可以和重装计数器CAR和计数值CNT之间进行配合控制系统的运行周期和时间。


计数方式

高级定时器提供三种计数方式,第一中方式的向上计数,第二种方式是向下计数,第三种方式是中央对齐的方式进行计数,以下是本人对于三种计数方式的理解。

1:向上计数方式

CAR 表示的是重装计数器的值,当计数值重装计数器的值时系统产生一个中断,此时CNT计数值加 1同时计数重新设置为0开始下一轮的计数

2: 向下计数的方式

向下计数的方式和向上计数的方式原理上是一样的,一个的数值是向上加,一个的数值是向下减,当计数的值减到0的时候产生中断计数一次。

3:中央对齐的方式计数

计数的值首相向上开始计数,当计数到规定的数值的时候产生一个中断注意:在嵌入式系统中中断是一定存在的,无论是你想还是不想中断一定存在),达到中断触发条件中断被触发此时开始向另外的一个方向开始新的一轮计数。


定时器使用场景

定时器的使用场景主要有三种,第一种是基本的定时功能,当定时计数器达到某个设定的值之后去执行认为规定的系统相关操作,输出比较:主要是对PWM脉冲宽度调制的比较,输入捕获,主要是对PWM的波形进行捕获之后对一些外设或者时钟的周期进行相关的操作。

定时器的基本原理

定时器的本质:

定时器被本质上是一个电子计数器,当输入端输入三个周期的脉冲信号之后,CNT的计数值 + 3

假设一个脉冲的信号周期的时间是1s,那么三个时钟周期的时间就过去了三秒。

系统主频和定时时间的关系:

在GD32这款单片机系统的主频是120MHz的时钟主频那么一个周期的计数时间就是T = 1 / 120us,那么如果计数120次,对应过去的时间t就是1us。

定时器计时的上限:

CAR寄存器是用于设置CNT计时上限的,比如将Car设置成12000,CNT从0开始向上计数,当CNT=12000时、CNT将被系统清零.同时,系统可以产生一个定时中断。

这里将CNT的值设置为12000,系统时钟的主频是120MHz,这个时候一个计时的周期就是T = 1/ 120 ,然后定时的时间t = CNT * (1/120)us = 12000 * 1 / 120 = 100us

预分频器:

PSC是预分频器,可以灵活调整进入计数器的时钟频率,比如PSC设置为120后,那么实际进入CNT的信号周期也就变成了1us,然后再把CAR设置成100,此时定时中断的周期依然是100us,所以PSC可以从硬件层面调节定时周期的长短,非常灵活:

高级定时器的硬件结构:

这里定时器配置主要使用到的是CNT计数器,CAR重装载寄存器,预分频器PSC


定时器驱动配置

这里设置的是一个1ms中断一次的定时器延时函数,基于1ms中断一次,我们这里默认系统时钟的主频是120MHz的时钟主频,然后预分频器的值设置为120这个时候t定时一个的时间就是1us,那么我们设置重装载寄存器的值为1000,也就是计数值达到1000的时候溢出一次,产生一次定时器中断,那么1000 * 1us就是1ms,也就是我们这个定时器定时到1ms的时候会产生一次定时中断。

注:在定时器配置的同时,定时器预分频的值和CAR重装载寄存器的值要减去1,原因数和C语言的数组下标类似,数组的下表是从0开始的,我们配置定时器CAR的值如果我们希望是1000那么如果直接写1000的话实际的值是1001,所以需要将我们的数值减去1这样CAR的值才是1000。


代码编写

这里使用通用定时器4为例进行定时器驱动的编写,创建定时器初始化函数,这里的static静态函数表示这个函数的作用域只在本C语言文件中起作用,无法通过extern等关键字的声明被外部的C语言文件调用,函数的生命周期就是本C语言文件的生命周期。

定时器接口函数:

// 初始化定时器接口函数
void TIMER_Init(void)
{
	// 参数表示定时的周期单位是us
	TimerInit(1000);
}

 定时器初始化函数:

static void TimerInit(uint32_t periodUs)
{
	// 使能定时器时钟
	rcu_periph_clock_enable(RCU_TIMER4);
	// 复位定时器
	timer_deinit(TIMER4);
	// 初始化定时器结构体
	timer_parameter_struct timerInitPara;
	// 结构体变量初始化函数,定时器没有初始化赋值之前值是不确定的,通过这个初始化赋值
	timer_struct_para_init(&timerInitPara);
	// 设置重装载寄存器CAR的值
	timerInitPara.prescaler = 120 - 1;
	// 预分频器的值
	timerInitPara.period = periodUs - 1;
    // 使能定时器更新中断
	timer_interrupt_enable(TIMER4,TIMER_INT_UP);
	// 使能定时器中断和优先级,设置优先级为最高
	nvic_irq_enable(TIMER4_IRQn,0,0);
	// 使能定时器
	timer_enable(TIMER4);
}

定时器中断函数:

当定时的时间达到指定的时间时触发定时器中断,这个中断是自动触发的无需外部的调用,定时器中断函数的函数名在hal库gd32的启动文件中。

void TIMER4_IRQHandler(void)
{
		// 获取中断标志位
		if(timer_interrupt_flag_get(TIMER4,TIMER_INT_FLAG_UP) == SET)
		{
				// 清除中断标志位
				timer_interrupt_flag_clear(TIMER4,TIMER_INT_FLAG_UP);
				// LED1对应的gpio口反转
				ToggleLed(LED1);
		}
}

注:在中断服务函数中需要获取中断服务函数的中断标志位,如果中断函数中断标志位等于 “1”表示进入中断,这里RESET == 0, SET == 1,两者之间是一种等价的关系,因此 0 可以使用RESET进行代替,SET可以使用 1 来代替,注意获取中断标志位之后要手动清除中断标志位,如果:程序员不手动清除中断标志位的话会导致程序频繁的进入中断,系统会在这个位置卡死,然后执行LED灯的翻转工作,这里实际上是使用示波器查看LED灯对应引脚的PWM(脉冲宽度调制)波形是否是我们设定的中断时间。


完整代码

#include <stdint.h>
#include "gd32f30x.h"
#include "led_drv.h"


#if 0
// 初始化定时器
static void TimerInit(uint32_t periodUs)
{
	// 使能定时器时钟
	rcu_periph_clock_enable(RCU_TIMER0);
	// 复位定时器
	timer_deinit(TIMER0);
	// 初始化定时器结构体
	timer_parameter_struct timerInitPara;
	// 结构体变量初始化函数,定时器没有初始化赋值之前值是不确定的,通过这个初始化赋值
	timer_struct_para_init(&timerInitPara);
	// 设置重装载数值
	timerInitPara.prescaler = 120 - 1;
	timerInitPara.period = periodUs - 1;
    // 使能定时器更新中断
	timer_interrupt_enable(TIMER0,TIMER_INT_UP);
	// 使能定时器中断和优先级,设置优先级为最高
	nvic_irq_enable(TIMER0_UP_IRQn,0,0);
	// 使能定时器
	timer_enable(TIMER0);
}

void TIMER0_UP_IRQHandler(void)
{
		// 获取中断标志位
		if(timer_interrupt_flag_get(TIMER0,TIMER_INT_FLAG_UP) == SET)
		{
				// 清除中断标志位
				timer_interrupt_flag_clear(TIMER0,TIMER_INT_FLAG_UP);
				// LED1对应的gpio口反转
				ToggleLed(LED1);
		}
}
#else
static void TimerInit(uint32_t periodUs)
{
	// 使能定时器时钟
	rcu_periph_clock_enable(RCU_TIMER4);
	// 复位定时器
	timer_deinit(TIMER4);
	// 初始化定时器结构体
	timer_parameter_struct timerInitPara;
	// 结构体变量初始化函数,定时器没有初始化赋值之前值是不确定的,通过这个初始化赋值
	timer_struct_para_init(&timerInitPara);
	// 设置重装载寄存器CAR的值
	timerInitPara.prescaler = 120 - 1;
	// 预分频器的值
	timerInitPara.period = periodUs - 1;
    // 使能定时器更新中断
	timer_interrupt_enable(TIMER4,TIMER_INT_UP);
	// 使能定时器中断和优先级,设置优先级为最高
	nvic_irq_enable(TIMER4_IRQn,0,0);
	// 使能定时器
	timer_enable(TIMER4);
}

void TIMER4_IRQHandler(void)
{
		// 获取中断标志位
		if(timer_interrupt_flag_get(TIMER4,TIMER_INT_FLAG_UP) == SET)
		{
				// 清除中断标志位
				timer_interrupt_flag_clear(TIMER4,TIMER_INT_FLAG_UP);
				// LED1对应的gpio口反转
				ToggleLed(LED1);
		}
}

#endif

// 初始化定时器接口函数
void TIMER_Init(void)
{
	// 参数表示定时的周期单位是us
	TimerInit(1000);
}

头文件:

#ifndef _TIMING_DRV_H_
#define _TIMING_DRV_H_
#include <stdint.h>

void TIMER_Init(void);

#endif

 注:以上的程序是关于定时器配置的,为后续的PWM知识点做铺垫


PWM 定义

Def: PWM (脉冲宽度调制)通过调节PWM脉冲的宽度调节外设做工的功率,简单来说就是占空比,W脉宽(也就是在一个技术周期内高电平所占的比例)W所占的比例越大做工的功率就越大,占空比,也就是高电平的时间占用,W越大D越大,那么D越大输出的功率就越大。

不同等级定时器输入捕获和输出比较资源的概述


PWM波形的生成

相较于计时功能,除了CAR自动重装载寄存器,输出比较模式还要用到CHxCV寄存器。它是输入捕获和输出比较寄存器,是一个可以复用的寄存器。


CV 通常代表“Capture/Compare Value”,即捕获/比较值寄存器。这个寄存器用于存储一个预设的数值,该数值可以用来与定时器的计数器进行比较(比较模式),或者用于存储从定时器输入捕获到的时间点(捕获模式)。综上所述,TIMERx_CHxCV 可以理解为“定时器 x 的通道 x 捕获/比较值寄存器”。


注:输入捕获和输出比较寄存器主要是用于配置PWM中W也就是脉宽的值,或者是脉宽所占的比例而CAR寄存器则是用来配置 T 一个脉冲的时间周期。

PWM 波形的生成

计数值从0开始向上升的过程在达到输出捕获和输出比较寄存器的限定值之前一直处于高电平的状态,当超出输入捕获输出比较的限定值时转换为低电平的状态,一个高低电平的起伏变化时间就是一个周期。

因为:预分频器的值是120

T = (1/120)us 注意这里的120指的是时钟的主频,GD32的时钟主频是
120 默认值为120,具体可以参考GD32的时钟树结构

然后PSC = 120 这里的时钟主频是120 所以

一份周期T 就是 120 * 1/120 = 1us是T1经过预分频后得到的值

t = 500 * 1us = 500us

那么输出捕获和输出比较寄存器的值就是250
注:实际配置的时候需要将各个的值减去1,“因为:计时值从0开始的”

高级定时器的硬件结构

定时器配置


代码实现

以下代码的作用主要是生成一段PWM(脉冲宽度调制波形),

定时器初始化

void PWM_Init(void)
{
	// gpio 初始化
	rcu_periph_clock_enable(RCU_GPIOA);
	// 初始化gpio
	gpio_init
	(
		GPIOA,
		GPIO_MODE_AF_PP,
		GPIO_OSPEED_10MHZ,
		GPIO_PIN_8
	);
	// 使能定时器时钟
	rcu_periph_clock_enable(RCU_TIMER0);
	// 复位定时器
	timer_deinit(TIMER0);
	// 配置定时器
	timer_parameter_struct timerInitStruct;
	// 给定时器赋值默认的初始值
	timer_struct_para_init(&timerInitStruct);
	// 预分频器的值
	timerInitStruct.prescaler = 120 - 1;
	// 设置重装计数器的值
	timerInitStruct.period = 500 - 1;
    // 初始化定时
	timer_init(TIMER0,&timerInitStruct);
	
	// 配置定时器通道
	timer_oc_parameter_struct ocInitPara;
	// 结构体调用初始化接口函数
	timer_channel_output_struct_para_init(&ocInitPara);
	// 设置通道为输出的功能
	ocInitPara.outputstate = TIMER_CCX_ENABLE;
	// 设置通道的输出极性
	ocInitPara.ocpolarity = TIMER_OC_POLARITY_HIGH;
	timer_channel_output_config(TIMER0, TIMER_CH_0, &ocInitPara);
	
	/* 设置占空比设置chcv寄存器的数值;*/
	timer_channel_output_pulse_value_config(TIMER0, TIMER_CH_0, 250 - 1); 
	/* 设置通道输出PWM模式;*/
	timer_channel_output_mode_config(TIMER0, TIMER_CH_0, TIMER_OC_MODE_PWM0);
	// 使能互补通道保护寄存器
	timer_primary_output_config(TIMER0, ENABLE);
	/* 使能定时器;*/ 
	timer_enable(TIMER0);	
}

以下是对以上库函数做出的解释:

rcu 时钟初始化,这里主要是对MCU的CPIOA这组引脚进行时钟的初始化

	rcu_periph_clock_enable(RCU_GPIOA);

初始化gpio,参数包含GPIO的引脚这里的引脚是GPIOA这个引脚,使用的模式是复用推挽输出

这里的推挽指的是高低电平都是有效的,力量大,由片上外设提供,具体参考芯片手册,第三个参数是时钟的频率有50MHz的这里选用10MHz即可,最后一个参数是GPIO的引脚也可以是原理图对应的外设引脚,通过控制这个外设来查看PWM输出结果。

	// 初始化gpio
	gpio_init
	(
		GPIOA,
		GPIO_MODE_AF_PP,
		GPIO_OSPEED_10MHZ,
		GPIO_PIN_8
	);

 以下代码的含义是:(初始化定时器)

第一行代码:使能定时器0的时钟

第二行代码:复位定时0的时钟,这里复位的作用是方便后面对定时器的配置,复位的结果是设置为默认的值。

第三行代码:定时器结构体初始化,和STM32的结构体初始化类似

第四行代码: 这段代码的作用是在定时器没有初始化之前给未初始化的定时器赋值默认的值防止出现定时器没有定义的情况

第四行代码:赋值预分频器的值

第五行代码:赋值CAR重装载寄存器的值

最后一行代码表示的含义是定时器初始化,后面的一个参数将结构体初始化后的地址传递进去和STM32的标准库是类似的。

	// 使能定时器时钟
	rcu_periph_clock_enable(RCU_TIMER0);
	// 复位定时器
	timer_deinit(TIMER0);
	// 配置定时器
	timer_parameter_struct timerInitStruct;
	// 给定时器赋值默认的初始值
	timer_struct_para_init(&timerInitStruct);
	// 预分频器的值
	timerInitStruct.prescaler = 120 - 1;
	// 设置重装计数器的值
	timerInitStruct.period = 500 - 1;
    // 初始化定时
	timer_init(TIMER0,&timerInitStruct);

初始化定时器通道

以下代码是定时器通道配置,这里我们使用到的定时器是高级定时器定时器0,高级定时器拥有所有的通用定时器和普通定时器拥有的功能(以上值题外话,在回顾知识点),这段代码从源码出手

我们转到对应函数的定义中进行查看

 对应的参数:

timer_channel_output_config外设通道输出配置
timer_channel_output_struct_para_init将TIMER通道输出参数结构体中所有参数初始化为默认值

 结构体含义:

	// 配置定时器通道
	timer_oc_parameter_struct ocInitPara;
	// 结构体调用初始化接口函数
	timer_channel_output_struct_para_init(&ocInitPara);
	// 设置通道为输出的功能
	ocInitPara.outputstate = TIMER_CCX_ENABLE;
	// 设置通道的输出极性,通道输出的极性是高
	ocInitPara.ocpolarity = TIMER_OC_POLARITY_HIGH;
	timer_channel_output_config(TIMER0, TIMER_CH_0, &ocInitPara);

 以下是上面库函数对应参数的含义

  • ocpara: 输出比较参数结构体,用于配置定时器的输出比较功能。
    • outputstate: 输出状态,可以是:
      • TIMER_CCX_ENABLE: 启用输出比较功能
      • TIMER_CCX_DISABLE: 禁用输出比较功能。
    • outputnstate: 输出互补状态,可以是:
      • TIMER_CCXN_ENABLE: 启用输出比较的互补输出。
      • TIMER_CCXN_DISABLE: 禁用输出比较的互补输出。
    • ocpolarity: 输出极性,可以是:
      • TIMER_OC_POLARITY_HIGH: 非互补输出在有效电平为高时激活。
      • TIMER_OC_POLARITY_LOW: 非互补输出在有效电平为低时激活。
    • ocnpolarity: 输出互补极性,可以是:
      • TIMER_OCN_POLARITY_HIGH: 互补输出在有效电平为高时激活。
      • TIMER_OCN_POLARITY_LOW: 互补输出在有效电平为低时激活。
    • ocidlestate: 在非活动状态下的输出状态,可以是:
      • TIMER_OC_IDLE_STATE_LOW: 在非活动状态下输出为低电平。
      • TIMER_OC_IDLE_STATE_HIGH: 在非活动状态下输出为高电平。
    • ocnidlestate: 在非活动状态下的互补输出状态,可以是:
      • TIMER_OCN_IDLE_STATE_LOW: 在非活动状态下互补输出为低电平。
      • TIMER_OCN_IDLE_STATE_HIGH: 在非活动状态下互补输出为高电平。

这些参数通常用于配置定时器输出比较通道的行为,包括何时启用或禁用输出、输出的极性以及在非活动状态下的默认输出电平。这些配置对于实现PWM(脉冲宽度调制)等功能非常重要。

初始化输入捕获与输出比较寄存器的值

参数说明

  • channel: 选择要配置的定时器输出比较通道。

    • TIMER_CH_0: 定时器通道0 (TIMERx 其中 x=0..4,7..13)
    • TIMER_CH_1: 定时器通道1 (TIMERx 其中 x=0..4,7,8,11)
    • TIMER_CH_2: 定时器通道2 (TIMERx 其中 x=0..4,7)
    • TIMER_CH_3: 定时器通道3 (TIMERx 其中 x=0..4,7)
  • ocmode: 设置输出比较通道的工作模式。

    • TIMER_OC_MODE_TIMING: 定时模式。在这种模式下,当计数器达到比较寄存器中的值时,输出状态改变。
    • TIMER_OC_MODE_ACTIVE: 激活模式。在这种模式下,当计数器小于或等于比较寄存器中的值时,输出被激活。
    • TIMER_OC_MODE_INACTIVE: 非激活模式。与激活模式相反,当计数器大于比较寄存器中的值时,输出被激活。
    • TIMER_OC_MODE_TOGGLE: 切换模式。每当计数器达到比较寄存器中的值时,输出状态就会切换。
    • TIMER_OC_MODE_LOW: 强制低模式。输出总是保持低电平。
    • TIMER_OC_MODE_HIGH: 强制高模式。输出总是保持高电平。
    • TIMER_OC_MODE_PWM0: PWM0 模式。输出作为 PWM 信号使用,当计数器值小于比较寄存器值时输出为低电平。
    • TIMER_OC_MODE_PWM1: PWM1 模式。输出作为 PWM 信号使用,当计数器值小于比较寄存器值时输出为高电平。

总结

这些参数用于配置定时器的输出比较通道,以控制输出信号的行为。通过选择不同的通道和工作模式,您可以实现各种功能,如定时、PWM 生成等。这些配置对于许多应用来说都非常关键,比如电机控制、LED 亮度调节等。

	/* 设置占空比设置chcv寄存器的数值;*/
	timer_channel_output_pulse_value_config(TIMER0, TIMER_CH_0, 250 - 1); 
	/* 设置通道输出PWM模式;*/
	timer_channel_output_mode_config(TIMER0, TIMER_CH_0, TIMER_OC_MODE_PWM0);
	// 使能互补通道保护寄存器
	timer_primary_output_config(TIMER0, ENABLE);
	/* 使能定时器;*/ 
	timer_enable(TIMER0);	

以上代码的含义可以直接鼠标右键转到源码进行查看


PWM 输出比较测试函数

注:该函数的功能是实现LED灯的翻转对应到mcu上就是PA8号引脚LED1主要的功能是实现呼吸灯。

void PwmDrvTest(void)
{
	for(uint32_t i = 0; i < 500; i = i + 10)
	{
		// 改变脉宽占空比
		timer_channel_output_pulse_value_config(TIMER0, TIMER_CH_0, i); 
		DelayNms(50);
	}
	for(uint32_t i = 500; i > 0; i = i - 10)
	{
		// 改变脉宽占空比
		timer_channel_output_pulse_value_config(TIMER0, TIMER_CH_0, i); 
		DelayNms(50);
	}
}

PWM 输出比较完整代码

#include "gd32f30x.h"                   // Device header
#include <stdint.h>
#include "delay.h"

void PWM_Init(void)
{
	// gpio 初始化
	rcu_periph_clock_enable(RCU_GPIOA);
	// 初始化gpio
	gpio_init
	(
		GPIOA,
		GPIO_MODE_AF_PP,
		GPIO_OSPEED_10MHZ,
		GPIO_PIN_8
	);
	
	// 使能定时器时钟
	rcu_periph_clock_enable(RCU_TIMER0);
	// 复位定时器
	timer_deinit(TIMER0);
	// 配置定时器
	timer_parameter_struct timerInitStruct;
	// 给定时器赋值默认的初始值
	timer_struct_para_init(&timerInitStruct);
	// 预分频器的值
	timerInitStruct.prescaler = 120 - 1;
	// 设置重装计数器的值
	timerInitStruct.period = 500 - 1;
    // 初始化定时
	timer_init(TIMER0,&timerInitStruct);
	
	// 配置定时器通道
	timer_oc_parameter_struct ocInitPara;
	// 结构体调用初始化接口函数
	timer_channel_output_struct_para_init(&ocInitPara);
	// 设置通道为输出的功能
	ocInitPara.outputstate = TIMER_CCX_ENABLE;
	// 设置通道的输出极性
	ocInitPara.ocpolarity = TIMER_OC_POLARITY_HIGH;
	timer_channel_output_config(TIMER0, TIMER_CH_0, &ocInitPara);
	
	/* 设置占空比设置chcv寄存器的数值;*/
	timer_channel_output_pulse_value_config(TIMER0, TIMER_CH_0, 250 - 1); 
	/* 设置通道输出PWM模式;*/
	timer_channel_output_mode_config(TIMER0, TIMER_CH_0, TIMER_OC_MODE_PWM0);
	// 使能互补通道保护寄存器
	timer_primary_output_config(TIMER0, ENABLE);
	/* 使能定时器;*/ 
	timer_enable(TIMER0);	
}

void PwmDrvTest(void)
{
	for(uint32_t i = 0; i < 500; i = i + 10)
	{
		// 改变脉宽占空比
		timer_channel_output_pulse_value_config(TIMER0, TIMER_CH_0, i); 
		DelayNms(50);
	}
	for(uint32_t i = 500; i > 0; i = i - 10)
	{
		// 改变脉宽占空比
		timer_channel_output_pulse_value_config(TIMER0, TIMER_CH_0, i); 
		DelayNms(50);
	}
}

头文件代码:

#ifndef  _PWM_DRV_H_
#define  _PWM_DRV_H_
#include <stdint.h>

void PWM_Init(void);

void PwmDrvTest(void);
#endif


主函数调用


以上的代码是关于PWM输出比较的代码,通过配置PWM输出比较实现LED灯功能。


PWM 输入捕获

使用PWM输入捕获可以测量信号周期频率和脉冲的宽度,我们可以在第一个上升沿的位置开始计数并将第一次上升沿的数值保存在输入捕获寄存器中名字可以看做是T1,然后再到达第二个上升沿的位置开始计数名字称之为T2,使用T2减去T1在乘以计数的周期就能得到信号的周期。

注:除了以上测量输入捕获的方式之外还可以使用中断的方式测量输入捕获的周期,在中断中无需进行差值运算,且定时器一直处于运行状态,定时器不会停止,会一直的运行下去。


输入捕获定时器配置

频率之间的转换公式

频率之间的转换通常涉及到不同的单位。在国际单位制(SI)中,频率的基本单位是赫兹(Hz),表示每秒的周期数。以下是几种常见的频率单位及其转换关系:

  1. 赫兹 (Hz): 基本单位,表示每秒一次周期。
  2. 千赫兹 (kHz): 1 kHz = 1,000 Hz
  3. 兆赫兹 (MHz): 1 MHz = 1,000,000 Hz = 1,000 kHz
  4. 吉赫兹 (GHz): 1 GHz = 1,000,000,000 Hz = 1,000 MHz

初始化GPIO与定时器时钟

void CaptureDrvInit(void)
{
	GpioInit();
	TimerInit();
}

GPIO初始化

static void GpioInit(void)
{
	rcu_periph_clock_enable(RCU_GPIOA);
	gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_10MHZ, GPIO_PIN_0);
}

定时器初始化

static void TimerInit(void)
{
	/* 使能定时器时钟;*/
	rcu_periph_clock_enable(RCU_TIMER1);
	/* 复位定时器;*/
	timer_deinit(TIMER1);

	timer_parameter_struct timerInitPara;
	timer_struct_para_init(&timerInitPara);
	/* 设置预分频器值;*/
	timerInitPara.prescaler = 120 - 1;     // 输入给计数器的时钟频率为1Mhz,周期1us
	/* 设置自动重装载值;*/ 
	timerInitPara.period = 65535;
	timer_init(TIMER1, &timerInitPara);

	timer_ic_parameter_struct icInitPara;
	timer_channel_input_struct_para_init(&icInitPara);
	/* 设置上升沿/下降沿捕获;*/
	icInitPara.icpolarity = TIMER_IC_POLARITY_RISING;
	/* 设置输入通道;*/
	icInitPara.icselection = TIMER_IC_SELECTION_DIRECTTI;
	timer_input_capture_config(TIMER1, TIMER_CH_0, &icInitPara);
	
	/* 使能定时器的捕获中断;*/
	timer_interrupt_flag_clear(TIMER1, TIMER_INT_FLAG_CH0);
	timer_interrupt_enable(TIMER1, TIMER_INT_CH0);
	/* 使能定时器中断和优先级;*/
	nvic_irq_enable(TIMER1_IRQn, 0, 0);

	/* 使能定时器;*/ 
	timer_enable(TIMER1);
}

中断初始化

static uint32_t g_icValue;

void TIMER1_IRQHandler(void)
{
	if (timer_interrupt_flag_get(TIMER1, TIMER_INT_FLAG_CH0) == SET)
	{
		timer_interrupt_flag_clear(TIMER1, TIMER_INT_FLAG_CH0);
		// 读取chxcv里面的数值,输入捕获寄存器里面的数值,+1 的原因是计数值是从0开始读取到999 那么要 + 1
		g_icValue = timer_channel_capture_value_register_read(TIMER1, TIMER_CH_0) + 1;
		// 清零CNT计数寄存器里面的数值
		timer_counter_value_config(TIMER1, 0);
	}
}

输入捕获周期测试函数

// 打印输出捕获的周期
void CaptureDrvTest(void)
{
	// 获取的计数值表示的是几个微秒
	printf("period is %d us.\n", g_icValue);
	DelayNms(500);
}

PWM 输入捕获完整代码

#include <stdint.h>
#include <stdio.h>
#include "gd32f30x.h"
#include "delay.h"

static void GpioInit(void)
{
	rcu_periph_clock_enable(RCU_GPIOA);
	gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_10MHZ, GPIO_PIN_0);
}

static void TimerInit(void)
{
	/* 使能定时器时钟;*/
	rcu_periph_clock_enable(RCU_TIMER1);
	/* 复位定时器;*/
	timer_deinit(TIMER1);

	timer_parameter_struct timerInitPara;
	timer_struct_para_init(&timerInitPara);
	/* 设置预分频器值;*/
	timerInitPara.prescaler = 120 - 1;     // 输入给计数器的时钟频率为1Mhz,周期1us
	/* 设置自动重装载值;*/ 
	timerInitPara.period = 65535;
	timer_init(TIMER1, &timerInitPara);

	timer_ic_parameter_struct icInitPara;
	timer_channel_input_struct_para_init(&icInitPara);
	/* 设置上升沿/下降沿捕获;*/
	icInitPara.icpolarity = TIMER_IC_POLARITY_RISING;
	/* 设置输入通道;*/
	icInitPara.icselection = TIMER_IC_SELECTION_DIRECTTI;
	timer_input_capture_config(TIMER1, TIMER_CH_0, &icInitPara);
	
	/* 使能定时器的捕获中断;*/
	timer_interrupt_flag_clear(TIMER1, TIMER_INT_FLAG_CH0);
	timer_interrupt_enable(TIMER1, TIMER_INT_CH0);
	/* 使能定时器中断和优先级;*/
	nvic_irq_enable(TIMER1_IRQn, 0, 0);

	/* 使能定时器;*/ 
	timer_enable(TIMER1);
}

static uint32_t g_icValue;

void TIMER1_IRQHandler(void)
{
	if (timer_interrupt_flag_get(TIMER1, TIMER_INT_FLAG_CH0) == SET)
	{
		timer_interrupt_flag_clear(TIMER1, TIMER_INT_FLAG_CH0);
		// 读取chxcv里面的数值,输入捕获寄存器里面的数值,+1 的原因是计数值是从0开始读取到999 那么要 + 1
		g_icValue = timer_channel_capture_value_register_read(TIMER1, TIMER_CH_0) + 1;
		// 清零CNT计数寄存器里面的数值
		timer_counter_value_config(TIMER1, 0);
	}
}

void CaptureDrvInit(void)
{
	GpioInit();
	TimerInit();
}

// 打印输出捕获的周期
void CaptureDrvTest(void)
{
	// 获取的计数值表示的是几个微秒
	printf("period is %d us.\n", g_icValue);
	DelayNms(500);
}

头文件代码

#ifndef _CAPTURE_DRV_H_
#define _CAPTURE_DRV_H_

void CaptureDrvInit(void);
void CaptureDrvTest(void);
#endif

结语

通过本次的学习掌握定时器的基本配置方法,了解预分频器以及定时周期,定时时间的计算,PWM输出比较的实现,基于PWM输出比较的配置,本次文章的编写参考GD32数据手册,以及郭天祥ARM32课程进行编写,仅供学习参考

......

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐