之前发帖
【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值并通过解算采集的信号,将信号转换成实际的脉搏值。该文章实现了如下功能:
- ADC采集心率数值并解算出心率值;
- SPI驱动TFTLCD显示心率值以及心率波形;(没有使用LVGL,本来想使用的,但是LVGL的lv_line功能画出来的曲线有断点,所以就自己写了波形显示函数)
- 基于RT-Thread实现
硬件连接
脉搏传感器
脉搏传感器使用的是Pulse Sensor,关于其描述,网上可以直接搜索到不在赘述,直接说下电路连接。
pulse sensor | MCU |
---|---|
+ | 3.3V |
- | GND |
S | PA1 |
原理简述:
心率传感器采集原理是,通过发出绿色光,测量反馈的光的强度,转换成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;
}
显示效果
左上角显示当前采集的心率值,下部显示心率波形。
演示视频见bilibili:
https://www.bilibili.com/vide...
程序代码
程序放置到了gitee,其中还包括了硬件IIC实现了OLED的驱动代码,只不过在这个工程中并没有使用。
https://gitee.com/hehung/GD32F427_pulse_monitor