[i=s] 本帖最后由 宇岚 于 2025-5-14 19:21 编辑 [/i]<br />
<br />
#申请原创# #技术资源#
一、简介
书接上回,继续将本项目中其他模块实现思路详细进行描述
二、系统框图
回顾,本项目使用APM32ER8T6作为主控,使用到的模块有:DS18B20、MAX30102、ATGM336H以及ADXL345等,可实现温度、血压、血氧的测量、GPS定位、计时功能以及步数感应功能;

三、模块实现
下面将从模块原理、硬件设计和程序设计三部分详细介绍各个模块的实现思路:
心率血氧传感器
MAX30102心率血氧传感器是一种集成了光学传感器和信号处理器的模块,它具有高度集成、低功耗、高精度等特点,能够实时检测心率和血氧饱和度。
模块工作原理
光电容积法(PPG):光电容积法的基本原理是利用人体组织在血管搏动时造成透光率不同来进行脉搏和血氧饱和度测量的。光源一般采用对动脉血中氧合血红蛋白(Hb02)和血红蛋白(Hb)有选择性的特定波长的发光二极管(一般选用660nm附近的红光和900nm附近的红外光)。
当光束透过人体外周血管,由于动脉搏动充血容积变化导致这束光的透过光率发生改变,此时由光电变换器接收人体组织反射的光线,转变为电信号并将其放大和输出。由于脉搏是随心脏的搏动而周期性变化的信号,动脉血管容积也周期性变化,因此光电变换器的电信号变化周期就是脉搏率。同时根据血氧饱和度的定义,计算如下:

心率测量:
含氧血红蛋白(HbO2)具有吸收红外光的特性,血液越红,吸收的红外光就越多。当心脏跳动时,血液被泵入和泵出,其反射光的强弱会发生变化,光电探测器接收透过皮肤的光线,将其转化为电信号,产生变化的波形。通过测量这些电信号的变化,可以计算出心率。
但是,由于测量时总会出现某些干扰,所以我们需要进行滤波处理。
Heart rate=(N/T)*60 = 60N / T (单位:次/min)
血氧测量:
MAX30102能够同时发射红光和红外光,通过光电检测器检测反射光量,可以判断出红光、红外光被吸收的多少,从而推断出含氧血红蛋白(HbO2)和脱氧血红蛋白(Hb)所占的比例。最后,可以计算出血压中的氧含量。
R = (ACred / DCred) / (ACired / DCired)
=((ir_max+ir_min)(red_max-red_min) )/ ((red_max+red_min)(irmax-ir_min))
SpO2 = -45.060 R R+ 30.354 * R + 94.845
硬件设计
实物图:

原理图:

引脚信息:
引脚名称 |
描述 |
VIN |
供给电压2.5~5.5V |
SCL |
I2C的时钟引脚,接PB11 |
SDA |
I2C的数据引脚,接PB10 |
INT |
悬空 |
IRD |
IR_LED接地端,悬空 |
RD |
RED_LED接地端,悬空 |
GND |
接地 |
程序设计
第一步,模块初始化,I2C引脚模块初始化,然后通过模块复位引脚进行复位
void Init_MAX30102(void)
{
int32_t i;
un_brightness = 0;
un_min = 0x3FFFF;
un_max = 0;
bsp_InitI2C();//IIC初始化
maxim_max30102_reset(); //复位 MAX30102 模块
maxim_max30102_read_reg(REG_INTR_STATUS_1, &uch_dummy); //读取/清除中断状态寄存器
maxim_max30102_init(); //初始化 MAX30102模块
n_ir_buffer_length = 150; //缓冲区长度为150可以存储以50样本/秒(sps)运行的3秒样本
//读取150样本数据,明确样本信号范围
for(i = 0; i < n_ir_buffer_length; i++)
{
//while(KEY0 == 1); //等待直到中断引脚被触发
maxim_max30102_read_fifo((aun_ir_buffer+i), (aun_red_buffer+i)); //新版本 //read from MAX30102 FIFO
if(un_min > aun_red_buffer[i])
un_min = aun_red_buffer[i]; //更新样本最低值
if(un_max < aun_red_buffer[i])
un_max = aun_red_buffer[i]; //更新样本最高值
}
un_prev_data = aun_red_buffer[i];
//要计算心率(HR)和血氧饱和度(SpO2),需要进一步处理这最初的150个样本
maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, n_ir_buffer_length, aun_red_buffer, &n_spo2, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid);
}
bool maxim_max30102_init(void)
{
if(!maxim_max30102_write_reg(REG_INTR_ENABLE_1, 0xc0)) // INTR setting
return false;
if(!maxim_max30102_write_reg(REG_INTR_ENABLE_2, 0x00))
return false;
if(!maxim_max30102_write_reg(REG_FIFO_WR_PTR, 0x00)) //FIFO_WR_PTR[4:0]
return false;
if(!maxim_max30102_write_reg(REG_OVF_COUNTER, 0x00)) //OVF_COUNTER[4:0]
return false;
if(!maxim_max30102_write_reg(REG_FIFO_RD_PTR, 0x00)) //FIFO_RD_PTR[4:0]
return false;
if(!maxim_max30102_write_reg(REG_FIFO_CONFIG, 0x6f)) //sample avg = 8, fifo rollover=false, fifo almost full = 17
return false;
if(!maxim_max30102_write_reg(REG_MODE_CONFIG, 0x03)) //0x02 for Red only, 0x03 for SpO2 mode 0x07 multimode LED
return false;
if(!maxim_max30102_write_reg(REG_SPO2_CONFIG, 0x2F)) // SPO2_ADC range = 4096nA, SPO2 sample rate (400 Hz), LED pulseWidth (411uS)
return false;
if(!maxim_max30102_write_reg(REG_LED1_PA, 0x17)) //Choose value for ~ 4.5mA for LED1
return false;
if(!maxim_max30102_write_reg(REG_LED2_PA, 0x17)) // Choose value for ~ 4.5mA for LED2
return false;
if(!maxim_max30102_write_reg(REG_PILOT_PA, 0x7f)) // Choose value for ~ 25mA for Pilot LED
return false;
return true;
}
第二步,读取心率血氧数据,分别存储,并进行处理
void GetHeartRateSpO2(void)
{
int32_t i;
float f_temp;
static u8 COUNT=8;
unsigned char x=0;
i = 0;
un_min = 0x3FFFF;
un_max = 0;
//将前50个样本从内存中移除或保存(dump),将剩下的100个样本向前移动,填补原来前50个样本的位置
for(i = 50; i < 150; i++)
{
aun_red_buffer[i - 50] = aun_red_buffer[i];
aun_ir_buffer[i - 50] = aun_ir_buffer[i];
//更新样本的最大值和最小值
if(un_min > aun_red_buffer[i])
un_min = aun_red_buffer[i];
if(un_max < aun_red_buffer[i])
un_max = aun_red_buffer[i];
}
//在计算心率之前,先获取50组样本数据.
for(i = 100; i < 150; i++)
{
un_prev_data = aun_red_buffer[i - 1];
maxim_max30102_read_fifo((aun_ir_buffer+i), (aun_red_buffer+i)); //新版本
//计算LED的亮度
if(aun_red_buffer[i] > un_prev_data)
{
f_temp = aun_red_buffer[i] - un_prev_data;
f_temp /= (un_max - un_min);
f_temp *= MAX_BRIGHTNESS;
f_temp = un_brightness - f_temp;
if(f_temp < 0)
un_brightness = 0;
else
un_brightness = (int)f_temp;
}
else
{
f_temp = un_prev_data - aun_red_buffer[i];
f_temp /= (un_max - un_min);
f_temp *= MAX_BRIGHTNESS;
un_brightness += (int)f_temp;
if(un_brightness > MAX_BRIGHTNESS)
un_brightness = MAX_BRIGHTNESS;
}
}
maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, n_ir_buffer_length, aun_red_buffer, &n_spo2, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid);
if(COUNT++ > 8)
{
COUNT = 0;
//对心率数据处理
if ((ch_hr_valid == 1) && (n_heart_rate < 150) && (n_heart_rate > 60))
{
hrTimeout = 0;
// 丢弃每五个数据中的一个,如果数据异常
if (hrValidCnt == 4)
{
hrThrowOutSamp = 1;
hrValidCnt = 0;
for (i = 12; i < 16; i++)
{
if (n_heart_rate < hr_buf[i] + 10)
{
hrThrowOutSamp = 0;
hrValidCnt = 4;
}
}
}
else
hrValidCnt = hrValidCnt + 1;
if (hrThrowOutSamp == 0)
{
// 将新的样本数据加入到缓冲区中
for(i = 0; i < 15; i++)
hr_buf[i] = hr_buf[i + 1];
hr_buf[15] = n_heart_rate;
// 更新缓冲区的填充值
if (hrBuffFilled < 16) //如果小于16,则平移
hrBuffFilled = hrBuffFilled + 1;
// 对缓冲区值进行移动平均处理
hrSum = 0;
if (hrBuffFilled < 2) //如果小于2,最终结果为0
hrAvg = 0;
else if (hrBuffFilled < 4) //如果小于4
{
for(i = 14; i < 16; i++){
hrSum = hrSum + hr_buf[i];} // 求和14、15位
hrAvg = hrSum >> 1; // 右移一位---相当于除2
}
else if (hrBuffFilled < 8) //如果小于8
{
for(i = 12; i < 16; i++){
hrSum = hrSum + hr_buf[i];}// 求和12到15的四个样本
hrAvg = hrSum >> 2; // 右移两位位---相当于除4
}
else if (hrBuffFilled < 16)//如果小于16
{
for(i = 8; i < 16; i++){
hrSum = hrSum + hr_buf[i];}// 求和8到15的8个样本
hrAvg = hrSum >> 3;// 右移三位位---相当于除8
}
else
{
for(i = 0; i < 16; i++){
hrSum = hrSum + hr_buf[i];}// 求和0到15的8个样本
hrAvg = hrSum >> 4;// 右移四位位---相当于除16
}
}
hrThrowOutSamp = 0;
}
else
{
hrValidCnt = 0;
if (hrTimeout == 4)//如果超过次数
{
// hrAvg = 0; //清除平均结果
hrBuffFilled = 0; //清除缓冲区数据
}
else
hrTimeout++;
}
//对血氧饱和度数据处理
if ((ch_spo2_valid == 1) && (n_spo2 > 80))
{
spo2Timeout = 0;
// 在每5个有效的样本中,如果有异常值(wacky表示奇怪的、异常的),可以丢弃其中最多1个样本
if (spo2ValidCnt == 4)
{
spo2ThrowOutSamp = 1;
spo2ValidCnt = 0;
for (i = 12; i < 16; i++)
{
if (n_spo2 > spo2_buf[i] - 10)
{
spo2ThrowOutSamp = 0;
spo2ValidCnt = 4;
}
}
}
else
spo2ValidCnt = spo2ValidCnt + 1;
if (spo2ThrowOutSamp == 0)
{
// 将新的样本数据加入到缓冲区中
for(i = 0; i < 15; i++)
spo2_buf[i] = spo2_buf[i + 1];
spo2_buf[15] = n_spo2;
// 更新缓冲区的填充值
if (spo2BuffFilled < 16) //如果小于16,则平移
spo2BuffFilled = spo2BuffFilled + 1;
// 对缓冲区值进行移动平均处理
spo2Sum = 0;
if (spo2BuffFilled < 2) //如果小于2,最终结果为0
spo2Avg = 0;
else if (spo2BuffFilled < 4) //如果小于4
{
for(i = 14; i < 16; i++){
spo2Sum = spo2Sum + spo2_buf[i];}// 求和14、15位
spo2Avg = spo2Sum >> 1; // 右移一位---相当于除2
}
else if (spo2BuffFilled < 8) //如果小于8
{
for(i = 12; i < 16; i++){
spo2Sum = spo2Sum + spo2_buf[i];}// 求和12到15位
spo2Avg = spo2Sum >> 2; // 右移两位---相当于除4
}
else if (spo2BuffFilled < 16) //如果小于16
{
for(i = 8; i < 16; i++){
spo2Sum = spo2Sum + spo2_buf[i];}// 求和12到15位
spo2Avg = spo2Sum >> 3; // 右移三位---相当于除8
}
else
{
for(i = 0; i < 16; i++)
{
spo2Sum = spo2Sum + spo2_buf[i];// 求和16位
}
spo2Avg = spo2Sum >> 4; // 右移四位---相当于除16
}
}
spo2ThrowOutSamp = 0;
}
else
{
spo2ValidCnt = 0;
if (spo2Timeout == 4)
{
// spo2Avg = 0;
spo2BuffFilled = 0;
}
else
spo2Timeout++;
}
}
}
加速度传感器
ADXL345是一款由Analog Devices(ADI)公司生产的三轴数字加速度传感器。它采用了高分辨率的13位AD转换器和16位数据输出,能够测量 ±2g/±4g/±8g/±16g 四种量程范围内的加速度,具有高精度、低功耗等优点。可以同时测量X、Y和Z三个轴的加速度和倾斜角度,支持自由空间运动和倾斜角度检测,支持I²C和SPI数字接口,方便与微控制器和其他数字设备进行通信。
模块工作原理
采用了微小的振动结构,将物体加速度转化为机械振动,再将振动信号转化成电信号,最终通过内部处理产生数字输出。ADXL345传感器内部集成有一个微型结构,由若干个有弹性的薄膜和固定在薄膜上的质量块组成。当物体受到加速度时,质量块相应地发生位移运动并造成薄膜出现变形,在其上形成一种称为“压电效应”的局部电荷分布作用,从而产生电信号输出。这些电信号被信号调理电路进行放大、滤波和模数转换。
查看MAX30102用户手册,将需要用到的寄存器位用红框圈出;

角度计算:
加速度传感器 轴与自然坐标系X轴的夹角:

加速度传感器Y轴与自然坐标系Y轴的夹角:

加速度传感器Z轴与自然坐标系Z轴的夹角:

硬件设计
实物图:

引脚信息:
引脚名称 |
描述 |
VCC |
供给电压5V |
GND |
地线 |
SCL |
I2C时钟线,PB10 |
SDA |
I2C数据线,PB11 |
INT1 |
中断1输出,悬空 |
INT2 |
中断2输出,悬空 |
程序设计
第一步,模块初始化,初始化I2C模块,并配置ADXL345相关寄存器
void ADXL345_Init()
{
ADXL345_IIC_Init();
adxl345_write_reg(0X31,0X0B); //低电平中断输出,13位全分辨率,输出数据右对齐,16g量程
adxl345_write_reg(0x2C,0x0B); //数据输出速度为100Hz
adxl345_write_reg(0x2D,0x08); //链接使能,测量模式,省电特性
adxl345_write_reg(0X2E,0x80); //不使用中断
adxl345_write_reg(0X1E,0x00);
adxl345_write_reg(0X1F,0x00);
adxl345_write_reg(0X20,0x05);
}
void ADXL345_IIC_Init()
{
GPIO_Config_T GPIO_InitStructure;
RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_GPIOAB); //使能SCL端口时钟
GPIO_InitStructure.pin = GPIO_Pin_10; //配置为推挽输出,SCL
GPIO_InitStructure.mode= GPIO_Mode_Out_PP;
GPIO_InitStructure.speed= GPIO_Speed_50MHz; //IO口速度为50MHz
GPIO_Config(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB,ADXL345_SCL_PIN);
RCC_APB2PeriphClockCmd(ADXL345_SDA_GPIO_CLK, ENABLE); //使能SDA端口时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; //配置为推挽输出,SDA
GPIO_SetBits(ADXL345_SDA_PORT,ADXL345_SDA_PIN);
// I2C_SCL_H;
// I2C_SDA_H;//均拉高
}
第二步,读取相相关数据位,连续读取6次,分别位X轴低四位、高四位数据和Y轴低四位、高四位数据以及Z轴高四位、低四位数据;
/读取数据函数
void adxl345_read_data(short *x,short *y,short *z)
{
u8 buf[6];
u8 i;
ADXL345_IIC_Start();
ADXL345_IIC_Send_Byte(slaveaddress); //发送写器件指令
ADXL345_IIC_Wait_Ack();
ADXL345_IIC_Send_Byte(0x32); //发送寄存器地址(数据缓存的起始地址为0X32)
ADXL345_IIC_Wait_Ack();
ADXL345_IIC_Start(); //重新启动
ADXL345_IIC_Send_Byte(regaddress); //发送读器件指令
ADXL345_IIC_Wait_Ack();
for(i=0;i<6;i++)
{
if(i==5)buf[i]=ADXL345_IIC_Read_Byte(0);//读取一个字节,不继续再读,发送NACK
else buf[i]=ADXL345_IIC_Read_Byte(1); //读取一个字节,继续读,发送ACK
}
ADXL345_IIC_Stop(); //产生一个停止条件
*x=(short)(((u16)buf[1]<<8)+buf[0]); //合成数据
*y=(short)(((u16)buf[3]<<8)+buf[2]);
*z=(short)(((u16)buf[5]<<8)+buf[4]);
}
第三步,连续读取数据,对数据进行处理(取平均值)
//times 取平均值的次数
void adxl345_read_average(float *x,float *y,float *z,u8 times)
{
u8 i;
short tx,ty,tz;
*x=0;
*y=0;
*z=0;
if(times)//读取次数不为0
{
for(i=0;i<times;i++)//连续读取times次
{
adxl345_read_data(&tx,&ty,&tz);
*x+=tx;
*y+=ty;
*z+=tz;
delay_ms(5);
}
*x/=times;
*y/=times;
*z/=times;
}
}
第四步,根据读取数据,计算XYZ三轴角度值
void get_angle(float *x_angle,float *y_angle,float *z_angle)
{
float ax,ay,az;
adxl345_read_average(&ax,&ay,&az,10);
*x_angle=atan(ax/sqrt((az*az+ay*ay)))*180/3.14;
*y_angle=atan(ay/sqrt((ax*ax+az*az)))*180/3.14;
*z_angle=atan(sqrt((ax*ax+ay*ay)/az))*180/3.14;
//return x_angle;
}
第五步,判断X、Y、Z轴角度值变化,来增加步数
float x_angle,y_angle,z_angle; //用于存储计算的角度值
float x_angle1,y_angle1,z_angle1; //同样用于存储角度值,用于比较角度值的变化
float x_angle2,y_angle2,z_angle2; //同样用于存储角度值,用于比较角度值的变化
get_angleint Step_count(void)
{
int step=0;
get_angle(&x_angle,&y_angle,&z_angle);
x_angle1 = x_angle;y_angle 1= y_angle; z_angle1 = z_angle;
x_angle2 = x_angle - x_angle1;
y_angle2 = y_angle - y_angle1;
z_angle2 = z_angle - z_angle1;
if(abs(x_angle2 )>10) //判断X、Y、Z轴角度值是否发生突变
{
if(abs(y_angle2 )>30)
{
if(abs(z_angle2 )>5)
{
step++; //步数加1
}
}
}
}
至此,ADXL345模块加速度进而获取步数变化功能实现;
四、文献出处
1.本片所涉及软件代码基于APM32E030官方SDK库,可在官网直接下载使用;
https://www.geehy.com/design/document
2.本问所设计参考文章有:
https://blog.csdn.net/qq_44016222/article/details/141891589?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522e7af7917f40c999f2c8cc5d843e92589%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=e7af7917f40c999f2c8cc5d843e92589&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-141891589-null-null.142^v102^pc_search_result_base6&utm_term=ADXL345&spm=1018.2226.3001.4187
-----ADXL345加速度传感器(IIC协议 STM32)-CSDN博客
https://blog.csdn.net/qq_44016222/article/details/141892460?ops_request_misc=%257B%2522request%255Fid%2522%253A%25221463bb2ffa789bbc3e1f5c680aba8df2%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=1463bb2ffa789bbc3e1f5c680aba8df2&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-2-141892460-null-null.142^v102^pc_search_result_base6&utm_term=max30102%E5%BF%83%E7%8E%87%E8%A1%80%E6%B0%A7%E4%BC%A0%E6%84%9F%E5%99%A8&spm=1018.2226.3001.4187 -----MAX30102心率血氧传感器(IIC协议 STM32)-CSDN博客
3.由于篇幅原因,该项目分为上下两章详细描述;
最后,本项目已在GitHub上开源:
https://github.com/Yulan-S/SmartWatch/tree/main