定时器及其简单使用
文章目录
- 定时器及其简单使用
- ARM32单片机定时器
- 定时器原理
- 系统主频与定时时间的关系
- 定时器计时上限
- 预分频器
- 基于定时器应用配置
- Timer.c
- PSC、CAR减1原因
- PWM
- 什么是PWM?
- 如何生成PWM?
- PWM输出使用
- PWM模式有效电平
- 定时器可以输出几个通道的PWM?
- 输入捕获
- 输入捕获应用
- 红外NEC
- 红外NEC通信协议
- 红外遥控软件实现
ARM32单片机定时器
根据功能和用途的不同,GD32F303芯片的定时器可以分为以下几类:
- 内核定时器:用于提供系统tick计数,是系统延时和任务调度的基础。
- SysTick定时器:SysTick定时器是Cortex-M内核自带的定时器,用于提供系统tick计数,是系统延时和任务调度的基础。
- 基本定时器:功能相对简单,主要用于产生PWM波形、捕获外部信号等。
- 常规定时器:功能与基本定时器类似,但具有更丰富的配置选项,如支持多路PWM输出、输入捕获等。
- 通用定时器:功能更加强大,支持多种工作模式,如PWM输出、输入捕获、脉冲计数、DMA传输等。
- 高级定时器:具有更强的定时精度和控制能力,支持多路PWM输出、输入捕获、脉冲计数、DMA传输等。
- 独立看门狗:用于检测系统是否正常运行,如果系统发生故障,看门狗会复位芯片。
- 专用定时器:用于特定功能,如I2C、SPI、USART等通信接口的定时。
- 窗口看门狗:与独立看门狗类似,但具有更灵活的配置选项,如可以设置看门狗窗口范围等。
- RTC实时时钟:用于提供精确的时间信息,即使芯片掉电后也能保持时间准确。
定时器应用场景
GD32F303定时器资源概述
定时器原理
定时器本质上是一个电子计数器,当输入端输入3个周期的数字脉冲信号后,计数值将增加3,如果我们知道这个信号的周期T=1s,那么也就表示时间t已经过去了3s。
系统主频与定时时间的关系
如果输入的信号就是系统主频120Mhz信号,周期T=(1/120)us,从0开始计数,假如记录到120时,对应过去了时间t为:
定时器计时上限
CAR寄存器是用于设置(NT计时上限的,比如将(AR设置成12000,(NT从0开始向上计数,当(NT=12000时,(NT将被系统清零。同时,系统可以产生一个定时中断:
预分频器
PSC是预分频器,可以灵活调整进入计数器的时钟频率,比如PS(设置为120后,那么实际进入CNT的信号周期也变成了1us,然后再把CAR设置成100,此时定时中断的周期依然是100us,所以PS(可以从硬件层面,节定时周期的长短,非常灵活·
高级定时器硬件结构
- 时钟源
- CK_INT:内部时钟,频率为系统时钟的2倍
- ETR:外部触发输入,可以是外部脉冲信号或定时器1的输出
- ITRx:内部触发输入,可以是其他定时器的输出
- 预分频器
- PSC:预分频系数,可以为1-65536
- 计数器
- CNT:16位计数器,可以向上计数或向下计数
- 自动重载寄存器
- ARR:自动重载寄存器,当计数器值等于ARR时,计数器会自动重载
- 比较寄存器
- CCR1-CCR4:4个比较寄存器,当计数器值等于CCR1-CCR4时,会产生一个比较事件
- 输出控制寄存器
- CCER:输出控制寄存器,用于控制输出模式、输出极性等
- DMA请求寄存器
- DMAR:DMA请求寄存器,用于请求DMA传输
- 中断寄存器
- SR:状态寄存器,用于指示中断标志
- IER:中断使能寄存器,用于使能中断
- CCR1-CCR4:比较寄存器,当计数器值等于CCR1-CCR4时,会产生一个中断
工作模式
高级定时器支持多种工作模式,如: - PWM模式:产生PWM波形
- 输入捕获模式:捕获外部信号
- 脉冲计数模式:计数脉冲信号
- DMA传输模式:通过DMA传输数据
配置步骤
要配置高级定时器,需要按照以下步骤进行:
1.选择定时器
2.选择工作模式
3.配置时钟源
4.配置预分频系数
5.配置计数器
6.配置比较寄存器
7.配置输出控制寄存器
8.配置中断
基于定时器应用配置
设计一个1ms的定时中断,基于定时器0,如何配置?
Timer.c
#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; // 输入给计数器的时钟频率为1Mhz,周期1us
/* 设置自动重装载值;*/
timerInitPara.period = periodUs - 1;
timer_init(TIMER0, &timerInitPara);
/* 使能定时器的计数更新中断;*/
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);
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);
/* 设置预分频器值;*/
timerInitPara.prescaler = 120 - 1; // 输入给计数器的时钟频率为1Mhz,周期1us
/* 设置自动重装载值;*/
timerInitPara.period = periodUs - 1;
timer_init(TIMER4, &timerInitPara);
/* 使能定时器的计数更新中断;*/
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);
ToggleLed(LED1);
}
}
#endif
1.TimerInit 函数:
- 首先,通过 rcu_periph_clock_enable(RCU_TIMER4) 使能了定时器4的时钟,确保定时器可以正常工作。
- 然后,通过 timer_deinit(TIMER4) 将定时器4进行了复位,以确保其处于初始状态。
- 之后,定义了一个结构体 timer_parameter_struct timerInitPara 用于配置定时器的参数,并通过 timer_struct_para_init(&timerInitPara) 将该结构体初始化为默认值。
- 接着,设置了预分频器的值为 120 - 1,这表示定时器的时钟频率为输入给计数器的时钟频率为1MHz,即周期为1微秒。
- 设置了自动重载值为 periodUs - 1,其中 periodUs 是作为参数传入的定时器的周期值(单位为微秒)。
- 最后,通过 timer_init(TIMER4, &timerInitPara) 对定时器进行初始化配置,使其按照指定的参数进行工作。
- 使能了定时器的计数更新中断和相应的中断向量 TIMER4_IRQn,并设置了中断的优先级。
- 最后,通过 timer_enable(TIMER4) 使能了定时器,使其开始计数。
2.TIMER4_IRQHandler函数: - 这是定时器4的中断处理函数,当定时器4的计数更新中断触发时,该函数将被调用。
- 在函数内部,首先通过 timer_interrupt_flag_get(TIMER4, TIMER_INT_FLAG_UP) 判断是否是计数更新中断触发,如果是则执行下一步。
- 使用 timer_interrupt_flag_clear(TIMER4, TIMER_INT_FLAG_UP) 清除定时器4的计数更新中断标志位。
- 调用 ToggleLed(LED1) 函数,这个函数可能是用来控制 LED1 灯的状态,可能是将其切换为开或关状态。
PSC、CAR减1原因
在实际编程时,并不是把PSC设置成120,CAR设置成1000,而是把PSC设置成(120-1),得到1us的时钟周期,再把CAR设置成(1000-1)才行,为什么都需要-1呢?
先看CAR的设置,因为计数器需要实现0-1000循环计数,而当计数来到999后,再来一个机器周期CNT会变成0,也就是说第1000个周期实际是999变0,而不是999变1000,所以当我们需要1000个计数周期循环计数时,需要设置到999。
计数模式
PWM
什么是PWM?
PWM, Pulse width modulation,脉冲宽度调制,通过调节PWM的脉冲宽度进而调节功率。PWM的周期T,脉宽W,占空比D=W/T,假设W是负载的有效工作时间,在一个工作周期T内, W越长也就相当于对负载输出的功率越大,也就是说D越大,输出功率越大。
定时器输入捕获和输出比较资源概述
如何生成PWM?
相较于计时功能,除了CAR自动重装载寄存器,输出比较模式还要用到CHxCV寄存器。它是输入捕获和输出比较寄存器,是一个可以复用的寄存器。
PWM输出使用
设计输出一个周期500us(频率2Khz)占空比50%的PWM,基于定时器0通道0,如何配置?
static void GpioInit(void)
{
rcu_periph_clock_enable(RCU_GPIOA);
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_10MHZ, GPIO_PIN_8);
}
static void TimerInit(void)
{
/* 使能定时器时钟;*/
rcu_periph_clock_enable(RCU_TIMER0);
/* 复位定时器;*/
timer_deinit(TIMER0);
timer_parameter_struct timerInitPara;
timer_struct_para_init(&timerInitPara);
/* 设置预分频器值;*/
timerInitPara.prescaler = 120 - 1; // 输入给计数器的时钟频率为1Mhz,周期1us
/* 设置自动重装载值;*/
timerInitPara.period = 500 - 1;
timer_init(TIMER0, &timerInitPara);
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);
/* 设置占空比;*/
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 PwmDrvInit(void)
{
GpioInit();
TimerInit();
}
void PwmDrvTest(void)
{
for (uint32_t i = 0; i < 500; i += 10)
{
timer_channel_output_pulse_value_config(TIMER0, TIMER_CH_0, i);
DelayNms(50);
}
for (uint32_t i = 500; i > 0; i -= 10)
{
timer_channel_output_pulse_value_config(TIMER0, TIMER_CH_0, i);
DelayNms(50);
}
}
PWM模式有效电平
PMW模式分PWMO和PWM1两种模式:
PWMO模式下,当CNT(计数值)<CHxCV的设置值时,输出为有效电平,否则为无效电平;
PWM1模式下,当CNT(计数值)<CHxCV的设置值时,输出为无效电平,否则为有效电平。
有效电平可以简单理解为,能使开关打开的电平:比如,我们控制NPN三极管导通,需要配置有效电平为高;而PNP三极管是低电平导通,因此需要配置有效电平为低。
两者区别在于,脉宽(有效电平)在一个周期中的位置。PWMO模式脉宽靠左;PWM1模式脉宽靠右。
有效电平是可以配置的,如果把有效电平定义为高电平,那么我们重新整理上面一段话得到:
PWMO模式下,当CNT(计数值)<CHxCV的设置值时,输出为高电平,否则为低电平;
PWM1模式下,当CNT(计数值) <CHxCV的设置值时,输出为低电平,否则为高电平。
定时器可以输出几个通道的PWM?
一个定时器只有一个CAR寄存器,但是可以有多个(Hx(V寄存器,定时器0和7有4个通道,所以也有4个CHxCV0也是说每个定时器是可以输出4路频率相同占空比不同的PWM。
输入捕获
输入捕获模式可以用来测量信号周期频率或脉冲宽度。
输入捕获模式可以用来测量信号周期频率或脉冲宽度。
在中断处理中,定时器不会停止,会一直运行。
输入捕获应用
测量一个周期为500us(频率2Khz)的PWM信号的周期,基于定时器1通道0,如何配置?
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);
g_icValue = timer_channel_capture_value_register_read(TIMER1, TIMER_CH_0) + 1;
timer_counter_value_config(TIMER1, 0);
}
}
void CaptureDrvInit(void)
{
GpioInit();
TimerInit();
}
void CaptureDrvTest(void)
{
printf("period is %d us.\n", g_icValue);
DelayNms(500);
}
红外NEC
红外NEC通信协议
红外接收头内部的三极管电路具有信号反向的功能,也是将1变为0,0变为1,所以数据0是0.56ms的低电平和0·56ms的高电平,数据1是0.56ms的低电平和1.69ms的高电平,引导码的9ms是高电平变为低电平。
接收0x55数据,二进制01010101:
红外遥控软件实现
ir.h
#ifndef _IR_H_
#define _IR_H_
#include <stdint.h>
#include <stdbool.h>
#define KEY1_CODE 0X45
#define KEY2_CODE 0X46
/**
***********************************************************
* @brief 红外接收硬件初始化函数
* @param
* @return
***********************************************************
*/
void IrDrvInit(void);
/**
***********************************************************
* @brief 获取遥控按键码值
* @param code,输出,按键码值
* @return 返回是否成功获取到按键码值
***********************************************************
*/
bool GetIrCode(uint8_t *code);
#endif
ir.c
#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>
#include "gd32f30x.h"
static void GpioInit(void)
{
rcu_periph_clock_enable(RCU_GPIOC);
gpio_init(GPIOC, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_10MHZ, GPIO_PIN_6);
}
static void TimerInit(void)
{
/* 使能定时器时钟;*/
rcu_periph_clock_enable(RCU_TIMER7);
/* 复位定时器;*/
timer_deinit(TIMER7);
timer_parameter_struct timerInitPara;
timer_struct_para_init(&timerInitPara);
/* 设置预分频器值;*/
timerInitPara.prescaler = 120 - 1; // 输入给计数器的时钟频率为1Mhz,周期1us
/* 设置自动重装载值;*/
timerInitPara.period = 65535;
timer_init(TIMER7, &timerInitPara);
timer_ic_parameter_struct icInitPara;
timer_channel_input_struct_para_init(&icInitPara);
/* 设置上升沿/下降沿捕获;*/
icInitPara.icpolarity = TIMER_IC_POLARITY_FALLING;
/* 设置输入通道;*/
icInitPara.icselection = TIMER_IC_SELECTION_DIRECTTI;
timer_input_capture_config(TIMER7, TIMER_CH_0, &icInitPara);
/* 使能定时器的捕获中断;*/
timer_interrupt_flag_clear(TIMER7, TIMER_INT_FLAG_CH0);
timer_interrupt_enable(TIMER7, TIMER_INT_CH0);
/* 使能定时器中断和优先级;*/
nvic_irq_enable(TIMER7_Channel_IRQn, 0, 0);
/* 使能定时器;*/
timer_enable(TIMER7);
}
#define TICK_HEAD_MAX 20000 // 表示引导码周期最大值20000us
#define TICK_HEAD_MIN 10000 // 表示引导码周期最小值10000us
#define TICK_0_MAX 1800 // 表示二进制0周期最大值1800us
#define TICK_0_MIN 500 // 表示二进制0周期最小值500us
#define TICK_1_MAX 3000 // 表示二进制1周期最大值3000us
#define TICK_1_MIN 1800 // 表示二进制1周期最小值1800us
static uint8_t g_irCode[4];
static bool g_irCodeFlag = false;
/**
***********************************************************
* @brief 解析按键码值
* @param tickNum,捕获计数值,单位us
* @return
***********************************************************
*/
static void ParseIrFrame(uint32_t tickNum)
{
static bool s_headFlag = false;//静态局部变量,标识是否正确获取到引导码
static uint8_t s_index = 0;//静态局部变量,保存解析了多少位的0和1
if (tickNum > TICK_HEAD_MIN && tickNum < TICK_HEAD_MAX)
{
s_headFlag = true;
return;
}
if (!s_headFlag)
{
return;
}
if (tickNum > TICK_1_MIN && tickNum < TICK_1_MAX)
{
g_irCode[s_index / 8] >>= 1;
g_irCode[s_index / 8] |= 0x80;
s_index++;
}
if (tickNum > TICK_0_MIN && tickNum < TICK_0_MAX)
{
g_irCode[s_index / 8] >>= 1;
s_index++;
}
if (s_index == 32)
{
if ((g_irCode[2] & g_irCode[3]) == 0) //(g_irCode[2] == (uint8_t)~g_irCode[3])
{
g_irCodeFlag = true;
}
else
{
g_irCodeFlag = false;
}
s_headFlag = false;
s_index = 0;
}
}
/**
***********************************************************
* @brief 获取遥控按键码值
* @param code,输出,按键码值
* @return 返回是否成功获取到按键码值
***********************************************************
*/
bool GetIrCode(uint8_t *code)
{
if (!g_irCodeFlag)
{
return false;
}
*code = g_irCode[2];
g_irCodeFlag = false;
return true;
}
void TIMER7_Channel_IRQHandler(void)
{
uint32_t icValue;
if (timer_interrupt_flag_get(TIMER7, TIMER_INT_FLAG_CH0) == SET)
{
timer_interrupt_flag_clear(TIMER7, TIMER_INT_FLAG_CH0);
icValue = timer_channel_capture_value_register_read(TIMER7, TIMER_CH_0) + 1;
timer_counter_value_config(TIMER7, 0);
ParseIrFrame(icValue);
}
}
/**
***********************************************************
* @brief 红外接收硬件初始化函数
* @param
* @return
***********************************************************
*/
void IrDrvInit(void)
{
GpioInit();
TimerInit();
}
1.GpioInit函数:
- 该函数初始化了一个 GPIO 端口,用于连接红外接收器。具体地,使用 rcu_periph_clock_enable(RCU_GPIOC) 使能了 GPIOC 端口的时钟。
- 使用 gpio_init(GPIOC, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_10MHZ, GPIO_PIN_6) 初始化了 GPIOC 的第6个引脚,设置为浮空输入模式。
2.TimerInit 函数: - 该函数初始化了定时器7(TIMER7)用于捕获红外接收器接收到的信号。
- 首先,通过 rcu_periph_clock_enable(RCU_TIMER7) 使能了定时器7的时钟,并通过 timer_deinit(TIMER7) 对定时器7进行了复位。
- 然后,设置了定时器的预分频器值为120 - 1,使定时器的输入时钟频率为1MHz,周期为1微秒,并设置了自动重载值为65535。
- 使用 timer_init(TIMER7, &timerInitPara) 对定时器进行了初始化配置。
- 初始化了定时器7的通道0用于输入捕获,设置了捕获的触发边沿为下降沿,并通过 timer_input_capture_config(TIMER7, TIMER_CH_0, &icInitPara) 进行了配置。
- 使能了定时器7的通道0的捕获中断,并通过 nvic_irq_enable(TIMER7_Channel_IRQn, 0, 0) 使能了定时器7的中断。
3.ParseIrFrame 函数: - 该函数用于解析红外信号的帧,根据红外信号的脉冲宽度判断数据位的值。
- 首先判断是否获取到引导码,若未获取到则直接返回。
- 若获取到引导码,则根据不同的脉冲宽度判断数据位是0还是1,并逐步解析出完整的按键码值。
- 当解析完成32位的按键码值后,判断按键码值的有效性,并将解析状态置为未获取引导码的状态。
4.GetIrCode 函数: - 该函数用于获取解析到的红外按键码值。
- 若成功获取到按键码值,则将按键码值保存到参数 code 中,并将解析状态置为未获取的状态。
5.TIMER7_Channel_IRQHandler 函数: - 该函数为定时器7的中断处理函数,当捕获中断触发时被调用。
- 首先判断是否是通道0的捕获中断触发,若是则清除中断标志,并读取捕获寄存器的值,并进行解析。
6.IrDrvInit 函数: - 该函数用于初始化红外接收硬件,包括初始化 GPIO 端口和定时器。