EM7028 是一款专门用于心率检测和血氧检测的低功耗传感器芯片,广泛应用于可穿戴设备(如智能手表、手环)和健康监测设备。它可以基于 PPG(光电容积脉搏波)技术,通过发射光线并接收反射信号来计算心率和血氧饱和度(SpO₂)。
工作原理
心率芯片包括发射 LED 和 PD 接收两部分。
芯片工作时,LED 发出的光打到皮肤下并反射回接收端,反射回的光强便包含了脉搏信息,这个光强经过处理后被 ADC 转换为数值,这个值会随着脉搏跳动周期性的变化。

工作模式
连续模式
在正常 HRS 模式下,其中一个 LED 亮起,HRS 传感器同时检测环境光与绿色 LED 发出的光。光敏度为 1 勒克斯/计数,全范围内总共有65536 勒克斯;ADC 的典型分辨率为16位,转换时间为 25 毫秒。
在低亮度模式下,选择 8 倍 ADC 增益来接收光线,检测范围为 0.125 亮度到 8192 亮度。
LED 点亮:LED1 或 LED2 持续发光。
光检测:传感器实时接收 反射光线 和 环境光线,并进行测量。
数据处理:通过 ADC 转换,将光线信号转化为数字信号,提供给主控 MCU。
输出数据:数据存储到寄存器(如 HRS_DATA0),主控 MCU 通过 I²C 通信读取数据。
脉冲模式
脉冲模式下的心率传感器在脉冲状态下打开 LED,检测反射光。
LED 脉冲点亮:LED 以一定周期脉冲式点亮,亮起时间较短。
光检测:在 LED 点亮期间,传感器检测光线反射强度。
ADC 转换:将检测到的光信号通过 ADC 转换为数字信号。
数据输出:将数据存储到寄存器,主控 MCU 在需要时读取。
LED 熄灭:在脉冲关闭期间,LED 熄灭,传感器休眠以节省功耗。
通过 EMP7028 的寄存器配置工作模式与相关参数
引脚说明

PinNo |
PinName |
Type |
Description |
1 |
SDA |
I/O (开漏) |
I²C 数据线:用于 I²C 通信的数据传输,需外接上拉电阻。 |
2 |
INT |
O (开漏) |
中断引脚:当传感器有数据或事件触发时,该引脚输出中断信号。 |
3 |
LED1 |
O (开漏) |
LED 电流驱动引脚 1:可提供高达 200mA 电流,通常用于驱动高功率 LED 光源。 |
4 |
LED2 |
O (开漏) |
LED 电流驱动引脚 2:可提供高达 12mA 电流,通常用于低功率 LED 光源。 |
5 |
LEDA |
I |
LED 阳极端:需要连接到 LED 电源 VDD_LED,用于 LED 驱动电路供电。 |
6 |
GND |
— |
电源地:所有电压均参考此引脚接地。 |
7 |
SCL |
I (开漏) |
I²C 时钟线:提供 I²C 通信的时钟信号,需外接上拉电阻。 |
8 |
VDD |
— |
电源电压:为芯片提供供电,通常为 3.3V 或符合设备要求的电压。 |
I2C 时序特性

fclek:SCL 时钟频率
tSUDAT:数据设置时间 (tSUDAT)
- 主机在时钟信号(SCL)上升沿前,数据线(SDA)上的数据必须保持稳定的最短时间。
tHDDAT:数据保持时间 (tHDDAT)
- 在时钟信号(SCL)下降沿之后,数据线(SDA)上的数据必须继续保持的时间。
trise:时钟/数据上升时间
- 上升时间 (trise):SCL 和 SDA 从低电平到高电平的时间。
tfall:时钟/数据下降时间
- 下降时间 (tfall):SCL 和 SDA 从高电平到低电平的时间。
tLOW:SCL 低电平时间
tHIGH:SCL 高电平时间
tBUF:起始状态和停止状态之间的空闲时间
- tBUF 定义了停止条件与下一次起始条件之间的最小空闲时间。
tHDSTA:起始条件保持时间(重复启动)
tSUSTA:起始条件设置时间
- 主机发送起始条件前,数据线 SDA 必须稳定的最短时间。
tSUSTO:停止条件设置时间
- 主机发送停止条件前,数据线 SDA 必须稳定的最短时间。
tTIMEOUT:低检测时钟/数据超时时间
Cload:每条总线线的电容负载
RBUS:SDA 和 SCL 的上拉电阻
tVD:数据有效时间
tVDACK:数据有效确认时间
- 定义了数据在传输中的有效性以及接收方发送确认(ACK)时的时序要求。
I2C 状态机

IDLE
- 初始状态或空闲状态。
- 系统处于等待信号或数据的状态。
Address Dec(Address Decode)
- 解析或解码地址的状态。
- 设备从总线或数据流中获取地址,并进行地址匹配。
ACK(Acknowledge)
- 确认状态。
- 当设备接收到正确的数据或地址后,会发送 ACK 信号(确认响应)。
REV(Receive)
TRANS(Transmit)
- 数据传输状态。
- 系统将数据传输给通信对方(比如主机或其他设备)。
IDLE → Address Dec
- 系统从空闲状态进入地址解析状态,可能是因为接收到通信请求(如 Start 信号)。
Address Dec → IDLE
- 如果地址解析失败,系统返回空闲状态,等待下一次请求。
Address Dec → ACK
- 当地址成功匹配时,进入 ACK 状态,表示地址有效并响应确认信号。
ACK → REV
- 系统进入接收数据状态,表示通信对方发送数据过来,系统开始接收。
ACK → TRANS
- 系统进入数据传输状态,表示系统主动发送数据给对方。
REV → ACK 和 TRANS → ACK
- 数据传输或接收完成后,系统返回 ACK 状态,进行确认或等待下一步操作。
ACK → IDLE
- 在某些情况下,数据传输或接收结束,系统返回空闲状态,等待下一次通信。
传输顺序

从设备地址
The I2C Interface and 7-bit slave address is 0x24.
示例电路
HRS2 脉冲模式的典型应用电路(VDD2.63.6V,VDD PC1.63.6V、VDD LED 2.6~4.5V)

HRS1 连续模式下的典型应用电路

VDD、VDD_I2C、VDD_LED可以连接在一起,形成VDD3.3V至3.3V的电压。
VDD3.3V应将0.1uF电容器接地。
如果不需要中断模式,INT可以断开。
R0是LED2调节电阻器,R0的典型值为100Ω。
示例代码
利用 I2C 写入对应命令后读取对应寄存器的数值后在利用波峰检算法即可,具体查看对应代码。
波峰检测算法
波峰检测算法,通过传感器采集到的数据推算出心率值。1
在心率算法中,通过波峰(即心跳信号的峰值)之间的时间间隔计算心率,常用以下公式:

人体正常的心率范围一般在 60 BPM(一分钟 60 次) 到 180 BPM 之间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
|
#define QUEUE_SIZE 7
typedef struct { int8_t front; int8_t rear; int8_t size; uint32_t data[QUEUE_SIZE]; } Queue;
uint8_t Hr_Ave_Filter(uint32_t *HrList, uint8_t lenth) { uint32_t ave = 0; uint8_t i = 0; for(i = 0;i<lenth;i++) { ave += HrList[i]; } ave /= lenth; }
uint16_t HR_Calculate(uint16_t present_dat,uint32_t present_time) { static uint16_t peaks_time[]={0,0}; static uint8_t HR=0;
if(isQueueFull(&datas)) { dequeue(&datas); } if(isQueueFull(×)) { dequeue(×); } if(isQueueFull(&HR_List)) { dequeue(&HR_List); }
enqueue(&datas,present_dat); enqueue(×,present_time);
if((datas.data[3]>=datas.data[2]) && (datas.data[3]>=datas.data[1]) && (datas.data[3]>datas.data[0]) && (datas.data[3]>=datas.data[4]) && (datas.data[3]>=datas.data[5]) && (datas.data[3]>datas.data[6])) { if((times.data[3]-peaks_time[0]) > 425) { peaks_time[1] = peaks_time[0]; peaks_time[0] = times.data[3]; enqueue(&HR_List,60000/(peaks_time[0]-peaks_time[1])); if(HR_List.data[6]!=0) { HR = Hr_Ave_Filter(HR_List.data,7); } } } return HR; } ==========================================================================
void HRDataUpdateTask(void *argument){ uint8_t IdleBreakstr=0; uint16_t dat=0; uint8_t hr_temp=0; while(1){ if(Page_Get_NowPage()->page_obj == &ui_HRPage){ osMessageQueuePut(IdleBreak_MessageQueue, &IdleBreakstr, 0, 1); EM7028_hrs_Enable(); if(!HWInterface.HR_meter.ConnectionError){ vTaskSuspendAll(); hr_temp = HR_Calculate(EM7028_Get_HRS1(),user_HR_timecount); xTaskResumeAll(); if(HWInterface.HR_meter.HrRate != hr_temp && hr_temp>50 && hr_temp<120) { HWInterface.HR_meter.HrRate = hr_temp; } } } osDelay(50); } }
|
维护队列:
- 如果 datas、times 和 HR_List 队列已满,则先移除最早的数据点。
- 将当前的传感器数据 present_dat 和时间 present_time 加入队列。
波峰检测:
- 判断 datas.data[3](第4个数据点)是否为波峰。
- 判断条件:第4个数据点必须大于前面 3 个和后面 3 个数据点(即满足局部最大值)。
- 原理:通过比较前后数据点,检测光信号数据中的局部峰值,通常对应于心跳导致的光反射峰。
波峰间隔时间检查:
- 如果当前检测到的波峰与上一个波峰的时间间隔 times.data[3] - peaks_time[0] > 425(大约 425 毫秒,即心率下限 60/0.425 ≈ 141 BPM),则认为是有效波峰。
- 更新 peaks_time 数组,保存当前和上一个波峰的时间戳。
心率计算:
- 使用公式: \text{HR} = \frac{60000}{\text{peaks_time[0]} - \text{peaks_time[1]}} 其中,60000 表示 1 分钟的时间(单位为毫秒)。
- 将计算得到的心率值存入 HR_List 队列。
心率平滑滤波:
- 当 HR_List 数据满 7 个心率值时,调用 Hr_Ave_Filter 函数对最近 7 个心率值进行平均滤波,减少抖动,提高稳定性。
- Hr_Ave_Filter 负责对心率值进行滤波平滑处理,返回最终的平滑心率值。
返回心率:
算法中的固定阈值 425 毫秒(对应心率上限约为 141 BPM)在高心率场景下存在局限性,无法正确检测心率超过 141 BPM 的波峰。因此,波峰间隔阈值不应设为固定值,而是应根据用户状态动态调整,应当结合陀螺仪数据判断用户当前处于静止、运动或剧烈活动状态,从而设置适合的波峰间隔阈值以提高算法的适应性和准确性。
驱动库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
| #ifndef __EM70X8_H__ #define __EM70X8_H__
#include "iic_hal.h"
#define EM7028_ID 0x36 #define EM7028_ADDR 0x24
#define ID_REG 0x00 #define HRS_CFG 0x01 #define HRS_INT_CTRL 0x02 #define HRS_LT_L 0X03 #define HRS_LT_H 0x04 #define HRS_HT_L 0x05 #define HRS_HT_H 0x06 #define LED_CRT 0x07 #define HRS2_DATA_OFFSET 0x08 #define HRS2_CTRL 0x09 #define HRS2_GAIN_CTRL 0x0A #define HRS1_CTRL 0x0D #define INT_CTRL 0x0E #define SOFT_RESET 0x0F
#define HRS2_DATA0_L 0x20 #define HRS2_DATA0_H 0x21 #define HRS2_DATA1_L 0x22 #define HRS2_DATA1_H 0x23 #define HRS2_DATA2_L 0x24 #define HRS2_DATA2_H 0x25 #define HRS2_DATA3_L 0x26 #define HRS2_DATA3_H 0x27
#define HRS1_DATA0_L 0x28 #define HRS1_DATA0_H 0x29 #define HRS1_DATA1_L 0x2A #define HRS1_DATA1_H 0x2B #define HRS1_DATA2_L 0x2C #define HRS1_DATA2_H 0x2D #define HRS1_DATA3_L 0x2E #define HRS1_DATA3_H 0x2F
uint8_t EM7028_ReadOneReg(unsigned char RegAddr); void EM7028_WriteOneReg(unsigned char RegAddr, unsigned char dat);
uint8_t EM7028_Get_ID(void); uint8_t EM7028_hrs_init(void); uint8_t EM7028_hrs_Enable(void); uint8_t EM7028_hrs_DisEnable(void); uint16_t EM7028_Get_HRS1(void);
#endif ==========================================================================
#include "em70x8.h"
#define CLK_ENABLE __HAL_RCC_GPIOB_CLK_ENABLE(); iic_bus_t EM7028_bus = { .IIC_SDA_PORT = GPIOB, .IIC_SCL_PORT = GPIOB, .IIC_SDA_PIN = GPIO_PIN_13, .IIC_SCL_PIN = GPIO_PIN_14, };
uint8_t EM7028_ReadOneReg(unsigned char RegAddr){ unsigned char dat; dat = IIC_Read_One_Byte(&EM7028_bus, EM7028_ADDR, RegAddr); return dat; }
void EM7028_WriteOneReg(unsigned char RegAddr, unsigned char dat){ IIC_Write_One_Byte(&EM7028_bus, EM7028_ADDR, RegAddr, dat); }
uint8_t EM7028_Get_ID(){ return EM7028_ReadOneReg(ID_REG); }
uint8_t EM7028_hrs_init(){ uint8_t i = 5; CLK_ENABLE; IICInit(&EM7028_bus); while(EM7028_Get_ID() != 0x36 && i){ HAL_Delay(100); i--; } if(!i){return 1;} EM7028_WriteOneReg(HRS_CFG,0x00); EM7028_WriteOneReg(HRS2_DATA_OFFSET, 0x00); EM7028_WriteOneReg(HRS2_GAIN_CTRL, 0x7f); EM7028_WriteOneReg(HRS1_CTRL, 0x47); 设置 INT_CTRL 寄存器,关闭中断控制。 EM7028_WriteOneReg(INT_CTRL, 0x00); return 0; }
uint8_t EM7028_hrs_Enable(){ uint8_t i = 5; while(EM7028_Get_ID() != 0x36 && i){ HAL_Delay(100); i--; } if(!i) {return 1;} EM7028_WriteOneReg(HRS_CFG,0x08); return 0; }
uint8_t EM7028_hrs_DisEnable(){ uint8_t i = 5; while(EM7028_Get_ID() != 0x36 && i){ HAL_Delay(100); i--; } if(!i) {return 1;} EM7028_WriteOneReg(HRS_CFG,0x00); return 0; }
uint16_t EM7028_Get_HRS1(void){ uint16_t dat; dat = EM7028_ReadOneReg(HRS1_DATA0_H); dat <<= 8; dat |= EM7028_ReadOneReg(HRS1_DATA0_L); return dat; }
|
https://github.com/TooUpper/SensorDrive