文章目录
-
- 一、方案整体设计与原理说明
-
- 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(读),接线如下:
| PB6 | SCL | I2C时钟线 |
| PB7 | SDA | I2C数据线 |
| 3.3V | VCC | 传感器供电(必须3.3V) |
| GND | GND | 共地 |
(2)STM32与OLED显示屏接线
OLED同样采用I2C通信,接线与MAX30102复用I2C总线(节省引脚):
| PB6 | SCL | I2C时钟线(与传感器复用) |
| PB7 | SDA | I2C数据线(与传感器复用) |
| 3.3V | VCC | OLED供电 |
| GND | GND | 共地 |
(3)STM32与报警模块接线
| PA0 | 有源蜂鸣器IN端 | 低电平触发蜂鸣器报警 |
| PA1 | 绿色LED正极 | 正常状态常亮(串220Ω电阻) |
| PA2 | 红色LED正极 | 异常状态闪烁(串220Ω电阻) |
| GND | 蜂鸣器/LED负极 | 共地 |
三、软件开发环境搭建
3.1 环境准备
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, ®_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_len–1; i++)
{
if((filtered_data[i] > threshold) &&
(filtered_data[i] > filtered_data[i–1]) &&
(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=i–1; 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 代码编译与烧录
- 若出现“I2C相关未定义”错误:检查STM32CubeMX是否正确生成I2C初始化代码;
- 若出现“OLED字库”错误:忽略(演示代码简化了字库,实际需添加8*16字库数组,可网上下载通用字库)。
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Ω限流电阻 |
网硕互联帮助中心






评论前必须登录!
注册