本項(xiàng)目為RT-Thread嵌入式大賽獲獎(jiǎng)作品,基于RT-Thread和兆易創(chuàng)新GD32F527I-EVAL的健康監(jiān)測(cè)站。
目錄

項(xiàng)目概述
系統(tǒng)硬件框架結(jié)構(gòu)
基礎(chǔ)驅(qū)動(dòng)程序?qū)崿F(xiàn)
整體驅(qū)動(dòng)實(shí)現(xiàn)
工程效果
演示視頻及代碼
演示視頻鏈接:https://www.bilibili.com/video/BV1WgUoBXE2n/?pop_share=1&vd_source=e1bd226340c8b87027d5dcfc6b0c3344
1 項(xiàng)目概述
1.1 項(xiàng)目背景
血氧、心率監(jiān)測(cè)是人們最常關(guān)心的,特別是一些特殊的群體比如老人,患有心血管系統(tǒng)、呼吸系統(tǒng)疾病的人。能方便及時(shí)的監(jiān)測(cè)到心率與血氧。本項(xiàng)目主要利用了開發(fā)板的大內(nèi)存、大屏幕,移植LVGL,能夠讓老年人也看得清楚。
1.2 系統(tǒng)功能介紹
基于GD32F527I-EVAL開發(fā)板實(shí)現(xiàn)如下功能:
使用管方的RTTherad庫,管理整個(gè)系統(tǒng)資源。
移植LCD驅(qū)動(dòng),給LVGL提供顯示基礎(chǔ)
移植觸摸驅(qū)動(dòng),給LVGL提供用戶輸入基礎(chǔ)
移植USART5,實(shí)現(xiàn)與傳感器的交互接口。
移植LVGL,設(shè)計(jì)GUI界面
實(shí)現(xiàn)傳感器驅(qū)動(dòng),實(shí)現(xiàn)血氧、心率的監(jiān)測(cè)。
1.3 系統(tǒng)使用的技術(shù)要點(diǎn)
整個(gè)系統(tǒng)由國(guó)產(chǎn)開源操作系統(tǒng)RT-Thead實(shí)現(xiàn)驅(qū)動(dòng)的模塊化設(shè)計(jì),以及系統(tǒng)調(diào)度。
硬件:GD32F527I-EVAL
開發(fā)板語言:C、GuiGuider
2 系統(tǒng)硬件框架結(jié)構(gòu)

2.1 TFT—LCD接口
開發(fā)板板載了LCD屏,其原理圖如下:

2.2 SDRAM接口
由于開發(fā)板上外擴(kuò)了RAM,給LVGL驅(qū)動(dòng)提供了大內(nèi)存空間,原理圖如下:

2.3 USART5接口
由于開發(fā)板的外設(shè)非常多,找到攝像頭接口的PC6、PC7作為USART5的接口:

3 基礎(chǔ)驅(qū)動(dòng)程序?qū)崿F(xiàn)
3.1 基礎(chǔ)工程
3.1.1 下載RT-Thread源碼
https://gitee.com/rtthread/rt-thread
下載源碼到本地。
3.1.2 同步并打包
下載好源碼后,進(jìn)入rt-thread/tree/master/bsp/gd32/arm/gd32527I-eval目錄下面執(zhí)行pkgs —upgrade-force同步
然后進(jìn)行pkgs —update
執(zhí)行 scons —target=mdk5
然后進(jìn)行 scons —dist 打包單獨(dú)的工程。
復(fù)制打包好的工程到單獨(dú)的目錄下。
現(xiàn)在單獨(dú)的工程就創(chuàng)建好了。
3.2 移植LVGL
創(chuàng)建好工程后,首先就需要生成用戶交互界面。我在論壇有單獨(dú)的作品:
https://club.rt-thread.org/ask/article/a2dba0eaa063757a.html
3.3 移植觸摸驅(qū)動(dòng)
LVGL需要驅(qū)動(dòng)觸摸屏,我也有單獨(dú)的作品:
https://club.rt-thread.org/ask/article/104ea5e6b33e788e.html
3.4 移植mks傳感器串口接口
詳見單獨(dú)作品:
https://club.rt-thread.org/ask/article/1074357ba9757cdb.html
4 整體驅(qū)動(dòng)實(shí)現(xiàn)
通過上的基工程的實(shí)現(xiàn),接下來就是整合驅(qū)動(dòng),實(shí)整個(gè)工程。
4.1 界面設(shè)計(jì)
4.1.1 GuiGuide設(shè)計(jì)
我使用開源的GuiGuider設(shè)了用戶交互界面:

控件1:btn_start,實(shí)現(xiàn)開始、停止測(cè)量的復(fù)用功能
控件2:label_heart 用于顯示測(cè)量到的心率值
控件3:label_spo2 用于顯示測(cè)量到的心率值
控件4:bar 用于顯示測(cè)量的進(jìn)度條
其余控件為固定標(biāo)簽。
4.1.2 事件添加
為btn_start 添加clicked事件

最后生成C語言的工程。
4.2 LVGL工程移植
4.2.1 復(fù)制guiguider工程
在生成工程的目錄中,我復(fù)制generated文件夾到基礎(chǔ)工程下的LVGL目錄下面,替換掉原來的generated文件夾。
4.2.2 添加文件到工程
在mdk中,將generated下面的所有.c/h添加到mdk工程中:

4.2.3 添加bnt事件代碼
根據(jù)傳感器的手冊(cè),我們開始測(cè)量時(shí),是向串口發(fā)送數(shù)據(jù)0x8A,停止是向串口發(fā)送0x88。
添加按鍵的的驅(qū)動(dòng)代碼如下:
extern lv_ui guider_ui;externintmks_cmd(int argc, char *argv[]);// 全局/靜態(tài)變量:控制進(jìn)度條和定時(shí)器(確保回調(diào)中可訪問)staticlv_timer_t *progress_timer = NULL; // 進(jìn)度條定時(shí)器staticint current_progress = 0; // 當(dāng)前進(jìn)度值(0-100)// 進(jìn)度條定時(shí)器回調(diào)函數(shù)(每隔 200ms 觸發(fā)一次)staticvoidprogress_timer_cb(lv_timer_t *timer){current_progress++; // 每次進(jìn)度 +1%(200ms × 100 = 20 秒)// 更新進(jìn)度條值(關(guān)閉動(dòng)畫,避免和定時(shí)器沖突)lv_bar_set_value(guider_ui.screen_bar, current_progress, LV_ANIM_OFF);// 進(jìn)度達(dá)到 100%,停止定時(shí)器并重置狀態(tài)if (current_progress >= 100) {lv_timer_del(progress_timer); // 刪除定時(shí)器(釋放資源)progress_timer = NULL; // 重置定時(shí)器指針current_progress = 0; // 重置進(jìn)度值char *argv[] = {"mks_cmd", "stop", NULL};int argc = 2;mks_cmd(argc, argv);// 可選:進(jìn)度完成后,自動(dòng)將按鈕切回「開始」lv_label_set_text(guider_ui.screen_btn_start_label, "Start");lv_bar_set_value(guider_ui.screen_bar, 0, LV_ANIM_OFF);}}staticvoidscreen_btn_start_event_handler(lv_event_t *e){lv_event_code_t code = lv_event_get_code(e);switch (code) {case LV_EVENT_CLICKED:{constchar *btn_text = lv_label_get_text(guider_ui.screen_btn_start_label);;if (btn_text != NULL && strcmp(btn_text, "Start") == 0) {rt_kprintf("start test\n");// 停止已存在的定時(shí)器(避免重復(fù)啟動(dòng),防止進(jìn)度加速)if (progress_timer != NULL) {lv_timer_del(progress_timer);progress_timer = NULL;}// 初始化進(jìn)度狀態(tài)current_progress = 0;lv_bar_set_range(guider_ui.screen_bar, 0, 100); // 設(shè)置進(jìn)度條范圍:0-100(百分比)lv_bar_set_value(guider_ui.screen_bar, 0, LV_ANIM_OFF); // 進(jìn)度條歸零// 創(chuàng)建并啟動(dòng)定時(shí)器:周期 200ms,觸發(fā)回調(diào)函數(shù)progress_timer = lv_timer_create(progress_timer_cb, 200, NULL);lv_timer_resume(progress_timer); // 啟動(dòng)定時(shí)器(LVGL 定時(shí)器默認(rèn)創(chuàng)建后啟動(dòng),保險(xiǎn)起見手動(dòng)調(diào)用)// 構(gòu)造參數(shù)并調(diào)用char *argv[] = {"mks_cmd", "start", NULL};int argc = 2;mks_cmd(argc, argv);lv_label_set_text(guider_ui.screen_btn_start_label, "Stop");}elseif (btn_text != NULL && strcmp(btn_text, "Stop") == 0) {// 停止并刪除定時(shí)器if (progress_timer != NULL) {lv_timer_del(progress_timer);progress_timer = NULL;}// 進(jìn)度條歸零,狀態(tài)重置current_progress = 0;lv_bar_set_value(guider_ui.screen_bar, 0, LV_ANIM_OFF);char *argv[] = {"mks_cmd", "stop", NULL};int argc = 2;mks_cmd(argc, argv);lv_label_set_text(guider_ui.screen_btn_start_label, "Start");lv_obj_center(guider_ui.screen_btn_start_label);}break;}default:break;}}
4.2.4 添加初始化gui代碼
在main.c中添加用于lvgl 心跳包的任務(wù),與初始化lvgl的代碼:
lv_ui guider_ui;staticrt_thread_t lvgl_task_thread = RT_NULL;staticrt_thread_t lvgl_tick_thread = RT_NULL;staticvoidlvgl_tick_thread_entry(void *parameter){while (1){lv_tick_inc(5);rt_thread_delay(5);}}staticvoidlvgl_task_thread_entry(void *parameter){lv_init();lcd_init();lv_port_disp_init();lv_port_indev_init();setup_ui(&guider_ui);setup_ui(&guider_ui);events_init(&guider_ui);while (1){lv_task_handler();rt_thread_delay(10);}}intmain(void){/* set LED1 pin mode to output */rt_pin_mode(LED1_PIN, PIN_MODE_OUTPUT);exmc_synchronous_dynamic_ram_init(EXMC_SDRAM_DEVICE0);//在這里啟用兩個(gè)任務(wù)lvgl_task_thread = rt_thread_create("lvgl_task", lvgl_task_thread_entry, RT_NULL, 4096, 25, 10);if (lvgl_task_thread != RT_NULL)rt_thread_startup(lvgl_task_thread);lvgl_tick_thread = rt_thread_create("lvgl_tick", lvgl_tick_thread_entry, RT_NULL, 512, 25, 10);if (lvgl_tick_thread != RT_NULL)rt_thread_startup(lvgl_tick_thread);while (1){rt_thread_mdelay(1000); // 主線程可以做其他事情}return RT_EOK;}
將程序下載到開發(fā)板后,按下按鍵,能在USART5上面看到的0x8A、0x88輸出,說明程序運(yùn)轉(zhuǎn)正常。
4.3 mks傳感器驅(qū)動(dòng)
4.3.1 mks傳感器通信協(xié)議:

根據(jù)通信協(xié)議進(jìn)行串口的代碼實(shí)現(xiàn)。
4.3.2 mks串口添加:
在menuconfig中,打開usart5,保存工程并重新生成工程。
4.3.3 mks串口驅(qū)動(dòng)實(shí)現(xiàn)
在驅(qū)動(dòng)中,按照rtthead srial的標(biāo)準(zhǔn)驅(qū)動(dòng)。
由于mkd傳感器的波特率為38400,因此需要在初始化后,重新配置serial的驅(qū)動(dòng)結(jié)構(gòu)體,并進(jìn)行配置。
定義他為uart5 實(shí)現(xiàn)中斷接收功能,詳見驅(qū)動(dòng)代碼如下:
接收傳感器并實(shí)現(xiàn)解碼,并實(shí)時(shí)將接收到的數(shù)據(jù)通過LVGL顯示到界面中。
/* 用于接收消息的信號(hào)量 */staticstructrt_semaphorerx_sem;staticrt_device_t serial;// --- 全局變量定義 ---int8_t mks_waveform_data[MKS_WAVEFORM_SAMPLES];uint8_t mks_heart_rate = 0;uint8_t mks_spo2 = 0;volatileuint8_t mks_new_data_flag = 0; // Use volatile as it might be checked in timer/ISR context later// --- 內(nèi)部接收緩沖區(qū) ---staticuint8_t mks_rx_buffer[MKS_PACKET_SIZE];staticuint8_t mks_bytes_received = 0;// lvglextern lv_ui guider_ui;staticchar buf[4];/* 接收數(shù)據(jù)回調(diào)函數(shù) */staticrt_err_tuart_input(rt_device_t dev, rt_size_t size){/* 串口接收到數(shù)據(jù)后產(chǎn)生中斷,調(diào)用此回調(diào)函數(shù),然后發(fā)送接收信號(hào)量 */rt_sem_release(&rx_sem);return RT_EOK;}/*** 串口接受線程* @param parameter*/staticvoidserial_thread_entry(void *parameter){char ch;mks_bytes_received = 0; // Reset buffer statemks_new_data_flag = 0; // 初始無新數(shù)據(jù)// 初始化數(shù)據(jù)為0memset(mks_rx_buffer,0, RECEIVE_LENGTH);mks_heart_rate = 0;mks_spo2 = 0;while (1){/* 從串口讀取一個(gè)字節(jié)的數(shù)據(jù),沒有讀取到則等待接收信號(hào)量 */while (rt_device_read(serial, -1, &ch, 1) != 1){/* 阻塞等待接收信號(hào)量,等到信號(hào)量后再次讀取數(shù)據(jù) */rt_sem_take(&rx_sem, RT_WAITING_FOREVER);}if(ch == PACKET_HEADER){mks_bytes_received = 1;mks_new_data_flag = 0;mks_rx_buffer[0] = (uint8_t)ch;}else{if(mks_bytes_received>0){//mks_bytes_received++;mks_rx_buffer[mks_bytes_received++] = (uint8_t)ch;if(mks_bytes_received >= RECEIVE_LENGTH){mks_new_data_flag = 1;mks_bytes_received = 0;sprintf(buf, "%d", mks_rx_buffer[65]);lv_label_set_text(guider_ui.screen_label_heart, buf); //更新到LVGLsprintf(buf, "%d", mks_rx_buffer[66]);lv_label_set_text(guider_ui.screen_label_spo2, buf);//更新到LVGLmemset(mks_rx_buffer,0, RECEIVE_LENGTH);mks_new_data_flag = 0;}}}}}staticintuart_sample(int argc, char *argv[]){rt_err_t ret = RT_EOK;struct serial_configurecfg; // 配置結(jié)構(gòu)體char uart_name[RT_NAME_MAX];char str[] = "hello RT-Thread!\r\n";if (argc == 2){rt_strncpy(uart_name, argv[1], RT_NAME_MAX);}else{rt_strncpy(uart_name, SAMPLE_UART_NAME, RT_NAME_MAX);}/* 查找系統(tǒng)中的串口設(shè)備 */serial = rt_device_find(uart_name);if (!serial){rt_kprintf("find %s failed!\n", uart_name);return RT_ERROR;}cfg.baud_rate = BAUD_RATE_38400; // 目標(biāo)波特率(可改為 38400、19200 等)cfg.data_bits = DATA_BITS_8; // 8 數(shù)據(jù)位cfg.stop_bits = STOP_BITS_1; // 1 停止位cfg.parity = PARITY_NONE; // 無校驗(yàn)cfg.bit_order = BIT_ORDER_LSB; // 低位優(yōu)先(默認(rèn))cfg.invert = NRZ_NORMAL;cfg.bufsz = RT_SERIAL_RB_BUFSZ;cfg.flowcontrol = RT_SERIAL_FLOWCONTROL_NONE;cfg.reserved = 0;rt_device_control(serial, RT_DEVICE_CTRL_CONFIG, &cfg);/* 初始化信號(hào)量 */rt_sem_init(&rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO);/* 以中斷接收及輪詢發(fā)送模式打開串口設(shè)備 */rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);/* 設(shè)置接收回調(diào)函數(shù) */rt_device_set_rx_indicate(serial, uart_input);/* 發(fā)送字符串 */rt_device_write(serial, 0, str, (sizeof(str) - 1));/* 創(chuàng)建 serial 線程 */rt_thread_t thread = rt_thread_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10);/* 創(chuàng)建成功則啟動(dòng)線程 */if (thread != RT_NULL){rt_thread_startup(thread);}else{ret = RT_ERROR;}return ret;}intmks_cmd(int argc, char *argv[]){rt_err_t ret = RT_EOK;uint8_t send_data = 0; // 要發(fā)送的字節(jié)數(shù)據(jù)// 第一步:檢查串口設(shè)備是否已初始化(首次使用時(shí)查找并打開)if (serial == RT_NULL){// 查找串口設(shè)備serial = rt_device_find(SAMPLE_UART_NAME);if (serial == RT_NULL){rt_kprintf("錯(cuò)誤:未找到串口設(shè)備 %s!\n", SAMPLE_UART_NAME);return RT_ERROR;}// 打開串口(RT_DEVICE_OFLAG_RDWR:讀寫模式)if (rt_device_open(serial, RT_DEVICE_OFLAG_RDWR) != RT_EOK){rt_kprintf("錯(cuò)誤:打開串口設(shè)備 %s 失敗!\n", SAMPLE_UART_NAME);serial = RT_NULL; // 打開失敗,重置句柄return RT_ERROR;}}// 第二步:解析命令參數(shù)if (argc == 2) // 僅支持 "mks start" 或 "mks stop"(2個(gè)參數(shù)){// 比較第二個(gè)參數(shù)(argv[1])if (strcmp(argv[1], "start") == 0){send_data = 0x8A; // start 對(duì)應(yīng)發(fā)送 0x8Art_kprintf("執(zhí)行 mks start,發(fā)送數(shù)據(jù):0x%02X\n", send_data);}elseif (strcmp(argv[1], "stop") == 0){send_data = 0x88; // stop 對(duì)應(yīng)發(fā)送 0x88rt_kprintf("執(zhí)行 mks stop,發(fā)送數(shù)據(jù):0x%02X\n", send_data);}else{// 無效參數(shù):提示正確用法rt_kprintf("錯(cuò)誤:無效命令參數(shù)!\n");rt_kprintf("正確用法:\n");rt_kprintf(" mks start - 發(fā)送 0x8A\n");rt_kprintf(" mks stop - 發(fā)送 0x88\n");return -RT_EINVAL;}// 第三步:通過串口發(fā)送數(shù)據(jù)(發(fā)送1個(gè)字節(jié))ret = rt_device_write(serial, 0, &send_data, 1);if (ret != 1) // rt_device_write 返回實(shí)際發(fā)送的字節(jié)數(shù),成功應(yīng)為 1{rt_kprintf("錯(cuò)誤:串口發(fā)送失敗!返回值:%d\n", ret);return -RT_ERROR;}}else{// 參數(shù)個(gè)數(shù)錯(cuò)誤:提示正確用法rt_kprintf("錯(cuò)誤:命令格式錯(cuò)誤!\n");rt_kprintf("正確用法:\n");rt_kprintf(" mks start - 發(fā)送 0x8A\n");rt_kprintf(" mks stop - 發(fā)送 0x88\n");return -RT_EINVAL;}return ret;}// 3. 導(dǎo)出命令到 FinSH 終端(支持在終端直接輸入 mks 命令)MSH_CMD_EXPORT(mks_cmd, mks command: mks start/stop);/* 導(dǎo)出到 msh 命令列表中 */MSH_CMD_EXPORT(uart_sample, uart device sample);
接收傳感器并實(shí)現(xiàn)解碼,并實(shí)時(shí)將接收到的數(shù)據(jù)通過LVGL顯示到界面中。
5 工程效果

6 演示視頻及代碼
演示視頻鏈接:https://www.bilibili.com/video/BV1WgUoBXE2n/?pop_share=1&vd_source=e1bd226340c8b87027d5dcfc6b0c3344
源代碼:https://club.rt-thread.org/file_download/a6fe3781d8bcf12d
非常感謝RTT論壇,通過這次參賽讓我進(jìn)一步的體會(huì)到了RT-Thread開源操作系統(tǒng)的強(qiáng)大。
RT-Thread Github 開源倉庫,歡迎撒個(gè)星(Star)支持,更期待你的代碼貢獻(xiàn): https://github.com/RT-Thread/rt-thread




想要在RT-Thread平臺(tái)或社區(qū)投放內(nèi)容?
或想?yún)⑴c相關(guān)直播活動(dòng)及賽事?
RT-Thread已開放對(duì)接窗口,
請(qǐng)通過郵件與我們?nèi)〉寐?lián)系,期待合作!
合作郵箱: tongfangyi@rt-thread.com
