公司动态

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

📅 2026/8/5 7:06:52
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个高级定时器TIM14个通用定时器TIM2、TIM3、TIM4TIM5在某些C8T6封装上可能被阉割需查具体型号和2个基本定时器TIM6、TIM7。选型时简单的延时或PWM用通用定时器涉及电机全桥驱动必须用高级定时器纯粹高精度时基用基本定时器。2.2 核心时钟树定时器的脉搏从哪里来定时器之所以能“定时”根本在于它有时钟源在“滴答”驱动计数器。STM32的时钟树有点复杂但理解定时器的时钟路径是关键。定时器的时钟源CK_PSC可以来自内部时钟CK_INT最常用的方式。来自APB总线时钟。这里有个重要细节当APB预分频系数为1时定时器时钟等于APB时钟当APB预分频系数不为1通常为2、4、8、16时定时器时钟会是APB时钟的2倍。例如系统时钟72MHzAPB1总线时钟最大36MHz如果APB1预分频设为2则APB1时钟为36MHz而挂载在APB1下的定时器如TIM2-4实际得到的时钟是72MHz这是为了兼容性设计但很多人在计算定时时间时会在这里出错。外部时钟模式1来自定时器自身输入通道引脚TIx用于外部事件计数或测频。外部时钟模式2来自外部触发输入引脚ETR可用于同步多个定时器。内部触发输入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中断Tout0.001s系统时钟72MHzAPB1分频为2故TIM2时钟为72MHz。设置PSC7199则CK_CNT 72MHz / (71991) 10kHz。再设置ARR9则Tout (91) * (71991) / 72MHz 10 * 7200 / 72,000,000 0.001s。ARR和PSC的值都要在初始化时设定好。3. 通用定时器四大工作模式深度解析与实操3.1 定时中断模式从基础延时到系统时基这是定时器最经典的应用。我们通过配置ARR和PSC设定一个固定的周期使能更新中断然后在中断服务函数里执行特定任务。标准库V3.5实现步骤使能时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);初始化时基结构体TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; TIM_TimeBaseInitStruct.TIM_Period 999; // ARR值计数到1000溢出 TIM_TimeBaseInitStruct.TIM_Prescaler 7199; // PSC值72MHz/720010kHz TIM_TimeBaseInitStruct.TIM_ClockDivision TIM_CKD_DIV1; // 时钟分频与输入滤波相关通常设为DIV1 TIM_TimeBaseInitStruct.TIM_CounterMode TIM_CounterMode_Up; // 向上计数模式 TIM_TimeBaseInit(TIM2, TIM_TimeBaseInitStruct);此时定时周期 (9991)*(71991)/72MHz 0.1秒。使能更新中断TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);配置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);使能定时器TIM_Cmd(TIM2, ENABLE);编写中断服务函数在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库的中断处理逻辑已经包含了标志位清除用户只需重写回调函数即可。实操心得中断服务函数必须“快进快出”避免在中断内进行复杂运算、延时或等待。如果任务耗时可以设置标志位在主循环中处理。清除中断标志是铁律在标准库中如果你忘记调用TIM_ClearITPendingBit程序会不断进入中断导致系统卡死。在HAL库中虽然库函数帮你做了但也要知道有这个机制。计算ARR和PSC的窍门先根据所需定时周期和时钟频率确定计数器频率CK_CNT 1/Tout。然后CK_CNT Tclk / (PSC1)。为了得到整数分频PSC Tclk / CK_CNT - 1。ARR 定时周期 * CK_CNT - 1。尽量让PSC和ARR的值在合理范围内不要都取很小或很大以提高灵活性并减少误差。3.2 PWM输出模式驱动舵机、控制LED亮度、电机调速PWM脉冲宽度调制是定时器输出比较模式的一个典型应用。通过调节CCR捕获/比较寄存器的值来改变一个周期内高电平的时间从而改变占空比。标准库配置PWM输出以TIM3_CH2即PB5为例初始化时基结构体同上设定ARR决定PWM频率PSC决定计时精度。PWM频率 CK_CNT / (ARR 1)。例如CK_CNT72MHz/720100kHzARR999则PWM频率为100Hz。初始化输出比较结构体TIM_OCInitTypeDef TIM_OCInitStruct; TIM_OCInitStruct.TIM_OCMode TIM_OCMode_PWM1; // 模式1CNTCCR时有效电平 TIM_OCInitStruct.TIM_OutputState TIM_OutputState_Enable; // 使能输出 TIM_OCInitStruct.TIM_OCPolarity TIM_OCPolarity_High; // 输出极性高电平为有效电平 TIM_OCInitStruct.TIM_Pulse 500; // 设置初始CCR值即初始脉冲宽度。占空比 CCR/(ARR1) TIM_OC2Init(TIM3, TIM_OCInitStruct); // 初始化通道2使能预装载寄存器可选但推荐TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);这样CCR的修改会在下次更新事件生效避免当前周期出现毛刺。使能定时器TIM_Cmd(TIM3, ENABLE);使能主输出高级定时器需要对于TIM1/TIM8需要额外调用TIM_CtrlPWMOutputs(TIM1, ENABLE);。动态修改占空比在程序运行中通过TIM_SetCompare2(TIM3, newCCRValue);来改变占空比。关键参数解析PWM模式1 vs 模式2主要区别在于有效电平的定义。模式1向上计数时CNTCCR为有效电平向下计数时CNTCCR为有效电平。模式2则相反。结合TIM_OCPolarity可以灵活定义高低电平何为“有效”。输出极性TIM_OCPolarity_High表示比较匹配时输出高电平有效电平为高。如果你接的负载是低电平有效比如某些LED模块共阳极接法可以设为TIM_OCPolarity_Low。预装载机制强烈建议使能。当你需要平滑改变PWM占空比时它保证修改的CCR值只在下一个周期开始应用防止在当前周期中间突然改变导致脉冲畸形。3.3 输入捕获模式精准测量脉冲宽度与频率输入捕获用于测量外部信号的脉宽或周期。其原理是当捕获引脚上发生预设的边沿上升沿或下降沿时硬件会自动将当前计数器CNT的值锁存到对应的CCR寄存器中并可以产生中断。通过记录两次捕获值例如一次上升沿一次下降沿其差值乘以计数周期就是脉宽。测量高电平脉宽的标准库步骤以TIM2_CH1PA0为例初始化时基结构体重点在于设置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);初始化输入捕获结构体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);使能捕获中断和更新中断TIM_ITConfig(TIM2, TIM_IT_CC1 | TIM_IT_Update, ENABLE); // CC1是捕获中断Update是溢出中断配置NVIC并启动定时器。在中断服务函数中编写状态机逻辑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 * (PSC1) / Tclk TIM_OC1PolarityConfig(TIM2, TIM_ICPolarity_Rising); // 改回上升沿准备下一次测量 captureFlag 0; } TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); } }注意事项滤波与分频TIM_ICFilter可以设置输入滤波用于消除毛刺值越大滤波时间常数越大。TIM_ICPrescaler是捕获分频可以设置为每N个事件捕获一次用于降低中断频率。溢出处理当脉冲宽度可能超过定时器ARR值时必须使能更新中断并在中断中记录溢出次数最终脉宽 溢出次数 * (ARR1) 两次捕获值之差。上面的示例代码简化了溢出处理实际项目需要更严谨。输入映射TIM_ICSelection很重要。DirectTI表示通道信号直接连接到对应的TIx输入。IndirectTI则表示交叉连接。具体要看数据手册的定时器输入框图。3.4 编码器接口模式读取电机转速与方向这是定时器硬件集成的强大功能用于直接读取正交编码器的输出。两颗霍尔传感器输出相位差90度的A、B相脉冲。编码器模式会自动根据A、B相的相位关系控制计数器向上或向下计数无需CPU干预。标准库配置编码器模式以TIM3为例A相PA6B相PA7GPIO初始化将两个引脚配置为浮空输入或上拉输入。定时器时基初始化此时ARR通常设置为编码器最大计数值如65535PSC通常为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);配置编码器接口TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);TIM_EncoderMode_TI12使用TI1和TI2两个通道。还有TIM_EncoderMode_TI1或TIM_EncoderMode_TI2只使用单个通道但会丢失方向信息或分辨率减半。后两个参数是通道1和通道2的极性。通常都设为TIM_ICPolarity_Rising表示在上升沿计数。如果电机方向反了可以尝试交换A、B相接线或者将其中一个极性改为TIM_ICPolarity_Falling。设置输入捕获参数可选但推荐配置滤波和分频。TIM_ICInitTypeDef TIM_ICInitStruct; TIM_ICInitStruct.TIM_ICFilter 0x6; // 适当滤波消除抖动 TIM_ICInit(TIM3, TIM_ICInitStruct); // 需要分别配置两个通道代码略使能定时器TIM_Cmd(TIM3, ENABLE);读取位置与速度位置直接读取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为例控制半桥时基与输出通道初始化与通用定时器PWM初始化类似但需要初始化两个通道CH1和CH1N。关键配置刹车和死区寄存器TIMx_BDTRTIM_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_OCIdleState和TIM_OCNIdleState配置实现硬件级保护。使能主输出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也会同时启动。用定时器触发ADCTIM2产生一个PWM用其更新事件或比较匹配事件作为TRGO连接到ADC的外部触发源。这样可以实现与PWM波精确同步的ADC采样在电机控制中用于电流采样非常有用。配置示例TIM2主TIM3从同步启动配置主定时器TIM2正常初始化并设置主模式将更新事件作为触发输出。TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); // 设置更新事件为TRGO配置从定时器TIM3正常初始化时基。然后配置触发源和从模式。TIM_SelectInputTrigger(TIM3, TIM_TS_ITR1); // 选择内部触发源1对应TIM2 TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Trigger); // 设置为触发从模式启动只需要使能主定时器TIM2TIM3会自动被触发启动。4.3 定时器与DMA联动实现无CPU干预的数据搬运这是实现高效数据流处理的高级技巧。例如你想产生一个复杂的、预先计算好的PWM波形序列或者连续捕获大量脉冲宽度数据。如果每个点都靠CPU修改CCR或读取CCR会占用大量资源。此时可以让DMA来帮忙。应用一PWM DMA模式如播放LED呼吸灯序列在内存中定义一个数组uint16_t pwmDataBuffer[100];里面存放了100个不同的CCR值。配置定时器为PWM输出模式。配置DMA通道将内存地址pwmDataBuffer作为源地址定时器的CCR寄存器地址TIM3-CCR1作为目标地址。传输方向为内存到外设。配置定时器使其更新事件UEV作为DMA请求源。启动DMA和定时器。每次定时器溢出产生更新事件时DMA会自动将下一个数据从内存搬运到CCR寄存器从而自动改变PWM占空比CPU完全不用管。应用二输入捕获DMA模式高速连续记录配置定时器为输入捕获模式。配置DMA通道将定时器的CCR寄存器地址作为源地址一个内存数组作为目标地址。传输方向为外设到内存。配置定时器使其捕获比较事件CC1作为DMA请求源。启动DMA和定时器。每次捕获到边沿时当前的CNT值已锁存到CCR会自动通过DMA存入内存数组。这样可以实现极高频率的连续捕获而不会因为中断处理延迟丢失数据。实操心得使用DMA时要特别注意DMA缓冲区的大小和循环模式/单次模式的选择。在PWM DMA中通常使用循环模式让波形循环播放。在捕获DMA中要确保缓冲区足够大并在半满或全满时产生DMA中断通知CPU来处理数据防止数据覆盖。5. 常见问题排查与调试技巧实录5.1 定时器不工作/无输出的经典排查流程时钟未使能这是最常见的新手错误。检查RCC_APBxPeriphClockCmd或HAL中的__HAL_RCC_TIMx_CLK_ENABLE()是否已调用。GPIO模式错误对于PWM输出或输入捕获对应的GPIO必须配置为复用推挽输出AF_PP或浮空输入IN_FLOATING/上拉输入IPU。使用标准库的GPIO_Init或HAL的HAL_GPIO_Init仔细检查。高级定时器未开启主输出对于TIM1/TIM8生成PWM必须调用TIM_CtrlPWMOutputs(TIMx, ENABLE)。ARR或PSC值为0ARR设置为0则计数器从0计数到0立即溢出行为可能异常。PSC设置为0则不分频。中断未正确配置如果依赖中断检查NVIC配置是否正确中断服务函数名是否拼写正确以及是否在向量表中。复用功能未映射对于某些引脚定时器功能是重映射的。检查GPIO_PinRemapConfig是否启用如果需要。例如TIM3_CH2默认在PB5但重映射后可能在PC7。使用调试器查看寄存器最直接的方法是在调试模式下如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 输入捕获测量值跳动大/不准信号噪声编码器或传感器信号有毛刺。解决增大输入捕获滤波器值TIM_ICFilter。这个值每增加1滤波时间会延长几个时钟周期可以有效滤除窄脉冲干扰。中断处理延迟在输入捕获中断中做了太多事情导致第二次边沿到来时中断还没处理完错过了捕获。解决中断函数只做最必要的记录保存CCR值、切换极性将复杂的计算如换算成时间、判断宽度放到主循环中基于标志位处理。未处理计数器溢出测量的脉冲宽度超过了定时器的ARR值即计数器溢出了一次或多次。解决务必使能更新中断TIM_IT_Update并在中断中用一个变量记录溢出次数。最终脉宽 溢出次数 * (ARR1) (第二次捕获值 - 第一次捕获值)。时钟精度如果对绝对时间精度要求高需检查外部晶振精度以及内部PLL的锁相环稳定性。对于us级测量使用内部RC振荡器HSI可能引入较大误差。5.4 编码器模式计数方向反了/计数次数不对方向反了交换电机的A、B相接线。或者在软件中交换两个通道的极性配置将一个通道的TIM_ICPolarity从Rising改为Falling。计数次数是预期的4倍这是正常现象。在TIM_EncoderMode_TI12双边沿计数模式下编码器每转动一个线距会在A、B相各产生一个上升沿和一个下降沿共4个边沿计数器会计数4次。这是4倍频提高了分辨率。如果你想要1倍频每个线距计1次需要使用TIM_EncoderMode_TI1或TIM_EncoderMode_TI2单边沿计数但会丢失一个方向的计数能力或分辨率减半。计数有漏码或跳变编码器信号质量差或转速过高超过定时器计数频率。解决增加输入滤波提高定时器计数频率减小PSC检查编码器供电和信号线是否受到干扰。5.5 软件定时器 vs 硬件定时器在小型系统或任务简单时很多人会用for循环或while循环来做软件延时。但对于STM32项目我强烈建议将硬件定时器作为唯一的精确延时和定时事件源。软件延时如HAL_Delay的问题阻塞CPU在延时期间CPU什么也做不了浪费资源。不精确容易受到中断干扰时间波动大。难以管理多个定时任务需要自己维护多个时间戳和状态机代码复杂易错。硬件定时器的优势精准由硬件时钟驱动精度高不受其他代码影响。不阻塞采用中断或DMA方式CPU在定时期间可以处理其他任务。多任务一个定时器通过多个比较通道或分时复用可以管理多个不同周期的定时任务。功能强大如前所述还能做PWM、输入捕获等。一个实用的做法是使用一个基本定时器如TIM6产生一个稳定的时基中断例如1ms在这个中断里维护一个全局的millis计数器类似Arduino的millis()函数。然后所有需要延时的任务都通过比较当前millis和任务设定的目标时间来判断是否到期。这样就实现了非阻塞的多任务定时调度这是许多简单RTOS或调度器的核心思想。调试定时器相关功能一个逻辑分析仪是性价比极高的工具。它可以直观地显示PWM波形、脉冲宽度、编码器信号相位关系是验证代码是否正确、硬件连接是否正常的利器。相比于示波器它在分析数字时序和协议时更加方便。