STM32F4超声波测距实现避障功能

你有没有遇到过这样的场景:机器人一头撞上墙,或者扫地机在沙发底下卡住动弹不得?😅 其实问题就出在“眼睛”不够亮——环境感知能力太弱。而今天我们要聊的这个小系统,正是让设备“睁眼看世界”的第一步: 用STM32F4 + HC-SR04实现超声波避障

别看它结构简单,背后可藏着不少工程智慧。从硬件时序到中断处理,再到滤波算法和动作决策,每一步都决定了你的小车是优雅绕行,还是原地懵圈💥。


超声波测距是怎么“听”出距离的?

我们先来聊聊那个小小的HC-SR04模块。它长得像一对“电子耳朵”,其实更像一个会喊话又会听回音的家伙。

工作流程特别像你在山谷里大喊一声:“喂——”,然后数着秒等“喂——”回来。只不过它用的是 40kHz的超声波 ,人耳听不见,但精度很高。

整个过程就这么几步:

  1. 我们给它的 Trig 脚发个 至少10μs的高电平 ,相当于说:“开始测!”
  2. 它自己啪啪啪连发8个超声波脉冲;
  3. 回波回来后,它的 Echo 脚就会变成高电平,持续的时间正好等于声音往返的时间;
  4. 我们只要算出这个高电平持续了多久,就能知道距离。

📏 距离公式长这样:

$$
\text{Distance (cm)} = \frac{\text{Time (μs)} \times 0.034}{2}
$$

别忘了除以2哦,因为这是来回一趟的距离!声音在空气中跑得差不多是340m/s,也就是0.034 cm/μs。

听起来挺简单的对吧?但真要让它稳定工作,可不是接上线就完事了。


硬件细节不能马虎

先看看HC-SR04的关键参数:

参数
工作电压 5V(注意!)
测量范围 2~400 cm
分辨率 0.3 cm
响应频率 ~60Hz(两次测量间隔 ≥60ms)

⚠️ 重点来了
虽然HC-SR04的 Trig 引脚可以接受3.3V逻辑输入(STM32输出刚好匹配),但它的 Echo 输出是 5V高电平 !而STM32F4的IO口最大只能承受3.6V……直接连上去?轻则读数不准,重则芯片报废😱。

✅ 解决方案有两个:
- 加个 分压电阻 (比如1kΩ+2kΩ),把5V降到3.3V左右;
- 或者上 电平转换芯片 (如TXS0108E),更稳妥但也更贵。

另外提醒一句:别让电机电源和传感器共地不干净,否则一启动电机,超声波就读出一堆“鬼影数据”👻。


时间测量靠谁?定时器才是幕后英雄!

你说,能不能用 HAL_Delay() 或for循环来测时间?理论上能,但实际上—— 误差大到没法用

为什么?因为软件延时受CPU调度、中断优先级影响太大。哪怕只差几个微秒,距离偏差就可能超过1厘米,这还怎么精准避障?

所以我们得请出STM32的大杀器: 输入捕获(Input Capture)功能

TIM2出手,精确捕捉边沿

我们选了TIM2通道1来监听 Echo 引脚的变化。配置要点如下:

htim2.Instance = TIM2;
htim2.Init.Prescaler = 84 - 1;        // 168MHz / 84 = 2MHz → 每tick = 0.5μs
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 0xFFFFFFFF;       // 32位自动重载
HAL_TIM_IC_Start(&htim2, TIM_CHANNEL_1);

预分频设为83(即84-1),这样定时器每0.5μs加一次数。再配合输入捕获中断,就能在上升沿和下降沿瞬间抓取时间戳。

动态切换边沿检测,代码这么写:

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
    if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {
        if (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) == GPIO_PIN_SET) {
            // 上升沿 → 开始计时
            echo_start = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
            __HAL_TIM_CLEAR_IT(&htim2, TIM_IT_CC1);
            // 改为下降沿触发
            __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING);
        } else {
            // 下降沿 → 结束计时
            echo_end = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
            pulse_duration = (echo_end - echo_start) * 0.5f;  // 单位:μs(因为每tick=0.5μs)
            distance_cm = (pulse_duration * 0.034) / 2.0f;
            __HAL_TIM_CLEAR_IT(&htim2, TIM_IT_CC1);
            // 恢复上升沿检测
            __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);
        }
    }
}

👉 这里有个小技巧:通过修改CCER寄存器动态切换捕获极性,避免使用外部中断或其他复杂机制,简洁高效!


微秒级延时怎么做?别用SysTick了!

你可能会想:触发信号只需要10μs高电平,随便Delay一下就行了吧?

但标准 HAL_Delay() 最小单位是1ms,显然不行。我们可以用另一个定时器(比如TIM5)来做微秒延时:

TIM_HandleTypeDef htim5;

void MX_TIM5_Init(void) {
    htim5.Instance = TIM5;
    htim5.Init.Prescaler = 84 - 1;     // 2MHz计数频率
    htim5.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim5.Init.Period = 0xFFFFFFFF;
    HAL_TIM_Base_Start(&htim5);
}

void delay_us(uint32_t us) {
    uint32_t start = __HAL_TIM_GET_COUNTER(&htim5);
    while ((__HAL_TIM_GET_COUNTER(&htim5) - start) < us);
}

这样调用就OK啦:

HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET);
delay_us(10);
HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET);

💡 小贴士:如果你追求极致精度,还可以开启DWT(Data Watchpoint and Trace)单元的Cycle Counter,直接读CPU周期数。不过移植性较差,适合调试阶段使用。


主控逻辑:从测距到避障决策

现在我们已经能稳定拿到前方距离了,接下来就是“大脑”做判断的时候了。

系统整体架构大概是这样:

[STM32F4]
   │
   ├─ 控制Motor Driver → 驱动左右轮
   └─ 触发HC-SR04 → 获取前方障碍物距离
         ↓
      决策引擎 → 前进 / 停止 / 后退 / 转向

主循环非常直观:

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_TIM2_Init();
    MX_TIM5_Init();

    float threshold = 20.0f;  // 避障阈值:20cm

    while (1) {
        Ultrasonic_Trigger();           // 发射一次
        HAL_Delay(60);                  // 符合模块建议周期

        if (distance_cm > 2 && distance_cm < 400) {
            if (distance_cm < threshold) {
                Motor_Stop();
                HAL_Delay(100);
                Motor_Backward();
                HAL_Delay(500);
                Motor_TurnLeft();       // 随便转个方向逃出生天 😂
                HAL_Delay(300);
            } else {
                Motor_Forward();        // 安全,继续前进
            }
        } else {
            Motor_Forward();            // 数据异常,默认前进
        }
    }
}

看起来很完美?但现实往往没那么理想……


实战中的坑,一个都不能少

❌ 问题1:数据跳来跳去,像抽风一样?

这是最常见的问题。空气流动、表面材质(比如窗帘吸音)、多路径反射都会导致回波不稳定。

🔧 解法:上滤波!

推荐一个简单高效的 滑动平均滤波器

#define FILTER_SIZE 5
float dist_buffer[FILTER_SIZE] = {0};
int idx = 0;

float apply_filter(float raw) {
    float sum = 0.0f;
    dist_buffer[idx] = raw;
    for (int i = 0; i < FILTER_SIZE; i++) sum += dist_buffer[i];
    idx = (idx + 1) % FILTER_SIZE;
    return sum / FILTER_SIZE;
}

然后在主循环中替换原始值:

distance_cm = apply_filter((pulse_duration * 0.034) / 2.0f);

效果立竿见影,数据平稳多了📊。


❌ 问题2:只会看前面,侧面撞墙怎么办?

单个传感器只能感知正前方,容易陷入“盲区”。

🔧 解法: 三向布局

加两个额外的HC-SR04,分别朝左前和右前安装,形成三角探测区。主控根据三个方向的距离综合判断转向策略:

  • 前方近,左侧远 → 右转
  • 前方近,右侧远 → 左转
  • 四面楚歌?原地打转求生🤣

当然,也可以考虑换成带舵机旋转的单探头方案,节省IO资源。


❌ 问题3:反应慢半拍,等发现时已经撞上了?

这通常是由于主循环阻塞造成的。比如用了 HAL_Delay() 太久,或者其他任务占用了CPU。

🔧 解法思路:
- 使用 非阻塞延时 (基于定时器中断或FreeRTOS延迟);
- 把测距任务封装成独立任务,用RTOS调度;
- 或者结合DMA+定时器触发多路采样,提升并发能力。


✅ 设计建议清单

项目 推荐做法
供电设计 给HC-SR04单独加LDO稳压5V,避免电机拉低电压
IO保护 Echo脚串联220Ω电阻 + 并联3.3V稳压二极管
安装角度 保证传感器水平,倾斜会导致测距偏大
温度补偿 外接DS18B20,动态调整声速:
$$ v = 331.5 + 0.6T \,\text{(m/s)} $$
T为摄氏温度
校准方法 实际测量不同距离下的返回值,拟合修正系数

为什么STM32F4特别适合这类应用?

你可能会问:为啥非要用STM32F4?便宜的F1系列不行吗?

当然不是不行,而是F4真的强太多了:

  • Cortex-M4+FPU :浮点运算不用模拟, distance_cm = ... 这种计算更流畅;
  • 最高180MHz主频 :响应更快,支持更高采样率;
  • 多个通用定时器(TIM2~TIM5) :轻松实现一路测距+多路PWM输出;
  • HAL库成熟稳定 :开发效率高,移植方便;
  • 支持RTOS :未来扩展路径清晰。

换句话说, 它既能让初学者快速入门,又能支撑进阶开发需求 ,简直是教学与实战两开花🌸。


结语:一个小功能,通往智能世界的入口

别小看这个“嘀——嘀——”测距的小系统。它其实是一个完整的 感知-决策-执行闭环

  • 感知:HC-SR04获取环境信息;
  • 决策:MCU判断是否需要避障;
  • 执行:驱动电机做出反应。

而这正是所有智能移动设备的核心逻辑。今天的避障小车,明天可能就是AGV物流车、巡检机器人,甚至是自主导航的无人机🚀。

而且一旦掌握了这套方法论,你会发现——换激光雷达?只是换个传感器驱动;加陀螺仪融合定位?不过是多几个I2C通信而已。

所以啊,嵌入式开发的魅力就在于: 从最基础的一行代码开始,一步步搭起通往智能世界的桥梁

你现在写的每一行 HAL_GPIO_WritePin() ,都在悄悄改变机器“看”世界的方式👀。

Keep coding, keep exploring! 💻✨

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐