1. 认识你的硬件小伙伴:STM32F103C8T6与HC-SR04

大家好,我是老李,一个在嵌入式领域摸爬滚打十多年的老工程师。今天我要和大家分享一个非常实用的项目:如何使用STM32F103C8T6这款性价比极高的MCU,搭配HC-SR04超声波模块,打造一个智能避障系统。这个项目特别适合刚入门嵌入式开发的朋友,我会用最直白的语言,带你一步步完成从硬件连接到软件编程的全过程。

先来认识一下我们的两位主角。STM32F103C8T6是ST公司推出的一款基于ARM Cortex-M3内核的32位微控制器,主频高达72MHz,内置64KB Flash和20KB RAM,外设丰富,价格亲民,被广大开发者亲切地称为"蓝色药丸"。HC-SR04则是超声波测距模块中的明星产品,它通过发射和接收超声波来测量距离,测量范围2cm-400cm,精度可达3mm,非常适合机器人避障、智能小车等应用。

我第一次接触这个组合是在五年前的一个智能小车项目上,当时就被HC-SR04的简单易用和STM32的强大功能所折服。记得那时候为了调试一个距离跳变的问题,整整折腾了一个周末,最后发现是电源噪声导致的。这些踩坑经验我都会在后面的内容中分享给大家,让你们少走弯路。

2. 硬件连接:让STM32和HC-SR04"握手成功"

硬件连接是项目的第一步,也是很多新手容易出错的地方。我们先来看看HC-SR04的四个引脚:VCC、GND、Trig(触发)和Echo(回波)。VCC接5V电源,GND接地,这两个是电源引脚。Trig和Echo是信号引脚,需要连接到STM32的GPIO口。

这里有个重要的注意事项:HC-SR04的工作电压是5V,而STM32F103C8T6的GPIO口是3.3V电平。这意味着Echo引脚输出的5V信号不能直接连接到STM32的GPIO口,否则可能会损坏芯片。解决方法有两种:一是使用电平转换电路,二是利用STM32的5V耐受引脚。经过我的实测,STM32的某些引脚是5V耐受的,比如PA8、PA9等,具体可以参考芯片的数据手册。

在我的项目中,我是这样连接的:

  • HC-SR04 VCC → 5V电源
  • HC-SR04 GND → 共地
  • HC-SR04 Trig → PA1 (STM32的任意GPIO输出模式)
  • HC-SR04 Echo → PA0 (STM32的5V耐受引脚,输入模式)

电源部分要特别注意,最好给HC-SR04单独供电,或者使用质量较好的5V稳压源。我曾经遇到过因为电源噪声导致测距不准的问题,后来在VCC和GND之间加了一个100uF的电解电容和一个0.1uF的瓷片电容,问题就解决了。

接线完成后,建议先用万用表检查一下连通性,确保没有短路或虚接。特别是电源引脚,接反了可能会烧坏模块。这些都是我用真金白银换来的经验,希望大家引以为戒。

3. 开发环境搭建:CubeMX和Keil的完美组合

工欲善其事,必先利其器。对于STM32开发,我强烈推荐使用STM32CubeMX + Keil MDK这个组合。CubeMX是ST官方推出的图形化配置工具,可以直观地配置引脚、时钟、外设等,生成初始化代码,大大提高了开发效率。

首先安装STM32CubeMX和Keil MDK,这两个软件都可以在官网下载。安装过程中需要注意,Keil需要注册和激活,社区版有32KB代码限制,但对于我们这个项目已经足够了。安装完成后,打开CubeMX,点击"New Project",在芯片选择框中输入"STM32F103C8T6",然后选择对应的型号。

时钟配置是关键的一步。STM32F103C8T6最高可以运行在72MHz,我们需要正确配置时钟树才能发挥芯片的最佳性能。具体配置如下:选择HSE(外部高速时钟)作为时钟源,PLL倍频到72MHz,APB1总线时钟设为36MHz(最大频率),APB2总线时钟设为72MHz。这些配置听起来复杂,但在CubeMX中都是图形化操作,只需要点选相应的选项即可。

外设配置方面,我们需要配置一个GPIO口用于Trig引脚(输出模式),一个GPIO口用于Echo引脚(输入模式),一个定时器用于测量高电平时间,以及一个串口用于调试输出。定时器的选择很重要,最好选择32位定时器或者16位定时器带预分频的,以确保能够测量足够长的时间。

配置完成后,点击"Generate Code",CubeMX会自动生成完整的工程文件。然后用Keil打开工程,编译一下,确保没有错误。至此,开发环境就搭建完成了。我第一次用CubeMX的时候,简直被它的便捷性震惊了,以前需要手动编写的大量初始化代码,现在点几下鼠标就搞定了。

4. HAL库驱动编写:让超声波模块"开口说话"

现在进入最核心的部分——编写驱动程序。HC-SR04的工作原理很简单:给Trig引脚一个至少10us的高电平脉冲,模块就会自动发射8个40kHz的超声波脉冲,然后Echo引脚会输出一个高电平,高电平的持续时间就是超声波从发射到返回的时间。

首先我们编写一个触发函数,用于产生10us的触发脉冲:

void HC_SR04_Trigger(void)
{
    HAL_GPIO_WritePin(Trig_GPIO_Port, Trig_Pin, GPIO_PIN_SET);
    delay_us(15);  // 保持15us高电平,略大于10us要求
    HAL_GPIO_WritePin(Trig_GPIO_Port, Trig_Pin, GPIO_PIN_RESET);
}

这里的delay_us函数需要自己实现,因为HAL库提供的HAL_Delay()最小延时是1ms。我们可以用定时器或者nop指令实现微秒级延时:

void delay_us(uint16_t us)
{
    __HAL_TIM_SET_COUNTER(&htim2, 0);
    while (__HAL_TIM_GET_COUNTER(&htim2) < us);
}

接下来是最关键的距离测量函数。我们需要测量Echo引脚高电平的持续时间,然后根据公式计算距离:距离 = (高电平时间 × 声速) / 2。声速在常温下大约是340m/s,但实际使用时建议考虑温度补偿,这个我们后面会讲到。

测量高电平时间有两种方法:输入捕获和外部中断+定时器。我推荐使用输入捕获方式,因为精度更高。配置一个定时器为输入捕获模式,上升沿触发,当Echo引脚出现上升沿时,记录定时器值;出现下降沿时,再次记录定时器值,两次的差值就是高电平时间。

float HC_SR04_GetDistance(void)
{
    uint32_t start_time, end_time;
    float distance;
    
    // 发送触发脉冲
    HC_SR04_Trigger();
    
    // 等待Echo引脚变高
    while(HAL_GPIO_ReadPin(Echo_GPIO_Port, Echo_Pin) == GPIO_PIN_RESET);
    start_time = __HAL_TIM_GET_COUNTER(&htim2);
    
    // 等待Echo引脚变低
    while(HAL_GPIO_ReadPin(Echo_GPIO_Port, Echo_Pin) == GPIO_PIN_SET);
    end_time = __HAL_TIM_GET_COUNTER(&htim2);
    
    // 计算距离
    distance = (end_time - start_time) * 0.034 / 2;  // 单位cm
    return distance;
}

在实际使用中,我发现直接使用while循环等待电平变化可能会造成程序卡死,所以最好加上超时判断。比如如果等待时间超过38ms(HC-SR04的最大测量时间),就返回错误值。

5. 智能避障算法设计:让系统"会思考"

有了距离测量功能,我们就可以实现智能避障了。最简单的避障策略是设定一个阈值距离,当测量距离小于这个阈值时,就执行避障动作(比如停止、转向等)。但这种简单策略在实际应用中往往效果不佳,因为会遇到误判、抖动等问题。

我经过多次实践,总结出了一套改进的避障算法。首先,采用多次测量取平均值的方法来减少随机误差。通常取5-10次测量值的平均,效果比较好。但要注意,每次测量之间需要间隔至少60ms,因为HC-SR04需要一定的恢复时间。

#define SAMPLE_COUNT 5

float GetAverageDistance(void)
{
    float sum = 0;
    for(int i = 0; i < SAMPLE_COUNT; i++) {
        sum += HC_SR04_GetDistance();
        HAL_Delay(70);  // 每次测量间隔70ms
    }
    return sum / SAMPLE_COUNT;
}

其次,加入滤波算法处理异常值。我发现超声波测量偶尔会出现明显偏离实际值的异常数据,这些数据会严重影响系统判断。可以采用中值滤波或者限幅滤波来消除这些异常值。

float MedianFilter(float new_value)
{
    static float buffer[5] = {0};
    static int index = 0;
    float temp[5];
    
    buffer[index] = new_value;
    index = (index + 1) % 5;
    
    // 复制到临时数组进行排序
    memcpy(temp, buffer, sizeof(buffer));
    for(int i = 0; i < 4; i++) {
        for(int j = i+1; j < 5; j++) {
            if(temp[i] > temp[j]) {
                float swap = temp[i];
                temp[i] = temp[j];
                temp[j] = swap;
            }
        }
    }
    return temp[2];  // 返回中值
}

最后,根据具体应用场景设计避障策略。对于智能小车,我通常采用三级避障策略:当距离大于50cm时,正常前进;距离在20-50cm之间时,减速并准备转向;距离小于20cm时,立即停止并寻找新的路径。这种渐进式的策略比简单的阈值判断更加平滑可靠。

6. 系统优化与调试技巧:让性能"更上一层楼"

项目基本功能实现后,我们还需要进行一些优化和调试,让系统更加稳定可靠。首先要注意的是温度补偿。声速随温度变化而变化,公式是:v = 331.4 + 0.607 * T,其中T是摄氏温度。如果要求精度高,可以添加温度传感器进行实时补偿。

电源噪声是影响测量精度的一个重要因素。我在项目中遇到过因为电源噪声导致距离跳变的问题,后来在HC-SR04的VCC和GND之间加了去耦电容(100uF电解电容并联0.1uF瓷片电容),问题就解决了。如果使用电机等大功率设备,最好给控制部分和动力部分分开供电。

软件方面的优化也很重要。中断处理要尽量简短,避免在中断服务程序中做复杂计算。我建议在中断中只记录时间戳,在主循环中进行计算和判断。另外,可以添加看门狗定时器,防止程序跑飞。

调试时,串口打印是最常用的手段。可以将测量到的原始数据、计算过程、最终结果都打印出来,方便分析问题。但要注意,调试完成后要删除或者禁用这些打印语句,因为它们会影响程序实时性。

printf("Start: %lu, End: %lu, Distance: %.2f cm\n", start_time, end_time, distance);

还有一个常见问题是多模块干扰。如果系统中有多个HC-SR04模块,它们可能会相互干扰。解决方法一是分时工作,二是使用不同的触发频率。我曾经在一个项目中使用4个HC-SR04模块实现360度避障,就是采用分时触发的策略,每个模块间隔20ms触发一次,避免了相互干扰。

7. 实际应用案例:智能小车避障系统

现在让我们把这些知识应用到一个实际项目中——智能小车避障系统。这个系统使用两个HC-SR04模块,一个朝前,一个朝左,实现基本的前进和避障功能。

硬件方面,我们需要STM32F103C8T6最小系统板、两个HC-SR04模块、L298N电机驱动板、直流电机和车轮、锂电池等。接线时要注意,电机驱动部分要和控制部分分开供电,否则电机启动时的电流冲击可能会造成STM32复位。

软件逻辑主要是一个状态机,根据超声波测距结果决定小车的运动状态。我定义了以下几个状态:前进、左转、右转、后退、停止。状态转换的条件基于两个超声波模块的测距结果。

typedef enum {
    STATE_FORWARD,
    STATE_TURN_LEFT, 
    STATE_TURN_RIGHT,
    STATE_BACKWARD,
    STATE_STOP
} CarState;

void CarStateMachine(void)
{
    float front_dist = GetFrontDistance();
    float left_dist = GetLeftDistance();
    
    switch(current_state) {
        case STATE_FORWARD:
            if(front_dist < 20.0f) {
                if(left_dist > 30.0f) {
                    current_state = STATE_TURN_LEFT;
                } else {
                    current_state = STATE_TURN_RIGHT;
                }
            }
            break;
        case STATE_TURN_LEFT:
            // 转弯一定时间后恢复前进
            if(turn_timer > 500) {
                current_state = STATE_FORWARD;
                turn_timer = 0;
            }
            break;
        // 其他状态处理...
    }
}

在实际调试中,我发现单纯依靠超声波有时会遇到"死胡同"问题——小车在狭窄空间内来回转圈。后来我加入了陀螺仪和编码器,记录小车的运动轨迹,实现了更智能的避障算法。这些都是后续可以扩展的功能。

这个项目我从最初的原型到最终稳定运行,花了大概两周时间,期间遇到了无数问题,但每一个问题的解决都让我对超声波测量和嵌入式系统有了更深的理解。现在这个小车已经能够很流畅地在复杂环境中自主导航了,看着它灵活地避开障碍物,那种成就感是无法用语言形容的。

8. 常见问题与解决方案

在开发过程中,大家可能会遇到各种问题,我总结了一些常见问题及解决方法。第一个常见问题是测量值不稳定,距离跳变严重。这通常是电源噪声或环境干扰导致的。解决方法包括:给HC-SR04加去耦电容、避免测量表面过于光滑的物体、增加软件滤波等。

第二个问题是测量距离明显偏差。可能是声速参数设置不正确,或者温度变化没有补偿。可以用卷尺实际测量一段距离,然后调整声速参数进行校准。我在实验室环境下测得的校准系数是0.0343(单位cm/us),但建议大家根据自己的环境重新校准。

第三个问题是响应速度慢。超声波测量需要一定时间,特别是测量较远距离时,Echo高电平持续时间较长。如果系统对实时性要求高,可以设置超时机制,超过一定时间就放弃本次测量。也可以使用中断方式而不是轮询方式等待Echo信号变化。

第四个问题是多模块干扰。当多个HC-SR04同时工作时,可能会相互干扰。解决方法包括分时触发、物理隔离、使用不同的工作频率等。在我的四模块系统中,采用20ms的时间片轮询方式,每个模块在自己的时间片内工作,有效避免了干扰。

最后提醒大家注意HC-SR04的2cm盲区问题。模块无法测量2cm以内的距离,在实际应用中要考虑到这个限制。如果需要测量更近的距离,可以考虑使用红外测距等其他传感器。

这些问题的解决方法都是我通过实际项目总结出来的,有些甚至是付出了烧坏几个模块的代价才得到的经验。希望大家在开发过程中能够少走弯路,顺利实现自己的项目。

Logo

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

更多推荐