STM32实战:基于STM32F103的HC-SR04超声波测距与OLED显示

文章目录

    • 一、前言
      • [1.1 技术背景](#1.1 技术背景)
      • [1.2 本文目标](#1.2 本文目标)
      • [1.3 读者收获](#1.3 读者收获)
    • 二、环境准备
      • [2.1 硬件准备](#2.1 硬件准备)
      • [2.2 HC-SR04传感器详解](#2.2 HC-SR04传感器详解)
        • [2.2.1 技术参数](#2.2.1 技术参数)
        • [2.2.2 工作原理](#2.2.2 工作原理)
      • [2.3 OLED显示屏详解](#2.3 OLED显示屏详解)
        • [2.3.1 SSD1306控制器](#2.3.1 SSD1306控制器)
        • [2.3.2 I2C通信协议](#2.3.2 I2C通信协议)
    • 三、项目创建与配置
      • [3.1 使用STM32CubeMX创建工程](#3.1 使用STM32CubeMX创建工程)
        • [3.1.1 新建工程](#3.1.1 新建工程)
        • [3.1.2 配置时钟树](#3.1.2 配置时钟树)
        • [3.1.3 配置GPIO](#3.1.3 配置GPIO)
        • [3.1.4 配置TIM2(用于超声波计时)](#3.1.4 配置TIM2(用于超声波计时))
        • [3.1.5 配置I2C1](#3.1.5 配置I2C1)
        • [3.1.6 配置GPIO参数](#3.1.6 配置GPIO参数)
        • [3.1.7 生成代码](#3.1.7 生成代码)
    • 四、核心代码实现
      • [4.1 OLED驱动代码](#4.1 OLED驱动代码)
      • [4.2 HC-SR04驱动代码](#4.2 HC-SR04驱动代码)
      • [4.3 主程序代码](#4.3 主程序代码)
      • [4.4 头文件修改](#4.4 头文件修改)
    • 五、系统架构与流程
      • [5.1 系统架构图](#5.1 系统架构图)
      • [5.2 测距流程图](#5.2 测距流程图)
      • [5.3 OLED显示流程](#5.3 OLED显示流程)
    • 六、编译与调试
      • [6.1 编译工程](#6.1 编译工程)
      • [6.2 硬件连接](#6.2 硬件连接)
      • [6.3 测试验证](#6.3 测试验证)
    • 七、故障排查与问题解决
    • 八、总结与扩展
      • [8.1 核心知识点回顾](#8.1 核心知识点回顾)
      • [8.2 扩展学习方向](#8.2 扩展学习方向)
      • [8.3 学习资源](#8.3 学习资源)

一、前言

1.1 技术背景

超声波测距技术是一种非接触式距离测量方法,广泛应用于机器人避障、液位检测、倒车雷达、工业测量等领域。HC-SR04是一款经典的超声波测距模块,以其价格低廉、接口简单、测量精度适中(3mm)的特点,成为嵌入式开发中最常用的距离传感器之一。

HC-SR04的工作原理基于超声波的回声定位:模块发射40kHz的超声波脉冲,遇到障碍物后反射回来,通过测量发射到接收的时间差,结合声速计算出距离。这种测距方式不受光线、颜色、透明度等因素影响,适用于多种复杂环境。

OLED(有机发光二极管)显示屏以其高对比度、宽视角、低功耗、响应速度快等优势,逐渐取代传统的LCD显示屏,成为嵌入式系统的首选显示方案。0.96寸OLED屏(128×64分辨率)配合SSD1306驱动芯片,通过I2C或SPI接口与MCU通信,可以清晰显示文字、图形和简单动画。

1.2 本文目标

本文将带领读者完成以下实战项目:

  • 理解HC-SR04超声波测距的工作原理和时序要求
  • 使用STM32F103的GPIO和定时器实现精确测距
  • 掌握SSD1306 OLED显示屏的I2C驱动方法
  • 实现距离数据的实时采集和可视化显示
  • 学习多传感器协同工作的系统设计

1.3 读者收获

完成本教程后,你将能够:

  1. 独立使用HC-SR04进行距离测量
  2. 掌握超声波测距的时序控制和计算方法
  3. 理解I2C通信协议,实现OLED显示驱动
  4. 具备多外设协同工作的开发能力
  5. 学会设计简单的人机交互界面

技术栈:

  • 开发板:STM32F103C8T6
  • 测距模块:HC-SR04超声波传感器
  • 显示屏:0.96寸OLED(SSD1306,I2C接口)
  • 开发环境:STM32CubeIDE
  • 通信协议:GPIO、I2C
  • 核心外设:定时器(TIM)、GPIO

二、环境准备

2.1 硬件准备

必需硬件:

设备 型号/规格 数量 说明
STM32开发板 STM32F103C8T6 1 核心控制器
超声波模块 HC-SR04 1 距离测量
OLED显示屏 0.96寸 128×64 I2C 1 数据显示
杜邦线 母对母/公对母 若干 连接电路
面包板 标准面包板 1 搭建电路

HC-SR04模块引脚说明:

复制代码
HC-SR04模块
┌─────────────┐
│ VCC  │  电源正极(5V)
│ Trig │  触发引脚(输入)
│ Echo │  回响引脚(输出)
│ GND  │  电源负极
└─────────────┘

注意:HC-SR04需要5V供电,但信号引脚可兼容3.3V

OLED显示屏引脚说明(I2C接口):

复制代码
SSD1306 OLED (I2C)
┌─────────────┐
│ VCC  │  电源正极(3.3V-5V)
│ GND  │  电源负极
│ SCL  │  I2C时钟线
│ SDA  │  I2C数据线
└─────────────┘

I2C地址:0x78(写)或 0x3C(7位地址)

硬件连接图:

复制代码
STM32F103C8T6        HC-SR04           OLED (I2C)
┌──────────┐        ┌───────┐         ┌─────────┐
│ 5V       │───────→│ VCC   │         │         │
│ GND      │───────→│ GND   │         │         │
│ PA0      │───────→│ Trig  │         │         │
│ PA1      │←───────│ Echo  │         │         │
│ PB6      │──────────────────────────→│ SCL     │
│ PB7      │──────────────────────────→│ SDA     │
│ 3.3V     │──────────────────────────→│ VCC     │
│ GND      │──────────────────────────→│ GND     │
└──────────┘        └───────┘         └─────────┘

注意:
1. HC-SR04的VCC需要接5V,Echo输出3.3V可兼容STM32
2. OLED的VCC可接3.3V或5V
3. I2C需要上拉电阻(模块通常已集成)

2.2 HC-SR04传感器详解

2.2.1 技术参数
参数 规格
工作电压 DC 5V
工作电流 15mA
测量角度 <15°
测量范围 2cm - 400cm(或 450cm)
测量精度 ±3mm
工作频率 40kHz
触发信号 10μs TTL脉冲
回响信号 TTL电平,脉宽与距离成正比
测量周期 建议≥60ms
2.2.2 工作原理
复制代码
┌─────────────────────────────────────────────────────────┐
│                    HC-SR04 工作流程                      │
├─────────────────────────────────────────────────────────┤
│                                                          │
│  1. 触发阶段                                              │
│     MCU ──10μs高电平脉冲──→ Trig引脚                     │
│                                                          │
│  2. 发射阶段                                              │
│     HC-SR04自动发射8个40kHz超声波脉冲                     │
│                                                          │
│  3. 回响阶段                                              │
│     Echo引脚输出高电平,持续时间 = 声波往返时间            │
│                                                          │
│  4. 计算阶段                                              │
│     距离 = (高电平时间 × 声速) / 2                       │
│     声速 = 340m/s (常温下)                               │
│                                                          │
└─────────────────────────────────────────────────────────┘

时序图:

复制代码
Trig:    ─┐     ┌────────────────────────────────────────
          └10μs─┘

Echo:    ───────┐                              ┌────────
                └──────────────T────────────────┘
                
超声波:          ▓▓▓▓▓▓▓▓  →→→  ●●●●●●●●  →→→
                 发射8个脉冲      遇到障碍物      接收回波

距离计算公式:
距离(cm) = T(μs) × 0.017  或  T(μs) / 58
距离(mm) = T(μs) × 0.17   或  T(μs) / 5.8

2.3 OLED显示屏详解

2.3.1 SSD1306控制器

SSD1306是一款单芯片CMOS OLED/PLED驱动控制器,支持:

  • 分辨率:128 × 64 点阵
  • 显示颜色:单色(白色/蓝色/黄蓝双色)
  • 通信接口:I2C(默认)、SPI、并行
  • 内置显示RAM:128 × 64 bits
  • 工作电压:3.3V - 5V
2.3.2 I2C通信协议

SSD1306的I2C地址:

  • 写地址:0x78(0111 1000)
  • 读地址:0x79(0111 1001)
  • 7位地址:0x3C(011 1100)

控制字节格式:

复制代码
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ Co  │ D/C │  0  │  0  │  0  │  0  │  0  │  0  │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘

Co (Continue bit): 
  0 = 后续字节均为数据/命令
  1 = 后续只有1个字节

D/C (Data/Command bit):
  0 = 命令字节
  1 = 数据字节

三、项目创建与配置

3.1 使用STM32CubeMX创建工程

3.1.1 新建工程
  1. 打开STM32CubeMX,点击 File → New Project
  2. 选择 STM32F103C8Tx ,点击 Start Project
3.1.2 配置时钟树
  1. 点击 Clock Configuration 标签
  2. 配置HSE为Crystal/Ceramic Resonator
  3. 设置PLL倍频为x9,系统时钟72MHz
  4. TIM2时钟72MHz(用于超声波计时)
3.1.3 配置GPIO

配置HC-SR04引脚:

  1. 点击PA0引脚,选择 GPIO_Output,User Label:"TRIG_PIN"
  2. 点击PA1引脚,选择 GPIO_Input,User Label:"ECHO_PIN"

配置I2C引脚:

  1. 点击PB6引脚,选择 I2C1_SCL
  2. 点击PB7引脚,选择 I2C1_SDA
3.1.4 配置TIM2(用于超声波计时)
  1. 点击 Timers → TIM2
  2. Clock Source :选择 Internal Clock
  3. Channel1 :选择 Input Capture direct mode
  4. Configuration
    • Prescaler:71(72MHz / 72 = 1MHz,即1μs计数一次)
    • Counter Period:65535(最大计时65.535ms)
    • 可测最大距离:65.535ms × 340m/s / 2 ≈ 11m(远超HC-SR04量程)
3.1.5 配置I2C1
  1. 点击 Connectivity → I2C1
  2. Mode :选择 I2C
  3. Configuration
    • I2C Speed Mode:Fast Mode(400kHz)或 Standard Mode(100kHz)
    • Clock Stretching:Disable(SSD1306不需要)
3.1.6 配置GPIO参数
  1. 点击 GPIO 标签

  2. 配置PA0(TRIG_PIN):

    • Mode:Output Push Pull
    • Pull:No pull-up/pull-down
    • Speed:High
    • User Label:TRIG_PIN
  3. 配置PA1(ECHO_PIN):

    • Mode:Input
    • Pull:No pull-up/pull-down
    • User Label:ECHO_PIN
3.1.7 生成代码
  1. 点击 Project → Settings
  2. Project Name:HCSR04_OLED
  3. Toolchain:STM32CubeIDE
  4. 勾选 Generate peripheral initialization as a pair of '.c/.h' files
  5. 点击 GENERATE CODE

四、核心代码实现

4.1 OLED驱动代码

📄 创建文件:Core/Inc/ssd1306.h

c 复制代码
#ifndef __SSD1306_H
#define __SSD1306_H

#include "main.h"
#include "stm32f1xx_hal.h"

// I2C地址
#define SSD1306_I2C_ADDR    0x78

// 屏幕尺寸
#define SSD1306_WIDTH       128
#define SSD1306_HEIGHT      64

// 命令定义
#define SSD1306_CMD         0x00
#define SSD1306_DATA        0x40

// 函数声明
uint8_t SSD1306_Init(void);
void SSD1306_Clear(void);
void SSD1306_UpdateScreen(void);
void SSD1306_DrawPixel(uint8_t x, uint8_t y, uint8_t color);
void SSD1306_DrawChar(uint8_t x, uint8_t y, char c, uint8_t size);
void SSD1306_DrawString(uint8_t x, uint8_t y, char *str, uint8_t size);
void SSD1306_DrawLine(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t color);
void SSD1306_DrawRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color);
void SSD1306_FillRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color);
void SSD1306_DrawCircle(uint8_t x0, uint8_t y0, uint8_t r, uint8_t color);
void SSD1306_DrawBitmap(uint8_t x, uint8_t y, const uint8_t *bitmap, uint8_t w, uint8_t h);

// 显示缓冲区
extern uint8_t SSD1306_Buffer[SSD1306_HEIGHT / 8][SSD1306_WIDTH];

#endif /* __SSD1306_H */

📄 创建文件:Core/Src/ssd1306.c

c 复制代码
#include "ssd1306.h"
#include "stdlib.h"
#include "string.h"

// 外部I2C句柄(在main.c中定义)
extern I2C_HandleTypeDef hi2c1;

// 显示缓冲区:128×64像素,按页组织(8页×128字节)
uint8_t SSD1306_Buffer[SSD1306_HEIGHT / 8][SSD1306_WIDTH];

// 字体数据(8×6 ASCII字符)
static const uint8_t Font6x8[][6] = {
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 空格
    {0x00, 0x00, 0x5F, 0x00, 0x00, 0x00}, // !
    // ... 其他字符(此处省略,实际使用完整字体表)
    {0x3E, 0x51, 0x49, 0x45, 0x3E, 0x00}, // 0
    {0x00, 0x42, 0x7F, 0x40, 0x00, 0x00}, // 1
    {0x42, 0x61, 0x51, 0x49, 0x46, 0x00}, // 2
    {0x21, 0x41, 0x45, 0x4B, 0x31, 0x00}, // 3
    {0x18, 0x14, 0x12, 0x7F, 0x10, 0x00}, // 4
    {0x27, 0x45, 0x45, 0x45, 0x39, 0x00}, // 5
    {0x3C, 0x4A, 0x49, 0x49, 0x30, 0x00}, // 6
    {0x01, 0x71, 0x09, 0x05, 0x03, 0x00}, // 7
    {0x36, 0x49, 0x49, 0x49, 0x36, 0x00}, // 8
    {0x06, 0x49, 0x49, 0x29, 0x1E, 0x00}, // 9
};

/**
  * @brief  向SSD1306写入命令
  * @param  cmd: 命令字节
  * @retval HAL状态
  */
static HAL_StatusTypeDef SSD1306_WriteCommand(uint8_t cmd)
{
    uint8_t data[2] = {0x00, cmd};  // 0x00 = 命令控制字节
    return HAL_I2C_Master_Transmit(&hi2c1, SSD1306_I2C_ADDR, data, 2, 100);
}

/**
  * @brief  向SSD1306写入数据
  * @param  data: 数据指针
  * @param  len: 数据长度
  * @retval HAL状态
  */
static HAL_StatusTypeDef SSD1306_WriteData(uint8_t *data, uint16_t len)
{
    uint8_t *buffer = malloc(len + 1);
    if(buffer == NULL) return HAL_ERROR;
    
    buffer[0] = 0x40;  // 数据控制字节
    memcpy(buffer + 1, data, len);
    
    HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(&hi2c1, SSD1306_I2C_ADDR, buffer, len + 1, 100);
    free(buffer);
    
    return status;
}

/**
  * @brief  初始化SSD1306
  * @param  None
  * @retval 0: 成功, 1: 失败
  */
uint8_t SSD1306_Init(void)
{
    HAL_Delay(100);  // 等待OLED上电稳定
    
    // 初始化命令序列
    uint8_t init_commands[] = {
        0xAE,  // 关闭显示
        0xD5,  // 设置时钟分频
        0x80,
        0xA8,  // 设置多路复用
        0x3F,  // 1/64 duty
        0xD3,  // 设置显示偏移
        0x00,
        0x40,  // 设置显示起始行
        0x8D,  // 使能电荷泵
        0x14,
        0x20,  // 设置内存寻址模式
        0x00,  // 水平寻址模式
        0xA1,  // 设置段重映射(左右翻转)
        0xC8,  // 设置COM扫描方向(上下翻转)
        0xDA,  // 设置COM引脚配置
        0x12,
        0x81,  // 设置对比度
        0xCF,
        0xD9,  // 设置预充电周期
        0xF1,
        0xDB,  // 设置VCOMH取消选择电平
        0x40,
        0xA4,  // 全局显示开启
        0xA6,  // 正常显示(非反相)
        0xAF,  // 开启显示
    };
    
    for(uint8_t i = 0; i < sizeof(init_commands); i++)
    {
        if(SSD1306_WriteCommand(init_commands[i]) != HAL_OK)
        {
            return 1;  // 初始化失败
        }
    }
    
    SSD1306_Clear();
    SSD1306_UpdateScreen();
    
    return 0;  // 初始化成功
}

/**
  * @brief  清空显示缓冲区
  * @param  None
  * @retval None
  */
void SSD1306_Clear(void)
{
    memset(SSD1306_Buffer, 0x00, sizeof(SSD1306_Buffer));
}

/**
  * @brief  更新屏幕显示(将缓冲区数据发送到OLED)
  * @param  None
  * @retval None
  */
void SSD1306_UpdateScreen(void)
{
    // 设置列地址
    SSD1306_WriteCommand(0x21);  // 设置列起始和结束地址
    SSD1306_WriteCommand(0);
    SSD1306_WriteCommand(SSD1306_WIDTH - 1);
    
    // 设置页地址
    SSD1306_WriteCommand(0x22);  // 设置页起始和结束地址
    SSD1306_WriteCommand(0);
    SSD1306_WriteCommand(7);
    
    // 发送显示数据
    for(uint8_t page = 0; page < 8; page++)
    {
        SSD1306_WriteData(SSD1306_Buffer[page], SSD1306_WIDTH);
    }
}

/**
  * @brief  绘制像素点
  * @param  x: X坐标(0-127)
  * @param  y: Y坐标(0-63)
  * @param  color: 0=黑色, 1=白色
  * @retval None
  */
void SSD1306_DrawPixel(uint8_t x, uint8_t y, uint8_t color)
{
    if(x >= SSD1306_WIDTH || y >= SSD1306_HEIGHT) return;
    
    if(color)
    {
        SSD1306_Buffer[y / 8][x] |= (1 << (y % 8));
    }
    else
    {
        SSD1306_Buffer[y / 8][x] &= ~(1 << (y % 8));
    }
}

/**
  * @brief  绘制字符(6×8字体)
  * @param  x: X坐标
  * @param  y: Y坐标
  * @param  c: 字符
  * @param  size: 字体大小(1=6×8, 2=12×16)
  * @retval None
  */
void SSD1306_DrawChar(uint8_t x, uint8_t y, char c, uint8_t size)
{
    if(x >= SSD1306_WIDTH || y >= SSD1306_HEIGHT) return;
    if(c < ' ' || c > '~') return;
    
    uint8_t char_index = c - ' ';
    
    if(size == 1)
    {
        // 6×8字体
        for(uint8_t i = 0; i < 6; i++)
        {
            uint8_t line = Font6x8[char_index][i];
            for(uint8_t j = 0; j < 8; j++)
            {
                if(line & (1 << j))
                {
                    SSD1306_DrawPixel(x + i, y + j, 1);
                }
            }
        }
    }
    else if(size == 2)
    {
        // 12×16字体(放大2倍)
        for(uint8_t i = 0; i < 6; i++)
        {
            uint8_t line = Font6x8[char_index][i];
            for(uint8_t j = 0; j < 8; j++)
            {
                if(line & (1 << j))
                {
                    SSD1306_DrawPixel(x + i * 2, y + j * 2, 1);
                    SSD1306_DrawPixel(x + i * 2 + 1, y + j * 2, 1);
                    SSD1306_DrawPixel(x + i * 2, y + j * 2 + 1, 1);
                    SSD1306_DrawPixel(x + i * 2 + 1, y + j * 2 + 1, 1);
                }
            }
        }
    }
}

/**
  * @brief  绘制字符串
  * @param  x: X坐标
  * @param  y: Y坐标
  * @param  str: 字符串
  * @param  size: 字体大小
  * @retval None
  */
void SSD1306_DrawString(uint8_t x, uint8_t y, char *str, uint8_t size)
{
    uint8_t char_width = (size == 1) ? 6 : 12;
    
    while(*str)
    {
        if(x + char_width > SSD1306_WIDTH)
        {
            x = 0;
            y += (size == 1) ? 8 : 16;
        }
        if(y + 8 > SSD1306_HEIGHT) break;
        
        SSD1306_DrawChar(x, y, *str, size);
        x += char_width;
        str++;
    }
}

/**
  * @brief  绘制直线(Bresenham算法)
  * @param  x0, y0: 起点坐标
  * @param  x1, y1: 终点坐标
  * @param  color: 颜色
  * @retval None
  */
void SSD1306_DrawLine(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t color)
{
    int16_t dx = abs(x1 - x0);
    int16_t dy = abs(y1 - y0);
    int16_t sx = (x0 < x1) ? 1 : -1;
    int16_t sy = (y0 < y1) ? 1 : -1;
    int16_t err = dx - dy;
    
    while(1)
    {
        SSD1306_DrawPixel(x0, y0, color);
        
        if(x0 == x1 && y0 == y1) break;
        
        int16_t e2 = 2 * err;
        if(e2 > -dy)
        {
            err -= dy;
            x0 += sx;
        }
        if(e2 < dx)
        {
            err += dx;
            y0 += sy;
        }
    }
}

/**
  * @brief  绘制矩形(空心)
  * @param  x, y: 左上角坐标
  * @param  w, h: 宽度和高度
  * @param  color: 颜色
  * @retval None
  */
void SSD1306_DrawRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color)
{
    SSD1306_DrawLine(x, y, x + w - 1, y, color);
    SSD1306_DrawLine(x, y + h - 1, x + w - 1, y + h - 1, color);
    SSD1306_DrawLine(x, y, x, y + h - 1, color);
    SSD1306_DrawLine(x + w - 1, y, x + w - 1, y + h - 1, color);
}

/**
  * @brief  绘制矩形(实心)
  * @param  x, y: 左上角坐标
  * @param  w, h: 宽度和高度
  * @param  color: 颜色
  * @retval None
  */
void SSD1306_FillRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color)
{
    for(uint8_t i = x; i < x + w && i < SSD1306_WIDTH; i++)
    {
        for(uint8_t j = y; j < y + h && j < SSD1306_HEIGHT; j++)
        {
            SSD1306_DrawPixel(i, j, color);
        }
    }
}

4.2 HC-SR04驱动代码

📄 创建文件:Core/Inc/hcsr04.h

c 复制代码
#ifndef __HCSR04_H
#define __HCSR04_H

#include "main.h"
#include "stm32f1xx_hal.h"

// 函数声明
void HCSR04_Init(void);
uint16_t HCSR04_GetDistance(void);
float HCSR04_GetDistanceFloat(void);

#endif /* __HCSR04_H */

📄 创建文件:Core/Src/hcsr04.c

c 复制代码
#include "hcsr04.h"
#include "stdio.h"

// 外部定时器句柄(在main.c中定义)
extern TIM_HandleTypeDef htim2;

// 距离计算常量
#define HCSR04_SOUND_SPEED      340.0f      // 声速 m/s
#define HCSR04_TIME_TO_CM       0.017f      // 时间(μs)转距离(cm)系数
#define HCSR04_TIME_TO_MM       0.17f       // 时间(μs)转距离(mm)系数

// 测量超时时间(对应约4米)
#define HCSR04_TIMEOUT_US       25000       // 25ms

/**
  * @brief  HC-SR04初始化
  * @param  None
  * @retval None
  */
void HCSR04_Init(void)
{
    // 触发引脚初始化为低电平
    HAL_GPIO_WritePin(TRIG_PIN_GPIO_Port, TRIG_PIN_Pin, GPIO_PIN_RESET);
    
    // 启动定时器
    HAL_TIM_Base_Start(&htim2);
}

/**
  * @brief  微秒级延时(使用定时器)
  * @param  us: 延时时间(微秒)
  * @retval None
  */
static void HCSR04_Delay_us(uint16_t us)
{
    uint16_t start = __HAL_TIM_GET_COUNTER(&htim2);
    while((__HAL_TIM_GET_COUNTER(&htim2) - start) < us);
}

/**
  * @brief  获取距离(整数,单位cm)
  * @param  None
  * @retval 距离值(cm),0表示测量失败
  */
uint16_t HCSR04_GetDistance(void)
{
    uint32_t start_time = 0;
    uint32_t end_time = 0;
    uint32_t duration = 0;
    uint32_t timeout = 0;
    
    // 步骤1:发送触发信号
    HAL_GPIO_WritePin(TRIG_PIN_GPIO_Port, TRIG_PIN_Pin, GPIO_PIN_SET);
    HCSR04_Delay_us(10);  // 保持10μs高电平
    HAL_GPIO_WritePin(TRIG_PIN_GPIO_Port, TRIG_PIN_Pin, GPIO_PIN_RESET);
    
    // 步骤2:等待Echo引脚变高
    timeout = 0;
    while(HAL_GPIO_ReadPin(ECHO_PIN_GPIO_Port, ECHO_PIN_Pin) == GPIO_PIN_RESET)
    {
        if(++timeout > HCSR04_TIMEOUT_US) return 0;  // 超时
        HCSR04_Delay_us(1);
    }
    
    // 步骤3:记录开始时间
    start_time = __HAL_TIM_GET_COUNTER(&htim2);
    
    // 步骤4:等待Echo引脚变低
    timeout = 0;
    while(HAL_GPIO_ReadPin(ECHO_PIN_GPIO_Port, ECHO_PIN_Pin) == GPIO_PIN_SET)
    {
        end_time = __HAL_TIM_GET_COUNTER(&htim2);
        duration = end_time - start_time;
        
        if(duration > HCSR04_TIMEOUT_US) return 0;  // 超出量程
        if(++timeout > HCSR04_TIMEOUT_US) return 0;  // 超时保护
        HCSR04_Delay_us(1);
    }
    
    // 步骤5:计算距离
    // 距离 = 时间(μs) × 声速(m/s) / 2 × 100(cm/m) / 1000000(μs/s)
    // 简化:距离(cm) = 时间(μs) × 0.017
    uint16_t distance = (uint16_t)(duration * HCSR04_TIME_TO_CM);
    
    // 限制测量范围(2cm - 400cm)
    if(distance < 2) distance = 2;
    if(distance > 400) distance = 400;
    
    return distance;
}

/**
  * @brief  获取距离(浮点数,单位cm)
  * @param  None
  * @retval 距离值(cm),0.0表示测量失败
  */
float HCSR04_GetDistanceFloat(void)
{
    uint32_t start_time = 0;
    uint32_t end_time = 0;
    uint32_t duration = 0;
    uint32_t timeout = 0;
    
    // 发送触发信号
    HAL_GPIO_WritePin(TRIG_PIN_GPIO_Port, TRIG_PIN_Pin, GPIO_PIN_SET);
    HCSR04_Delay_us(10);
    HAL_GPIO_WritePin(TRIG_PIN_GPIO_Port, TRIG_PIN_Pin, GPIO_PIN_RESET);
    
    // 等待Echo变高
    timeout = 0;
    while(HAL_GPIO_ReadPin(ECHO_PIN_GPIO_Port, ECHO_PIN_Pin) == GPIO_PIN_RESET)
    {
        if(++timeout > HCSR04_TIMEOUT_US) return 0.0f;
        HCSR04_Delay_us(1);
    }
    
    // 记录开始时间
    start_time = __HAL_TIM_GET_COUNTER(&htim2);
    
    // 等待Echo变低
    timeout = 0;
    while(HAL_GPIO_ReadPin(ECHO_PIN_GPIO_Port, ECHO_PIN_Pin) == GPIO_PIN_SET)
    {
        end_time = __HAL_TIM_GET_COUNTER(&htim2);
        duration = end_time - start_time;
        
        if(duration > HCSR04_TIMEOUT_US) return 0.0f;
        if(++timeout > HCSR04_TIMEOUT_US) return 0.0f;
        HCSR04_Delay_us(1);
    }
    
    // 计算距离(浮点数)
    float distance = duration * HCSR04_TIME_TO_CM;
    
    if(distance < 2.0f) distance = 2.0f;
    if(distance > 400.0f) distance = 400.0f;
    
    return distance;
}

4.3 主程序代码

📝 修改文件:Core/Src/main.c

/* USER CODE BEGIN Includes *//* USER CODE END Includes */之间添加:

c 复制代码
/* USER CODE BEGIN Includes */
#include "ssd1306.h"
#include "hcsr04.h"
#include "stdio.h"
#include "string.h"
/* USER CODE END Includes */

/* USER CODE BEGIN PV *//* USER CODE END PV */之间添加:

c 复制代码
/* USER CODE BEGIN PV */
uint16_t distance_cm = 0;
float distance_float = 0.0f;
char display_buffer[32];
/* USER CODE END PV */

/* USER CODE BEGIN PFP *//* USER CODE END PFP */之间添加:

c 复制代码
/* USER CODE BEGIN PFP */
void Display_Distance(uint16_t dist);
void Display_Distance_Bar(uint16_t dist);
void System_Init_Display(void);
/* USER CODE END PFP */

/* USER CODE BEGIN 0 *//* USER CODE END 0 */之间添加显示函数:

c 复制代码
/* USER CODE BEGIN 0 */

/**
  * @brief  系统初始化显示
  * @param  None
  * @retval None
  */
void System_Init_Display(void)
{
    SSD1306_Clear();
    
    // 显示标题
    SSD1306_DrawString(20, 0, "HC-SR04", 1);
    SSD1306_DrawString(10, 10, "Ultrasonic", 1);
    SSD1306_DrawString(25, 20, "Sensor", 1);
    
    // 显示初始化信息
    SSD1306_DrawString(0, 40, "Initializing...", 1);
    SSD1306_UpdateScreen();
    
    HAL_Delay(1000);
    
    SSD1306_Clear();
    SSD1306_DrawString(0, 40, "Ready!", 1);
    SSD1306_UpdateScreen();
    
    HAL_Delay(500);
}

/**
  * @brief  显示距离值
  * @param  dist: 距离值(cm)
  * @retval None
  */
void Display_Distance(uint16_t dist)
{
    SSD1306_Clear();
    
    // 显示标题
    SSD1306_DrawString(25, 0, "Distance", 1);
    SSD1306_DrawLine(0, 9, 127, 9, 1);
    
    // 显示距离值(大号字体效果)
    sprintf(display_buffer, "%3d", dist);
    SSD1306_DrawString(30, 20, display_buffer, 2);
    SSD1306_DrawString(90, 28, "cm", 1);
    
    // 显示状态
    if(dist < 10)
    {
        SSD1306_DrawString(10, 50, "Too Close!", 1);
    }
    else if(dist > 350)
    {
        SSD1306_DrawString(10, 50, "Out of Range", 1);
    }
    else
    {
        SSD1306_DrawString(25, 50, "Normal", 1);
    }
    
    SSD1306_UpdateScreen();
}

/**
  * @brief  显示距离柱状图
  * @param  dist: 距离值(cm)
  * @retval None
  */
void Display_Distance_Bar(uint16_t dist)
{
    SSD1306_Clear();
    
    // 显示标题
    SSD1306_DrawString(20, 0, "Distance Bar", 1);
    SSD1306_DrawLine(0, 9, 127, 9, 1);
    
    // 计算柱状图长度(最大400cm对应100像素)
    uint8_t bar_length = (uint8_t)((dist > 400) ? 100 : (dist / 4));
    
    // 绘制柱状图背景
    SSD1306_DrawRectangle(10, 20, 108, 15, 1);
    
    // 绘制填充
    if(bar_length > 0)
    {
        SSD1306_FillRectangle(12, 22, bar_length, 11, 1);
    }
    
    // 显示数值
    sprintf(display_buffer, "%d cm", dist);
    SSD1306_DrawString(40, 45, display_buffer, 1);
    
    // 显示范围标记
    SSD1306_DrawString(10, 60, "0", 1);
    SSD1306_DrawString(55, 60, "200", 1);
    SSD1306_DrawString(100, 60, "400", 1);
    
    SSD1306_UpdateScreen();
}

/* USER CODE END 0 */

main()函数的/* USER CODE BEGIN 2 *//* USER CODE END 2 */之间添加初始化代码:

c 复制代码
/* USER CODE BEGIN 2 */
// 初始化OLED
if(SSD1306_Init() != 0)
{
    // OLED初始化失败
    while(1);
}

// 初始化HC-SR04
HCSR04_Init();

// 显示启动画面
System_Init_Display();
/* USER CODE END 2 */

main()函数的/* USER CODE BEGIN WHILE *//* USER CODE END WHILE */之间添加主循环:

c 复制代码
/* USER CODE BEGIN WHILE */
while (1)
{
    // 读取距离值
    distance_cm = HCSR04_GetDistance();
    
    // 显示距离(数字模式)
    Display_Distance(distance_cm);
    HAL_Delay(500);
    
    // 显示距离(柱状图模式)
    Display_Distance_Bar(distance_cm);
    HAL_Delay(500);
    
    /* USER CODE END WHILE */
}

4.4 头文件修改

📝 修改文件:Core/Inc/main.h

/* Private defines -----------------------------------------------------------*/区域添加:

c 复制代码
#define TRIG_PIN_Pin GPIO_PIN_0
#define TRIG_PIN_GPIO_Port GPIOA
#define ECHO_PIN_Pin GPIO_PIN_1
#define ECHO_PIN_GPIO_Port GPIOA

五、系统架构与流程

5.1 系统架构图

显示设备
传感器
STM32F103C8T6
Trig/Echo
回响信号
SCL/SDA
主程序
HC-SR04驱动
SSD1306驱动
GPIO控制
TIM2定时器
I2C1通信
HC-SR04

超声波模块
SSD1306

OLED显示屏

5.2 测距流程图

超时
检测到高电平
超时
检测到低电平
开始测距
初始化GPIO
Trig输出10μs脉冲
等待Echo变高
返回错误
记录开始时间
等待Echo变低
记录结束时间
计算时间差
距离=时间×0.017
限制范围2-400cm
返回距离值
结束
结束

5.3 OLED显示流程

开始显示
清空缓冲区
绘制标题
绘制距离数值
绘制柱状图
绘制状态信息
发送数据到OLED
结束


六、编译与调试

6.1 编译工程

  1. 在STM32CubeIDE中,点击 Project → Build All
  2. 确保编译无错误

6.2 硬件连接

复制代码
完整连接图:

STM32F103C8T6
├─ 5V ───────→ HC-SR04 VCC
├─ GND ──────→ HC-SR04 GND
├─ PA0 ──────→ HC-SR04 Trig
├─ PA1 ──────→ HC-SR04 Echo
├─ 3.3V ─────→ OLED VCC
├─ GND ──────→ OLED GND
├─ PB6 ──────→ OLED SCL
└─ PB7 ──────→ OLED SDA

注意:
1. HC-SR04必须接5V才能正常工作
2. OLED可以接3.3V或5V
3. 确保所有GND共地

6.3 测试验证

测试步骤:

  1. 在OLED前放置障碍物(如书本、手掌)
  2. 观察OLED显示的距离值
  3. 改变障碍物距离,验证数值变化
  4. 测试范围:2cm - 400cm

预期现象:

  • 距离小于10cm时显示"Too Close!"
  • 距离大于350cm时显示"Out of Range"
  • 正常范围内显示具体距离值和柱状图

七、故障排查与问题解决

7.1 测量异常

问题1:距离值始终为0或固定值

排查步骤:

  1. 检查硬件连接

    • 确认Trig和Echo引脚没有接反
    • 检查HC-SR04是否接5V电源
    • 使用万用表测量Trig引脚是否有10μs脉冲
  2. 检查时序

    • 确认触发脉冲宽度为10μs
    • 检查定时器时钟配置(应为1MHz)

调试代码:

c 复制代码
// 在HCSR04_GetDistance中添加调试
printf("Start: %lu, End: %lu, Duration: %lu\r\n", 
       start_time, end_time, duration);

问题2:测量值跳动大

原因分析:

  • 超声波反射不稳定
  • 环境噪声干扰
  • 测量频率过高

解决方案:

方案1:软件滤波

c 复制代码
uint16_t HCSR04_GetDistance_Average(uint8_t times)
{
    uint32_t sum = 0;
    uint8_t valid_count = 0;
    
    for(uint8_t i = 0; i < times; i++)
    {
        uint16_t dist = HCSR04_GetDistance();
        if(dist > 0)
        {
            sum += dist;
            valid_count++;
        }
        HAL_Delay(60);  // HC-SR04最小测量周期
    }
    
    return (valid_count > 0) ? (sum / valid_count) : 0;
}

方案2:中值滤波

c 复制代码
uint16_t HCSR04_GetDistance_Median(void)
{
    uint16_t values[5];
    
    for(uint8_t i = 0; i < 5; i++)
    {
        values[i] = HCSR04_GetDistance();
        HAL_Delay(60);
    }
    
    // 冒泡排序取中值
    for(uint8_t i = 0; i < 4; i++)
    {
        for(uint8_t j = 0; j < 4-i; j++)
        {
            if(values[j] > values[j+1])
            {
                uint16_t temp = values[j];
                values[j] = values[j+1];
                values[j+1] = temp;
            }
        }
    }
    
    return values[2];  // 返回中值
}

7.2 OLED显示异常

问题3:OLED不显示或显示乱码

排查步骤:

  1. 检查I2C地址

    • 使用I2C扫描程序确认设备地址
    • 常见地址:0x78(写)或0x3C(7位地址)
  2. 检查I2C连接

    • 确认SCL和SDA没有接反
    • 检查是否有上拉电阻(4.7KΩ)

I2C扫描代码:

c 复制代码
void I2C_Scan(void)
{
    printf("Scanning I2C bus...\r\n");
    for(uint8_t addr = 0; addr < 128; addr++)
    {
        if(HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 1, 100) == HAL_OK)
        {
            printf("Found device at 0x%02X\r\n", addr);
        }
    }
}

7.3 性能优化

问题4:测量刷新率低

优化方案:

  1. 使用定时器输入捕获
c 复制代码
// 配置TIM2为输入捕获模式
// 在Echo上升沿和下降沿自动捕获时间
// 减少CPU占用,提高精度
  1. DMA传输
c 复制代码
// 使用DMA传输OLED数据
// 减少CPU等待时间

八、总结与扩展

8.1 核心知识点回顾

  1. 超声波测距原理:理解声波传播时间和距离的关系,掌握HC-SR04的时序控制
  2. 定时器应用:学会使用定时器进行微秒级精确计时
  3. I2C通信协议:理解I2C总线协议,掌握SSD1306的驱动方法
  4. 显示缓冲区管理:理解帧缓冲区的概念,实现图形绘制
  5. 多外设协同:学会协调多个外设的工作时序

8.2 扩展学习方向

功能扩展:

  • 多传感器组网:使用多个HC-SR04实现全方位测距
  • 数据记录:将距离数据保存到SD卡
  • 无线传输:通过WiFi/蓝牙实时上传数据
  • 报警系统:距离过近时声光报警
  • 自动跟随:结合舵机实现自动跟踪

进阶项目:

  • 倒车雷达系统:多传感器+蜂鸣器
  • 液位监测系统:长期监测水箱液位
  • 智能垃圾桶:自动开盖+满溢检测
  • 机器人避障:结合电机实现自动避障

8.3 学习资源

官方文档:


💡 提示:HC-SR04的测量精度受温度影响(声速随温度变化),在精度要求高的场景下,建议添加温度补偿:实际声速 = 331.5 + 0.6 × 温度(℃)。可以结合DHT11温湿度传感器实现温度补偿,提高测量精度。

相关推荐
yoyobravery1 小时前
蓝桥杯第16届单片机
单片机·职场和发展·蓝桥杯
somi71 小时前
ARM-04-驱动-Misc ,Platform ,DTS
arm开发·单片机·嵌入式硬件·自用
never forget shyang2 小时前
CCS20.2.0使用教程
c语言·git·单片机
UTP协同自动化测试11 小时前
物联网模组测试难点 |APP指令下发+UART 响应+GPIO 电平变化,如何一次性验证?
功能测试·嵌入式硬件·物联网·模块测试
yoyobravery12 小时前
蓝桥杯第15届单片机满分
单片机·职场和发展·蓝桥杯
4caf114 小时前
作业2:6位数码管静态显示
嵌入式硬件·51单片机
不做无法实现的梦~14 小时前
STM32解析PPM协议
stm32·单片机·嵌入式硬件
czhaii15 小时前
基于Arm Cortex-M7内核GD32H7
单片机·嵌入式硬件
番茄灭世神15 小时前
MCU开发常见软件BUG总结(持续更新)
c语言·stm32·单片机·嵌入式·gd32