大连做网站电话,开发建设网站多久,设计师门户网站源码,开发公司网签的流程1. 从理论到实战#xff1a;为什么输入捕获和PWM模式是嵌入式开发的“左右手” 朋友们#xff0c;今天咱们来聊聊STM32通用定时器里最实用、也最容易让人迷糊的两个功能#xff1a;输入捕获和PWM模式。我干了这么多年嵌入式#xff0c;发现很多新手朋友一看到数据手册里那些…1. 从理论到实战为什么输入捕获和PWM模式是嵌入式开发的“左右手”朋友们今天咱们来聊聊STM32通用定时器里最实用、也最容易让人迷糊的两个功能输入捕获和PWM模式。我干了这么多年嵌入式发现很多新手朋友一看到数据手册里那些复杂的框图、一堆的寄存器头就大了。其实你完全可以把它们想象成你工具箱里的两把“瑞士军刀”一把用来“测量”输入捕获一把用来“控制”PWM输出。比如你想做个智能小车得用编码器测轮子转速吧这就是输入捕获的活儿。测出转速后你想用电机驱动板去控制轮子转快点或慢点这通常就需要PWM信号去调节——瞧这不就用上了吗所以这俩功能往往是成对出现的理解了它们你就能解决项目中一大半跟“时间”和“脉冲”相关的问题。我记得刚入行那会儿用51单片机测个脉冲宽度得小心翼翼地用外部中断配合定时器代码写起来啰嗦不说精度还很难保证。后来用上STM32的TIM定时器才发现原来硬件能帮我们做这么多事。输入捕获功能说白了就是定时器帮你“盯”着某个引脚一旦发现电平跳变比如从低变高就立刻把当前定时器计数器的值“咔嚓”一下存起来。这个值本质上就是时间。你连续抓两次跳变两个时间值一减脉冲的宽度或者周期不就出来了吗整个过程几乎不占用CPU精度直接由定时器的时钟决定72MHz的系统下理论精度能达到十几纳秒级别这比软件轮询不知道高到哪里去了。而PWM模式则是定时器“主动出击”的典范。你设定好一个周期ARR寄存器和一个“比较值”CCR寄存器定时器就会自动地、周而复始地让对应引脚输出高低电平。比较值决定了高电平的时间也就是占空比。这样一来你就能用数字信号模拟出“模拟量”的效果轻松控制LED亮度、电机转速、舵机角度等等。很多朋友觉得配置PWM输出挺复杂其实你只要抓住三个核心寄存器ARR决定频率、CCR决定占空比、以及CCMR里的模式位选PWM1还是PWM2剩下的初始化步骤都是套路化的。接下来我就用最直白的方式带大家手把手过一遍这两个功能的实战代码保准你看完就能在自己的板子上跑起来。2. 高精度脉冲宽度测量输入捕获功能全解析与代码实战2.1 输入捕获的“三板斧”滤波、边沿与通道映射想要用好输入捕获你得先理解它的工作流程我把它总结为“三板斧”。第一板斧是滤波。现实世界的信号可不干净比如按键抖动、电机换向产生的毛刺都可能被误认为是有效的边沿跳变。STM32的输入捕获内置了数字滤波器你可以把它理解成一个“投票器”。比如你设置滤波参数为“连续8次采样有效才确认”那么一个短暂的毛刺脉冲可能只持续了2个时钟周期就会被过滤掉只有稳定持续了足够时间的电平跳变才会被认可。这个功能在工业环境等干扰较大的场合特别有用能极大提高系统的抗干扰能力。配置滤波主要在TIMx_CCMR1/2寄存器的ICxF位需要结合输入信号的特性和定时器时钟来权衡。第二板斧是边沿检测。你到底想抓上升沿、下降沿还是双边沿都抓这由TIMx_CCER寄存器的CCxP和CCxNP位决定。比如测量一个高电平脉冲的宽度经典做法就是先配置为上升沿捕获捕获到后立刻在中断里切换为下降沿捕获等下降沿到来再次捕获两次捕获值之差就是高电平期间计数器的增量乘以计数周期就是脉宽。这里有个关键点定时器的计数器CNT是自由运行的只要定时器不停止它就会一直累加或按照设定模式循环。所以两次捕获的CNT值直接相减只有在CNT没有发生溢出回绕的情况下才是正确的脉宽。如果脉冲很长CNT可能已经溢出从0重新开始了这时候就需要我们在软件里对溢出次数进行计数补偿。第三板斧也是最容易绕晕的就是输入通道与捕获通道的映射关系。STM32的每个定时器有4个输入引脚TI1到TI4对应4个捕获/比较寄存器CCR1到CCR4。但它们不是简单的一对一通过TIMx_CCMRx寄存器的CCxS位你可以灵活配置。比如TI1引脚的信号可以映射到IC1捕获通道也可以映射到IC2捕获通道。这种“交叉连接”的设计有个绝妙的应用PWM输入模式。它允许你用一个引脚如TI1同时测量PWM信号的频率和占空比。原理是把TI1同时映射给IC1和IC2一个通道抓上升沿用于测周期另一个通道抓下降沿用于测高电平时间硬件还能自动复位计数器非常方便。这个我们后面会细说。2.2 手把手编码实现一个按键长按检测的输入捕获例程光说不练假把式咱们直接上代码。假设我们用TIM2的通道1对应PA0引脚来测量一个按键按下的持续时间即高电平脉宽。这里我们不使用复杂的PWM输入模式就用最基本的单通道捕获让大家把流程吃透。首先是GPIO和定时器的初始化。PA0要配置为浮空输入或者上拉输入具体看你的硬件电路。关键是定时器TIM2的初始化我们选择内部时钟72MHz为了能测量较长的脉宽我们需要给定时器分频。假设我们设置预分频器PSC7199那么计数时钟CK_CNT就是72MHz / (71991) 10KHz即每个计数代表0.1毫秒。自动重装载值ARR可以设为最大值0xFFFF65535这样计数器最多能计到6.5535秒才会溢出对于按键检测足够了。void TIM2_IC_Init(void) { // 1. 开启时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 2. 初始化GPIO PA0 为浮空输入 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 初始化定时器时基 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 0xFFFF; // ARR最大值 TIM_TimeBaseStructure.TIM_Prescaler 7199; // 10KHz计数频率 TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; // 时钟分频与滤波相关 TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; // 向上计数 TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // 4. 初始化输入捕获通道 TIM_ICInitTypeDef TIM_ICInitStructure; TIM_ICInitStructure.TIM_Channel TIM_Channel_1; // 通道1 TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; // 初始为上升沿捕获 TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; // 映射到TI1 TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; // 捕获不分频每个边沿都捕获 TIM_ICInitStructure.TIM_ICFilter 0x0; // 不滤波可根据需要调整 TIM_ICInit(TIM2, TIM_ICInitStructure); // 5. 开启捕获/比较中断和更新溢出中断 TIM_ITConfig(TIM2, TIM_IT_CC1 | TIM_IT_Update, ENABLE); // 6. 配置NVIC使能中断此处省略NVIC配置代码 // 7. 使能定时器 TIM_Cmd(TIM2, ENABLE); }接下来是中断服务函数里的逻辑这是核心。我们需要几个全局变量来记录状态和数值Capture_Stage记录当前是第几次捕获0-空闲1-已捕获上升沿2-已捕获下降沿Capture_Val_Rise和Capture_Val_Fall分别记录上升沿和下降沿的捕获值Timer_Overflow_Count记录定时器溢出的次数。volatile uint8_t Capture_Stage 0; volatile uint32_t Capture_Val_Rise 0, Capture_Val_Fall 0; volatile uint16_t Timer_Overflow_Count 0; void TIM2_IRQHandler(void) { // 处理更新溢出中断 if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { Timer_Overflow_Count; // 溢出次数加1 TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } // 处理捕获/比较1中断 if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { if (Capture_Stage 0) { // 第一次捕获上升沿 Capture_Val_Rise TIM_GetCapture1(TIM2); Capture_Val_Rise (uint32_t)Timer_Overflow_Count * 65536UL; // 补偿溢出 // 切换为下降沿捕获 TIM_OC1PolarityConfig(TIM2, TIM_ICPolarity_Falling); Capture_Stage 1; Timer_Overflow_Count 0; // 重置溢出计数为测量脉宽做准备 } else if (Capture_Stage 1) { // 第二次捕获下降沿 Capture_Val_Fall TIM_GetCapture1(TIM2); Capture_Val_Fall (uint32_t)Timer_Overflow_Count * 65536UL; // 计算脉宽单位0.1ms uint32_t PulseWidth Capture_Val_Fall - Capture_Val_Rise; // 这里你可以处理脉宽值比如判断长按短按 // 测量完成恢复为上升沿捕获等待下一次按键 TIM_OC1PolarityConfig(TIM2, TIM_ICPolarity_Rising); Capture_Stage 0; Timer_Overflow_Count 0; } TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); } }这个例程虽然简单但涵盖了输入捕获最核心的流程初始化、边沿切换、溢出处理。在实际项目中你可能还需要加入去抖处理可以用硬件滤波也可以用软件在第一次捕获后延时一小段时间再切换边沿以及超时处理防止按键一直按下导致程序卡在某个状态。3. PWM输入模式一键搞定频率与占空比测量3.1 硬件自动化的魅力PWM输入模式工作原理刚才我们用了两个步骤上升沿、下降沿和一个通道才测出脉宽要测周期还得再抓一个上升沿。对于标准的PWM信号STM32提供了一个更强大的“一键全自动”方案——PWM输入模式。这个模式是输入捕获的一个特例它巧妙地利用了之前提到的通道交叉映射和定时器的从模式控制器。它的工作流程堪称精妙首先你将一个输入信号比如TI1同时映射到两个捕获通道IC1和IC2上。然后你把IC1配置为上升沿捕获IC2配置为下降沿捕获。最关键的一步来了你配置从模式控制器让TI1的上升沿即IC1的捕获事件同时作为复位触发源。这意味着每次在TI1上检测到上升沿时硬件会自动把计数器CNT清零这样一来IC1每次捕获到的值实际上就是从上一次上升沿到这一次上升沿的计数值也就是一个完整的PWM周期。而IC2在下降沿捕获到的值就是从本次上升沿到接下来的下降沿的计数值也就是高电平的宽度。频率和占空比直接通过两个捕获寄存器的值就能算出来完全由硬件自动完成软件只需要在两次捕获完成后去读取CCR1和CCR2即可效率极高精度也完全由硬件保证。3.2 实战配置测量舵机PWM信号假设我们要用TIM3的通道1对应PA6引脚来测量一个舵机的控制PWM信号标准舵机信号是周期20ms高电平脉宽0.5ms-2.5ms。使用PWM输入模式可以让我们同时得到周期和脉宽从而判断舵机目标角度。配置步骤如下这里会用到标准外设库的函数void TIM3_PWMI_Init(void) { // 1. 开启时钟初始化GPIO PA6为输入略 // 2. 初始化时基单元主要目的是设置时钟分频降低计数频率以扩大测量范围 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 0xFFFF; // ARR设为最大 TIM_TimeBaseStructure.TIM_Prescaler 71; // 72MHz / (711) 1MHz计数频率1个计数1us TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 3. 配置PWM输入模式 // 注意库函数TIM_PWMIConfig会一次性配置两个通道 TIM_ICInitTypeDef TIM_ICInitStructure; TIM_ICInitStructure.TIM_Channel TIM_Channel_1; // 选择通道1 TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; // 对于主通道IC1这里设上升沿 TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; // 映射到TI1 TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0; // 根据信号质量设置滤波 // 这个函数会智能配置将TI1映射到IC1和IC2IC1为上升沿IC2为下降沿并设置从模式为复位模式 TIM_PWMIConfig(TIM3, TIM_ICInitStructure); // 4. 选择有效的输入触发源TI1FP1和从模式 TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1); // 触发源选TI1FP1 TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset); // 从模式设为复位 // 5. 开启捕获中断可以只开IC1或IC2因为它们是同时触发的 TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE); // 6. 使能定时器 TIM_Cmd(TIM3, ENABLE); }配置完成后当有PWM信号输入PA6引脚硬件就会自动工作。在中断服务函数里我们读取两个捕获寄存器的值void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_CC1) ! RESET) { // 读取周期CCR1的值 uint16_t Period TIM_GetCapture1(TIM3); // 单位微秒计数 // 读取高电平脉宽CCR2的值 uint16_t Pulse TIM_GetCapture2(TIM3); // 单位微秒计数 // 计算实际时间 float Period_ms Period / 1000.0; // 转换为毫秒 float Pulse_ms Pulse / 1000.0; // 转换为毫秒 // 计算占空比 float DutyCycle (float)Pulse / Period * 100.0; // 这里可以根据Pulse_ms判断舵机角度... // 例如Pulse_ms1.5ms 对应中间角度 TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); } }你看用了PWM输入模式软件逻辑变得异常简洁。所有的时序控制、计数器复位都由硬件完成CPU只需要在捕获完成后进行简单的计算即可。这种“硬件自动化”的思想在STM32的许多高级外设中都有体现能极大地提高系统效率和可靠性。4. 输出比较与PWM生成从呼吸灯到电机控制4.1 PWM输出配置详解关键参数与寄存器说完了“测量”咱们再来看看“控制”的利器——PWM输出。配置一个PWM输出你主要跟三个参数打交道频率、占空比和极性。频率由定时器的时钟和自动重装载寄存器ARR共同决定。公式是PWM频率 定时器时钟 / ((ARR 1) * (PSC 1))。比如定时器时钟72MHz预分频PSC71ARR999那么PWM频率就是72MHz / ((711)*(9991)) 1000Hz。占空比则由捕获/比较寄存器CCRx决定。在PWM模式下CCRx的值就是那个“比较值”当计数器CNT小于CCRx时输出一种电平大于CCRx但小于ARR时输出另一种电平。占空比 CCRx / (ARR 1)。极性决定了有效电平是高还是低。通过TIMx_CCER寄存器的CCxP位配置。比如控制一个共阳极LED低电平才点亮那么你可能需要设置PWM模式为有效电平低这样占空比越大低电平时间越长LED反而越暗。配置寄存器时主要关注三个TIMx_CCMRx模式选择、TIMx_CCER输出使能与极性、TIMx_CCRx比较值。在TIMx_CCMRx中OCxM位设置为110PWM模式1或111PWM模式2两者的区别在于输出电平比较逻辑相反。OCxPE位建议使能这样你对CCRx的写入会先进入预装载寄存器等到下次更新事件时才生效可以避免在PWM周期中间修改占空比导致输出毛刺。4.2 经典案例实现一个平滑的呼吸灯效果呼吸灯是学习PWM的“Hello World”。我们使用TIM4的通道1对应PB6引脚来驱动一个LED。目标是让LED亮度从暗到亮再从亮到暗循环往复。void TIM4_PWM_Init(void) { // 1. 开启时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 2. 初始化GPIO PB6 为复用推挽输出 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 3. 初始化定时器时基 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 999; // ARR 999 TIM_TimeBaseStructure.TIM_Prescaler 71; // PSC 71 // 计算PWM频率 72M / ((711)*(9991)) 1000Hz TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM4, TIM_TimeBaseStructure); // 4. 初始化PWM输出通道 TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; // 模式1 TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; // 输出使能 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; // 输出极性高 // 初始占空比设为0LED最暗假设LED阴极接PB6阳极接VCC TIM_OCInitStructure.TIM_Pulse 0; // 这就是CCR1的初始值 TIM_OC1Init(TIM4, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable); // 使能预装载 // 5. 使能ARR的预装载 TIM_ARRPreloadConfig(TIM4, ENABLE); // 6. 使能定时器 TIM_Cmd(TIM4, ENABLE); }初始化完成后我们只需要在一个循环里或者用定时器中断不断修改CCR1的值就能改变亮度。为了达到平滑的呼吸效果我们需要让占空比缓慢变化。// 全局变量用于控制呼吸方向 uint16_t pwmVal 0; uint8_t dir 1; // 1:渐亮0:渐暗 // 在主循环或一个定时器中断中调用 void Breath_LED_Update(void) { if (dir) { pwmVal; if (pwmVal 1000) { // ARR是999这里用1000作为上限 dir 0; } } else { pwmVal--; if (pwmVal 0) { dir 1; } } // 更新比较值改变占空比 TIM_SetCompare1(TIM4, pwmVal); // 可以加一个小的延时控制呼吸速度 Delay_ms(1); }这个例子中我们通过软件线性地改变CCR1的值。更高级的做法是利用定时器自身的更新中断在中断里更新CCR1这样可以做到非常精确和稳定的亮度变化曲线甚至可以实现正弦波等非线性呼吸效果。PWM输出的精度和稳定性使得它在电机驱动如直流电机调速、步进电机细分、开关电源、DAC模拟等场合大放异彩。当你需要用一个数字引脚去控制一个“模拟量”时PWM几乎总是第一选择。