Design of a Dual-Mode Biometric Data Acquisition System Using STM32
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.