【STM32】【STM32CubeMX】定时器输入捕获与超声波测距实战:从配置到代码实现

📅 发布时间:2026/7/11 18:54:03 👁️ 浏览次数:
【STM32】【STM32CubeMX】定时器输入捕获与超声波测距实战:从配置到代码实现
1. 超声波测距与STM32为什么选择输入捕获大家好我是老李一个在嵌入式领域摸爬滚打了十多年的工程师。今天想和大家聊聊一个非常经典且实用的项目用STM32的定时器输入捕获功能来实现超声波测距。很多刚接触STM32的朋友一听到“输入捕获”、“定时器中断”这些词可能就有点发怵觉得太底层、太复杂。其实不然只要你理解了它的工作原理再借助像STM32CubeMX这样的图形化配置工具你会发现这件事儿比想象中简单得多而且做出来的效果非常稳定可靠。超声波测距简单来说就是利用超声波在空气中的传播速度是已知的约340米/秒通过测量超声波从发射到遇到障碍物再返回的时间就能计算出距离。这个“时间”的测量就是我们今天要解决的核心问题。你可能会问为什么非要用定时器的输入捕获功能呢我用普通的GPIO引脚在程序里用循环去检测高电平的持续时间不行吗理论上可以但实测下来精度和稳定性会差很多。因为超声波模块返回的高电平脉冲宽度通常在几百微秒到几十毫秒之间用软件延时或循环去计时很容易被其他中断或任务打断导致测量误差巨大甚至完全不可用。而STM32的定时器输入捕获功能就是专门为精确测量脉冲宽度或频率而生的硬件模块。它能在检测到引脚上特定的边沿比如上升沿或下降沿时自动“捕获”当前定时器的计数值并产生中断通知CPU。整个过程由硬件完成不受软件运行状态的影响精度可以达到微秒甚至纳秒级别。对于超声波测距这种对时间敏感的应用简直是绝配。接下来我就手把手带你从硬件连接到CubeMX配置再到代码编写完整地走一遍这个流程。我保证只要你跟着做一定能做出一个测量准确、反应迅速的超声波测距仪。2. 硬件准备与连接给STM32找个好搭档工欲善其事必先利其器。在开始写代码之前我们得先把硬件平台搭建好。这个项目需要的硬件非常简单核心就三样一块STM32开发板我用的是STM32F103C8T6也就是常说的“蓝色小药丸”性价比极高、一个超声波模块HC-SR04最经典的一款、以及几根杜邦线。如果你手头是其他型号的STM32比如F4、L4系列原理也完全一样跟着配置就行。我们先来看看超声波模块HC-SR04。它长得像一对大眼睛有四个引脚VCC、GND、Trig触发和Echo回响。它的工作原理是这样的我们通过STM32的某个GPIO引脚给Trig引脚一个至少10微秒的高电平脉冲这个脉冲就像扣动了扳机模块内部就会发射出一串40kHz的超声波。如果前方有障碍物超声波就会被反射回来模块接收到回波后会在Echo引脚输出一个高电平。关键点来了这个高电平的持续时间正好等于超声波从发射到返回所经历的时间。所以我们只要精确测出Echo引脚高电平的宽度就能算出距离了。计算公式很简单距离 (高电平时间 * 声速) / 2。因为声音走了一个来回所以要除以2。接下来是连接。以STM32F103C8T6为例我们需要用到两个GPIO引脚触发引脚 (Trig)我们可以任意选一个普通的GPIO比如PA1配置为推挽输出模式。它负责发送那个10us的触发脉冲。回响引脚 (Echo)这个引脚必须连接到一个具有输入捕获功能的定时器通道上。我强烈推荐使用TIM3的通道1它对应的引脚是PA6对于大多数STM32F1系列。为什么是TIM3因为它是一个16位通用定时器功能齐全且引脚布局方便。将HC-SR04的Echo引脚接到PA6。接线总结一下HC-SR04.VCC → 开发板5VHC-SR04.GND → 开发板GNDHC-SR04.Trig → STM32 PA1 (或其他你喜欢的GPIO)HC-SR04.Echo → STM32 PA6 (TIM3_CH1)硬件连接好后建议你先用万用表测一下供电是否正常。很多新手容易犯的错误是把模块的VCC接到了开发板的3.3V上导致HC-SR04供电不足工作不稳定甚至不工作。HC-SR04需要5V供电请务必确认。硬件部分就这么简单接下来我们进入重头戏——软件配置。3. STM32CubeMX配置详解图形化点一点底层不用管现在打开我们的神器——STM32CubeMX。它的最大好处就是让我们摆脱了繁琐的寄存器配置通过图形化界面勾勾选选就能生成完整的初始化代码。我们一步一步来我会把每个配置选项背后的原因都讲清楚。第一步创建工程与时钟配置。打开CubeMX选择“New Project”在芯片选择器里输入你的型号比如STM32F103C8。选中后我们首先配置时钟。在“Pinout Configuration”标签页找到“RCC”选项。如果你的开发板有外部高速晶振通常8MHz就在“High Speed Clock (HSE)”选择“Crystal/Ceramic Resonator”。然后转到“Clock Configuration”标签页配置时钟树。对于F103我们可以将系统时钟SYSCLK设置为72MHz。这一步是为了让芯片跑在最高性能当然你也可以用默认的内部时钟但外部晶振更准。第二步配置触发引脚普通GPIO。在芯片引脚图上找到PA1或者你选择的Trig引脚左键点击它在弹出的菜单中选择“GPIO_Output”。然后在左侧的“System Core” - “GPIO”设置中点击这个引脚将其默认输出电平设为低电平模式就是推挽输出上下拉电阻选择无即可。这个引脚很简单就是用来输出一个脉冲的。第三步核心配置——定时器TIM3的输入捕获。这是整个配置的灵魂。在左侧“Timers”目录下找到TIM3并点击。首先我们需要选择时钟源。在“Clock Source”选项卡选择“Internal Clock”内部时钟。然后切换到“Channel1”选项卡这里我们要将其配置为输入捕获模式。点击下拉菜单选择“Input Capture direct mode”输入捕获直接模式。接下来点击“Parameter Settings”选项卡这里有一堆参数别慌我们一个个看Prescaler (PSC - 预分频器)这是决定定时器计数频率的关键。我们的目标是让定时器每1微秒计数一次这样捕获到的时间值单位就是微秒计算距离非常方便。TIM3的时钟来源于APB1总线。在72MHz系统时钟下APB1时钟是36MHz但经过定时器倍频后TIM3的实际输入时钟是72MHz。要让72MHz的时钟变成1MHz周期1us预分频值应设置为72 - 1 71。所以这里填71。Counter Mode (计数模式)选择“Up”向上计数。Counter Period (ARR - 自动重装载值)这是定时器计数的上限计数值达到这个数就会溢出归零。对于16位定时器最大值是65535。超声波测距最远一般测到4米左右对应的高电平时间大约为(4m * 2) / 340m/s ≈ 23.5ms也就是23500us。为了留有余量我们可以设置为50000。但要注意如果测量更远的距离这个值要相应增大。这里我们先设为50000 - 1 49999。Auto-reload preload (自动重装载预装载)使能Enable。这可以让修改ARR的操作更平滑。然后在下方找到“Input Capture Channel 1”的配置ICPolarity (捕获极性)选择“Rising Edge”上升沿。这意味着定时器将首先在Echo引脚出现上升沿从低变高时触发捕获。IC Selection (捕获选择)选择“Direct”直接表示通道直接映射到输入引脚。IC Prescaler (捕获预分频)选择“No division”不分频。每个边沿都捕获。IC Filter (输入滤波器)设置为0。滤波器可以滤除毛刺但如果设置过大可能会滤掉正常的短脉冲。对于超声波模块输出比较干净设为0即可。第四步开启定时器中断。光有硬件捕获还不够我们需要在捕获发生时让CPU知道。点击“NVIC Settings”选项卡找到“TIM3 global interrupt”勾选“Enabled”。优先级可以保持默认。这样每次在PA6上检测到我们设定的边沿时TIM3就会产生一个中断我们的代码就能在中断服务函数里处理捕获到的值了。第五步生成代码。点击右上角的“Project Manager”选项卡给你的工程起个名字选择好保存路径切记路径不要有中文在“Toolchain / IDE”里选择你用的IDE比如MDK-ARM (Keil)。然后点击“Generate Code”。CubeMX会自动生成一个完整的Keil工程所有外设的初始化代码都准备好了。是不是感觉省了大力气但这只是骨架灵魂还得我们自己去填充。4. 代码实战上编写超声波触发与核心中断逻辑生成了代码我们用Keil打开工程。CubeMX生成的代码结构很清晰用户代码要写在/* USER CODE BEGIN */和/* USER CODE END */之间这样下次用CubeMX重新生成代码时我们的代码不会被覆盖。我们先从简单的触发函数开始。首先实现一个微秒级延时函数。因为给Trig引脚的高电平脉冲只需要10us以上我们需要一个比较精确的微秒延时。HAL库提供了HAL_Delay()但那是毫秒级的。我们可以用定时器或者简单的空循环来实现。这里用一个简单的循环函数放在gpio.c文件的用户代码区/* USER CODE BEGIN 2 */ // 微秒级延时函数适用于72MHz系统时钟粗略延时 void Delay_us(uint32_t us) { uint32_t delay us * 9; // 72MHz下这个系数需要根据实际测试调整 while(delay--) { __NOP(); // 执行空操作 } } /* USER CODE END 2 */注意这种循环延时并不精确受编译器优化和中断影响。但对于10us~50us的触发脉冲是足够的。如果追求高精度可以用一个专门的定时器来做精确定时。接着编写触发函数HC_SR04_Trigger()同样放在gpio.cvoid HC_SR04_Trigger(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); // 拉高Trig引脚 Delay_us(20); // 维持高电平20us满足模块要求 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); // 拉低Trig引脚 }接下来是重头戏输入捕获中断回调函数。这个函数是测距的核心逻辑所在。CubeMX已经帮我们生成了定时器初始化和中断开关的代码我们需要重写HAL库提供的输入捕获回调函数。打开tim.c文件找到/* USER CODE BEGIN 0 */区域我们先定义几个全局变量来存储状态和数据/* USER CODE BEGIN 0 */ volatile uint32_t capture_start 0; // 记录上升沿捕获时的定时器值 volatile uint32_t capture_width 0; // 存储高电平脉宽单位定时器计数次数 volatile uint8_t capture_flag 0; // 测量完成标志0-未完成1-已完成 /* USER CODE END 0 */然后在/* USER CODE BEGIN 1 */区域我们重写回调函数void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { // 确保是TIM3通道1的中断 if (htim-Instance TIM3 htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { if (capture_flag 0) { // 第一次进入是上升沿捕获 capture_start HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 读取捕获值 // 立刻改变捕获极性为下降沿为下一次捕获做准备 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); capture_flag 1; // 表示已捕获到上升沿 } else if (capture_flag 1) { // 第二次进入是下降沿捕获 uint32_t capture_end HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 计算高电平期间的计数值。注意定时器可能溢出过 if (capture_end capture_start) { capture_width capture_end - capture_start; } else { // 发生了溢出计算时要考虑ARR值 capture_width (htim-Instance-ARR - capture_start) capture_end 1; } // 停止捕获一次测量完成 HAL_TIM_IC_Stop_IT(htim, TIM_CHANNEL_1); // 重置捕获极性为上升沿为下一次测量做准备 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); capture_flag 2; // 表示测量完成数据就绪 } } }这段代码的逻辑是典型的“上升沿-下降沿”捕获流程。第一次中断上升沿记录起始时间并立刻切换为等待下降沿第二次中断下降沿记录结束时间计算出脉宽。这里我加入了对定时器溢出的处理这是一个很重要的细节。因为如果障碍物很远高电平时间可能超过定时器从0计数到ARR的时间导致计数值“回转”。上面的判断和计算能正确处理这种情况。写好这个回调函数硬件部分的工作就完成了80%。5. 代码实战下主循环逻辑与距离计算有了触发函数和中断处理逻辑我们还需要在主程序中组织整个测距流程。打开main.c文件。首先在/* USER CODE BEGIN Includes */后面我们包含一下标准输入输出头文件方便用printf打印结果前提是你已经重定向了串口。/* USER CODE BEGIN Includes */ #include stdio.h /* USER CODE END Includes */然后在main函数中找到/* USER CODE BEGIN 2 */区域这里我们进行外设启动和初始触发/* USER CODE BEGIN 2 */ printf(HC-SR04 Distance Measurement Test Start!\r\n); // 先启动一次触发开始第一次测量 HC_SR04_Trigger(); // 启动TIM3的输入捕获中断初始极性是上升沿在CubeMX中设置的 HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1); /* USER CODE END 2 */接下来我们进入主循环while (1)。在/* USER CODE BEGIN 3 */区域编写测距循环逻辑/* USER CODE BEGIN 3 */ if (capture_flag 2) // 如果一次测量完成 { // 计算距离。capture_width的单位是“定时器计数次数”每个计数代表1us因为我们预分频设成了71 // 距离(cm) (时间(us) * 声速(cm/us)) / 2 // 声速340m/s 34000cm/s 0.034cm/us float distance_cm (capture_width * 0.034) / 2.0; // 过滤无效数据。HC-SR04的有效测距范围一般是2cm~400cm if (distance_cm 2.0 distance_cm 400.0) { printf(Distance: %.2f cm\r\n, distance_cm); } else { printf(Out of range or error.\r\n); } // 准备下一次测量 capture_flag 0; // 清除完成标志 HAL_Delay(60); // 等待至少60ms防止上一次回波干扰。HC-SR04测量周期建议60ms HC_SR04_Trigger(); // 再次发送触发脉冲 // 重新启动输入捕获中断此时极性已在上次回调中被重置为上升沿 HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1); } /* USER CODE END 3 */这段主循环逻辑清晰明了等待测量完成标志计算并打印距离然后延时一段时间后开始下一次测量。这里有几个我踩过坑的要点测量周期HC-SR04模块在一次测量结束后需要一段时间才能进行下一次测量。数据手册建议周期不低于60ms否则可能会收到上一次的回波导致数据错乱。所以这里加了HAL_Delay(60)。数据过滤超声波模块在近距离2cm或超出量程时返回的数据可能不稳定或是一个固定值比如33ms高电平。通过一个有效范围判断可以滤掉这些无效数据让输出更干净。标志位管理capture_flag这个变量是关键它沟通了中断服务函数和主循环。在中断里设置为2在主循环里检测到2后处理数据并重置为0。注意这个变量要用volatile修饰防止编译器优化出错。6. 进阶优化与避坑指南基本的测距功能已经实现了但如果你想让它更稳定、更专业或者在实际项目中应用这里还有一些进阶的优化点和常见的坑需要注意。第一点提高测量精度与处理溢出。我们之前设置的ARR是49999对应50ms。如果测量距离超过(0.034 * 50000 / 2) ≈ 8.5米定时器就会溢出。虽然我们的中断处理代码考虑了溢出但更稳妥的做法是开启定时器的更新溢出中断。在CubeMX的TIM3配置中NVIC设置里也勾选“TIM3 update interrupt”。然后在stm32f1xx_it.c的中断服务函数里或者在TIM3的更新中断回调函数HAL_TIM_PeriodElapsedCallback中对一个全局的溢出计数器进行累加。在计算总时间时将溢出次数乘以ARR值也加进去。这样就能实现超长距离的无忧测量。第二点消除测量误差。声速受温度和湿度影响。340m/s是15℃干燥空气中的值。如果对精度要求高可以加入温湿度传感器如DHT11进行声速补偿。公式可以简化为V 331.4 0.6 * T其中T是摄氏温度。另外模块本身也有几毫米的固有误差可以通过在固定距离下实测然后给计算结果加一个固定的校准偏移量。第三点使用DMA替代中断。对于需要极高频率采样或者主循环非常繁忙的系统频繁进入输入捕获中断每次测量进两次可能带来负担。此时可以考虑使用定时器的输入捕获DMA模式。当捕获事件发生时硬件会自动把捕获寄存器的值通过DMA搬运到指定的内存数组中完全不需要CPU干预。你只需要在DMA传输完成一半或全部的回调函数里处理一批数据即可。这在多路超声波同时测距时尤其有用。第四点CubeMX配置的常见坑。我遇到过不少朋友配置完后发现捕获不到信号。除了检查接线请务必确认以下几点GPIO模式用于输入捕获的引脚如PA6在CubeMX中必须是复用功能模式而不能是普通的输入模式。CubeMX在配置定时器通道时会自动设置你千万不要手贱再去把它改成“GPIO_Input”。引脚冲突确保你选的定时器通道引脚没有被其他外设占用比如同时配置成了UART或SPI。中断优先级如果系统中有其他高优先级、长时间执行的中断比如某些复杂算法的处理可能会阻塞定时器中断导致捕获值不准。可以适当提高定时器中断的优先级。第五点软件滤波。在实际环境中偶尔可能会出现一两次跳变的异常值。我们可以在软件中做一下平滑滤波比如连续采样5次去掉一个最大值和一个最小值然后对剩下的3个值取平均最后输出这个平均值。这样显示出来的距离值就会非常稳定不会跳来跳去。#define SAMPLE_SIZE 5 float distance_buffer[SAMPLE_SIZE]; uint8_t buffer_index 0; // 在主循环中将计算出的有效distance_cm存入缓冲区 distance_buffer[buffer_index] distance_cm; buffer_index (buffer_index 1) % SAMPLE_SIZE; // 当缓冲区满后进行排序和平均滤波这里省略排序代码 // ... 排序并去掉头尾 ... float filtered_distance (distance_buffer[1] distance_buffer[2] distance_buffer[3]) / 3.0; printf(Filtered Distance: %.2f cm\r\n, filtered_distance);把这些优化点都考虑进去你的超声波测距项目就从“能用”升级到了“好用”和“稳定”的级别。嵌入式开发就是这样实现基础功能只是第一步不断的调试、优化和解决实际环境中遇到的问题才是真正积累经验的过程。希望这篇长文能帮你彻底搞懂STM32的输入捕获并能举一反三应用到其他需要精确计时的项目中去。