隨着生活質量的提高和生活節奏的加快,人們愈加需要關注自己的健康狀況,本項目意在設計一種基於雲平台+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界面
欲獲得源碼請持續關注我

