hehung · 2022年12月15日 · 四川

【GD32F427开发板试用】8. 脉搏监控仪

之前发帖

【GD32F427开发板试用】1. 串口实现scanf输入控制LED
【GD32F427开发板试用】2. RT-Thread标准版移植
【GD32F427开发板试用】3. 硬件IIC0驱动OLED显示中文
【GD32F427开发板试用】4. ADC采集摇杆模块移动量
【GD32F427开发板试用】5. SPI驱动TFTLCD屏幕
【GD32F427开发板试用】6. 定时器运用之精确定时1s
【GD32F427开发板试用】7. 移植LVGL到GD32F427V

前言

本文章实现了一个心率监控设备,可以通过脉搏传感器采集脉搏信息的ADC值并通过解算采集的信号,将信号转换成实际的脉搏值。该文章实现了如下功能:

  1. ADC采集心率数值并解算出心率值;
  2. SPI驱动TFTLCD显示心率值以及心率波形;(没有使用LVGL,本来想使用的,但是LVGL的lv_line功能画出来的曲线有断点,所以就自己写了波形显示函数)
  3. 基于RT-Thread实现

硬件连接

脉搏传感器

脉搏传感器使用的是Pulse Sensor,关于其描述,网上可以直接搜索到不在赘述,直接说下电路连接。
1.png

pulse sensorMCU
+3.3V
-GND
SPA1

原理简述:
心率传感器采集原理是,通过发出绿色光,测量反馈的光的强度,转换成ADC值并放大输出到S引脚,单片机将S引脚连接到ADC上即可,采集的ADC值就是心率变化值,通过计算出两个脉搏之间的距离就可以算出每分钟的心跳次数。

LCD连接

这块不在赘述,直接参考我之前文章即可:【GD32F427开发板试用】5. SPI驱动TFTLCD屏幕
本次驱动没有使用lvgl功能,因为我需要绘制曲线,使用lvgl的lv-line功能绘制出来的曲线存在断点,不够连续,没找到问题原因,所有我就自己通过画点来绘制了心率曲线图,效果还可以。

软件实现

本部分只说明实现的核心部分,整个工程在gitee上,有需要可以参考,见文章末尾。

ADC数据采集

采集ADC数据,并且转换为10bit精度,本来想直接使用10bit精度的,但是设置了发现没生效,不知道为什么。

/* 阅读采集自心率传感器的ADC值 */
static void Ps_ReadSampleValueFromAdc(void)
{
    rt_uint32_t value;

    /* 读取采样值 */
    value = Adc_ChSample(ADC_CHANNEL_1);

    /* 转换采集到的ADC值的精度为10bits */
    Signal = value>>2;
}

心率处理部分

此部分包含了采集的ADC信号中的心率信号进行识别的逻辑,代码中有比较详细的说明,本文不在赘述。

//心率采集相关变量
int BPM;                            //脉搏率==就是心率
int Signal;                         //传入的原始数据。
int IBI = 600;                      //节拍间隔,两次节拍之间的时间(ms)。计算:60/IBI(s)=心率(BPM)
bool Pulse = false;        //脉冲高低标志。当脉波高时为真,低时为假。
bool QS = false;           //当发现一个节拍时,就变成了真实。
int rate[10];                       //数组保存最后10个IBI值。
uint32_t sampleCounter = 0;    //用于确定脉冲定时。
uint32_t lastBeatTime = 0;     //用于查找IBI
int P = 512;                         //用于在脉冲波中寻找峰值
int T = 512;                        //用于在脉冲波中寻找波谷
int thresh = 512;                   //用来寻找瞬间的心跳
int amp = 100;                      //用于保持脉冲波形的振幅
int Num;
uint8_t firstBeat = true;     //第一个脉冲节拍
uint8_t secondBeat = false;   //第二个脉冲节拍,这两个变量用于确定两个节拍


/* 采集心率信号的处理函数 */
static void Ps_HeartRateDeal(void)
{
    unsigned int runningTotal;
    uint8_t i;

    sampleCounter += 2;
    Num = sampleCounter - lastBeatTime;             //监控最后一次节拍后的时间,以避免噪声

    //找到脉冲波的波峰和波谷
    if((Signal < thresh) && (Num > (IBI/5)*3))  //为了避免需要等待3/5个IBI的时间
    {
        if(Signal < T)
        {                                       //T是阈值
            T = Signal;                         //跟踪脉搏波的最低点,改变阈值
        }
    }
    if((Signal > thresh) && (Signal > P))       //采样值大于阈值并且采样值大于峰值
    {
        P = Signal;                             //P是峰值,改变峰值
    }
    //现在开始寻找心跳节拍
    if (Num > 250)              //避免高频噪声
    {
        if ((Signal > thresh) && (Pulse == false) && (Num > (IBI/5)*3))
        {
            Pulse = true;                               //当有脉冲的时候就设置脉冲信号。
            IBI = sampleCounter - lastBeatTime;         //测量节拍的ms级的时间
            lastBeatTime = sampleCounter;               //记录下一个脉冲的时间。
            if(secondBeat)          //如果这是第二个节拍,如果secondBeat == TRUE,表示是第二个节拍
            {
                secondBeat = false;                  //清除secondBeat节拍标志
                for(i=0; i<=9; i++)     //在启动时,种子的运行总数得到一个实现的BPM。
                {
                    rate[i] = IBI;
                }
            }
            if(firstBeat)           //如果这是第一次发现节拍,如果firstBeat == TRUE。
            {
                firstBeat = false;                   //清除firstBeat标志
                secondBeat = true;                   //设置secongBeat标志
                return;                              //IBI值是不可靠的,所以放弃它。
            }
            //保留最后10个IBI值的运行总数。
            runningTotal = 0;                  //清除runningTotal变量

            for(i=0; i<=8; i++)             //转换数据到rate数组中
            {
                rate[i] = rate[i+1];                  //去掉旧的的IBI值。
                runningTotal += rate[i];              //添加9个以前的老的IBI值。
            }

            rate[9] = IBI;                          //将最新的IBI添加到速率数组中。
            runningTotal += rate[9];                //添加最新的IBI到runningTotal。
            runningTotal /= 10;                     //平均最后10个IBI值。
            BPM = 60000/runningTotal;               //一分钟有多少拍。即心率BPM
            if(BPM>200)
                BPM=200;         //限制BPM最高显示值
            if(BPM<30)
                BPM=30;               //限制BPM最低显示值
            QS = true;
        }
    }

    if (Signal < thresh && Pulse == true)       //当值下降时,节拍就结束了。
    {
        Pulse = false;                         //重设脉冲标记,这样方便下一次的计数
        amp = P - T;                           //得到脉冲波的振幅。
        thresh = amp/2 + T;                    //设置thresh为振幅的50%。
        P = thresh;                            //重新设置下一个时间
        T = thresh;
    }

    if (Num > 2500)             //如果2.5秒过去了还没有节拍
    {
        thresh = 512;                          //设置默认阈值
        P = 512;                               //设置默认P值
        T = 512;                               //设置默认T值
        lastBeatTime = sampleCounter;          //把最后的节拍跟上来。
        firstBeat = true;                      //设置firstBeat为true方便下一次处理
        secondBeat = false;
    }
}

波形处理

此部分包含了TFTLCD需要显示的波形数据,将采集的波形数据放置到一个数组中,用于显示。

//波形处理函数,放在定时器中执行,20ms执行一次
static void Ps_WaveformDeal(uint16_t adc_value)
{
    int16_t temp;
    uint16_t i = 0;

    temp = adc_value - 224;
    temp = 500 - temp;
    if (temp < 0)
        temp = 0;
    else if (temp > 500)
        temp = 500;
    temp = (uint8_t)(temp/2);

#if (COMMON_USE_LVGL == COMMON_OFF)
    /* 超过LCD显示范围之后从新开始 */
    for(i = 0; i < (PS_WAVE_POINT_NUM-1); i++)
    {
        ps_waveformlist[i] = ps_waveformlist[i+1];
    }
    ps_waveformlist[PS_WAVE_POINT_NUM-1] = temp;
#else
    /* 超过LCD显示范围之后从新开始 */
    for(i = 0; i < (PS_WAVE_POINT_NUM-1); i++)
    {
        ps_waveformlist[i].x = i;
        ps_waveformlist[i].y = ps_waveformlist[i+1].y;
    }
    ps_waveformlist[PS_WAVE_POINT_NUM-1].x = PS_WAVE_POINT_NUM-1;
    ps_waveformlist[PS_WAVE_POINT_NUM-1].y = temp;
#endif

    QS = 0;
}

定时器中断

软件使能了一个2ms的定时器,用于没2ms采集一次数据并进行分析。

/* Timer interrupt service function */
void TIMER1_IRQHandler(void)
{
    if(SET == timer_interrupt_flag_get(TIMER1, TIMER_INT_UP))
    {
        /* Sample adc every 2ms */
        Ps_ReadSampleValueFromAdc();
        /* Calculate heart rate */
        Ps_HeartRateDeal();
        /* clear TIMER interrupt flag */
        timer_interrupt_flag_clear(TIMER1, TIMER_INT_UP);
    }
}

LCD显示函数

此部分包含了心率显示,心率采集波形显示等。

void Gui_Init(void)
{
    /* Initialize lcd */
    lcd_init();
    lcd_clear(WHITE);

    lcd_draw_font_gbk16(5, 0, BLUE, WHITE, "GD32F427V-START | aijishu.com | hehung");
}

static void Gui_MainFunction(void)
{
    char bpm_str[20];
    uint16_t x;
    uint16_t y;

    sprintf(bpm_str, "BPM:%d     ", Ps_GetBpm());
    lcd_draw_font_gbk16(5, 16, RED, WHITE, bpm_str);

    pulse_data_point = Ps_GetWaveformList();
    /* 显示波形 */
    LCD_CS_CLR;
    for (x = 0; x < PS_WAVE_POINT_NUM; x++)
    {
        for (y = 0; y < 200; y++)
        {
            if (pulse_data_point[x] == y)
            {
                lcd_draw_point(x, y+40, BLUE);
            }
            else if ((x != (PS_WAVE_POINT_NUM-1)) && (y > Gui_GetNumMin(pulse_data_point[x], pulse_data_point[x+1])) &&
                    (y < Gui_GetNumMax(pulse_data_point[x], pulse_data_point[x+1])))
            {
//                rt_kprintf("test:%d\n",y);
                lcd_draw_point(x, y+40, BLUE);
            }
            else
            {
                lcd_draw_point(x, y+40, WHITE);
            }
        }
    }
    LCD_CS_SET;
}

显示效果

左上角显示当前采集的心率值,下部显示心率波形。
2.png

3.png

演示视频见bilibili:
https://www.bilibili.com/vide...

程序代码

程序放置到了gitee,其中还包括了硬件IIC实现了OLED的驱动代码,只不过在这个工程中并没有使用。
https://gitee.com/hehung/GD32F427_pulse_monitor

推荐阅读
关注数
10708
内容数
187
中国高性能通用微控制器领域的领跑者兆易创新GD系列芯片技术专栏。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息