查看: 47761|回复: 0

[评测分享] 【STM32H735-DK 测评】指尖血氧动态监护仪

[复制链接]
  • TA的每日心情
    开心
    昨天 08:20
  • 签到天数: 303 天

    连续签到: 34 天

    [LV.8]以坛为家I

    发表于 2024-3-25 17:59:21 | 显示全部楼层 |阅读模式
    分享到:
    本帖最后由 eefocus_3914144 于 2024-3-25 18:08 编辑

    【实验器材】
    STM32H735-DK开发板
    MAX30100血氧传感器
    【开发软件】
    TouchGFX 4.23.2 Designer
    STM32CubeMAX
    STM32CubeIDE
    MAX30100简介】
    MAX30100是一款集成有脉搏血氧仪和心率监测传感器的模块。该器件集成有两个LED、一个光电探测器, 经过优化的光学器件和低噪声模拟信号处理器,可检测脉搏血氧及心率信号。MAX30100采用1.8V和3.3V的电源电压。可通过软件来关断电源,待机模式下的电流消耗量可忽略不计,因而可以始终保持电源连接。并且 MAX30100采用iic通信方式
    【开发步骤】
    1、使用TouchGFX 4.23.2 Designer创建一个基于STM32H735-DK的空白工程。
    2、添加一个标签textState用于显示检测状态
    图片1.png
    3、再创建两个texArea,用于显示心率与血氧浓度:

    图片2.png 图片3.png 图片3.png
    4、放置两个图表控件,用于显示血氧与心跳的图形。
    图片4.png
    5、生成工程后使用stm32cubIDE打开工程,添加max30100的驱动max30100.c以及血氧数据采集的blood.cFFT分析algorithm.c三个文件。由于开发板给用户使用的i2c4触摸屏也在使用,由于他们的初始化的问题,我这次使用软件i2c来实现对max30100的驱动,所以添加了IICcom.c的驱动。添加好驱动后的工程如下图所示:
    图片5.png
    6、程序组织实现的流程图如下
    图片6.png
    大的流程为在freertos的任务中,周期的检测传感器的数据,如果达到显示的次数,测置显示标志为1,如果达到fft的采样次数,测进行fft数据处理后,更新状态标志与血氧、心率的数据。
    TouchGFXmodel中,使用tick来检测两个标志,分别向persenterview传递需要的显示的数据,并同时更新显示。
    【主要代码分析】
    1、max30100初始化:
    void max30100_init(void)
    {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Pin = GPIO_PIN_3;
    HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
    PY_usDelayTest();
    max30100_Bus_Write(0x06, 0x0b); //mode configuration : temp_en[3] MODE[2:0]=010 HR only enabled 011 SP02 enabled
    //max30100_Bus_Write(0x06, 0x0a); //MODE[2:0]=010 HR only enabled when used is mode ,the red led is not used.
    max30100_Bus_Write(0x01, 0xF0); //open all of interrupt
    max30100_Bus_Write(INTERRUPT_REG, 0x00); //all interrupt clear
    max30100_Bus_Write(0x09, 0x33); //r_pa=3,ir_pa=3

    #if (SAMPLES_PER_SECOND == 50)
    max30100_Bus_Write(0x07, 0x43); //SPO2_SR[4:2]=000 50 per second LED_PW[1:0]=11 16BITS
    #elif (SAMPLES_PER_SECOND == 100)
    max30100_Bus_Write(0x07, 0x47); //SPO2_SR[4:2]=001 100 per second LED_PW[1:0]=11 16BITS
    #elif (SAMPLES_PER_SECOND == 200)
    max30100_Bus_Write(0x07, 0x4F);
    #elif (SAMPLES_PER_SECOND == 400)
    max30100_Bus_Write(0x07, 0x53);
    #endif

    max30100_Bus_Write(0x02, 0x00); //set FIFO write Pointer reg = 0x00 for clear it
    max30100_Bus_Write(0x03, 0x00); //set Over Flow Counter reg = 0x00 for clear it
    max30100_Bus_Write(0x04, 0x0F); //set FIFO Read Pointer reg = 0x0f for
    //waitting write pointer eq read pointer to interrupts INTERRUPT_REG_A_FULL
    }
    我这次使用的IO为开发板上的CN4D8D9为模拟II2C的接口。在初始化时初始为普通IO输出。
    图片7.png
    同时使用PY_usDelayTest();初始化一个微秒的延时函数。经过初化后,MAX30100LED灯才能工作。
    2、同时实现三个写入与读取寄存器数据函数如下:
    uint8_t max30100_Bus_Write(uint8_t Register_Address, uint8_t Word_Data)
    {

    /* 采用串行EEPROM随即读取指令序列,连续读取若干字节 */
    I2C_Start();
    /* 1步:发起I2C总线启动信号 */
    /* 2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
    I2C_SendByte(max30100_WR_address | I2C_WR); /* 此处是写指令 */

    /* 3步:发送ACK */
    if (I2C_WaitAck() != 0)
    {
    goto cmd_fail; /* EEPROM器件无应答 */
    }

    /* 4步:发送字节地址 */
    I2C_SendByte(Register_Address);
    if (I2C_WaitAck() != 0)
    {
    goto cmd_fail; /* EEPROM器件无应答 */
    }

    /* 5步:开始写入数据 */
    I2C_SendByte(Word_Data);

    /* 6步:发送ACK */
    if (I2C_WaitAck() != 0)
    {
    goto cmd_fail; /* EEPROM器件无应答 */
    }

    /* 发送I2C总线停止信号 */
    I2C_Stop();
    return 1; /* 执行成功 */

    cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
    /* 发送I2C总线停止信号 */
    I2C_Stop();
    return 0;
    }


    uint8_t max30100_Bus_Read(uint8_t Register_Address)
    {
    uint8_t data;


    /* 1步:发起I2C总线启动信号 */
    I2C_Start();

    /* 2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
    I2C_SendByte(max30100_WR_address | I2C_WR); /* 此处是写指令 */

    /* 3步:发送ACK */
    if (I2C_WaitAck() != 0)
    {
    goto cmd_fail; /* EEPROM器件无应答 */
    }

    /* 4步:发送字节地址, */
    I2C_SendByte((uint8_t)Register_Address);
    if (I2C_WaitAck() != 0)
    {
    goto cmd_fail; /* EEPROM器件无应答 */
    }


    /* 6步:重新启动I2C总线。下面开始读取数据 */
    I2C_Start();

    /* 7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
    I2C_SendByte(max30100_WR_address | I2C_RD); /* 此处是读指令 */

    /* 8步:发送ACK */
    if (I2C_WaitAck() != 0)
    {
    goto cmd_fail; /* EEPROM器件无应答 */
    }

    /* 9步:读取数据 */
    {
    data = I2C_RadeByte(); /* 1个字节 */

    I2C_NoAck(); /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
    }
    /* 发送I2C总线停止信号 */
    I2C_Stop();
    return data; /* 执行成功 返回data*/

    cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
    /* 发送I2C总线停止信号 */
    I2C_Stop();
    return 0;
    }

    void max30100_FIFO_Read(uint8_t Register_Address,uint16_t Word_Data[][2],uint8_t count)
    {
    uint8_t i=0;
    uint8_t no = count;
    uint8_t data1, data2;
    /* 1步:发起I2C总线启动信号 */
    I2C_Start();

    /* 2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
    I2C_SendByte(max30100_WR_address | I2C_WR); /* 此处是写指令 */

    /* 3步:发送ACK */
    if (I2C_WaitAck() != 0)
    {
    goto cmd_fail; /* EEPROM器件无应答 */
    }

    /* 4步:发送字节地址, */
    I2C_SendByte((uint8_t)Register_Address);
    if (I2C_WaitAck() != 0)
    {
    goto cmd_fail; /* EEPROM器件无应答 */
    }


    /* 6步:重新启动I2C总线。下面开始读取数据 */
    I2C_Start();

    /* 7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
    I2C_SendByte(max30100_WR_address | I2C_RD); /* 此处是读指令 */

    /* 8步:发送ACK */
    if (I2C_WaitAck() != 0)
    {
    goto cmd_fail; /* EEPROM器件无应答 */
    }

    /* 9步:读取数据 */
    while (no)
    {
    data1 = I2C_RadeByte();
    I2C_Ack();
    data2 = I2C_RadeByte();
    I2C_Ack();
    Word_Data[0] = (((uint16_t)data1 << 8) | data2); //


    data1 = I2C_RadeByte();
    I2C_Ack();
    data2 = I2C_RadeByte();
    if(1==no)
    I2C_NoAck(); /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
    else
    I2C_Ack();
    Word_Data[1] = (((uint16_t)data1 << 8) | data2);

    no--;
    i++;
    }
    /* 发送I2C总线停止信号 */
    I2C_Stop();

    cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
    /* 发送I2C总线停止信号 */
    I2C_Stop();
    }
    #endif


    3血液检测信息更新
    void blood_data_update(void)
    {
    uint16_t temp_num=0;
    uint16_t fifo_word_buff[1][2];

    temp_num = max30100_Bus_Read(INTERRUPT_REG);

    //标志位被使能时 读取FIFO
    if (INTERRUPT_REG_A_FULL&temp_num)
    {
    //HAL_GPIO_WritePin(GPIOG,GPIO_PIN_5,1);
    //读取FIFO
    max30100_FIFO_Read(0x05,fifo_word_buff,1); //read the hr and spo2 data form fifo in reg=0x05

    //将数据写入fft输入并清除输出
    for(int i = 0;i < 1;i++)
    {
    if(g_fft_index < FFT_N)
    {
    s1[g_fft_index].real = fifo_word_buff[0];
    s1[g_fft_index].imag= 0;
    s2[g_fft_index].real = fifo_word_buff[1];
    s2[g_fft_index].imag= 0;
    g_fft_index++;
    }
    }
    //信息更新标志位
    g_blooddata.update++;
    }
    else
    {
    //HAL_GPIO_WritePin(GPIOG,GPIO_PIN_5,0);
    }
    }
    在这个程序中,读取标志位,如果使能则读取转后的数据,并更新到缓冲数组s1s2中,如果未达FFT的采集数量,测将数组列新到buffk。同时更新检测信息的标志位。
    4血液信息转换
    void blood_data_translate(void)
    {
    //缓冲区写入结束
    if(g_fft_index>=FFT_N)
    {
    //开始变换显示
    //Gui_DrawFont_GBK16(102,2,BLACK,GREEN,"FFT");

    g_fft_index = 0;

    //数据更新标志位
    g_blooddata.display = 1;

    //快速傅里叶变换
    FFT(s1);
    FFT(s2);

    //解平方
    for(int i = 0;i < FFT_N;i++)
    {
    s1.real=sqrtf(s1.real*s1.real+s1.imag*s1.imag);
    s2.real=sqrtf(s2.real*s2.real+s2.imag*s2.imag);
    }

    //读取峰值点 结果的物理意义为
    uint16_t s1_max_index = find_max_num_index(s1, 60);
    uint16_t s2_max_index = find_max_num_index(s2, 60);

    //检查HbO2Hb的变化频率是否一致
    if(s1_max_index == s2_max_index)
    {
    //心率计算
    uint16_t Heart_Rate = 60 * SAMPLES_PER_SECOND *
    s2_max_index / FFT_N;

    g_blooddata.heart = Heart_Rate - 10;

    //血氧含量计算
    float sp02_num = (s2[s1_max_index].real * s1[0].real)
    /(s1[s1_max_index].real * s2[0].real);

    sp02_num = (1 - sp02_num) * SAMPLES_PER_SECOND + CORRECTED_VALUE;

    g_blooddata.SpO2 = sp02_num;

    //状态正常
    g_blooddata.state = BLD_NORMAL;
    }
    else //数据发生异常
    {
    g_blooddata.heart = 0;
    g_blooddata.SpO2 = 0;
    g_blooddata.state = BLD_ERROR;
    }
    //结束变换显示
    //Gui_DrawFont_GBK16(102,2,GREEN,BLACK,"FFT");
    }
    }
    如果是达到了FFT的数据采集数量,则进行fft转换,得到血氧、心率的数值,同时更新标志位,通知显示任务进行显示。

    5、在TouchGFXmodel.ctick周期函数中,实现图表更新
    if(g_blooddata.update >= 8)
    {
    //清除图标更新标志位
    g_blooddata.update = 0;
    //血液波形数据更新
    blood_wave_update();
    //绘制波形
    //tft_draw_wave();
    modelListener->draw_wave( g_BloodWave.HpO2*5,g_BloodWave.Hp*5);
    }
    //转换后的数据更新
    if(g_blooddata.display >= 1)
    {
    //清除更新标志位
    g_blooddata.display = 0;
    //显示血液状态信息
    modelListener->max30100_draw_State(g_blooddata.state,g_blooddata.SpO2,g_blooddata.heart);
    //心率血氧数据刷新
    //tft_draw_hrsp();
    }
    7、view的显示函数:
    void Screen1View::max30100_draw_State(bool state, float SpO2, int heart)
    {

    if(!state)
    {
    Unicode::snprintf(textStateBuffer, 20, "Nomarl");
    textState.invalidate(); //刷新
    }
    else
    {
    Unicode::snprintf(textStateBuffer, 20, "ERROR");
    textState.invalidate(); //刷新
    }
    Unicode::snprintf(textHRBuffer, TEXTHR_SIZE, "%d", heart);
    textHR.invalidate();
    Unicode::snprintfFloat(textSpo2Buffer, TEXTSPO2_SIZE, "%.1f",SpO2);
    textSpo2.invalidate();
    //把数据添加进曲线


    }

    void Screen1View::draw_wave(float SpO2,int heart)
    {
    dynamicGraHR.addDataPoint(heart);
    dynamicGraphSpO2.addDataPoint(SpO2);
    }
    函数max30100_draw_State实现了对状态、血氧、心率的更新。函数draw_wave就是简单的新点新增到图表中,由TouchGFX实现图形的自主显示。
    【总结】
    MAX30100的工程实现是学习githug上的基于stm32f103的代码工程进行移植实现的。相比于使用tft的画线提示,TouchGFX给了强大的图形实现工程,只需要通过简单的设计,就可以实现复杂的功能。

    【视频分享】


    回复

    使用道具 举报

    您需要登录后才可以回帖 注册/登录

    本版积分规则

    关闭

    站长推荐上一条 /4 下一条

    手机版|小黑屋|与非网

    GMT+8, 2024-11-24 07:13 , Processed in 0.115215 second(s), 17 queries , MemCache On.

    ICP经营许可证 苏B2-20140176  苏ICP备14012660号-2   苏州灵动帧格网络科技有限公司 版权所有.

    苏公网安备 32059002001037号

    Powered by Discuz! X3.4

    Copyright © 2001-2024, Tencent Cloud.