基于RT-Thread和兆易创新GD32F527系列MCU的健康监测站 | 技术集结

描述

本项目为RT-Thread嵌入式大赛获奖作品,基于RT-Thread和兆易创新GD32F527I-EVAL的健康监测站。

目录


 

项目概述


 

系统硬件框架结构


 

基础驱动程序实现


 

整体驱动实现


 

工程效果


 

演示视频及代码


 

演示视频链接:https://www.bilibili.com/video/BV1WgUoBXE2n/?pop_share=1&vd_source=e1bd226340c8b87027d5dcfc6b0c3344

 

1 项目概述

1.1 项目背景

血氧、心率监测是人们最常关心的,特别是一些特殊的群体比如老人,患有心血管系统、呼吸系统疾病的人。能方便及时的监测到心率与血氧。本项目主要利用了开发板的大内存、大屏幕,移植LVGL,能够让老年人也看得清楚。

1.2 系统功能介绍

基于GD32F527I-EVAL开发板实现如下功能:

使用管方的RTTherad库,管理整个系统资源。

移植LCD驱动,给LVGL提供显示基础

移植触摸驱动,给LVGL提供用户输入基础

移植USART5,实现与传感器的交互接口。

移植LVGL,设计GUI界面

实现传感器驱动,实现血氧、心率的监测。

1.3 系统使用的技术要点

整个系统由国产开源操作系统RT-Thead实现驱动的模块化设计,以及系统调度。

硬件:GD32F527I-EVAL

开发板语言:C、GuiGuider

2 系统硬件框架结构

RT-Thread

2.1 TFT—LCD接口

开发板板载了LCD屏,其原理图如下:

RT-Thread

2.2 SDRAM接口

由于开发板上外扩了RAM,给LVGL驱动提供了大内存空间,原理图如下:

RT-Thread

2.3 USART5接口

由于开发板的外设非常多,找到摄像头接口的PC6、PC7作为USART5的接口:

RT-Thread
 

3 基础驱动程序实现

3.1 基础工程

3.1.1 下载RT-Thread源码

https://gitee.com/rtthread/rt-thread

下载源码到本地。

3.1.2 同步并打包

下载好源码后,进入rt-thread/tree/master/bsp/gd32/arm/gd32527I-eval目录下面执行pkgs —upgrade-force同步

然后进行pkgs —update

执行 scons —target=mdk5

然后进行 scons —dist 打包单独的工程。

复制打包好的工程到单独的目录下。

现在单独的工程就创建好了。

3.2 移植LVGL

创建好工程后,首先就需要生成用户交互界面。我在论坛有单独的作品:

https://club.rt-thread.org/ask/article/a2dba0eaa063757a.html

3.3 移植触摸驱动

LVGL需要驱动触摸屏,我也有单独的作品:

https://club.rt-thread.org/ask/article/104ea5e6b33e788e.html

3.4 移植mks传感器串口接口

详见单独作品:

https://club.rt-thread.org/ask/article/1074357ba9757cdb.html

4 整体驱动实现

通过上的基工程的实现,接下来就是整合驱动,实整个工程。

4.1 界面设计

4.1.1 GuiGuide设计

我使用开源的GuiGuider设了用户交互界面:

RT-Thread

控件1:btn_start,实现开始、停止测量的复用功能

控件2:label_heart 用于显示测量到的心率值

控件3:label_spo2 用于显示测量到的心率值

控件4:bar 用于显示测量的进度条

其余控件为固定标签。

4.1.2 事件添加

为btn_start 添加clicked事件

RT-Thread

最后生成C语言的工程。

4.2 LVGL工程移植

4.2.1 复制guiguider工程

在生成工程的目录中,我复制generated文件夹到基础工程下的LVGL目录下面,替换掉原来的generated文件夹。

4.2.2 添加文件到工程

在mdk中,将generated下面的所有.c/h添加到mdk工程中:

RT-Thread

4.2.3 添加bnt事件代码

根据传感器的手册,我们开始测量时,是向串口发送数据0x8A,停止是向串口发送0x88。

添加按键的的驱动代码如下:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

extern lv_ui guider_ui;externintmks_cmd(int argc, char *argv[]);// 全局/静态变量:控制进度条和定时器(确保回调中可访问)staticlv_timer_t *progress_timer = NULL;  // 进度条定时器staticint current_progress = 0;          // 当前进度值(0-100)// 进度条定时器回调函数(每隔 200ms 触发一次)staticvoidprogress_timer_cb(lv_timer_t *timer){    current_progress++;  // 每次进度 +1%(200ms × 100 = 20 秒)    // 更新进度条值(关闭动画,避免和定时器冲突)    lv_bar_set_value(guider_ui.screen_bar, current_progress, LV_ANIM_OFF);    // 进度达到 100%,停止定时器并重置状态    if (current_progress >= 100) {        lv_timer_del(progress_timer);  // 删除定时器(释放资源)        progress_timer = NULL;         // 重置定时器指针        current_progress = 0;          // 重置进度值        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_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");         // 停止已存在的定时器(避免重复启动,防止进度加速)         if (progress_timer != NULL) {             lv_timer_del(progress_timer);             progress_timer = NULL;         }         // 初始化进度状态         current_progress = 0;         lv_bar_set_range(guider_ui.screen_bar, 0, 100);  // 设置进度条范围:0-100(百分比)         lv_bar_set_value(guider_ui.screen_bar, 0, LV_ANIM_OFF);  // 进度条归零         // 创建并启动定时器:周期 200ms,触发回调函数         progress_timer = lv_timer_create(progress_timer_cb, 200, NULL);         lv_timer_resume(progress_timer);  // 启动定时器(LVGL 定时器默认创建后启动,保险起见手动调用)         // 构造参数并调用         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) {         // 停止并删除定时器         if (progress_timer != NULL) {         lv_timer_del(progress_timer);         progress_timer = NULL;         }         // 进度条归零,状态重置         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 心跳包的任务,与初始化lvgl的代码:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

#define LED1_PIN GET_PIN(E, 3)#define LVGL_TASK_PERIOD 10  // ms#define LVGL_TICK_PERIOD 5   // mslv_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);    //在这里启用两个任务    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;}

将程序下载到开发板后,按下按键,能在USART5上面看到的0x8A、0x88输出,说明程序运转正常。

4.3 mks传感器驱动

4.3.1 mks传感器通信协议:

RT-Thread

根据通信协议进行串口的代码实现。

4.3.2 mks串口添加:

在menuconfig中,打开usart5,保存工程并重新生成工程。

4.3.3 mks串口驱动实现

在驱动中,按照rtthead srial的标准驱动。

由于mkd传感器的波特率为38400,因此需要在初始化后,重新配置serial的驱动结构体,并进行配置。

定义他为uart5 实现中断接收功能,详见驱动代码如下:

接收传感器并实现解码,并实时将接收到的数据通过LVGL显示到界面中。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

#include"app.h"#include"string.h"#include#include#include#include"lvgl.h"#include"gui_guider.h"#define MAX_BUFFSIZE 128#define RECEIVE_LENGTH 88 #define PACKET_HEADER 0xFF#define SAMPLE_UART_NAME       "uart5"/* 用于接收消息的信号量 */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// --- 内部接收缓冲区 ---staticuint8_t mks_rx_buffer[MKS_PACKET_SIZE];staticuint8_t mks_bytes_received = 0;// lvglextern lv_ui guider_ui; staticchar buf[4];/* 接收数据回调函数 */staticrt_err_tuart_input(rt_device_t dev, rt_size_t size){    /* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */    rt_sem_release(&rx_sem);    return RT_EOK;}/** * 串口接受线程 * @param parameter */staticvoidserial_thread_entry(void *parameter){    char ch;    mks_bytes_received = 0;     // Reset buffer state    mks_new_data_flag = 0;  // 初始无新数据    // 初始化数据为0    memset(mks_rx_buffer,0, RECEIVE_LENGTH);    mks_heart_rate = 0;    mks_spo2 = 0;    while (1)    {        /* 从串口读取一个字节的数据,没有读取到则等待接收信号量 */        while (rt_device_read(serial, -1, &ch, 1) != 1)        {            /* 阻塞等待接收信号量,等到信号量后再次读取数据 */            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); //更新到LVGL                    sprintf(buf,  "%d", mks_rx_buffer[66]);                    lv_label_set_text(guider_ui.screen_label_spo2, buf);//更新到LVGL                    memset(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;  // 配置结构体    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);    }    /* 查找系统中的串口设备 */    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;    // 目标波特率(可改为 38400、19200 等)    cfg.data_bits = DATA_BITS_8;  // 8 数据位    cfg.stop_bits = STOP_BITS_1;  // 1 停止位    cfg.parity    = PARITY_NONE;  // 无校验    cfg.bit_order = BIT_ORDER_LSB;    // 低位优先(默认)    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);    /* 初始化信号量 */    rt_sem_init(&rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO);    /* 以中断接收及轮询发送模式打开串口设备 */    rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);    /* 设置接收回调函数 */    rt_device_set_rx_indicate(serial, uart_input);    /* 发送字符串 */    rt_device_write(serial, 0, str, (sizeof(str) - 1));    /* 创建 serial 线程 */    rt_thread_t thread = rt_thread_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10);    /* 创建成功则启动线程 */    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;  // 要发送的字节数据    // 第一步:检查串口设备是否已初始化(首次使用时查找并打开)    if (serial == RT_NULL)    {        // 查找串口设备        serial = rt_device_find(SAMPLE_UART_NAME);        if (serial == RT_NULL)        {            rt_kprintf("错误:未找到串口设备 %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("错误:打开串口设备 %s 失败!\n", SAMPLE_UART_NAME);            serial = RT_NULL;  // 打开失败,重置句柄            return RT_ERROR;        }    }    // 第二步:解析命令参数    if (argc == 2)  // 仅支持 "mks start" 或 "mks stop"(2个参数)    {        // 比较第二个参数(argv[1])        if (strcmp(argv[1], "start") == 0)        {            send_data = 0x8A;  // start 对应发送 0x8A            rt_kprintf("执行 mks start,发送数据:0x%02X\n", send_data);        }        elseif (strcmp(argv[1], "stop") == 0)        {            send_data = 0x88;  // stop 对应发送 0x88            rt_kprintf("执行 mks stop,发送数据:0x%02X\n", send_data);        }        else        {            // 无效参数:提示正确用法            rt_kprintf("错误:无效命令参数!\n");            rt_kprintf("正确用法:\n");            rt_kprintf("  mks start  - 发送 0x8A\n");            rt_kprintf("  mks stop   - 发送 0x88\n");            return -RT_EINVAL;        }        // 第三步:通过串口发送数据(发送1个字节)        ret = rt_device_write(serial, 0, &send_data, 1);        if (ret != 1)  // rt_device_write 返回实际发送的字节数,成功应为 1        {            rt_kprintf("错误:串口发送失败!返回值:%d\n", ret);            return -RT_ERROR;        }    }    else    {        // 参数个数错误:提示正确用法        rt_kprintf("错误:命令格式错误!\n");        rt_kprintf("正确用法:\n");        rt_kprintf("  mks start  - 发送 0x8A\n");        rt_kprintf("  mks stop   - 发送 0x88\n");        return -RT_EINVAL;    }    return ret;}// 3. 导出命令到 FinSH 终端(支持在终端直接输入 mks 命令)MSH_CMD_EXPORT(mks_cmd, mks command: mks start/stop);/* 导出到 msh 命令列表中 */MSH_CMD_EXPORT(uart_sample, uart device sample);

接收传感器并实现解码,并实时将接收到的数据通过LVGL显示到界面中。

5 工程效果

RT-Thread

6 演示视频及代码

演示视频链接:https://www.bilibili.com/video/BV1WgUoBXE2n/?pop_share=1&vd_source=e1bd226340c8b87027d5dcfc6b0c3344

源代码:https://club.rt-thread.org/file_download/a6fe3781d8bcf12d


 

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分