文章目录
-
- 一、前言
-
- [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 测试验证)
- 七、故障排查与问题解决
-
- [7.1 测量异常](#7.1 测量异常)
- [7.2 OLED显示异常](#7.2 OLED显示异常)
- [7.3 性能优化](#7.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 读者收获
完成本教程后,你将能够:
- 独立使用HC-SR04进行距离测量
- 掌握超声波测距的时序控制和计算方法
- 理解I2C通信协议,实现OLED显示驱动
- 具备多外设协同工作的开发能力
- 学会设计简单的人机交互界面
技术栈:
- 开发板: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 新建工程
- 打开STM32CubeMX,点击 File → New Project
- 选择 STM32F103C8Tx ,点击 Start Project
3.1.2 配置时钟树
- 点击 Clock Configuration 标签
- 配置HSE为Crystal/Ceramic Resonator
- 设置PLL倍频为x9,系统时钟72MHz
- TIM2时钟72MHz(用于超声波计时)
3.1.3 配置GPIO
配置HC-SR04引脚:
- 点击PA0引脚,选择 GPIO_Output,User Label:"TRIG_PIN"
- 点击PA1引脚,选择 GPIO_Input,User Label:"ECHO_PIN"
配置I2C引脚:
- 点击PB6引脚,选择 I2C1_SCL
- 点击PB7引脚,选择 I2C1_SDA
3.1.4 配置TIM2(用于超声波计时)
- 点击 Timers → TIM2
- Clock Source :选择 Internal Clock
- Channel1 :选择 Input Capture direct mode
- 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
- 点击 Connectivity → I2C1
- Mode :选择 I2C
- Configuration :
- I2C Speed Mode:Fast Mode(400kHz)或 Standard Mode(100kHz)
- Clock Stretching:Disable(SSD1306不需要)
3.1.6 配置GPIO参数
-
点击 GPIO 标签
-
配置PA0(TRIG_PIN):
- Mode:Output Push Pull
- Pull:No pull-up/pull-down
- Speed:High
- User Label:TRIG_PIN
-
配置PA1(ECHO_PIN):
- Mode:Input
- Pull:No pull-up/pull-down
- User Label:ECHO_PIN
3.1.7 生成代码
- 点击 Project → Settings
- Project Name:HCSR04_OLED
- Toolchain:STM32CubeIDE
- 勾选 Generate peripheral initialization as a pair of '.c/.h' files
- 点击 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 编译工程
- 在STM32CubeIDE中,点击 Project → Build All
- 确保编译无错误
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 测试验证
测试步骤:
- 在OLED前放置障碍物(如书本、手掌)
- 观察OLED显示的距离值
- 改变障碍物距离,验证数值变化
- 测试范围:2cm - 400cm
预期现象:
- 距离小于10cm时显示"Too Close!"
- 距离大于350cm时显示"Out of Range"
- 正常范围内显示具体距离值和柱状图
七、故障排查与问题解决
7.1 测量异常
问题1:距离值始终为0或固定值
排查步骤:
-
检查硬件连接
- 确认Trig和Echo引脚没有接反
- 检查HC-SR04是否接5V电源
- 使用万用表测量Trig引脚是否有10μs脉冲
-
检查时序
- 确认触发脉冲宽度为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不显示或显示乱码
排查步骤:
-
检查I2C地址
- 使用I2C扫描程序确认设备地址
- 常见地址:0x78(写)或0x3C(7位地址)
-
检查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:测量刷新率低
优化方案:
- 使用定时器输入捕获
c
// 配置TIM2为输入捕获模式
// 在Echo上升沿和下降沿自动捕获时间
// 减少CPU占用,提高精度
- DMA传输
c
// 使用DMA传输OLED数据
// 减少CPU等待时间
八、总结与扩展
8.1 核心知识点回顾
- 超声波测距原理:理解声波传播时间和距离的关系,掌握HC-SR04的时序控制
- 定时器应用:学会使用定时器进行微秒级精确计时
- I2C通信协议:理解I2C总线协议,掌握SSD1306的驱动方法
- 显示缓冲区管理:理解帧缓冲区的概念,实现图形绘制
- 多外设协同:学会协调多个外设的工作时序
8.2 扩展学习方向
功能扩展:
- 多传感器组网:使用多个HC-SR04实现全方位测距
- 数据记录:将距离数据保存到SD卡
- 无线传输:通过WiFi/蓝牙实时上传数据
- 报警系统:距离过近时声光报警
- 自动跟随:结合舵机实现自动跟踪
进阶项目:
- 倒车雷达系统:多传感器+蜂鸣器
- 液位监测系统:长期监测水箱液位
- 智能垃圾桶:自动开盖+满溢检测
- 机器人避障:结合电机实现自动避障
8.3 学习资源
官方文档:
💡 提示:HC-SR04的测量精度受温度影响(声速随温度变化),在精度要求高的场景下,建议添加温度补偿:实际声速 = 331.5 + 0.6 × 温度(℃)。可以结合DHT11温湿度传感器实现温度补偿,提高测量精度。