1. 项目概述PwmIn是一个面向嵌入式系统的轻量级 PWM 输入信号解析库其核心设计目标是在资源受限的 MCU 上以确定性、低开销的方式精确捕获外部 PWM 波形的周期与占空比。该库不依赖硬件定时器输入捕获Input Capture外设而是基于InterruptIn接口通过 GPIO 引脚电平跳变中断实现边沿检测结合高精度滴答计时器如 SysTick 或 HAL_GetTick()完成周期测量与占空比计算。这一设计路径具有明确的工程意图硬件兼容性优先并非所有 Cortex-M0/M0/M3 微控制器都具备足够数量的输入捕获通道或其通道与目标引脚存在复用冲突InterruptIn方案可部署于任意支持外部中断的 GPIO 引脚极大提升引脚布局灵活性资源占用可控避免为单路 PWM 输入独占一个高级定时器如 TIM1/TIM8节省宝贵的外设资源供电机控制、ADC 同步采样等关键任务使用实时性可保障中断服务程序ISR仅执行边沿时间戳记录无复杂运算确保最坏响应延迟 ≤ 2–3 个 CPU 周期取决于内核与编译优化等级满足典型遥控信号如 RC PWM周期 20 ms、伺服控制信号如 SG90周期 20 ms及工业编码器脉冲kHz 级的解析需求。需特别强调PwmIn并非替代硬件输入捕获的通用方案而是在特定约束下的务实选型——它牺牲了硬件捕获的亚微秒级精度与零 CPU 占用率换取了引脚自由度与外设资源释放。工程师应在项目初期根据信号频率范围、精度要求、MCU 外设资源余量三者进行权衡决策。2. 核心原理与实现逻辑2.1 信号建模与状态机设计PWM 输入信号在时域上由连续的高低电平交替构成其关键参数为高电平持续时间Ton从上升沿到下降沿的时间间隔低电平持续时间Toff从下降沿到下一个上升沿的时间间隔周期T Ton Toff占空比Duty Ton / T × 100%。PwmIn将信号解析抽象为一个四状态有限状态机FSM每个状态对应一个确定的电平跳变事件状态触发条件记录动作状态转移IDLE初始态等待首个上升沿记录上升沿时间戳t_rise_0→WAIT_FALLWAIT_FALL检测到下降沿记录下降沿时间戳t_fall计算Ton t_fall - t_rise_0→WAIT_RISE_NEXTWAIT_RISE_NEXT检测到下一个上升沿记录新上升沿时间戳t_rise_1计算T t_rise_1 - t_rise_0Duty Ton / T→WAIT_FALL循环ERROR连续两次同向跳变如两次上升沿清除历史时间戳重置为IDLE→IDLE该状态机完全在 ISR 中运行无分支预测失败风险代码路径高度线性化。关键在于时间戳的原子性读取所有时间戳均采用uint32_t类型且 MCU 的 SysTick 或 HAL 滴答计数器频率需 ≥ 1 MHz推荐 1–10 MHz以保证在典型 20 ms 周期下时间分辨率达 1 µs满足 RC 信号 ±10 µs 的工业精度要求。2.2 中断服务程序ISR精简实现PwmIn的 ISR 严格遵循“快进快出”原则仅执行以下三类操作时间戳捕获读取当前滴答计数器值如HAL_GetTick()或直接读取SysTick-VAL配合SysTick-LOAD计算状态机迁移根据当前状态与触发边沿类型更新内部状态变量临界区保护对共享的时间戳变量使用__disable_irq()/__enable_irq()或__DMB()内存屏障防止主循环读取时发生撕裂tearing。以 STM32 HAL 库为例其 ISR 核心逻辑如下// PwmIn.h 中声明的静态变量需在 .c 文件中定义 static volatile uint32_t s_rise_time 0; static volatile uint32_t s_fall_time 0; static volatile uint32_t s_last_rise_time 0; static volatile uint8_t s_state STATE_IDLE; // 外部中断回调由 HAL_GPIO_EXTI_Callback() 调用 void PwmIn_IRQHandler(uint16_t GPIO_Pin) { const uint32_t now HAL_GetTick(); // 假设 SysTick 配置为 1ms tick __disable_irq(); // 进入临界区 switch (s_state) { case STATE_IDLE: if (GPIO_Pin PWM_IN_PIN HAL_GPIO_ReadPin(PWM_IN_PORT, GPIO_Pin) GPIO_PIN_SET) { s_rise_time now; s_state STATE_WAIT_FALL; } break; case STATE_WAIT_FALL: if (GPIO_Pin PWM_IN_PIN HAL_GPIO_ReadPin(PWM_IN_PORT, GPIO_Pin) GPIO_PIN_RESET) { s_fall_time now; s_state STATE_WAIT_RISE_NEXT; } break; case STATE_WAIT_RISE_NEXT: if (GPIO_Pin PWM_IN_PIN HAL_GPIO_ReadPin(PWM_IN_PORT, GPIO_Pin) GPIO_PIN_SET) { const uint32_t period now - s_rise_time; const uint32_t on_time s_fall_time - s_rise_time; // 更新用户可见的周期与占空比需双缓冲防读取撕裂 s_last_period period; s_last_duty (on_time * 1000) / (period ? period : 1); // 单位0.1% s_rise_time now; // 为下一轮准备 s_state STATE_WAIT_FALL; } break; } __enable_irq(); // 退出临界区 }注意HAL_GetTick()默认为 1 ms 分辨率若需 µs 级精度必须替换为更高频计数器例如static inline uint32_t get_us_tick(void) { return (SysTick-LOAD - SysTick-VAL) * (1000000 / SysTick-LOAD); }此处假设 SysTick 配置为 1 MHzLOAD SystemCoreClock / 1000000实际需根据系统时钟动态计算。2.3 主循环数据同步机制ISR 采集的原始时间戳需安全地暴露给主应用线程。PwmIn采用双缓冲原子标志策略规避竞态定义两组变量s_buf_aISR 写入、s_buf_b主循环读取ISR 每次完成一次完整周期解析后将结果写入s_buf_a并置位s_update_flag 1主循环调用PwmIn_GetValues(period_ms, duty_per_mille)时检查s_update_flag若为 1则原子交换s_buf_a与s_buf_b的指针或 memcpy清零标志返回s_buf_b中的数据。此机制确保主循环读取的始终是 ISR 已确认有效的完整周期数据杜绝了读取到半更新状态的风险。3. API 接口详解PwmIn提供极简的 C 函数接口全部声明于头文件PwmIn.h中。所有函数均为static inline或普通extern无动态内存分配符合嵌入式硬实时要求。3.1 初始化与配置函数原型功能说明参数说明典型调用示例void PwmIn_Init(GPIO_TypeDef* port, uint16_t pin, void (*isr_handler)(uint16_t))初始化 PWM 输入引脚配置 GPIO 为浮空输入并使能 EXTI 中断port: GPIO 端口如GPIOApin: 引脚号如GPIO_PIN_0isr_handler: 用户提供的中断回调函数指针用于转发 EXTI 事件cbrPwmIn_Init(GPIOA, GPIO_PIN_0, EXTI0_IRQHandler);brvoid PwmIn_SetDebounceMs(uint16_t ms)设置软件消抖时间过滤高频干扰毛刺默认 0 msms: 消抖阈值单位ms若两次跳变间隔 ms则丢弃后一次cbr// 抑制 2ms 内的误触发brPwmIn_SetDebounceMs(2);br消抖原理在 ISR 中记录上次有效跳变时间last_valid_time当前跳变时间now满足now - last_valid_time debounce_ms时才进入状态机否则忽略。3.2 数据获取与状态查询函数原型功能说明返回值/副作用注意事项bool PwmIn_IsLocked(void)查询当前是否已捕获到稳定周期即至少完成一次完整rise→fall→rise流程true: 已锁定数据可信false: 仍在初始化或信号丢失信号中断超过 2×T 后自动退锁uint32_t PwmIn_GetPeriodUs(void)获取最新解析的周期单位微秒若已锁定返回周期值否则返回 0需配合PwmIn_IsLocked()使用uint16_t PwmIn_GetDutyPerMille(void)获取最新占空比单位千分比0–1000 对应 0%–100%若已锁定返回占空比否则返回 0例如 1500 表示 15.0%void PwmIn_GetValues(uint32_t* period_us, uint16_t* duty_pm)批量获取周期与占空比线程安全通过指针返回值避免多次函数调用开销推荐在主循环中高频调用3.3 高级控制接口函数原型功能说明工程价值void PwmIn_ResetLock(void)强制清除锁定状态重新开始信号同步用于信号源切换或故障恢复uint32_t PwmIn_GetRawRiseTime(void)获取最后一次上升沿的原始时间戳调试用用于分析信号抖动、时序偏差void PwmIn_EnableIRQ(bool enable)动态使能/禁用 PWM 输入中断实现低功耗模式信号空闲时关闭中断唤醒后重新使能4. 典型应用场景与代码示例4.1 RC 遥控信号解码标准 PWM标准 RC 接收机输出为 50 Hz20 ms 周期PWM脉宽 1000–2000 µs 对应舵机 0°–180°。PwmIn可直接解析#include PwmIn.h #include stm32f4xx_hal.h // 在 main() 中初始化 int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); // 初始化 PA0 为 PWM 输入 PwmIn_Init(GPIOA, GPIO_PIN_0, EXTI0_IRQHandler); while (1) { if (PwmIn_IsLocked()) { const uint32_t period_us PwmIn_GetPeriodUs(); const uint16_t duty_pm PwmIn_GetDutyPerMille(); const uint32_t pulse_width_us (period_us * duty_pm) / 1000; // 脉宽映射1000us→0°, 1500us→90°, 2000us→180° int16_t angle (pulse_width_us - 1000) * 180 / 1000; angle CLAMP(angle, 0, 180); // 限幅 // 控制舵机例如通过 TIM1 CH1 输出 PWM __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, angle_to_compare(angle)); } HAL_Delay(10); // 100Hz 更新率 } }4.2 与 FreeRTOS 集成任务间安全传递 PWM 数据在 RTOS 环境中需将 PWM 数据通过队列传递至处理任务避免主循环阻塞// 定义队列句柄 QueueHandle_t xPwmQueue; // 在创建任务前初始化队列10 个元素每个含周期占空比 xPwmQueue xQueueCreate(10, sizeof(PwmData_t)); // ISR 中发送数据需使用 FromISR 版本 void EXTI0_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0); } void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin GPIO_PIN_0 PwmIn_IsLocked()) { PwmData_t data { .period_us PwmIn_GetPeriodUs(), .duty_pm PwmIn_GetDutyPerMille() }; BaseType_t xHigherPriorityTaskWoken pdFALSE; xQueueSendFromISR(xPwmQueue, data, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } } // PWM 处理任务 void vPwmTask(void *pvParameters) { PwmData_t data; for (;;) { if (xQueueReceive(xPwmQueue, data, portMAX_DELAY) pdTRUE) { // 在此处执行滤波、PID 计算、CAN 发送等 process_pwm_signal(data); } } }4.3 多路 PWM 输入扩展PwmIn支持多实例只需为每路分配独立的 GPIO 引脚与 EXTI 线。以 STM32F407 为例PA0–PA15 均可映射至 EXTI0–EXTI15// 定义两个实例 PwmIn_Handle_t g_pwm_ch1 { .port GPIOA, .pin GPIO_PIN_0 }; PwmIn_Handle_t g_pwm_ch2 { .port GPIOB, .pin GPIO_PIN_1 }; // 分别初始化 PwmIn_Init(g_pwm_ch1.port, g_pwm_ch1.pin, EXTI0_IRQHandler); PwmIn_Init(g_pwm_ch2.port, g_pwm_ch2.pin, EXTI1_IRQHandler); // 在各自 EXTI IRQ Handler 中调用对应实例的 ISR 处理函数 void EXTI0_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0); PwmIn_ProcessEdge(g_pwm_ch1, GPIO_PIN_0); } void EXTI1_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_1); PwmIn_ProcessEdge(g_pwm_ch2, GPIO_PIN_1); }关键点PwmIn_ProcessEdge()是库提供的实例化 ISR 处理函数接受PwmIn_Handle_t*参数实现多路隔离。5. 性能边界与工程实践建议5.1 信号频率支持范围PwmIn的理论上限由以下因素决定中断响应延迟Cortex-M3/M4 典型为 12 个周期72 MHz ≈ 167 nsISR 执行时间纯状态机 时间戳读取约 0.5–1 µs最小可分辨周期需至少包含rise→fall→rise三个事件故最低周期T_min ≈ 3 × (ISR_time delay)实测数据STM32F407 168 MHz信号类型最大可靠频率原因分析RC PWM (20 ms)100% 可靠周期长余量充足编码器 A/B 相 (100 kHz)可靠需 1 µs 分辨率ISR 时间占比 10%高速电机反馈 (500 kHz)不推荐中断频率达 1 MHzCPU 占用率 30%易丢边沿建议对 100 kHz 信号优先选用硬件输入捕获PwmIn最佳适用区间为1 Hz – 50 kHz。5.2 抗干扰设计要点硬件层在输入引脚串联 100 Ω 电阻对地并联 10 nF 电容构成 RC 低通滤波截止频率 ≈ 160 kHz软件层启用PwmIn_SetDebounceMs(1)滤除 1 ms 的毛刺电源层为 PWM 输入引脚所在端口单独敷铜就近放置 100 nF 退耦电容PCB 层输入走线远离高速时钟线、DC-DC 电感长度 5 cm。5.3 调试与验证方法逻辑分析仪抓取对比PwmIn解析结果与原始波形验证Ton、Toff误差LED 指示在 ISR 中翻转 LED观察闪烁频率是否等于输入信号频率粗略验证串口输出在主循环中定期打印PwmIn_GetPeriodUs()与PwmIn_GetDutyPerMille()观察稳定性FreeRTOS Tracealyzer监控PwmInISR 的执行频率与最大延迟确保无堆积。6. 与同类方案对比分析特性PwmInInterruptIn硬件输入捕获TIMx IC轮询方式GPIO Read精度1–10 µs取决于滴答频率1–10 ns取决于定时器时钟 100 µs受主频与轮询间隔限制CPU 占用极低仅边沿时刻零DMA 可选持续占用需高频轮询引脚约束任意 EXTI 引脚仅 TIMx_CHy 映射引脚任意 GPIO多路扩展易独立 EXTI 线受限TIM 通道数易但效率低抗干扰中依赖软件消抖高硬件滤波器低易受噪声触发适用场景中低速信号、引脚紧张、成本敏感高速/高精度信号、外设资源充足超低成本、超低性能要求工程师应依据项目约束选择若 MCU 有富余 TIM 通道且信号 100 kHz首选硬件捕获若需 4 路以上 PWM 输入且主频 48 MHzPwmIn是更优解。7. 源码结构与移植指南PwmIn库组织为三个文件PwmIn.hAPI 声明、配置宏、数据结构PwmIn.c核心状态机、ISR 处理、时间戳管理PwmIn_Port.hMCU 专用适配层需用户实现。移植关键步骤在PwmIn_Port.h中定义#define PWMIN_GET_TICK_US() ((SysTick-LOAD - SysTick-VAL) * 1000) // 假设 SysTick 1ms #define PWMIN_ENABLE_EXTI(pin) HAL_GPIO_EnableIRQ(pin) #define PWMIN_DISABLE_EXTI(pin) HAL_GPIO_DisableIRQ(pin)实现PwmIn_Port_Init()完成 GPIO 模式配置浮空输入与 EXTI 初始化将EXTIx_IRQHandler重定向至PwmIn_IRQHandler若使用 LL 库替换HAL_GPIO_ReadPin为LL_GPIO_IsInputPinSet()。该库已在 STM32F0/F4/L4/H7 系列、NXP Kinetis、ESP32-S2 上完成验证移植工作量通常 1 小时。8. 故障排查清单当PwmIn_IsLocked()始终返回false时按以下顺序检查硬件连接确认信号源已接入万用表测量引脚电压是否在 0/3.3 V 跳变中断配置用逻辑分析仪确认 EXTI 中断是否触发监测 NVIC IABR 寄存器GPIO 模式检查GPIOx_MODER寄存器确保目标引脚为0b00输入模式时钟使能确认RCC-AHB1ENR中 GPIO 和 SYSCFG 时钟已开启时间戳溢出若使用 16 位计数器检查now是否发生回绕未处理消抖过大临时注释PwmIn_SetDebounceMs()排除过度滤波导致边沿丢失。若数据跳变剧烈重点检查电源噪声示波器观测 VDD 波纹输入引脚未加 RC 滤波HAL_GetTick()被其他高优先级任务长时间阻塞检查 SysTick 中断是否被屏蔽。PwmIn的设计哲学是“简单即可靠”——它不追求炫技的算法而将全部工程努力倾注于中断路径的极致精简与数据同步的绝对安全。在无数个深夜调试伺服失控、无人机姿态异常的现场正是这样一段不足百行的 ISR成为系统稳定的最后防线。