ESP32-C5管道流体脉动采集

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

管道内部流体流动会产生压力脉动,脉动信号包含流量变化、湍流、振动、泵体异常等特征。传统管道流体监测设备体积大、成本高,本项目使用ESP32‑C5搭配SEN0209振动传感器,以800Hz固定采样频率采集管道壁振动信号,通过滑动平均滤波与快速傅里叶变换(FFT)把时域振动波形转换成频谱,再通过内置Web网页实时展示波形,支持最多8组历史工况本地保存对比,实现低成本无源式管道流体脉动在线采集与特征分析。

-“无源”含义:本装置只贴附在管道外壁,不需要开孔、不需要侵入管道内部,依靠拾取管道壁传导的流体振动信号完成监测,属于非侵入式采集。

创作环境

硬件

ESP32‑C5开发板、SEN0209压电振动传感器、USB 5V供电

接线:SEN0209 → VCC接3.3V,GND接GND,AO模拟输出接入ESP32‑C5 GPIO4,传感器紧贴管道外壁,实现非侵入式采集管壁传导的流体振动信号。

无源管道流体脉动采集装置 · ESP32-C5 + SEN0209_image_1.webp

选择本产品的原因:

柔性PVDF压电薄膜,贴合曲面管道更友好;带数字/模拟双输出、灵敏度调节电位器,内置电荷放大模块,频率响应范围

软件

Arduino IDE 2.x,esp32板包≥3.0.0,依赖库:arduinoFFT;通信方式:ESP32内置WebServer,浏览器Chart.js绘图,LocalStorage本地存储最多8组工况。

核心参数:采样频率800Hz,单帧256点,频率分辨率3.125Hz,5点滑动平均,汉明窗FFT。

主要原理介绍

管道内部流体产生的压力脉动会传递到管道外壁,SEN0209压电传感器将管壁机械振动转换成模拟电压信号;ESP32‑C5使用MCPWM硬件定时器产生稳定800Hz定时中断,采集ADC信号,经过滑动平均降噪,再用FFT将时域波形转为频谱,提取流体脉动的主要特征频率。
网页端可以实时看到时域波形与频谱,支持多工况频谱叠加对比,同时增加硬件启停控制,网页点暂停时,ESP32会关闭MCPWM定时器,停止采样,降低主控负载。

代码编写

第1步:引入头文件、定义基础配置

首先导入WiFi、WebServer、FFT库以及MCPWM驱动,定义硬件引脚、采样频率、FFT点数,预留全局数组用来存放原始采样、滤波后、FFT虚部实部数据。

#include "WiFi.h"
#include "WebServer.h"
#include "arduinoFFT.h"
#include "driver/mcpwm.h"
#include "freertos/portmacro.h"

// ========= 用户配置区 =========
const char* WIFI_SSID = "你的WiFi名称";
const char* WIFI_PASS = "你的WiFi密码";
#define SEN0209_PIN     4
#define SAMPLING_FREQ   800
#define FFT_N           256
#define FILTER_SIZE     5
// ==============================

WebServer server(80);
arduinoFFT FFT = arduinoFFT();

double vReal[FFT_N];
double vImag[FFT_N];
double rawBuf[FFT_N];
double filterBuf[FFT_N];
float ADC_MID = 2048.0f;
uint32_t frameId = 0;

volatile bool adcSampleRequest = false;
volatile bool frameReady = false;
volatile bool processingReady = false;
int sampleIndex = 0;
bool samplingEnabled = false;

要点: volatile  标记中断中会修改的变量,防止编译器优化掉; samplingEnabled  就是后面用来控制硬件启停的总开关。

第2步:MCPWM定时中断 + 采样启停函数

MCPWM产生稳定800Hz定时,中断里面只置标记,不在中断里面读取ADC,避免中断里面执行耗时操作;新增startSampling / stopSampling,用来启动、关闭MCPWM硬件定时器,同时复位采样状态。

void IRAM_ATTR mcpwmIsrHandler(void *arg)
{
  adcSampleRequest = true;
  portYIELD_FROM_ISR(pdFALSE);
}

void initMcpwmTimer(uint32_t sampleFreq)
{
  mcpwm_config_t mcpwmCfg = {
    .frequency = sampleFreq,
    .duty_cycle = 50.0f,
    .counter_mode = MCPWM_UP_COUNTER,
    .carrier_mode = MCPWM_CARRIER_DISABLE
  };
  mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_0, &mcpwmCfg);
  mcpwm_isr_register(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, mcpwmIsrHandler, NULL);
  Serial.printf("[MCPWM] Timer config done, sampling freq: %d Hz\n", sampleFreq);
}

void startSampling()
{
  if(!samplingEnabled)
  {
    mcpwm_start(MCPWM_UNIT_0, MCPWM_TIMER_0);
    samplingEnabled = true;
    Serial.println("[Sampling] START");
  }
}

void stopSampling()
{
  if(samplingEnabled)
  {
    mcpwm_stop(MCPWM_UNIT_0, MCPWM_TIMER_0);
    samplingEnabled = false;
    adcSampleRequest = false;
    frameReady = false;
    processingReady = false;
    sampleIndex = 0;
    Serial.println("[Sampling] STOP");
  }
}

第3步:信号预处理函数(滑动平均滤波 + ADC零点标定)

SEN0209静态时输出一个固定基线电压,ADC零点标定用来自动测出基线,后续采样减去基线,把静态值归零;5点滑动平均用来抑制ADC高频随机噪声。

double slidingFilter(double *src, int idx)
{
  double sum = 0;
  int cnt = 0;
  for(int k = idx - ((FILTER_SIZE-1)/2); k <= idx + ((FILTER_SIZE-1)/2); k++)
  {
    if(k >= 0 && k < FFT_N)
    {
      sum += src[k];
      cnt++;
    }
  }
  return sum / cnt;
}

void calibrateADC()
{
  Serial.println("[Calibrate] Start ADC zero calibration, keep SEN0209 static...");
  float sum = 0.0f;
  for(int i = 0; i < 30; i++)
  {
    sum += analogRead(SEN0209_PIN);
    delay(10);
  }
  ADC_MID = sum / 30.0f;
  Serial.printf("[Calibrate] ADC mid value = %.2f\n", ADC_MID);
}

第4步:ADC采集与FFT运算

hardwareSampleTask  在主循环执行,当MCPWM中断标记到来时读取ADC,填满256点一整帧后标记  frameReady=true ; processFFTFrame  执行滑动平均、汉明窗、FFT正向计算、频谱幅值归一化,完成一帧频域转换。

void hardwareSampleTask()
{
  if(!samplingEnabled) return;
  if(adcSampleRequest)
  {
    adcSampleRequest = false;
    if(sampleIndex < FFT_N)
    {
      rawBuf[sampleIndex] = analogRead(SEN0209_PIN) - ADC_MID;
      sampleIndex++;
      if(sampleIndex >= FFT_N)
      {
        frameReady = true;
        sampleIndex = 0;
      }
    }
  }
}

void processFFTFrame()
{
  for(int i = 0; i < FFT_N; i++)
  {
    filterBuf[i] = slidingFilter(rawBuf, i);
    vReal[i] = filterBuf[i];
    vImag[i] = 0;
  }
  FFT.Windowing(vReal, FFT_N, FFT_WIN_TYP_HAMMING, FFT_FORWARD);
  FFT.Compute(vReal, vImag, FFT_N, FFT_FORWARD);
  FFT.ComplexToMagnitude(vReal, vImag, FFT_N);
  for(int i = 0; i < FFT_N/2; i++)
  {
    vReal[i] = vReal[i] / (FFT_N / 2.0);
  }
  frameId++;
  processingReady = true;
}

第5步:辅助函数,提取主频、拼接JSON

getTop3FreqString  遍历频谱数组,找到幅值最大前3个频率作为管道脉动特征主频; getTimeJSON  /  getFftJSON  将时域、频谱数组转为JSON字符串,用于网页传输。

String getTop3FreqString()
{
  double freqRes = (double)SAMPLING_FREQ / FFT_N;
  static double val[FFT_N/2];
  static int idxList[3];
  for(int i = 0; i < FFT_N/2; i++) val[i] = vReal[i];
  for(int n = 0; n < 3; n++)
  {
    double maxV = -1;
    int pos = 0;
    for(int i = 0; i < FFT_N/2; i++)
    {
      if(val[i] > maxV)
      {
        maxV = val[i];
        pos = i;
      }
    }
    idxList[n] = pos;
    val[pos] = -99999;
  }
  char buf[128];
  snprintf(buf, sizeof(buf), "%.2f Hz, %.2f Hz, %.2f Hz",
    idxList[0] * freqRes,
    idxList[1] * freqRes,
    idxList[2] * freqRes
  );
  return String(buf);
}

String getTimeJSON()
{
  String s = "[";
  for(int i = 0; i < FFT_N; i++)
  {
    s += String(filterBuf[i]);
    if(i != FFT_N -1) s += ",";
  }
  s += "]";
  return s;
}

String getFftJSON()
{
  String s = "[";
  for(int i = 0; i < FFT_N/2; i++)
  {
    s += String(vReal[i]);
    if(i != (FFT_N/2)-1) s += ",";
  }
  s += "]";
  return s;
}

第6步:Web路由、内嵌前端网页

定义Web服务路由:

  •  /  返回完整前端网页,包含Chart.js绘图、工况保存、启停按钮
  • ​/data  返回最新一帧时域、频谱、前3主频JSON数据
  • ​/start 、 /stop  接收前端指令,调用上面  startSampling()  /  stopSampling() ,实现硬件启停同步
void handleRoot()
{
  String html = R"HTML(




管道流体脉动采集 SEN0209




无源管道流体脉动采集装置 ESP32-C5 + SEN0209

开始采集 保存当前工况 清空已保存工况 清空全部界面曲线 清空未保存曲线 特征频率:等待采集

)HTML";
server.send(200, "text/html", html);
}

void handleData()
{
if(processingReady)
{
String json = "{";
json += "\"frameId\":" + String(frameId) + ",";
json += "\"time\":" + getTimeJSON() + ",";
json += "\"fft\":" + getFftJSON() + ",";
json += "\"top3\":\"" + getTop3FreqString() + "\"";
json += "}";
server.send(200, "application/json", json);
processingReady = false;
}
else
{
server.send(200, "application/json", "{\"frameId\":0,\"time\":[],\"fft\":[],\"top3\":\"等待计算\"}");
}
}

void handleStart()
{
startSampling();
server.send(200, "text/plain", "ok");
}

void handleStop()
{
stopSampling();
server.send(200, "text/plain", "ok");
}

第7步:setup初始化 + loop主循环

setup里面依次执行串口初始化、ADC配置、WiFi连接、ADC零点标定、MCPWM定时器初始化、注册全部Web路由,启动WebServer;loop循环里面持续处理HTTP请求,执行ADC采集任务,当一帧采集满后执行FFT运算。

void setup()
{
  Serial.begin(115200);
  analogSetAttenuation(ADC_11db);
  pinMode(SEN0209_PIN, INPUT);

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

  calibrateADC();
  initMcpwmTimer(SAMPLING_FREQ);

  server.on("/", handleRoot);
  server.on("/data", handleData);
  server.on("/start", handleStart);
  server.on("/stop", handleStop);
  server.begin();
  Serial.println("Web server started");
}

void loop()
{
  server.handleClient();
  hardwareSampleTask();
  if(frameReady)
  {
    processFFTFrame();
    frameReady = false;
  }
}

完整代码

#include "WiFi.h"
#include "WebServer.h"
#include "arduinoFFT.h"
#include "driver/mcpwm.h"
#include "freertos/portmacro.h"

// ========= 用户配置区 =========
const char* WIFI_SSID = "你的WiFi名称";
const char* WIFI_PASS = "你的WiFi密码";
#define SEN0209_PIN     4
#define SAMPLING_FREQ   800
#define FFT_N           256
#define FILTER_SIZE     5
// ==============================

WebServer server(80);
arduinoFFT FFT = arduinoFFT();

double vReal[FFT_N];
double vImag[FFT_N];
double rawBuf[FFT_N];
double filterBuf[FFT_N];
float ADC_MID = 2048.0f;
uint32_t frameId = 0;

volatile bool adcSampleRequest = false;
volatile bool frameReady = false;
volatile bool processingReady = false;
int sampleIndex = 0;
bool samplingEnabled = false;

void IRAM_ATTR mcpwmIsrHandler(void *arg)
{
  adcSampleRequest = true;
  portYIELD_FROM_ISR(pdFALSE);
}

void initMcpwmTimer(uint32_t sampleFreq)
{
  mcpwm_config_t mcpwmCfg = {
    .frequency = sampleFreq,
    .duty_cycle = 50.0f,
    .counter_mode = MCPWM_UP_COUNTER,
    .carrier_mode = MCPWM_CARRIER_DISABLE
  };
  mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_0, &mcpwmCfg);
  mcpwm_isr_register(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, mcpwmIsrHandler, NULL);
  Serial.printf("[MCPWM] Timer config done, sampling freq: %d Hz\n", sampleFreq);
}

void startSampling()
{
  if(!samplingEnabled)
  {
    mcpwm_start(MCPWM_UNIT_0, MCPWM_TIMER_0);
    samplingEnabled = true;
    Serial.println("[Sampling] START");
  }
}

void stopSampling()
{
  if(samplingEnabled)
  {
    mcpwm_stop(MCPWM_UNIT_0, MCPWM_TIMER_0);
    samplingEnabled = false;
    adcSampleRequest = false;
    frameReady = false;
    processingReady = false;
    sampleIndex = 0;
    Serial.println("[Sampling] STOP");
  }
}

double slidingFilter(double *src, int idx)
{
  double sum = 0;
  int cnt = 0;
  for(int k = idx - ((FILTER_SIZE-1)/2); k <= idx + ((FILTER_SIZE-1)/2); k++)
  {
    if(k >= 0 && k < FFT_N)
    {
      sum += src[k];
      cnt++;
    }
  }
  return sum / cnt;
}

void calibrateADC()
{
  Serial.println("[Calibrate] Start ADC zero calibration, keep SEN0209 static...");
  float sum = 0.0f;
  for(int i = 0; i < 30; i++)
  {
    sum += analogRead(SEN0209_PIN);
    delay(10);
  }
  ADC_MID = sum / 30.0f;
  Serial.printf("[Calibrate] ADC mid value = %.2f\n", ADC_MID);
}

void hardwareSampleTask()
{
  if(!samplingEnabled) return;
  if(adcSampleRequest)
  {
    adcSampleRequest = false;
    if(sampleIndex < FFT_N)
    {
      rawBuf[sampleIndex] = analogRead(SEN0209_PIN) - ADC_MID;
      sampleIndex++;
      if(sampleIndex >= FFT_N)
      {
        frameReady = true;
        sampleIndex = 0;
      }
    }
  }
}

void processFFTFrame()
{
  for(int i = 0; i < FFT_N; i++)
  {
    filterBuf[i] = slidingFilter(rawBuf, i);
    vReal[i] = filterBuf[i];
    vImag[i] = 0;
  }
  FFT.Windowing(vReal, FFT_N, FFT_WIN_TYP_HAMMING, FFT_FORWARD);
  FFT.Compute(vReal, vImag, FFT_N, FFT_FORWARD);
  FFT.ComplexToMagnitude(vReal, vImag, FFT_N);
  for(int i = 0; i < FFT_N/2; i++)
  {
    vReal[i] = vReal[i] / (FFT_N / 2.0);
  }
  frameId++;
  processingReady = true;
}

String getTop3FreqString()
{
  double freqRes = (double)SAMPLING_FREQ / FFT_N;
  static double val[FFT_N/2];
  static int idxList[3];
  for(int i = 0; i < FFT_N/2; i++) val[i] = vReal[i];
  for(int n = 0; n < 3; n++)
  {
    double maxV = -1;
    int pos = 0;
    for(int i = 0; i < FFT_N/2; i++)
    {
      if(val[i] > maxV)
      {
        maxV = val[i];
        pos = i;
      }
    }
    idxList[n] = pos;
    val[pos] = -99999;
  }
  char buf[128];
  snprintf(buf, sizeof(buf), "%.2f Hz, %.2f Hz, %.2f Hz",
    idxList[0] * freqRes,
    idxList[1] * freqRes,
    idxList[2] * freqRes
  );
  return String(buf);
}

String getTimeJSON()
{
  String s = "[";
  for(int i = 0; i < FFT_N; i++)
  {
    s += String(filterBuf[i]);
    if(i != FFT_N -1) s += ",";
  }
  s += "]";
  return s;
}

String getFftJSON()
{
  String s = "[";
  for(int i = 0; i < FFT_N/2; i++)
  {
    s += String(vReal[i]);
    if(i != (FFT_N/2)-1) s += ",";
  }
  s += "]";
  return s;
}

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




管道流体脉动采集 SEN0209




无源管道流体脉动采集装置 ESP32-C5 + SEN0209

开始采集 保存当前工况 清空已保存工况 清空全部界面曲线 清空未保存曲线 特征频率:等待采集 )HTML"; server.send(200, "text/html", html); } void handleData() { if(processingReady) { String json = "{"; json += "\"frameId\":" + String(frameId) + ","; json += "\"time\":" + getTimeJSON() + ","; json += "\"fft\":" + getFftJSON() + ","; json += "\"top3\":\"" + getTop3FreqString() + "\""; json += "}"; server.send(200, "application/json", json); processingReady = false; } else { server.send(200, "application/json", "{\"frameId\":0,\"time\":[],\"fft\":[],\"top3\":\"等待计算\"}"); } } void handleStart() { startSampling(); server.send(200, "text/plain", "ok"); } void handleStop() { stopSampling(); server.send(200, "text/plain", "ok"); } void setup() { Serial.begin(115200); analogSetAttenuation(ADC_11db); pinMode(SEN0209_PIN, INPUT); Serial.printf("Connecting WiFi: %s\n", WIFI_SSID); WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS); while(WiFi.stat


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


登录 后发表评论
3条评论
阿伟不emo
1楼 · 1天前

项目很实用

拆机党
2楼 · 1天前

代码清晰