使用定时器 2 通道 2 来捕获按键 2 按下时间,并通过串口打印。 计一个数的时间:1us,PSC=71,ARR=65535 下降沿捕获、输入通道 2 映射在 TI2 上、不分频、不滤波。
ic.c:
cpp
#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=0
TIM_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:
cpp
#include "sys.h" // 时钟配置接口 stm32_clock_init()
#include "delay.h" // 简易延时(若只用串口与捕获,非必须)
#include "led.h" // LED 初始化(与本实验无强相关)
#include "uart1.h" // 串口初始化/printf 重定向
#include "ic.h" // 本模块:ic_init / pressed_time_get
int 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
。
- F1 输入捕获做输入即可;更高系列常见
-
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
:
cpp
printf("按下时间:%d us\r\n", capture_status.timout_cnt * 65536 + last_cnt);
换 ARR 后必须改为通用写法(推荐这样写,永远正确):
cpp
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 测量模块)
cpp
// 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=71
htim2.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; // 直连 TI1
sConfigIC.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; // 间接选择 TI1
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
sConfigIC.ICFilter = 4;
HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_2);
// 4) 从模式:Reset(每个上升沿复位计数器),触发源:TI1FP1
TIM_SlaveConfigTypeDef sSlave = {0};
sSlave.SlaveMode = TIM_SLAVEMODE_RESET; // 触发就复位 CNT
sSlave.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(演示:初始化 + 打印频率/占空比)
cpp
// 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) {} // TXE
USART1->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)。 -
无信号/常电平:不会触发回调,不会更新数据;可额外用"更新中断"计超时来判"信号丢失"。