PWM输入捕获(测量按键按下时间、测量PWM波)
使用定时器 2 通道 2 来捕获按键 2 按下时间,并通过串口打印。 计一个数的时间:1us,PSC=71,ARR=65535 下降沿捕获、输入通道 2 映射在 TI2 上、不分频、不滤波。
ic.c:
#include "ic.h" // 本模块头文件:声明 ic_init/pressed_time_get 等接口
#include "stdio.h" // printf 调试输出
#include "string.h" // memset 等/* 捕获状态机:* succeed_flag=1:已完成一次“按下时长”的测量(等待应用层读取并清零)* rising_flag/ falling_flag:最近一次捕获到的边沿类型标记(本例只用 falling_flag)* timeout_cnt:计数器溢出次数(用更新中断统计每次 CNT 从 0→ARR 的次数)*/
struct
{uint8_t succeed_flag;uint8_t rising_flag;uint8_t falling_flag;uint16_t timout_cnt; // 注意拼写:timeout_cnt 更贴切;uint16_t 足够覆盖 65.536ms/次
} capture_status = {0};uint16_t last_cnt = 0; // 保存“释放时”捕获到的计数值(单位:计数拍=1µs)TIM_HandleTypeDef ic_handle = {0}; // 定时器句柄,全局唯一,供 HAL 中断/回调使用/* 输入捕获初始化* @param arr 自动重装载值(ARR),本例传 65536-1,使溢出周期 = (ARR+1)/1MHz = 65.536ms* @param psc 预分频(PSC),本例传 72-1,使计数时钟 CK_CNT = 72MHz/(71+1) = 1MHz(1us/计数)*/
void ic_init(uint16_t arr, uint16_t psc)
{TIM_IC_InitTypeDef ic_config = {0}; // 输入捕获通道配置结构体ic_handle.Instance = TIM2; // 选择定时器实例:TIM2(APB1)ic_handle.Init.Prescaler = psc; // 预分频:72-1 → 1MHz 计数ic_handle.Init.Period = arr; // 自动重装载:65535 → 16bit 全范围ic_handle.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数HAL_TIM_IC_Init(&ic_handle); // 初始化输入捕获(会回调 MSP 做时钟/GPIO/NVIC)ic_config.ICPolarity = TIM_ICPOLARITY_FALLING; // 初始捕获“下降沿”(按下瞬间,视硬件接法而定)ic_config.ICSelection = TIM_ICSELECTION_DIRECTTI; // 直连 TI2(把 CH2 直接映射到 TI2)ic_config.ICPrescaler = TIM_ICPSC_DIV1; // 不分频(每个有效边沿都捕获)ic_config.ICFilter = 0; // 不滤波(去抖依赖外部或软件)HAL_TIM_IC_ConfigChannel(&ic_handle, &ic_config, TIM_CHANNEL_2); // 配置 CH2__HAL_TIM_ENABLE_IT(&ic_handle, TIM_IT_UPDATE); // 使能“更新中断”:用于统计溢出次数HAL_TIM_IC_Start_IT(&ic_handle, TIM_CHANNEL_2); // 启动 CH2 输入捕获(使能 CC2 中断)
}/* MSP(底层)初始化:由 HAL_TIM_IC_Init 调用* 这里完成与板级相关的时钟/GPIO/NVIC 配置*/
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM2){GPIO_InitTypeDef gpio_initstruct;// 1) 打开外设时钟__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能 GPIOA 时钟(TIM2_CH2 默认映射在 PA1)__HAL_RCC_TIM2_CLK_ENABLE(); // 使能 TIM2 时钟// 2) 配置 PA1 为输入模式(F1 输入捕获可用普通输入;推荐上拉/下拉按硬件而定)gpio_initstruct.Pin = GPIO_PIN_1; // TIM2_CH2 → PA1(默认映射)gpio_initstruct.Mode = GPIO_MODE_INPUT; // 输入模式(F1 没有专门 AF-Input)gpio_initstruct.Pull = GPIO_PULLUP; // 上拉:配合“按下接地”的按钮gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH; // 对输入意义不大,保持默认HAL_GPIO_Init(GPIOA, &gpio_initstruct);// 3) 配置 NVIC(更新中断与 CC2 中断共用 TIM2_IRQn)HAL_NVIC_SetPriority(TIM2_IRQn, 2, 2);HAL_NVIC_EnableIRQ(TIM2_IRQn);}
}/* TIM2 中断向量:统一交给 HAL 分发(判断是更新还是 CC2 捕获,并调用回调) */
void TIM2_IRQHandler(void)
{HAL_TIM_IRQHandler(&ic_handle);
}/* CCx 捕获回调:每次捕获到所设定极性的边沿都会进来 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{// printf("捕获到下降沿\r\n"); // 不建议在中断里用 printf(阻塞/重入风险)if(htim->Instance == TIM2){if(capture_status.succeed_flag == 0) // 只在一次测量窗口内处理{if(capture_status.falling_flag == 1){// 第二次边沿:捕获到“上升沿”(释放瞬间)printf("捕获到上升沿\r\n");capture_status.succeed_flag = 1; // 标记:一次测量完成// 读取 CH2 捕获值(释放时的 CNT 数;单位:1µs)last_cnt = HAL_TIM_ReadCapturedValue(&ic_handle, TIM_CHANNEL_2);// 恢复为“下降沿等待下一次测量”TIM_RESET_CAPTUREPOLARITY(&ic_handle, TIM_CHANNEL_2);TIM_SET_CAPTUREPOLARITY(&ic_handle, TIM_CHANNEL_2, TIM_ICPOLARITY_FALLING);// 注:不在这里清状态,等 pressed_time_get() 打印后统一 memset 清零}else{// 第一次边沿:捕获到“下降沿”(按下瞬间,测量起点)printf("捕获到下降沿\r\n");memset(&capture_status, 0, sizeof(capture_status)); // 确保清空旧状态capture_status.falling_flag = 1; // 进入“测量中”状态__HAL_TIM_DISABLE(&ic_handle); // 停止计数,避免切换极性时毛刺__HAL_TIM_SET_COUNTER(&ic_handle, 0); // 以“按下瞬间”为 T=0TIM_RESET_CAPTUREPOLARITY(&ic_handle, TIM_CHANNEL_2);TIM_SET_CAPTUREPOLARITY(&ic_handle, TIM_CHANNEL_2, TIM_ICPOLARITY_RISING); // 改为捕“上升沿”__HAL_TIM_ENABLE(&ic_handle); // 重新启动计数与捕获}}}
}/* 更新事件(溢出)回调:CNT 从 ARR 回到 0 时触发(约每 65.536ms 一次) */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM2){if(capture_status.succeed_flag == 0) // 仅在“测量中”累加溢出{if(capture_status.falling_flag == 1)capture_status.timout_cnt++; // 记录溢出次数}}
}/* 应用层读取函数:若一次测量完成,计算并打印“按下时长(us)”,然后复位状态 */
void pressed_time_get(void)
{if(capture_status.succeed_flag == 1){// 总时长 = 溢出次数 * (ARR+1) + 释放时捕获值(单位:计数拍=1µs)printf("按下时间:%d us\r\n", capture_status.timout_cnt * 65536 + last_cnt);// 复位状态,准备下一次测量memset(&capture_status, 0, sizeof(capture_status));}
}
main.c:
#include "sys.h" // 时钟配置接口 stm32_clock_init()
#include "delay.h" // 简易延时(若只用串口与捕获,非必须)
#include "led.h" // LED 初始化(与本实验无强相关)
#include "uart1.h" // 串口初始化/printf 重定向
#include "ic.h" // 本模块:ic_init / pressed_time_getint main(void)
{HAL_Init(); /* 初始化 HAL 库(Systick/优先级分组等) */stm32_clock_init(RCC_PLL_MUL9); /* 系统时钟 72MHz(HSE*9) */led_init(); /* LED 初始化:可用于调试指示 */uart1_init(115200); /* 串口1:115200 8N1 */printf("hello world!\r\n");// 定时器 2 输入捕获初始化:ARR=65535,PSC=71 → 1MHz 计数(1µs/计数)ic_init(65536 - 1, 72 - 1);while(1){// 轮询读取一次测量结果(打印后会复位状态)pressed_time_get();// 其它任务...}
}
1)定时器基类(ic_handle.Init
字段)
Prescaler
(PSC):0..65535
计数时钟
CK_CNT = TIMxCLK / (PSC + 1)
;F1 若 APBx 分频≠1,TIMxCLK=2×PCLKx。
Period
(ARR):0..65535
(F103 通用定时器 16bit)溢出周期
T_update = (ARR + 1)/CK_CNT
。
CounterMode
:TIM_COUNTERMODE_UP
/TIM_COUNTERMODE_DOWN
/TIM_COUNTERMODE_CENTERALIGNED1/2/3
(输入捕获常用 UP)。
ClockDivision
(若需要,默认 DIV1):TIM_CLOCKDIVISION_DIV1/DIV2/DIV4
(对数字滤波采样分频有影响)。
2)输入捕获通道(TIM_IC_InitTypeDef
)
ICPolarity
(捕获哪种边沿):TIM_ICPOLARITY_RISING
(上升沿)TIM_ICPOLARITY_FALLING
(下降沿)TIM_ICPOLARITY_BOTHEDGE
(双沿,并非所有 F1 通用定时器/通道都支持;以参考手册为准)
ICSelection
(通道映射与测相位差用):TIM_ICSELECTION_DIRECTTI
:直接把 CHx 接 TIx(最常用)TIM_ICSELECTION_INDIRECTTI
:把 CHx 接另一输入(如 CH2 接 TI1),配合第二路实现测高/低电平宽度TIM_ICSELECTION_TRC
:选择触发控制器 TRC,配合从模式做特殊测量
ICPrescaler
(边沿预分频):TIM_ICPSC_DIV1/DIV2/DIV4/DIV8
(每 1/2/4/8 个有效边沿才触发一次捕获)
ICFilter
(数字滤波,0..15):0:无滤波;1~15:不同采样频率与样本数的组合(编码见 RM0008:TIMx_CCMR寄存器的 ICxF 位)
用途:去抖/抗毛刺。按键输入建议设大一些,如 8~15;代价是对窄脉冲不敏感。
3)HAL 相关 API/宏
HAL_TIM_IC_Init/ConfigChannel/Start_IT/Stop_IT
:初始化/配置/启动/停止输入捕获(带中断)。HAL_TIM_ReadCapturedValue(&htim, TIM_CHANNEL_1/2/3/4)
:读 CCRx 捕获值。__HAL_TIM_ENABLE_IT(&htim, TIM_IT_UPDATE/CC1/CC2/...)
:使能指定中断源。常见:
TIM_IT_UPDATE
(溢出),TIM_IT_CC1~CC4
(捕获),TIM_IT_TRIGGER
等。
__HAL_TIM_SET_COUNTER(&htim, val)
/__HAL_TIM_GET_COUNTER(&htim)
:设/读 CNT。__HAL_TIM_ENABLE/ __HAL_TIM_DISABLE(&htim)
:开/关计数(置/清 CEN)。TIM_RESET_CAPTUREPOLARITY(&htim, TIM_CHANNEL_x)
:先复位极性配置(安全改边沿)TIM_SET_CAPTUREPOLARITY(&htim, TIM_CHANNEL_x, TIM_ICPOLARITY_*)
:设置新的捕获边沿。最佳实践:改极性前可以先
HAL_TIM_IC_Stop_IT(...)
或__HAL_TIM_DISABLE(&htim)
,改完再启,避免半周期毛刺。
4)GPIO(GPIO_InitTypeDef
)
Pin
:如GPIO_PIN_1
(PA1);Mode
:GPIO_MODE_INPUT
/GPIO_MODE_AF_PP
(输出复用)/GPIO_MODE_IT_FALLING
(外部中断)等;F1 输入捕获做输入即可;更高系列常见
GPIO_MODE_AF_INPUT
。
Pull
:GPIO_NOPULL
/GPIO_PULLUP
/GPIO_PULLDOWN
(按硬件接法选择)。Speed
:GPIO_SPEED_FREQ_LOW/MEDIUM/HIGH
(输入无所谓,输出相关)。
实现计时的原理
把定时器当“高精度计时器”:
设
PSC=71
、ARR=65535
,在 72 MHz 下得到CK_CNT=1 MHz
,即 CNT 每 1 µs +1。溢出周期为
(65535+1)/1MHz = 65.536 ms
,超过此时间 CNT 回到 0 触发更新中断
。
两次边沿确定时间窗:
初始捕获“下降沿”(按钮按下瞬间,假定按下把 PA1 拉低)。
第一次捕获到下降沿:把 CNT 置 0,同时把捕获极性改为“上升沿”(期待按钮释放)。
第二次捕获到上升沿:读取
CCR2
(释放瞬间锁存的 CNT 值)。这段时间就是“按下持续时间”,但可能跨越多个溢出。
跨溢出计数:
在
PeriodElapsedCallback
(更新中断)里,每溢出一次timeout_cnt++
。总时间(µs)=
timeout_cnt * (ARR+1) + CCR2
。本例
ARR+1=65536
,单位是 1µs,因此最后直接打印us
。
抗抖与健壮性:
抖动可能导致在按下/释放附近多次进中断。做法:
设置
ICFilter>0
进行硬件数字滤波;软件上在第一次边沿后立即改极性且短暂禁止计数/捕获(你已
__HAL_TIM_DISABLE/ENABLE
)。
避免在 ISR 里
printf
(容易阻塞/丢中断),推荐置标志,主循环打印。
额外建议
把
timeout_cnt
/last_cnt
扩成 32 位(防极端长按),并在printf
用%lu
。输入滤波:
ic_config.ICFilter = 8~12
,明显改善按键抖动;或外部 RC + 施密特触发/软件去抖。另一种写法:用 从模式(Slave Mode)+ 触发复位 自动在下降沿清 CNT,上升沿捕获 CCR,结构更优雅(RM0008:SMCR.SMS=Reset Mode,触发源 TS=TI2FP2),就不必手工改极性/清 CNT。
ic_init(65536 - 1, 72 - 1);为什么是65536 - 1,可以是其他数据吗?
简短说:ARR = 65536 - 1
是把 TIM2 的 16 位计数范围用满。在你把 PSC=72-1
配成 1 MHz 计数(1 µs/计数) 时,ARR=65535
代表溢出周期是
这样既能保留 1 µs 分辨率(由 PSC
决定),又让“更新中断(溢出)”不那么频繁,CPU 负担更小,同时单次捕获的计数范围最大。
能不能不是 65536-1?当然可以
ARR
可以设成其它值,但要理解它带来的权衡(PSC
不变、仍是 1 µs/计数):
分辨率(最小时间单位):由
PSC
决定,与你怎么设ARR
无关。你现在就是 1 µs。更新中断频率:由
ARR
决定。ARR
越小,更频繁溢出→中断更密→CPU 开销更大;ARR
越大,中断更少。单次“块”的时间:我们用
溢出次数 * (ARR+1) + CCR
算总时间。ARR
越小,每块时间越短,乘法的块大小变小,但总精度仍是 1 µs(由PSC
保证)。可测最大时长(在你当前
uint16_t timeout_cnt
下):
如果
ARR=65535
→ 约 71.6 分钟如果
ARR=9999
(10 ms 溢出)→ 约 10.9 分钟如果
ARR=999
(1 ms 溢出)→ 约 65.5 秒
选择建议:
想减轻中断负担、又不担心超长按 → 用 大 ARR(如 65535)。
想更快知道溢出、或对极端长按不敏感 → 可用较小 ARR(比如 9999/999)。
分辨率仍是 1 µs,不会因改变 ARR 而变粗。
改了 ARR,要同步改代码里的计算
现在打印里写死了 65536
:
printf("按下时间:%d us\r\n", capture_status.timout_cnt * 65536 + last_cnt);
换 ARR 后必须改为通用写法(推荐这样写,永远正确):
void pressed_time_get(void)
{if (capture_status.succeed_flag == 1){uint32_t arrp1 = (uint32_t)__HAL_TIM_GET_AUTORELOAD(&ic_handle) + 1U;uint32_t total_us = (uint32_t)capture_status.timout_cnt * arrp1 + (uint32_t)last_cnt;printf("按下时间:%lu us\r\n", total_us);memset(&capture_status, 0, sizeof(capture_status));}
}
同时把 timeout_cnt/last_cnt
至少按上面这样在计算处提升到 32 位,避免乘加溢出。
什么时候需要把 ARR 改小?
想让更新中断更频繁,例如配合某些“超时保护”逻辑(更细粒度地知晓过长按)。
确实不需要超过若干秒/分钟的最大测量窗口。
想用“溢出的节拍”去做额外的心跳逻辑。
反之,如果你只是想稳定测时、CPU 中断压力要小,ARR=65535
是很好的选择。
小结
ARR=65536-1
是为了最大计数范围 + 最少更新中断;不是强制。你可以换其它
ARR
,分辨率仍然是 1 µs(由PSC
决定),但要权衡更新中断频率与最大可测时长,并把打印计算改成overflows * (ARR+1) + CCR
的通用公式。
基于 HAL 库、用 TIM2_CH1(PA0)测量输入 PWM 的频率与占空比
思路采用**“PWM 输入模式”:把 CH1 配成上升沿直连**、CH2 配成下降沿间接,再把定时器设成从模式 Reset,使每个上升沿复位计数器;这样:
CCR1
自动锁存周期计数(两次上升沿间的计数);CCR2
自动锁存高电平计数(上升→下降间的计数)。
计数时钟设为 1 MHz(1us/计数):PSC=71
(72 MHz/(71+1)=1 MHz),ARR=0xFFFF
(防溢出)。
pwm.c(输入 PWM 测量模块)
// pwm.c — TIM2_CH1 输入捕获测 PWM 的频率/占空比(HAL)
// 引脚:PA0 = TIM2_CH1(默认映射)
// 分辨率:1 us/计数(PSC=71),周期/高电平都以“计数值”采集#include "stm32f1xx_hal.h"
#include <stdint.h>// ====== 模块内部状态 ======
static TIM_HandleTypeDef htim2; // TIM2 句柄
static volatile uint32_t g_tick_hz = 1000000U; // 计数时钟(Hz),由 PSC 计算得出
static volatile uint32_t g_period_cnt = 0; // 周期计数(CCR1)
static volatile uint32_t g_high_cnt = 0; // 高电平计数(CCR2)
static volatile uint8_t g_ready = 0; // 新数据就绪标志// ====== 对外 API 原型(给 main.c 用)======
void pwm_input_init_TIM2_CH1(uint16_t psc, uint16_t arr);
int pwm_input_read(float *freq_hz, float *duty);// ====== MSP:底层时钟/GPIO/NVIC 初始化(由 HAL_TIM_IC_Init 调用)======
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{if (htim->Instance == TIM2){__HAL_RCC_TIM2_CLK_ENABLE(); // TIM2 时钟__HAL_RCC_GPIOA_CLK_ENABLE(); // GPIOA 时钟// PA0 -> TIM2_CH1 输入(F1 无 AF-Input,直接普通输入即可)GPIO_InitTypeDef io = {0};io.Pin = GPIO_PIN_0;io.Mode = GPIO_MODE_INPUT; // 输入模式io.Pull = GPIO_PULLDOWN; // 下拉(若信号源开路时默认为低)HAL_GPIO_Init(GPIOA, &io);// 中断优先级与使能(TIM2:更新/捕获共用同一 IRQ 入口)HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0);HAL_NVIC_EnableIRQ(TIM2_IRQn);}
}// ====== 初始化:配置 PWM 输入模式(CH1=上升沿直连,CH2=下降沿间接;从模式 Reset)======
void pwm_input_init_TIM2_CH1(uint16_t psc, uint16_t arr)
{// 1) 配置定时器时基htim2.Instance = TIM2;htim2.Init.Prescaler = psc; // 72MHz/(psc+1) = 1MHz when psc=71htim2.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数htim2.Init.Period = arr; // ARR(溢出保护)htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;HAL_TIM_IC_Init(&htim2);// 计算计数时钟(tick 频率),考虑 F1 APB1 的“×2 定时器时钟”规则// 这里直接用 PSC 推导出的 1MHz;若你用其它 PSC,可改为公式计算:// g_tick_hz = TIM2CLK / (PSC+1)// 更稳妥:读取 PCLK1 并判断 APB1 分频是否为 1,然后乘以 2{uint32_t pclk1 = HAL_RCC_GetPCLK1Freq();uint32_t timclk = pclk1;// APB1 分频不为 1 时,定时器时钟 = 2 * PCLK1(F1 特性)if ((RCC->CFGR & RCC_CFGR_PPRE1) != RCC_CFGR_PPRE1_DIV1) {timclk = pclk1 * 2U;}g_tick_hz = timclk / (uint32_t)(psc + 1U);}// 2) 配置 CH1 为“上升沿 + 直连 TI1”(周期捕获)TIM_IC_InitTypeDef sConfigIC = {0};sConfigIC.ICPolarity = TIM_ICPOLARITY_RISING; // 捕获上升沿sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI; // 直连 TI1sConfigIC.ICPrescaler = TIM_ICPSC_DIV1; // 不对边沿再分频sConfigIC.ICFilter = 4; // 简单数字滤波(0..15),抗抖/毛刺HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1);// 3) 配置 CH2 为“下降沿 + 间接 TI1”(高电平捕获)// 间接选择会把 CH2 与 TI1 绑定,用来在相反极性边沿锁存高电平宽度sConfigIC.ICPolarity = TIM_ICPOLARITY_FALLING; // 捕获下降沿sConfigIC.ICSelection = TIM_ICSELECTION_INDIRECTTI; // 间接选择 TI1sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;sConfigIC.ICFilter = 4;HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_2);// 4) 从模式:Reset(每个上升沿复位计数器),触发源:TI1FP1TIM_SlaveConfigTypeDef sSlave = {0};sSlave.SlaveMode = TIM_SLAVEMODE_RESET; // 触发就复位 CNTsSlave.InputTrigger = TIM_TS_TI1FP1; // 触发源 = TI1 上升沿滤波后的信号sSlave.TriggerPolarity = TIM_TRIGGERPOLARITY_NONINVERTED;sSlave.TriggerPrescaler = TIM_TRIGGERPRESCALER_DIV1;sSlave.TriggerFilter = 0;HAL_TIM_SlaveConfigSynchro(&htim2, &sSlave);// 5) 启动捕获(带中断),两路都要开HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); // 周期捕获HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2); // 高电平捕获
}// ====== 中断入口:统一交给 HAL 分发 ======
void TIM2_IRQHandler(void)
{HAL_TIM_IRQHandler(&htim2);
}// ====== 捕获回调:一到“上升沿捕获”(CH1)就读 CCR1/CCR2,更新测量值 ======
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{if (htim->Instance != TIM2) return;// HAL 会在回调前设置活动通道,可据此判断当前是哪一路触发if (HAL_TIM_GetActiveChannel(htim) == HAL_TIM_ACTIVE_CHANNEL_1){uint32_t period = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 周期计数uint32_t high = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2); // 高电平计数if (period != 0U) { // 防止除零g_period_cnt = period;g_high_cnt = (high <= period) ? high : period; // 夹紧g_ready = 1; // 标记:有新数据}}
}// ====== 读取一次测量结果:freq(Hz)、duty(0..1)。返回1=有新值,0=无新值 ======
int pwm_input_read(float *freq_hz, float *duty)
{if (!g_ready) return 0;g_ready = 0;// 频率 = 计数时钟 / 周期计数;占空比 = 高电平计数 / 周期计数*freq_hz = (float)g_tick_hz / (float)g_period_cnt;*duty = (g_period_cnt == 0U) ? 0.0f : ((float)g_high_cnt / (float)g_period_cnt);return 1;
}
main.c(演示:初始化 + 打印频率/占空比)
// main.c — 初始化系统时钟/串口,启动 PWM 输入测量并打印结果#include "stm32f1xx_hal.h"
#include <stdio.h>// 你的工程里若已有 sys.h/uart1.h,可替换为现有接口
static void SystemClock_Config(void);
static void USART1_Init_115200(void);// 来自 pwm.c 的 API
void pwm_input_init_TIM2_CH1(uint16_t psc, uint16_t arr);
int pwm_input_read(float *freq_hz, float *duty);// printf 重定向到 USART1(简易版)
int fputc(int ch, FILE *f) {while ((USART1->SR & (1<<7)) == 0) {} // TXEUSART1->DR = (uint8_t)ch;return ch;
}int main(void)
{HAL_Init();SystemClock_Config();USART1_Init_115200();printf("PWM input measure demo (TIM2_CH1@PA0)\r\n");// 1us/计数:PSC=71;ARR=0xFFFF(最大量程、减少溢出)pwm_input_init_TIM2_CH1(71, 0xFFFF);while (1){float f, d;if (pwm_input_read(&f, &d)) {printf("Freq = %.2f Hz, Duty = %.1f %%\r\n", f, d * 100.0f);}HAL_Delay(50); // 打印节流}
}
关键点速记
接法:把被测 PWM 信号接到 PA0(TIM2_CH1)。若电平空闲可能漂移,给合适的上/下拉。
原理:
从模式 Reset + 触发源 TI1 上升沿 → 每次上升沿复位 CNT,
CCR1
得到完整周期计数。CH2 选 INDIRECTTI + FALLING → 在下降沿把“上升至下降的计数”锁到
CCR2
(高电平宽度)。频率=
tick_hz / CCR1
;占空比=CCR2 / CCR1
。
分辨率:由 PSC 决定。
PSC=71
→ 1 MHz,每计数 1 µs。量程:最大可测周期约
(ARR+1)/tick_hz
。本例 ≈ 65.536 ms(≈15.26 Hz 最低频率)。更低频可把 PSC 降低或 ARR 提大(F1 通用定时器 16 位)。抗抖/抗毛刺:
ICFilter
适当调大(如 4~8)。无信号/常电平:不会触发回调,不会更新数据;可额外用“更新中断”计超时来判“信号丢失”。