一、定时器简介
STM32F1 系列中,除了互联型的产品,共有 8
个定时器,分为基本定时器,通用定时器和高级定时器。
基本定时器 TIM6
和 TIM7
是一个 16 位的只能向上计数的定时器,只能定时,没有外部 IO。
通用定时器 TIM2/3/4/5
是一个 16 位的可以向上/下计数的定时器,可以定时,可以输出比较,可以输入捕捉,每个定时器有四个外部 IO。
高级定时器 TIM1/8
是一个 16 位的可以向上/下计数的定时器,可以定时,可以输出比较,可以输入捕捉,还可以有三相电机互补输出信号,每个定时器有 8 个外部 IO。
二、输入捕获
输入捕获模式可以用来测量脉冲宽度或者测量频率。STM32 的定时器,除了TIM6、TIM7,其他的定时器都有输入捕获的功能。
2.1 输入捕获的工作原理
①先设置输入捕获为上升沿检测,
②记录发生上升沿时TIMx_CNT(计数器)的值
③配置捕获信号为下降沿捕获,当下降沿到来的时候发生捕获
④记录此时的TIMx_CN(计数器)T的值
⑤前后两次TIMx_CNT(计数器)的值之差就是高电平的脉宽。同时根据TIM的计数频率,我们就能知道高电平脉宽的准确时间。
简单说:
当你设置的捕获开始的时候,cpu会将计数寄存器
的值复制到捕获比较寄存器
中并开始计数,当再次捕捉到电平变化时,这是计数寄存器中的值减去刚才复制的值就是这段电平的持续时间,你可以设置上升沿捕获、下降沿捕获、或者上升沿下降沿都捕获。
2.2 溢出时间计算
t1时刻检测到高电平,发生中断,在中断里将计数值置0,开始记溢出次数N,
其中每计数0xFFFF次溢出一次,直到t2时刻跳变回低电平,
获取最后一次溢出时到t2时刻的计数值TIM5CH1_CAPTURE_VAL
则 高电平时间 = 溢出次数x65535+TIM5CH1_CAPTURE_VAL us;根据定时器初始化时的频率即可计算出溢出总次数所占用的时间,即为高电平时间。
如果计数器值为 32 bit 那么最大为0xFFFFFFFF
高电平时间:
三、新建工程
1. 打开 STM32CubeMX 软件,点击“新建工程”
2. 选择 MCU 和封装
3. 配置时钟
RCC 设置,选择 HSE(外部高速时钟) 为 Crystal/Ceramic Resonator(晶振/陶瓷谐振器)
开启 LSE(外部低速时钟) 为 Crystal/Ceramic Resonator(晶振/陶瓷谐振器)
选择 Clock Configuration,配置系统时钟 SYSCLK 为 72MHz
修改 HCLK 的值为 72 后,输入回车,软件会自动修改所有配置
4. 配置调试模式
非常重要的一步,否则会造成第一次烧录程序后续无法识别调试器
SYS 设置,选择 Debug 为 Serial Wire
四、TIM5通用定时器
4.1 参数配置
在 Timers
中选择 TIM5
设置,勾选 Internal Clock
使用内部时钟。
Channel1
通道1选择 Input Capture direct mode
输入捕获模式。
注:TIM5 的通道1对应开发板上 KEY1 的 PA0。
在 Parameter Settings
进行具体参数配置。
Tclk 即内部时钟CK_INT,经过APB1预分频器后分频提供,如果APB1预分频系数等于1,则频率不变,否则频率乘以2,库函数中APB1预分频的系数是2,即PCLK1=36M,如图所以定时器时钟Tclk=36*2=72M。
计时器时钟频率为 1MHz,此时 1us 计数一次。
定时器溢出时间:
Tout = 1 / (Tclk / (psc + 1)) ∗ (arr + 1)
- 定时器时钟Tclk:72MHz
- 预分频器psc:71
- 自动重装载寄存器arr:65535
即 Tout = 1/(72MHz/(71+1))∗(65535+1) = 65535us
-
Prescaler(时钟预分频数):72-1
则驱动计数器的时钟 CK_CNT = CK_INT(即72MHz)/(71+1) = 1MHz
- Counter Mode(计数模式):Up(向上计数模式)
- Counter Period(自动重装载值):65535
- auto-reload-preload(自动重装载):Disable(不使能)
-
TRGO Parameters(触发输出):不使能
在定时器的定时时间到达的时候输出一个信号(如:定时器更新产生TRGO信号来触发ADC的同步转换)
-
Input Capture Channel 1(输入捕获通道1)
- Polarity Selection:Rising Edge(上升沿捕获)
-
IC Selection:Direct
默认
-
Prescaler Division Ratio:No division
默认
- Input Filter(4 bits calue)(滤波值):0
4.2 配置NVIC
使能定时器中断
4.3 生成代码
输入项目名和项目路径
选择应用的 IDE 开发环境 MDK-ARM V5
每个外设生成独立的
’.c/.h’
文件不勾:所有初始化代码都生成在 main.c
勾选:初始化代码生成在对应的外设文件。 如 GPIO 初始化代码生成在 gpio.c 中。
点击 GENERATE CODE 生成代码
4.4 修改定时器中断回调函数
打开 stm32f1xx_it.c
中断服务函数文件,找到 TIM5 中断的服务函数 TIM5_IRQHandler()
中断服务函数里面就调用了定时器中断处理函数 HAL_TIM_IRQHandler()
打开 stm32f1xx_hal_tim.c
文件,找到定时器中断处理函数原型 HAL_TIM_IRQHandler()
,其主要作用就是判断是哪个定时器产生哪种事件中断,清除中断标识位,然后调用中断回调函数 HAL_TIM_PeriodElapsedCallback()
。
/* NOTE: This function Should not be modified, when the callback is needed,
the HAL_TIM_PeriodElapsedCallback could be implemented in the user file
*/
这个函数不应该被改变,如果需要使用回调函数,请重新在用户文件中实现该函数。
HAL_TIM_PeriodElapsedCallback()
按照官方提示我们应该再次定义该函数,__weak
是一个弱化标识,带有这个的函数就是一个弱化函数,就是你可以在其他地方写一个名称和参数都一模一样的函数,编译器就会忽略这一个函数,而去执行你写的那个函数;而 UNUSED(htim)
,这就是一个防报错的定义,当传进来的定时器号没有做任何处理的时候,编译器也不会报出警告。其实我们在开发的时候已经不需要去理会中断服务函数了,只需要找到这个中断回调函数并将其重写即可而这个回调函数还有一点非常便利的地方这里没有体现出来,就是当同时有多个中断使能的时候,STM32CubeMX会自动地将几个中断的服务函数规整到一起并调用一个回调函数,也就是无论几个中断,我们只需要重写一个回调函并判断传进来的定时器号即可。
接下来我们就在 stm32f1xx_it.c
这个文件的最下面添加 HAL_TIM_PeriodElapsedCallback()
/* USER CODE BEGIN 1 */
/* TIM5CH1_CAP_STA 各数据位说明
** bit7 捕获完成标志
** bit6 捕获到高电平标志
** bit5~0 捕获高电平后定时器溢出的次数*/
uint8_t TIM5CH1_CAP_STA = 0; // 输入捕获状态
uint16_t TIM5CH1_CAP_VAL; // 输入捕获值
// 中断服务函数里面会自动调用这个回调函数,这个是定时器更新中断处理的函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM5) // 判断定时器5是否发生中断
{
if((TIM5CH1_CAP_STA & 0X80) == 0) // 还未成功捕获
{
if(TIM5CH1_CAP_STA & 0X40) // 已经捕获到高电平
{
if((TIM5CH1_CAP_STA & 0X3F) == 0X3F)// 高电平时间太长了,做溢出处理
{
TIM5CH1_CAP_STA |= 0X80; // 标记为完成一次捕获
TIM5CH1_CAP_VAL = 0XFFFF; // 计数器值
}
else
{
TIM5CH1_CAP_STA++; // 若没有溢出,就只让TIM5CH1_CAP_STA自加
}
}
}
}
}
/* USER CODE END 1 */
4.5 修改输入捕获中断回调函数
打开 stm32f1xx_hal_tim.c
HAL库定时器接口文件,找到输入捕获中断处理回调函数 HAL_TIM_IC_CaptureCallback()
/* NOTE : This function should not be modified, when the callback is needed,
the HAL_TIM_IC_CaptureCallback could be implemented in the user file
*/
这个函数不应该被改变,如果需要使用回调函数,请重新在用户文件中实现该函数。
接下来我们就在 stm32f1xx_it.c
这个文件的最下面添加 HAL_TIM_IC_CaptureCallback()
// 定时器输入捕获中断处理回调函数,该函数在 HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim) 中会被调用
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if((TIM5CH1_CAP_STA & 0X80) == 0) // 还未成功捕获
{
if(TIM5CH1_CAP_STA & 0X40) // 捕获到一个下降沿
{
TIM5CH1_CAP_STA |= 0X80; // 标记成功捕获到一次高电平脉宽
TIM5CH1_CAP_VAL = HAL_TIM_ReadCapturedValue(&htim5, TIM_CHANNEL_1); // 获取当前的计数器值
TIM_RESET_CAPTUREPOLARITY(&htim5, TIM_CHANNEL_1); // 清除原来的设置
TIM_SET_CAPTUREPOLARITY(&htim5,TIM_CHANNEL_1, TIM_ICPOLARITY_RISING);// 设置上升沿捕获
}
else
{
TIM5CH1_CAP_STA = 0; // 清空自定义的状态寄存器
TIM5CH1_CAP_VAL = 0; // 清空捕获值
TIM5CH1_CAP_STA |= 0X40; // 标记捕获到上升沿
__HAL_TIM_DISABLE(&htim5); // 关闭定时器
__HAL_TIM_SET_COUNTER(&htim5, 0); // 计数器值清零
TIM_RESET_CAPTUREPOLARITY(&htim5,TIM_CHANNEL_1); // 一定要先清除原来的设置 !!
TIM_SET_CAPTUREPOLARITY(&htim5,TIM_CHANNEL_1,TIM_ICPOLARITY_FALLING); // 设置下降沿捕获
__HAL_TIM_ENABLE(&htim5); // 使能定时器
}
}
}
此处的TIM_RESET_CAPTUREPOLARITY() 函数不同版本库可能有一处HAL库函数错误,会导致编译该函数报错,解决办法是找到该函数在 stm32f1xx_hal_tim.h 文件中的定义,删除多余的一个反括号 ‘)’
最新版HAL库可跳过这一步
stm32f1xx_hal_tim.h
//修改前
#define TIM_RESET_CAPTUREPOLARITY(__HANDLE__, __CHANNEL__) \
(((__CHANNEL__) == TIM_CHANNEL_1) ? ((__HANDLE__)->Instance->CCER &= ~(TIM_CCER_CC1P | TIM_CCER_CC1NP))) :\
((__CHANNEL__) == TIM_CHANNEL_2) ? ((__HANDLE__)->Instance->CCER &= ~(TIM_CCER_CC2P | TIM_CCER_CC2NP)) :\
((__CHANNEL__) == TIM_CHANNEL_3) ? ((__HANDLE__)->Instance->CCER &= ~(TIM_CCER_CC3P)) :\
((__HANDLE__)->Instance->CCER &= ~(TIM_CCER_CC4P)))
//修改后
#define TIM_RESET_CAPTUREPOLARITY(__HANDLE__, __CHANNEL__) \
(((__CHANNEL__) == TIM_CHANNEL_1) ? ((__HANDLE__)->Instance->CCER &= ~(TIM_CCER_CC1P | TIM_CCER_CC1NP)) :\
((__CHANNEL__) == TIM_CHANNEL_2) ? ((__HANDLE__)->Instance->CCER &= ~(TIM_CCER_CC2P | TIM_CCER_CC2NP)) :\
((__CHANNEL__) == TIM_CHANNEL_3) ? ((__HANDLE__)->Instance->CCER &= ~(TIM_CCER_CC3P)) :\
((__HANDLE__)->Instance->CCER &= ~(TIM_CCER_CC4P)))
4.6 修改main函数
在上面 HAL_TIM_PeriodElapsedCallback()
回调函数中用以处理计数次数和时间;
在上面 HAL_TIM_IC_CaptureCallback()
回调函数负责处理捕获到的上升沿和下降沿。
接下来编写高电平持续时间处理代码
首先在 main.c
声明定义在 stm32f1xx_it.c
的外部变量
extern uint8_t TIM5CH1_CAP_STA;
extern uint16_t TIM5CH1_CAP_VAL;
然后修改 main()
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
long long temp = 0;// 定义一个变量用以存储捕获到的时间 long long型是为了防止数据溢出
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_USART1_UART_Init();
MX_TIM5_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_IC_Start_IT(&htim5,TIM_CHANNEL_1); // 一定要开启TIM5通道1的捕获中断
__HAL_TIM_ENABLE_IT(&htim5,TIM_IT_UPDATE); // 一定要开启TIM5的更新中断
printf("This is TIM_CAP test...\n");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_Delay(500);
if(TIM5CH1_CAP_STA & 0X80) // 完成一次高电平捕获
{
temp = TIM5CH1_CAP_STA & 0X3F;
temp *= 65536; // 溢出总时间
temp += TIM5CH1_CAP_VAL; // 总的高电平时间
printf("High level duration:%lld us\r\n",temp);
TIM5CH1_CAP_STA = 0; // 准备下一次捕获
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
五、查看打印
串口打印功能查看 STM32CubeMX学习笔记(6)——USART串口使用
按下 KEY1 键,串口会打印出相应的高电平持续时间
六、注意事项
用户代码要加在 USER CODE BEGIN N
和 USER CODE END N
之间,否则下次使用 STM32CubeMX 重新生成代码后,会被删除。
• 由 Leung 写于 2021 年 3 月 25 日
• 参考:STM32CubeMX系列|输入捕获
第五节:STM32输入捕获(用CubeMX学习STM32)
【STM32】HAL库 STM32CubeMX教程八---定时器输入捕获