一、学习背景
最近弄一个二氧化碳传感器,使用的是Senseair S8-PWM型的传感器,这个传感器输出是以方波输出,也就是测量到的二氧化碳浓度会以方波形式输出,其中高电平的时间长短就和二氧化碳浓度有一定的关系,所以需要使用到定时器的输入捕获功能。
二、核心原理
输入捕获就像给单片机装了一个“自动秒表”。
CNT (计数器):一直在不停地数数(比如每 1us 数一下)。
边沿检测:当引脚上出现设定的电平跳变(比如上升沿)时。
捕获动作:硬件瞬间把当前 CNT 的数值“拍照”并存入 CCR (捕获寄存器) 中。
-
计算:我们读取 CCR 的值,做差运算,就能算出两个边沿之间经过了多少时间。
工作原理演示
三、cubemx的初始化配置
1、选定定时器:例如 TIM1。
2、通道设置:
Channel x: 选择 Input Capture direct mode (直连模式)。
(进阶) 如果想硬件自动测频率占空比,可选 PWM Input on CH1 (需占用两个通道)。3、参数配置 (Parameter Settings):
PSC (预分频器):决定“秒表”的精度。
推荐:配置为 72-1 (假设主频72MHz),这样 1 tick = 1us,方便计算。
ARR (重装载值):决定“秒表”的量程。
推荐:设置为最大 65535 (0xFFFF),防止频繁溢出导致逻辑复杂。
Polarity (极性):默认 Rising Edge (上升沿)。
Input Filter (滤波):非常重要! 如果信号有毛刺,建议设置为 10~15。4、中断设置 (NVIC):
勾选 TIM1 capture compare interrupt (捕获中断)。
勾选 TIM1 update interrupt (溢出中断,测低频/长周期信号必选)。

四、 软件逻辑 (单通道测频率 & 占空比)
使用“状态机”思想,在中断回调中手动切换极性:
状态 0 (周期开始):
捕获上升沿,记为 T1。
清零溢出计数 (Overflow_Count = 0)。
动作:将极性改为下降沿捕获。
状态 1 (高电平结束):
捕获下降沿,记为 T2。
计算高电平时间:High = T2 + (Overflow * 65536) - T1。
动作:将极性改回上升沿捕获。
状态 2 (周期结束):
捕获上升沿,记为 T3。
计算周期:Period = T3 + (Overflow * 65536) - T1。
计算结果:Freq = 1M / Period,Duty = High / Period。
五、代码
/* ==========================================================
区域 1: 在 main.c 顶部的 Includes 和 变量定义区
========================================================== */
#include <stdio.h>
// --- 全局变量定义 ---
volatile uint32_t IC_Val1 = 0;
volatile uint32_t IC_Val2 = 0;
volatile uint32_t IC_Val3 = 0;
volatile uint32_t High_Time = 0;
volatile uint32_t Period_Time = 0;
volatile uint32_t Overflow_Count = 0;
volatile uint8_t Capture_State = 0;
volatile uint8_t Data_Ready_Flag = 0;
float Frequency = 0;
float Duty_Cycle = 0;
// ... (串口重定向代码保持不变,请确保已添加) ...
/* ==========================================================
区域 3: 在 main() 函数初始化部分
========================================================== */
// 启动定时器更新中断(处理溢出)
HAL_TIM_Base_Start_IT(&htim1);
// 启动输入捕获中断
HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_1);
printf("STM32 Input Capture Restored (Basic Version)\r\n");
/* ==========================================================
区域 4: 在 while(1) 循环打印区
========================================================== */
if (Data_Ready_Flag == 1) {
// 打印结果
printf("Freq: %.2f Hz, Duty: %.2f %%\r\n", Frequency, Duty_Cycle);
Data_Ready_Flag = 0;
HAL_Delay(500);
}
/* ==========================================================
区域 5: 中断回调函数 (逻辑核心 - 基础版)
========================================================== */
// 1. 定时器溢出中断
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM1) {
Overflow_Count++;
}
}
// 2. 输入捕获中断
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {
// --- 状态 0: 捕获第一个上升沿 (周期开始) ---
if (Capture_State == 0) {
IC_Val1 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
Overflow_Count = 0;
Capture_State = 1;
// 切换极性:先关 -> 改极性 -> 清标志 -> 再开
htim->Instance->CCER &= ~TIM_CCER_CC1E;
__HAL_TIM_SET_CAPTUREPOLARITY(&htim1, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING);
__HAL_TIM_CLEAR_FLAG(&htim1, TIM_FLAG_CC1);
htim->Instance->CCER |= TIM_CCER_CC1E;
}
// --- 状态 1: 捕获下降沿 (高电平结束) ---
else if (Capture_State == 1) {
IC_Val2 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
// 计算高电平时间
High_Time = IC_Val2 + (Overflow_Count * 65536) - IC_Val1;
Capture_State = 2;
// 切换极性:先关 -> 改极性 -> 清标志 -> 再开
htim->Instance->CCER &= ~TIM_CCER_CC1E;
__HAL_TIM_SET_CAPTUREPOLARITY(&htim1, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);
__HAL_TIM_CLEAR_FLAG(&htim1, TIM_FLAG_CC1);
htim->Instance->CCER |= TIM_CCER_CC1E;
}
// --- 状态 2: 捕获第二个上升沿 (周期结束) ---
else if (Capture_State == 2) {
IC_Val3 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
// 计算周期
Period_Time = IC_Val3 + (Overflow_Count * 65536) - IC_Val1;
if (Period_Time > 0) {
// 默认 1MHz 时钟 (1us/tick)
Frequency = 1000000.0f / Period_Time;
Duty_Cycle = ((float)High_Time / Period_Time) * 100.0f;
Data_Ready_Flag = 1;
}
Capture_State = 0;
Overflow_Count = 0;
}
}
}
二氧化碳传感器转换的代码
if (Data_Ready_Flag == 1) {
// 1. 将高电平时间转换为毫秒 (1us = 0.001ms)
float Thigh_ms = High_Time / 1000.0f;
// 2. 根据手册公式计算: PPM = 2 * Thigh_ms - 4
CO2_PPM = 2.0f * Thigh_ms - 4.0f;
// 3. 打印结果
// 正常情况下,Cycle应为 ~1004ms,Freq应为 ~0.99Hz
printf("Cycle: %lu ms | High: %.1f ms | CO2: %.0f ppm\r\n",
Period_Time/1000, Thigh_ms, CO2_PPM);
Data_Ready_Flag = 0;
HAL_Delay(500);
}
六、测试




遇到的问题易错点总结
定时器溢出坑 (长周期信号)
现象:高频准,低频不准(例如测 1Hz 或 CO2 传感器的 1秒周期)。
原因:16位定时器最大只能数 65535。以 1us/tick 计算,65ms 就溢出了。测量 1秒 的信号需要溢出 15 次。
解决:
开启 Update 中断。
在HAL_TIM_PeriodElapsedCallback中统计Overflow_Count。
计算时间公式必须加上Overflow_Count * 65536。
网上大部分都只是设计采集高频的信号,没有考虑低频信号溢出的问题
硬件电气坑 (最隐蔽!)
现象:代码逻辑完美,但无法测出合理的值。我使用示波器测发现波形的幅值是1.37V,我接其他引脚是正常,唯独接这个输入捕获的引脚电就只有1.37V。
原因:传感器 SenseAir S8是 Open Drain (开漏) 输出,且内部上拉能力极弱。STM32 的引脚如果配置了下拉或保护电路导通,会拉低电压,导致低于 STM32 的(高电平门限,通常 > 2.0V)。
解决:要么必须在信号线和 3.3V 之间接一个外部上拉电阻 (4.7kΩ 或 10kΩ);要么修改代码配置为GPIO_NOPULL,(cubemx配置出来的默认是GPIO_InitStruct.Pull = GPIO_PULLDOWN;
)
调试过程中还有一个弱智的问题,我一直使用的是手里的exlink多功能调试器输出波形,因为刚用,我用示波器简单测试了一下输出pwm的功能,测试了10K,1K等,唯独没有测低频的。然后我在调试的时候发现设置150HZ以下就不行,我一直因为是32的代码没有配置好,一直读不到正确的频率和占空比。折腾了两天,结果今天我不知道什么原因我说怀疑一下是不是本来输出的波形就不对,我用示波器测了一下这玩意的波形,结果确实是这个exlink的输出就不对!!!!
