
1. 项目缘起与核心需求拆解1.1 为什么选择超声波测距加温度补偿这个方向做单片机项目的人都有一个共识超声波测距是入门嵌入式最经典的练手项目之一但也是最容易被低估的项目。很多人觉得HC-SR04驱动一下、读个回波时间、乘个声速就完事了实际上真正拿到实际场景里跑你会发现数据飘得厉害。夏天中午测出来的距离和冬天早上测出来的距离对着同一面墙能差出好几厘米。这不是传感器坏了而是声速本身在变。声速在空气中的传播速度与温度强相关0摄氏度时大约是331.4米每秒每升高1摄氏度声速大约增加0.6米每秒。到了40摄氏度的高温环境声速已经变成355米每秒左右了。如果你固件里写死340米每秒来算距离在高温环境下误差能到4%以上。测个3米远的物体误差就超过12厘米。对于避障小车、液位检测、停车辅助这类应用来说这个误差是不可接受的。所以这个项目的核心价值就一句话用DS18B20实时采集环境温度动态修正声速让超声波测距在不同温度下都能保持稳定精度。STM32作为主控负责定时器捕获回波信号、单总线驱动DS18B20、以及最终的数据处理和显示输出。1.2 这个系统适合谁参考如果你正在做基于STM32的毕业设计、课程设计或者想做一个有实际精度的测距模块用于自己的小车、机器人项目这套方案可以直接抄作业。它不复杂但涉及的知识点很全面GPIO输入输出、定时器输入捕获、单总线时序、串口通信、OLED显示基本上把STM32开发的常用外设都串了一遍。对于刚学完STM32基础外设、想找一个综合项目练手的朋友来说这个项目的性价比非常高。另外如果你之前做过超声波测距但没加温度补偿发现数据总是不够准那这篇文章正好能帮你把最后那块短板补上。我会把温度补偿的数学原理、DS18B20的时序细节、定时器捕获的配置要点都拆开讲清楚让你不仅知道怎么接更知道为什么这么接。2. 系统整体设计与方案选型2.1 硬件架构的确定整个系统的硬件框图很清晰STM32F103C8T6作为主控HC-SR04超声波模块负责发射和接收DS18B20数字温度传感器采集环境温度0.96寸OLED显示屏做本地数据显示另外预留串口用于调试和上位机通信。为什么选STM32F103C8T6这颗芯片在嵌入式圈子里被称为“国民MCU”72MHz主频、64KB Flash、20KB SRAM对于这个项目来说绰绰有余。更重要的是它的定时器资源丰富有4个通用定时器加2个高级定时器输入捕获功能成熟稳定资料铺天盖地遇到问题随便一搜就有答案。价格也便宜核心板十块钱左右就能拿下。HC-SR04就更不用说了几块钱的东西测距范围2cm到400cm精度标称3mm实际使用中配合温度补偿能做到1cm以内。它的工作原理很简单Trig引脚给一个10微秒以上的高电平脉冲模块自动发射8个40kHz的超声波脉冲然后Echo引脚会输出一个高电平高电平的持续时间就是超声波从发射到遇到障碍物反射回来的总时间。DS18B20选它的理由也很直接数字输出不需要额外的ADC单总线接口只占一个GPIO测温范围-55到125摄氏度精度在-10到85范围内是正负0.5摄氏度。对于温度补偿来说这个精度完全够用。而且它支持 parasite power 模式可以只用两根线数据线和地线工作接线更简洁。2.2 温度补偿的数学原理这里我把温度补偿的公式推导说清楚因为很多人只知道要补偿但不知道公式怎么来的。声速与温度的关系在标准大气压下可以用这个经验公式近似v 331.4 0.607 × T其中v是声速单位米每秒T是摄氏温度。这个公式在零下20度到零上60度的范围内误差很小完全满足我们的需求。那么距离的计算就变成距离 (回波高电平时间 × v) / 2除以2是因为超声波跑了个来回。回波时间单位是秒v单位是米每秒算出来距离单位是米。如果要显示厘米再乘100。举个例子25摄氏度时v 331.4 0.607 × 25 346.575 米每秒。如果回波时间是 10ms那么距离 (0.01 × 346.575) / 2 1.7329 米也就是173.29厘米。如果不做温度补偿用固定的340米每秒算距离 (0.01 × 340) / 2 1.7米差了将近3.3厘米。在25度这个常温下就已经有可见误差了更别说极端温度环境。2.3 为什么不用其他方案有人可能会问为什么不直接用激光测距模块激光测距精度确实高但成本也高而且对于透明物体、软质材料表面的测量效果不好。超声波对物体材质不敏感玻璃、液体、布料都能测这是它的天然优势。还有人问为什么不用DHT11测温DHT11精度是正负2摄氏度对于温度补偿来说误差偏大。DS18B20的0.5度精度能把声速误差控制在0.3米每秒以内对应到距离上基本可以忽略。而且DHT11响应速度慢一秒钟只能读一次DS18B20可以做到750毫秒一次转换时间更短。3. 核心细节解析与实操要点3.1 HC-SR04的驱动细节与定时器捕获配置HC-SR04的驱动看起来简单但有几个坑必须提前说清楚。首先是Trig脉冲的宽度。数据手册写的是最小10微秒但实际测试下来建议给到15到20微秒太短了模块可能不响应太长了也没必要。我一般用20微秒稳定可靠。其次是Echo信号的读取方式。新手最容易犯的错误是用 while 循环去等Echo变高再变低然后手动算时间。这种方式在简单测试里能用但一旦系统里有其他任务比如要同时刷新OLED、要读DS18B20就会严重阻塞导致测距频率上不去甚至丢数据。正确的做法是用定时器的输入捕获功能。以STM32F103的TIM2为例配置通道1为输入捕获模式上升沿触发。当Echo引脚出现上升沿时定时器自动记录当前计数值到捕获寄存器同时触发中断。在中断里把捕获值读出来然后把捕获极性改成下降沿。等下降沿到来时再次读取捕获值两次捕获值之差就是高电平的持续时间。这里有个细节定时器的计数频率。TIM2挂在APB1总线上时钟频率是72MHz。如果预分频器设为71那么计数频率就是1MHz每个计数代表1微秒。这样捕获值之差直接就是微秒数省去了换算的麻烦。但要注意16位定时器最大计数值是65535也就是最多能测65.535毫秒。超声波在空气中的速度大约是340米每秒65毫秒对应的时间足够超声波跑22米远超HC-SR04的4米量程所以完全够用。如果担心溢出可以在中断里判断是否发生了更新事件。但实际使用中只要被测物体在量程内基本不会溢出。我个人的习惯是加一个溢出标志位万一溢出了就返回一个错误码避免数据错乱。3.2 DS18B20的单总线时序要点DS18B20的时序是很多人的噩梦因为单总线协议对时间要求非常严格差几个微秒就可能读不出数据。但只要你理解了它的本质其实并不难。单总线协议的核心思想是主机和从机共用一根数据线通过拉低和释放总线来传递信息。所有的操作都围绕“复位脉冲-存在脉冲-读写时隙”这个流程展开。复位脉冲主机拉低总线至少480微秒然后释放。DS18B20检测到上升沿后等待15到60微秒然后拉低总线60到240微秒作为应答。主机在这个窗口内采样如果读到低电平说明器件存在。写时隙要写1主机拉低总线1到15微秒然后释放整个时隙至少60微秒。要写0主机拉低总线60微秒以上然后释放。DS18B20在主机拉低后的15到60微秒窗口内采样总线电平。读时隙主机拉低总线至少1微秒然后释放。DS18B20会在15微秒内把数据放到总线上。主机在拉低后的15微秒左右采样读到的高或低就是数据位。整个读时隙也要至少60微秒。听起来很复杂但用STM32实现起来其实很直接。关键是要有一个微秒级的延时函数。我一般用SysTick或者DWT来实现精度比空循环靠谱得多。另外单总线操作期间要关中断否则一个中断进来打乱了时序数据就废了。还有一个容易忽略的点DS18B20的温度转换需要时间。默认12位分辨率下转换时间最长750毫秒。如果你发完转换命令立刻去读读到的肯定是上一次的结果或者85度的默认值。正确的流程是发跳过ROM命令发温度转换命令然后等750毫秒再发跳过ROM命令发读暂存器命令连续读9个字节前两个字节就是温度值。3.3 温度数据的解析与声速计算DS18B20读出来的温度是16位有符号数低4位是小数部分高12位是整数部分。具体来说如果读到的两个字节是LSB和MSB那么温度值 (MSB 8 | LSB) × 0.0625。比如读到的数据是0x0191二进制是0000 0001 1001 0001。符号位是0正温度。整数部分是0000 0001 1001也就是25。小数部分是0001也就是1乘以0.0625等于0.0625。所以温度是25.0625摄氏度。算出声速之后还要注意一个细节温度补偿不是每次测距都要重新读温度。环境温度变化很慢一秒读一次完全够用。我一般是在主循环里每500毫秒更新一次温度值测距时直接用最近一次的温度来算声速。这样既保证了精度又不会因为频繁读DS18B20而拖慢测距速度。另外如果DS18B20读取失败要有降级策略。我的做法是保留上一次的有效温度值如果连续多次读取失败就退回到默认的25摄氏度同时通过串口输出警告信息。这样即使温度传感器出问题测距功能也不会完全瘫痪。4. 实操过程与核心环节实现4.1 硬件接线与供电注意事项先把接线说清楚因为接错了后面全是坑。STM32F103C8T6核心板通过Micro USB或者ST-Link供电3.3V输出给OLED和DS18B20。HC-SR04需要5V供电这一点特别重要3.3V下HC-SR04工作不稳定测出来的数据跳得厉害。所以你需要一个5V电源可以从核心板的5V引脚取电如果核心板没有5V输出就用外部电源模块。具体接线如下模块引脚STM32引脚说明HC-SR04VCC5V必须5V供电HC-SR04TrigPA0普通GPIO输出HC-SR04EchoPA1TIM2_CH2输入捕获DS18B20VCC3.3V可 parasite 供电DS18B20DQPA2单总线数据DS18B20GNDGND共地OLEDVCC3.3VOLEDSCLPB6I2C1_SCLOLEDSDAPB7I2C1_SDAEcho引脚接PA1是因为TIM2的通道2支持输入捕获而且PA1和PA0相邻接线方便。DS18B20的数据线要接一个4.7kΩ的上拉电阻到3.3V这是单总线协议的要求不加上拉电阻大概率读不出数据。注意HC-SR04的Echo输出是5V电平而STM32的GPIO是3.3V耐压。虽然很多人的板子直接接上去也能用但长期运行有损坏风险。稳妥的做法是在Echo和PA1之间加一个电平转换电路最简单的就是用两个电阻分压比如1kΩ和2kΩ串联从中间取点接到PA1。4.2 定时器输入捕获的代码实现先看TIM2的初始化。这里我用标准库来写因为标准库在F103上资料最多遇到问题好查。void TIM2_Cap_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period arr; TIM_TimeBaseStructure.TIM_Prescaler psc; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_2; 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 0x00; TIM_ICInit(TIM2, TIM_ICInitStructure); TIM_ITConfig(TIM2, TIM_IT_CC2, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE); }这里arr设为65535psc设为71这样计数频率是1MHz每个计数1微秒。捕获极性初始设为上升沿因为Echo信号空闲时是低电平第一个边沿是上升沿。中断服务函数里要做状态机切换void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC2) ! RESET) { if (capture_state 0) { capture_val1 TIM_GetCapture2(TIM2); capture_state 1; TIM_OC2PolarityConfig(TIM2, TIM_ICPolarity_Falling); } else { capture_val2 TIM_GetCapture2(TIM2); capture_state 0; TIM_OC2PolarityConfig(TIM2, TIM_ICPolarity_Rising); if (capture_val2 capture_val1) echo_time capture_val2 - capture_val1; else echo_time 0xFFFF - capture_val1 capture_val2; capture_done 1; } TIM_ClearITPendingBit(TIM2, TIM_IT_CC2); } }这段代码里有个细节如果capture_val2小于capture_val1说明定时器在两次捕获之间溢出了需要加上0xFFFF再算差值。虽然实际使用中很少遇到但加上这个判断更稳妥。4.3 DS18B20的驱动代码与温度读取DS18B20的驱动我封装成几个函数复位、写字节、读字节、读温度。#define DS18B20_DQ_OUT PAout(2) #define DS18B20_DQ_IN PAin(2) void DS18B20_Rst(void) { DS18B20_DQ_OUT 0; delay_us(750); DS18B20_DQ_OUT 1; delay_us(15); } u8 DS18B20_Check(void) { u8 retry 0; DS18B20_DQ_IN; while (DS18B20_DQ_IN retry 200) { retry; delay_us(1); } if (retry 200) return 1; else retry 0; while (!DS18B20_DQ_IN retry 240) { retry; delay_us(1); } if (retry 240) return 1; return 0; }写字节和读字节的时序严格按照前面说的来这里就不贴完整代码了核心就是控制好拉低和释放的时间。读温度的函数流程是复位、跳过ROM、发转换命令、延时750毫秒、复位、跳过ROM、发读暂存器命令、读两个字节、组合成温度值。实操心得DS18B20的延时函数一定要准。我试过用for循环做延时在不同优化等级下延时时间不一样导致时序错乱。后来改用SysTick做微秒延时问题就解决了。如果你用DWT记得先使能DWT的CYCCNT计数器。4.4 主循环逻辑与数据输出主循环的逻辑很清晰每500毫秒读一次DS18B20温度更新声速值每100毫秒触发一次超声波测距测距完成后计算距离刷新OLED显示通过串口输出温度、声速、距离数据触发测距的函数很简单void HC_SR04_Trig(void) { TRIG 1; delay_us(20); TRIG 0; }然后在主循环里等待capture_done标志一旦置位就计算距离if (capture_done) { capture_done 0; float speed 331.4 0.607 * temperature; float distance (echo_time * speed) / 20000.0; OLED_ShowFloat(0, 2, distance, 2); printf(Temp: %.2f C, Speed: %.2f m/s, Dist: %.2f cm\r\n, temperature, speed, distance); }这里echo_time单位是微秒speed单位是米每秒乘出来是微米除以10000得到厘米。我写成除以20000是因为要除以2来回再除以10000微米转厘米合并就是20000。OLED显示我用的是SSD1306驱动I2C接口网上现成的库很多直接拿来用就行。串口配置成115200波特率8位数据位1位停止位无校验。5. 常见问题与排查技巧实录5.1 测距数据跳动大的排查思路这是最常见的问题原因可能有以下几种第一电源干扰。HC-SR04在发射超声波的瞬间会有一个较大的电流冲击如果和STM32共用一路电源可能导致电压跌落影响Echo信号的稳定性。解决办法是在HC-SR04的VCC和GND之间并一个100微法的电解电容再并一个0.1微法的陶瓷电容。第二Echo信号被中断打断。如果系统里有其他高优先级中断比如串口接收中断可能在Echo高电平期间打断捕获过程。解决办法是把TIM2的捕获中断优先级设高一点或者在测距期间暂时关闭其他中断。第三多次测量取中值。单次测量难免有异常值我一般连续测5次去掉最大和最小取剩下3次的平均值。这样处理之后数据非常稳。5.2 DS18B20读不出数据的排查DS18B20读不出来90%的情况是时序问题剩下10%是硬件问题。先检查上拉电阻。数据线必须接4.7kΩ上拉电阻到3.3V不接的话总线空闲时不是高电平复位脉冲都发不出去。再检查延时精度。用示波器看数据线的波形复位脉冲应该是480微秒以上的低电平然后释放。如果低电平时间明显不对说明延时函数有问题。还有一个容易忽略的点DS18B20上电后默认是12位分辨率转换时间750毫秒。如果你发完转换命令只等了100毫秒就去读读到的就是85度的默认值。这个85度是DS18B20的上电默认值看到这个数就说明转换还没完成。5.3 温度补偿后精度仍然不理想的可能原因如果加了温度补偿但测距精度还是不行可以从这几个方面排查问题现象可能原因解决办法近距离准远距离偏大声速公式在远距离下误差累积检查温度值是否准确确认DS18B20没有被其他热源影响数据整体偏小回波时间测量偏短检查定时器计数频率是否正确确认预分频值数据整体偏大回波时间测量偏长检查是否有其他中断干扰捕获过程温度变化时数据跳变温度更新频率太低提高DS18B20读取频率到250毫秒一次同一温度下数据仍跳动超声波本身的物理特性多次测量取中值增加软件滤波5.4 常见问题速查表问题排查方向快速验证方法OLED不亮I2C地址错误、接线松动用I2C扫描程序确认地址串口无输出波特率不匹配、TX/RX接反用示波器看TX引脚有无波形测距一直返回0Echo未接到捕获引脚、Trig未触发用示波器看Echo引脚有无高电平温度值恒为85转换未完成就读取增加延时到750毫秒以上温度值恒为0单总线复位失败检查上拉电阻和接线程序跑飞中断优先级冲突、栈溢出检查NVIC配置和局部变量大小6. 项目扩展与个人经验分享6.1 可以继续深挖的几个方向这套基础框架搭好之后能扩展的方向很多。比如加一个SD卡模块做数据记录把温度、声速、距离按时间戳存下来方便后期分析。或者加一个蓝牙模块把数据传到手机端实时显示。如果做避障小车可以把测距数据通过串口发给电机控制板实现自动避障。还有一个有意思的扩展是加卡尔曼滤波。超声波测距数据本身噪声比较大用卡尔曼滤波做一下平滑效果比简单的滑动平均好很多。我试过在STM32上跑一个简化版的卡尔曼滤波计算量不大F103完全能扛住滤波后的数据曲线明显更平滑。6.2 我在这个项目上踩过的坑第一个坑是电源。最开始我把HC-SR04接到3.3V上测出来的数据跳得厉害换了5V供电之后立刻稳定了。这个坑花了我一个下午才找到原因。第二个坑是DS18B20的延时。我用了一个网上的延时函数在Keil的默认优化等级下没问题后来改成-O2优化延时时间变了DS18B20就读不出来了。后来老老实实用SysTick做延时再也没出过问题。第三个坑是中断优先级。我把TIM2捕获中断和串口接收中断设成了同一个优先级结果串口一收数据测距就丢包。后来把TIM2的抢占优先级设高问题解决。6.3 最后分享一个小技巧如果你觉得每次改代码都要重新编译下载很麻烦可以在串口输出里加一个简单的命令行解析。比如发送“T”就立即读一次温度发送“D”就立即测一次距离发送“S”就连续输出数据流。这样调试的时候不用反复烧录效率高很多。这个项目整体做下来硬件成本不到三十块钱但涉及的知识点覆盖了STM32开发的方方面面。从GPIO到定时器从单总线到I2C从中断到串口基本上把常用外设都练了一遍。如果你能把温度补偿这个细节吃透以后做任何传感器项目都会多一层思考这个传感器的输出会不会受环境因素影响需不需要做补偿这种思维方式比具体的技术点更有价值。