穿戴式生命体征监测仪:STM32 + MAX30102 + GY906 + MSP20 + GPS + 阿里云

这是一套基于 STM32F103C8T6 的穿戴式多参数健康监测终端,能同时采集心率、血氧、血压、体温,并通过 ATGM336H GPS 模块获取定位,最终经 ESP8266 WiFi 以 MQTT 协议上传到阿里云物联网平台。本地 OLED 实时显示,异常时蜂鸣器 + LED 声光报警。适合作为健康监测、老人看护、户外运动等场景的参考方案。

一、项目背景与功能

系统的核心目标是做一款“戴在身上就能知道身体状态”的小型监测设备:

  • 心率/血氧:MAX30102 光电传感器,基于 PPG + FFT 算法计算心率和血氧饱和度。
  • 体温:GY906(MLX90614)非接触红外测温,测量人体额头/手腕温度。
  • 血压:MSP20 压力传感器,通过 ADC 采集并换算为血压值(kPa)。
  • GPS 定位:ATGM336H 模块解析 NMEA 语句,获取经纬度。
  • 本地显示:0.96 寸 IIC OLED 显示所有体征数据。
  • 声光报警:心率/血氧/体温/血压任一超阈值,蜂鸣器 + LED 报警。
  • 阈值可调:4 个独立按键设置心率、血氧、体温、血压报警阈值。
  • 远程上云:通过 ESP8266 连接 WiFi,以 MQTT 接入阿里云物联网平台,周期性上报健康数据和位置信息。

二、硬件清单

元器件 数量 说明
STM32F103C8T6 最小系统板 1 主控 MCU,72MHz Cortex-M3
0.96 寸 OLED 显示屏 1 IIC 接口,128×64 分辨率
MAX30102 传感器模块 1 PPG 心率血氧,IIC 接口
GY906(MLX90614)红外测温模块 1 SMBus/IIC 接口
MSP20 压力传感器 1 模拟输出,接 PA0 用于血压估算
ATGM336H GPS 模块 1 串口输出 NMEA 数据
ESP8266 WiFi 模块 1 串口 AT 指令,连接阿里云
有源蜂鸣器 1 PA15 驱动
LED 1 PA12 状态/报警指示灯
独立按键 4 PB12~PB15,上拉输入
DC-005 电源接口 1 5V 输入供电

IO 资源分配

// MAX30102:PA6(SCL)、PA7(SDA)
// GY906: PB10(SCL)、PB11(SDA)
// OLED: PB0(SCL)、PB1(SDA)
// MSP20: PA0(ADC1_CH0)
// GPS: USART1 PA9(TX)、PA10(RX),9600bps
// WiFi: USART2 PA2(TX)、PA3(RX),115200bps
// WiFi 复位:PA4
// 蜂鸣器: PA15
// 报警 LED:PA12
// 按键: PB12(ADD)、PB13(REDUCE)、PB14(SET)、PB15(CHOICE)

三、硬件设计

3.1 原理图

原理图

原理图主要分几个区域:

  • 主控开发板:STM32F103C8T6 最小系统,引出 USART1/2、ADC、多路 IIC 和 GPIO。
  • 电源模块:DC-005 接口 + 330uF/100nF 滤波,5V 转 3.3V 供 MCU 和模块。
  • MAX30102 模块:IIC 接 PA6/PA7,INT 中断脚接 PA4(与 WiFi 复位复用,实际代码中 INT 由 MAX30102_INTPin_Read 读取)。
  • GY906 模块:IIC 接 PB10/PB11。
  • MSP20 压力传感:模拟信号接 PA0。
  • ATGM336H GPS:串口接 PA9/PA10。
  • ESP8266 WiFi:串口接 PA2/PA3,RST 接 PA4。
  • OLED 屏幕:IIC 接 PB0/PB1。
  • 蜂鸣器/LED/按键:常规 GPIO 驱动。

四、软件设计

4.1 主循环初始化

main.c 负责初始化所有外设,然后进入主循环采集 GPS、心率和血氧:

int main(void)
{
delay_init();
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
SPECIAL_PIN_Init();

USART1_Init(9600); // GPS
Usart2_Init(115200); // ESP8266 WiFi

Init_adc(); // MSP20 血压 ADC
KEY_Init();
LED_Init();
BEEP_Init();
OLED_Init();
OLED_Clear();
page_one(); // 显示首页

IIC_GPIO_INIT(); // MAX30102 IIC
MAX30102_GPIO();
Max30102_reset();
MAX30102_Config();

Mlx96014_Configuration(); // GY906 初始化

WiFi_ResetIO_Init();
MQTT_Buff_Init();
AliIoT_Parameter_Init(); // 阿里云 MQTT 参数

TIM1_Time_Init(99, 7199); // 10ms 中断
TIM4_Init(300, 7200); // 30ms 中断,MQTT 保活

while (1)
{
if (ceshi_status)
blood_Loop(); // 采集心率和血氧

parseGpsBuffer(); // 解析 GPS NMEA
printGpsBuffer();

// 根据 NMEA 中的格式把经纬度转换为 double
if (Save_Data.longitude[5] == '.')
jingdu = ...;
else if (Save_Data.longitude[4] == '.')
jingdu = ...;

if (Save_Data.latitude[5] == '.')
weidu = ...;
else if (Save_Data.latitude[4] == '.')
weidu = ...;

if (show_mode == 1) // GPS 页面显示经纬度
{
OLED_ShowNum(72, 0, (u8)jingdu, 3, 16);
OLED_ShowNum(104, 0, (u16)(jingdu * 100) % 100, 2, 16);
OLED_ShowNum(72, 2, (u8)weidu, 3, 16);
OLED_ShowNum(104, 2, (u16)(weidu * 100) % 100, 2, 16);
}
Delay_Ms(500);
}
}

4.2 MAX30102 心率血氧检测

MAX30102 通过 IIC 读取 FIFO 中的红光/红外光数据,然后做 FFT 频谱分析提取脉搏波频率,从而计算心率和血氧:

void blood_data_update(void)
{
g_fft_index = 0;
while (g_fft_index < FFT_N)
{
while (MAX30102_INTPin_Read() == 0)
{
max30102_read_fifo();
if (g_fft_index < FFT_N)
{
s1[g_fft_index].real = fifo_red; // 红光
s2[g_fft_index].real = fifo_ir; // 红外光
g_fft_index++;
}
}
}
}

void blood_data_translate(void)
{
// 1. 去直流分量
// 2. 移动平均滤波
// 3. FFT 变换
FFT(s1);
FFT(s2);

// 4. 求幅值并累加 AC 分量
for (i = 1; i < FFT_N; i++)
{
ac_red += s1[i].real;
ac_ir += s2[i].real;
}

// 5. 找最大谱线索引,换算为心率
s1_max_index = find_max_num_index(s1, 30);
Heart_Rate = 60.00 * ((100.0 * s1_max_index) / 512.00);
g_blooddata.heart = Heart_Rate;

// 6. 根据 R 值计算血氧
R = (ac_ir * dc_red) / (ac_red * dc_ir);
sp02_num = -45.060 * R * R + 30.354 * R + 94.845;
g_blooddata.SpO2 = sp02_num;
}

心率和血氧结果会分别更新到全局变量 heart_shujuspo2_shuju,供显示和上传使用。

4.3 GY906 体温采集

GY906(MLX90614)通过 SMBus 协议读取人体表面温度,代码中使用软件模拟时序:

// 初始化 PB10/PB11 为开漏输出
void Mlx96014_Configuration(void)
{
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; // SCL
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_Init(GPIOB, &GPIO_InitStructure);

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // SDA
GPIO_Init(GPIOB, &GPIO_InitStructure);
SDA_H; SCL_H;
}

// 在 TIM1 中断中每 10ms 读取一次,500ms 取平均
people_temp = temp_zong / 50;

4.4 MSP20 血压采集

MSP20 是压力传感器,输出模拟电压与压力成比例。代码使用 ADC1 通道 0 + DMA 循环采集:

void Init_adc(void)
{
// PA0 模拟输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);

// DMA1 通道 1 循环传输 1 个半字
DMA_InitStructure.DMA_BufferSize = 1;
DMA_Init(DMA1_Channel1, &DMA_InitStructure);

// ADC1 连续转换
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5);
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

在 TIM1 中断中,把 ADC 值累加后换算为 kPa:

ADC_ConvertedValueLocal = (float)ADC_ConvertedValue[0] / 4096 * 3.3f;
msp_zong += ADC_ConvertedValueLocal;
if (ADC_count >= 50) // 500ms 取平均
{
ADC_count = 0;
msp20_data = (float)(msp_zong / 165 * 100);
msp_zong = 0;
}

4.5 GPS 经纬度解析

USART1 接收 ATGM336H 的 $GNRMC 语句,解析出 UTC 时间、纬度、经度等信息:

void USART1_IRQHandler(void)
{
Res = USART_ReceiveData(USART1);
if (Res == '$') point1 = 0;
USART_RX_BUF[point1++] = Res;

if (USART_RX_BUF[0] == '$' && USART_RX_BUF[4] == 'M' && USART_RX_BUF[5] == 'C')
{
if (Res == '\n')
{
memcpy(Save_Data.GPS_Buffer, USART_RX_BUF, point1);
Save_Data.isGetData = true;
point1 = 0;
}
}
}

主循环中调用 parseGpsBuffer() 拆分逗号字段,并把度分格式转换为十进制度:

if (Save_Data.longitude[5] == '.')
jingdu = hextoDec(...) * 100 + ... + (...)/60;

4.6 阿里云 MQTT 数据上传

ESP8266 通过 AT 指令连接 WiFi 和阿里云 MQTT 服务器,然后使用自定义的 MQTT 协议栈封装数据:

// 阿里云三元组
#define PRODUCTKEY "a1AAS8HI34v"
#define DEVICENAME "Device"
#define DEVICESECRE "16dda89e8b35e1c6a6b0ff42249caf85"
#define P_TOPIC_NAME "/sys/a1AAS8HI34v/Device/thing/event/property/post"

连接成功后,TIM2 定时器每 10 秒触发一次,交替上报两组数据:

void TIM2_IRQHandler(void)
{
static u8 i = 0;
i++;
if (i == 3) i = 1;

if (i == 1)
{
sprintf(temp, "{\"params\":{\"heart\":%d,\"spo2\":%0.2f,\"temp\":%0.1f,\"msp\":%0.1f}}",
heart_shuju, spo2_shuju, people_temp, msp20_data);
MQTT_PublishQs0(P_TOPIC_NAME, temp, strlen(temp));
}
else if (i == 2)
{
sprintf(temp, "{\"params\":{\"jing\":%0.2f,\"wei\":%0.2f,\"bao\":%d}}",
jingdu, weidu, baojing_status);
MQTT_PublishQs0(P_TOPIC_NAME, temp, strlen(temp));
}
}

TIM4 每 30ms 运行一次 MQTT 状态机,负责 CONNECT、SUBSCRIBE、PING 保活和重连。

4.7 TIM1 中断:按键 + 显示 + 报警

TIM1 每 10ms 进入一次更新中断,完成所有周期性任务:

void TIM1_UP_IRQHandler(void)
{
// 每 50ms 扫描一次按键
if (KEY_count >= 5)
{
KEY_count = 0;
key = KEY_Scan(0);
if (key == SET_PRES) // 切换显示页面
{
show_mode = (show_mode + 1) % 6;
// page_one ~ page_six
}
else if (key == CHOICE_PRES) // 切换粗调/细调
{
if (show_mode != 0) tiaozheng_status = !tiaozheng_status;
else ceshi_status = !ceshi_status;
}
else if (key == ADD_PRES) // 阈值加
{
if (show_mode == 2) heart_yuzhi += (tiaozheng_status ? 10 : 1);
else if (show_mode == 3) spo2_yuzhi += (tiaozheng_status ? 1.0 : 0.1);
else if (show_mode == 4) temp_yuzhi += (tiaozheng_status ? 1.0 : 0.1);
else if (show_mode == 5) msp20_yuzhi += (tiaozheng_status ? 1.0 : 0.1);
}
else if (key == REDUCE_PRES) // 阈值减
{
// 同理递减
}
}

// GY906 温度累加,500ms 平均
temp_zong += SMBus_ReadTemp();
if (GPY96_count >= 50) { people_temp = temp_zong / 50; temp_zong = 0; }

// MSP20 血压累加,500ms 平均
ADC_ConvertedValueLocal = (float)ADC_ConvertedValue[0] / 4096 * 3.3f;
msp_zong += ADC_ConvertedValueLocal;
if (ADC_count >= 50) { msp20_data = msp_zong / 165 * 100; msp_zong = 0; }

// 报警判断
if (heart_shuju > heart_yuzhi || spo2_shuju < spo2_yuzhi ||
people_temp > temp_yuzhi || msp20_data > msp20_yuzhi)
baojing_status = 1;
else
baojing_status = 0;

if (baojing_status) beep = 1, led = 1;
else beep = 0, led = 0;

// 500ms 刷新 OLED
if (MESSAGE_count >= 50)
{
if (show_mode == 0)
{
OLED_ShowNum(88, 0, heart_shuju, 3, 16);
OLED_ShowNum(72, 2, spo2_shuju, 3, 16);
OLED_ShowNum(104, 2, (u32)(spo2_shuju * 100) % 100, 2, 16);
OLED_ShowNum(72, 4, people_temp, 3, 16);
OLED_ShowNum(104, 4, (int)(people_temp * 10) % 10, 1, 16);
OLED_ShowNum(72, 6, msp20_data, 2, 16);
OLED_ShowNum(96, 6, (int)(msp20_data * 10) % 10, 1, 16);
}
// page_two ~ page_six 同理
}
}

显示页面设计:

  • page_one:首页,显示当前心率、血氧、体温、血压。
  • page_two:GPS 页面,显示经度、纬度和报警状态。
  • page_three~six:分别设置心率阈值、血氧阈值、体温阈值、血压阈值。

五、实物展示

实物图

实物板采用绿色 PCB,中间是 STM32F103C8T6 最小系统板,上方为 OLED 显示屏,左侧为 ESP8266 WiFi 模块和 MAX30102 传感器,下方为 4 个独立按键,右侧为蜂鸣器和电源接口。OLED 首页可看到心率、血氧、体温、血压四项数据。

实物图2

测试时可以通过手指按压 MAX30102 模拟心率血氧采集,同时用注射器对 MSP20 压力传感器加压,观察血压数值变化。

六、功能特点

功能 说明
四体征监测 心率、血氧、体温、血压同时采集
GPS 定位 ATGM336H 实时获取经纬度
阿里云上云 ESP8266 + MQTT 周期性上报健康/位置数据
本地显示 0.96 寸 OLED 六页菜单
声光报警 超阈值时蜂鸣器 + LED 响应
阈值可调 4 按键设置心率/血氧/体温/血压阈值
FFT 算法 MAX30102 采用 FFT 频谱法计算心率血氧
多 IIC 设备 MAX30102、GY906、OLED 分时复用不同 IIC 引脚

七、项目总结

这个项目把嵌入式系统常见的几类技术都串起来了:

  • 传感器采集:PPG 心率血氧、红外测温、压力传感、GPS NMEA 解析。
  • 模拟量采集:MSP20 通过 ADC + DMA 实现连续采样。
  • 数字信号处理:FFT 提取脉搏波频谱,计算心率和血氧。
  • 多总线协议:三路 IIC/SMBus 分别驱动 MAX30102、GY906、OLED。
  • 无线通信:ESP8266 AT 指令 + MQTT 协议栈接入阿里云。
  • 人机交互:OLED 菜单 + 独立按键 + 蜂鸣器/LED 报警。

对于想做一个“能穿戴、能显示、能联网”的健康监测类项目的同学,这套方案可以直接复刻参考。

资料下载

完整资料包含:

  • STM32 工程源码(Keil 标准库工程)
  • 原理图 PDF
  • 嘉立创 EDA 工程文件
  • 测试视频
  • 硬件清单 BOM

资料链接见博客侧边栏头像下方的 资料链接(下载图标)处。