Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Design of a Dual-Mode Biometric Data Acquisition System Using STM32

Tech May 12 2

System Architecture Overview

This project implements a portable biometric monitoring device centered around the STM32F103C8T6 microcontroller. The system integrates both photoplethysmography (PPG) and electrocardiography (ECG) sensing technologies to provide comprehensive cardiac health monitoring. It supports real-time local visualization via an OLED display, data logging to an SD card, and wireless telemetry using Bluetooth or Wi-Fi modules. The design focuses on low-power operation, signal integrity, and modularity, making it suitable for telemedicine and wearable health applications.

Hardware Implementation

The system architecture is divided into distinct functional blocks: the analog front-end, the digital processing core, the user interface, and connectivity modules. Communication between the MCU and peripherals utilizes standard protocols including I2C, SPI, UART, and ADC.

Key Hardware Components

Signal Conditioning Circuits

1. Optical Path (MAX30102): The sensor interfaces via I2C (SCL: PB10, SDA: PB11). It is configured in SpO2 mode to simultaneously acquire both Red and Infrared light intensity. The integration time is set to 411µs with a sample rate of 100Hz to optimize power consumption while maintaining signal resolution. An active interrupt pin (PA7) triggers the MCU when the FIFO buffer reaches the water-mark level.

2. Bio-electrical Path (AD8232): This module provides a signal gain of 1000V/V and includes a right-leg drive (RLD) circuit to minimize common-mode noise. The output signal, centered around 0.5V DC bias, is fed into the MCU's ADC1 Channel 0 (PA0). A hardware band-pass filter limits the bandwidth to 0.5Hz - 40Hz to focus on the QRS complex and suppress motion artifacts.

3. Power Management: A TP4056 IC manages the constant-current/constant-voltage charging of the single-cell Li-ion battery. An AMS1117-3.3 LDO regulator steps down the battery voltage to a stable 3.3V rail for all logic circuits. The firmware leverages the STM32's Stop Mode, cutting power to non-essential peripherals during idle periods to extend battery life beyond 72 hours.

Firmware Design (C Language)

The firmware is structured using a modular architecture. It employs a main loop that polls status flags and executes state machine logic for data acquisition, processing, and transmission.

System Main Control Loop

#include "stm32f10x.h"
#include "optical_sensor.h"
#include "ecg_adc.h"
#include "display_drv.h"
#include "comm_uart.h"
#include "file_system.h"
#include "signal_proc.h"

// Device state context
typedef struct {
    uint8_t op_mode;           // 0=PPG Mode, 1=ECG Mode
    uint8_t active_flag;
    float bpm_val;
    float oxygen_sat;
    float ecg_buffer[256];
    bool bpm_ready;
} DeviceContext;

int main(void) {
    // Peripheral Initialization
    HAL_Init();
    SystemClock_Config();
    
    OPT_Init();           // Configure MAX30102
    ECG_ADC_Init();       // Configure ADC for AD8232
    DISP_Init();          // OLED Startup
    BLE_Init();           // UART Configuration
    FS_Init();            // SD Card Mount
    DSP_Init();           // Algorithm coefficients
    
    DeviceContext dev_ctx = {0};
    dev_ctx.op_mode = 0;   // Default to PPG
    dev_ctx.active_flag = 1;

    while (1) {
        if (dev_ctx.active_flag) {
            
            // Data Acquisition Handler
            if (dev_ctx.op_mode == 0) {
                // PPG Data Path
                OPT_Fetch_Vitals(&dev_ctx.bpm_val, &dev_ctx.oxygen_sat, &dev_ctx.bpm_ready);
            } else {
                // ECG Data Path
                ECG_Capture_Segment(dev_ctx.ecg_buffer, 256);
                // Process QRS complex to determine HR
                dev_ctx.bpm_val = DSP_Compute_HR_ECG(dev_ctx.ecg_buffer, 256);
                dev_ctx.bpm_ready = true;
            }

            // UI Update
            DISP_Render_Stats(dev_ctx.bpm_val, dev_ctx.oxygen_sat, dev_ctx.ecg_buffer, dev_ctx.op_mode);

            // Data Logging
            FS_Log_Segment(&dev_ctx);

            // Wireless Telemetry
            if (dev_ctx.bpm_ready) {
                BLE_Transmit_Packet(dev_ctx.bpm_val, dev_ctx.oxygen_sat);
            }

            // Power Saving Strategy
            if (Is_Idle_Timeout_Reached()) {
                PWR_EnterStopMode();
            }
        }
    }
}

Optical Sensor Driver (MAX30102)

#define OPT_I2C_ADDR    0xAE

// Register Map Definitions
#define REG_MODE_CFG    0x09
#define REG_SPO2_CFG    0x0A
#define REG_FIFO_DATA   0x07
#define REG_LED_PA_1    0x0C

void OPT_Init(void) {
    // Soft Reset
    I2C_WriteByte(OPT_I2C_ADDR, REG_MODE_CFG, 0x40);
    HAL_Delay(10);
    
    // Enable SpO2 Mode (Red + IR)
    I2C_WriteByte(OPT_I2C_ADDR, REG_MODE_CFG, 0x03);
    
    // Set Sample Rate: 100sps, Pulse Width: 411us, ADC Range: 4096
    I2C_WriteByte(OPT_I2C_ADDR, REG_SPO2_CFG, 0x27);
    
    // LED Current: 7.0mA for both channels
    I2C_WriteByte(OPT_I2C_ADDR, REG_LED_PA_1, 0x24);
    I2C_WriteByte(OPT_I2C_ADDR, REG_LED_PA_1 + 1, 0x24);
}

void OPT_Fetch_Vitals(float *bpm, float *spo2, bool *status) {
    uint8_t rx_buf[6];
    uint32_t ir_raw, red_raw;
    
    // Read 3 bytes Red + 3 bytes IR
    I2C_ReadBuffer(OPT_I2C_ADDR, REG_FIFO_DATA, rx_buf, 6);
    
    // Reconstruct 18-bit data
    red_raw = ((uint32_t)rx_buf[0] << 16) | ((uint32_t)rx_buf[1] << 8) | rx_buf[2];
    red_raw &= 0x3FFFF;
    
    ir_raw = ((uint32_t)rx_buf[3] << 16) | ((uint32_t)rx_buf[4] << 8) | rx_buf[5];
    ir_raw &= 0x3FFFF;
    
    // Execute processing algorithms
    *bpm = DSP_Calc_PPG_HR(ir_raw, red_raw, status);
    *spo2 = DSP_Calc_SpO2(ir_raw, red_raw);
}

ECG Analog Front-End Driver

void ECG_ADC_Init(void) {
    // Enable GPIOA and ADC1 clocks
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
    
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // ADC Configuration Structure
    ADC_InitTypeDef ADC_InitStruct = {0};
    ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStruct.ADC_ScanConvMode = DISABLE;
    ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; // Single conversion per trigger
    ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStruct.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &ADC_InitStruct);
    
    // Configure Channel 0 with 239.5 cycles sampling time for stability
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5);
    
    ADC_Cmd(ADC1, ENABLE);
    
    // Run Calibration
    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));
}

void ECG_Capture_Segment(float *buffer, uint16_t count) {
    for(uint16_t i = 0; i < count; i++) {
        ADC_SoftwareStartConvCmd(ADC1, ENABLE);
        while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC));
        
        uint16_t raw_adc = ADC_GetConversionValue(ADC1);
        
        // Convert to Voltage and remove DC offset (0.5V)
        float voltage_mv = ((float)raw_adc * 3300.0f / 4095.0f) - 500.0f;
        buffer[i] = voltage_mv;
    }
}

Signal Processing Algorithms

// Adaptive threshold for QRS detection
#define QRS_AMP_LIMIT  800.0f  
#define REFRACTORY_MS  200

float DSP_Compute_HR_ECG(float *data, uint16_t len) {
    static uint32_t last_peak_tick = 0;
    float local_max = 0;
    int16_t peak_idx = -1;
    
    // Scan for maximum peak in the window
    for(int i = 2; i < len - 2; i++) {
        // Check for peak condition (higher than neighbors)
        if(data[i] > data[i-1] && data[i] > data[i+1]) {
            if(data[i] > local_max && data[i] > QRS_AMP_LIMIT) {
                local_max = data[i];
                peak_idx = i;
            }
        }
    }
    
    if(peak_idx != -1) {
        uint32_t current_tick = HAL_GetTick();
        // Validate refractory period
        if((current_tick - last_peak_tick) > REFRACTORY_MS && last_peak_tick != 0) {
            uint32_t interval_ms = current_tick - last_peak_tick;
            last_peak_tick = current_tick;
            // Calculate BPM
            return 60000.0f / (float)interval_ms;
        }
        last_peak_tick = current_tick;
    }
    return 0.0f; // No valid beat detected
}

System Validation and Optimization

Performance Metrics

Future Enhancements

  • Advanced Filtering: Implementation of digital filters (Butterworth/IIR) to replace basic thresholding and improve noise rejection for ECG signals during movement.
  • Algorithm Upgrade: Migrating to the Pan-Tompkins algorithm for robust QRS detection in noisy environments.
  • Data Compression: Utilizing delta-encoding or Huffman coding on the SD card to maximize storage duration for long-term Holter monitoring.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.