EM7028

EM7028 是一款专门用于心率检测血氧检测的低功耗传感器芯片,广泛应用于可穿戴设备(如智能手表、手环)和健康监测设备。它可以基于 PPG(光电容积脉搏波)技术,通过发射光线并接收反射信号来计算心率和血氧饱和度(SpO₂)。

工作原理

心率芯片包括发射 LED 和 PD 接收两部分。

芯片工作时,LED 发出的光打到皮肤下并反射回接收端,反射回的光强便包含了脉搏信息,这个光强经过处理后被 ADC 转换为数值,这个值会随着脉搏跳动周期性的变化。

gzyl

工作模式

连续模式

在正常 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 的寄存器配置工作模式与相关参数

引脚说明

yjsm

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 时序特性

sstx

fclek:SCL 时钟频率

  • Normal Mode 下,SCL 的最大频率为 100 kHz

  • Fast Mode 下,SCL 的最大频率可达 400 kHz

tSUDAT:数据设置时间 (tSUDAT)

  • 主机在时钟信号(SCL)上升沿前,数据线(SDA)上的数据必须保持稳定的最短时间。

tHDDAT:数据保持时间 (tHDDAT)

  • 在时钟信号(SCL)下降沿之后,数据线(SDA)上的数据必须继续保持的时间。

trise:时钟/数据上升时间

  • 上升时间 (trise):SCL 和 SDA 从低电平到高电平的时间。

tfall:时钟/数据下降时间

  • 下降时间 (tfall):SCL 和 SDA 从高电平到低电平的时间。

tLOW:SCL 低电平时间

tHIGH:SCL 高电平时间

tBUF:起始状态和停止状态之间的空闲时间

  • tBUF 定义了停止条件与下一次起始条件之间的最小空闲时间。

tHDSTA:起始条件保持时间(重复启动)

  • 起始条件(START)信号的保持时间。

tSUSTA:起始条件设置时间

  • 主机发送起始条件前,数据线 SDA 必须稳定的最短时间。

tSUSTO:停止条件设置时间

  • 主机发送停止条件前,数据线 SDA 必须稳定的最短时间。

tTIMEOUT:低检测时钟/数据超时时间

Cload:每条总线线的电容负载

RBUS:SDA 和 SCL 的上拉电阻

tVD:数据有效时间

tVDACK:数据有效确认时间

  • 定义了数据在传输中的有效性以及接收方发送确认(ACK)时的时序要求。

I2C 状态机

I2Cztj

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 → ACKTRANS → ACK

  • 数据传输或接收完成后,系统返回 ACK 状态,进行确认或等待下一步操作。

ACK → IDLE

  • 在某些情况下,数据传输或接收结束,系统返回空闲状态,等待下一次通信。

传输顺序

cssx

从设备地址

The I2C Interface and 7-bit slave address is 0x24.

示例电路

HRS2 脉冲模式的典型应用电路(VDD2.63.6V,VDD PC1.63.6V、VDD LED 2.6~4.5V)

mcsldl

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

lxmsdl

VDD、VDD_I2C、VDD_LED可以连接在一起,形成VDD3.3V至3.3V的电压。

VDD3.3V应将0.1uF电容器接地。

如果不需要中断模式,INT可以断开。

R0是LED2调节电阻器,R0的典型值为100Ω。

示例代码

利用 I2C 写入对应命令后读取对应寄存器的数值后在利用波峰检算法即可,具体查看对应代码。

波峰检测算法

波峰检测算法,通过传感器采集到的数据推算出心率值。1

在心率算法中,通过波峰(即心跳信号的峰值)之间的时间间隔计算心率,常用以下公式:

xlsf

人体正常的心率范围一般在 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
// HRAlgorithm.c

// 定义队列结构
#define QUEUE_SIZE 7

typedef struct {
int8_t front;
int8_t rear;
int8_t size;
uint32_t data[QUEUE_SIZE];
} Queue;

// 计算 HrList 的平均值,用于平滑处理心率数据,从而减小心率计算中的波动,提高数据稳定性。
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;
}

/*
present_dat:当前时刻传感器采集到的数据值(光信号强度值)。
present_time:当前时刻的时间戳(单位为毫秒)。
*/
uint16_t HR_Calculate(uint16_t present_dat,uint32_t present_time) {

// peaks_time:保存最近检测到的两个波峰的时间戳。
// peaks_time[0]:当前检测到的波峰时间。
// peaks_time[1]:上一个波峰的时间。
static uint16_t peaks_time[]={0,0};
// 当前计算得到的心率值。
static uint8_t HR=0;

// isQueueFull() 判断队列是否以满
// dequeue() 从队列中移除旧数据。
if(isQueueFull(&datas)) {
dequeue(&datas);
}
if(isQueueFull(&times)) {
dequeue(&times);
}
if(isQueueFull(&HR_List)) {
dequeue(&HR_List);
}

// 将新数据加入队列。
enqueue(&datas,present_dat);
enqueue(&times,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;
}
==========================================================================
//user_SendUpdateTask.c
// 周期性地更新心率数据
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);
//sensor wake up
EM7028_hrs_Enable();
//receive the sensor wakeup message, sensor wakeup
if(!HWInterface.HR_meter.ConnectionError){
//Hr messure
vTaskSuspendAll();
// 调用 HR_Calculate 函数进行心率计算。EM7028_Get_HRS1() 获取心率传感器的数据,
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 负责对心率值进行滤波平滑处理,返回最终的平滑心率值。

返回心率

  • 返回当前平滑处理后的心率值 HR。

算法中的固定阈值 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
// em70x8.h
#ifndef __EM70X8_H__
#define __EM70X8_H__

#include "iic_hal.h"

#define EM7028_ID 0x36 // EM7028 默认产品 ID
#define EM7028_ADDR 0x24 // EM7028 I2C 设备地址

/* 寄存器地址 */
#define ID_REG 0x00 // 芯片的 PID(产品ID)
#define HRS_CFG 0x01 // 模式选择寄存器(HRS1 or HRS2)
#define HRS_INT_CTRL 0x02 // 中断相关寄存器
#define HRS_LT_L 0X03 // HRS 低八位中断低阈值
#define HRS_LT_H 0x04 // HRS 低八位中断高阈值
#define HRS_HT_L 0x05 // HRS 高八位中断低阈值
#define HRS_HT_H 0x06 // HRS 高八位中断高阈值
#define LED_CRT 0x07 // LED 电流校准控制
#define HRS2_DATA_OFFSET 0x08 // HRS2 数据偏移
#define HRS2_CTRL 0x09
#define HRS2_GAIN_CTRL 0x0A
#define HRS1_CTRL 0x0D
#define INT_CTRL 0x0E
#define SOFT_RESET 0x0F // 重置

/* HRS2 数据寄存器 */
#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

/* HRS1 数据寄存器 */
#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
==========================================================================
// em70x8.c
#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,
};

// 通过 I2C 总线读取 EM7028 芯片的一个寄存器值
uint8_t EM7028_ReadOneReg(unsigned char RegAddr){
unsigned char dat;
dat = IIC_Read_One_Byte(&EM7028_bus, EM7028_ADDR, RegAddr);
return dat;
}

// 通过 I2C 总线写入 EM7028 芯片的一个寄存器值
void EM7028_WriteOneReg(unsigned char RegAddr, unsigned char dat){
IIC_Write_One_Byte(&EM7028_bus, EM7028_ADDR, RegAddr, dat);
}

// 获取芯片的 PID(产品ID)
uint8_t EM7028_Get_ID(){
return EM7028_ReadOneReg(ID_REG);
}

// 初始化 EM7028 芯片的心率传感器(HRS)模块。
uint8_t EM7028_hrs_init(){
uint8_t i = 5;

CLK_ENABLE;
// 初始化 I2C 总线
IICInit(&EM7028_bus);

// 检查设备 ID,最多五次
while(EM7028_Get_ID() != 0x36 && i){
HAL_Delay(100);
i--;
}
// 超时返回
if(!i){return 1;}
// 启用心率传感器 1(HRS1),禁用心率传感器 2(HRS2),并启用 LED1 来进行心率测量。
EM7028_WriteOneReg(HRS_CFG,0x00);
// 设置传感器数据的偏移量为 0。
EM7028_WriteOneReg(HRS2_DATA_OFFSET, 0x00);
// 设置 HRS2_GAIN_CTRL 寄存器的增益控制为 0x7f,即增益值为 1。
EM7028_WriteOneReg(HRS2_GAIN_CTRL, 0x7f);
//配置 HRS1_CTRL 寄存器,设置心率传感器 1(HRS1)的增益为 1
// 范围为 8,频率为 2.62144MHz,分辨率为 16 位,并设置为模式工作。
EM7028_WriteOneReg(HRS1_CTRL, 0x47);
设置 INT_CTRL 寄存器,关闭中断控制。
EM7028_WriteOneReg(INT_CTRL, 0x00);
//如果所有初始化操作成功完成,函数返回 0,表示初始化成功。
return 0;
}

// 启用 EM7028 芯片的心率传感器(HRS)
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;
}

// 关闭 EM7028 芯片的心率传感器功能。
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;
}

// 从 EM7028 芯片读取心率传感器 1(HRS1)的数据并将其合并为一个 16 位的值
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