STM32定时器PWM输出与输入捕获全解析:从呼吸灯到信号测量
1. 项目概述:从信号生成到信号测量的闭环
在嵌入式开发,尤其是电机控制、LED调光、舵机驱动这些场景里,PWM(脉冲宽度调制)绝对是绕不开的核心技术。简单说,PWM就是通过快速开关来控制一个信号的平均电压或功率,开关时间的长短(即占空比)决定了最终输出的“力度”。但很多朋友在玩转STM32的PWM输出后,往往就止步了。你有没有想过,如何让单片机不仅能“发出”PWM信号,还能“读懂”外部设备发来的PWM信号呢?比如,你想用遥控器控制自己的小车,遥控器发出的就是PWM信号;或者你想测量一个风扇的转速,风扇的转速传感器输出的往往也是频率和占空比变化的PWM。这时候,PWM输入捕获功能就派上用场了。
这个项目,就是基于STM32的HAL库,手把手带你打通PWM的“输出”与“输入”这两个环节。我们会用同一个定时器,先把它配置成PWM输出模式,驱动一个LED实现呼吸灯效果,直观感受占空比的变化。然后,我们再把这个定时器重新配置成输入捕获模式,专门用来测量外部输入的PWM信号的频率和占空比。通过这一个完整的闭环实验,你不仅能掌握HAL库操作定时器的核心API,更能深刻理解定时器在不同工作模式下的底层逻辑,这对于构建复杂的控制系统至关重要。
2. 核心硬件与原理剖析
2.1 STM32定时器:多面手的核心
STM32的定时器(Timer)功能之强大,堪称外设中的“瑞士军刀”。对于PWM相关应用,我们主要关注通用定时器(如TIM2, TIM3, TIM4, TIM5)和高级定时器(如TIM1, TIM8)。它们都内置了专门的PWM生成和输入捕获电路。
PWM输出原理:定时器就像一个精准的秒表(计数器),它从0开始向上计数到我们设定的一个值(自动重装载值ARR),然后清零重新开始,如此循环。我们设定另一个值(比较寄存器CCRx),当计数器的值小于CCRx时,输出高电平;大于等于CCRx时,输出低电平。这样,在一个计数周期内,高电平的时间占比(即占空比)就等于CCRx / (ARR + 1)。通过改变CCRx的值,我们就能动态调整输出波形的占空比。ARR+1决定了PWM的频率,频率 = 定时器时钟源 / (ARR + 1)。
PWM输入捕获原理:这是测量外部PWM信号的高级模式。它需要用到定时器的两个输入通道(通常是CH1和CH2),并将其配置为“PWM输入模式”。在此模式下,两个通道一个(如CH1)被设置为上升沿触发,另一个(如CH2)被设置为下降沿触发,并且它们共同映射到同一个输入信号上。当信号上升沿到来时,计数器当前值被捕获到CCR1寄存器并清零计数器;当下降沿到来时,当前值被捕获到CCR2寄存器。这样,CCR1的值就代表了信号的一个完整周期(频率的倒数),CCR2的值则代表了高电平的时间。通过计算,我们就能得到频率和占空比。STM32的硬件直接完成了最关键的边沿捕获和计数器锁存,大大减轻了CPU负担并提高了测量精度。
2.2 硬件连接规划
为了完成这个闭环实验,我们需要进行简单的硬件连接。假设我们使用STM32F103C8T6核心板(蓝色药丸板),这是一个非常普及的型号。
- PWM输出部分:我们选择定时器TIM3的通道1(对应引脚PA6)作为PWM输出引脚。将一个LED的正极通过一个220Ω的限流电阻连接到PA6,负极连接到GND。这样,通过改变PWM占空比,就能控制LED的亮度。
- PWM输入部分:我们需要一个外部PWM信号源。这里有一个巧妙的“自环”测试方法:将刚才的PWM输出引脚(PA6),用一根杜邦线直接连接到另一个配置为输入捕获的引脚上。我们选择定时器TIM2的通道1(对应引脚PA0)作为输入捕获引脚。这样,单片机自己产生的PWM信号,又被自己测量回来,非常适合验证功能。
- 注意:更真实的场景是,PA0连接外部信号源(如遥控接收机、传感器等)。自环测试时,务必确保输出引脚和输入引脚属于不同的定时器,因为一个定时器在同一时刻只能工作于一种主模式(输出或输入)。
所需材料清单:
- STM32F103C8T6核心板 1个
- USB转串口模块(用于程序下载和调试打印) 1个
- LED(颜色不限) 1个
- 220Ω 电阻 1个
- 杜邦线 若干
- 面包板(可选,方便连接) 1块
3. 开发环境搭建与工程创建
3.1 工具链选择与配置
我习惯使用STM32CubeIDE,它是ST官方推出的免费集成开发环境,集成了CubeMX图形化配置工具和基于Eclipse的IDE,对HAL库支持最好,一站式解决所有问题。当然,你也可以使用Keil MDK或者IAR,配合独立的STM32CubeMX软件进行配置,步骤是相通的。
首先,打开STM32CubeIDE,新建一个STM32项目。选择你的芯片型号(例如STM32F103C8Tx)。在项目初始化阶段,CubeMX的图形界面会自动打开。
3.2 时钟树配置要点
系统时钟是定时器精准工作的基石。对于STM32F103,我们通常使用外部8MHz晶振(HSE),通过PLL倍频到72MHz作为系统时钟(SYSCLK)。
- 在Pinout & Configuration标签页,转到
System Core->RCC。 - 将
High Speed Clock (HSE)设置为Crystal/Ceramic Resonator。 - 转到
Clock Configuration标签页。你会看到一个复杂的时钟树图。 - 在输入频率部分,将HSE设置为8MHz。
- 找到PLL相关设置,将PLL源选择为HSE,PLL倍频因子设置为9。这样PLL输出就是 8MHz * 9 = 72MHz。
- 将SYSCLK的来源选择为PLL,这样系统时钟就是72MHz。
- 关键一步:找到定时器的时钟源APB1和APB2。对于F103,TIM2、3、4挂在APB1总线上,TIM1挂在APB2上。APB1的预分频器默认是2,所以APB1的时钟是36MHz。但是,当APB1预分频系数不为1时,连接到APB1的定时器会获得一个x2的时钟,因此TIM2的时钟实际是72MHz。同理,APB2时钟是72MHz,TIM1的时钟也是72MHz。这个细节直接影响后续ARR和PSC的计算,务必弄清楚。
注意:很多朋友计算出的PWM频率不对,问题十有八九出在时钟树配置上。一定要在Clock Configuration界面确认最终到达你所用定时器的“Timer clocks”频率是多少。CubeMX会帮你计算并显示出来,仔细核对。
3.3 GPIO与定时器外设初步配置
配置PWM输出引脚(PA6):
- 在芯片引脚图上找到PA6,左键点击,选择
TIM3_CH1。这表示将PA6复用为定时器3的通道1功能。 - 在左侧分类中找到
Timers->TIM3。 - 将
Clock Source设置为Internal Clock(内部时钟源)。 - 将
Channel1设置为PWM Generation CH1。 - 下方参数配置中:
Prescaler (PSC - 16 bits value):预分频器,设置为71。定时器时钟 = 72MHz / (71+1) = 1MHz。这个1MHz就是计数器递增的频率。Counter Mode:Up(向上计数)。Counter Period (AutoReload Register - 16 bits value):自动重装载值ARR,设置为999。这样,PWM周期 = (ARR+1) / 定时器时钟 = 1000 / 1MHz = 1ms,即PWM频率为1kHz。auto-reload preload:设置为Enable,这可以避免在更新ARR时产生毛刺。
- 切换到
Parameter Settings选项卡下的PWM Generation Channel 1子菜单:Pulse (16 bits value):脉冲宽度,即初始的CCR1值,先设为500,即50%占空比。CH Polarity:High,表示有效电平为高。即当计数器值小于CCR1时,输出高电平。
- 在芯片引脚图上找到PA6,左键点击,选择
配置PWM输入捕获引脚(PA0):
- 在芯片引脚图上找到PA0,左键点击,选择
TIM2_CH1。 - 在左侧找到
Timers->TIM2。 - 将
Clock Source设置为Internal Clock。 - 将
Channel1设置为Input Capture direct mode。但先别急,这不是PWM输入模式。STM32的“PWM输入模式”是一种特殊的映射模式,需要在代码中配置。 - 我们暂时先将其设为直接输入捕获模式,用于验证引脚功能。更复杂的PWM输入模式配置我们留在代码部分详细说明。
- 在芯片引脚图上找到PA0,左键点击,选择
配置串口用于调试打印(可选但强烈推荐):
- 找到USART1(通常PA9为TX,PA10为RX),将其模式设置为
Asynchronous(异步通信)。 - 在
Parameter Settings中,设置波特率为115200。
- 找到USART1(通常PA9为TX,PA10为RX),将其模式设置为
配置完成后,点击右上角的“GENERATE CODE”,让CubeIDE生成初始化代码。
4. PWM输出功能实现详解
4.1 代码生成与工程结构解析
代码生成后,我们主要关注三个文件:
main.c:主程序循环所在。tim.c:包含了TIM3和TIM2的初始化代码(MX_TIM3_Init等)。usart.c:包含了串口初始化代码。
首先,在main.c的main函数中,在MX_TIM3_Init()函数调用之后,我们需要启动PWM输出。
/* 启动TIM3的PWM输出 */ HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);这一行代码至关重要,它启动了定时器3的计数器,并开启了通道1的PWM输出。没有它,引脚上不会有任何波形。
4.2 呼吸灯效果实现:动态调整占空比
我们的目标是让LED像呼吸一样渐亮渐暗。这需要在一个循环中,动态地改变CCR1(即脉冲宽度)的值。
在main.c的while(1)主循环中,我们添加以下代码:
uint16_t pwmVal = 0; uint8_t dir = 1; // 方向,1为增加,0为减少 while (1) { // 更新PWM占空比 __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pwmVal); // 根据方向更新pwmVal if(dir) { pwmVal++; if(pwmVal >= 1000) { // ARR是999,所以最大值为999,这里用1000作为上限判断 dir = 0; } } else { pwmVal--; if(pwmVal == 0) { dir = 1; } } // 延时,控制呼吸速度 HAL_Delay(1); // 1ms延时,这样一次完整的呼吸周期大约2秒 }代码解析:
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pwmVal):这是一个HAL库的宏,用于安全地更新通道1的比较寄存器CCR1的值。直接操作htim3.Instance->CCR1在某些情况下可能有问题,使用这个宏更稳妥。- 我们让
pwmVal从0递增到999,再递减回0,循环往复。pwmVal直接作为CCR1的值,因此占空比就在0%到100%之间平滑变化。 HAL_Delay(1)控制每次变化的间隔。1ms的间隔,结合1000个步进,完成一次呼吸大约需要2秒。你可以调整这个延时来改变呼吸的快慢。
实操心得:
HAL_Delay()函数依赖于系统滴答定时器(Systick),它会阻塞整个CPU。在简单的演示中没问题,但在实际产品中,这种需要精准定时或需要同时处理其他任务的情况,应该使用定时器中断来非阻塞地更新PWM值。例如,可以配置一个基本定时器(如TIM6)每1ms产生一次中断,在中断服务程序里更新pwmVal和CCR1。
4.3 示波器观测与问题排查
将代码编译下载到单片机后,你应该能看到LED开始呼吸。此时,用示波器探头连接到PA6引脚和GND,可以观察到标准的1kHz PWM波形,其占空比在缓慢地周期性变化。
如果看不到波形或波形异常,请按以下步骤排查:
- 检查硬件连接:LED和电阻是否接对?PA6是否确实连接到了示波器探头?
- 检查时钟配置:回顾3.2节,确认TIM3的时钟频率是否正确(应为72MHz)。在
tim.c的MX_TIM3_Init函数中,检查htim3.Init.Prescaler和htim3.Init.Period的值是否与CubeMX设置一致。 - 检查PWM启动函数:确认
HAL_TIM_PWM_Start()是否被调用。 - 检查GPIO复用:确认PA6是否被正确配置为复用推挽输出(AF_PP)。在生成的
tim.c初始化代码里可以看到GPIO的配置。 - 检查ARR和CCR值:确保
pwmVal没有超过ARR的值(999)。超过的话,占空比将永远为100%。
5. PWM输入捕获功能实现详解
接下来是重头戏:测量外部PWM信号的频率和占空比。我们将使用TIM2的通道1和通道2来实现完整的PWM输入模式。
5.1 输入捕获模式配置解析
在CubeMX中,我们只将TIM2_CH1设为了直接输入捕获。要实现PWM输入,我们需要手动修改代码。PWM输入模式本质上是将输入信号同时映射到两个捕获通道(TI1和TI2),并配置从模式、触发源和交叉映射。
打开tim.c,找到MX_TIM2_Init函数。我们需要在HAL库初始化结构体的基础上,添加PWM输入模式的配置。
修改后的MX_TIM2_Init函数关键部分:
static void MX_TIM2_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; TIM_IC_InitTypeDef sConfigIC = {0}; TIM_SlaveConfigTypeDef sSlaveConfig = {0}; htim2.Instance = TIM2; htim2.Init.Prescaler = 71; // 预分频,72MHz / (71+1) = 1MHz计数频率 htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 0xFFFF; // 作为输入捕获,ARR设为最大,防止溢出 htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; //输入捕获通常不使能预装载 if (HAL_TIM_Base_Init(&htim2) != HAL_OK) { Error_Handler(); } // 配置时钟源(内部时钟) sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK) { Error_Handler(); } // --- 关键配置:PWM输入模式 --- // 1. 配置通道1为输入捕获,映射到TI1(即PA0引脚) sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING; // 上升沿捕获 sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI; // 直接映射到TI1 sConfigIC.ICPrescaler = TIM_ICPSC_DIV1; // 不分频,每个有效边沿都捕获 sConfigIC.ICFilter = 0; // 不滤波 if (HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1) != HAL_OK) { Error_Handler(); } // 2. 配置通道2为输入捕获,映射到TI1(交叉映射) sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_FALLING; // 下降沿捕获 sConfigIC.ICSelection = TIM_ICSELECTION_INDIRECTTI; // 交叉映射,映射到TI1 if (HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_2) != HAL_OK) { Error_Handler(); } // 3. 配置从模式:复位模式,触发源为TI1FP1 sSlaveConfig.SlaveMode = TIM_SLAVEMODE_RESET; sSlaveConfig.InputTrigger = TIM_TS_TI1FP1; // 触发信号为经过滤波后的TI1 sSlaveConfig.TriggerPolarity = TIM_INPUTCHANNELPOLARITY_RISING; // 上升沿触发复位 sSlaveConfig.TriggerFilter = 0; sSlaveConfig.TriggerPrescaler = TIM_ICPSC_DIV1; if (HAL_TIM_SlaveConfigSynchro(&htim2, &sSlaveConfig) != HAL_OK) { Error_Handler(); } // 主模式输出触发配置(本例不需要,可省略或保持默认) // sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; // sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; // if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK) // { // Error_Handler(); // } }配置逻辑解读:
- 通道1(TIM_CHANNEL_1):配置为在TI1(即PA0引脚)的上升沿进行捕获,捕获值存入CCR1。同时,这个上升沿被设置为触发源(TIM_TS_TI1FP1)。
- 通道2(TIM_CHANNEL_2):配置为交叉映射到TI1(
TIM_ICSELECTION_INDIRECTTI),在TI1的下降沿进行捕获,捕获值存入CCR2。 - 从模式(Slave Mode):设置为
TIM_SLAVEMODE_RESET(复位模式)。这意味着当触发事件(即TI1的上升沿)发生时,定时器2的计数器(CNT)会立刻清零。这是实现周期测量的关键。 - 工作流程:外部PWM信号从PA0输入。第一个上升沿到来时,触发两件事:a) 计数器CNT被清零;b) 当前CNT值(0)被捕获到CCR1。然后计数器开始从0递增。当下降沿到来时,当前的CNT值被捕获到CCR2。当下一个上升沿到来时,CNT再次被清零,并被捕获到CCR1(此时CCR1中的值就是上一个完整周期的计数值)。如此循环。
5.2 测量逻辑与代码实现
初始化完成后,我们需要启动输入捕获,并定期读取CCR1和CCR2的值来计算频率和占空比。
在main.c的main函数中,TIM2初始化后,启动输入捕获:
/* 启动TIM2的输入捕获(通道1和通道2) */ HAL_TIM_IC_Start(&htim2, TIM_CHANNEL_1); HAL_TIM_IC_Start(&htim2, TIM_CHANNEL_2);然后,在while(1)循环中(可以与呼吸灯代码并存,但为了清晰,我们先注释掉呼吸灯部分),添加测量和打印逻辑:
uint32_t frequency = 0; uint32_t dutyCycle = 0; uint32_t ic1Value = 0, ic2Value = 0; char msg[100]; while (1) { // 读取输入捕获值 ic1Value = __HAL_TIM_GET_COMPARE(&htim2, TIM_CHANNEL_1); // 周期值 ic2Value = __HAL_TIM_GET_COMPARE(&htim2, TIM_CHANNEL_2); // 高电平值 // 计算频率和占空比 (定时器时钟为1MHz,即每秒计数1,000,000次) if(ic1Value > 0) { // 频率 = 时钟频率 / 一个周期的计数值 frequency = 1000000 / ic1Value; // 单位 Hz // 占空比 = (高电平计数值 / 周期计数值) * 100% dutyCycle = (ic2Value * 100) / ic1Value; } // 通过串口打印结果 sprintf(msg, "Freq: %lu Hz, Duty: %lu%%\r\n", frequency, dutyCycle); HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 100); HAL_Delay(200); // 每200ms打印一次,避免串口数据刷屏 }计算原理:
ic1Value:两个上升沿之间的计数值,代表信号周期T。- 定时器计数频率我们之前配置为1MHz(
F_cnt)。 - 因此,信号频率
F_signal = F_cnt / ic1Value。 ic2Value:上升沿到下降沿之间的计数值,代表高电平时间T_high。- 因此,信号占空比
Duty = (T_high / T) * 100% = (ic2Value / ic1Value) * 100%。
5.3 自环测试与结果验证
现在进行关键的“自环测试”:
- 用一根杜邦线,将PWM输出引脚PA6连接到PWM输入引脚PA0。
- 将程序下载到单片机(记得在
main函数里只开启PWM输出和输入捕获,暂时注释掉呼吸灯循环里动态改变CCR1的代码,让PWM输出固定占空比,比如50%)。 - 打开串口助手(如Putty、SecureCRT等),设置正确的串口号和波特率(115200)。
你应该能看到串口持续打印出测量结果。由于PA6输出的是1kHz(1000Hz)、50%占空比的PWM波,串口打印的结果应该非常接近“Freq: 1000 Hz, Duty: 50%”。可能会存在微小误差,这是由于定时器时钟分频、中断处理延迟等造成的,在允许范围内。
测试进阶:重新启用呼吸灯的动态占空比代码。此时,PA6输出的PWM频率固定为1kHz,但占空比在0%~100%之间变化。观察串口打印,你会发现频率值基本稳定在1000Hz左右,而占空比值会跟随呼吸灯节奏从0到100再回到0循环变化。这就完美验证了PWM输入捕获功能的正确性。
6. 高级话题与深度优化
6.1 高精度测量与溢出处理
上面的例子中,我们将定时器的ARR设置为最大值0xFFFF(65535),计数频率为1MHz。这意味着我们能无溢出测量的最长周期是 65535 / 1000000 ≈ 65.5ms,对应最低频率约为15.26Hz。如果输入信号频率低于此值,计数器就会在下一个上升沿到来前溢出归零(从0重新开始),导致测量错误。
解决方案:
- 降低计数频率:增大预分频器PSC的值。例如,将PSC设为7199,则计数频率降为72MHz / 7200 = 10kHz。此时最长可测周期为6.55秒,最低频率0.15Hz。但这会降低测量分辨率。
- 使用定时器溢出中断:开启定时器的更新(溢出)中断。在中断服务函数中,对一个全局变量(如
overflowCount)进行加一操作。在计算周期时,总计数 =overflowCount * (ARR + 1) + ic1Value。这样可以极大地扩展测量范围,但编程复杂度增加。 - 使用32位定时器:如果你的STM32型号拥有32位通用定时器(如TIM2/TIM5在某些系列是32位的),直接使用它,其最大计数值约42.9亿,在1MHz计数频率下可测量长达71分钟的单周期,完全不用担心溢出。
实操心得:在项目规划阶段,就要根据待测信号的频率范围来选择合适的定时器和配置参数。对于频率变化范围大的应用(如测速),溢出中断方案是更稳健的选择。在中断服务程序里,记得要清除更新中断标志位
__HAL_TIM_CLEAR_IT(&htimx, TIM_IT_UPDATE)。
6.2 抗干扰与数字滤波配置
在实际工业环境中,输入信号可能带有毛刺噪声,导致错误的边沿触发和捕获值。STM32的输入捕获通道提供了数字滤波器(ICFilter)功能。
在TIM_IC_InitTypeDef结构体中,ICFilter可以设置为0到15之间的值。它不是一个简单的软件延时,而是基于采样时钟的硬件滤波器。例如,当ICFilter设为4时,输入信号会被以f_DTS(由ClockDivision决定)的频率采样,只有当连续4个采样点都是有效电平时,边沿检测才被确认。
配置建议:
- 对于低速、噪声明显的信号,可以适当增加
ICFilter值,如设为6或8。 - 对于高速信号,过大的滤波值可能导致边沿检测延迟,甚至错过窄脉冲,此时应设为0或较小的值(如2)。
f_DTS的频率可以在ClockDivision中设置。TIM_CLOCKDIVISION_DIV1表示f_DTS = f_CK_INT(定时器输入时钟)。分频后可以降低采样频率,配合ICFilter实现更宽的滤波窗口。
6.3 使用DMA传输捕获数据
在需要连续高速测量PWM参数(例如用于高性能闭环控制)时,频繁进入中断读取CCR寄存器会增加CPU开销并可能丢失数据。此时,可以使用DMA(直接存储器访问)功能。
STM32的定时器支持在捕获事件发生时,自动将CCR寄存器的值通过DMA传输到指定的内存数组中。你可以配置DMA循环模式,连续存储多个周期的捕获值。然后,CPU只需要定期(比如每100ms)去处理这块内存中的数据即可,计算平均频率和占空比,或者进行更复杂的分析。
配置步骤概要:
- 在CubeMX中启用TIM2的通道1和/或通道2的DMA请求。
- 配置DMA为循环模式,从外设(TIM2->CCR1)到内存。
- 在代码中启动DMA传输:
HAL_TIM_IC_Start_DMA(&htim2, TIM_CHANNEL_1, buffer, BUFFER_SIZE)。 - 可以开启DMA半传输完成和传输完成中断,在中断中处理数据。
这种方法将CPU从频繁的捕获中断中解放出来,极大地提高了系统效率和实时性。
7. 常见问题排查与调试技巧
在实际操作中,你可能会遇到各种问题。下面是一个快速排查指南:
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| PWM无输出 | 1. GPIO未正确复用。 2. 定时器未启动。 3. 时钟未使能。 | 1. 检查tim.c中GPIO初始化代码,模式应为GPIO_MODE_AF_PP。2. 确认 HAL_TIM_PWM_Start()已被调用。3. 在 main()开头使用__HAL_RCC_TIM3_CLK_ENABLE()显式开启时钟(CubeMX通常已做)。 |
| PWM频率不对 | 1. 系统时钟配置错误。 2. PSC或ARR计算错误。 | 1. 使用CubeMX的Clock Configuration界面反复核对,或用示波器测量一个已知频率的引脚(如MCO输出)。 2. 复核公式: F_pwm = F_tim_clk / ((PSC+1)*(ARR+1))。 |
| 输入捕获值始终为0 | 1. 输入引脚配置错误。 2. 输入信号电压不匹配。 3. 从模式或触发源配置错误。 | 1. 确认引脚配置为浮空输入或上拉输入,且复用功能正确。 2. 确保输入信号是标准的3.3V电平,用示波器确认信号已到达引脚。 3. 仔细检查5.1节中的从模式( SlaveMode)和触发源(InputTrigger)配置,必须为RESET模式和TI1FP1/TI2FP2。 |
| 捕获值跳动大,不稳定 | 1. 信号有噪声。 2. 计数器溢出。 3. 中断或主循环处理太慢。 | 1. 尝试增加输入捕获滤波器的值(ICFilter)。在信号线上加一个小电容(如10-100pF)到地。2. 测量低频率信号时,确保ARR足够大或启用溢出计数。 3. 优化代码,避免在中断服务程序中进行复杂运算。考虑使用DMA。 |
| 占空比测量接近100%或0% | 1. 输入捕获极性配置反了。 2. 通道映射错误。 | 1. 检查sConfigIC.ICPolarity,通道1应为RISING,通道2应为FALLING。2. 检查 sConfigIC.ICSelection,通道1应为DIRECTTI,通道2应为INDIRECTTI。 |
| 串口无打印数据 | 1. 串口引脚接错。 2. 波特率不匹配。 3. 代码未进入打印循环。 | 1. 检查TX/RX线是否接反,USB转串口模块是否驱动正常。 2. 确认代码和串口助手设置的波特率、数据位、停止位、校验位完全一致。 3. 在 sprintf前加一个LED闪烁或设置一个GPIO翻转,确认程序运行到了这里。 |
调试技巧:
- 善用仿真器:如果使用ST-Link等调试器,可以在IDE中设置断点,实时查看CCR1、CCR2、CNT等寄存器的值,非常直观。
- GPIO翻转调试法:在关键代码段(如中断服务程序开始和结束处)用一条
HAL_GPIO_TogglePin()语句翻转一个空闲的GPIO引脚,然后用示波器观察该引脚波形,可以精确测量代码段的执行时间,判断是否超时。 - 分步验证:不要试图一次性写完所有功能。先确保PWM输出正常,再用信号发生器给输入引脚一个标准方波,验证输入捕获功能,最后再做“自环”或连接真实信号源。
