云计算百科
云计算领域专业知识百科平台

场景实战:基于STM32的光电式心率监测仪与报警系统解决方案解析

文章目录

    • 一、方案整体设计与原理说明
      • 1.1 核心原理
      • 1.2 整体架构流程图
    • 二、硬件选型与接线
      • 2.1 核心硬件清单
      • 2.2 详细接线说明
        • (1)STM32与MAX30102心率传感器接线
        • (2)STM32与OLED显示屏接线
        • (3)STM32与报警模块接线
    • 三、软件开发环境搭建
      • 3.1 环境准备
      • 3.2 STM32CubeMX初始化配置步骤
        • 步骤1:新建工程
        • 步骤2:配置RCC时钟
        • 步骤3:配置I2C1(用于传感器和OLED通信)
        • 步骤4:配置GPIO引脚(报警模块)
        • 步骤5:配置定时器(用于定时采样和LED闪烁)
        • 步骤6:生成工程代码
    • 四、核心代码编写
      • 4.1 头文件与全局变量定义(main.h)
      • 4.2 外设初始化代码(main.c)
      • 4.3 MAX30102传感器驱动函数(main.c续)
      • 4.4 心率计算与滤波函数(main.c续)
      • 4.5 OLED显示驱动函数(main.c续)
      • 4.6 报警控制与定时中断函数(main.c续)
      • 4.7 主函数(main.c续)
    • 五、代码烧录与调试
      • 5.1 代码编译与烧录
      • 5.2 硬件调试步骤
        • 步骤1:电源调试
        • 步骤2:传感器调试
        • 步骤3:心率计算调试
        • 步骤4:报警功能调试
      • 5.3 常见问题与解决
    • 六、功能扩展建议
    • 总结

一、方案整体设计与原理说明

你需要实现的是基于STM32的光电式心率监测仪与报警系统,核心功能包含实时心率采集计算和心率异常报警两大模块。本方案以STM32F103C8T6最小系统板为核心控制器,采用MAX30102光电心率传感器采集PPG(光电容积脉搏波)信号,通过数字信号处理算法计算实时心率,当心率超出预设上下阈值时,触发蜂鸣器+LED声光报警,同时通过OLED屏幕实时显示心率数值和状态。整个方案采用模块化编程,步骤拆解细致,零基础小白可完全复刻落地。

1.1 核心原理

  • 光电式心率监测原理:MAX30102传感器内置红外LED和光电探测器,红外光照射皮肤后,血液容积随心脏搏动变化,反射光强度也随之变化,探测器将光信号转换为电信号,传感器内部完成AD转换后通过I2C总线将数字信号传输给STM32;STM32对采集到的PPG原始数据进行滤波、峰值检测等处理,计算出每分钟心率(BPM)。
  • 报警系统原理:STM32实时对比计算出的心率值与预设的心率上下阈值(如下限50BPM、上限120BPM),当心率超出阈值范围时,立即控制GPIO口输出高低电平,驱动蜂鸣器发声、LED闪烁,同时OLED屏幕显示“心率异常”报警提示。
  • 数据显示原理:STM32通过I2C总线与0.96寸OLED屏幕通信,实时刷新显示当前心率数值、心率状态(正常/异常)、采样进度等信息。

1.2 整体架构流程图

以下是系统完整工作流程的Mermaid流程图,采用深色背景、白色字体,排版美观且逻辑层级清晰:

#mermaid-svg-75X31mHVD0RD0ajC{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-75X31mHVD0RD0ajC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-75X31mHVD0RD0ajC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-75X31mHVD0RD0ajC .error-icon{fill:#552222;}#mermaid-svg-75X31mHVD0RD0ajC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-75X31mHVD0RD0ajC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-75X31mHVD0RD0ajC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-75X31mHVD0RD0ajC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-75X31mHVD0RD0ajC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-75X31mHVD0RD0ajC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-75X31mHVD0RD0ajC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-75X31mHVD0RD0ajC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-75X31mHVD0RD0ajC .marker.cross{stroke:#333333;}#mermaid-svg-75X31mHVD0RD0ajC svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-75X31mHVD0RD0ajC p{margin:0;}#mermaid-svg-75X31mHVD0RD0ajC .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-75X31mHVD0RD0ajC .cluster-label text{fill:#333;}#mermaid-svg-75X31mHVD0RD0ajC .cluster-label span{color:#333;}#mermaid-svg-75X31mHVD0RD0ajC .cluster-label span p{background-color:transparent;}#mermaid-svg-75X31mHVD0RD0ajC .label text,#mermaid-svg-75X31mHVD0RD0ajC span{fill:#333;color:#333;}#mermaid-svg-75X31mHVD0RD0ajC .node rect,#mermaid-svg-75X31mHVD0RD0ajC .node circle,#mermaid-svg-75X31mHVD0RD0ajC .node ellipse,#mermaid-svg-75X31mHVD0RD0ajC .node polygon,#mermaid-svg-75X31mHVD0RD0ajC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-75X31mHVD0RD0ajC .rough-node .label text,#mermaid-svg-75X31mHVD0RD0ajC .node .label text,#mermaid-svg-75X31mHVD0RD0ajC .image-shape .label,#mermaid-svg-75X31mHVD0RD0ajC .icon-shape .label{text-anchor:middle;}#mermaid-svg-75X31mHVD0RD0ajC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-75X31mHVD0RD0ajC .rough-node .label,#mermaid-svg-75X31mHVD0RD0ajC .node .label,#mermaid-svg-75X31mHVD0RD0ajC .image-shape .label,#mermaid-svg-75X31mHVD0RD0ajC .icon-shape .label{text-align:center;}#mermaid-svg-75X31mHVD0RD0ajC .node.clickable{cursor:pointer;}#mermaid-svg-75X31mHVD0RD0ajC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-75X31mHVD0RD0ajC .arrowheadPath{fill:#333333;}#mermaid-svg-75X31mHVD0RD0ajC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-75X31mHVD0RD0ajC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-75X31mHVD0RD0ajC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-75X31mHVD0RD0ajC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-75X31mHVD0RD0ajC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-75X31mHVD0RD0ajC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-75X31mHVD0RD0ajC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-75X31mHVD0RD0ajC .cluster text{fill:#333;}#mermaid-svg-75X31mHVD0RD0ajC .cluster span{color:#333;}#mermaid-svg-75X31mHVD0RD0ajC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-75X31mHVD0RD0ajC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-75X31mHVD0RD0ajC rect.text{fill:none;stroke-width:0;}#mermaid-svg-75X31mHVD0RD0ajC .icon-shape,#mermaid-svg-75X31mHVD0RD0ajC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-75X31mHVD0RD0ajC .icon-shape p,#mermaid-svg-75X31mHVD0RD0ajC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-75X31mHVD0RD0ajC .icon-shape rect,#mermaid-svg-75X31mHVD0RD0ajC .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-75X31mHVD0RD0ajC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-75X31mHVD0RD0ajC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-75X31mHVD0RD0ajC :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-75X31mHVD0RD0ajC .darkStyle>*{fill:#2c3e50!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-75X31mHVD0RD0ajC .darkStyle span{fill:#2c3e50!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-75X31mHVD0RD0ajC .darkStyle tspan{fill:#ffffff!important;}#mermaid-svg-75X31mHVD0RD0ajC .startEnd>*{fill:#e74c3c!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-75X31mHVD0RD0ajC .startEnd span{fill:#e74c3c!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-75X31mHVD0RD0ajC .startEnd tspan{fill:#ffffff!important;}#mermaid-svg-75X31mHVD0RD0ajC .decision>*{fill:#3498db!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-75X31mHVD0RD0ajC .decision span{fill:#3498db!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-75X31mHVD0RD0ajC .decision tspan{fill:#ffffff!important;}#mermaid-svg-75X31mHVD0RD0ajC .process>*{fill:#27ae60!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-75X31mHVD0RD0ajC .process span{fill:#27ae60!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-75X31mHVD0RD0ajC .process tspan{fill:#ffffff!important;}#mermaid-svg-75X31mHVD0RD0ajC .alarm>*{fill:#f39c12!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-75X31mHVD0RD0ajC .alarm span{fill:#f39c12!important;stroke:#ffffff!important;stroke-width:2px!important;color:#ffffff!important;fontSize:14px!important;fontFamily:Arial!important;}#mermaid-svg-75X31mHVD0RD0ajC .alarm tspan{fill:#ffffff!important;}

系统上电初始化

GPIO/I2C/定时器/中断配置

初始化MAX30102传感器

初始化OLED显示屏

清空OLED缓存,显示初始界面

通过I2C读取MAX30102原始PPG数据

对原始数据进行滤波处理(去除噪声)

峰值检测算法提取脉搏特征点

计算实时心率(BPM)

心率是否在阈值范围内?

OLED显示“心率正常”+当前BPM值

触发声光报警(蜂鸣器响+LED闪烁)

OLED显示“心率异常”+当前BPM值+阈值提示

系统是否持续运行?

系统停止

二、硬件选型与接线

2.1 核心硬件清单

硬件名称型号/规格数量作用
STM32控制器 STM32F103C8T6最小系统板 1 核心控制与数据处理
光电心率传感器 MAX30102模块(I2C版) 1 采集PPG脉搏波信号
OLED显示屏 0.96寸I2C接口(128*64) 1 实时显示心率与状态
有源蜂鸣器模块 低电平触发型 1 异常报警发声
LED指示灯 红色/绿色(含限流电阻) 各1 状态指示(绿=正常,红=报警)
电源模块 5V USB供电模块 1 给整个系统供电
杜邦线 公对公/公对母 若干 硬件接线
ST-Link下载器 V2版 1 代码烧录与调试

2.2 详细接线说明

(1)STM32与MAX30102心率传感器接线

MAX30102采用I2C通信,默认I2C地址为0xAE(写)/0xAF(读),接线如下:

STM32引脚MAX30102引脚说明
PB6 SCL I2C时钟线
PB7 SDA I2C数据线
3.3V VCC 传感器供电(必须3.3V)
GND GND 共地
(2)STM32与OLED显示屏接线

OLED同样采用I2C通信,接线与MAX30102复用I2C总线(节省引脚):

STM32引脚OLED引脚说明
PB6 SCL I2C时钟线(与传感器复用)
PB7 SDA I2C数据线(与传感器复用)
3.3V VCC OLED供电
GND GND 共地
(3)STM32与报警模块接线
STM32引脚外设引脚说明
PA0 有源蜂鸣器IN端 低电平触发蜂鸣器报警
PA1 绿色LED正极 正常状态常亮(串220Ω电阻)
PA2 红色LED正极 异常状态闪烁(串220Ω电阻)
GND 蜂鸣器/LED负极 共地

三、软件开发环境搭建

3.1 环境准备

  • STM32CubeMX:下载V6.0及以上版本(官网:https://www.st.com/en/development-tools/stm32cubemx.html),用于图形化配置STM32外设,自动生成初始化代码。
  • Keil MDK-ARM:下载V5.36及以上版本,安装STM32F103C8T6对应的Device Pack(在Keil中通过“Pack Installer”安装),用于代码编写、编译和烧录。
  • ST-Link驱动:安装ST-Link V2驱动,确保电脑能识别下载器。
  • 辅助工具:串口调试助手(可选,用于调试输出)、万用表(检查接线和供电)。
  • 3.2 STM32CubeMX初始化配置步骤

    步骤1:新建工程
    • 打开STM32CubeMX,点击“New Project”,在搜索框输入“STM32F103C8T6”,选择对应型号后点击“Start Project”。
    • 弹出“Project Manager”提示框,点击“OK”跳过。
    步骤2:配置RCC时钟
    • 左侧菜单栏选择“RCC”,在“High Speed Clock (HSE)”中选择“Crystal/Ceramic Resonator”(外部晶振),启用外部8MHz晶振。
    • 点击顶部“Clock Configuration”,将系统时钟(SYSCLK)配置为72MHz:
      • HSE = 8MHz
      • PLLMUL = 9(8*9=72MHz)
      • AHB Prescaler = 1(HCLK=72MHz)
      • APB1 Prescaler = 2(PCLK1=36MHz)
      • APB2 Prescaler = 1(PCLK2=72MHz)
    步骤3:配置I2C1(用于传感器和OLED通信)
    • 左侧菜单栏选择“I2C1”:
      • 模式选择“I2C Master Mode”(主机模式);
      • 配置参数:
        • Clock Speed(SCL时钟频率):100kHz(标准模式,兼容传感器和OLED);
        • Addressing Mode:7-bit(7位地址模式);
        • 其余参数默认。
    • 引脚映射:PB6(I2C1_SCL)、PB7(I2C1_SDA),确认引脚复用功能正确。
    步骤4:配置GPIO引脚(报警模块)
    • 左侧菜单栏选择“GPIO”:
      • PA0:设置为“Output Push Pull”(推挽输出),命名为“BUZZER”,初始电平“High”(蜂鸣器默认不响);
      • PA1:设置为“Output Push Pull”,命名为“LED_GREEN”,初始电平“Low”(默认熄灭,正常后常亮);
      • PA2:设置为“Output Push Pull”,命名为“LED_RED”,初始电平“Low”(默认熄灭,异常后闪烁);
      • 所有GPIO引脚速度设置为“High”,无上拉/下拉。
    步骤5:配置定时器(用于定时采样和LED闪烁)
    • 左侧菜单栏选择“TIM2”:
      • 模式选择“Up Counter”(向上计数);
      • 预分频器(Prescaler):71(72MHz/72=1MHz,计数频率1MHz);
      • 自动重装值(ARR):9999(1MHz/10000=100Hz,定时10ms);
      • 启用“Auto-reload preload”;
      • 开启TIM2中断(NVIC Settings中勾选TIM2 global interrupt,优先级设为1)。
    步骤6:生成工程代码
    • 点击顶部“Project Manager”:
      • Project Name:设置为“HeartRate_Monitor”;
      • Project Path:选择非中文路径(如D:\\STM32_Projects\\HeartRate_Monitor);
      • Toolchain/IDE:选择“MDK-ARM”,版本“V5”;
      • 点击“Code Generator”,勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”(按外设分文件生成代码);
      • 点击“Generate Code”,生成完成后点击“Open Project”直接打开Keil工程。

    四、核心代码编写

    4.1 头文件与全局变量定义(main.h)

    #ifndef __MAIN_H
    #define __MAIN_H

    #include "stm32f1xx_hal.h"

    /* 引脚宏定义 */
    #define BUZZER_PIN GPIO_PIN_0
    #define BUZZER_GPIO_PORT GPIOA
    #define LED_GREEN_PIN GPIO_PIN_1
    #define LED_GREEN_GPIO_PORT GPIOA
    #define LED_RED_PIN GPIO_PIN_2
    #define LED_RED_GPIO_PORT GPIOA

    /* 心率阈值定义(可根据需求调整) */
    #define HEART_RATE_LOW_THRESHOLD 50 // 心率下限(BPM)
    #define HEART_RATE_HIGH_THRESHOLD 120 // 心率上限(BPM)

    /* MAX30102相关宏定义 */
    #define MAX30102_I2C_ADDR 0xAE // I2C写地址(读地址0xAF)
    #define MAX30102_REG_INT_STATUS1 0x00 // 中断状态寄存器1
    #define MAX30102_REG_INT_ENABLE1 0x01 // 中断使能寄存器1
    #define MAX30102_REG_FIFO_WR_PTR 0x02 // FIFO写指针
    #define MAX30102_REG_OVF_COUNTER 0x03 // 溢出计数器
    #define MAX30102_REG_FIFO_RD_PTR 0x04 // FIFO读指针
    #define MAX30102_REG_FIFO_DATA 0x05 // FIFO数据寄存器
    #define MAX30102_REG_MODE_CONFIG 0x06 // 模式配置寄存器
    #define MAX30102_REG_SPO2_CONFIG 0x07 // 血氧/心率配置寄存器
    #define MAX30102_REG_LED1_PA 0x09 // LED1(红外)功率
    #define MAX30102_REG_LED2_PA 0x0A // LED2(红光)功率

    /* 全局变量 */
    extern uint32_t heart_rate; // 实时心率值(BPM)
    extern uint8_t heart_rate_status; // 心率状态:0=正常,1=过低,2=过高
    extern uint16_t ppg_raw_data[100]; // 存储PPG原始数据(环形缓冲区)
    extern uint8_t ppg_data_index; // 数据缓冲区索引
    extern uint8_t alarm_flag; // 报警标志:0=无报警,1=报警

    /* 函数声明 */
    void SystemClock_Config(void);
    static void MX_GPIO_Init(void);
    static void MX_I2C1_Init(void);
    static void MX_TIM2_Init(void);

    // MAX30102传感器相关函数
    void MAX30102_Init(void);
    void MAX30102_WriteReg(uint8_t reg_addr, uint8_t data);
    uint8_t MAX30102_ReadReg(uint8_t reg_addr);
    void MAX30102_ReadFIFO(uint16_t *ir_data);

    // 心率计算相关函数
    void PPG_Data_Filter(uint16_t raw_data, uint16_t *filtered_data);
    uint32_t Calculate_HeartRate(uint16_t *filtered_data, uint8_t data_len);

    // OLED显示相关函数
    void OLED_Init(void);
    void OLED_Clear(void);
    void OLED_ShowString(uint8_t x, uint8_t y, uint8_t *str);
    void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len);
    void OLED_ShowHeartRate(uint32_t bpm, uint8_t status);

    // 报警控制函数
    void Alarm_Control(uint8_t status);

    #endif /* __MAIN_H */

    4.2 外设初始化代码(main.c)

    #include "main.h"

    /* 全局变量定义 */
    uint32_t heart_rate = 0;
    uint8_t heart_rate_status = 0;
    uint16_t ppg_raw_data[100] = {0};
    uint8_t ppg_data_index = 0;
    uint8_t alarm_flag = 0;

    /* 外设句柄 */
    I2C_HandleTypeDef hi2c1;
    TIM_HandleTypeDef htim2;

    /* 系统时钟配置函数 */
    void SystemClock_Config(void)
    {
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

    /* 配置外部晶振HSE */
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;
    RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
    {
    Error_Handler();
    }

    /* 配置系统时钟总线 */
    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
    |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
    {
    Error_Handler();
    }
    }

    /* I2C1初始化函数 */
    static void MX_I2C1_Init(void)
    {
    hi2c1.Instance = I2C1;
    hi2c1.Init.ClockSpeed = 100000; // 100kHz标准模式
    hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; // 50%占空比
    hi2c1.Init.OwnAddress1 = 0;
    hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
    hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
    hi2c1.Init.OwnAddress2 = 0;
    hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
    hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
    if (HAL_I2C_Init(&hi2c1) != HAL_OK)
    {
    Error_Handler();
    }
    }

    /* TIM2初始化函数(10ms定时中断) */
    static void MX_TIM2_Init(void)
    {
    TIM_ClockConfigTypeDef sClockSourceConfig = {0};
    TIM_MasterConfigTypeDef sMasterConfig = {0};

    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 71; // 预分频器71,计数频率1MHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 9999; // 自动重装值9999,定时10ms
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
    {
    Error_Handler();
    }

    sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
    if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK)
    {
    Error_Handler();
    }

    sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
    sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
    if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK)
    {
    Error_Handler();
    }

    // 启动TIM2中断
    HAL_TIM_Base_Start_IT(&htim2);
    }

    /* GPIO初始化函数 */
    static void MX_GPIO_Init(void)
    {
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    /* 使能GPIOA时钟 */
    __HAL_RCC_GPIOA_CLK_ENABLE();
    /* 使能GPIOB时钟(I2C引脚) */
    __HAL_RCC_GPIOB_CLK_ENABLE();

    /* 配置报警模块引脚 */
    GPIO_InitStruct.Pin = BUZZER_PIN|LED_GREEN_PIN|LED_RED_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* 初始状态:蜂鸣器关闭,LED熄灭 */
    HAL_GPIO_WritePin(BUZZER_GPIO_PORT, BUZZER_PIN, GPIO_PIN_SET);
    HAL_GPIO_WritePin(LED_GREEN_GPIO_PORT, LED_GREEN_PIN, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(LED_RED_GPIO_PORT, LED_RED_PIN, GPIO_PIN_RESET);
    }

    /* 错误处理函数(死循环) */
    void Error_Handler(void)
    {
    __disable_irq();
    while (1)
    {
    // 出错时红色LED快速闪烁(100ms一次)
    HAL_GPIO_TogglePin(LED_RED_GPIO_PORT, LED_RED_PIN);
    HAL_Delay(100);
    }
    }

    #ifdef USE_FULL_ASSERT
    void assert_failed(uint8_t *file, uint32_t line)
    {
    // 断言失败处理(可选添加串口打印)
    }
    #endif /* USE_FULL_ASSERT */

    4.3 MAX30102传感器驱动函数(main.c续)

    /* MAX30102写寄存器函数 */
    void MAX30102_WriteReg(uint8_t reg_addr, uint8_t data)
    {
    uint8_t tx_data[2] = {reg_addr, data};
    // I2C写操作:地址+寄存器+数据
    HAL_I2C_Master_Transmit(&hi2c1, MAX30102_I2C_ADDR, tx_data, 2, 100);
    }

    /* MAX30102读寄存器函数 */
    uint8_t MAX30102_ReadReg(uint8_t reg_addr)
    {
    uint8_t rx_data = 0;
    // 先发送寄存器地址
    HAL_I2C_Master_Transmit(&hi2c1, MAX30102_I2C_ADDR, &reg_addr, 1, 100);
    // 再读取寄存器数据
    HAL_I2C_Master_Receive(&hi2c1, MAX30102_I2C_ADDR+1, &rx_data, 1, 100);
    return rx_data;
    }

    /* MAX30102初始化函数(配置为心率监测模式) */
    void MAX30102_Init(void)
    {
    // 1. 软复位传感器
    MAX30102_WriteReg(MAX30102_REG_MODE_CONFIG, 0x40);
    HAL_Delay(100);

    // 2. 关闭所有中断
    MAX30102_WriteReg(MAX30102_REG_INT_ENABLE1, 0x00);

    // 3. 配置FIFO指针(写指针、读指针、溢出计数器清零)
    MAX30102_WriteReg(MAX30102_REG_FIFO_WR_PTR, 0x00);
    MAX30102_WriteReg(MAX30102_REG_OVF_COUNTER, 0x00);
    MAX30102_WriteReg(MAX30102_REG_FIFO_RD_PTR, 0x00);

    // 4. 配置模式:心率监测模式(仅启用红外LED)
    MAX30102_WriteReg(MAX30102_REG_MODE_CONFIG, 0x02);

    // 5. 配置采样率和分辨率:100Hz采样率,18位分辨率
    MAX30102_WriteReg(MAX30102_REG_SPO2_CONFIG, 0x27);

    // 6. 设置红外LED功率(中等功率,避免过亮)
    MAX30102_WriteReg(MAX30102_REG_LED1_PA, 0x1F);
    MAX30102_WriteReg(MAX30102_REG_LED2_PA, 0x00); // 关闭红光LED

    HAL_Delay(200); // 等待传感器稳定
    }

    /* 读取MAX30102 FIFO中的PPG数据(仅红外通道) */
    void MAX30102_ReadFIFO(uint16_t *ir_data)
    {
    uint8_t fifo_data[3] = {0};
    uint8_t rd_ptr = MAX30102_ReadReg(MAX30102_REG_FIFO_RD_PTR);
    uint8_t wr_ptr = MAX30102_ReadReg(MAX30102_REG_FIFO_WR_PTR);

    // 仅当FIFO有数据时读取
    if(wr_ptr != rd_ptr)
    {
    // 读取FIFO数据寄存器(3字节,18位数据)
    HAL_I2C_Master_Transmit(&hi2c1, MAX30102_I2C_ADDR, &MAX30102_REG_FIFO_DATA, 1, 100);
    HAL_I2C_Master_Receive(&hi2c1, MAX30102_I2C_ADDR+1, fifo_data, 3, 100);

    // 解析18位数据(仅取高16位,低2位舍去)
    *ir_data = ((uint16_t)fifo_data[0] << 8) | fifo_data[1];

    // 移动读指针
    MAX30102_WriteReg(MAX30102_REG_FIFO_RD_PTR, rd_ptr+1);
    }
    else
    {
    *ir_data = 0;
    }
    }

    4.4 心率计算与滤波函数(main.c续)

    /* PPG原始数据滤波(滑动平均滤波,去除高频噪声) */
    void PPG_Data_Filter(uint16_t raw_data, uint16_t *filtered_data)
    {
    static uint16_t filter_buf[5] = {0}; // 5点滑动窗口
    static uint8_t filter_index = 0;
    uint32_t sum = 0;

    // 将新数据存入滤波缓冲区
    filter_buf[filter_index] = raw_data;
    filter_index = (filter_index + 1) % 5;

    // 计算平均值
    for(uint8_t i=0; i<5; i++)
    {
    sum += filter_buf[i];
    }
    *filtered_data = sum / 5;
    }

    /* 心率计算函数(峰值检测法) */
    uint32_t Calculate_HeartRate(uint16_t *filtered_data, uint8_t data_len)
    {
    uint8_t peak_count = 0; // 峰值数量
    uint16_t threshold = 0; // 峰值检测阈值
    uint32_t total_interval = 0; // 总峰间间隔
    uint8_t last_peak_index = 0; // 上一个峰值索引

    // 1. 计算数据平均值作为阈值(自适应阈值)
    uint32_t sum = 0;
    for(uint8_t i=0; i<data_len; i++)
    {
    sum += filtered_data[i];
    }
    threshold = sum / data_len + 50; // 阈值=平均值+50(避免误检测)

    // 2. 峰值检测(当前点>阈值,且大于前后点)
    for(uint8_t i=1; i<data_len1; i++)
    {
    if((filtered_data[i] > threshold) &&
    (filtered_data[i] > filtered_data[i1]) &&
    (filtered_data[i] > filtered_data[i+1]))
    {
    peak_count++;
    if(peak_count > 1)
    {
    // 计算峰间间隔(单位:10ms,因为采样间隔10ms)
    total_interval += (i last_peak_index) * 10;
    }
    last_peak_index = i;
    }
    }

    // 3. 计算心率(BPM = 60000 / 平均峰间间隔)
    if(peak_count >= 2)
    {
    uint32_t avg_interval = total_interval / (peak_count 1);
    return 60000 / avg_interval;
    }
    else
    {
    return 0; // 峰值不足,无法计算
    }
    }

    4.5 OLED显示驱动函数(main.c续)

    /* OLED写命令函数 */
    void OLED_WriteCmd(uint8_t cmd)
    {
    uint8_t tx_data[2] = {0x00, cmd}; // 0x00=命令标志
    HAL_I2C_Master_Transmit(&hi2c1, 0x78, tx_data, 2, 100);
    }

    /* OLED写数据函数 */
    void OLED_WriteData(uint8_t data)
    {
    uint8_t tx_data[2] = {0x40, data}; // 0x40=数据标志
    HAL_I2C_Master_Transmit(&hi2c1, 0x78, tx_data, 2, 100);
    }

    /* OLED初始化函数 */
    void OLED_Init(void)
    {
    HAL_Delay(100); // 上电延时

    OLED_WriteCmd(0xAE); // 关闭显示
    OLED_WriteCmd(0x00); // 设置列起始地址低4位
    OLED_WriteCmd(0x10); // 设置列起始地址高4位
    OLED_WriteCmd(0x40); // 设置行起始地址
    OLED_WriteCmd(0xB0); // 设置页地址
    OLED_WriteCmd(0x81); // 对比度设置
    OLED_WriteCmd(0xFF); // 最大对比度
    OLED_WriteCmd(0xA1); // 段重映射(正常显示)
    OLED_WriteCmd(0xA6); // 正常显示(非反色)
    OLED_WriteCmd(0xA8); // 多路复用率
    OLED_WriteCmd(0x3F); // 64行
    OLED_WriteCmd(0xC8); // COM扫描方向(反向)
    OLED_WriteCmd(0xD3); // 显示偏移
    OLED_WriteCmd(0x00); // 无偏移
    OLED_WriteCmd(0xD5); // 时钟分频
    OLED_WriteCmd(0x80); // 默认值
    OLED_WriteCmd(0xD9); // 预充电周期
    OLED_WriteCmd(0xF1); // 增强对比度
    OLED_WriteCmd(0xDA); // COM引脚配置
    OLED_WriteCmd(0x12);
    OLED_WriteCmd(0xDB); // VCOMH电压
    OLED_WriteCmd(0x40);
    OLED_WriteCmd(0x8D); // 电荷泵使能
    OLED_WriteCmd(0x14); // 使能
    OLED_WriteCmd(0xAF); // 开启显示

    OLED_Clear(); // 清屏
    }

    /* OLED清屏函数 */
    void OLED_Clear(void)
    {
    uint8_t i, j;
    for(i=0; i<8; i++)
    {
    OLED_WriteCmd(0xB0 + i); // 设置页地址
    OLED_WriteCmd(0x00); // 列低地址
    OLED_WriteCmd(0x10); // 列高地址
    for(j=0; j<128; j++)
    {
    OLED_WriteData(0x00); // 写空数据
    }
    }
    }

    /* OLED显示字符串函数(8*16字体) */
    void OLED_ShowString(uint8_t x, uint8_t y, uint8_t *str)
    {
    uint8_t i = 0;
    while(str[i] != '\\0')
    {
    // 字符偏移:ASCII码-32(适配8*16字库)
    uint8_t chr = str[i] 32;
    for(uint8_t j=0; j<8; j++)
    {
    // 定位:x列,y页
    OLED_WriteCmd(0xB0 + y);
    OLED_WriteCmd((x & 0x0F));
    OLED_WriteCmd(((x >> 4) & 0x0F) | 0x10);
    // 从字库取数据(此处简化,使用默认字库映射)
    // 实际需添加8*16字库数组,此处用测试数据演示
    OLED_WriteData(0xFF);
    x += 8; // 字符宽度8像素
    if(x >= 128)
    {
    x = 0;
    y++;
    }
    }
    i++;
    }
    }

    /* OLED显示数字函数 */
    void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len)
    {
    uint8_t buf[10] = {0};
    uint8_t i = 0;
    // 数字转字符串
    while(len)
    {
    buf[i++] = num % 10 + '0';
    num /= 10;
    }
    // 逆序显示
    for(uint8_t j=i1; j>=0; j)
    {
    OLED_ShowString(x, y, &buf[j]);
    x += 8;
    }
    }

    /* OLED显示心率信息函数 */
    void OLED_ShowHeartRate(uint32_t bpm, uint8_t status)
    {
    OLED_Clear(); // 清屏

    // 显示标题
    uint8_t title[] = "Heart Rate Monitor";
    OLED_ShowString(0, 0, title);

    // 显示心率数值
    uint8_t bpm_str[] = "BPM: ";
    OLED_ShowString(0, 2, bpm_str);
    OLED_ShowNum(32, 2, bpm, 3);

    // 显示心率状态
    if(status == 0)
    {
    uint8_t normal_str[] = "Status: Normal";
    OLED_ShowString(0, 4, normal_str);
    }
    else if(status == 1)
    {
    uint8_t low_str[] = "Status: Too Low";
    OLED_ShowString(0, 4, low_str);
    }
    else if(status == 2)
    {
    uint8_t high_str[] = "Status: Too High";
    OLED_ShowString(0, 4, high_str);
    }

    // 显示阈值提示
    uint8_t threshold_str[] = "Range: 50-120 BPM";
    OLED_ShowString(0, 6, threshold_str);
    }

    4.6 报警控制与定时中断函数(main.c续)

    /* 报警控制函数 */
    void Alarm_Control(uint8_t status)
    {
    switch(status)
    {
    case 0: // 正常:关闭蜂鸣器,绿灯常亮,红灯熄灭
    HAL_GPIO_WritePin(BUZZER_GPIO_PORT, BUZZER_PIN, GPIO_PIN_SET);
    HAL_GPIO_WritePin(LED_GREEN_GPIO_PORT, LED_GREEN_PIN, GPIO_PIN_SET);
    HAL_GPIO_WritePin(LED_RED_GPIO_PORT, LED_RED_PIN, GPIO_PIN_RESET);
    alarm_flag = 0;
    break;
    case 1: // 过低/过高:开启蜂鸣器,红灯闪烁,绿灯熄灭
    HAL_GPIO_WritePin(BUZZER_GPIO_PORT, BUZZER_PIN, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(LED_GREEN_GPIO_PORT, LED_GREEN_PIN, GPIO_PIN_RESET);
    // 红灯闪烁(由定时中断控制)
    alarm_flag = 1;
    break;
    default:
    Alarm_Control(0);
    break;
    }
    }

    /* TIM2定时中断回调函数(10ms一次) */
    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
    {
    static uint8_t blink_count = 0;
    uint16_t raw_data = 0;
    uint16_t filtered_data = 0;

    if(htim->Instance == TIM2)
    {
    // 1. 读取PPG原始数据
    MAX30102_ReadFIFO(&raw_data);

    // 2. 滤波处理
    if(raw_data > 0)
    {
    PPG_Data_Filter(raw_data, &filtered_data);
    // 存入环形缓冲区
    ppg_raw_data[ppg_data_index] = filtered_data;
    ppg_data_index = (ppg_data_index + 1) % 100;
    }

    // 3. 每1秒计算一次心率(100个采样点,10ms*100=1000ms)
    static uint8_t sample_count = 0;
    sample_count++;
    if(sample_count >= 100)
    {
    heart_rate = Calculate_HeartRate(ppg_raw_data, 100);
    sample_count = 0;

    // 4. 判断心率状态
    if(heart_rate < HEART_RATE_LOW_THRESHOLD && heart_rate > 0)
    {
    heart_rate_status = 1; // 过低
    Alarm_Control(1);
    }
    else if(heart_rate > HEART_RATE_HIGH_THRESHOLD)
    {
    heart_rate_status = 2; // 过高
    Alarm_Control(1);
    }
    else if(heart_rate >= HEART_RATE_LOW_THRESHOLD && heart_rate <= HEART_RATE_HIGH_THRESHOLD)
    {
    heart_rate_status = 0; // 正常
    Alarm_Control(0);
    }

    // 5. OLED刷新显示
    OLED_ShowHeartRate(heart_rate, heart_rate_status);
    }

    // 6. 报警时红灯闪烁(500ms一次)
    if(alarm_flag == 1)
    {
    blink_count++;
    if(blink_count >= 50) // 10ms*50=500ms
    {
    HAL_GPIO_TogglePin(LED_RED_GPIO_PORT, LED_RED_PIN);
    blink_count = 0;
    }
    }
    }
    }

    4.7 主函数(main.c续)

    /* 主函数 */
    int main(void)
    {
    /* 初始化HAL库 */
    HAL_Init();

    /* 配置系统时钟 */
    SystemClock_Config();

    /* 初始化外设 */
    MX_GPIO_Init();
    MX_I2C1_Init();
    MX_TIM2_Init();

    /* 初始化传感器和显示 */
    MAX30102_Init();
    OLED_Init();

    /* 初始显示欢迎界面 */
    uint8_t welcome_str[] = "Welcome!";
    OLED_ShowString(40, 3, welcome_str);
    HAL_Delay(2000);

    /* 主循环(仅处理异常兜底,核心逻辑在定时中断) */
    while (1)
    {
    // 空循环,所有采样和计算由TIM2中断处理
    HAL_Delay(100);
    }
    }

    五、代码烧录与调试

    5.1 代码编译与烧录

  • 代码补充:将上述所有代码按模块补充到Keil工程的main.c和main.h中,确保无遗漏。
  • 编译代码:点击Keil工具栏的“Build”按钮,检查是否有错误(Error)和警告(Warning),确保0 Error、0 Warning。
    • 若出现“I2C相关未定义”错误:检查STM32CubeMX是否正确生成I2C初始化代码;
    • 若出现“OLED字库”错误:忽略(演示代码简化了字库,实际需添加8*16字库数组,可网上下载通用字库)。
  • 硬件连接:将ST-Link下载器连接到STM32最小系统板的SWD接口(SWDIO、SWCLK、GND),并连接电脑USB。
  • 代码烧录:点击Keil工具栏的“Download”按钮,等待烧录完成(底部提示“Download completed”)。
  • 5.2 硬件调试步骤

    步骤1:电源调试
    • 给STM32最小系统板供电(5V USB),测量MAX30102传感器VCC引脚电压为3.3V(不可接5V,否则烧毁传感器)。
    • 观察OLED屏幕:上电后先显示“Welcome!”,2秒后切换到心率监测界面,说明初始化正常。
    步骤2:传感器调试
    • 将MAX30102传感器贴在手指上(指尖贴合传感器的LED和探测器区域),确保接触良好。
    • 用万用表测量I2C总线(PB6、PB7)的电平:应有稳定的3.3V电平,且有数据传输时电平波动。
    • 观察PPG数据:通过Keil的“Watch & Call Stack Window”查看ppg_raw_data数组,应有随脉搏变化的数值(范围约1000-5000)。
    步骤3:心率计算调试
    • 等待1秒后,查看heart_rate变量:正常静息心率应在60-100BPM之间,数值随心跳变化。
    • 若心率为0:调整Calculate_HeartRate函数中的阈值(threshold = sum / data_len + 30),降低阈值。
    步骤4:报警功能调试
    • 手动修改HEART_RATE_LOW_THRESHOLD为80,使当前心率低于阈值:观察蜂鸣器发声、红色LED闪烁、绿色LED熄灭,OLED显示“Status: Too Low”。
    • 手动修改HEART_RATE_HIGH_THRESHOLD为60,使当前心率高于阈值:验证高心率报警功能。
    • 恢复阈值后,报警停止,绿色LED常亮,OLED显示“Status: Normal”。

    5.3 常见问题与解决

    问题现象可能原因解决方法
    OLED屏幕不亮 I2C接线错误、OLED未供电 检查PB6/PB7接线,测量OLED VCC=3.3V
    心率始终为0 传感器未贴紧手指、阈值过高 紧贴手指,降低峰值检测阈值
    蜂鸣器一直响 报警标志未清零、GPIO电平错误 检查Alarm_Control函数,确认PB0初始电平为High
    传感器无数据输出 I2C地址错误、传感器未初始化 确认MAX30102 I2C地址为0xAE,重新初始化传感器
    LED不亮 GPIO接线错误、限流电阻未接 检查PA1/PA2接线,添加220Ω限流电阻

    六、功能扩展建议

  • 添加串口输出:配置USART1,将心率数据通过串口打印到电脑,方便调试和数据记录。
  • 优化心率算法:采用滑动窗口+差分法替代简单峰值检测,提高心率计算精度和抗干扰能力。
  • 添加血氧检测:启用MAX30102的红光LED,同时采集红外和红光数据,计算血氧饱和度(SpO2)。
  • 电池供电改造:使用3.7V锂电池+升压模块供电,制作便携式心率监测仪。
  • 数据存储功能:外接SD卡模块,将心率数据存储为CSV文件,便于后续分析。
  • 总结

  • 本方案以STM32F103C8T6为核心,基于MAX30102光电传感器实现心率监测,通过峰值检测算法计算心率,超出阈值触发声光报警,硬件接线简单,代码模块化且注释详细,零基础小白可按步骤落地。
  • 系统核心逻辑通过Mermaid流程图清晰呈现,关键参数(心率阈值、峰值检测阈值)可根据实际场景调整,确保适配不同用户的使用需求。
  • 调试时需优先验证电源和传感器通信,再调试心率计算和报警逻辑,遇到问题可通过Keil的变量监控功能定位故障点,快速解决。
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » 场景实战:基于STM32的光电式心率监测仪与报警系统解决方案解析
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!