管道内部流体流动会产生压力脉动,脉动信号包含流量变化、湍流、振动、泵体异常等特征。传统管道流体监测设备体积大、成本高,本项目使用ESP32‑C5搭配SEN0209振动传感器,以800Hz固定采样频率采集管道壁振动信号,通过滑动平均滤波与快速傅里叶变换(FFT)把时域振动波形转换成频谱,再通过内置Web网页实时展示波形,支持最多8组历史工况本地保存对比,实现低成本无源式管道流体脉动在线采集与特征分析。
-“无源”含义:本装置只贴附在管道外壁,不需要开孔、不需要侵入管道内部,依靠拾取管道壁传导的流体振动信号完成监测,属于非侵入式采集。
创作环境
硬件
ESP32‑C5开发板、SEN0209压电振动传感器、USB 5V供电
接线:SEN0209 → VCC接3.3V,GND接GND,AO模拟输出接入ESP32‑C5 GPIO4,传感器紧贴管道外壁,实现非侵入式采集管壁传导的流体振动信号。

选择本产品的原因:
柔性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










项目很实用
代码清晰
想复刻