简言

我之前学linux的时候,觉得linux的命令很酷,最近又有写代码的热情,于是手撸了一个串口终端。

初次使用

介绍特性

  1. 像终端一样使用

    • 输入niubi后,键入回车,输出自己写的回调函数中的内容。

  2. 支持tab键补全

    • 按下n后按下tab键后,到库中匹配关键词,匹配到niubi,显示到下一行。

    • 当有多个关键词可以匹配时,提示匹配关键词,并在新的一行显示刚刚输入的内容。

  3. 支持Backspace键

    • 当输入有误时,可以按Backspace键,回退到上一格。

宏定义的定义与配置

yxy_open_cmd.h 中修改对应的宏:

  • 主要修改第一个串口句柄的宏,以及最后一个换行的宏,其他的根据需求来修改。

初始化

全局区(添加自定义终端指令变量和对应的回调函数)

// 全局区
User_USART_RX uClear;/* 关键词clear回调函数 */
static void clear_callback(void)
{yxy_DEBUG("\x1b[2J\x1b[H");
}

main函数

// main
uart_RX_init();rx_Pack_add(&uClear, (uint8_t *)"clear", str_len((uint8_t *)"clear"), (uint8_t *)"清屏", clear_callback);

使用

循环


uart_rx_scan();
HAL_Delay(1);

效果

主要代码

yxy_open_cmd.c

/* 头文件 */
#include "yxy_open_cmd.h"
#include "stdlib.h"
#include "stdarg.h"
#include "string.h"
#include "usart.h"
#include "yxy_debug.h"/* 全局变量 */// 接收数据结构体
rx_Struct rx_InitStr;// 包链表数据头节点
struct pack_struct * packhandNode = NULL;// 轮询调用函数指针,以处理对应的包函数
pack_Callback scanHandle = NULL;// 添加 过 的特殊包的总数量
uint16_t packIdNum = 0;// 屏幕上的数据数量
uint32_t cmd_buff = 0;/**********************   内部函数区域*********************/static HAL_StatusTypeDef string_contrast(uint8_t * s1, uint16_t s1Len, uint8_t * s2);
static uint16_t string_compare_hand(uint8_t * s1, uint8_t * s2);
static void rx_struct_Clear(void);
static void pack_classify(void);/*** @brief  字符串对比函数* @param  字符串1* @param  字符串2* @retval 返回HAL_OK,匹配成功;返回HAL_ERROR,匹配失败。*/
static HAL_StatusTypeDef string_contrast(uint8_t * s1, uint16_t s1Len, uint8_t * s2)
{uint32_t len = s1Len;uint8_t *pS1 = s1, *pS2 = s2;while(len>0){//      yxy_DEBUG("s1: %c, S2: %c\r\n", *pS1, *pS2);if(*pS1 != *pS2){//          yxy_DEBUG("%s != %s\r\n", s1, s2);return HAL_ERROR;}pS1++;pS2++;len--;}return HAL_OK;
}/*** @brief  子串比较函数* @param  子串1* @param  子串2* @retval 从头比较的相同字符的子串长度*/
static uint16_t string_compare_hand(uint8_t * s1, uint8_t * s2)
{uint16_t sonLen = 0;uint16_t l1=0, l2=0;uint8_t *pS1=s1, *pS2=s2;l1 = str_len(s1);l2 = str_len(s2);//   yxy_DEBUG("comp : l1=%d, l2=%d\r\n", l1, l2);// 比较 关键词串 和 接收串,一旦 接收串>=关键词串,则不用执行tab。if(l1 <= l2){return  0;}while(*pS1 == *pS2){pS1++;pS2++;sonLen++;}return sonLen;
}/* 接收结构体清除 */
static void rx_struct_Clear(void)
{int i;for(i=0; i<RX_BUF_SIZE; i++){rx_InitStr.rx_buf[i] = 0;}rx_InitStr.a_rx_buf = 0;rx_InitStr.rx_cnt = 0;
}/*** @brief  tab处理函数,用于查找并适配库中的文本* @retval 无*/
static void tab_handler(void)
{uint16_t maxLen = 0;  // 记录最大长度uint16_t middleLen = 0; // 记录一次比较的长度uint16_t sameCount = 0;  // 记录相同长度的计数uint16_t wrapCnt = 0;  // 相同关键词的换行计数,一行超过5个,则换行uint8_t *pRxBuf = rx_InitStr.rx_buf;struct pack_struct * pTemp[5] = {NULL}; // 用来存储子串相同的struct pack_struct * pPN = packhandNode->packNext;while(pPN != NULL){middleLen = string_compare_hand(pPN->special, pRxBuf);if((maxLen == middleLen) && (maxLen != 0)){pTemp[sameCount++] = pPN;}if(maxLen < middleLen){maxLen = middleLen;sameCount = 0;pTemp[sameCount++] = pPN;}pPN = pPN->packNext;}// 如果只有1个元素if(sameCount == 1){while(rx_InitStr.rx_cnt <= pTemp[0]->specialLen){rx_InitStr.rx_buf[rx_InitStr.rx_cnt] = pTemp[0]->special[rx_InitStr.rx_cnt];rx_InitStr.rx_cnt++;}rx_InitStr.rx_cnt -= 1;yxy_DEBUG("\r\n%s", pTemp[0]->special);return;}// 如果有一堆元素yxy_DEBUG("\r\n");while(sameCount--){// 一行过多字符换行wrapCnt++;if(wrapCnt >= 3){yxy_DEBUG("\r\n");wrapCnt=0;}// 显示有可能的字符yxy_DEBUG("%s  ", pTemp[sameCount]->special);}yxy_DEBUG("\r\n");// 显示原本输入的
//  yxy_DEBUG("\r\nux cnt %d\r\n", rx_InitStr.rx_cnt);yxy_DEBUG("%s", rx_InitStr.rx_buf);
}/*** @brief  包处理分类函数* @retval */
static void pack_classify(void)
{uint16_t iCnt = 0;uint8_t *pRxBuf = rx_InitStr.rx_buf;struct pack_struct * pPN = packhandNode->packNext; // 节点特殊字节点while(pPN){// 如果输入的词句与关键词匹配成功if(string_contrast(pPN->special, pPN->specialLen, pRxBuf) == HAL_OK){// 算出关键词后的,数据的长度pPN->userRX->dataLen = str_len(pRxBuf) - pPN->specialLen;// 赋值关键词的数据while(iCnt < pPN->userRX->dataLen){pPN->userRX->dataVal[iCnt] = pRxBuf[pPN->specialLen+iCnt];iCnt++;}pPN->userRX->dataVal[iCnt] = '\0';//           yxy_DEBUG("keyword data      : %s \r\n", pPN->userRX->dataVal);
//          yxy_DEBUG("keyword data size : %d \r\n", pPN->userRX->dataLen);
//          yxy_DEBUG("keyword           : %s be ready.\r\n\r\n", pPN->special);// 则置位在 scanHandle 变量中,等待下次在扫描函数中去调用scanHandle = pPN->callbackFun;return;}pPN = pPN->packNext;}
}/**********************   外部函数区域*********************//* 手写printf,避免使用微库造成不必要的错误 */
void U_Printf(const char *fmt, ...)
{char Uart_buf[TX_BUF_SIZE];va_list args;va_start(args, fmt);int length = vsnprintf(Uart_buf, sizeof(Uart_buf) - 1, fmt, args);va_end(args);cmd_buff += length;HAL_UART_Transmit(&uuart, (uint8_t *)Uart_buf, length, 0xfff);
}/* 串口接收初始化 */
HAL_StatusTypeDef uart_RX_init(void)
{// 接收结构体清除rx_struct_Clear();// 串口接收中断初始化HAL_UART_Receive_IT(&uuart, &rx_InitStr.a_rx_buf, 1);packhandNode = (struct pack_struct *)malloc(sizeof(struct pack_struct));if(packhandNode == NULL){yxy_DEBUG("open usart initialization failed... \r\n");return HAL_ERROR;}packhandNode->packNext = NULL;packhandNode->userRX = NULL;packhandNode->packId = 0xffff;strcpy((char *)packhandNode->special, "x");strcpy((char *)packhandNode->packText, "USER");//  yxy_DEBUG("special : %s, size = %d\r\n", packhandNode->special, sizeof("xxxxxxxxx"));
//  yxy_DEBUG("packText : %s, size = %d\r\n", packhandNode->packText, sizeof("UART link hand node."));return HAL_OK;
}/*** @brief  串口接收添加可识别的包,包头为特殊字符串当接收到这个特殊字符串时,既包开始接收,数据不定长,需要用户在自己的回调中处理,当接收到回车换行时,处理包的内容,并在下次main循环中调用对应的回调函数。* @param  userRX 用户定义的初始化结构体* @param  special 需要添加的特殊字符串* @param  specialLen 特殊字符串的长度* @param  packText 包的解释语言* @param  callbackFun 解析包的回调函数* @retval 返回hal库错误码*/
HAL_StatusTypeDef rx_Pack_add(User_USART_RX *userRX, uint8_t * special, uint8_t specialLen,uint8_t * packText, pack_Callback callbackFun)
{uint16_t iCnt = 0;struct pack_struct * pPN = packhandNode; // 节点特殊字节点// 找到空的节点while(pPN->packNext != NULL){pPN = pPN->packNext;}pPN->packNext = (struct pack_struct *)malloc(sizeof(struct pack_struct));pPN = pPN->packNext;if(pPN == NULL){yxy_DEBUG("usart add failed... \r\n");return HAL_ERROR;}pPN->packNext = NULL;pPN->specialLen = specialLen;pPN->userRX = userRX;pPN->packId = packIdNum;strcpy((char *)pPN->special, (const char *)special);strcpy((char *)pPN->packText, (const char *)packText);yxy_DEBUG("add special : %s, size = %d\r\n", pPN->special, str_len(special));
//  yxy_DEBUG("add packText : %s, size = %d\r\n", pPN->packText, str_len(packText));pPN->callbackFun = callbackFun;// 用户句柄赋值userRX->special = pPN->special;userRX->packText = pPN->packText;userRX->specialLen = pPN->specialLen;userRX->packId = packIdNum;while(iCnt++ < SPECIAL_DATA_SIZE){userRX->dataVal[iCnt] = 0;}userRX->dataLen = 0;packIdNum++;return HAL_OK;
}/*** @brief  串口接收可识的包的删除* @param  * @retval 返回hal库错误码*/
HAL_StatusTypeDef rx_Pack_clear(User_USART_RX *userRX)
{struct pack_struct * pPN = packhandNode; // 节点特殊字节点struct pack_struct * pTextN = NULL; // 找到空的节点while(pPN->packNext != NULL){// 如果id匹配的则删除对应id的特殊包节点if(pPN->packNext->packId == userRX->packId){pTextN = pPN->packNext;pPN->packNext = pPN->packNext->packNext;free(pTextN);pTextN = NULL;userRX->packId = 0;yxy_DEBUG("clear rx ok\r\n");return HAL_OK;}pPN = pPN->packNext;}return HAL_ERROR;
}/*** @brief  接收扫描函数,一旦接收到关键词,在这里处理 * @retval */
void uart_rx_scan(void)
{if(scanHandle != NULL){scanHandle();scanHandle = NULL;}
}/* 串口中断函数 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{if(rx_InitStr.rx_cnt >= RX_BUF_SIZE)//溢出判断{yxy_DEBUG("ESP data overflow\r\n");
//          yxy_DEBUG("%s\r\n", rx_InitStr.rx_buf);rx_struct_Clear();}else //数据存入{HAL_UART_Transmit(&uuart, (uint8_t *)&(rx_InitStr.a_rx_buf), 1, 0xff);
//          yxy_DEBUG("%c: 0x%02x \r\n", rx_InitStr.a_rx_buf, rx_InitStr.a_rx_buf);//         yxy_DEBUG("%d : %c\r\n", rx_InitStr.rx_cnt, rx_InitStr.a_rx_buf);rx_InitStr.rx_buf[rx_InitStr.rx_cnt] = rx_InitStr.a_rx_buf;rx_InitStr.rx_cnt++;// 一旦等于删除建,就退回到之前的if((rx_InitStr.a_rx_buf == 0x08) && (rx_InitStr.rx_cnt >= 2)){rx_InitStr.rx_cnt -= 2;}// 等于水平制表符键位if(rx_InitStr.a_rx_buf == 0x09){rx_InitStr.rx_cnt--;rx_InitStr.rx_buf[rx_InitStr.rx_cnt] = '\0';if(rx_InitStr.rx_cnt != 0){tab_handler();}}#if (CR_LF_IS_WHITCH == 0)// 当上一个字符为\r这一次字符为\n,说明一行的结束if(rx_InitStr.rx_buf[rx_InitStr.rx_cnt-2] == '\r' && rx_InitStr.rx_buf[rx_InitStr.rx_cnt-1] == '\n'){rx_InitStr.rx_buf[rx_InitStr.rx_cnt-2] = '\0';
#elif (CR_LF_IS_WHITCH == 1)// 当这一次字符为\n,说明一行的结束if(rx_InitStr.rx_buf[rx_InitStr.rx_cnt-1] == '\n'){#elif (CR_LF_IS_WHITCH == 2)if(rx_InitStr.rx_buf[rx_InitStr.rx_cnt-1] == '\r'){#endif
//          yxy_DEBUG("\r\ninput = %s\r\n", rx_InitStr.rx_buf);yxy_DEBUG("\r\n");rx_InitStr.rx_buf[rx_InitStr.rx_cnt-1] = '\0';pack_classify();rx_struct_Clear();}}HAL_UART_Receive_IT(&uuart, (uint8_t *)&(rx_InitStr.a_rx_buf), 1);   //再开启接收中断
}

yxy_open_cmd.h

#ifndef __YXY_OPEN_CMD_H_
#define __YXY_OPEN_CMD_H_#ifdef __cplusplus
extern "C" {#endif/**********************      INCLUDES*********************/#include "stdio.h"
#include "usart.h"/**********************      DEFINES*********************//* 串口的hal库的句柄 */
#define uuart huart1/* 串口发送缓冲区 */
#define TX_BUF_SIZE 64/* 串口接收buff大小 */
#define RX_BUF_SIZE 64/* 包关键词支持的大小 */
#define SPECIAL_CHAR_SIZE 10/* 包关键词的说明文档支持的大小 */
#define SPECIAL_TEXT_SIZE 50/* 包关键词的数据支持的大小 */
#define SPECIAL_DATA_SIZE 15/* 回车换行是\r\n 则设置为0是;为\n(0x0a),则设置为 1 ;为\r(0x0d),则设置成 2*/
#define CR_LF_IS_WHITCH 2#if (CR_LF_IS_WHITCH == 0)
#   define USER_CRLF \r\n
#elif (CR_LF_IS_WHITCH == 1)
#   define USER_CRLF \n
#elif (CR_LF_IS_WHITCH == 2)
#   define USER_CRLF \r
#else
#   define USER_CRLF
#endif/***********************      TYPEDEFS**********************//* 包回调函数格式 */
typedef void(*pack_Callback)(void);/* 接收结构体 */
typedef struct rx_struct
{uint8_t  rx_buf[RX_BUF_SIZE];  // 接收缓冲存取区uint8_t  a_rx_buf;                // 缓冲区元素暂存位置uint16_t rx_cnt;                //当前接收计数
}rx_Struct;/* 用户存储信息指针结构体 */
typedef struct
{uint8_t *special;  // 关键词uint8_t *packText;    // 关键词解释uint8_t specialLen; // 关键词长度uint8_t dataVal[SPECIAL_DATA_SIZE]; // 数据值uint8_t dataLen;  // 数据长度uint16_t packId; // 特殊字id
}User_USART_RX;/* 接收包的链表结构体 */
struct pack_struct
{uint8_t special[SPECIAL_CHAR_SIZE];    // 需要判定的特殊字符串uint8_t packText[SPECIAL_TEXT_SIZE];// 特殊字的说明  uint8_t specialLen;                 // 特殊字长度uint16_t packId;                    // 特殊字idUser_USART_RX * userRX;             // 访问用户的指针pack_Callback callbackFun;            // 包回调函数struct pack_struct * packNext;      // 下一个节点
};/**********************
* GLOBAL PROTOTYPES
**********************/
/*** @brief  获取字符串长度* @param  字符串指针* @retval 返回字符串长度*/
inline static uint16_t str_len(uint8_t * str)
{uint16_t len=0;while(*str != '\0'){len++;str++;}return len;
}/* 测试 *///HAL_StatusTypeDef string_cont(uint8_t * s1, uint8_t * s2);/**********************
* GLOBAL PROTOTYPES
**********************/extern uint32_t cmd_buff;void U_Printf(const char *fmt, ...);/*  //用法U_Printf("niubi%d\n", 12);*/HAL_StatusTypeDef uart_RX_init(void);
HAL_StatusTypeDef rx_Pack_add(User_USART_RX *userRX, uint8_t * special, uint8_t specialLen,uint8_t * packText, pack_Callback callbackFun);
HAL_StatusTypeDef rx_Pack_clear(User_USART_RX *userRX);void uart_rx_scan(void);#ifdef __cplusplus
} /*extern "C"*/
#endif#endif /*__YXY_OPEN_CMD_H_*/

yxy_debug.c

#include "yxy_debug.h"
#include "stdio.h"#ifdef YXY_DEBUG
#   if (YXY_DEBUG_SELE == 0)//重定向c库函数printf到串口DEBUG_USART,重定向后可使用printf函数int fputc(int ch, FILE *f){/* 发送一个字节数据到串口DEBUG_USART */HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);   return (ch);}
#   endif
#endifvoid error_printf(const char * file, int line, const char * func, const char * format, ...)
{USER_PASS;
}

yxy_debug.h

#ifndef __YXY_DEBUG_H_
#define __YXY_DEBUG_H_#ifdef __cplusplus
extern "C" {#endif/**********************      INCLUDES*********************/#include "stdint.h"/**********************      DEFINES*********************/// 相当于python的 pass 语句
#define USER_PASS ((void)0U)// 打印转换后的宏
#define _VNAME(name) (#name)
#define VNAME(name) (_VNAME(name)) 弱定义
//#define __WEAK                                 __attribute__((weak))/* 按键串口调试支持 0 不支持 , 1 支持*/
#define YXY_DEBUG 1
#ifdef YXY_DEBUG/* debug 选择,* 为 1 选择外部的 用户自定义的打印函数,* 为0则调用stdio库中的printf*/
#   define YXY_DEBUG_SELE 1/* 开启debug,这里的外部的debug文件可以重写 *//* 调试的外部函数,可以是printf或者是其他手写的多参数打印函数* 多参数打印函数的格式是 :*      void function(const char *, ...);*/
#   if (YXY_DEBUG_SELE == 1)
#       include "yxy_open_cmd.h"
#       define __uprintf__ U_Printf
#   else
#       include "stdio.h"
#       define __uprintf__ printf
#   endif#endif/* 调试函数实现 */
#if (YXY_DEBUG == 1)
#   define yxy_DEBUG(...) __uprintf__(__VA_ARGS__)
#else
#   define yxy_DEBUG(...) do{}while(0)
#endif
/***********************      TYPEDEFS**********************//**********************
*  GLOBAL PROTOTYPES
**********************/#ifdef __cplusplus
} /*extern "C"*/
#endif#endif /*__YXY_DEBUG_H_*/

完整代码工程

开源终端完整工程代码点这里

cubemx stm32 串口终端的实现与使用 (可以由用户自定义各种终端指令任意) 驱动代码相关推荐

  1. CubeMX STM32串口1DMA使用IDLE中断接收、串口2DMA接收DMX512信号(标准)

    CubeMX STM32串口1DMA使用IDLE中断接收.串口2DMA收发DMX512信号(标准) DMX512协议 CubeMX 代码部分 串口1 串口2 外部中断 定时器1 总结 DMX512协议 ...

  2. ESP8266 WIFI 模块串口调试过程-实现通过互联网实现数据远程传输(结尾含驱动代码链接)

    一. ESP8266 WIFI模块调试(串口发送AT指令调试). ESP8266 WIFI模块的调试算是最复杂的了,虽然通信是简单的串口通信,但是要设置ESP8266连接服务器并稳定无误的将数据上传, ...

  3. STM32之 ESP8266 WIFI 模块驱动代码-可以通过互联网实现数据远程传输(程序稳定,清晰明了非常容易移植到51单片机上)

    成品展示部分 :(ESP8266只是其中一个小部分而已) 实物图: 基于互联网的农业大棚环境监控系统设计 电路图: 农业大棚环境监控设计电路图(彩色)  调试部分:ESP8266 WIFI 模块串口调 ...

  4. cubemx stm32 配置两个串口_STM32CubeMX的串口配置,以及驱动代码

    1.STM32CubeMX的配置没啥子好说的,使能然后改一下波特率和字长,然后在将中断勾选,把中断等级调到1(一定要比systick的优先级垃圾!!!) 2.驱动代码 在生成的it.c文件中,例如用的 ...

  5. STM32CUBEMX配置教程(八)STM32串口轮询发送中断接收+重定义+优化

    STM32CUBEMX配置教程(八)STM32串口轮询发送中断接收+重定义+优化 基于STM32H743VI 使用STM32CUBEMX两年了,始终觉得这个工具非常的方便,但因为不是经常使用,导致有些 ...

  6. 通过python实现安卓手机与stm32串口通信

    一.材料 (1)安卓终端1台,本文使用的魅蓝手机 (2)stm32微控制器(可以串口通信的) 二.通信内容 上位机发送AA,熄灭stm32上的LED灯 上位机发送BB,点亮stm32上的LED灯 上位 ...

  7. stm32串口UASART

    通讯的基本概念 1 串行通讯与并行通讯 串行通讯是指设备之间通过少量数据信号线(一般是 8 根以下),地线以及控制信号线,按数据位形式一位一位地传输数据的通讯方式.而并行通讯一般是指使用 8. 16. ...

  8. 【STM32串口通信】

    STM32串口通信 学习计划 一.串口通信知识点 二.硬件部分 1.所需硬件 2.部分硬件连接 三.阻塞式 0.串口阻塞式发送和接收概念 1.STM32CUBEMX配置 2.编写阻塞式串口发送与接收代 ...

  9. 【嵌入式】STM32串口通信

    [嵌入式]STM32串口通信 一.串口通信协议 1.串口通信简介 2.串口通信原理 二.RS232通信协议 1.RS232协议简介 2.机械规约 3.电气规约 三.STM32的USART串口通信(查询 ...

最新文章

  1. [Asp.net MVC]Asp.net MVC5系列——第一个项目
  2. 《数学之美》第5章 隐含马尔可夫模型
  3. 解读互联网40年中的10大“杀手”病毒
  4. sqldeveloper 连接oracle时 ora-12505 错误
  5. c语言计算机二级改错题类型,C语言计算机二级改错题
  6. php-protobuf扩展和代码生成工具使用
  7. 对生信与计算生物的一点认识[转载]
  8. 电商网站前台模板_电商热潮汹涌,兴长信达PEC零售商城系统为企业注入新力量...
  9. 可能是求质数最高效的算法
  10. 无线串口模块SX1278的使用后记
  11. 玉米社:SEM竞价推广转化成本高?做好细节转化率蹭蹭往上涨
  12. html5倒计时效果,html5+css3进度条倒计时动画特效代码【推荐】
  13. 【安洵杯 2019】easy-web
  14. 锁定计算机时候的屏幕壁纸,电脑锁屏的时候屏幕壁纸怎么更改
  15. 6大应用,大象机器人双臂协作机器人,即将7月上市,一切就绪!
  16. java获取发送时间间隔工具类,1秒前,1分钟前,1小时前...
  17. stm32 软件怎么设置写保护_STM32F407 读保护,写保护,解锁过程【芯片已设置读保护,无法读取更多信息】...
  18. 计算机自带的超级锁怎么设置,万能锁加锁了文件夹加密超级大师的快捷方式怎么办?...
  19. 超微服务器如何升级微信,局域网升级微信版本
  20. 编写php程序_计算1+2+3+… +100的和_并输出计算结果.,下列程序是用来计算1+2+3……+10的程序段,请补充完该程序。()...

热门文章

  1. aspect 方法入参 获取_谈谈Spring AOP中@Aspect的高级用法示例
  2. 关于高考志愿的美好回忆
  3. matlab最小费用最大流函数,使用matlab求解最小费用最大流算问题
  4. Android中的网络编程-黄俊东-专题视频课程
  5. 水果店开业需要准备什么用品,水果店开业有什么讲究
  6. 手机上存储的文字文档怎么在网上进行打印?
  7. 不用找,你想要的促销海报设计模板素材都在这里
  8. ubuntu配置VNC远程连接服务器图形界面
  9. Mybatis中selectKey 标签的作用,主键回填,找了好多文章没一个解释清楚。。
  10. 2018 AI Challenger全球AI挑战赛‘眼底水肿病变区域自动分割’赛道比赛总结