超声波测距在近几届蓝桥杯中几乎每年都考。这篇文章基于项目中的两版超声波驱动代码,把PCA定时器的使用、40KHz发射时序、距离计算和温度补偿全部讲透。
测距原理
超声波模块(HC-SR04)的工作方式很简单:
-
发射端发出一串40KHz的超声波脉冲
-
超声波碰到障碍物反射回来
-
接收端检测到回波
-
根据往返时间计算距离
距离 = 声速 × 飞行时间 / 2
= 340 m/s × T / 2
硬件连接:
- TX(发射):P1.0
- RX(接收):P1.1
PCA定时器
STC15的PCA(可编程计数器阵列)本质上是一个16位定时器,非常适合超声波的微秒级计时。
cs
CMOD = 0x00; // PCA时钟源:Fosc/12(12T模式)
CH = CL = 0; // 清零计数器
CR = 1; // 启动PCA计时
CF = 0; // PCA溢出标志
在12MHz晶振、12T模式下,每个PCA计数值 = 1us。16位最大计数65535,对应最大计时约65ms,可测距离约11m------远超超声波的实际量程(2cm~4m)。
超声波驱动完整代码
项目 Driver/U_Wave.c 中的实现:
cs
#include <STC15F2K60S2.H>
#include <intrins.h>
sbit RX = P1^1; // 接收
sbit TX = P1^0; // 发射
/* 12us延时函数 */
void Delay12us(void) { // @12.000MHz
unsigned char i;
_nop_();
i = 3;
while(--i);
}
/* 发射8个40KHz超声波脉冲 */
void U_Wave_Init() {
unsigned char i;
EA = 0; // 关中断!保护发射时序
for(i = 0; i < 8; i++) {
TX = 1;
Delay12us(); // 高电平12us
TX = 0;
Delay12us(); // 低电平12us
}
EA = 1;
}
/* 完整测距函数 */
unsigned char U_Wave_Data() {
unsigned long int U_Wave_Time;
CMOD = 0x00;
CH = CL = 0; // 清零PCA
U_Wave_Init(); // 发射超声波
CR = 1; // 启动PCA
while((RX == 1) && (CF == 0)); // 等待回波或溢出
CR = 0; // 停止PCA
if(CF == 0) { // 未溢出 = 正常测距
U_Wave_Time = (CH << 8) | CL;
return U_Wave_Time * 0.017; // 距离(cm) = 时间(us) × 0.017
} else { // 溢出 = 超出量程
CF = 0;
return 0;
}
}
几个关键细节
1) 为什么发射时关中断?
40KHz超声波的周期是25us,半周期12.5us。代码用12us延时,已经非常接近。如果发射过程中Timer1中断触发,中断服务函数里会执行数码管扫描(大约需要几十微秒),这就破坏了发射脉冲的宽度,导致发出的不是40KHz的信号,测距会出错。
2) 为什么是8个脉冲?
8个脉冲只是经验值。理论上越多越好(接收端更容易识别),但太多会增加发射时间,而且需要精确的时序控制。8个脉冲(约192us)是硬件手册推荐的配置。
3) 距离计算公式推导
声速 V = 340 m/s = 0.034 cm/us
距离 d = V × t / 2 = 0.034 × t / 2 = 0.017 × t
其中 t 是PCA计数值(单位:us,12T模式下)
所以 d = PCA值 × 0.017 cm
4) 返回值类型问题
注意 U_Wave_Data() 返回 unsigned char,最大值255。这意味着距离超过255cm的测量结果会溢出截断。第16届省赛改进了这一点:
cs
// 第16届省赛版:超量程返回255(区别于正常值0)
unsigned char Sonic_Read(void) {
unsigned int time;
CMOD = 0x00;
CH = CL = 0x00;
CR = 1;
Sonic_Init();
while((RX == 1) && (CF == 0));
CR = 0;
if(CF == 0) {
time = CH << 8 | CL;
return (time * 0.017);
} else {
CF = 0;
return 255; // 返回255表示超量程
}
}
虽然返回类型仍然是 unsigned char,但用255作为超量程标志,与正常的0距离区分开了。实际使用时,如果收到255就知道这次测距失败了。
温度补偿
声速不是恒定的,它随温度变化:
V(T) = 331.4 + 0.6 × T(°C)
0°C: 331.4 m/s
20°C: 343.4 m/s
40°C: 355.4 m/s
温度从0°C到40°C,声速差了约7%,对测距精度的影响非常大。
第16届省赛的补偿公式:
cs
// 用DS18B20读到的温度修正测距结果
distance = (Sonic_Read() / 340.0) * (330 + 0.6 * temperature);
推导过程:
超声波模块内部用固定声速340m/s计算:
raw_distance = time × 340 / 2 = time × 0.017
实际距离:
real_distance = time × V(T) / 2 = time × (331.4 + 0.6T) / 2
所以:
real_distance = raw_distance × V(T) / 340
= (raw_distance / 340) × (331.4 + 0.6T)
≈ (raw_distance / 340) × (330 + 0.6T) ← 简化版
DAC同步输出
第16届省赛还把测距结果映射到DAC输出(1~5V):
cs
void DAC_Transition(unsigned int distance) {
if(distance <= 20)
DA_Output(1); // 20cm以下 → 1V
else if(distance >= 100)
DA_Output(5); // 100cm以上 → 5V
else
DA_Output(((distance - 20) / 20.0) + 1); // 20~100cm线性映射
}
20cm→1V, 40cm→2V, 60cm→3V, 80cm→4V, 100cm→5V,线性关系。
避障检测应用
国赛代码中的避障逻辑:
cs
void Get_Distance() {
unsigned char temp;
temp = U_Wave_Data();
Distance_Now = temp;
if(Distance_Now < 30) { // 距离小于30cm
Barrier_Clean_Flag = 0; // 有障碍物
if(Running_Mode == 2)
Running_Mode = 1; // 运行 → 暂停
} else {
Barrier_Clean_Flag = 1; // 障碍物已清除
}
}
第16届省赛的运动状态检测更有意思------根据两次测距的差值判断前方物体是静止、徘徊还是跑动:
cs
// 根据距离变化判断运动状态
if(D_Distance < 5)
Temp_Motion_State = 0; // 静止
else if(D_Distance < 10)
Temp_Motion_State = 1; // 徘徊
else
Temp_Motion_State = 2; // 跑动
// 状态变化锁定(防止频繁切换)
// 检测到状态变化后锁定3秒,3秒内不允许再次变化
常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
| 始终返回0 | TX和RX接反了 | 交换P1.0和P1.1 |
| 读数不稳定 | 声波多径反射 | 多次测量取均值/中值 |
| 测量偏大 | 声速不准 | 加温度补偿 |
| 近距离(<2cm)测不到 | 发射和接收重叠 | 加入最小距离判断 |
while((RX==1)&&(CF==0)) 死循环 |
接收引脚一直为高 | 检查模块连接 |