STM32定时器深度解析:从基础定时到PWM、编码器与电机控制实战

STM32定时器深度解析:从基础定时到PWM、编码器与电机控制实战

1. 项目概述:为什么STM32的定时器值得深挖?

如果你正在玩STM32F103C8T6,或者任何一款STM32,却还在用HAL_Delay或者软件循环来计时,那可能错过了这颗芯片一半以上的潜力。定时器,这个看似基础的片上外设,实际上是STM32从“能跑程序”升级到“能精准控制”的关键枢纽。我最初接触STM32时,也觉得定时器无非就是定个时、产生个PWM,但随着项目深入,从电机控制到通信同步,再到复杂的多任务调度,几乎每一个对时序有要求的场景,背后都站着定时器。

STM32F103C8T6这颗经典的“蓝桥杯”神片,虽然属于Cortex-M3内核的入门款,但其定时器系统却一点也不含糊。它内部集成了多达8个定时器,包括高级控制定时器(TIM1, TIM8)、通用定时器(TIM2-TIM5)和基本定时器(TIM6, TIM7)。每一种类型都有其独特的定位和能力边界。理解它们,就像是拿到了控制芯片内部“时钟脉搏”的遥控器。

这个内容的目标,就是带你彻底吃透STM32F103C8T6的定时器。我不会只停留在标准库或HAL库的函数调用层面,而是会深入到寄存器,结合原理图和代码,把定时器的工作模式、应用场景和那些容易踩坑的细节掰开揉碎讲清楚。无论你是想用PWM驱动舵机、用输入捕获测量脉冲宽度、用编码器模式读取电机转速,还是用定时器触发ADC实现精准采样,这里都会有从原理到代码的完整路径。我们不止要“会用”,更要“懂为什么这么用”,以及“怎么用得更稳”。

2. 定时器整体架构与核心设计思路

2.1 定时器家族分类与能力矩阵

拿到STM32F103C8T6的数据手册,翻到定时器章节,你可能会被一堆TIMx搞得眼花缭乱。别慌,我们可以按功能和复杂度把它们分成三大类,这直接决定了你的项目该选哪个。

第一类:基本定时器(TIM6, TIM7)这是定时器里的“基本功”。它们只有最核心的计时功能,即一个16位的自动重装载寄存器(ARR)和一个16位的预分频器(PSC)。它们没有外部引脚,不能产生PWM,也不能捕获输入。那它们有什么用?两个核心场景:一是为DAC(数模转换器)提供精确的触发时钟;二是作为纯粹的软件定时基准,比如为操作系统提供时基。它们的结构简单,中断源单一(仅更新中断),因此时序抖动最小,最适合对纯粹时间间隔精度要求极高的场合。

第二类:通用定时器(TIM2, TIM3, TIM4, TIM5)这是项目中的“多面手”和主力军。我们绝大部分功能都靠它们实现。以TIM2-TIM5为例,它们同样是16位(TIM2和TIM5是32位?这里有个常见误区:STM32F103C8T6的TIM2和TIM5是32位向上/向下计数器,但ARR和PSC寄存器仍然是16位的。32位指的是计数器的计数范围可以扩展到32位,通过级联等方式实现,但基础架构仍是16位定时器扩展而来)。它们拥有全部基本定时器功能,并大幅扩展:

  • 输入捕获:可以捕获外部引脚上的边沿信号,并记录下发生时的计数器值,用于测频率、脉宽。
  • 输出比较:可以比较计数器值与一个预设值,匹配时产生相应动作(如翻转引脚、产生中断),这是生成PWM、单脉冲的基础。
  • PWM生成:输出比较的特化应用,产生占空比可调的方波。
  • 编码器接口:可以直接连接正交编码器,硬件自动识别方向和计数,极大减轻CPU负担。
  • 支持内外时钟源选择、主从模式、触发同步等。

第三类:高级控制定时器(TIM1, TIM8)这是定时器里的“瑞士军刀”,在通用定时器基础上,为电机控制和数字电源等复杂应用量身定制。关键增强包括:

  • 互补输出带死区插入:这是驱动三相全桥电路(如驱动无刷电机)的必备功能,可以生成6路带可编程死区时间的PWM,防止上下桥臂直通短路。
  • 刹车输入:紧急情况下,可以通过一个外部引脚快速关闭所有PWM输出,确保系统安全。
  • 更灵活的重装载影子寄存器、重复计数器等。

对于STM32F103C8T6,它拥有1个高级定时器TIM1,4个通用定时器TIM2、TIM3、TIM4(TIM5在某些C8T6封装上可能被阉割,需查具体型号),和2个基本定时器TIM6、TIM7。选型时,简单的延时或PWM用通用定时器;涉及电机全桥驱动必须用高级定时器;纯粹高精度时基用基本定时器。

2.2 核心时钟树:定时器的脉搏从哪里来?

定时器之所以能“定时”,根本在于它有时钟源在“滴答”驱动计数器。STM32的时钟树有点复杂,但理解定时器的时钟路径是关键。定时器的时钟源(CK_PSC)可以来自:

  1. 内部时钟(CK_INT):最常用的方式。来自APB总线时钟。这里有个重要细节:当APB预分频系数为1时,定时器时钟等于APB时钟;当APB预分频系数不为1(通常为2、4、8、16)时,定时器时钟会是APB时钟的2倍。例如,系统时钟72MHz,APB1总线时钟最大36MHz,如果APB1预分频设为2,则APB1时钟为36MHz,而挂载在APB1下的定时器(如TIM2-4)实际得到的时钟是72MHz!这是为了兼容性设计,但很多人在计算定时时间时会在这里出错。
  2. 外部时钟模式1:来自定时器自身输入通道引脚(TIx),用于外部事件计数或测频。
  3. 外部时钟模式2:来自外部触发输入引脚(ETR),可用于同步多个定时器。
  4. 内部触发输入(ITRx):一个定时器可以作为另一个定时器的预分频器,实现定时器级联,扩展计时范围。

注意:在标准库中,通过TIM_InternalClockConfig()选择内部时钟;在HAL库中,通常在初始化结构体里配置ClockSource成员。务必在初始化定时器前,确认系统时钟和APB总线时钟已正确配置(使用SystemInit()或HAL的时钟配置函数)。

2.3 定时器核心寄存器组解读

无论用库函数还是寄存器,底层都是在操作以下几组核心寄存器。了解它们,调试时看寄存器值会心里有数:

  • TIMx_CR1(控制寄存器1):配置计数方向(向上、向下、中央对齐)、时钟分频、自动重装载预装载使能等。
  • TIMx_PSC(预分频器寄存器):决定对输入时钟CK_PSC进行多少分频。实际驱动计数器的时钟CK_CNT = CK_PSC / (PSC + 1)。这是一个16位寄存器,理论上分频系数可达65536。
  • TIMx_ARR(自动重装载寄存器):定时器的周期。计数器计数到ARR值(向上计数)或计数到0(向下计数)时,产生更新事件,并重载预装载值。ARR的大小决定了定时周期。
  • TIMx_CNT(计数器寄存器):当前计数值,可读可写。
  • TIMx_SR(状态寄存器):查看中断标志位,如更新中断标志(UIF)。
  • TIMx_DIER(中断使能寄存器):控制哪些事件可以产生中断,如更新中断使能(UIE)。
  • 对于输入捕获/输出比较:还有TIMx_CCMR1/2(捕获/比较模式寄存器)、TIMx_CCER(捕获/比较使能寄存器)、TIMx_CCR1-4(捕获/比较寄存器)。

定时时间的计算:这是最基础的公式。定时器溢出时间Tout = (ARR + 1) * (PSC + 1) / Tclk。其中Tclk是定时器实际时钟频率。例如,要产生1ms中断(Tout=0.001s),系统时钟72MHz,APB1分频为2,故TIM2时钟为72MHz。设置PSC=7199,则CK_CNT = 72MHz / (7199+1) = 10kHz。再设置ARR=9,则Tout = (9+1) * (7199+1) / 72MHz = 10 * 7200 / 72,000,000 = 0.001s。ARR和PSC的值都要在初始化时设定好。

3. 通用定时器四大工作模式深度解析与实操

3.1 定时中断模式:从基础延时到系统时基

这是定时器最经典的应用。我们通过配置ARR和PSC设定一个固定的周期,使能更新中断,然后在中断服务函数里执行特定任务。

标准库V3.5实现步骤:

  1. 使能时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
  2. 初始化时基结构体
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; TIM_TimeBaseInitStruct.TIM_Period = 999; // ARR值,计数到1000溢出 TIM_TimeBaseInitStruct.TIM_Prescaler = 7199; // PSC值,72MHz/7200=10kHz TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频,与输入滤波相关,通常设为DIV1 TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式 TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct);
    此时,定时周期 = (999+1)*(7199+1)/72MHz = 0.1秒。
  3. 使能更新中断
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
  4. 配置NVIC(嵌套向量中断控制器):设置中断优先级和使能。
    NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStruct);
  5. 使能定时器TIM_Cmd(TIM2, ENABLE);
  6. 编写中断服务函数:在stm32f10x_it.c中实现TIM2_IRQHandler
    void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { // 你的定时任务代码,例如翻转LED GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)(1-GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_0))); TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 清除中断标志!必须! } }

HAL库实现关键点:HAL库将步骤封装得更简洁,但原理一致。使用HAL_TIM_Base_Init()初始化,用HAL_TIM_Base_Start_IT()启动中断。中断回调函数是HAL_TIM_PeriodElapsedCallback()。需要注意的是,HAL库的中断处理逻辑已经包含了标志位清除,用户只需重写回调函数即可。

实操心得

  1. 中断服务函数必须“快进快出”:避免在中断内进行复杂运算、延时或等待。如果任务耗时,可以设置标志位,在主循环中处理。
  2. 清除中断标志是铁律:在标准库中,如果你忘记调用TIM_ClearITPendingBit,程序会不断进入中断,导致系统卡死。在HAL库中,虽然库函数帮你做了,但也要知道有这个机制。
  3. 计算ARR和PSC的窍门:先根据所需定时周期和时钟频率,确定计数器频率CK_CNT = 1/Tout。然后,CK_CNT = Tclk / (PSC+1)。为了得到整数分频,PSC = Tclk / CK_CNT - 1。ARR = 定时周期 * CK_CNT - 1。尽量让PSC和ARR的值在合理范围内(不要都取很小或很大),以提高灵活性并减少误差。

3.2 PWM输出模式:驱动舵机、控制LED亮度、电机调速

PWM(脉冲宽度调制)是定时器输出比较模式的一个典型应用。通过调节CCR(捕获/比较寄存器)的值来改变一个周期内高电平的时间,从而改变占空比。

标准库配置PWM输出(以TIM3_CH2,即PB5为例):

  1. 初始化时基结构体:同上,设定ARR决定PWM频率,PSC决定计时精度。PWM频率 = CK_CNT / (ARR + 1)。例如,CK_CNT=72MHz/720=100kHz,ARR=999,则PWM频率为100Hz。
  2. 初始化输出比较结构体
    TIM_OCInitTypeDef TIM_OCInitStruct; TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; // 模式1:CNT<CCR时有效电平 TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; // 使能输出 TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; // 输出极性:高电平为有效电平 TIM_OCInitStruct.TIM_Pulse = 500; // 设置初始CCR值,即初始脉冲宽度。占空比 = CCR/(ARR+1) TIM_OC2Init(TIM3, &TIM_OCInitStruct); // 初始化通道2
  3. 使能预装载寄存器(可选,但推荐):TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);这样CCR的修改会在下次更新事件生效,避免当前周期出现毛刺。
  4. 使能定时器TIM_Cmd(TIM3, ENABLE);
  5. 使能主输出(高级定时器需要):对于TIM1/TIM8,需要额外调用TIM_CtrlPWMOutputs(TIM1, ENABLE);
  6. 动态修改占空比:在程序运行中,通过TIM_SetCompare2(TIM3, newCCRValue);来改变占空比。

关键参数解析:

  • PWM模式1 vs 模式2:主要区别在于有效电平的定义。模式1:向上计数时,CNT<CCR为有效电平;向下计数时,CNT>CCR为有效电平。模式2则相反。结合TIM_OCPolarity可以灵活定义高低电平何为“有效”。
  • 输出极性TIM_OCPolarity_High表示比较匹配时输出高电平(有效电平为高)。如果你接的负载是低电平有效(比如某些LED模块共阳极接法),可以设为TIM_OCPolarity_Low
  • 预装载机制:强烈建议使能。当你需要平滑改变PWM占空比时,它保证修改的CCR值只在下一个周期开始应用,防止在当前周期中间突然改变导致脉冲畸形。

3.3 输入捕获模式:精准测量脉冲宽度与频率

输入捕获用于测量外部信号的脉宽或周期。其原理是:当捕获引脚上发生预设的边沿(上升沿或下降沿)时,硬件会自动将当前计数器CNT的值锁存到对应的CCR寄存器中,并可以产生中断。通过记录两次捕获值(例如一次上升沿,一次下降沿),其差值乘以计数周期就是脉宽。

测量高电平脉宽的标准库步骤(以TIM2_CH1,PA0为例):

  1. 初始化时基结构体:重点在于设置PSC,使得计数频率足够高,测量精度满足要求。例如,要测量us级脉宽,计数频率至少需要1MHz。同时,ARR要设置得足够大,以防在测量期间计数器溢出。
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; TIM_TimeBaseInitStruct.TIM_Period = 0xFFFF; // ARR设为最大,防止溢出干扰(简单处理) TIM_TimeBaseInitStruct.TIM_Prescaler = 71; // 72MHz/72 = 1MHz,一个计数对应1us TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct);
  2. 初始化输入捕获结构体
    TIM_ICInitTypeDef TIM_ICInitStruct; TIM_ICInitStruct.TIM_Channel = TIM_Channel_1; TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising; // 先捕获上升沿 TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI; // 映射到TI1输入 TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 每个边沿都捕获 TIM_ICInitStruct.TIM_ICFilter = 0x0; // 不滤波 TIM_ICInit(TIM2, &TIM_ICInitStruct);
  3. 使能捕获中断和更新中断
    TIM_ITConfig(TIM2, TIM_IT_CC1 | TIM_IT_Update, ENABLE); // CC1是捕获中断,Update是溢出中断
  4. 配置NVIC并启动定时器
  5. 在中断服务函数中编写状态机逻辑
    volatile uint32_t captureValue1 = 0, captureValue2 = 0; volatile uint8_t captureFlag = 0; // 0:等待上升沿, 1:已捕获上升沿等待下降沿 volatile uint32_t pulseWidth = 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { // 处理计数器溢出,如果脉宽可能很长,需要记录溢出次数 TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET) { if (captureFlag == 0) { // 第一次捕获,是上升沿 captureValue1 = TIM_GetCapture1(TIM2); TIM_OC1PolarityConfig(TIM2, TIM_ICPolarity_Falling); // 改为捕获下降沿 captureFlag = 1; } else { // 第二次捕获,是下降沿 captureValue2 = TIM_GetCapture1(TIM2); if (captureValue2 > captureValue1) { pulseWidth = (captureValue2 - captureValue1); // 单位是计数周期 } else { // 处理了计数器溢出,需要加上ARR值 pulseWidth = (0xFFFF - captureValue1 + captureValue2 + 1); } // 计算实际时间:pulseWidth_us = pulseWidth * (PSC+1) / Tclk TIM_OC1PolarityConfig(TIM2, TIM_ICPolarity_Rising); // 改回上升沿,准备下一次测量 captureFlag = 0; } TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); } }

注意事项

  1. 滤波与分频TIM_ICFilter可以设置输入滤波,用于消除毛刺,值越大,滤波时间常数越大。TIM_ICPrescaler是捕获分频,可以设置为每N个事件捕获一次,用于降低中断频率。
  2. 溢出处理:当脉冲宽度可能超过定时器ARR值时,必须使能更新中断并在中断中记录溢出次数,最终脉宽 = 溢出次数 * (ARR+1) + 两次捕获值之差。上面的示例代码简化了溢出处理,实际项目需要更严谨。
  3. 输入映射TIM_ICSelection很重要。DirectTI表示通道信号直接连接到对应的TIx输入。IndirectTI则表示交叉连接。具体要看数据手册的定时器输入框图。

3.4 编码器接口模式:读取电机转速与方向

这是定时器硬件集成的强大功能,用于直接读取正交编码器的输出。两颗霍尔传感器输出相位差90度的A、B相脉冲。编码器模式会自动根据A、B相的相位关系,控制计数器向上或向下计数,无需CPU干预。

标准库配置编码器模式(以TIM3为例,A相PA6,B相PA7):

  1. GPIO初始化:将两个引脚配置为浮空输入或上拉输入。
  2. 定时器时基初始化:此时ARR通常设置为编码器最大计数值(如65535),PSC通常为0(不分频),以获得最高的计数响应速度。
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; TIM_TimeBaseInitStruct.TIM_Period = 0xFFFF; TIM_TimeBaseInitStruct.TIM_Prescaler = 0; TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; // 在编码器模式下此设置可能被覆盖 TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);
  3. 配置编码器接口
    TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
    • TIM_EncoderMode_TI12:使用TI1和TI2两个通道。还有TIM_EncoderMode_TI1TIM_EncoderMode_TI2,只使用单个通道,但会丢失方向信息或分辨率减半。
    • 后两个参数是通道1和通道2的极性。通常都设为TIM_ICPolarity_Rising,表示在上升沿计数。如果电机方向反了,可以尝试交换A、B相接线,或者将其中一个极性改为TIM_ICPolarity_Falling
  4. 设置输入捕获参数(可选,但推荐):配置滤波和分频。
    TIM_ICInitTypeDef TIM_ICInitStruct; TIM_ICInitStruct.TIM_ICFilter = 0x6; // 适当滤波,消除抖动 TIM_ICInit(TIM3, &TIM_ICInitStruct); // 需要分别配置两个通道,代码略
  5. 使能定时器TIM_Cmd(TIM3, ENABLE);
  6. 读取位置与速度
    • 位置:直接读取TIM_GetCounter(TIM3)。注意计数器是16位的,可能会溢出,需要软件处理溢出(结合更新中断)。
    • 速度:常用方法是定时(比如用另一个定时器每10ms中断一次)读取计数器值,并清零计数器。差值即为这段时间内的脉冲数,除以时间得到频率,再根据编码器线数即可换算成转速。

关于极性选择的深度解析: 网络热词中提到“编码器模式下极性选择时直接翻转电平了吗”,这是一个很好的问题。在编码器模式下,TIM_ICPolarity的设置不会翻转输入引脚的实际电平。它控制的是定时器内部,哪个边沿(上升沿或下降沿)被用作有效的计数边沿。例如,TIM_ICPolarity_Rising意味着引脚上的上升沿触发计数操作。改变极性,就改变了计数发生的时机,从而影响了A、B相序的逻辑判断,最终可能表现为计数方向的反转。它是在信号进入边沿检测电路时起作用的,而不是对原始信号进行取反。

4. 高级定时器进阶应用与系统集成

4.1 互补PWM与死区插入:无刷电机驱动核心

高级定时器(TIM1, TIM8)最不可替代的功能就是生成带死区的互补PWM,这是驱动三相全桥逆变电路(驱动无刷直流电机或永磁同步电机)的基石。互补PWM是指一对PWM信号(如CH1和CH1N),它们通常是反相的。死区时间是在这对信号状态切换时插入的一段两者都为低电平(或无效电平)的时间,防止上下桥臂的开关管同时导通造成短路直通而烧毁。

标准库配置(以TIM1_CH1和CH1N为例,控制半桥):

  1. 时基与输出通道初始化:与通用定时器PWM初始化类似,但需要初始化两个通道(CH1和CH1N)。
  2. 关键:配置刹车和死区寄存器(TIMx_BDTR)
    TIM_BDTRInitTypeDef TIM_BDTRInitStruct; TIM_BDTRInitStruct.TIM_OSSRState = TIM_OSSRState_Enable; TIM_BDTRInitStruct.TIM_OSSIState = TIM_OSSIState_Enable; TIM_BDTRInitStruct.TIM_LOCKLevel = TIM_LOCKLevel_OFF; TIM_BDTRInitStruct.TIM_DeadTime = 0x5A; // 死区时间设置,具体值需计算 TIM_BDTRInitStruct.TIM_Break = TIM_Break_Enable; TIM_BDTRInitStruct.TIM_BreakPolarity = TIM_BreakPolarity_Low; // 刹车输入低电平有效 TIM_BDTRInitStruct.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable; TIM_BDTRInitStruct.TIM_BreakFilter = 0x0; TIM_BDTRConfig(TIM1, &TIM_BDTRInitStruct);
    • TIM_DeadTime:死区时间设置。这个寄存器的值(DTG[7:0])需要根据芯片数据手册中的公式计算。死区时间 = DTG值对应的固定时间 + DTG值 * Tdts。其中Tdts是定时器时钟经过一个特定分频后的周期。计算时必须仔细查阅数据手册的“死区时间生成”章节。
    • TIM_Break:刹车功能使能。当指定的刹车输入引脚(BKIN)有效时,所有输出通道会被强制置为安全状态(可通过TIM_OCIdleStateTIM_OCNIdleState配置),实现硬件级保护。
  3. 使能主输出TIM_CtrlPWMOutputs(TIM1, ENABLE);这一步至关重要,没有它高级定时器的PWM输出将全部无效。

4.2 定时器主从模式与触发同步:构建精密定时网络

在复杂系统中,可能需要多个定时器协同工作。例如,用一个定时器(主)周期性地触发另一个定时器(从)启动一次ADC转换,或者让两个定时器完全同步启动。这就是主从模式。

常见应用场景

  • 定时器同步启动:TIM2作为主,TIM3作为从。配置TIM2的更新事件(UEV)作为触发输出(TRGO),配置TIM3的触发源(TS)为ITR1(假设TIM2连接到TIM3的内部触发1),并设置TIM3为从模式下的触发模式(Slave Mode = Trigger Mode)。当TIM2使能或产生更新时,TIM3也会同时启动。
  • 用定时器触发ADC:TIM2产生一个PWM,用其更新事件或比较匹配事件作为TRGO,连接到ADC的外部触发源。这样可以实现与PWM波精确同步的ADC采样,在电机控制中用于电流采样非常有用。

配置示例(TIM2主,TIM3从,同步启动):

  1. 配置主定时器(TIM2):正常初始化,并设置主模式,将更新事件作为触发输出。
    TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); // 设置更新事件为TRGO
  2. 配置从定时器(TIM3):正常初始化时基。然后配置触发源和从模式。
    TIM_SelectInputTrigger(TIM3, TIM_TS_ITR1); // 选择内部触发源1,对应TIM2 TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Trigger); // 设置为触发从模式
  3. 启动:只需要使能主定时器TIM2,TIM3会自动被触发启动。

4.3 定时器与DMA联动:实现无CPU干预的数据搬运

这是实现高效数据流处理的高级技巧。例如,你想产生一个复杂的、预先计算好的PWM波形序列,或者连续捕获大量脉冲宽度数据。如果每个点都靠CPU修改CCR或读取CCR,会占用大量资源。此时可以让DMA来帮忙。

应用一:PWM DMA模式(如播放LED呼吸灯序列)

  1. 在内存中定义一个数组uint16_t pwmDataBuffer[100];,里面存放了100个不同的CCR值。
  2. 配置定时器为PWM输出模式。
  3. 配置DMA通道,将内存地址(pwmDataBuffer)作为源地址,定时器的CCR寄存器地址(&TIM3->CCR1)作为目标地址。传输方向为内存到外设。
  4. 配置定时器,使其更新事件(UEV)作为DMA请求源。
  5. 启动DMA和定时器。每次定时器溢出产生更新事件时,DMA会自动将下一个数据从内存搬运到CCR寄存器,从而自动改变PWM占空比,CPU完全不用管。

应用二:输入捕获DMA模式(高速连续记录)

  1. 配置定时器为输入捕获模式。
  2. 配置DMA通道,将定时器的CCR寄存器地址作为源地址,一个内存数组作为目标地址。传输方向为外设到内存。
  3. 配置定时器,使其捕获比较事件(CC1)作为DMA请求源。
  4. 启动DMA和定时器。每次捕获到边沿时,当前的CNT值(已锁存到CCR)会自动通过DMA存入内存数组。这样可以实现极高频率的连续捕获,而不会因为中断处理延迟丢失数据。

实操心得:使用DMA时,要特别注意DMA缓冲区的大小和循环模式/单次模式的选择。在PWM DMA中,通常使用循环模式,让波形循环播放。在捕获DMA中,要确保缓冲区足够大,并在半满或全满时产生DMA中断,通知CPU来处理数据,防止数据覆盖。

5. 常见问题排查与调试技巧实录

5.1 定时器不工作/无输出的经典排查流程

  1. 时钟未使能:这是最常见的新手错误。检查RCC_APBxPeriphClockCmd或HAL中的__HAL_RCC_TIMx_CLK_ENABLE()是否已调用。
  2. GPIO模式错误:对于PWM输出或输入捕获,对应的GPIO必须配置为复用推挽输出(AF_PP)或浮空输入(IN_FLOATING)/上拉输入(IPU)。使用标准库的GPIO_Init或HAL的HAL_GPIO_Init仔细检查。
  3. 高级定时器未开启主输出:对于TIM1/TIM8,生成PWM必须调用TIM_CtrlPWMOutputs(TIMx, ENABLE)
  4. ARR或PSC值为0:ARR设置为0,则计数器从0计数到0,立即溢出,行为可能异常。PSC设置为0,则不分频。
  5. 中断未正确配置:如果依赖中断,检查NVIC配置是否正确,中断服务函数名是否拼写正确,以及是否在向量表中。
  6. 复用功能未映射:对于某些引脚,定时器功能是重映射的。检查GPIO_PinRemapConfig是否启用(如果需要)。例如,TIM3_CH2默认在PB5,但重映射后可能在PC7。
  7. 使用调试器查看寄存器:最直接的方法是在调试模式下(如ST-Link + Keil/IAR),暂停程序,查看TIMx->CR1寄存器的值。确保CEN位(Counter ENable)为1。查看SR寄存器是否有标志位产生。

5.2 PWM输出异常(无输出、波形不对、占空比不可控)

现象可能原因排查方法
完全无输出GPIO模式错误;高级定时器未开主输出;该通道未使能输出(TIM_CCxCmd)。检查GPIO初始化;检查TIMx_BDTR的MOE位或调用TIM_CtrlPWMOutputs;检查TIMx_CCER寄存器的CCxE位。
输出恒定高/低电平CCR值设置极端(0或等于ARR);输出极性设置反了。动态修改CCR值,用示波器观察。尝试切换TIM_OCPolarity
频率不对ARR或PSC计算错误;定时器时钟源频率不对。重新计算Tout公式。检查系统时钟配置和APB分频。
占空比变化不线性未使能预装载寄存器,在错误的时间点修改了CCR。确保调用TIM_OCxPreloadConfig(TIMx, TIM_OCPreload_Enable),并在更新事件时修改CCR。
输出有毛刺开关其他GPIO或外设造成干扰;PCB布局问题。尝试在修改CCR的代码前后关中断;检查硬件电路,PWM走线远离敏感信号。

5.3 输入捕获测量值跳动大/不准

  1. 信号噪声:编码器或传感器信号有毛刺。解决:增大输入捕获滤波器值(TIM_ICFilter)。这个值每增加1,滤波时间会延长几个时钟周期,可以有效滤除窄脉冲干扰。
  2. 中断处理延迟:在输入捕获中断中做了太多事情,导致第二次边沿到来时,中断还没处理完,错过了捕获。解决:中断函数只做最必要的记录(保存CCR值、切换极性),将复杂的计算(如换算成时间、判断宽度)放到主循环中基于标志位处理。
  3. 未处理计数器溢出:测量的脉冲宽度超过了定时器的ARR值(即计数器溢出了一次或多次)。解决:务必使能更新中断(TIM_IT_Update),并在中断中用一个变量记录溢出次数。最终脉宽 = 溢出次数 * (ARR+1) + (第二次捕获值 - 第一次捕获值)。
  4. 时钟精度:如果对绝对时间精度要求高,需检查外部晶振精度以及内部PLL的锁相环稳定性。对于us级测量,使用内部RC振荡器(HSI)可能引入较大误差。

5.4 编码器模式计数方向反了/计数次数不对

  1. 方向反了:交换电机的A、B相接线。或者,在软件中交换两个通道的极性配置(将一个通道的TIM_ICPolarityRising改为Falling)。
  2. 计数次数是预期的4倍:这是正常现象。在TIM_EncoderMode_TI12(双边沿计数)模式下,编码器每转动一个线距,会在A、B相各产生一个上升沿和一个下降沿,共4个边沿,计数器会计数4次。这是4倍频,提高了分辨率。如果你想要1倍频(每个线距计1次),需要使用TIM_EncoderMode_TI1TIM_EncoderMode_TI2(单边沿计数),但会丢失一个方向的计数能力或分辨率减半。
  3. 计数有漏码或跳变:编码器信号质量差,或转速过高超过定时器计数频率。解决:增加输入滤波;提高定时器计数频率(减小PSC);检查编码器供电和信号线是否受到干扰。

5.5 软件定时器 vs 硬件定时器

在小型系统或任务简单时,很多人会用for循环或while循环来做软件延时。但对于STM32项目,我强烈建议将硬件定时器作为唯一的精确延时和定时事件源

软件延时(如HAL_Delay)的问题

  • 阻塞CPU:在延时期间,CPU什么也做不了,浪费资源。
  • 不精确:容易受到中断干扰,时间波动大。
  • 难以管理多个定时任务:需要自己维护多个时间戳和状态机,代码复杂易错。

硬件定时器的优势

  • 精准:由硬件时钟驱动,精度高,不受其他代码影响。
  • 不阻塞:采用中断或DMA方式,CPU在定时期间可以处理其他任务。
  • 多任务:一个定时器通过多个比较通道或分时复用,可以管理多个不同周期的定时任务。
  • 功能强大:如前所述,还能做PWM、输入捕获等。

一个实用的做法是:使用一个基本定时器(如TIM6)产生一个稳定的时基中断(例如1ms),在这个中断里维护一个全局的millis计数器(类似Arduino的millis()函数)。然后所有需要延时的任务,都通过比较当前millis和任务设定的目标时间来判断是否到期。这样就实现了非阻塞的多任务定时调度,这是许多简单RTOS或调度器的核心思想。

调试定时器相关功能,一个逻辑分析仪是性价比极高的工具。它可以直观地显示PWM波形、脉冲宽度、编码器信号相位关系,是验证代码是否正确、硬件连接是否正常的利器。相比于示波器,它在分析数字时序和协议时更加方便。