可以说,STM32的定时器是32这块芯片中功能最多,结构最复杂,内容涉及最多的外设之一了。从学习形式来看,可以归纳为两个层面:原理层和软件层。那么由于内容太多,我也按照这个架构来逐一进行讲解。
目录
一:定时器架构与原理
1.时基
时基是STM32所有定时器的核心区域,任何类别的定时器都具有时基结构。它的原理也非常的典型并且简单:
其中:
CLK 时钟来源,一般来说时HCLK,特殊情况下还可以从输入捕获通道讲来源配置到外部。
PSC 分频频率,说白了就是对HCLK进行分频的寄存器
ARR 重装值这个表述其实并不准确,后续细讲。
2.计数方式
计数方式分为三种:
向上计数法和向下计数法:这两种计数法都非常好理解。一种是递减计数,一种是递增计数。但是这里就能看出来,如果ARR单纯的是当溢出后重装给定时器的值,那么这样是完全不合理的。所以准确的说:向下计数时,ARR是重装值,向上计数时,ARR时是溢出值。
中心对齐计数法:先从0开始向上计数,当CNT=ARR-1时,可以产生中断。此时开始向下计数,当CNT=1时,可以再次产生中断。
听不懂?好说,上图:
另外,这里可以得出一条公式:
其中,Ft是时钟来源频率,Tout是溢出的时间长度。
3.定时器的架构
定时器的种类有三种,在32中一般分为:
基本定时器 拥有基本定时功能和中断功能
通用定时器 拥有基本定时器功能外加输入捕获;输出比较;内外时钟源选择
高级定时器 拥有通用定时器所有功能,外加重复计数器;死区重生;互补和刹车输入
从这你就能开出来定时器的功能有多么丰富,再看看架构图:
这是一个通用定时器的架构,其实主要就分三部分:
时钟来源---时基---GPIO--通道
其中,通道就是下面这些重复的部分,也是实现主要功能的区域。每一个寄存器都配备有很多个通道,并且对应的GPIO口对应的通道往往是确定的:比如LED1
这里的TIM4_CH3就是定时器4通道3.
二.定时器功能
通道是32定时器的核心学习内容,可以说定时器这个部分复杂就复杂在了通道这个部分。其原因就是 STM32的所有的定时器特殊功能几乎都来源于这些通达。
1.基础溢出中断功能
当定时器的值小于0或者当它大于ARR又或者是到达某个设定特殊值时,将产生中断,这个原理非常简单,主要是软件的配置部分对于不熟悉新手而言比较饶。
我给个基本的步骤吧:
时基的基本的配置 ---> 时钟的使能 ---> GPIO配置(可以没有)--->NVIC配置 ---> 中断服务函数配置 ---> 中断溢出回调函数具体配置
其中,按颜色来分布记忆最好,开始成为时基的配置,中间为MSP配置,最后为中断内容配置。
废话少说,我上代码吧:
#include "timer.h"
#include "led.h"
TIM_HandleTypeDef timer_init = {0};
void Timer_init(uint16_t arr,uint16_t psc){
timer_init.Instance = TIM2; //选择定时器
timer_init.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; //重装使能
timer_init.Init.CounterMode = TIM_COUNTERMODE_UP; //计数方式
timer_init.Init.Period = arr; //arr,重装载初始值
timer_init.Init.Prescaler = psc; //psc, 分频数
HAL_TIM_Base_Init(&timer_init);
HAL_TIM_Base_Start_IT(&timer_init);
}
//初始化msp,该函数会被HAL_TIM_Base_Init自动调用,就是像中断中的公共处理函数一样
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim){
if(htim->Instance == TIM2){
__HAL_RCC_TIM2_CLK_ENABLE();
HAL_NVIC_SetPriority(TIM2_IRQn,0,2);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
}
}
void TIM2_IRQHandler(){
HAL_TIM_IRQHandler(&timer_init);
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *timer_init){
if(timer_init->Instance == TIM2)
led_triggle();
}
这是一个溢出中断控制小灯规律亮灭的配置。
这里我带一个常识:
HAL库中的中断函数往往是 中断服务函数---中断公共处理函数---xxx功能回调函数 其中回调函数的数量往往是相当的庞大的,你找个函数能累死你的,所以一会儿到最后我给一些常用的通用的API做个总结。
2.输出比较功能
输出比较功能是指,存在一个寄存器可以将自身的值和定时器的CNT进行比较,通过结果来控制对应的GPIO口的极性。
图中可以看到的 捕获/比较寄存器 就是CRR寄存器。说白了,就是把CRR的值跟CNT比较以下,然后可以根据结果来调整对应输出接的GPIO口的电平,就这么简单。
CNT是变化的,也就是说,单位周期里一段时间内有CNT>CRR也有CNT<CRR。并且CNT的变化速度是平均的,所以我们完全可以通过控制CRR来控制单位时间内CNT有多久>CRR多久<。对应到GPIO口就是单位时间内可以控制多久位高电平多久为低电平。
怎么样,有没有觉得很熟悉呼之欲出?是的,完全可以搞出来一个PWM波。
上代码!
//PWM部分:
#include "pwm.h"
#include "led.h"
TIM_HandleTypeDef timer_init = {0};
void Timer_init(uint16_t arr,uint16_t psc){
timer_init.Instance = TIM4; //选择定时器
timer_init.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; //重装使能
timer_init.Init.CounterMode = TIM_COUNTERMODE_UP; //计数方式
timer_init.Init.Period = arr; //arr,重装载初始值
timer_init.Init.Prescaler = psc; //psc, 分频数
TIM_OC_InitTypeDef pwm_config = {0};
pwm_config.OCMode = TIM_OCMODE_PWM1;
pwm_config.Pulse = arr/2;
pwm_config.OCPolarity = TIM_OCPOLARITY_LOW;
HAL_TIM_PWM_Init(&timer_init);
HAL_TIM_PWM_ConfigChannel(&timer_init,&pwm_config,TIM_CHANNEL_3);
HAL_TIM_PWM_Start_IT(&timer_init,TIM_CHANNEL_3);
}
//初始化msp,该函数会被HAL_TIM_Base_Init自动调用,就是像中断中的公共处理函数一样
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim){
if(htim->Instance == TIM4){
__HAL_RCC_TIM4_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef gpio_init={0};
gpio_init.Pin = GPIO_PIN_8;
gpio_init.Pull = GPIO_PULLUP;
gpio_init.Mode = GPIO_MODE_AF_PP;
gpio_init.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB,&gpio_init);
}
}
void CRR_SET(uint16_t val){
__HAL_TIM_SET_COMPARE(&timer_init, TIM_CHANNEL_3, val);
}
//main部分
int main(void)
{
stm32_clock_init(RCC_PLL_MUL9);
HAL_Init();
led_init();
Timer_init(500 - 1,72 - 1);
uint16_t i = 0;
while(1)
{
for(i = 0; i<200; i++){
CRR_SET(2*i);
delay_ms(5);
}
for(i = 0; i<200; i++){
CRR_SET(499 - 2*i);
delay_ms(5);
}
}
}
这里最后就是用PWM波控制了一手小灯,让他慢慢的亮慢慢的灭。俗称呼吸灯。
3.输入捕获
其实输入捕获和输出比较用的是同一个寄存器,只不过重点和使用方法变了:
是的,之前是通过CRR最后控制到GPIO上,这次反过来了,这次是通过GPIO控制到CRR上了。
那么具体的是怎么个事儿呢?
就是通过检测某一个GPIO引脚的电平,通过配置产生中断。此时内部的CRR接收到响应的请求后通过更新中断捕获此时定时器的CNT的值,并把该值赋值给了CRR。
这其实还是不难,但是说白了,这里所有的难度都在于软件配置的理解上,废话不多说,上代码:
#include "pwm.h"
#include "led.h"
#include "uart1.h"
#include "string.h"
struct{
uint8_t success_flag;
uint8_t falling_flag;
uint8_t rasing_flag;
uint8_t timeout_cnt;
uint16_t count;
}FLAG = {0};
TIM_HandleTypeDef timer_init = {0};
void Timer_init(uint16_t arr,uint16_t psc){
//时基初始化
timer_init.Instance = TIM2; //选择定时器
timer_init.Init.CounterMode = TIM_COUNTERMODE_UP; //计数方式
timer_init.Init.Period = arr; //arr,重装载初始值
timer_init.Init.Prescaler = psc; //psc, 分频数
HAL_TIM_IC_Init(&timer_init);
//通道的选择和配置
TIM_IC_InitTypeDef chennel_config = {0};
chennel_config.ICFilter = 0;
chennel_config.ICPolarity = TIM_ICPOLARITY_FALLING; //指定触发电平边沿
chennel_config.ICPrescaler = TIM_ICPSC_DIV1; //指定分频数
chennel_config.ICSelection = TIM_ICSELECTION_DIRECTTI; //指定输入
HAL_TIM_IC_ConfigChannel(&timer_init,&chennel_config,TIM_CHANNEL_2);
__HAL_TIM_ENABLE_IT(&timer_init,TIM_IT_UPDATE);
HAL_TIM_IC_Start_IT(&timer_init, TIM_CHANNEL_2);
}
//初始化msp,该函数会被HAL_TIM_Base_Init自动调用,就是像中断中的公共处理函数一样
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim){
if(htim->Instance == TIM2){
__HAL_RCC_TIM2_CLK_ENABLE(); //定时器时钟配置
__HAL_RCC_GPIOA_CLK_ENABLE(); //引脚时钟配置
GPIO_InitTypeDef gpio_init={0};
gpio_init.Pin = GPIO_PIN_1;
gpio_init.Pull = GPIO_PULLUP;
gpio_init.Mode = GPIO_MODE_INPUT;
gpio_init.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA,&gpio_init);
HAL_NVIC_SetPriority(TIM2_IRQn, 2, 2); //NVIC中断优先组设置
HAL_NVIC_EnableIRQ(TIM2_IRQn); //NVIC中断使能
}
}
void TIM2_IRQHandler(){
HAL_TIM_IRQHandler(&timer_init);
}
//公共中断处理函数中的捕获回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim){
if(htim->Instance == TIM2){
if(FLAG.success_flag == 0){
if(FLAG.falling_flag == 0){
FLAG.count = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2); //捕获当前定时器内的val
led_on();
FLAG.falling_flag = 1;
TIM_RESET_CAPTUREPOLARITY(&timer_init, TIM_CHANNEL_2); //清空通道设置
TIM_SET_CAPTUREPOLARITY(&timer_init, TIM_CHANNEL_2, TIM_ICPOLARITY_RISING); //设置通道设置
}else{
FLAG.count -= HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2); //再次捕获当前val
FLAG.success_flag = 1;
led_off();
printf("按下按键时长:%d us /r/n",FLAG.count+FLAG.timeout_cnt*65536); //计算
TIM_RESET_CAPTUREPOLARITY(&timer_init, TIM_CHANNEL_2); //清空通道设置
TIM_SET_CAPTUREPOLARITY(&timer_init, TIM_CHANNEL_2, TIM_ICPOLARITY_FALLING); //设置通道设置
memset(&FLAG,0,sizeof(FLAG));
}
}
}
}
//溢出中断函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
FLAG.timeout_cnt ++; //记录溢出了多少次
}
到这里你就发现了,钩子配置三次三次配置连时基的初始函数都不一样。哈哈是的,非常之c蛋,所以我才说到最最后的时候我给你来个API总结。说白了也是给我自己做个总结,因为钩子实在是TM太多了。
这个代码的配置我就不写流程了,和我之前说的流程是吻合的。
4.时钟来源配置
如果你能撑到这里还在看,那哥们你确实是挺行的,因为作者我写到这儿我都有点受不了了。
废话不多说,这里的功能其实也是非常简单。默认的定时器本来的来源基本都是HCLK,也就是72Mhz,奥对了,我之前居然不知道这里的72M其实是Mllion就是一百万的意思。扯回来,所以这里其实就是配置一个外面的时钟源,原理上没啥好讲的。
唯一有一点要注意就是这里多了一个部分:叫从模式控制器配置
然后这里只上核心代码吧:
TIM_SlaveConfigTypeDef Slave_config = {0};
Slave_config.SlaveMode = TIM_SLAVEMODE_EXTERNAL1;
Slave_config.InputTrigger = TIM_TS_TI2FP2;
Slave_config.TriggerFilter = 0;
Slave_config.TriggerPolarity = TIM_TRIGGERPOLARITY_FALLING;
HAL_TIM_SlaveConfigSynchro(&timer_init,&Slave_config);
HAL_TIM_IC_Start(&timer_init,TIM_CHANNEL_2);
这里选择TIM2 和 TIM_CHANNEL_2的原因是因为这里把外源配置给了Key1,也就是最后我按一下定时器的CNT++一下。
三:API的总结
我是真不想做这个部分,但是还是觉得挺必要的:
时基API
HAL_TIM_Base_Init()
HAL_TIM_Base_Start_IT()
HAL_TIM_Base_MspInit()
HAL_TIM_PeriodElapsedCallback()
__HAL_RCC_TIM4_CLK_ENABLE()
输出比较API
HAL_TIM_PWM_Init()
HAL_TIM_PWM_ConfigChannel()
HAL_TIM_PWM_Start_IT()
HAL_TIM_PWM_MspInit()
输入捕获API
HAL_TIM_IC_Init()
HAL_TIM_IC_ConfigChannel()
__HAL_TIM_ENABLE_IT()
HAL_TIM_IC_Start_IT()
HAL_TIM_IC_MspInit()
HAL_TIM_IC_CaptureCallback()
TIM_RESET_CAPTUREPOLARITY
TIM_SET_CAPTUREPOLARITY
外源时钟配置API
HAL_TIM_SlaveConfigSynchro()
好了,作者我彻底干了,这里这些API没有一个是重复的,但是功能都是非常相似的,所以我称之为初学者收集者。来一个给你劝退一个,我的建议是如果你看原理图听各种流程各种难受,那你就直接上项目。东西整出来了什么你也就会了,别丧失自信。
标签:定时器,细致,Init,timer,STM32,TIM,init,HAL From: https://blog.csdn.net/wwwkkkxxx12138/article/details/140612388