定时器就是计数器
预分频器:设置N分屏,将分频器数设置为N-1
内部时钟:APB1,APB2
外部时钟:ETR,TI1,TI2(CNT 的计数脉冲来自外部引脚输入的脉冲信号,同时由于外部信息有干扰,要开启滤波)
一、定时器分类
高级定时器 TIM1/TIM8:互补 PWM、死区、刹车,APB2
通用定时器 TIM2~TIM5:定时中断、PWM 输出、输入捕获;TIM2/TIM5 支持 32 位计数
基本定时器 TIM6/TIM7:无通道,最简单(只有最简单的定时功能,以及一个触发DAC的功能)
HAL 库时钟规则不变:
APB 预分频≠1 → 定时器时钟 = APB 时钟 ×2;F103 主频 72MHz,APB1 预分频 2,TIM2~TIM7 定时器时钟 = 72MHz。
二、CubeMX 配置定时器完整步骤(核心流程,实操必考)
RCC 配置:设置 HSE 外部晶振,系统时钟 72MHz
SYS:Debug 选择 Serial Wire(调试下载)
进入Timers选项卡,选择定时器(例:TIM3)
配置模式:
生成定时中断:Internal Clock(内部时钟)
PWM 输出:PWM Generation CHx
输入捕获:Input Capture direct mode
Parameter Settings 参数设置
Prescaler(PSC):预分频 PSC
Counter Period(ARR):自动重装载值 ARR
Counter Mode:Up 向上 / Down 向下 / Center-aligned 中央对齐
Auto-Reload Preload:ARR 预装载使能(默认 Enable)
NVIC 设置:开启定时器全局中断,设置抢占优先级 & 子优先级
项目管理器:生成代码,勾选 Generate initialization as .c/.h files,生成 HAL 库工程,用 Keil 打开
三、HAL 库定时器结构体TIM_HandleTypeDef htim3; // 定时器句柄
句柄内部包含:
Instance:TIM 外设基地址 TIM3
Init:TIM 初始化结构体(PSC、ARR、计数模式等)
Channel:通道配置结构体(PWM / 捕获用)
State:定时器运行状态
Lock:锁状态
四、三大常用功能(HAL 库函数 + 代码模板)
- 定时器更新中断(定时中断,最基础)
CubeMX:TIM3 选择 Internal Clock,开启 Update 中断
启动定时器(HAL 库两种方式)// 阻塞式启动(不推荐中断场景)
HAL_TIM_Base_Start(&htim3);//定时器启动函数
__HAL_TIM_GET_COUNTER(&htim4);//获取计数器的值
__HAL_TIM_SET_COUNTER(&htim4);//设置计数器的值
// 中断方式启动【定时中断必须用这个!】
HAL_TIM_Base_Start_IT(&htim3);
_IT:Interrupt,开启更新中断
中断回调函数(重写,用户写业务代码)// 定时器更新溢出回调函数,溢出自动调用
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM3) // 判断是哪个定时器
{
// 定时到了,执行你的代码,例如翻转IO
HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_0);
}
}
注意:
不要在回调函数写耗时代码!中断回调要快
不要手动写 TIM3_IRQHandler,CubeMX 自动生成中断服务函数,里面会调用 HAL 库中断处理函数 HAL_TIM_IRQHandler(&htim3),最终触发回调。
关闭定时中断:HAL_TIM_Base_Stop_IT(&htim3);
2.PWM 输出(通用定时器通道 PWM)CubeMX:TIM3 → PWM Generation CH1,参数 PSC、ARR 设置 PWM 周期;Pulse 就是 CCR 值,占空比 = CCR / (ARR+1)
3. // 启动PWM通道
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
输出脉冲宽度
// 修改CCR,动态调节占空比
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, CCR的值);
// 读取当前CCR值
uint32_t ccr_val = __HAL_TIM_GET_COMPARE(&htim3, TIM_CHANNEL_1);
宏定义:
__HAL_TIM_SET_AUTORELOAD() 设置 ARR
__HAL_TIM_SET_PRESCALER() 设置 PSC
PWM 模式 1(CubeMX 默认,最常用)
向上计数:
CNT < CCR:输出有效电平(高)(此时CCR越大,占空比越大)占空比=(CNT<CCR)/周期TCNT ≥ CCR:输出无效电平(低)
PWM 模式 2(反向)
向上计数:
CNT < CCR:输出无效电平CNT ≥ CCR:输出有效电平
- 输入捕获(测脉冲宽度 / 频率)CubeMX:TIMx CHx → Input Capture direct mode// 开启输入捕获中断
HAL_TIM_IC_Start(&htim3, TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
捕获中断回调:void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM3 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
{
uint32_t cap_val = HAL_TIM_ReadCapturedValue(&htim3, TIM_CHANNEL_1);
// cap_val:捕获到的CNT计数值,用于计算脉冲宽度
}
}
二、定时器编码器模式原理
硬件接线
- 编码器 A 相 → 定时器 CH1 引脚
- 编码器 B 相 → 定时器 CH2 引脚
三种可选模式(CubeMX 里选择)
1TI1 and TI2(最推荐,常用):A、B 相上升沿、下降沿都计数,4 倍频。分辨率 ×4。
2.TI1:仅在 TI1 边沿计数(2 倍频)
3.TI2:仅在 TI2 边沿计数(2 倍频)
4 倍频含义:A 上升、A 下降、B 上升、B 下降,四个边沿都会触发 CNT 计数。一圈脉冲 = 编码器线数 ×4
编码电机
直流电机 + 正交编码器;PWM(定时器输出)控制电机转动;定时器编码器接口模式读取编码器 AB 相脉冲,获取位置 / 转向
/ 启动编码器模式,CH1+CH2一起开启
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
// 停止编码器
HAL_TIM_Encoder_Stop(&htim3, TIM_CHANNEL_ALL);
// 读取CNT计数值(脉冲数)
int16_t pulse = (int16_t) __HAL_TIM_GET_COUNTER(&htim3);
// 清零CNT
__HAL_TIM_SET_COUNTER(&htim3,0);
回调函数(溢出中断时使用,用来处理 CNT 溢出,累计总脉冲)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM3)
{
// 判断是上溢还是下溢,累加总脉冲
}
}
HAL_TIM_Base_Start启动定时器,无中断
HAL_TIM_Base_Start_IT启动定时器 + 开启更新中断
HAL_TIM_Base_Stop / Stop_IT停止定时器
HAL_TIM_PWM_Start启动 PWM 通道
HAL_TIM_IC_Start_IT输入捕获中断启动
HAL_TIM_IRQHandler定时器通用中断处理函数(自动在 IRQ 服务函数调用)
六、HAL 库定时器常见坑点
影子寄存器:HAL 库默认预装载开启,修改 ARR/CCR 不会立刻生效,要等更新事件;想要立即生效:关闭预装载或者软件触发更新事件 __HAL_TIM_GENERATE_EVENT(&htim3, TIM_EVENTSOURCE_UPDATE);
HAL_TIM_PeriodElapsedCallback 是弱函数:自己在 main.c 新建同名函数重写,不要改 stm32xx_it.c 里面的中断服务函数
启动中断一定要带 _IT,很多人误用 HAL_TIM_Base_Start 然后疑惑没有中断
NVIC 优先级:CubeMX 中定时器中断优先级配置,优先级分组要统一,否则中断不触发
PWM 通道:HAL_TIM_PWM_Start 是通道启动,不是定时器整体;定时器开启后,每个通道需要单独 Start
定时器句柄不能写错,每个定时器对应独立 htim 句柄,htim1、htim3 互相独立
七、最简完整工程框架模板(Keil)
int main(void)
{
HAL_Init();
SystemClock_Config(); // CubeMX自动生成时钟配置
MX_GPIO_Init();
MX_TIM3_Init(); // 定时器初始化,CubeMX生成
HAL_TIM_Base_Start_IT(&htim3); // 开启TIM3更新中断
while (1)
{
//主循环,放后台任务,不要阻塞
}
}
//定时器溢出回调
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM3)
{
HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_0);
}
}