ESP32-C5弹簧振子阻尼实验

2026-10-25 10:02:45 88阅读
class="ck-content" data-v-caa9f93b>

作品简介

本项目基于ESP32‑C5主控板搭配URM09超声波传感器,实现弹簧振子阻尼振动位移采集、自动峰值识别、阻尼系数β最小二乘拟合,网页端同时展示实测振动曲线、理论指数衰减包络曲线与振动峰值标记点,用来探究空气阻尼下弹簧振子衰减规律,验证经典线性阻尼振动模型。

相关原理知识介绍

阻尼振动是振动系统受到阻力作用,机械能不断损耗、振幅随时间逐步衰减的运动。弱阻尼(欠阻尼)条件下,经典线性阻尼振动振幅满足指数衰减公式:

A₀:初始振幅,振动第一个峰值相对于平衡位置的位移

β:阻尼系数(单位 s⁻¹),表征系统能量衰减快慢,β越大振动衰减越快

​t:时间

本实验利用URM09超声波传感器非接触式测量弹簧振子竖直方向距离,ESP32‑C5定时采集位移数据;通过峰值检测算法自动提取每一次振动极大值,再对提取出来的峰值序列做最小二乘拟合求解阻尼系数β;最后利用得到的β反向绘制理论指数衰减包络线,将实测振动波形和理论衰减包络放在同一张图表对比,直观观察真实振动和线性阻尼理论之间的偏差,可进一步探究:不同初始释放振幅下,空气阻尼系数β是否保持不变,检验经典线性阻尼模型适用边界。

耗材清单:

ESP32‑C5开发板

​DFRobot URM09(SEN0307)重力超声波传感器

​弹簧振子实验装置(可自制,模型原理图在之后附上)

​5V供电电源、杜邦线

接线说明:

URM09 VCC → 5V

​URM09 GND → GND

​URM09 AOUT → ESP32‑C5 ADC引脚4

代码介绍

开发环境

Arduino IDE,ESP32核心库,依赖WiFi.h 、 WebServer.h 、 ArduinoJson.h

流程图

弹簧振子阻尼振动测量实验 · ESP32-C5_image_2.webp

第一步:头文件、宏定义与全局缓冲区

引入WiFi、网页服务器、JSON库;定义WiFi参数、ADC引脚、采样时长、峰值识别阈值;开辟数组用来存放原始采样值、滤波后位移、采样时间戳,同时定义结构体存储识别出来的振动峰值信息。

#include "WiFi.h"
#include "WebServer.h"
#include "ArduinoJson.h"

// ===================== 配置区 =====================
const char* WIFI_SSID = "你的WiFi名字";
const char* WIFI_PASS = "你的WiFi密码";
#define URM09_ADC_PIN       4
#define SAMPLE_RATE_MS      20
#define SAMPLE_WINDOW       750   // 15s / 20ms =750点
#define PEAK_MIN_DISTANCE   15
#define PEAK_MIN_AMP        8
// URM09 SEN0307 参数
#define URM09_VOLT_MIN      0.25
#define URM09_VOLT_MAX      2.75
#define URM09_RANGE_MIN_MM  200
#define URM09_RANGE_MAX_MM  5000
// ==================================================

WebServer server(80);
float rawBuf[SAMPLE_WINDOW];
float filterBuf[SAMPLE_WINDOW];
unsigned long timeBuf[SAMPLE_WINDOW];
int sampleIndex = 0;

// 采样状态机
enum SampleState {
  STATE_IDLE,        // 空闲等待触发
  STATE_SAMPLING,    // 正在15秒采样
  STATE_FINISHED     // 采集完成,已经算出峰值与β
};
SampleState sampleState = STATE_IDLE;
unsigned long sampleStartMs = 0;
const unsigned long SAMPLE_DURATION_MS = 15000;

// 峰值结构体
struct PeakPoint {
  unsigned long t;
  float amplitude;
  int index;
};
#define MAX_PEAK_COUNT 50
PeakPoint peakList[MAX_PEAK_COUNT];
int peakCount = 0;
float betaResult = 0.0f;
float A0 = 0.0f;
bool calcReady = false;

第二步:传感器读取与滑动平均滤波函数

readURM09MM :读取ADC原始值,转换成电压,映射得到超声波距离(单位mm);

 slidingFilter :5点滑动平均滤波,滤除超声波高频噪声,平滑位移曲线,降低虚假峰值出现概率。

float readURM09MM(void)
{
  int adcRaw = analogRead(URM09_ADC_PIN);
  float adcVolt = adcRaw * 3.3f / 4095.0f;
  float ratio = constrain((adcVolt - URM09_VOLT_MIN) / (URM09_VOLT_MAX - URM09_VOLT_MIN), 0.0f,1.0f);
  float distMM = URM09_RANGE_MIN_MM + ratio * (URM09_RANGE_MAX_MM - URM09_RANGE_MIN_MM);
  return distMM;
}

float slidingFilter(float newVal)
{
  static float fBuf[5];
  static int fPtr = 0;
  fBuf[fPtr] = newVal;
  fPtr = (fPtr + 1) % 5;
  float sum = 0;
  for(int i=0;i<5;i++) sum += fBuf[i];
  return sum / 5.0f;
}

第三步:峰值检测函数 detectPeaks

遍历全部采样点,比较当前点左右一段距离内数值,判断是否为局部极大值;再减去整条曲线平均值得到相对于平衡位置的振幅,只有振幅大于阈值  PEAK_MIN_AMP  才判定为有效振动峰值,存入峰值数组。

void detectPeaks(void)
{
  peakCount = 0;
  for(int i=PEAK_MIN_DISTANCE; i < SAMPLE_WINDOW - PEAK_MIN_DISTANCE; i++)
  {
    float curr = filterBuf[i];
    bool isPeak = true;
    for(int j = i - PEAK_MIN_DISTANCE; j < i; j++)
    {
      if(filterBuf[j] > curr) {isPeak = false; break;}
    }
    for(int j = i + 1; j < i + PEAK_MIN_DISTANCE; j++)
    {
      if(filterBuf[j] > curr) {isPeak = false; break;}
    }
    if(isPeak)
    {
      float meanVal = 0;
      for(int k=0;k PEAK_MIN_AMP && peakCount < MAX_PEAK_COUNT)
{
peakList[peakCount].t = timeBuf[i];
peakList[peakCount].amplitude = amp;
peakList[peakCount].index = i;
peakCount++;
}
}
}
}

第四步:最小二乘拟合阻尼系数 fitBeta

利用提取出来的有效峰值序列,对 ln(A) 和 t 做线性最小二乘拟合,求出斜率k,阻尼系数 β=-k,同时记录第一个峰值作为初始振幅A0,用于后续生成理论衰减曲线。

void fitBeta(void)
{
  A0 = 0;
  if(peakCount < 4) { betaResult = 0; calcReady = true; return; }
  A0 = peakList[0].amplitude;
  float sumT = 0, sumY = 0, sumTT = 0, sumTY = 0;
  int validN = 0;
  unsigned long t0 = peakList[0].t;
  for(int i=0;i

第五步:网页接口函数(三个API)

1.  startSample :网页点击「开始15秒采样」触发,清空缓冲区,切换状态到STATE_SAMPLING,正式启动采集
2.  getSampleStatus :网页每500ms轮询这个接口,获取当前状态、β、峰值数量,用来更新页面文字状态
3.  pullData :点击拉取数据时调用,把实测曲线、峰值点、根据β算出的理论指数衰减包络全部打包为JSON返回前端Chart渲染

void startSample(void)
{
  if(sampleState == STATE_SAMPLING)
  {
    server.send(400, "text/plain", "ERR: Sampling running");
    return;
  }
  sampleIndex = 0;
  memset(rawBuf,0,sizeof(rawBuf));
  memset(filterBuf,0,sizeof(filterBuf));
  memset(timeBuf,0,sizeof(timeBuf));
  peakCount = 0;
  betaResult = 0;
  A0 = 0;
  calcReady = false;
  sampleStartMs = millis();
  sampleState = STATE_SAMPLING;
  Serial.println("[INFO] Start 15 seconds sampling");
  server.send(200, "text/plain", "OK");
}

void getSampleStatus(void)
{
  StaticJsonDocument<256> doc;
  doc["state"] = sampleState;
  doc["beta"] = betaResult;
  doc["A0"] = A0;
  doc["peakCount"] = peakCount;
  String out;
  serializeJson(doc, out);
  server.send(200, "application/json", out);
}

void pullData(void)
{
  if(sampleState != STATE_FINISHED)
  {
    server.send(400, "text/plain", "ERR: No finished sample data");
    return;
  }
  StaticJsonDocument<12288> doc;
  JsonArray labels = doc.createNestedArray("labels");
  JsonArray filterData = doc.createNestedArray("filterData");
  JsonArray peakData = doc.createNestedArray("peakData");
  JsonArray theoryData = doc.createNestedArray("theoryData");
  doc["beta"] = betaResult;
  doc["A0"] = A0;
  doc["peakCount"] = peakCount;

  float meanVal = 0;
  for(int i=0;i 0) ? peakList[0].index : 0;
float dtSec = SAMPLE_RATE_MS / 1000.0f;

for(int i=0;i= startIdx && A0 > 1e-3 && betaResult > 1e-6)
{
float t = (i - startIdx) * dtSec;
float theoryAmp = A0 * exp(-betaResult * t);
theoryData.add(meanVal + theoryAmp);
}else{
theoryData.add(null);
}
}
for(int p=0;p=0 && idx < SAMPLE_WINDOW)
{
peakData[idx] = filterBuf[idx];
}
}
String out;
serializeJson(doc, out);
server.send(200, "application/json", out);
}

第六步:内置前端网页 getWebPage

将完整HTML页面写在C++字符串里面,ESP32收到浏览器访问根路径  /  直接返回网页;页面包含两个按钮、状态提示、β和峰值数量展示,基于Chart.js绘制三条曲线(蓝色实测、绿色虚线理论包络、红色圆点峰值),同时展示实验原理文字说明。

String getWebPage(void)
{
  String html = R"HTML(





弹簧振子阻尼 ESP32-C5|实测+理论衰减对比





  ▶️ 开始15秒采样
  📥 拉取本次采样数据
  状态:空闲,等待开始采样
  阻尼系数 β:0.000 s⁻¹
  检出峰值数量:0


  

📈 蓝色=实测滤波位移|绿色虚线=理论指数衰减包络|红色圆点=振动峰值

📖 相关知识点

弹簧振子阻尼振动模型:A(t)=A₀·e^(-β t),β即阻尼系数;A₀为初始振幅,绿色虚线是基于本次拟合β得到的理想指数衰减包络线,用来和实测振动峰值对比,观察实际振动与线性阻尼理论之间的偏差。本实验自变量为初始释放振幅,探究空气阻尼条件下β是否随振幅改变,检验经典线性阻尼模型的适用边界。

流程:点击按钮启动 → 连续15s采集URM09超声波距离 → 滑动平均滤波降噪 → 峰值提取识别振动波峰 → 最小二乘拟合指数衰减曲线得到阻尼β → 生成理论衰减包络曲线 → 手动拉取数据在网页可视化展示实测曲线、理论曲线与峰值特殊点。



)HTML";
return html;
}

第七步:WiFi连接、setup初始化、loop主循环

wifiConnect :ESP32上电连接指定WiFi,串口打印分配得到的IP地址;

 setup :初始化串口、ADC引脚,注册网页路由(根路径、/start、/status、/pulldata),启动WebServer;

 loop :持续处理网页请求;当状态处于STATE_SAMPLING,按照20ms间隔采集URM09数据存入缓冲区;15秒时间到后自动切换STATE_FINISHED,执行峰值检测和阻尼系数拟合。

void wifiConnect(void)
{
  Serial.printf("Connecting WiFi: %s\r\n", WIFI_SSID);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\r\nWiFi connected");
  Serial.print("ESP32-C5 IP Address: ");
  Serial.println(WiFi.localIP());
}

void setup(void)
{
  Serial.begin(115200);
  pinMode(URM09_ADC_PIN, INPUT);
  wifiConnect();
  server.on("/", [](){server.send(200, "text/html", getWebPage());});
  server.on("/start", startSample);
  server.on("/status", getSampleStatus);
  server.on("/pulldata", pullData);
  server.begin();
  Serial.println("Web server ready, with theory curve");
}

void loop(void)
{
  server.handleClient();
  static unsigned long lastSampleTick = 0;
  unsigned long now = millis();

  if(sampleState == STATE_SAMPLING)
  {
    if(now - sampleStartMs >= SAMPLE_DURATION_MS)
    {
      sampleState = STATE_FINISHED;
      Serial.println("[INFO] 15s sampling finished, start calculate peaks and beta");
      detectPeaks();
      fitBeta();
      Serial.printf("[INFO] calculation done, peakCount=%d, beta=%.4f, A0=%.2f\r\n", peakCount, betaResult, A0);
      return;
    }
    if(now - lastSampleTick >= SAMPLE_RATE_MS && sampleIndex < SAMPLE_WINDOW)
    {
      lastSampleTick = now;
      float rawVal = readURM09MM();
      float filVal = slidingFilter(rawVal);
      rawBuf[sampleIndex] = rawVal;
      filterBuf[sampleIndex] = filVal;
      timeBuf[sampleIndex] = now;
      sampleIndex++;
    }
  }
}

完整全部代码

/*
ESP32-C5 弹簧振子阻尼|15秒单次采样 + 手动拉取 + 叠加理论衰减曲线
传感器:DFRobot URM09 SEN0307 重力超声波(模拟AOUT输出)
功能:点击开始15s采样 → 采集结束自动算峰值、β、理论衰减曲线 → 点击拉取渲染三条曲线
曲线说明:蓝色=实测滤波位移,绿色虚线=理论指数衰减包络,红色圆点=振动峰值
*/
#include "WiFi.h"
#include "WebServer.h"
#include "ArduinoJson.h"

// ===================== 配置区 =====================
const char* WIFI_SSID = "你的WiFi名字";
const char* WIFI_PASS = "你的WiFi密码";
#define URM09_ADC_PIN       4
#define SAMPLE_RATE_MS      20
#define SAMPLE_WINDOW       750   // 15s / 20ms =750点
#define PEAK_MIN_DISTANCE   15
#define PEAK_MIN_AMP        8
// URM09 SEN0307 参数
#define URM09_VOLT_MIN      0.25
#define URM09_VOLT_MAX      2.75
#define URM09_RANGE_MIN_MM  200
#define URM09_RANGE_MAX_MM  5000
// ==================================================

WebServer server(80);

float rawBuf[SAMPLE_WINDOW];
float filterBuf[SAMPLE_WINDOW];
unsigned long timeBuf[SAMPLE_WINDOW];
int sampleIndex = 0;

// 采样状态机
enum SampleState {
  STATE_IDLE,        // 空闲等待触发
  STATE_SAMPLING,    // 正在15秒采样
  STATE_FINISHED     // 15s采集完成,已算出峰值和β
};
SampleState sampleState = STATE_IDLE;
unsigned long sampleStartMs = 0;
const unsigned long SAMPLE_DURATION_MS = 15000; // 15秒采样时长

struct PeakPoint {
  unsigned long t;
  float amplitude;
  int index;
};
#define MAX_PEAK_COUNT 50
PeakPoint peakList[MAX_PEAK_COUNT];
int peakCount = 0;
float betaResult = 0.0f;
float A0 = 0.0f; // 第一个峰值振幅,用于生成理论衰减曲线
bool calcReady = false;

// URM09读取距离mm
float readURM09MM(void)
{
  int adcRaw = analogRead(URM09_ADC_PIN);
  float adcVolt = adcRaw * 3.3f / 4095.0f;
  float ratio = constrain((adcVolt - URM09_VOLT_MIN) / (URM09_VOLT_MAX - URM09_VOLT_MIN), 0.0f,1.0f);
  float distMM = URM09_RANGE_MIN_MM + ratio * (URM09_RANGE_MAX_MM - URM09_RANGE_MIN_MM);
  return distMM;
}

// 滑动平均滤波,窗口5
float slidingFilter(float newVal)
{
  static float fBuf[5];
  static int fPtr = 0;
  fBuf[fPtr] = newVal;
  fPtr = (fPtr + 1) % 5;
  float sum = 0;
  for(int i=0;i<5;i++) sum += fBuf[i];
  return sum / 5.0f;
}

// 峰值提取,只在采集结束运行一次
void detectPeaks(void)
{
  peakCount = 0;
  for(int i=PEAK_MIN_DISTANCE; i < SAMPLE_WINDOW - PEAK_MIN_DISTANCE; i++)
  {
    float curr = filterBuf[i];
    bool isPeak = true;
    for(int j = i - PEAK_MIN_DISTANCE; j < i; j++)
    {
      if(filterBuf[j] > curr) {isPeak = false; break;}
    }
    for(int j = i + 1; j < i + PEAK_MIN_DISTANCE; j++)
    {
      if(filterBuf[j] > curr) {isPeak = false; break;}
    }
    if(isPeak)
    {
      float meanVal = 0;
      for(int k=0;k PEAK_MIN_AMP && peakCount < MAX_PEAK_COUNT)
{
peakList[peakCount].t = timeBuf[i];
peakList[peakCount].amplitude = amp;
peakList[peakCount].index = i;
peakCount++;
}
}
}
}

// 最小二乘拟合β,采集结束运行一次
void fitBeta(void)
{
A0 = 0;
if(peakCount < 4) { betaResult = 0; calcReady = true; return; }
// 取第一个有效峰值作为初始振幅A0
A0 = peakList[0].amplitude;
float sumT = 0, sumY = 0, sumTT = 0, sumTY = 0;
int validN = 0;
unsigned long t0 = peakList[0].t;
for(int i=0;i doc;
doc["state"] = sampleState;
doc["beta"] = betaResult;
doc["A0"] = A0;
doc["peakCount"] = peakCount;
String out;
serializeJson(doc, out);
server.send(200, "application/json", out);
}

// 拉取完整15s数据接口
void pullData(void)
{
if(sampleState != STATE_FINISHED)
{
server.send(400, "text/plain", "ERR: No finished sample data");
return;
}
StaticJsonDocument<12288> doc;
JsonArray labels = doc.createNestedArray("labels");
JsonArray filterData = doc.createNestedArray("filterData");
JsonArray peakData = doc.createNestedArray("peakData");
JsonArray theoryData = doc.createNestedArray("theoryData");
doc["beta"] = betaResult;
doc["A0"] = A0;
doc["peakCount"] = peakCount;

// 先求振动平衡位置均值
float meanVal = 0;
for(int i=0;i 0) ? peakList[0].index : 0;
float dtSec = SAMPLE_RATE_MS / 1000.0f;

for(int i=0;i= startIdx && A0 > 1e-3 && betaResult > 1e-6)
{
float t = (i - startIdx) * dtSec;
float theoryAmp = A0 * exp(-betaResult * t);
theoryData.add(meanVal + theoryAmp);
}else{
theoryData.add(null);
}
}
for(int p=0;p=0 && idx < SAMPLE_WINDOW)
{
peakData[idx] = filterBuf[idx];
}
}
String out;
serializeJson(doc, out);
server.send(200, "application/json", out);
}

String getWebPage(void)
{
String html = R"HTML(

弹簧振子阻尼 ESP32-C5|实测+理论衰减对比 ▶️ 开始15秒采样 📥 拉取本次采样数据 状态:空闲,等待开始采样 阻尼系数 β:0.000 s⁻¹ 检出峰值数量:0

📈 蓝色=实测滤波位移|绿色虚线=理论指数衰减包络|红色圆点=振动峰值

📖 相关知识点

弹簧振子阻尼振动模型:A(t)=A₀·e^(-β t),β即阻尼系数;A₀为初始振幅,绿色虚线是基于本次拟合β得到的理想指数衰减包络线,用来和实测振动峰值对比,观察实际振动与线性阻尼理论之间的偏差。本实验自变量为初始释放振幅,探究空气阻尼条件下β是否随振幅改变,检验经典线性阻尼模型的适用边界。

流程:点击按钮启动 → 连续15s采集URM09超声波距离 → 滑动平均滤波降噪 → 峰值提取识别振动波峰 → 最小二乘拟合指数衰减曲线得到阻尼β → 生成理论衰减包络曲线 → 手动拉取数据在网页可视化展示实测曲线、理论曲线与峰值特殊点。



)HTML";
return html;
}

void wifiConnect(void)
{
Serial.printf("Connecting WiFi: %s\r\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED)
{
del


作者:Lumen微光
链接:https://www.aiddithome.com/p/dadf425a78ff6.html
本作遵循 [CC BY 4.0 署名] 许可,版权归作者所有。若存在违规行为,您可以点击 “举报”。


登录 后发表评论
3条评论
阿伟不emo
1楼 · 2026-10-25 10:02:46

项目很实用

拆机党
2楼 · 2026-10-25 10:02:46

代码清晰

板砖搬不动
3楼 · 2026-10-25 10:02:46

想复刻