实战:心电图信号处理与ESP32心率血氧计

做这个小项目的想法来源于 HPC WEEK 比赛中 CT 图像并行计算的那道题。当时虽然我看不懂题目,但是大受震撼。因为不管是各种OJ还是Leetcode上我平时都不太会看到实际数据的处理,一般都是想象出来的场景,选择合适的算法和数据结构解决(毕竟以我们这种水平也做不了什么复杂的现实场景的东西)。

我后来问了下GPT,了解到了信号处理这个方向。打算从最基础的心电图信号处理做一个小项目进行实战。

环境配置

在项目文件夹下用venv创建python3.12环境,然后安装了numpy,scipy,matplotlib,wfdb库。

1
pip install numpy scipy matplotlib wfdb

其中:

wfdb:读 MIT-BIH 这类心电数据库

scipy:滤波、信号处理

matplotlib:画图

选择数据

我这里选了编号为100这个病人的数据(也就是第一个病人)。

wfdb.rdrecord() 是 WFDB 库的核心函数,用于读取生理信号记录(如 ECG 心电信号)。它返回一个record对象:

1
2
3
4
5
6
record.p_signal    # 生理信号数据(numpy 数组)
record.fs # 采样频率(Hz),MITDB 通常是 360 Hz
record.sig_name # 信号名称,如 ['MLII', 'V1', 'V2', 'V4', 'V5']
record.n_sig # 通道数
record.units # 信号单位,如 ['mV', 'mV']
record.record_name # 记录名 '100'

其中,record.p_signal是存储生理信号数据的 numpy 数组,通常为 (样本数, 通道数)

MLII (Modified Limb Lead II),是第1个通道,是最常用的监测导联。问GPT后说这个最常用,所以我就取了这个。

给原始数据画图

创建一个时间轴t,用signal采样点的序号除以频率(1/周期),就是该采样点的时间。

1
t = [i/fs for i in range(len(signal))]

再画前五秒的图:

1
2
3
4
5
plt.figure()
plt.plot(t[:5*fs], signal[:5*fs])
plt.xlabel("时间(s)")
plt.ylabel("电压(mV)")
plt.show()

去噪

带通滤波

带通滤波只保留需要的频段,这里我保留0.5–40 Hz的频段。Google后就可以知道,< 0.5 Hz的低频是基线漂移噪声,> 40 Hz的是肌电漂移。

1
b, a = butter(order, [low/nyq, high/nyq], btype='band')

这个是巴特沃斯带通滤波器,

陷波滤波

去除50Hz的工频干扰

1
2
3
def notch_filter(x, fs, f0=50, Q=30):
b, a = iirnotch(w0=f0, Q=Q, fs=fs)
return filtfilt(b, a, x)

R波检测

算心率

异常检测

下面我按**“买到 ESP32‑C3 → Arduino 连接 → 烧录 MAX30102 测血氧(SpO₂)程序”**的完整流程给你一套可直接照做的步骤,并附上一个可跑的示例代码与常见踩坑排查。

⚠️先提醒:MAX30102 这种光学法心率/血氧很容易受手指压力、环境光、手指位置影响,不适合医疗诊断用途。 [docs.sunfounder.com]


1) 硬件连接(ESP32‑C3 ↔ MAX30102)

MAX30102 模块通常是 I²C 设备(SDA/SCL),你只需要 4 根线:

  • VCC → 3.3V(建议 3.3V;很多模块也能接 5V但不推荐混用)
  • GND → GND
  • SCL → ESP32‑C3 任意可用 GPIO(作为 I²C SCL)
  • SDA → ESP32‑C3 任意可用 GPIO(作为 I²C SDA)

✅重点:ESP32 的 I²C 引脚不是固定的,你在代码里用 Wire.begin(SDA, SCL) 指定就行。
✅如果你买的是那种“小黑板 MAX30102”,手指容易碰到排针焊点,人体电容会干扰 I²C,导致读数突然断掉/全 0。可以通过绝缘或把 I²C 从 400k 降到 100k 来缓解。 [blog.csdn.net], [cnblogs.com]


2) Arduino IDE 安装 ESP32‑C3 开发板支持(最关键)

2.1 添加 ESP32 Boards Manager 地址

打开 Arduino IDE → 文件(Preferences/首选项) → “附加开发板管理器网址”填入(官方):

  • 稳定版:https://espressif.github.io/arduino-esp32/package_esp32_index.json [docs.espressif.com]
  • 开发版:https://espressif.github.io/arduino-esp32/package_esp32_dev_index.json [docs.espressif.com]

如果你在国内下载慢/失败,Espressif 官方也给了 Jihulab 镜像(带 _cn 后缀)

  • 稳定版(CN):https://jihulab.com/esp-mirror/espressif/arduino-esp32/-/raw/gh-pages/package_esp32_index_cn.json [docs.espressif.com]
  • 开发版(CN):https://jihulab.com/esp-mirror/espressif/arduino-esp32/-/raw/gh-pages/package_esp32_dev_index_cn.json [docs.espressif.com]

Espressif 文档还提到:在中国地区要选带 “-cn” 的包,并且自动更新可能不支持,需要手动更新。 [docs.espressif.com]

2.2 安装开发板包

工具 → 开发板 → 开发板管理器 → 搜索 esp32 → 安装 “esp32 by Espressif Systems”[docs.espressif.com]


3) 让 ESP32‑C3 在 Arduino 里“出现端口”并能烧录

ESP32‑C3 常见两种板型:

A) 你的板子是“原生 USB”(直接连到 C3 芯片 USB)

这种模式下,Espressif 提供了 USB CDC 烧录/串口功能,C3 属于 CDC only[docs.espressif.com]

在 Arduino IDE:

  • Tools(工具) 里把 USB CDC On Boot → Enabled(ESP32‑C3 的推荐设置) [docs.espressif.com]

如果第一次刷或端口没出来,按 Espressif 文档的方式进 Download Mode:

  • 按住 BOOT → 点一下 RESET → 松开,这时系统会枚举出新的 USB 设备端口,然后在 Tools → Port 里选它。 [docs.espressif.com]

B) 你的板子是“USB 转串口芯片”(CH340/CP210x 等)

这种情况下,系统会出现 COM 口/tty 口,Arduino 里 Tools → Port 选择对应端口即可;有时烧录需要按住 BOOT辅助进入下载。 [docs.espressif.com]

✅如果“板子亮灯但没有端口”,先排查:数据线是否为数据线、驱动是否装对、换 USB 口/线。


4) 安装 MAX30102 所需 Arduino 库(SparkFun MAX3010x)

很多教程/课程都使用 SparkFun MAX3010x 这个库,在 Arduino 库管理器里直接搜 “SparkFun MAX3010x” 安装即可。 [docs.sunfounder.com], [microcontr…erslab.com]

它的示例里包含心率、原始数据绘图、presence、温度等,也常被用来做 SpO₂/心率项目。 [microcontr…erslab.com]

另外不少文章也提到:在 Arduino 里用 MAX30105.h 这个类名虽然写的是 30105,但 MAX30102 也能用,并且 SpO₂ 常配合 spo2_algorithm.h[cnblogs.com], [jianshu.com]


5) 直接可烧录的 ESP32‑C3 + MAX30102(心率 + SpO₂)示例代码

说明:

  • 代码思路是:采集一段红光/红外数据 → 调用 spo2_algorithm 算法 → 输出 BPM 和 SpO₂。
  • I²C 我默认用 100kHzI2C_SPEED_STANDARD)来降低“手指触碰干扰”概率,这是社区常见解决方向。 [blog.csdn.net], [cnblogs.com]
  • 你只要改 SDA_PIN/SCL_PIN 为你实际接线的 GPIO 即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <Wire.h>
#include "MAX30105.h"
#include "spo2_algorithm.h"

MAX30105 particleSensor;

// === 改成你实际接线的GPIO ===
static const int SDA_PIN = 8;
static const int SCL_PIN = 9;

static const uint16_t SAMPLE_COUNT = 100; // 采样点数(越大越稳但越慢)
uint32_t irBuffer[SAMPLE_COUNT];
uint32_t redBuffer[SAMPLE_COUNT];

void setup() {
Serial.begin(115200);
delay(200);

// ESP32可自定义I2C引脚
Wire.begin(SDA_PIN, SCL_PIN);

// 以较低I2C速率初始化(降低干扰概率)
if (!particleSensor.begin(Wire, I2C_SPEED_STANDARD)) {
Serial.println("MAX30102 not found. Check wiring/power.");
while (1) delay(10);
}

// 传感器配置:以下是一个“可用的起点”,你可以再按需要调参
particleSensor.setup(
0x1F, // ledBrightness (0x00~0xFF)
4, // sampleAverage (1,2,4,8,16,32)
2, // ledMode (1=Red, 2=Red+IR, 3=Red+IR+Green)
100, // sampleRate (50,100,200,400,800,1000,1600,3200)
411, // pulseWidth (69,118,215,411)
4096 // adcRange (2048,4096,8192,16384)
);

Serial.println("Place your finger on the sensor.");
}

void loop() {
// 采集一段数据
for (uint16_t i = 0; i < SAMPLE_COUNT; i++) {
while (particleSensor.available() == false) {
particleSensor.check(); // 拉取FIFO
delay(1);
}
redBuffer[i] = particleSensor.getRed();
irBuffer[i] = particleSensor.getIR();
particleSensor.nextSample();
}

// 运行血氧算法
int32_t spo2;
int8_t validSPO2;
int32_t heartRate;
int8_t validHeartRate;

maxim_heart_rate_and_oxygen_saturation(
irBuffer, SAMPLE_COUNT,
redBuffer,
&spo2, &validSPO2,
&heartRate, &validHeartRate
);

Serial.print("HR=");
if (validHeartRate) Serial.print(heartRate);
else Serial.print("NA");

Serial.print(" bpm, SpO2=");
if (validSPO2) Serial.print(spo2);
else Serial.print("NA");
Serial.println(" %");

delay(200);
}

如何烧录

  1. Arduino IDE 选择:Tools → Board → ESP32C3 Dev Module(或你板子的对应 C3 型号)。
  2. 选择 Tools → Port(能看到端口才行)。
  3. 点“上传”。如果失败:按住 BOOT 再点上传,或按 Espressif 文档进 Download Mode(BOOT+RESET)。 [docs.espressif.com], [docs.espressif.com]

6) 常见问题速查(你大概率会遇到)

Q1:Arduino 没有端口/不识别开发板

  • 先换一根确认可传数据的 Type‑C/USB 线(很多线只能充电)。
  • 如果是 USB 转串口方案,检查驱动(CH340/CP210x);如果是原生 USB,按 BOOT+RESET 让它枚举端口。 [docs.espressif.com], [docs.espressif.com]

Q2:手指一放上去数据就卡住、变 0、串口不输出

这是 MAX30102 黑色小板常见坑:手指碰到排针焊点干扰 I²C。

  • 给排针背面做绝缘(热缩管/胶带/3D 打印壳)。
  • I²C 降速到 100k(上面代码已用 I2C_SPEED_STANDARD)。 [blog.csdn.net], [cnblogs.com]

Q3:SpO₂ 总是不准/跳变大

  • 这是算法与信号质量问题很常见;手指要稳定、遮光、压力适中。并且该方法不适合医疗用途。 [docs.sunfounder.com]

你如果想要我把步骤“对号入座”到你的板子

我只需要你补充 1 个信息就能把引脚和 Arduino 工具选项写到“完全匹配你的型号”的版本里:
你买的 ESP32‑C3 具体板子型号是什么?(比如 DevKitM‑1 / SuperMini / 其它)