1. 心电信号采集与显示
任务说明
本案例实现一个最小可复现的心电信号采集与显示系统:微控制器通过片内ADC读取模拟前端输出的心电信号,将采样数据经简单滤波后映射为LCD屏幕上的波形点,实现实时滚动显示。为便于独立验证,本案例使用STM32F407微控制器、内置ADC1、以及一块128×64像素的SSD1306 OLED显示屏(I2C接口)。模拟前端部分以信号发生器或分压电位器输出的0~3.3V模拟信号代替真实心电放大电路,聚焦于“采集→处理→显示”这一核心链路。
硬件连接
代码实现
以下代码包含完整的ADC初始化、定时器触发采样、滑动平均滤波、以及SSD1306驱动与波形绘制函数。主循环中每采集到2个样本即计算平均值并写入显示缓冲区,实现波形从右向左滚动。
// ============ ecg_display.c ============
// 依赖:STM32F4xx HAL库,I2C1(PA8-PB9重映射或PB6/PB7),TIM2,ADC1
// 编译:arm-none-eabi-gcc + STM32CubeF4 HAL
#include "main.h"
#include "ssd1306.h" // 提供 ssd1306_Init(), ssd1306_Fill(), ssd1306_DrawPixel(), ssd1306_UpdateScreen()
// ---- 配置常量 ----
#define SAMPLE_RATE_HZ 500
#define ADC_BUF_SIZE 2 // 每2个样本平均一次
#define DISPLAY_WIDTH 128
#define DISPLAY_HEIGHT 64
#define ECG_Y_MID (DISPLAY_HEIGHT/2)
#define ECG_AMPLITUDE 25 // 波形垂直幅度(像素)
// ---- 全局变量 ----
static ADC_HandleTypeDef hadc1;
static TIM_HandleTypeDef htim2;
static uint16_t adc_buf[ADC_BUF_SIZE];
static volatile uint8_t sample_count = 0;
static volatile uint16_t ecg_value = 0; // 滤波后的心电值(0~4095)
// ---- ADC与定时器初始化 ----
void MX_ADC1_Init(void) {
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
HAL_ADC_Init(&hadc1);
}
void MX_TIM2_Init(void) {
// 500 Hz 触发:84MHz / (168-1) / (1000-1) = 500 Hz
htim2.Instance = TIM2;
htim2.Init.Prescaler = 168 - 1;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 1000 - 1;
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
TIM_MasterConfigTypeDef sMasterConfig = {0};
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig);
}
// ---- ADC转换完成回调 ----
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
if (hadc->Instance == ADC1) {
adc_buf[sample_count++] = HAL_ADC_GetValue(&hadc1);
if (sample_count >= ADC_BUF_SIZE) {
sample_count = 0;
// 滑动平均滤波
ecg_value = (adc_buf[0] + adc_buf[1]) / 2;
}
// 启动下一次转换
HAL_ADC_Start_IT(&hadc1);
}
}
// ---- 波形绘制:右移一列,新点画在最右 ----
void ECG_DrawWaveform(uint16_t adc_val) {
// 将ADC值映射到屏幕Y坐标(0~63),并限制幅度
int y = ECG_Y_MID + ((int)adc_val - 2048) * ECG_AMPLITUDE / 2048;
if (y < 0) y = 0;
if (y > DISPLAY_HEIGHT-1) y = DISPLAY_HEIGHT-1;
// 整屏左移一列(滚动效果)
for (int x = 0; x < DISPLAY_WIDTH-1; x++) {
for (int yy = 0; yy < DISPLAY_HEIGHT; yy++) {
if (ssd1306_GetPixel(x+1, yy)) {
ssd1306_DrawPixel(x, yy, 1);
} else {
ssd1306_DrawPixel(x, yy, 0);
}
}
}
// 清除最右列
for (int yy = 0; yy < DISPLAY_HEIGHT; yy++) {
ssd1306_DrawPixel(DISPLAY_WIDTH-1, yy, 0);
}
// 绘制新采样点
ssd1306_DrawPixel(DISPLAY_WIDTH-1, y, 1);
ssd1306_UpdateScreen();
}
// ---- 主程序 ----
int main(void) {
HAL_Init();
SystemClock_Config(); // 系统时钟配置(168MHz)
MX_ADC1_Init();
MX_TIM2_Init();
ssd1306_Init();
ssd1306_Fill(0);
ssd1306_UpdateScreen();
// 启动ADC与定时器
HAL_ADC_Start_IT(&hadc1);
HAL_TIM_Base_Start(&htim2);
while (1) {
// 等待新滤波值产生(由中断更新)
static uint16_t last_val = 0;
if (ecg_value != last_val) {
last_val = ecg_value;
ECG_DrawWaveform(last_val);
}
}
}
验证方法
硬件准备:将信号发生器输出(或10kΩ电位器分压)接至PA0,GND共地。调节输出在0~3.3V之间变化(模拟心电信号的R波峰值)。
运行与观察:编译烧录后,OLED屏幕应显示一条从右向左滚动的波形曲线。当输入电压缓慢变化时,波形幅度随之上下移动;当输入为1.65V(ADC值约2048)时,波形应位于屏幕垂直中线附近。
判定标准:
- 屏幕持续刷新且无撕裂或闪烁(SSD1306内部缓冲保证);
- 输入电压升高时波形上移,降低时波形下移,方向正确;
- 用示波器测量PA0输入,确认ADC采样率约为500Hz(可通过调试器观察ecg_value更新频率)。
预期输出:当输入为1.0V时,ADC值约为1241,映射Y坐标约为(2048-1241)×25/2048≈9.8,即波形显示在屏幕中线上方约10像素处;输入为2.5V时,ADC值约为3103,波形显示在中线下方约13像素处。此结果由代码中的线性映射公式直接推出,可作为功能正确的参考。