当前位置: 首页 > news >正文

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核心板(蓝色药丸板),这是一个非常普及的型号。

  1. PWM输出部分:我们选择定时器TIM3的通道1(对应引脚PA6)作为PWM输出引脚。将一个LED的正极通过一个220Ω的限流电阻连接到PA6,负极连接到GND。这样,通过改变PWM占空比,就能控制LED的亮度。
  2. 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)。

  1. 在Pinout & Configuration标签页,转到System Core->RCC
  2. High Speed Clock (HSE)设置为Crystal/Ceramic Resonator
  3. 转到Clock Configuration标签页。你会看到一个复杂的时钟树图。
  4. 在输入频率部分,将HSE设置为8MHz。
  5. 找到PLL相关设置,将PLL源选择为HSE,PLL倍频因子设置为9。这样PLL输出就是 8MHz * 9 = 72MHz。
  6. 将SYSCLK的来源选择为PLL,这样系统时钟就是72MHz。
  7. 关键一步:找到定时器的时钟源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与定时器外设初步配置

  1. 配置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 ModeUp(向上计数)。
      • 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 PolarityHigh,表示有效电平为高。即当计数器值小于CCR1时,输出高电平。
  2. 配置PWM输入捕获引脚(PA0)

    • 在芯片引脚图上找到PA0,左键点击,选择TIM2_CH1
    • 在左侧找到Timers->TIM2
    • Clock Source设置为Internal Clock
    • Channel1设置为Input Capture direct mode但先别急,这不是PWM输入模式。STM32的“PWM输入模式”是一种特殊的映射模式,需要在代码中配置。
    • 我们暂时先将其设为直接输入捕获模式,用于验证引脚功能。更复杂的PWM输入模式配置我们留在代码部分详细说明。
  3. 配置串口用于调试打印(可选但强烈推荐)

    • 找到USART1(通常PA9为TX,PA10为RX),将其模式设置为Asynchronous(异步通信)。
    • Parameter Settings中,设置波特率为115200。

配置完成后,点击右上角的“GENERATE CODE”,让CubeIDE生成初始化代码。

4. PWM输出功能实现详解

4.1 代码生成与工程结构解析

代码生成后,我们主要关注三个文件:

  1. main.c:主程序循环所在。
  2. tim.c:包含了TIM3和TIM2的初始化代码(MX_TIM3_Init等)。
  3. usart.c:包含了串口初始化代码。

首先,在main.cmain函数中,在MX_TIM3_Init()函数调用之后,我们需要启动PWM输出。

/* 启动TIM3的PWM输出 */ HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);

这一行代码至关重要,它启动了定时器3的计数器,并开启了通道1的PWM输出。没有它,引脚上不会有任何波形。

4.2 呼吸灯效果实现:动态调整占空比

我们的目标是让LED像呼吸一样渐亮渐暗。这需要在一个循环中,动态地改变CCR1(即脉冲宽度)的值。

main.cwhile(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波形,其占空比在缓慢地周期性变化。

如果看不到波形或波形异常,请按以下步骤排查

  1. 检查硬件连接:LED和电阻是否接对?PA6是否确实连接到了示波器探头?
  2. 检查时钟配置:回顾3.2节,确认TIM3的时钟频率是否正确(应为72MHz)。在tim.cMX_TIM3_Init函数中,检查htim3.Init.Prescalerhtim3.Init.Period的值是否与CubeMX设置一致。
  3. 检查PWM启动函数:确认HAL_TIM_PWM_Start()是否被调用。
  4. 检查GPIO复用:确认PA6是否被正确配置为复用推挽输出(AF_PP)。在生成的tim.c初始化代码里可以看到GPIO的配置。
  5. 检查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. 通道1(TIM_CHANNEL_1):配置为在TI1(即PA0引脚)的上升沿进行捕获,捕获值存入CCR1。同时,这个上升沿被设置为触发源(TIM_TS_TI1FP1)。
  2. 通道2(TIM_CHANNEL_2):配置为交叉映射到TI1(TIM_ICSELECTION_INDIRECTTI),在TI1的下降沿进行捕获,捕获值存入CCR2。
  3. 从模式(Slave Mode):设置为TIM_SLAVEMODE_RESET(复位模式)。这意味着当触发事件(即TI1的上升沿)发生时,定时器2的计数器(CNT)会立刻清零。这是实现周期测量的关键。
  4. 工作流程:外部PWM信号从PA0输入。第一个上升沿到来时,触发两件事:a) 计数器CNT被清零;b) 当前CNT值(0)被捕获到CCR1。然后计数器开始从0递增。当下降沿到来时,当前的CNT值被捕获到CCR2。当下一个上升沿到来时,CNT再次被清零,并被捕获到CCR1(此时CCR1中的值就是上一个完整周期的计数值)。如此循环。

5.2 测量逻辑与代码实现

初始化完成后,我们需要启动输入捕获,并定期读取CCR1和CCR2的值来计算频率和占空比。

main.cmain函数中,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 自环测试与结果验证

现在进行关键的“自环测试”:

  1. 用一根杜邦线,将PWM输出引脚PA6连接到PWM输入引脚PA0
  2. 将程序下载到单片机(记得在main函数里只开启PWM输出和输入捕获,暂时注释掉呼吸灯循环里动态改变CCR1的代码,让PWM输出固定占空比,比如50%)。
  3. 打开串口助手(如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重新开始),导致测量错误。

解决方案

  1. 降低计数频率:增大预分频器PSC的值。例如,将PSC设为7199,则计数频率降为72MHz / 7200 = 10kHz。此时最长可测周期为6.55秒,最低频率0.15Hz。但这会降低测量分辨率。
  2. 使用定时器溢出中断:开启定时器的更新(溢出)中断。在中断服务函数中,对一个全局变量(如overflowCount)进行加一操作。在计算周期时,总计数 =overflowCount * (ARR + 1) + ic1Value。这样可以极大地扩展测量范围,但编程复杂度增加。
  3. 使用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)去处理这块内存中的数据即可,计算平均频率和占空比,或者进行更复杂的分析。

配置步骤概要

  1. 在CubeMX中启用TIM2的通道1和/或通道2的DMA请求。
  2. 配置DMA为循环模式,从外设(TIM2->CCR1)到内存。
  3. 在代码中启动DMA传输:HAL_TIM_IC_Start_DMA(&htim2, TIM_CHANNEL_1, buffer, BUFFER_SIZE)
  4. 可以开启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))
输入捕获值始终为01. 输入引脚配置错误。
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输出正常,再用信号发生器给输入引脚一个标准方波,验证输入捕获功能,最后再做“自环”或连接真实信号源。
http://www.cnnetsun.cn/news/3743809.html

相关文章:

  • Java开发环境搭建指南:从JDK安装到第一个程序运行
  • C++ STL list容器深度解析:从双向链表原理到LRU缓存实战应用
  • C/C++工程师成长:从开源库深度研读到面试实战
  • 2026翻板路障怎么选型?技术参数与方案配置指南
  • 2026小程序制作平台哪家好:高性价比平台与工具对比
  • 显卡驱动清理革命:用Display Driver Uninstaller告别驱动残留烦恼
  • FastAPI项目ORM选型指南:SQLAlchemy与Tortoise-ORM深度对比
  • Linux命令:alias
  • 两轮平衡小车PID调参实战:从零到稳的保姆级指南
  • 2026年AI内容检测工具实测与使用技巧
  • 提示词压缩率提升300%?揭秘LLM时代最被低估的缩写策略——3类高频失效场景+4种动态裁剪算法
  • 【2026必藏】6款智能降AIGC网站大公开,一键让AIGC率断崖式下跌!
  • 颗粒糖果自动包装机设计全解析:从理料到封合的核心技术与实战
  • 电动车路径优化:MOPGA-NSGA-II算法与Matlab实现
  • Office效率革命:Alt+=快捷键解锁专业数学公式编辑
  • C++核心考点与高频面试题深度解析:从指针到智能指针的实战指南
  • 职场晋升信号金字塔模型解析与应用
  • 餐饮分销平台哪家靠谱,推广佣金防作弊校验代码讲解
  • 基于 Zynq UltraScale+ MPSoC 的 PL DDR4 直写 NVMe 与 exFAT 文件系统方案
  • Scrapy高级应用:全站爬取、分布式与增量爬虫实战
  • 在Android设备上运行完整操作系统:Vectras-VM-Android深度解析
  • QT C++多窗口应用架构设计:从信号槽到窗口管理器的工程实践
  • 漏洞挖掘趋势:符号执行与 Fuzzing 的融合路径
  • 长路上听《朝圣之路》
  • 自动化PLC培训是学什么的?小白入门指南
  • Python自动化水文地质计算:渗透系数K与影响半径R的迭代求解实践
  • Simulink代码生成实战:从模型到嵌入式C代码的工程化指南
  • 终极指南:如何用League Akari本地智能助手提升你的英雄联盟游戏体验
  • Dify 自托管部署教程:使用 Docker Compose 在 Linux 服务器运行完整服务栈
  • Windows热键侦探:精准定位热键冲突的终极工具