A · HS-SR04P
—cm
—m/s
等待回波
本组暂无数据
比较 HS-SR04P 与 VL53L0X 激光测距。先观察静止时的波动,再比较移动时的响应。
设置显示范围。
—cm
—m/s
等待回波
本组暂无数据
—cm
—m/s
等待回波
本组暂无数据
0 条记录
每次采集到设定时间自动停止,下一组保留前面的数据。起止时刻控制当前图像,区间以外的数据不参与坐标缩放;冻结仅暂停绘图。缺测和剔除点不会连成有效曲线。
超声波每100ms触发一次;激光约100ms启动一次测量,时间预算50ms。激光用启动到就绪轮询的中点作为时间近似,超声波用触发加半回波脉宽,两路并非严格同时;移动时不能直接将A−B当作距离误差。
运动时用最近3个有效点拟合速度,100ms采样下跨度约0.2秒。超声波增加一帧确认:前后两点相差小于2cm,而中点偏离它们的插值超过4cm,才剔除该孤立点;单向快速运动不因位移大而剔除。超声波首次速度通常约0.4秒后出现,激光约0.2秒;速度对应拟合窗口中点,不是瞬时值。连续1.2秒内距离极差小于1.5cm时改用长窗口抑制静止噪声。
原始距离始终保留在CSV,剔除点显示缺口,处理后距离带有自己的时间戳。快速往返也可能符合孤立跳点形状,连续错误回波也可能漏过:此规则是可复查的启发式判断。残差超过10mm提示“变速或噪声”,仍给短窗口估计,不再将真实加速直接判成无速度。小于 max(0.003, min(0.01, 2×斜率标准误差)) m/s 的拟合速度显示为0;这仍可能掩盖很慢的真实运动。
速度由距离变化计算:v≈Δd/Δt。2m/s的小车每100ms前进20cm;旧1.2秒窗口需要小车在视野中运动2.4米,常常还没形成速度就出界。短窗口更快,但速度噪声约为 √2×距离噪声/Δt,因此提高采样频率本身不保证稳定。
激光采用库的长距离配置,保留50ms时间预算;它降低回光接收门限,也可能增加噪声,不能保证远距离准确。VL53L0X视场约25°,并非只测一个无限细的点;范围受反射率、目标大小和环境光影响。芯片无有效距离时不推算速度。长距离配置恢复了较远处有效输出;本装置后续观察到超声波超过约80cm误差增加、激光超过约100cm波动较大,不能用标称2m取代实测。
课堂测0.2~2m/s小车:探头沿轨道朝向小车上固定的平整反射板,尽量避免桌面、车轮和斜面进入探测范围。如果目标是可靠测量某段平均速度,两个光电门测 v=L/Δt 更直接;如果要连续速度曲线,需要足够的有效测距轨道和独立测速参照。
规格来源:ST VL53L0X 数据手册。名义工作范围不代表这套装置的已验证范围。
超声波使用开发板锁定的温湿度声速,OLED和网页一致;OLED上下两区分别显示超声波与激光距离、速度,温湿度只在后台参与补偿;激光直接使用芯片距离,不乘声速系数。激光信号不足或状态异常时显示原因,不把无效读数当距离。页面工作范围:超声波2cm–4m,激光2cm–2m;具体可靠范围还受目标、光照和安装影响。
连接:超声波TRIG25/ECHO26,DHT11 DATA27;OLED与VL53L0X共用SDA21/SCL22、3.3V与GND,地址分别0x3C、0x29。原第二路HC-SR04已移除。
先断开USB和电源再接线。GPIO数字是开发板引脚旁的编号,不是排针从左到右的序号;接好后用USB数据线供电。
| 模块 | 模块引脚 → ESP32 |
|---|---|
| HS-SR04P超声波 | VCC → 3V3;GND → GND;TRIG → GPIO25;ECHO → GPIO26 |
| DHT11温湿度模块 | VCC → 3V3;GND → GND;DATA(或S)→ GPIO27 |
| SSD1306 OLED(128×64,I²C) | VCC → 3V3;GND → GND;SDA → GPIO21;SCL → GPIO22 |
| GY-VL53L0X激光模块 | VIN → 3V3;GND → GND;SDA → GPIO21;SCL → GPIO22;XSHUT和GPIO1不接 |
所有GND接在一起。这里使用可在3.3V供电的HS-SR04P;普通5V HC-SR04的ECHO不可直接接ESP32。换模块时先核对型号、电源和信号电压。
OLED和激光共用21/22两根通信线,地址分别为0x3C和0x29;两模块的SDA都接21,SCL都接22。第一版没有蓝牙和蜂鸣器功能,使用USB连接对比页面。
Arduino C++,版本 ultrasonic-laser-oled-v10。main.cpp · motion_display.h · laser_sensor.h · platformio.ini
#include <Arduino.h>
#include <DHT.h>
#include <esp_timer.h>
#include "motion_display.h"
#include "laser_sensor.h"
// USB 保留原始回波 CSV;OLED 计算补偿距离、一帧跳点确认与自适应窗口速度。
static_assert(ULTRASONIC_TRIG_PIN != ULTRASONIC_ECHO_PIN,
"TRIG and ECHO must use different pins");
static_assert(DHT_DATA_PIN != ULTRASONIC_TRIG_PIN &&
DHT_DATA_PIN != ULTRASONIC_ECHO_PIN,
"DHT DATA must use a separate pin");
uint32_t kUltrasonicPeriodMs = 100; // 单路100ms;运动3点、静止长窗口;B路已移除
const uint8_t trigPins[2]={25,32},echoPins[2]={26,33};
volatile uint8_t activeChannel=0;
uint32_t channelSeq[2]={0,0};
constexpr uint32_t kEchoTimeoutUs = 40000;
constexpr uint32_t kDhtPeriodMs = 2500;
constexpr uint32_t kTriggerUs = 50;
DHT dht(DHT_DATA_PIN, DHT11);
portMUX_TYPE echoMux = portMUX_INITIALIZER_UNLOCKED;
volatile uint8_t echoState = 0; // 0 停止,1 等上升沿,2 等下降沿,3 完整
volatile int64_t echoRiseUs = 0;
volatile int64_t echoFallUs = 0;
uint32_t ultrasonicSeq = 0;
uint32_t dhtSeq = 0;
void IRAM_ATTR onEchoChange() {
const int level = digitalRead(echoPins[activeChannel]);
const int64_t now = esp_timer_get_time();
portENTER_CRITICAL_ISR(&echoMux);
if (echoState == 1 && level == HIGH) {
echoRiseUs = now;
echoState = 2;
} else if (echoState == 2 && level == LOW) {
echoFallUs = now;
echoState = 3;
}
portEXIT_CRITICAL_ISR(&echoMux);
}
void sampleUltrasonic(uint8_t channel) {
activeChannel=channel;
const char kind=channel==0?'U':'V';
const int64_t triggerUs = esp_timer_get_time();
ultrasonicSeq=++channelSeq[channel];
channels[channel].distance=NAN;channels[channel].velocity=NAN;channels[channel].status="NO ECHO";
if (digitalRead(echoPins[activeChannel]) == HIGH) {
Serial.printf("%c,%lu,%lld,,,,,,,echo_high_before_trigger\n",
kind, static_cast<unsigned long>(ultrasonicSeq), triggerUs);
channels[channel].reset();
return;
}
portENTER_CRITICAL(&echoMux);
echoRiseUs = 0;
echoFallUs = 0;
echoState = 1;
portEXIT_CRITICAL(&echoMux);
digitalWrite(trigPins[channel], HIGH);
delayMicroseconds(kTriggerUs);
digitalWrite(trigPins[channel], LOW);
// DHT 只能在等待结束后读取,避免屏蔽中断时漏掉 ECHO。
uint8_t state;
do {
portENTER_CRITICAL(&echoMux);
state = echoState;
portEXIT_CRITICAL(&echoMux);
if (state == 3) break;
delayMicroseconds(20);
} while (esp_timer_get_time() - triggerUs < kEchoTimeoutUs);
portENTER_CRITICAL(&echoMux);
state = echoState;
const int64_t rise = echoRiseUs;
const int64_t fall = echoFallUs;
echoState = 0;
portEXIT_CRITICAL(&echoMux);
if (state == 3 && fall > rise) {
const int64_t width = fall - rise;
channels[channel].update(triggerUs, width);
// 343 m/s 固定声速基线,后续可用原始脉宽重算温度补偿。
const double distanceMm = static_cast<double>(width) * 0.1715;
Serial.printf("%c,%lu,%lld,%lld,%lld,%lld,%.3f,,,ok\n",
kind, static_cast<unsigned long>(ultrasonicSeq), triggerUs,
rise, fall, width, distanceMm);
} else if (state == 2) {
channels[channel].reset();
Serial.printf("%c,%lu,%lld,%lld,,,,,,echo_fall_timeout\n",
kind, static_cast<unsigned long>(ultrasonicSeq), triggerUs, rise);
} else {
channels[channel].reset();
// 缺测留空,不能伪装成零距离。
Serial.printf("%c,%lu,%lld,,,,,,,echo_rise_timeout\n",
kind, static_cast<unsigned long>(ultrasonicSeq), triggerUs);
}
}
void sampleDht() {
const int64_t startUs = esp_timer_get_time();
const float humidity = dht.readHumidity();
const float temperature = dht.readTemperature(); // 使用同一次校验通过的数据
++dhtSeq;
if (isnan(humidity) || isnan(temperature)) {
Serial.printf("D,%lu,%lld,,,,,,,read_failed\n",
static_cast<unsigned long>(dhtSeq), startUs);
} else {
updateEnvironment(temperature, humidity);
if(environmentLocked) Serial.printf("# environment_locked temp=%.1f rh=%.1f c=%.6f\n",lockedTemp,lockedRh,lockedSpeed);
Serial.printf("D,%lu,%lld,,,,,%.1f,%.1f,ok\n",
static_cast<unsigned long>(dhtSeq), startUs,
temperature, humidity);
}
}
void setup() {
for(int i=0;i<1;i++){
digitalWrite(trigPins[i],LOW);pinMode(trigPins[i],OUTPUT);pinMode(echoPins[i],INPUT);
attachInterrupt(digitalPinToInterrupt(echoPins[i]),onEchoChange,CHANGE);
}
dht.begin();
Serial.begin(115200);
delay(300);
setupDisplay();
if(oledReady)testChineseFont();
setupLaser();
Serial.printf("# firmware=ultrasonic-laser-oled-v10; TRIG=%d ECHO=%d DHT=%d; "
"ultrasonic_ms=%lu; fit=adaptive_3point_static1200; trigger_us=%lu; dht_ms=%lu; c_m_s=343\n",
ULTRASONIC_TRIG_PIN, ULTRASONIC_ECHO_PIN, DHT_DATA_PIN,
static_cast<unsigned long>(kUltrasonicPeriodMs),
static_cast<unsigned long>(kTriggerUs),
static_cast<unsigned long>(kDhtPeriodMs));
Serial.println("# channel U=HS-SR04P pins=25/26; L=VL53L0X address=0x29; B=removed");
Serial.println("kind,seq,t_us,echo_rise_us,echo_fall_us,pulse_us,distance_mm_343,temp_c,rh_pct,status");
}
void loop() {
while(Serial.available()){
const char cmd=Serial.read();
if(cmd=='1'||cmd=='2'){
kUltrasonicPeriodMs=cmd=='1'?100:200;
for(auto &c:channels)c.reset();
Serial.printf("# sample_period_ms=%lu\n",(unsigned long)kUltrasonicPeriodMs);
}
}
static uint32_t lastUltrasonic = 0;
static uint32_t lastDht = 0;
if (static_cast<uint32_t>(millis() - lastUltrasonic) >= kUltrasonicPeriodMs) {
lastUltrasonic = millis();
sampleUltrasonic(0);
}
if (static_cast<uint32_t>(millis() - lastDht) >= kDhtPeriodMs) {
lastDht = millis();
sampleDht();
}
pollLaser();
static uint32_t lastDisplay = 0;
if (millis() - lastDisplay >= 200) { lastDisplay = millis(); drawDisplay(); }
delay(1);
}
#pragma once
#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include <math.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C screen(U8G2_R0,U8X8_PIN_NONE,22,21);
bool oledReady=false,environmentLocked=false;
double lockedSpeed=343;float lockedTemp=NAN,lockedRh=NAN;
float temperatureNow=NAN,humidityNow=NAN,candidateTemp=NAN,candidateRh=NAN;
uint32_t environmentTime=0;
double soundSpeed(double t,double rh) {
const double T=t+273.15,p=101325,xc=.0004;
const double psv=exp(1.2811805e-5*T*T-.019509874*T+34.04926034-6353.6311/T);
const double f=1.00062+3.14e-8*p+5.6e-7*t*t,xw=rh/100*f*psv/p;
return 331.5024+.603055*t-.000528*t*t+(51.471935+.1495874*t-.000782*t*t)*xw
+(-1.82e-7+3.73e-8*t-2.93e-10*t*t)*p+(-85.20931-.228525*t+5.91e-5*t*t)*xc
-2.835149*xw*xw-2.15e-13*p*p+29.179762*xc*xc+.000486*xw*p*xc;
}
// 一帧确认孤立跳点;快运动用最近3点,连续稳定才使用长窗口。
struct MotionChannel {
struct Point { double t,d; } points[20], raw[3];
int count=0,rawCount=0;double lastTrigger=0;
double distance=NAN,velocity=NAN;
const char* status="WAIT";
void reset(){count=rawCount=0;lastTrigger=0;distance=velocity=NAN;status="NO ECHO";}
void update(int64_t trigger,int64_t width){
if(!environmentLocked){reset();status="WAIT T";return;}
updateMeasurement(double(trigger)+width/2.,width*lockedSpeed/2000000.,true);
}
void updateMeasurement(double time,double measured,bool rejectSpikes){
if(lastTrigger&&(time<=lastTrigger||time-lastTrigger>300000))reset();
lastTrigger=time;distance=velocity=NAN;status="WARM";
if(measured<.02||measured>(rejectSpikes?4.:2.)){reset();status="RANGE";return;}
Point p={double(time),measured};
if(rejectSpikes){
if(rawCount==3){raw[0]=raw[1];raw[1]=raw[2];rawCount=2;}
raw[rawCount++]=p;if(rawCount<3)return;
p=raw[1];
const double predicted=raw[0].d+(raw[2].d-raw[0].d)*(p.t-raw[0].t)/(raw[2].t-raw[0].t);
if(fabs(raw[2].d-raw[0].d)<.02 && fabs(p.d-predicted)>.04){
status="JUMP";Serial.printf("# outlier t_us=%.0f raw_m=%.6f\n",p.t,p.d);return;
}
}
distance=p.d;
while(count && p.t-points[0].t>1250000.){for(int i=1;i<count;i++)points[i-1]=points[i];count--;}
if(count==20){for(int i=1;i<count;i++)points[i-1]=points[i];count--;}
points[count++]=p;if(count<3)return;
double low=points[0].d,high=low;
for(int i=1;i<count;i++){low=min(low,points[i].d);high=max(high,points[i].d);}
const bool stable=count>=5&&p.t-points[0].t>=1100000.&&high-low<.015;
const int first=stable?0:count-3,n=count-first;
if(p.t-points[first].t>450000.&&!stable)return;
double ts[20],tm=0,ym=0;
for(int i=0;i<n;i++){ts[i]=(points[first+i].t-points[first].t)/1e6;tm+=ts[i]/n;ym+=points[first+i].d/n;}
double num=0,denom=0;
for(int i=0;i<n;i++){num+=(ts[i]-tm)*(points[first+i].d-ym);denom+=(ts[i]-tm)*(ts[i]-tm);}
if(denom<=0){reset();status="TIME";return;}
const double v=num/denom;double sumSq=0;
for(int i=0;i<n;i++){double e=points[first+i].d-ym-v*(ts[i]-tm);sumSq+=e*e;}
const double threshold=max(.003,min(.01,2*sqrt(sumSq/(n-2)/denom)));
velocity=fabs(v)<=threshold?0:v;
// 曲率/加速度也会产生残差:提示不稳定,仍显示短窗口平均速度。
status=sqrt(sumSq/n)>.01?"JUMP":velocity==0?"STILL":v>0?"AWAY":"TOWARD";
}
} channels[2];
void updateEnvironment(float t,float rh){
temperatureNow=t;humidityNow=rh;environmentTime=millis();
if(!environmentLocked&&t>=0&&t<=30&&rh>=0&&rh<=100){
if(isfinite(candidateTemp)&&fabs(t-candidateTemp)<=2&&fabs(rh-candidateRh)<=10){
lockedSpeed=soundSpeed(t,rh);lockedTemp=t;lockedRh=rh;environmentLocked=true;
channels[0].reset();
Serial.printf("# environment_locked temp=%.1f rh=%.1f c=%.6f\n",t,rh,lockedSpeed);
}
candidateTemp=t;candidateRh=rh;
}else if(!environmentLocked){candidateTemp=candidateRh=NAN;}
}
void setupDisplay(){
Wire.begin(21,22);Wire.setTimeOut(20);Wire.beginTransmission(0x3C);oledReady=Wire.endTransmission()==0;
Serial.printf("# oled detected=%d addr=0x3C\n",oledReady);
if(oledReady){screen.setI2CAddress(0x3C*2);screen.setBusClock(400000);screen.begin();}
}
const char* chineseStatus(const char* status) {
if(!strcmp(status,"AWAY"))return "远离";
if(!strcmp(status,"TOWARD"))return "靠近";
if(!strcmp(status,"STILL"))return "近静止";
if(!strcmp(status,"JUMP"))return "不稳定";
if(!strcmp(status,"RANGE"))return "超范围";
if(!strcmp(status,"NO ECHO"))return "无回波";
if(!strcmp(status,"WAIT T"))return "等温度";
return "采集中";
}
void testChineseFont(){
screen.setFont(u8g2_font_wqy12_t_gb2312);
const uint16_t chars[]={0x58f0,0x6fc0,0x5149,0x5f85,0x6e29,0x5ea6,0x8865,0x507f,0x8fdc,0x79bb,0x9760,0x8fd1,0x9759,0x6b62,0x4e0d,0x7a33,0x5b9a,0x8d85,0x8303,0x56f4,0x65e0,0x56de,0x6ce2,0x7b49,0x91c7,0x96c6,0x4e2d};
int missing=0;for(auto ch:chars)if(!u8g2_IsGlyph(screen.getU8g2(),ch))missing++;
Serial.printf("# chinese_font missing=%d label_width=%u available_width=62\n",missing,screen.getUTF8Width("温度补偿"));
}
void drawDisplay() {
if(!oledReady)return;
screen.clearBuffer();char d[20],v[20],text[44];
for(int i=0;i<2;i++){
auto &c=channels[i];const int top=i*33;
const bool fresh=c.lastTrigger && esp_timer_get_time()-c.lastTrigger<500000;
screen.setFont(u8g2_font_wqy12_t_gb2312);
screen.drawUTF8(0,top+11,i==0?"超声波":"激光");
screen.drawUTF8(80,top+11,fresh?chineseStatus(c.status):"等待");
if(fresh&&isfinite(c.distance))snprintf(d,sizeof(d),"%.1fcm",c.distance*100);else snprintf(d,sizeof(d),"--cm");
if(fresh&&isfinite(c.velocity))snprintf(v,sizeof(v),fabs(c.velocity)<.0005?"%.3fm/s":"%+.3fm/s",fabs(c.velocity)<.0005?0.:c.velocity);else snprintf(v,sizeof(v),"--m/s");
snprintf(text,sizeof(text),"%s %s",d,v);screen.setFont(u8g2_font_6x12_tf);screen.drawStr(0,top+27,text);
}
screen.drawHLine(0,31,128);screen.sendBuffer();
Serial.printf("# oled A_d=%.6f A_v=%.6f A_status=%s B_d=%.6f B_v=%.6f B_status=%s\n",channels[0].distance,channels[0].velocity,channels[0].status,channels[1].distance,channels[1].velocity,channels[1].status);
}