1. 心电信号采集与显示

心电信号采集与显示 学习卡片

任务说明

本案例实现一个最小可复现的心电信号采集与显示系统:微控制器通过片内ADC读取模拟前端输出的心电信号,将采样数据经简单滤波后映射为LCD屏幕上的波形点,实现实时滚动显示。为便于独立验证,本案例使用STM32F407微控制器、内置ADC1、以及一块128×64像素的SSD1306 OLED显示屏(I2C接口)。模拟前端部分以信号发生器或分压电位器输出的0~3.3V模拟信号代替真实心电放大电路,聚焦于“采集→处理→显示”这一核心链路。

硬件连接

模拟前端 信号源/电位器 0~3.3V 输出 (模拟ECG波形) Vout GND STM32F407 ADC1_IN0 (PA0) 12位采样 0~4095 定时器触发采样 软件滤波+缩放 I2C1 (PB6/PB7) PA0 SCL (PB6) SDA (PB7) OLED 128×64 SSD1306 I2C 地址 0x3C 实时波形显示 采样率:500 Hz(TIM2触发) 显示:每2个采样点取平均 → 250点/秒滚动刷新

代码实现

以下代码包含完整的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像素处。此结果由代码中的线性映射公式直接推出,可作为功能正确的参考。