STM32 高级定时器及PWM
一、核心功能模块
输入捕获:测量外部脉冲的宽度或频率,需配合跳变沿捕获和溢出计数。
输出比较:产生特定宽度脉冲,或用于驱动DAC。
PWM 生成:输出脉宽调制信号,频率由ARR寄存器决定,占空比由CCRx寄存器决定。
单脉冲模式:接收一个触发信号后,输出单个脉冲。
可编程死区互补输出:单通道输出两路互补PWM,切换时插入死区时间,防止短路。
重复计数器(RCR):设定N+1次计数器溢出/下溢后,才产生真正的更新事件(UEV)。
二、时钟来源配置
定时器的时钟源决定了计数器(CNT)的计数基准。
1. 内部时钟
来源:芯片内部系统时钟(如APB总线时钟)。
配置:TIMx_SMCR 寄存器 SMS [2:0] 设为 000(禁止从模式),TIMx_CR1 寄存器 CEN 位置 1(使能计数器)。
路径:
CK_INT→预分频器(PSC)→计数器(CNT)
2. 外部时钟模式
外部时钟模式 1:通过 TIMx_CH1/2/3/4 引脚输入外部信号,经输入滤波器、边沿检测器处理后作为时钟。
信号路径1:
TIMx_CHx引脚→输入滤波器→边沿检测器→TIxFPx→触发选择器→TRGI→从模式控制器→CK_PSC→预分频器→计数器。
外部时钟模式2:外部信号直接驱动计数器,不经过触发控制器。
3. 内部触发输入(ITRx)- 定时器级联
原理:将一个定时器的触发输出(TRGO)连接到另一个定时器的内部触发输入(ITRx)。
信号路径:
TIMx计数器溢出→更新事件→TRGO输出→连接到目标定时器ITRx→TRGI→从模式控制器→CK_PSC→预分频器→计数器
应用:实现定时器的同步或级联,扩展定时周期或创建复杂时序。
三、PWM配置
1. 生成原理
频率:由自动重装载寄存器(ARR)决定。
频率 = 定时器时钟 / (ARR + 1)
占空比:由捕获/比较寄存器(CCRx)决定。
占空比 = CCRx / (ARR + 1)
2. 工作模式
PWM 模式 1(向上计数):
CNT<CCRx 时,OCxREF 为有效电平;
CNT≥CCRx 时,为无效电平。
PWM 模式 2(向上计数):
CNT<CCRx 时,OCxREF 为无效电平;
CNT≥CCRx 时,为有效电平。
极性控制:通过 CCER 寄存器 CCxP 位,定义有效电平为高或低。
3. 计数与中断特性(中央对齐模式)
CMS=01(中央对齐模式 1):
仅计数器向下计数且 CNT=CCRx 时置位比较标志,每个 PWM 周期 1 次比较中断。
CMS=10(中央对齐模式 2):
仅计数器向上计数且 CNT=CCRx 时置位比较标志,每个 PWM 周期 1 次比较中断。
CMS=11(中央对齐模式 3):
计数器向上 / 向下计数且 CNT=CCRx 时均置位比较标志,每个 PWM 周期 2 次比较中断。
4. 输出控制与预装载机制
输出控制
输出信号的状态由多个寄存器位共同决定。
MOE(主输出使能)
OSSI(空闲状态输出)
OSSR(运行状态输出)
CCxE/CCxNE(通道及互补通道输出使能)
预装载机制
作用:确保PWM参数(ARR, CCRx)在更新事件时同步更新,避免修改参数导致波形出现毛刺或断裂。
ARPE位:使能ARR寄存器的预装载。
OCxPE位:使能CCRx寄存器的预装载。
四、关键保护与高级功能
1. 死区时间配置
作用:防止半桥 / 全桥电路中两个开关管同时导通导致短路。
原理:在互补信号(OCx和OCxN)的切换时刻,插入一段双方都为无效电平的"死区时间"。
实现:通过死区时间发生器,基于 OCxREF 参考波形生成 OCx 和 OCxN 两路互补信号。
配置:由 BDTR 寄存器 DTG [7:0] 位设定。
T_dts=T_clk*T_dtg
T_clk 为定时器时钟周期
T_dtg 为 DTG 配置值
2. 刹车(断路)功能
触发条件
内部断路:时钟安全系统(CSS)检测到时钟故障。
外部断路:BKIN 引脚(最高优先级硬件保护引脚)输入有效信号。
断路后动作
主输出使能(MOE)位清零,输出进入无效状态。
输出通道及互补通道电平设为安全状态(由 OSSI 和 OSSR 位控制)。
置位断路中断标志(BIF),可触发中断和 DMA 请求。
启用自动输出使能(AOE)时,MOE 位在下一个更新事件自动置 1,恢复输出。
3. 重复计数器(RCR)
功能:设定在 N+1 个计数器上溢/下溢事件后,才产生一次真正的更新事件(UEV)。
应用:方便地生成频率为基准PWM 1/(N+1) 的复杂波形,无需软件频繁干预。
五、输入测量功能
1. 输入捕获(测量脉宽)
原理:通过两次跳变沿捕获计数器值,差值乘以计数周期即为脉宽时间。
脉宽 = (第二次捕获值 - 第一次捕获值 + 溢出次数 * ARR) * 计数周期
步骤:
配置输入捕获通道和初始捕获边沿(如上升沿);
第一次捕获后记录值并切换边沿(如下降沿);
第二次捕获后计算两次值的差值;
脉宽超 ARR 时,需通过溢出中断计数溢出次数。
2. PWM输入捕获模式(特殊模式)
使用 CH1 和 CH2 两个捕获通道,仅支持 TIMx 的 CH1/CH2 通道。
配置:
使用两个捕获通道(通常为IC1和IC2);
IC1捕获上升沿,并配置为触发信号(TRC);
IC2捕获下降沿;
设置从模式为复位模式,当IC1捕获到上升沿时,计数器立即复位。
结果:
周期 = IC1捕获的值(因为每次上升沿计数器都复位)。
高电平时间 = IC2捕获的值。
占空比 = (IC2 / IC1) * 100%
六、编码器模式
功能:测量机械旋转/直线运动的位置、速度,将机械信号转换为电信号,本质是带方向选择的外部时钟应用。
工作前提:计数器仅在0~TIMx_ARR之间计数,开始前需配置TIMx_ARR寄存器;捕获器、比较器等其他特性正常工作。
计数方式与规则:
仅在 TI1 边沿计数:TI1 边沿触发计数,TI2 电平决定方向
TI2 低→TI1 上升增 / 下降减;TI2 高→TI1 上升减 / 下降增
仅在 TI2 边沿计数:TI2 边沿触发计数,TI1 电平决定方向
TI1 低→TI2 上升增 / 下降减;TI1 高→TI2 上升减 / 下降增
在 TI1 和 TI2 边沿计数:两通道所有边沿均触发计数,分辨率提升 4 倍;
TI1 领先时计数器递增,TI2 领先时递减(适用于正交编码器)。
七、相关代码及分析
1. 定时器初始化函数
void MX_TIM1_Init(void)
{
// 定义并初始化定时器时钟配置结构体
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
// 定义并初始化定时器主模式配置结构体
TIM_MasterConfigTypeDef sMasterConfig = {0};
/* USER CODE BEGIN TIM1_Init 1 */
/* USER CODE END TIM1_Init 1 */
// 用户代码区域1,用户可以在此添加自定义初始化代码
// 设置定时器实例为TIM1(高级定时器)
htim1.Instance = TIM1;
// 设置预分频器为7200-1,即7199
// 如果系统时钟为72MHz,则定时器时钟 = 72MHz / (7199+1) = 10kHz
htim1.Init.Prescaler = 7200-1;
// 设置计数器模式为向上计数
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
// 设置自动重载寄存器周期为1000-1,即999
// 计数器从0计数到999,共1000个计数,产生更新事件
htim1.Init.Period = 1000-1;
// 设置时钟分频为不分频
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
// 设置重复计数器为0(高级定时器特有功能)
htim1.Init.RepetitionCounter = 0;
// 使能自动重载预装载功能
// 新的ARR值会在下次更新事件时生效,避免在运行时修改ARR导致的不一致
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
// 初始化定时器基础配置,如果初始化失败则调用错误处理函数
if (HAL_TIM_Base_Init(&htim1) != HAL_OK)
{
Error_Handler();
}
// 配置时钟源为内部时钟
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
// 配置定时器时钟源,如果配置失败则调用错误处理函数
if (HAL_TIM_ConfigClockSource(&htim1, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
// 设置主模式触发输出为复位(不产生触发信号)
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
// 禁用主从模式
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
// 配置主模式同步设置,如果配置失败则调用错误处理函数
if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
}
2. 定时器中断启动函数
HAL_TIM_Base_Start_IT(&htim1);
3. 定时器周期溢出回调函数
// 当定时器计数器达到重装载值(周期值)时,HAL库会自动调用此函数
void HAL_TIM_PeriodElapseCallback(TIM_HandleTypeDef *htim)
{
// 检查触发回调的定时器实例是否是TIM2
if(htim->Instance==TIM2)
{
// 切换GPIOA端口上LED_R_Pin、LED_G_Pin、LED_B_Pin引脚的电平状态
// 如果引脚当前为高电平,则变为低电平;如果为低电平,则变为高电平
HAL_GPIO_TogglePin(GPIOA,LED_R_Pin|LED_G_Pin|LED_B_Pin);
}
}
4. 使用TIM实现LED闪烁频率切换
// 定义定时器1的自动重装载值数组,包含两个不同的周期值
// 1000-1 = 999,对应较短的定时周期
// 10000-1 = 9999,对应较长的定时周期
uint32_t tim1_arr[2] = {1000-1, 10000-1};
// 定义模式变量并初始化为0,用于在两种定时周期间切换
uint8_t mode = 0;
// 声明系统时钟配置函数
void SystemClock_Config(void);
int main(void)
{
// 复位所有外设,初始化Flash接口和系统滴答定时器
HAL_Init();
// 配置系统时钟
SystemClock_Config();
// 初始化所有配置的外设
MX_GPIO_Init(); // 初始化GPIO
MX_TIM1_Init(); // 初始化定时器1
// 启动定时器1的中断模式
HAL_TIM_Base_Start_IT(&htim1);
while (1)
{
// 检测KEY_1按键是否被按下(高电平)
if(HAL_GPIO_ReadPin(GPIOA,KEY_1_Pin) == GPIO_PIN_SET)
{
// 切换模式(0变1,1变0)
mode = !mode;
// 停止定时器1的中断
HAL_TIM_Base_Stop_IT(&htim1);
// 根据当前模式设置定时器1的自动重装载值
// mode=0时使用tim1_arr[0]=999,mode=1时使用tim1_arr[1]=9999
__HAL_TIM_SET_AUTORELOAD(&htim1, tim1_arr[mode]);
// 将定时器1的计数器清零
__HAL_TIM_SET_COUNTER(&htim1, 0);
// 重新启动定时器1的中断
HAL_TIM_Base_Start_IT(&htim1);
// 按键消抖:等待按键释放
while(HAL_GPIO_ReadPin(GPIOA,KEY_1_Pin) == GPIO_PIN_SET);
}
}
}
5. 使用PWM实现舵机控制
// 定义SG90舵机PWM脉宽范围(单位:微秒)
#define SG90_MIN 500 // 舵机-90°位置对应的脉宽(0.5ms)
#define SG90_MAX 2500 // 舵机+90°位置对应的脉宽(2.5ms)
#define SG90_STEP 500 // 舵机每次转动的步进值(约45°)
// 定义当前舵机角度对应的PWM脉宽值,初始化为500(-90°位置)
uint16_t sg90_angle = 500;
// 声明系统时钟配置函数
void SystemClock_Config(void);
int main(void)
{
// 复位所有外设
HAL_Init();
// 配置系统时钟
SystemClock_Config();
// 初始化所有配置的外设
MX_GPIO_Init(); // 初始化GPIO
MX_TIM3_Init(); // 初始化定时器3(用于PWM输出)
// 启动TIM3通道3的PWM输出(对应引脚PB0)
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3);
// 初始化舵机角度到-90°位置
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, sg90_angle);
while (1)
{
// 检测KEY1按键是否按下(增加角度)
if(HAL_GPIO_ReadPin(GPIOA, KEY1_Pin) == GPIO_PIN_SET)
{
// 检查是否超过最大角度限制
if(sg90_angle + SG90_STEP <= SG90_MAX)
{
// 增加舵机角度(增加PWM脉宽)
sg90_angle += SG90_STEP;
}
// 更新PWM比较值,改变舵机角度
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, sg90_angle);
// 延时300ms,确保舵机有足够时间转动到位
HAL_Delay(300);
// 等待按键释放(消抖)
while(HAL_GPIO_ReadPin(GPIOA, KEY1_Pin) == GPIO_PIN_SET);
}
// 检测KEY2按键是否按下(减少角度)
// 注意:这里有错误,应该检测GPIOC,KEY2_Pin,但等待释放时检测的是KEY1
if(HAL_GPIO_ReadPin(GPIOC, KEY2_Pin) == GPIO_PIN_SET)
{
// 检查是否超过最小角度限制
if(sg90_angle - SG90_STEP >= SG90_MIN)
{
// 减少舵机角度(减少PWM脉宽)
sg90_angle -= SG90_STEP;
}
// 更新PWM比较值,改变舵机角度
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, sg90_angle);
// 延时300ms,确保舵机有足够时间转动到位
HAL_Delay(300);
// 等待按键释放(消抖)- 这里有错误,应该等待KEY2释放而不是KEY1
while(HAL_GPIO_ReadPin(GPIOA, KEY1_Pin) == GPIO_PIN_SET);
}
}
}
6. 使用PWM实现呼吸灯
// 定义PWM占空比的值,对应CCR寄存器的值,初始为0(最低亮度)
uint16_t pwm_val = 0;
// 定义亮度变化方向:10表示变亮,-10表示变暗,初始为变亮方向
int8_t dir = 10;
// 声明系统时钟配置函数
void SystemClock_Config(void);
int main(void)
{
// 复位所有外设,初始化Flash接口和系统滴答定时器
HAL_Init();
// 配置系统时钟
SystemClock_Config();
// 初始化所有配置的外设
MX_GPIO_Init(); // 初始化GPIO
MX_TIM2_Init(); // 初始化定时器2(用于PWM输出)
// 启动TIM2的三个PWM通道
// 通道2、3、4分别控制不同的LED(可能是RGB三色LED)
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4);
while (1)
{
// 设置三个PWM通道的比较值(占空比),控制LED亮度
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, pwm_val);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, pwm_val);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_4, pwm_val);
// 根据方向更新PWM值
pwm_val += dir;
// 边界检查:当PWM值达到最大值1000时,改变方向为减小(变暗)
if(pwm_val >= 1000)
dir = -10;
// 边界检查:当PWM值达到最小值0时,改变方向为增加(变亮)
if(pwm_val <= 0)
dir = 10;
// 延时5ms,控制呼吸灯变化速度
HAL_Delay(5);
}
}
7. 使用TIM触发ADC采集
// 定义变量存储ADC原始值
uint32_t adc_val = 0;
// 定义变量存储计算出的电压值
float voltage = 0.0f;
// 定义变量存储计算出的温度值
float tem = 0.0f;
// 声明系统时钟配置函数
void SystemClock_Config(void);
int main(void)
{
// 复位所有外设,初始化Flash接口和系统滴答定时器
HAL_Init();
// 配置系统时钟
SystemClock_Config();
// 初始化所有配置的外设
MX_GPIO_Init(); // 初始化GPIO
MX_ADC1_Init(); // 初始化ADC1(用于读取内部温度传感器)
MX_TIM3_Init(); // 初始化定时器3
MX_USART1_UART_Init(); // 初始化USART1串口(用于输出温度数据)
// 开始ADC1的校准,提高ADC精度
HAL_ADCEx_Calibration_Start(&hadc1);
// 启动ADC1转换
HAL_ADC_Start(&hadc1);
// 启动定时器3(可能用于其他功能或定时触发ADC)
HAL_TIM_Base_Start(&htim3);
// 延时3秒,等待系统稳定
HAL_Delay(3000);
while (1)
{
/* USER CODE END WHILE */
// 等待ADC转换完成,使用最大超时时间
if(HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY) == HAL_OK)
{
// 获取ADC转换结果(12位分辨率,范围0-4095)
adc_val = HAL_ADC_GetValue(&hadc1);
// 将ADC原始值转换为电压值(假设参考电压为3.3V)
// 公式:电压 = (ADC值 / 最大ADC值) × 参考电压
voltage = (float)adc_val / 4095 * 3.3f;
// 根据STM32内部温度传感器特性计算温度
// 公式:温度 = (1.43 - 电压) / 0.0043 + 25
// 其中:1.43V是25°C时的典型电压,0.0043是温度系数(V/°C)
tem = (1.43f - voltage) / 0.0043f + 25;
// 通过串口输出温度值,保留3位小数
printf("当前芯片内部温度为:%.3f\r\n", tem);
}
}
}
8. TIM编码器模式
// 定义编码器当前计数值
int16_t encoder_cnt = 0;
// 定义编码器上一次的计数值,用于检测变化
int16_t encoder_last = 0;
// 记录编码器停止移动的时间点
uint32_t no_move_time = 0;
// 编码器静止状态标志:0-运动中,1-已静止
uint8_t encoder_static = 0;
// 声明系统时钟配置函数
void SystemClock_Config(void);
int main(void)
{
// 复位所有外设,初始化Flash接口和系统滴答定时器
HAL_Init();
// 配置系统时钟
SystemClock_Config();
// 初始化所有配置的外设
MX_GPIO_Init(); // 初始化GPIO
MX_TIM3_Init(); // 初始化定时器3(配置为编码器模式)
MX_USART1_UART_Init(); // 初始化USART1串口(用于输出状态信息)
// 启动定时器3的编码器接口模式,同时使能两个通道
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
while (1)
{
// 获取编码器当前的计数值
encoder_cnt = __HAL_TIM_GET_COUNTER(&htim3);
// 检查编码器计数值是否发生变化(即编码器是否转动)
if(encoder_cnt != encoder_last)
{
// 重置静止标志,表示编码器正在运动
encoder_static = 0;
// 记录当前时间作为最后一次运动的时间
no_move_time = HAL_GetTick();
// 计算计数值的变化量
uint16_t diff = encoder_cnt - encoder_last;
// 判断编码器转动方向
if(__HAL_TIM_IS_TIM_COUNTING_DOWN(&htim3))
{
// 如果定时器在递减计数,表示逆时针转动
printf("逆时针运行:%d, 当前位置: %d\r\n", diff, encoder_cnt);
}
else
{
// 如果定时器在递增计数,表示顺时针转动
printf("顺时针运行:%d, 当前位置: %d\r\n", diff, encoder_cnt);
}
// 更新上一次的计数值
encoder_last = encoder_cnt;
}
else
{
// 编码器没有移动,检查是否静止超过2秒且尚未报告静止状态
if(HAL_GetTick() - no_move_time > 2000 && encoder_static == 0)
{
// 设置静止标志,避免重复报告
encoder_static = 1;
// 报告编码器已静止超过2秒
printf("当前编码器静止超过2s,当前位置: %d\r\n", encoder_cnt);
}
}
// 检测编码器的按键(SW)是否被按下(低电平有效)
if(HAL_GPIO_ReadPin(GPIOA, EC1_SW_Pin) == GPIO_PIN_RESET)
{
// 延时20ms进行按键消抖
HAL_Delay(20);
// 再次检测按键状态,确认按键确实被按下
if(HAL_GPIO_ReadPin(GPIOA, EC1_SW_Pin) == GPIO_PIN_RESET)
{
// 报告按键被按下
printf("sw按键被按下\r\n");
// 等待按键释放(防止连续触发)
while(HAL_GPIO_ReadPin(GPIOA, EC1_SW_Pin) == GPIO_PIN_RESET);
}
}
}
}
更多推荐
所有评论(0)