随着生活质量的提高和生活节奏的加快,人们愈加需要关注自己的健康状况,本项目意在设计一种基于云平台+APP+设备端的身体参数测试系统,利用脉搏传感器、红外传感器、微弱信号检测电路等实现人体参数的采集,数据通过无线网或其他方式上传云端存储,并提供网页端交互界面,为用户构建一种人体参数管理平台。
所用物料及实物图
主控:STM32F103
编译环境:MDK4.7
RT-Thread版本:2.0.0
【硬件设计】
1.MCU系统电路
本系统采用STM32103C8T6,其作为主控芯片一方面对传感器数据进行采集,另一方面将数据通过算法进行处理,并转发到云服务器,因此在电路设计时将两个ADC接口接入传感器。对于STM32系统,其必要组成部分还包括了启动模式选择电路、晶振复位电路等,在设计时还我另外加入了指示灯与按键作为备用。STM32系统电路如图4所示。STM32的供电电压以及心率、温度传感器的电压都是3.3V,因此如果采用5V电压供电则还需要进行电压转换,本系统采用了LDO稳压器LM1117将5V转为3.3V。对于电源和开关的部分,系统采用MICO USB接口进行供电和下载程序,该部分电路如图所示。
STM32系统电路
电源开关电路
2.USB转串口电路
利用USB作为系统程序下载接口,需要对其电平进行转换才能与STM32的串口进行通信,本系统采用了CP2102作为转换芯片,CP2102集成度高,内置USB2.0全速功能控制器、USB收发器、晶体振荡器、EEPROM及异步串行数据总线(UART),支持调制解调器全功能信号,无需任何外部的USB器件。CP2102与其他USB-UART转接电路的工作原理类似,通过驱动程序将PC的USB口虚拟成COM口以达到扩展的目的。该部分的电路设计图如图所示。
USB转串口电路
3.体温传感器电路
体温传感器利用热敏电阻与温度的特性曲线测量体温,采集的信号经过两级滤波和放大后传入STM32,温度测量的范围是30℃—44℃,采用3.3V电压供电时其温度对应的采集电压范围是2.127—1.193V。体温传感器的电路如图所示。
体温传感器电路
4.心率传感器电路
心率传感器采用了Pulse Sensor传感器,其算法开源、使用简便、成本低廉。它的原理是采用光电容积法,通过测量人体脉搏透光率来测量心跳,光电容积脉搏波描记法(PhotoPlethysmoGraphy,PPG)是借光电手段在活体组织中检测血液容积变化的一种无创检测方法。当一定波长的光束照射到指端皮肤表面时光束将通过透射或反射方式传送到光电接收器,在此过程中由于受到指端皮肤肌肉和血液的吸收衰减作用检测器检测到的光强度将减弱,其中皮肤肌肉组织等对光的吸收在整个血液循环中是保持恒定不变的,而皮肤内的血液容积在心脏作用下呈搏动性变化,当心脏收缩时外周血容量最多,光吸收量也最大,检测到的光强度最小;而在心脏舒张时正好相反。检测到的光强度最大使光接收器接收到的光强度随之呈脉动性变化,将此光强度变化信号转换成电信号便可获得容积脉搏血流的变化。该传感器采用峰值波长515nm的绿光LED结合光感都565nm的光感器APDS-9008来采集心率参数,由于脉搏信号频率较低,信号幅度很小,容易受到各种干扰,因此需要进行滤波和放大。在传感器后级采用了低通滤波器和运算放大器MCP-6001来滤波和放大信号。心率传感器的电路如图所示。
心率传感器电路
5.WiFi模块
WiFi模块采用了ESP8266模块,当使用该模块时需要设计其外部电路,包括电源电路、复位电路、模式选择电路等部分,设计完成的电路图如图所示。
WiFi模块电路
【软件设计】
1.主芯片程序设计
STM32的程序设计基于RT-Thread行开发。系统初始化之外,在主程序中,完成如下功能:
通过内部AD接口对传感器的AD数据进行采集;
将数据通过算法进行处理;
将处理好的数据打包提供WiFi模块发送给服务器;
喂狗。
按照以上4点功能进行设计,程序工作流程图如图所示。
主程序流程图
2.心率采集算法
心率采集算法的目标是找到瞬间心跳的连续时刻,并测量两者之间的时间间隔(IBI)。通过遵循PPG波形的可预测的形状和模式,我们能够做到这一点。当心脏将血液泵入人体时,每次搏动都会有一个脉冲波(有点像冲击波)沿着所有的动脉传到脉搏传感器附着的毛细血管组织的末端。实际的血液循环比脉搏波传播慢得多。
从下图所示的PPG上的T点开始跟踪事件。当脉搏波在传感器下方通过时,信号值迅速上升,然后信号回落到正常点。有时候,双向切口(向下尖峰)比其他更明显,但通常信号在下一个脉冲波冲洗之前稳定到背景噪声。由于波浪是重复的和可预测的,可以选择几乎任何可识别的特征作为参考点,比如峰值,并通过在每个峰值之间的时间计算心率。然而,这可能会从二分的切口中错误地读取,并且对基线噪声可能也是不准确的。理想情况下,想要找到心脏跳动的瞬间时刻需要准确的BPM计算,心率变异性(HRV)研究和脉搏传递时间(PTT)测量。
心跳PPG波形
对于心跳的计算,本算法在信号在快速上升过程中跨越波幅的50%的瞬间进行测量。BPM是从前10次IBI时间的平均值的每一个节拍中导出的。首先,要有足够高分辨率的正常采样率来获得每个节拍之间的时间的可靠测量。为此,我在STM32上使用了一个8位定时器,以便每隔一毫秒就会抛出一个中断。这样有了500Hz的采样率,以及2mS的节拍分辨率。接下来,需要跟踪PPG波的最高值和最低值,以获得精确的振幅测量值。变量P和T分别代表峰值和谷值。阈值变量初始化为512(模拟量范围的中间值),并在运行时间内变化,以跟踪振幅50%处的点,我们将在后面看到。在T更新之前必须经过3/5 IBI的时间段,以避免来自二分类缺口的噪音和错误读数。随后, 抓取一个大变量runningTotal来收集IBIs,然后将rate []的内容转移并添加到runnungTotal中。最早的IBI(11次前)不在位置0,而更新的IBI被置于位置9,接着对数组进行平均并计算BPM。最后要做的是设置QS标志。如果2.5秒内没有节拍事件,则用于查找心跳的变量将重新初始化为启动值。
通过使用定时器中断,我们的节拍查找算法在后台运行,并自动更新变量值。整体的算法流程图如图所示
3.服务器软件与网页设计
服务器端采用阿里云提供的云服务器,其数据传输协议是MQTT协议,测量采集端作为MQTT的设备端,云服务器作为MQTT的服务端,接收的数据存入SQL并通过网页展示,MQTT协议数据传输流程如图所示。
MQTT数据传输流程图
设计完成的网页如图
4.APP软件设计
移动终端APP第一次打开后进行手动配网,当搜索到指定的WIFI信号时进行连接,随后对TCP端口进行监听,对接受的数据包进行解析,随后将数据显示在屏幕上。设计完成的APP如下图。
5.上位机软件设计
上位机软件基于JAVA进行设计,通过端口接收测量终端传输的数据包,并进行解析,通过图形形象地展示出心率的实时状态,其工作界面如图所示。
【RTT使用简介】
本部分简单介绍了本系统中使用OLED和WIFI模块所涉及的SPI和串口通信在RTT中的使用过程,对函数的调用过程、关键函数的使用、设备驱动的调用分别进行了一些介绍。
1.OLED
OLED与芯片的通过SPI协议通信,设备驱动使用流程大致如下:
(1)定义设备对象,调用 rt_spi_bus_attach_device() 挂载设备到SPI总线
rt_err_t rt_spi_bus_attach_device(struct rt_spi_device *device, const char *name, const char *bus_name, void *user_data)
此函数用于挂载一个SPI设备到指定的SPI总线,向内核注册SPI设备,并将user_data保存到SPI设备device里。
参数 | 描述 |
---|---|
device | SPI设备句柄 |
name | SPI设备名称 |
bus_name | SPI总线名称 |
user_data | 用户数据指针 |
a. 首先需要定义好SPI设备对象device
b. SPI总线命名原则为spix, SPI设备命名原则为spixy,本项目的spi10 表示挂载在在 spi1设备。
c. SPI总线名称可以在msh shell输入list_device 命令查看,确定SPI设备要挂载的SPI总线。
d. user_data一般为SPI设备的CS引脚指针,进行数据传输时SPI控制器会操作此引脚进行片选。
本项目的底层驱动 drv_ssd1306.c 中 rt_hw_ssd1306_config() 挂载ssd1306设备到SPI总线源码如下:
#define SPI_BUS_NAME "spi1" /* SPI总线名称 */#define SPI_SSD1306_DEVICE_NAME "spi10" /* SPI设备名称 */static struct rt_spi_device spi_dev_ssd1306; /* SPI设备ssd1306对象 */static struct stm32_hw_spi_cs spi_cs; /* SPI设备CS片选引脚 */static int rt_hw_ssd1306_config(void){ rt_err_t res; /* oled use PC8 as CS */ spi_cs.pin = CS_PIN; rt_pin_mode(spi_cs.pin, PIN_MODE_OUTPUT); /* 设置片选管脚模式为输出 */res=rt_spi_bus_attach_device(&spi_dev_ssd1306,SPI_SSD1306_DEVICE_NAME, SPI_BUS_NAME, (void*)&spi_cs);if (res != RT_EOK){ OLED_TRACE("rt_spi_bus_attach_device!\r\n"); return res;}}
(2)调用 rt_spi_configure() 配置SPI总线模式。
挂载SPI设备到SPI总线后,为满足不同设备的时钟、数据宽度等要求,通常需要配置SPI模式、频率参数SPI从设备的模式决定主设备的模式,所以SPI主设备的模式必须和从设备一样两者才能正常通讯。
rt_err_t rt_spi_configure(struct rt_spi_device *device, struct rt_spi_configuration *cfg)
参数 | 描述 |
---|---|
device | SPI设备句柄 |
cfg | SPI传输配置参数指针 |
此函数会保存cfg指向的模式参数到device里,当device调用数据传输函数时都会使用此配置信息。
挂载SPI设备到SPI总线后必须使用此函数配置SPI设备的传输参数。
本项目底层驱动 drv_ssd1306.c 中 rt_hw_ssd1306_config() 配置SPI传输参数源码如下:
static int rt_hw_ssd1306_config(void){ /* config spi */ { struct rt_spi_configuration cfg; cfg.data_width = 8; cfg.mode = RT_SPI_MASTER | RT_SPI_MODE_0 | RT_SPI_MSB; cfg.max_hz = 20 * 1000 *1000; /* 20M,SPI max 42MHz,ssd1306 4-wire spi */ rt_spi_configure(&spi_dev_ssd1306, &cfg); }
(3) 使用 rt_spi_transfer() 等相关数据传输接口传输数据。
SPI设备挂载到SPI总线并配置好相关SPI传输参数后就可以调用RT-Thread提供的一系列SPI设备驱动数据传输函数。
struct rt_spi_message *rt_spi_transfer_message(struct rt_spi_device *device, struct rt_spi_message *message)
此函数可以传输一连串消息,用户可以很灵活的设置message结构体各参数的数值,从而可以很方便的控制数据传输方式。
发送指令和数据的函数源码如下:
rt_err_t ssd1306_write_cmd(const rt_uint8_t cmd){ rt_size_t len; rt_pin_write(DC_PIN, PIN_LOW); /* 命令低电平 */ len = rt_spi_send(&spi_dev_ssd1306, &cmd, 1); if (len != 1) { OLED_TRACE("ssd1306_write_cmd error. %d\r\n",len); return -RT_ERROR; } else { return RT_EOK; }}rt_err_t ssd1306_write_data(const rt_uint8_t data){ rt_size_t len; rt_pin_write(DC_PIN, PIN_HIGH); /* 数据高电平 */ len = rt_spi_send(&spi_dev_ssd1306, &data, 1); if (len != 1) { OLED_TRACE("ssd1306_write_data error. %d\r\n",len); return -RT_ERROR; } else { return RT_EOK; }}
(4)通过设备驱动的调用在OLED上显示图像和文字,首先需要确定信息在OLED上的行列起始地址,调用ssd1306_write_cmd() 向SSD1306发送指令,调用 ssd1306_write_data() 向SSD1306发送数据,源代码如下:
void set_column_address(rt_uint8_t start_address, rt_uint8_t end_address){ ssd1306_write_cmd(0x15); // Set Column Address ssd1306_write_data(start_address); // Default => 0x00 (Start Address) ssd1306_write_data(end_address); // Default => 0x7F (End Address)}void set_row_address(rt_uint8_t start_address, rt_uint8_t end_address){ ssd1306_write_cmd(0x75); // Set Row Address ssd1306_write_data(start_address); // Default => 0x00 (Start Address) ssd1306_write_data(end_address); // Default => 0x7F (End Address)}
2.串口
串口用来与WIFI 模块ESP8266进行通信,在串口的使用过程中,主要使用了以下几个函数进行初始化:
static void RCC_Configuration(void)
static void GPIO_Configuration(void)
static void NVIC_Configuration(struct stm32_uart *uart)
void rt_hw_usart_init();
(1)在void rt_hw_usart_init();
中对波特率、串口号、字长等进行设置。
实际的路径调用过程如下。
startup.c main()
-→ startup.c rtthread_startup()
-→ board.c rt_hw_board_init()
-→ usart.c rt_hw_usart_init()
(2)为了设备纳入到RTT的IO设备层中,需要为这个设备创建一个名为rt_device的数据结构。
该数据结构在rtdef.h中定义。需要一些函数来操作逻辑设备,这些函数在rt-thread/src/device.c文件中提供,它们是:
rt_err_t rt_device_register(rt_device_t dev, const char *name, rt_uint16_t flags)
将rt_device数据结构加入到RTT的设备层中,这个过程称为“注册”。RTT的设备管理层会为这个数据结构创建唯一的device_id。
rt_err_t rt_device_unregister(rt_device_t dev)
与注册相反,自然是注销了,将某个设备从RTT的设备驱动层中移除。
rt_device_t rt_device_find(const char *name)
根据设备的字符串名查找某个设备。
rt_err_t rt_device_init(rt_device_t dev)
通过调用rt_device数据结构中的init函数来初始设备。
rt_err_t rt_device_init_all(void)
初始化RTT设备管理层中的所有已注册的设备
rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflag)
通过调用rt_device数据结构中的open函数来打开设备。
rt_err_t rt_device_close(rt_device_t dev)
通过调用rt_device数据结构中的close函数来关闭设备。
rt_size_t rt_device_read(rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size)
通过调用rt_device数据结构中的read函数来从设备上读取数据。
rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size)
通过调用rt_device数据结构中的write函数来向设备写入数据(比如设备是flash,SD卡等,nand or nor flash等等)。
(3)open,read等函数的编写过程如下:
Ⅰ..init函数完成对设备数据结构的初始化工作。
RTT的设备驱动存在大量的预定义宏,它们在rtdef.h中定义。
static rt_err_t rt_serial_init (rt_device_t dev) { struct stm32_serial_device* uart = (struct stm32_serial_device*) dev->user_data; if (!(dev->flag & RT_DEVICE_FLAG_ACTIVATED)) {
if (dev->flag & RT_DEVICE_FLAG_INT_RX)
{
rt_memset(uart->int_rx->rx_buffer, 0, sizeof(uart->int_rx->rx_buffer));
uart->int_rx->read_index = 0;
uart->int_rx->save_index = 0;
} /* Enable USART */
USART_Cmd(uart->uart_device, ENABLE);
dev->flag |= RT_DEVICE_FLAG_ACTIVATED;
} return RT_EOK; }
Ⅱ.open
因为在usart.c中已经初始usart设备,然后init中通过USART_Cmd语句后,串口就会开始工作。因此open函数设置为空即可
close同colse,之间置空即可
Ⅲ.read
static rt_size_t rt_serial_read (rt_device_t dev, rt_off_t pos, void* buffer, rt_size_t size)
pos表示读写的位置,buffer是用于存储读取到数据的缓冲区。size为字节数目。对于USART这种串行的流设备来说,pos没有意义,因此这里的pos没有意义。 rt_device数据结构dev的的 user_data域存放了(struct stm32_serial_device*)型指针。【待修改】如果采用INT_RX模式,即中断接受模式,则主体代码为
while (size) { rt_base_t level; /* disable interrupt */ level = rt_hw_interrupt_disable(); if (uart->int_rx->read_index != uart->int_rx->save_index) { /* read a character */ *ptr++ = uart->int_rx->rx_buffer[uart->int_rx->read_index]; size--; /* move to next position */ uart->int_rx->read_index ++; if (uart->int_rx->read_index >= UART_RX_BUFFER_SIZE) uart->int_rx->read_index = 0; } else { /* set error code */ err_code = -RT_EEMPTY; /* enable interrupt */ rt_hw_interrupt_enable(level); break; } /* enable interrupt */ rt_hw_interrupt_enable(level); }
Ⅳ.write
向串口写入数据,即发送数据。
/* polling mode */
if (dev->flag & RT_DEVICE_FLAG_STREAM)
{
/* stream mode */
while (size)
{
if (*ptr == '\n')
{
while (!(uart->uart_device->SR & USART_FLAG_TXE));
uart->uart_device->DR = '\r';
/* interrupt mode Tx, does not support */
RT_ASSERT(0);
} while (!(uart->uart_device->SR & USART_FLAG_TXE));
uart->uart_device->DR = (*ptr & 0x1FF); ++ptr; --size;
}
}
else
{
/* write data directly */
while (size)
{
while (!(uart->uart_device->SR & USART_FLAG_TXE));
uart->uart_device->DR = (*ptr & 0x1FF); ++ptr; --size;
}
}
Ⅴ.control
static rt_err_t rt_serial_control (rt_device_t dev, rt_uint8_t cmd, void *args)
{
struct stm32_serial_device* uart; RT_ASSERT(dev != RT_NULL); uart = (struct stm32_serial_device*)dev->user_data;
switch (cmd)
{
case RT_DEVICE_CTRL_SUSPEND:
/* suspend device */
dev->flag |= RT_DEVICE_FLAG_SUSPENDED;
USART_Cmd(uart->uart_device, DISABLE);
break; case RT_DEVICE_CTRL_RESUME:
/* resume device */
dev->flag &= ~RT_DEVICE_FLAG_SUSPENDED;
USART_Cmd(uart->uart_device, ENABLE);
break;
} return RT_EOK;
}
Ⅶ.注册USART的rt_device结构
rt_err_t rt_hw_serial_register(rt_device_t device, const char* name, rt_uint32_t flag, struct stm32_serial_device *serial)
{
RT_ASSERT(device != RT_NULL); if ((flag & RT_DEVICE_FLAG_DMA_RX) ||
(flag & RT_DEVICE_FLAG_INT_TX))
{
RT_ASSERT(0);
} device->type = RT_Device_Class_Char;
device->rx_indicate = RT_NULL;
device->tx_complete = RT_NULL;
device->init = rt_serial_init;
device->open = rt_serial_open;
device->close = rt_serial_close;
device->read = rt_serial_read;
device->write = rt_serial_write;
device->control = rt_serial_control;
device->user_data = serial; /* register a character device */
return rt_device_register(device, name, RT_DEVICE_FLAG_RDWR | flag);
}
网页界面
在登录界面,用户输入自己的账户和密码进行登陆。
系统登陆界面
在数据查看面板,用户可以查看实时心跳和体温的测量数据与历史数据的曲线图。
数据查看界面
在个人信息界面,用户可以更新自己的个人信息,并可以绑定家人,可以查看家庭成员的数据与定位。
个人信息界面
同时本系统也提供定位信息的查看,用户可以在该界面找到使用者的定位信息。
地图定位界面
在消息提示界面,用户可以查看系统发送的消息,本系统具有健康预警的功能,对用户健康数据进行四个分级进行提醒。
消息提示界面
上位机与APP
在上位机界面,用户查看实时的测量曲线图;在APP界面,用户也可查看测量数据。
上位机界面
APP界面
欲获得源码请持续关注我