
简介面向嵌入式初、中级开发者的STM32单片机超声波测距完整工程源码以HC-SR04传感器采集距离数据经STM32处理后在OLED屏实时显示同时驱动蜂鸣器实现超限报警并通过UART将测距结果发送至串口调试助手适用于智能小车避障、安防监测等场景。压缩包共233个文件约6.33MB主要包含C源码.c、头文件.h、Keil工程配置.uvprojx/.uvoptx及编译中间文件.o/.d/.crf等目录结构完整便于直接打开和二次开发。已有3132人学习下载。项目代码涵盖STM32的HAL/LL库及外设驱动覆盖GPIO、定时器、串口中断、OLED显示、超声波回波捕获等关键模块并为蜂鸣器报警阈值、显示刷新等留有明确接口可帮助读者快速理解嵌入式多外设协同开发流程适合作为课程设计或毕业设计的参考资料。1. 测距不难难的是让测距不卡住整机做超声波测距第一版代码十个有九个是写 while 死等回波电平翻转。数据能出来但 OLED 刷新时测距会卡顿串口输出节奏跟着乱蜂鸣器报警也慢半拍。问题不在 HC-SR04而在 Echo 高电平最长要持续 23ms这段时间如果让主循环死等整机等于被传感器绑架。这个工程给出的方案是把回波高电平宽度的测量交给 STM32 的定时器输入捕获来干主循环只做计算、显示和报警串口还能按固定节奏把测距数据发给电脑上的串口调试助手。工程基于 STM32F103 标准外设库手里有最小系统板就能跑适合刚点亮过一颗 LED、想完整串一遍外设的人也适合拿它当课程设计或毕设的基座。2. STM32 定时器输入捕获把 HC-SR04 回波时间交给硬件打开工程前先确认 Keil 的 Pack Installer 里已经装了 STM32F1xx 器件包。Keil5 不像 Keil4 那样打开即用没装包时 Device 下拉是空的直接编译会报找不到目标芯片。这个工程用的是标准外设库SPL源文件里的 stm32f10x_tim.c、stm32f10x_i2c.c、stm32f10x_adc.c 正好对应三个外设定时器测回波、I2C 驱动 OLED、ADC 读阈值电位器。2.1 HC-SR04 测距的本质先测量时间再换算距离HC-SR04 正面两个金属筒一个发 40kHz 超声波脉冲一个接收回波。STM32 给 Trig 引脚一个至少 10us 的高电平模块内部会连发 8 个 40kHz 脉冲同时将 Echo 引脚拉高。Echo 高电平持续的时间就是声波从发射到碰到障碍物再返回的总时长所以测距本质是测量一段高电平宽度再用声速换算。空气中的声速取 340m/s高电平时间t_us对应的是往返时间换算成单程距离后公式化简为距离(cm) 时间(us) * 0.017推导过程是1us * 340m/s / 2 / 1000单位换算后正好是 0.017cm。反过来估算时间是时间(us) 距离(cm) * 58.8。0.017 这个系数带入 1cm 误差约 1mm对报警、显示和串口观察都足够。| 目标距离 | 声波往返时间 | 说明 | | 10cm | 588us | 阈值报警常出现的区间 | | 100cm | 5882us | 桌面测试最常用的距离 | | 400cm | 23529us | 接近传感器标称上限 |这个表解释代码里为什么要把时基配成 1MHz因为每个计数就是 1us读到的 CCR 值直接当时间用不用再乘分频系数。2.2 轮询 vs 输入捕获23ms 的等待时间不能浪费第一个版本大多数新手写的是轮询方式触发后 while 读 Echo 引脚等它变高再开秒表等它变低再停。优点是代码直白缺点是 Echo 在 4m 量程时高电平要持续约 23ms这段时间主循环被完全卡住。OLED 刷新会暂停蜂鸣器报警会延迟串口输出节奏也不稳定。输入捕获的方案则完全不同TIM2_CH1 检测到上升沿时硬件自动记录当前计数值并触发中断接着等下降沿再次捕获。整个过程主循环不参与电平等待这也是这个工程在结构上比第一版 demo 更接近真实产品的原因。配置要点如下表| 配置项 | 值 | 原因 | | Echo 引脚 | PA0 (TIM2_CH1) | 默认映射无需重映射 | | Trig 引脚 | PB10 | 避开 SWD 调试口和 I2C 引脚 | | 预分频器 | 71 | 72MHz/72 1MHz1 计数 1us | | 自动重装载 | 0xFFFF | 65535us 远大于回波最长时间 23529us |2.3 触发函数12us 高电平脉冲HC-SR04 的触发逻辑非常简短关键是 Trig 高电平的脉宽要够。规格书要求不小于 10us工程里给 12us 留出余量。#define TRIG_PORT GPIOB #define TRIG_PIN GPIO_Pin_10 void HC_SR04_Trigger(void) { GPIO_SetBits(TRIG_PORT, TRIG_PIN); delay_us(12); /* 规格书要求不小于 10us留余量 */ GPIO_ResetBits(TRIG_PORT, TRIG_PIN); }触发函数的重点是 12us 这个脉宽。如果板子上 Trig 走线较长分布电容会把窄脉冲吃掉这时可以把延时拉到 20us不影响最终测距精度因为 Echo 高电平宽度由模块内部逻辑决定与 Trig 脉宽无关。这里的delay_us在工程里一般由 SysTick 或 DWT 实现不要在中断里调用它。2.4 TIM2 输入捕获配置与中断处理输入捕获初始化里先开 TIM2 和 GPIOA 时钟再把 PA0 配成输入模式。Echo 空闲时低选择浮空输入即可。时基部分把计数频率拉到 1MHz计数范围用 16 位最大值的 0xFFFF保证 23ms 的回波不会造成计数器溢出。void TIM2_IC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_ICInitTypeDef TIM_ICInitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler 71; /* 72MHz / 72 1MHz1us 计数 */ TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0F; /* 滤掉窄毛刺 */ TIM_ICInit(TIM2, TIM_ICInitStructure); NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_ITConfig(TIM2, TIM_IT_CC1 | TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); }TIM_ICFilter 设为 0x0F 的作用是开启输入滤波可以剔除电子噪声引起的窄脉冲避免在 Echo 空闲状态误触发捕获中断。中断服务函数是测量逻辑的主体用一个状态变量区分正在等待上升沿还是下降沿。第一次进中断时把计数器清零并改为下降沿捕获第二次进中断时读 CCR1 值作为高电平时间。代码里TIM_OC1PolarityConfig虽然叫输出比较但它操作的是 CCER 寄存器的 CC1P 位恰好就是输入捕获边沿选择的位置。volatile uint32_t echo_us 0; volatile uint8_t echo_ready 0; static uint8_t echo_state 0; /* 0: 等待上升沿, 1: 等待下降沿 */ void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); if (echo_state 0) { TIM_SetCounter(TIM2, 0); /* 从0计数 */ TIM_OC1PolarityConfig(TIM2, TIM_ICPolarity_Falling); echo_state 1; } else { echo_us TIM_GetCapture1(TIM2); TIM_OC1PolarityConfig(TIM2, TIM_ICPolarity_Rising); echo_ready 1; echo_state 0; } } if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); if (echo_state 1) { /* 高电平超过 65ms按超时处理 */ echo_us 0xFFFFFFFF; echo_ready 1; echo_state 0; } } }距离转换不要放在中断里中断里只置echo_ready。主循环发现echo_ready后再做浮点计算否则浮点运算会拉长中断执行时间下一次捕获中断可能被延迟。超时分支把echo_us写成 0xFFFFFFFF主循环里判断这个值就能区分是正常回波还是空数据。3. OLED 屏幕的 SSD1306 驱动与 UART 串口数据上传0.96 寸 OLED 模块出厂默认是 I2C 或者 SPI 接口硬件的差异直接决定驱动代码。这个工程把 OLED 放在 PB6/PB7这两个引脚同时也是 I2C1 的复用引脚不过这里用软件模拟 I2C 来驱动。3.1 为什么用软件模拟 I2C 而不是硬件 I2C标准外设库的硬件 I2C 有总线仲裁、超时恢复等问题初始化错一个时序位问题就出现在看似正常的下一次通信上。软件模拟 I2C 完全由代码控制时钟和数据的翻转顺序出问题也好定位只要示波器或逻辑分析仪挂在 SCL/SDA 上就能看清是哪一步没走出来。网上大量“stm32hal库点亮oled屏幕全流程”的例子是基于 HAL 库的API 差异大直接搬进标准库工程要改引脚时钟、GPIO 模式和传输函数不建议混着抄。工程源码里保留 stm32f10x_i2c.c 是因为标准外设库工程通常会整组添加外设驱动文件实际驱动路径走的是软件模拟 IO。| 对比项 | 硬件 I2C | 软件模拟 | | 时钟产生 | 外设自动产生 | 代码翻转 GPIO | | 总线忙恢复 | 需要额外处理 | 重新初始化引脚即可 | | 代码量 | 少但难调 | 多但直观 | | 标准库支持 | 状态机复杂 | 无依赖 |3.2 软件 I2C 时序与 SSD1306 初始化SSD1306 是 OLED 的控制芯片128×64 分辨率I2C 设备地址由模块上电阻决定常见 7 位地址 0x3C写地址是 0x78。通信时先发器件地址再发控制字节控制字节 0x00 表示后续是命令、0x40 表示后续是数据。软件 I2C 的四步是起始、传字节、停止SCL 高电平期间 SDA 必须稳定否则会被误认为起始或停止信号。#define OLED_SCL_H() GPIO_SetBits(GPIOB, GPIO_Pin_6) #define OLED_SCL_L() GPIO_ResetBits(GPIOB, GPIO_Pin_6) #define OLED_SDA_H() GPIO_SetBits(GPIOB, GPIO_Pin_7) #define OLED_SDA_L() GPIO_ResetBits(GPIOB, GPIO_Pin_7) static void OLED_I2C_Start(void) { OLED_SDA_H(); OLED_SCL_H(); delay_us(5); OLED_SDA_L(); delay_us(5); OLED_SCL_L(); } static void OLED_I2C_Stop(void) { OLED_SDA_L(); OLED_SCL_H(); OLED_SDA_H(); delay_us(5); } static void OLED_I2C_SendByte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { if (dat 0x80) OLED_SDA_H(); else OLED_SDA_L(); OLED_SCL_H(); delay_us(2); OLED_SCL_L(); dat 1; } } static void OLED_WriteByte(uint8_t dat, uint8_t type) /* type 0:命令 1:数据 */ { OLED_I2C_Start(); OLED_I2C_SendByte(0x78); /* 写地址7位地址0x3C左移一位 */ OLED_I2C_SendByte(type ? 0x40 : 0x00); OLED_I2C_SendByte(dat); OLED_I2C_Stop(); } #define OLED_WR_CMD(cmd) OLED_WriteByte(cmd, 0) #define OLED_WR_DAT(dat) OLED_WriteByte(dat, 1)这段代码没有读 ACK。模块内部是慢速 I2C 从机延迟足够宽不读 ACK 不影响可靠性万一总线被拉死重新上电或复位再初始化即可。写地址如果不对现象通常是从头到尾白屏硬件上要区分模块背后的电阻配置是接 0x78 还是 0x7A。初始化序列里最容易漏的是电荷泵命令。SSD1306 内部升压电路默认关闭如果只写了 0xAF 开显示而没有先写 0x8D 0x14屏幕会一直白屏或完全不亮但 I2C 通信又是正常的示波器上能看到 SCL/SDA 有波形。初始化代码如下void OLED_Init(void) { delay_ms(100); /* 等模块上电稳定 */ OLED_WR_CMD(0xAE); /* 关闭显示 */ OLED_WR_CMD(0xD5); OLED_WR_CMD(0x80); OLED_WR_CMD(0xA8); OLED_WR_CMD(0x3F); /* 64行 */ OLED_WR_CMD(0xD3); OLED_WR_CMD(0x00); OLED_WR_CMD(0x40); /* 显示起始行 */ OLED_WR_CMD(0x8D); OLED_WR_CMD(0x14); /* 开电荷泵漏这句白屏 */ OLED_WR_CMD(0x20); OLED_WR_CMD(0x02); /* 页寻址 */ OLED_WR_CMD(0xA1); /* 段重映射 */ OLED_WR_CMD(0xC8); /* COM扫描方向 */ OLED_WR_CMD(0xDA); OLED_WR_CMD(0x12); OLED_WR_CMD(0x81); OLED_WR_CMD(0xCF); /* 对比度 */ OLED_WR_CMD(0xD9); OLED_WR_CMD(0xF1); OLED_WR_CMD(0xDB); OLED_WR_CMD(0x40); OLED_WR_CMD(0xA4); OLED_WR_CMD(0xA6); /* 正常显示 */ OLED_WR_CMD(0xAF); /* 打开显示 */ OLED_Clear(); }命令 0x20 的寻址模式要留意。页寻址下写入自动跳到同一页的下一列跨页需要重新设置列地址和页地址。如果显示出现字符错位多半是在页地址切换时漏了 OLED_Set_Pos 里的 0xB0 加页码命令。页地址范围是 0~7对应 64 行的 8 个行块每一页 8 个像素高。3.3 把距离显示在 OLED 上显示距离值实际上是把数字拆成单字符再查 ASCII 字模表写入显存。字符字模通过取模软件生成十六进制数组按列排列每个字符 16×8。OLED_Show_Num 的循环里用一个 divisor 取出特定位的数字再转成 ASCII 码传给 OLED_Show_Char。void OLED_Show_Num(uint8_t x, uint8_t y, uint16_t num, uint8_t len) { uint8_t i, k; for (i 0; i len; i) { uint16_t divisor 1; for (k 0; k len - 1 - i; k) { divisor * 10; } uint8_t ch (num / divisor) % 10; OLED_Show_Char(x i * 8, y, ch 0, 16); } }len表示显示的位数例如显示三位距离就填 3如果实际距离只有两位数最前面会显示 0所以通常配合背景填充把多余位刷成空白。OLED_Show_Char 内部会先设置页地址和列地址再把 16 行字模逐字节写入。主界面第一行可以放固定标题“Distance:”第二行显示数值第三行显示阈值布局如下OLED_Show_String(8, 0, Distance:, 16); OLED_Show_Num( 8, 2, (uint16_t)dist_cm, 3); OLED_Show_Num( 8, 4, (uint16_t)thr_cm, 3);3.4 UART 串口发送SSCOM 里看实时数据串口上传这部分用的是 USART1PA9 做 TXPA10 做 RX和 USB-TTL 连接时注意交叉连接板子的 TX 接模块的 RX板子的 RX 接模块的 TX还要共地。串口参数固定为 115200-8-N-1波特率不一致时现象是乱码或者数据偶尔跳一个字符。void USART1_Init(uint32_t baud) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate baud; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE); } int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }fputc 重定向之后main 里直接 printf 就能把数据发出去。有一个前提Keil 工程必须勾选 Use MicroLIB否则重定向的 printf 不生效现象是程序在 while 里正常跑但电脑上收不到任何数据。推荐的数据格式是带单位、带阈值的文本帧例如printf(DIST:%.1fcm,THR:%.1fcm\r\n, dist_cm, thr_cm);SSCOM 或正点原子 XCOM 这类串口调试助手都能直接显示。勾选“显示时间戳”可以看到每次测量的时间间隔如果间隔抖动明显说明 main 循环里的阻塞延时不是定值需要检查测距等待和 OLED 刷新的配合。串口发送不需要中断查询发送标志位就够了数据量只有一帧文本占不满串口带宽。4. 蜂鸣器报警联动与 ADC 动态阈值调节报警不是简单比较一次距离就响需要考虑蜂鸣器类型、误触发和阈值来源。这个工程里蜂鸣器用 GPIO 直控方案ADC 则负责把阈值做成可调。4.1 有源蜂鸣器与无源蜂鸣器的驱动差异蜂鸣器分有源和无源两类。“有源”的“源”指振荡源有源蜂鸣器内部有振荡电路通电就响音调固定无源蜂鸣器内部没有振荡源需要外部给方波或 PWM 才发声音调由频率决定。驱动电路上两者差别不大常见做法是用一个 NPN 三极管做开关GPIO 输出通过 1k 电阻接基极蜂鸣器接在 5V 和三极管集电极之间发射极接地。网上搜“无源蜂鸣器驱动电路图”看到的多数是这种结构区别只在于基极要不要加下拉电阻防止单片机上电瞬间 GPIO 悬空导致误响。| 类型 | 振荡源 | 驱动方式 | 声音特点 | 适用场景 | | 有源 | 内置 | GPIO 电平 | 固定频率 | 报警开关、提示音 | | 无源 | 无 | PWM/方波 | 频率可调 | 不同距离不同音调 |如果是有源蜂鸣器工程里只需要推挽输出控制电平。蜂鸣器接 PB5初始化代码如下void BEEP_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_ResetBits(GPIOB, GPIO_Pin_5); } #define BEEP_ON() GPIO_SetBits(GPIOB, GPIO_Pin_5) #define BEEP_OFF() GPIO_ResetBits(GPIOB, GPIO_Pin_5)换成无源蜂鸣器时把同一引脚复用为定时器 PWM 输出例如 TIM3_CH3 的 PB0初始化后通过修改 TIM_Period 控制频率、通过占空比控制音量。报警音如果做成距离越近音调越高人耳判断比看数字更直观。注意有源蜂鸣器的发声频率是固定的PWM 对无效非要做变调必须换无源蜂鸣器。注意切换蜂鸣器类型时驱动代码、供电电流和硬件电路都要一起改有源蜂鸣器直驱的 GPIO 推挽代码不能直接套在无源蜂鸣器上。4.2 阈值报警不需要每次数据都响应HC-SR04 在目标边缘和透明、吸音物体前会产生单次异常值比如距离突然从 50cm 跳到 10cm一次就判断并鸣叫会出现误报。报警逻辑里加一个连续计数连续若干次低于阈值才拉高蜂鸣器只要有一次正常值就把计数器清零。这样既不会漏报也不会被单个离群点干扰。#define OVER_LIMIT 3 #define DEFAULT_THR 30.0f /* 默认报警阈值单位cm */ static uint8_t over_cnt 0; static float thr_cm DEFAULT_THR; void Alarm_Update(float dist_cm) { if (dist_cm thr_cm) { if (over_cnt OVER_LIMIT) over_cnt; if (over_cnt OVER_LIMIT) { BEEP_ON(); } } else { over_cnt 0; BEEP_OFF(); } }OVER_LIMIT 设 3 意味着连续 3 次测距都低于阈值才报警。在 100ms 的测距周期下响应延迟约 300ms人耳几乎无感。如果场景里飞虫或落叶频繁穿越探头造成误报把 OVER_LIMIT 调到 5 即可代价是报警响应慢一些。4.3 ADC 读电位器报警阈值可调才方便实测stm32f10x_adc.c 在这个工程里承担的就是阈值采样。PA1 接一个 10k 电位器电位器两端分别接 3.3V 和 GND中间抽头接 PA1旋转电位器就能改变 PA1 上的分压值。STM32F103 的 ADC 是 12 位采样值范围 0~4095对应 0~3.3V。阈值距离用比例换算电位器调到最大时阈值接近 100cm调到最小时接近 0cm。void ADC1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); /* ADC时钟最高不超过14MHz */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5); ADC_Cmd(ADC1, ENABLE); } uint16_t ADC_Read_Threshold(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) RESET); return ADC_GetConversionValue(ADC1); }电位器信号是直流缓慢变化没必要开连续转换和扫描模式单次转换足够。ADC 采样时间用 55.5 周期内部采样电容能充分充电读数稳定如果电位器到 PA1 的走线很长可以在 PA1 对地并联一个 0.1uF 电容滤掉高频干扰ADC 值就不会反复跳。主循环里把这些模块串起来完整骨架如下int main(void) { uint16_t adc; float thr_cm_set; float dist_cm; SystemInit(); BEEP_GPIO_Init(); USART1_Init(115200); OLED_Init(); OLED_Show_String(8, 0, Distance:, 16); TIM2_IC_Init(); ADC1_Init(); while (1) { HC_SR04_Trigger(); delay_ms(60); /* 触发间隔必须大于最大回波时间 */ if (echo_ready) { if (echo_us 0xFFFFFFFF) { echo_ready 0; continue; } dist_cm echo_us * 0.017f; echo_ready 0; adc ADC_Read_Threshold(); thr_cm_set (float)adc * 100.0f / 4096.0f; OLED_Show_Num(8, 2, (uint16_t)dist_cm, 3); OLED_Show_Num(8, 4, (uint16_t)thr_cm_set, 3); Alarm_Update(dist_cm); printf(DIST:%.1fcm,THR:%.1fcm\r\n, dist_cm, thr_cm_set); } } }delay_ms(60) 是测距周期也是报警响应的一条时间基线。HC-SR04 在 4m 量程时回波最长 23ms60ms 周期可以保证上一次回波完全结束后才开始下一次触发丢波率明显下降。周期也不是越小越好太短会连续误触发数据帧会变得稀疏。5. 数据滤波、整机验证与故障排查5.1 中值滤波让距离数据不再乱跳HC-SR04 的数据抖动主要来自两种场景被测物边缘在波束边缘来回移动或者波束打到倾斜表面后反射路径不固定。这类异常点的特征是偶发且幅度大用滑动平均反而会把一次大的离群值带入结果影响后续好几帧数据。中值滤波输出的是窗口内排序后的中间值能直接丢掉离群点窗口取 5 实现如下#define FILTER_N 5 static uint16_t ring[FILTER_N]; static uint8_t ring_idx; uint16_t Median_Filter(uint16_t sample) { uint8_t i, j; uint16_t tmp, buf[FILTER_N]; ring[ring_idx] sample; ring_idx (ring_idx 1) % FILTER_N; for (i 0; i FILTER_N; i) { buf[i] ring[i]; } for (i 0; i FILTER_N - 1; i) { for (j i 1; j FILTER_N; j) { if (buf[j] buf[i]) { tmp buf[i]; buf[i] buf[j]; buf[j] tmp; } } } return buf[FILTER_N / 2]; }调用时把距离先算出来再进入滤波dist_cm Median_Filter((uint16_t)(echo_us * 0.017f)) * 1.0f;。这里的输出还是整数厘米对报警和显示足够。如果后续要做 0.1cm 精度的数据记录就把滤波对象改成 echo_us 而不是距离浮点放大后再滤波会保留更多分辨率。5.2 整机验证与排查清单整机验证建议按下面的顺序走先不接 HC-SR04单独用 USB-TTL 连串口确认上电能收到初始化打印再接 HC-SR04用手在探头前 20cm 和 80cm 之间来回移动观察 OLED 数字是否连续变化最后旋转电位器把阈值调到 30cm 左右用手快速划过探头确认蜂鸣器能响应。几个最常见的故障直接列在表里| 现象 | 原因 | 排查方向 | | ST-Link 报 no target found | SWD 速率太高或接触不良 | 在 ST-Link Utility 里把速率降到 1MHz检查 SWDIO/SWCLK 接线 | | 串口乱码 | 晶振频率和库配置不一致 | 核对 stm32f10x.h 的 HSE_VALUE 与实际晶振检查起振电容 | | OLED 白屏但 I2C 有波形 | 电荷泵命令缺失 | 检查 0x8D 0x14 是否在 0xAF 之前执行 | | 距离恒为 0 或恒为最大值 | 触发间隔太短或 Echo 引脚没接对 | 确认 Trig 脉宽测距周期保持 60ms 以上 |no target found 还有一个容易被忽略的点如果板子上同时接了 HC-SR04 和 OLED它们的电源滤波电容不足测量瞬间电流冲击会把调试器拉掉。给 VCC 和 GND 之间并一个 100uF 电解电容多数情况下就好了。最后在 SSCOM 里勾选时间戳观察数据帧间隔稳定在 100ms 左右、数值变化平滑说明整个采集链路是通的后面再往上叠透传、数据记录或者多传感器轮询都只是在这个骨架上加外设的问题。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。