三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

STM32超声波测距避障系统:从硬件驱动到智能决策全解析

STM32超声波测距避障系统:从硬件驱动到智能决策全解析

1. 项目概述:从“听到”到“避开”的距离艺术

搞嵌入式开发的朋友,对超声波测距这个经典课题肯定不陌生。它就像是给机器装上了一双能“听”的耳朵,通过发射和接收超声波,精准地计算出前方障碍物的距离。这次要聊的,就是基于STM32单片机实现超声波测距,并最终导向一个更实用的目标——避障。这不仅仅是点亮一个LCD屏显示几个数字那么简单,而是要把这个距离信息,转化为一套可靠的决策逻辑,让小车、机器人或者任何移动设备能自主绕开障碍物。无论是做智能小车、扫地机器人原型,还是工业上的非接触式检测,这套技术栈都是非常核心的基础。如果你手头正好有一块STM32开发板、一个HC-SR04模块和几个电机,那么跟着这篇从原理到代码再到策略的完整梳理,你就能搭建出一个反应灵敏的避障系统。我会把过程中那些数据手册里不会写的滤波技巧、时机把控的坑,以及如何让代码既稳定又易于维护的心得,都摊开来细说。

2. 核心硬件选型与电路连接解析

2.1 超声波模块:HC-SR04的里里外外

市面上最常见的超声波测距模块非HC-SR04莫属,性价比高,资料也多。它的工作原理是典型的“发射-接收-计时”模式。模块有四个引脚:VCC、Trig(触发)、Echo(回响)和GND。

其工作时序是核心:单片机给Trig引脚一个至少10us的高电平脉冲,模块内部便会自动发射一组8个40kHz的超声波脉冲。发射结束后,Echo引脚会拉高,直到模块接收到返回的回波后,Echo引脚才拉低。因此,Echo引脚高电平的持续时间,就是超声波从发射到返回的总时间。

这里有个关键计算:声音在空气中速度约为340m/s(即34000cm/s)。距离等于速度乘以时间,但注意Echo高电平时间t是超声波“一去一回”的总时间,所以单程距离d = (34000 * t) / 2,单位是厘米。更常用的公式是d = (t * 0.034) / 2(t单位us),或者d = t * 0.017(t单位us,d单位cm)。理解这个公式,是后续所有代码的基础。

注意:HC-SR04的测量范围官方标称2cm-400cm,但实际应用中,低于3cm时回波可能直接与发射波叠加,导致无法识别;超过3-4米后,回波信号过于微弱,测量也会不稳定。模块的测量角度约为15度,这意味着它探测的是一个锥形区域,而不是一个点。

2.2 STM32的接口策略:GPIO与定时器的黄金组合

STM32驱动HC-SR04,通常有两种主流方式,各有利弊。

方案一:普通GPIO配合输入捕获这是最直观的方法。用一个GPIO(如PA0)连接Trig,配置为推挽输出,用于发送触发信号。用另一个GPIO(如PA1)连接Echo,配置为浮空输入或上拉输入,用于检测高电平。

  1. 触发:将Trig引脚拉高,延时至少10us(用HAL_Delay()微秒级延时不太准,通常用简单的for循环空指令实现),然后拉低。
  2. 计时:在拉低Trig的同时,开启一个定时器(如TIM2)的计数器。然后不断查询Echo引脚的电平。
  3. 捕获:当检测到Echo变为高电平时,记录定时器当前值t1;当Echo变为低电平时,记录值t2。那么高电平持续时间t = (t2 - t1) * 定时器计数周期

这种方式逻辑清晰,但对主循环占用高(需要不断查询Echo),且计时精度受查询延迟和主循环其他任务影响。

方案二:全定时器驱动(推荐)这是更高效、更精准的做法,充分利用STM32定时器的输入捕获功能。我们以STM32F1的通用定时器TIM3为例:

  • Trig引脚:仍然使用一个普通GPIO输出触发脉冲。
  • Echo引脚:连接到TIM3的通道1(如PA6),并将该通道配置为输入捕获模式。
  • 工作流程
    1. 单片机通过GPIO给Trig一个10us高脉冲。
    2. 超声波模块发出波,Echo变高。此时TIM3的输入捕获功能会在Echo上升沿自动触发,硬件捕获当前计数器值CNT到捕获比较寄存器CCR1中,并可以产生一个中断。
    3. 在上升沿中断里,我们可以清除标志位,并改变捕获极性为下降沿。
    4. 当回波返回,Echo变低时,硬件再次自动捕获当前CNT值到CCR1,并产生中断。
    5. 在下降沿中断里,我们读取CCR1的值,这个值就是两次捕获之间的计数差值,对应Echo高电平时间。计算时间t = 捕获差值 * (1 / 定时器时钟频率) * 预分频系数

这种方式几乎不占用CPU,计时精度由硬件保证,是产品级应用的首选。本文后续的代码实现将基于这种方案展开。

2.3 电源与滤波:稳定测量的基石

连接很简单,但细节决定成败。VCC接3.3V或5V,GND共地。但务必注意:

  • 电源去耦:在HC-SR04的VCC和GND引脚之间,尽可能靠近模块焊接一个10uF的电解电容和一个0.1uF的瓷片电容。超声波发射瞬间电流较大,此举可以滤除电源毛刺,防止电压跌落导致单片机复位或测量错误。
  • 信号线:如果Trig和Echo走线较长(超过10cm),或者环境中有电机等大电流设备,考虑在信号线上串联一个22-100欧姆的小电阻,有助于抑制信号振铃和过冲。
  • 共地:单片机与模块的GND必须可靠连接,这是所有逻辑电平正确识别的根本。

3. 软件驱动与精准测距实现

3.1 定时器输入捕获配置详解

我们使用STM32CubeMX进行初始化配置,以TIM3通道1为例。

  1. 时钟树:确保APB1定时器时钟(TIM3的时钟源)被正确使能。比如系统主频72MHz,APB1分频后为36MHz,但TIM3挂在APB1上,如果APB1预分频系数≠1,定时器时钟会倍频,所以TIM3的时钟可能是72MHz。这点在计算实际计数频率时要搞清楚。
  2. TIM3配置
    • Prescaler(预分频器):设置为71。如果时钟是72MHz,则分频后计数器时钟为72MHz / (71+1) = 1MHz,即计数器每1us递增1。这样捕获的计数值直接就是微秒数,方便计算。
    • Counter Period(自动重装载值):设置为最大值65535(对于16位定时器)。因为测量距离最大对应时间约23.5ms(400cm),即23500us,小于65535us,不会溢出。
    • Capture/Compare Channel 1:选择“Input Capture direct mode”。极性选择“Rising Edge”(初始捕获上升沿)。IC Filter(输入滤波器)可以设置为0,或根据噪声情况适当增加。
  3. NVIC设置:使能TIM3的全局中断。
  4. 生成代码:生成工程后,在main.cwhile(1)之前调用HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);启动输入捕获中断。

3.2 中断服务程序与距离计算

关键逻辑在HAL_TIM_IC_CaptureCallback()这个回调函数中实现。这个函数会在捕获事件发生时被调用。

// 定义全局变量 volatile uint32_t g_ultrasound_high_level_time_us = 0; // 高电平时间,单位us volatile uint8_t g_echo_received_flag = 0; // 回波接收完成标志 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { static uint32_t t1 = 0, t2 = 0; // 静态变量,用于保存两次捕获值 if (htim->Instance == TIM3) { if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) { // 判断当前是上升沿还是下降沿捕获 if (__HAL_TIM_GET_CAPTUREPOLARITY(&htim3, TIM_CHANNEL_1) == TIM_INPUTCHANNELPOLARITY_RISING) { // 上升沿捕获:记录起始时间,并切换为下降沿捕获 t1 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); __HAL_TIM_SET_CAPTUREPOLARITY(&htim3, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); __HAL_TIM_SET_COUNTER(&htim3, 0); // 可选:清空计数器,从0开始计,防止溢出计算复杂 } else { // 下降沿捕获:记录结束时间,计算时间差,并切换回上升沿捕获等待下一次 t2 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); __HAL_TIM_SET_CAPTUREPOLARITY(&htim3, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); // 计算高电平时间(考虑计数器溢出) if (t2 >= t1) { g_ultrasound_high_level_time_us = t2 - t1; } else { // 计数器溢出,t2是第二次溢出后的值 g_ultrasound_high_level_time_us = (0xFFFF - t1) + t2; } g_echo_received_flag = 1; // 设置标志位,通知主循环数据就绪 } } } }

在主循环中,我们只需要周期性地(比如每50ms)触发一次测量,然后等待标志位并计算距离:

void Trigger_Ultrasound(void) { HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET); delay_us(12); // 使用精准的微秒延时函数,12us略大于10us,确保可靠 HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET); } // 在主循环或一个定时任务中 if (measure_timer_expired) { // 测量周期到 Trigger_Ultrasound(); measure_timer_reset(); } if (g_echo_received_flag) { float distance_cm = g_ultrasound_high_level_time_us * 0.017f; // 使用浮点数计算 // 或者使用整数运算避免浮点: distance_cm = time_us * 17 / 1000; g_echo_received_flag = 0; // 后续进行滤波和避障决策 }

3.3 软件滤波算法:让数据“听话”

原始的超声波距离值会跳动,尤其是在被测物体表面不平或环境有轻微气流时。直接使用这个跳动值做避障,设备会“抽风”。必须滤波。

1. 限幅滤波(消除粗大误差)这是第一道防线。根据物理限制和上次有效值,设定一个合理的变化范围。

#define MAX_DELTA_CM 50 // 两次测量间最大允许变化量(厘米) float last_valid_distance = 0; float current_raw_distance = ...; // 原始计算值 if (fabs(current_raw_distance - last_valid_distance) > MAX_DELTA_CM) { // 变化过大,认为是无效数据(可能是误触发或丢失回波),丢弃或用上次值代替 current_filtered_distance = last_valid_distance; } else { // 变化在合理范围内,进入下一步滤波 last_valid_distance = current_raw_distance; }

2. 滑动平均滤波(抑制随机波动)最常用且有效的平滑方法。维护一个固定长度的数组,存储最近N次的有效采样值,输出为这些值的平均值。

#define FILTER_WINDOW_SIZE 5 float distance_window[FILTER_WINDOW_SIZE] = {0}; uint8_t window_index = 0; // 在获得一个有效的限幅后数据后 distance_window[window_index] = current_valid_distance; window_index = (window_index + 1) % FILTER_WINDOW_SIZE; // 计算平均值 float sum = 0; for (int i = 0; i < FILTER_WINDOW_SIZE; i++) { sum += distance_window[i]; } float final_distance = sum / FILTER_WINDOW_SIZE;

窗口大小N需要权衡:N越大,曲线越平滑,但响应越迟钝。对于移动避障,N=3到5是比较好的选择。

3. 中位值滤波(对抗突发干扰)有时会有一个明显的离群跳点。可以先采集3-5个数据,排序后取中间值作为本次输出,能很好地抵抗单次突发干扰,再结合滑动平均效果更佳。

在实际项目中,我通常采用“限幅 + 滑动平均”的组合拳,代码简单,效果足够应对大多数场景。如果环境特别恶劣,可以再加上中位值滤波作为前置。

4. 从测距到避障:决策逻辑与控制策略

有了稳定的距离数据,避障的核心就变成了决策逻辑。这里介绍几种从简单到复杂的策略。

4.1 基础阈值判断法

这是最简单的避障逻辑,适用于三轮小车(两个驱动轮+一个万向轮)的差速转向。

  • 设定两个阈值
    • DANGER_DISTANCE(危险距离,如10cm):必须立刻停止并后退。
    • WARNING_DISTANCE(警告距离,如20cm):需要转向避开。
  • 逻辑实现
#define DANGER_DIST 10.0f #define WARNING_DIST 20.0f void Obstacle_Avoidance_Decision(float dist) { if (dist <= DANGER_DIST) { // 紧急停止,并后退一段距离 Motor_Stop(); HAL_Delay(200); Motor_Backward(SPEED_SLOW, 300); // 后退300ms Motor_Stop(); HAL_Delay(200); // 随机或固定方向转弯 Motor_Turn_Left(SPEED_NORMAL, 500); } else if (dist <= WARNING_DIST) { // 减速并向一侧转向(如左边有障碍就右转) Motor_Turn_Right(SPEED_SLOW, 200); // 向右慢速转弯200ms // 转弯后继续前进 Motor_Forward(SPEED_NORMAL); } else { // 安全,直行 Motor_Forward(SPEED_NORMAL); } }

这种方法反应直接,但行为比较“愣”,容易在障碍物附近来回震荡。

4.2 多传感器融合与区域划分

单一超声波探头只能探测正前方一个锥形区域,存在很大盲区。更可靠的方案是使用多个超声波或结合红外传感器。

经典三探头布局:左、中、右各安装一个HC-SR04。

  • 区域划分:根据三个传感器的读数,将前方空间划分为“左路畅通”、“中路有障”、“右路畅通”等状态。
  • 决策矩阵:基于状态查表决定行为。
typedef enum { STATE_CLEAR_ALL, STATE_OBSTACLE_LEFT, STATE_OBSTACLE_CENTER, STATE_OBSTACLE_RIGHT, STATE_OBSTACLE_LEFT_CENTER, // ... 其他组合状态 } ObstacleState_t; ObstacleState_t Get_Obstacle_State(float dist_l, float dist_c, float dist_r) { if(dist_c < WARNING_DIST && dist_l > WARNING_DIST && dist_r > WARNING_DIST) return STATE_OBSTACLE_CENTER; else if(dist_c > WARNING_DIST && dist_l < WARNING_DIST && dist_r > WARNING_DIST) return STATE_OBSTACLE_LEFT; // ... 其他判断 } void Decision_By_State(ObstacleState_t state) { switch(state) { case STATE_OBSTACLE_CENTER: // 中间有障碍,比较左右哪边更通畅 if(g_dist_left > g_dist_right) { Motor_Turn_Left(SPEED_NORMAL, 400); } else { Motor_Turn_Right(SPEED_NORMAL, 400); } break; case STATE_OBSTACLE_LEFT: // 左边有障碍,向右转 Motor_Turn_Right(SPEED_SLOW, 300); break; // ... 其他状态处理 default: Motor_Forward(SPEED_NORMAL); break; } }

这种策略使得避障行为更加智能,能够绕开非正前方的障碍物。

4.3 引入状态机与行为优先级

为了让机器人的行为更平滑、更可控,可以引入有限状态机(FSM)。例如,定义几个状态:前进避障左转避障右转后退停止。每个状态有其进入条件、执行动作和退出条件。

typedef enum { ROBOT_FORWARD, ROBOT_AVOID_LEFT, ROBOT_AVOID_RIGHT, ROBOT_BACKWARD, ROBOT_STOP } RobotState_t; RobotState_t current_state = ROBOT_FORWARD; void Robot_FSM_Update(float dist_c, float dist_l, float dist_r) { switch(current_state) { case ROBOT_FORWARD: Motor_Forward(SPEED_NORMAL); if(dist_c < DANGER_DIST) { current_state = ROBOT_BACKWARD; enter_backward_time = HAL_GetTick(); } else if(dist_c < WARNING_DIST) { // 根据左右距离决定转向方向 current_state = (dist_l > dist_r) ? ROBOT_AVOID_LEFT : ROBOT_AVOID_RIGHT; avoid_start_time = HAL_GetTick(); } break; case ROBOT_AVOID_LEFT: Motor_Turn_Left(SPEED_SLOW, 0); // 持续左转 // 转向持续一段时间,或者检测到侧方安全后退出 if((HAL_GetTick() - avoid_start_time > 500) || (dist_l > SAFE_DIST_SIDE)) { current_state = ROBOT_FORWARD; } // 如果在转向过程中正面突然出现危险,立即切换到后退 if(dist_c < DANGER_DIST) { current_state = ROBOT_BACKWARD; enter_backward_time = HAL_GetTick(); } break; case ROBOT_BACKWARD: Motor_Backward(SPEED_SLOW, 0); if(HAL_GetTick() - enter_backward_time > 300) { // 后退300ms current_state = ROBOT_STOP; } break; case ROBOT_STOP: Motor_Stop(); HAL_Delay(200); // 停止后,选择障碍物较少的一侧转向 current_state = (dist_l > dist_r) ? ROBOT_AVOID_LEFT : ROBOT_AVOID_RIGHT; avoid_start_time = HAL_GetTick(); break; } }

状态机的引入,将复杂的逻辑判断分解到各个状态中,使代码结构清晰,易于调试和扩展,机器人行为也更有条理。

5. 系统集成、调试与性能优化

5.1 多任务调度与实时性保障

在一个完整的移动机器人系统中,超声波测距避障只是一个任务。还可能存在电机控制、编码器反馈、遥控接收、状态显示等任务。如何协调它们?

基于时间片的前后台系统:在main函数的while(1)循环中,通过定时器标志位来调度不同任务。

// 在定时器中断(如1ms中断)中设置标志位 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim->Instance == TIM4) { // 1ms定时器 sys_tick_1ms++; if(sys_tick_1ms % 10 == 0) flag_10ms_task = 1; if(sys_tick_1ms % 50 == 0) flag_50ms_task = 1; // 超声波测量周期 if(sys_tick_1ms % 100 == 0) flag_100ms_task = 1; } } // 在主循环中 while (1) { if(flag_10ms_task) { flag_10ms_task = 0; Motor_Control_Task(); // 电机PID控制 } if(flag_50ms_task) { flag_50ms_task = 0; Ultrasound_Measurement_Task(); // 触发测量、处理数据、执行避障决策 } if(flag_100ms_task) { flag_100ms_task = 0; Update_Display_Task(); // 刷新显示 } // ... 其他低优先级或非实时任务 }

确保超声波测量和决策任务的周期稳定(如50ms),避障反应时间就有保障。

5.2 现场调试技巧与问题排查

调试避障系统,我习惯分两步走:

第一步:确保测距准确

  1. 静态测试:将模块对准墙壁,用串口打印出原始时间值和计算出的距离。用卷尺实际测量对比。如果误差大(>1cm),检查:
    • 定时器时钟配置和预分频计算是否正确?1us计数是否精准?
    • 声速补偿:公式中的0.034是25°C下的值。温度变化时声速会变(v = 331.4 + 0.6 * T,T为摄氏温度)。如果对精度要求高,可以加入温度传感器(如DS18B20)进行实时补偿。
    • 测量盲区:物体太近(<3cm)时,回波和发射波重叠,模块无法分辨,会输出一个错误的大数值(或恒定值)。在软件中要加入最小值判断,例如小于3cm的数据直接丢弃或视为“极近”。
  2. 动态测试:用手或书本在模块前来回移动,观察输出值是否平滑变化,有无跳变到极大值或零值。出现跳变,往往是丢失回波(超过最大量程)或误触发。加强软件滤波的限幅环节。

第二步:验证避障行为

  1. 分步调试:先将避障决策部分的输出(如Motor_Turn_Left)改为串口打印指令(如printf(“Turn Left\n”)),不实际控制电机。在障碍物前移动,看打印的指令是否符合预期逻辑。
  2. 参数微调DANGER_DISTWARNING_DIST的设定需要根据机器人的刹车距离、转向半径和速度来调整。速度快,这两个值就要设大。在空旷场地,用纸箱作为障碍物,反复测试调整,找到既能灵敏避障又不至于“胆小如鼠”反复转向的阈值。
  3. 边界情况
    • 斜面障碍:超声波打到斜面,回波可能散射导致接收不到,或返回时间计算距离比实际垂直距离长。这可能导致撞上斜面障碍。可以考虑在很近的距离(如DANGER_DIST)内,无论测量值如何都强制触发紧急停止(冗余安全)。
    • 细小障碍物:如桌腿,超声波波束较宽可能绕过,导致漏检。这是超声波传感器的固有局限,对于此类应用,需要结合红外对管等点状检测传感器进行补充。

5.3 抗干扰与可靠性增强

工业或复杂环境中,干扰无处不在。

  1. 电气隔离:电机驱动电路与单片机控制电路之间,最好用光耦或电平转换芯片隔离。电机的PWM信号和超声波信号线尽量远离。
  2. 软件抗扰
    • 超时处理:在触发测量后,启动一个定时器(比如30ms)。如果在定时器超时前仍未收到回波(g_echo_received_flag未置位),则强制结束本次测量,避免程序死等。将本次距离视为“无穷远”或上一个有效值。
    • 多次测量取优:连续进行3次测量,剔除明显异常值(通过限幅滤波),然后对剩下的值取平均。这比单次测量稳定得多。
    • “冻结”处理:当机器人正在执行转向或后退动作时,可以暂时“冻结”超声波测距或忽略其输入,因为此时传感器指向的方向可能没有参考价值,避免错误决策。

5.4 进阶优化方向

当基础功能稳定后,可以考虑以下优化:

  • 动态阈值:根据机器人当前速度动态调整告警和危险距离。速度越快,安全距离越大。
  • 历史轨迹预测:结合编码器记录机器人的运动轨迹,当检测到障碍物时,不仅考虑当前距离,还结合运动趋势判断是否会撞上,实现更早、更平滑的避让。
  • 传感器融合:如前所述,超声波易受温湿度、材质影响,且对细小物体不敏感。可以融合红外、TOF(飞行时间)激光传感器,甚至低成本摄像头进行图像识别,取长补短,实现更鲁棒的环境感知。

从点亮一个超声波模块,到构建一个能自主、稳定避障的移动平台,中间每一步都充满了工程实践的细节。最深的体会是,硬件上的稳定是软件逻辑正确的前提,而一个清晰、可调试的状态机框架,往往比复杂的算法更能快速解决问题。调试时,一定要把数据“可视化”出来(通过串口绘图或屏幕显示),亲眼看到波形和距离的变化,比凭空想象要高效百倍。最后,别忘了给你的机器人加上一个手动急停开关,在代码失控时,它能救你的硬件一命。

← 返回列表