
1. 项目概述PwmIn是一个面向嵌入式微控制器的轻量级 PWM 输入信号解析库其核心设计目标是在资源受限的 MCU 上以确定性、低开销的方式精确捕获外部 PWM 信号的周期与占空比。该库不依赖操作系统或复杂定时器外设而是基于InterruptIn中断输入机制通过 GPIO 引脚电平跳变触发中断在中断服务程序ISR中记录时间戳从而完成高精度边沿计时。在工业控制、电机驱动反馈如编码器 Z 相同步信号、遥控接收RC PWM 协议、传感器脉宽调制输出如某些温度/压力传感器等典型场景中MCU 常需实时解析来自外部设备的 PWM 波形。这类信号通常具有以下特征频率范围宽泛典型为 50 Hz ~ 10 kHz部分应用可达 100 kHz占空比动态变化如 5% ~ 95%对测量精度要求高周期误差需 1 µs占空比分辨率优于 0.1%系统无 RTOS 或仅有极简调度器无法承担高频率轮询或复杂状态机开销。PwmIn的工程价值正在于此它规避了传统方案中常见的三类缺陷——轮询法CPU 持续占用率高无法响应其他事件且易受代码执行延迟影响精度不可控通用定时器输入捕获ICU模式虽硬件支持但配置复杂多通道共享定时器资源且不同厂商外设寄存器差异大可移植性差软件定时器中断组合需额外维护定时器中断服务逻辑耦合度高易引入竞态条件。PwmIn采用“单引脚、双沿中断”策略仅需一个支持上升沿/下降沿触发的 GPIO 中断引脚即可完整重构 PWM 波形时序。其本质是一个基于时间戳的状态机在中断上下文中完成边沿检测、时间差计算、状态迁移与结果更新全程不阻塞主循环且所有操作均为原子性读写无临界区保护需求天然适配裸机与 FreeRTOS 环境。2. 核心原理与实现逻辑2.1 信号建模与状态机设计PWM 信号由连续重复的方波周期构成每个周期包含一个高电平段T_high和一个低电平段T_low周期T T_high T_low占空比D T_high / T。PwmIn将信号抽象为四状态有限自动机状态触发条件记录动作输出更新IDLE初始态等待首个上升沿记录上升沿时间戳t_rise_0无WAIT_FALL收到上升沿后等待下一个下降沿记录下降沿时间戳t_fall计算T_high t_fall - t_rise_0high_time_us更新WAIT_RISE收到下降沿后等待下一个上升沿记录上升沿时间戳t_rise_1计算T t_rise_1 - t_rise_0T_low T - T_highperiod_us,duty_cycle更新VALID完成一个完整周期测量t_rise_0 ← t_rise_1为下一周期准备所有参数稳定有效该状态机严格遵循“边沿驱动”避免任何延时等待确保响应实时性。关键设计点在于所有时间戳均以系统滴答SysTick或高精度定时器如 STM32 的 DWT_CYCCNT为基准而非依赖中断延迟补偿。因为现代 Cortex-M 内核的中断进入/退出开销高度可控典型 12~15 个周期且PwmInISR 仅执行寄存器读写与减法运算无分支预测失败风险故时间戳误差可稳定控制在 ±1 个 CPU 周期内。2.2 时间戳获取与精度保障PwmIn的精度瓶颈不在算法而在时间基准源的选择。库本身不绑定具体计数器但强烈推荐使用以下两类高精度源DWT Cycle CounterData Watchpoint and TraceCortex-M3/M4/M7/M33 内置的 32 位自由运行计数器频率等于 CPU 主频无溢出中断开销读取指令MRS r0, DWT_CYCCNT仅需 1 个周期。启用方式以 STM32 为例CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; // 使能 DWT DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; // 使能 CYCCNT DWT-CYCCNT 0; // 清零高分辨率定时器HRTIM、TIM1/TIM8 高级定时器当 DWT 不可用时选用预分频系数为 1、计数模式为向上计数的 32 位定时器其计数频率可达 100 MHz远超 PWM 信号频率。时间戳计算公式为uint32_t delta_ticks (current_ts - prev_ts) 0xFFFFFFFFUL; uint32_t delta_us (delta_ticks * 1000000UL) / SystemCoreClock;其中SystemCoreClock为 CPU 主频Hz。此计算在 ISR 中完成需注意使用uint32_t防止溢出32 位计数器满值对应约 4.29 秒 100 MHz除法运算在裸机中建议用查表或定点缩放替代FreeRTOS 下可接受vTaskDelay不在此路径若主频非整数 MHz如 72.000001 MHz应使用实际校准值提升精度。2.3 中断服务程序ISR精简实现PwmIn的 ISR 是整个库的性能核心必须满足执行时间恒定无分支、无循环无函数调用避免栈操作仅访问静态变量无参数传递开销。典型 ISR 结构以 ARM GCC 为例// 全局状态变量volatile 保证编译器不优化 static volatile uint32_t s_last_rise 0; static volatile uint32_t s_last_fall 0; static volatile uint32_t s_period_us 0; static volatile uint32_t s_high_us 0; static volatile uint8_t s_state STATE_IDLE; void PWM_IN_IRQHandler(void) { uint32_t ts DWT-CYCCNT; // 原子读取时间戳 uint32_t pin_val HAL_GPIO_ReadPin(PWM_IN_GPIO_PORT, PWM_IN_PIN); if (pin_val GPIO_PIN_SET) { // 上升沿 switch (s_state) { case STATE_IDLE: s_last_rise ts; s_state STATE_WAIT_FALL; break; case STATE_WAIT_RISE: s_period_us ((ts - s_last_rise) * 1000000UL) / SystemCoreClock; s_state STATE_WAIT_FALL; // fall-through case STATE_WAIT_FALL: s_last_rise ts; break; } } else { // 下降沿 if (s_state STATE_WAIT_FALL) { s_high_us ((ts - s_last_rise) * 1000000UL) / SystemCoreClock; s_state STATE_WAIT_RISE; } } // 清中断标志HAL_GPIO_EXTI_IRQHandler 自动处理 }此实现中s_state变量驱动状态迁移所有计算在寄存器内完成无内存依赖。volatile修饰确保每次访问均从内存读取防止编译器重排序。3. API 接口详解PwmIn提供极简 API 集全部为 C 函数无类封装符合裸机开发习惯。所有函数线程安全主循环与 ISR 可并发访问。3.1 初始化与配置函数原型说明PwmIn_Initvoid PwmIn_Init(GPIO_TypeDef* port, uint16_t pin, IRQn_Type irqn)初始化 GPIO 引脚为浮空输入配置 EXTI 线使能上升/下降沿触发设置 NVIC 优先级清除所有状态变量。irqn为对应 EXTI 中断号如EXTI9_5_IRQn。PwmIn_SetClockSourcevoid PwmIn_SetClockSource(uint32_t (*get_ts)(void))注册自定义时间戳获取函数。默认使用 DWT若需切换至定时器传入类似uint32_t GetTimerCount(void) { return TIM2-CNT; }的函数指针。3.2 数据读取接口函数原型说明返回值PwmIn_GetPeriodUsuint32_t PwmIn_GetPeriodUs(void)获取最新有效周期微秒。仅当s_state STATE_WAIT_RISE时返回真实值否则返回 0。0或0的周期值PwmIn_GetHighTimeUsuint32_t PwmIn_GetHighTimeUs(void)获取最新高电平时间微秒。在STATE_WAIT_FALL或STATE_WAIT_RISE下均有效。0或0的高电平时间PwmIn_GetDutyCycleuint16_t PwmIn_GetDutyCycle(void)获取占空比千分比0~1000。计算式(s_high_us * 1000UL) / MAX(s_period_us, 1)。0~1000PwmIn_IsValidbool PwmIn_IsValid(void)判断当前数据是否有效已完成至少一个完整周期测量。true/false3.3 高级控制接口函数原型说明PwmIn_Resetvoid PwmIn_Reset(void)强制清空所有状态回到STATE_IDLE丢弃当前未完成的测量。用于信号丢失后快速恢复。PwmIn_Enablevoid PwmIn_Enable(void)使能 EXTI 中断调用HAL_NVIC_EnableIRQ。PwmIn_Disablevoid PwmIn_Disable(void)禁用 EXTI 中断调用HAL_NVIC_DisableIRQ。关键约束所有读取函数Get*均返回快照值即调用瞬间的静态变量副本。因 ISR 与主循环无锁共享变量存在极小概率读取到中间状态如s_period_us已更新但s_high_us未更新。工程实践中可通过PwmIn_IsValid()配合双检验证解决if (PwmIn_IsValid()) { uint32_t period PwmIn_GetPeriodUs(); uint32_t high PwmIn_GetHighTimeUs(); uint16_t duty PwmIn_GetDutyCycle(); // 此时三者必然一致 }4. 实际工程集成示例4.1 STM32 HAL 库集成以 STM32F407VG 为例硬件连接PA0 引脚接入 PWM 信号源。初始化代码#include pwm_in.h #include stm32f4xx_hal.h // 全局句柄按需定义 extern TIM_HandleTypeDef htim2; // 自定义时间戳函数使用 TIM2100 MHz 计数 uint32_t GetTim2Count(void) { return __HAL_TIM_GET_COUNTER(htim2); } int main(void) { HAL_Init(); SystemClock_Config(); // 设置 SysClock168 MHz // 初始化 TIM2 作为高精度计数器不分频向上计数 __HAL_RCC_TIM2_CLK_ENABLE(); htim2.Instance TIM2; htim2.Init.Prescaler 0; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 0xFFFFFFFFUL; htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(htim2); HAL_TIM_Base_Start(htim2); // 初始化 PwmInPA0 - EXTI0 __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_IT_RISING_FALLING; GPIO_InitStruct.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_NVIC_SetPriority(EXTI0_IRQn, 5, 0); HAL_NVIC_EnableIRQ(EXTI0_IRQn); PwmIn_SetClockSource(GetTim2Count); PwmIn_Init(GPIOA, GPIO_PIN_0, EXTI0_IRQn); while (1) { if (PwmIn_IsValid()) { uint32_t period PwmIn_GetPeriodUs(); uint16_t duty PwmIn_GetDutyCycle(); // 例如通过 UART 发送数据 char buf[64]; sprintf(buf, PERIOD:%luus DUTY:%u.%01u%%\r\n, period, duty/10, duty%10); HAL_UART_Transmit(huart2, (uint8_t*)buf, strlen(buf), HAL_MAX_DELAY); } HAL_Delay(100); // 每 100ms 读取一次 } } // EXTI0 中断服务程序 void EXTI0_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0); } // HAL EXTI 回调被 HAL_GPIO_EXTI_IRQHandler 调用 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin GPIO_PIN_0) { PwmIn_IRQHandler(); // 调用 PwmIn 的 ISR 处理逻辑 } }4.2 FreeRTOS 任务集成在 RTOS 环境中可将PwmIn数据通过队列传递给处理任务避免主循环阻塞QueueHandle_t xPwmQueue; typedef struct { uint32_t period_us; uint32_t high_us; uint16_t duty; } PwmData_t; void PwmInTask(void *pvParameters) { PwmData_t data; for(;;) { if (xQueueReceive(xPwmQueue, data, portMAX_DELAY) pdPASS) { // 在此处执行控制算法如 PID 调节电机速度 float speed_ref (float)data.duty / 1000.0f * MAX_SPEED; SetMotorSpeed(speed_ref); } } } // 修改 ISR改为向队列发送数据 void PwmIn_IRQHandler(void) { // ... 原有状态机逻辑 ... if (s_state STATE_WAIT_RISE s_period_us 0) { PwmData_t data { .period_us s_period_us, .high_us s_high_us, .duty (s_high_us * 1000UL) / s_period_us }; BaseType_t xHigherPriorityTaskWoken pdFALSE; xQueueSendFromISR(xPwmQueue, data, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } } // 创建队列与任务 xPwmQueue xQueueCreate(10, sizeof(PwmData_t)); xTaskCreate(PwmInTask, PwmInTask, configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY 2, NULL);5. 性能边界与工程调优指南5.1 最高可测频率分析PwmIn的理论上限由 ISR 执行时间决定。以 STM32F407168 MHz为例DWT 时间戳读取1 周期GPIO 电平读取2 周期LDRLDRB状态判断与赋值约 8 周期总 ISR 开销 ≈ 12 周期 71.4 ns。这意味着最小可测周期 ≥ 2 × ISR 开销 142.8 ns→最高频率 ≈ 7 MHz实际工程中为留出余量并兼容抖动推荐最大频率为1 MHz周期 1 µs对于 50 Hz ~ 1 kHz 的主流应用伺服、RC精度达0.1% 占空比误差168 MHz。5.2 抗干扰与鲁棒性增强毛刺过滤在 ISR 中增加简单数字滤波例如要求同一电平持续 2 个连续中断才确认边沿需扩展状态机超时复位添加看门狗定时器若s_state在STATE_WAIT_*持续超过2×max_expected_period则调用PwmIn_Reset()多周期平均主循环中缓存 N 个周期数据用滑动平均降低随机噪声适用于低频信号。5.3 资源占用实测ARM GCC -O2项数值代码大小.text312 字节RAM 占用全局变量16 字节最大堆栈深度ISR12 字节中断延迟从引脚变化到 ISR 第一行≤ 120 ns该资源消耗使其可无缝集成至 Cortex-M0如 STM32G0等超低功耗平台。6. 常见问题与故障排查现象可能原因解决方案PwmIn_IsValid()始终返回falseGPIO 模式未设为IT_RISING_FALLINGEXTI 线未正确映射中断未使能检查HAL_GPIO_Init()参数确认SYSCFG_EXTILineConfig()调用F4/F7检查HAL_NVIC_EnableIRQ()周期值跳变剧烈时间戳源频率与SystemCoreClock不匹配信号存在严重抖动校准SystemCoreClock改用 DWT增加硬件 RC 滤波10 kΩ 100 pF占空比恒为 0 或 1000s_high_us或s_period_us未正确更新状态机卡死在 ISR 中添加__NOP()并用调试器单步跟踪状态变量检查s_state是否陷入IDLE多个PwmIn实例冲突全局变量未隔离中断向量共用为每个实例创建独立的.c/.h文件将静态变量改为结构体成员或使用宏生成实例化代码终极验证方法用逻辑分析仪捕获 PA0 波形同时读取PwmIn_GetPeriodUs()输出二者数值偏差应稳定在 ±1 µs 内。若偏差超限必为时间基准配置错误或主频定义不准。