Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
串口终端
源码分析
RT-Thread 系统串口终端流程源码分析以及命令导出机制的实现
发布于 2021-11-05 21:01:02 浏览:2158
订阅该版
[tocm] # RT-Thread 系统串口终端各功能的实现 当在串口终端输入各种命令的时候,RT-Thread 系统是怎么实现的 在我们使用终端的时候,输入tab键就会补全命令,输入回车就会执行命令,下面我们来一起分析一下RT-Thread的这部分源码吧 但是在这之前我们需要看的是,这些命令是如何导出供我们去运行的,导出命令的方式有很多 ```c MSH_CMD_EXPORT(name, desc); //自定义 msh 命令 MSH_CMD_EXPORT_ALIAS(command, alias, desc) //自定义 msh 命令重命名 FINSH_FUNCTION_EXPORT(name, desc); //自定义 C-Style 命令 FINSH_VAR_EXPORT(name, type, desc); //自定义 C-Style 变量 FINSH_FUNCTION_EXPORT_ALIAS(name, alias, desc); //自定义 C-Style 命令重命名 ``` 下面我们就从最常用的一个`MSH_CMD_EXPORT`来去分析,其他的类似 ## `MSH_CMD_EXPORT`命令的原理 ```c long version(void) { rt_show_version(); return 0; } MSH_CMD_EXPORT(version, show RT-Thread version information); ``` ### 第一层封装 ```c #define MSH_CMD_EXPORT(command, desc) \ FINSH_FUNCTION_EXPORT_CMD(command, __cmd_##command, desc) ``` 上述的`##`是用于连接 例如: ```c MSH_CMD_EXPORT(version, show RT-Thread version information); ``` 调用之后会变成 ```c FINSH_FUNCTION_EXPORT_CMD(version, __cmd_version, show RT-Thread version information) ``` ### 第二层封装 ```c #define FINSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \ const char __fsym_##cmd##_name[] SECTION(".rodata.name") = #cmd; \ const char __fsym_##cmd##_desc[] SECTION(".rodata.name") = #desc; \ RT_USED const struct finsh_syscall __fsym_##cmd SECTION("FSymTab")= \ { \ __fsym_##cmd##_name, \ __fsym_##cmd##_desc, \ (syscall_func)&name \ }; ``` 上述的`#`将值转化为字符串 例如: ```c FINSH_FUNCTION_EXPORT_CMD(version, __cmd_version, show RT-Thread version information) ``` 封装中的`cmd`表示`__cmd_version`,而`#cmd`表示`"__cmd_version"` `SECTION(".rodata.name")`将变量放入`.rodata.name`段中 `(syscall_func)&name`将值带入也就是`(syscall_func)&version`它指向`version`函数的指针 注,下图是rtthread.map文件中的一些段信息,这里不是对应的值,而是宏定义导出的名称 ![image.png](https://oss-club.rt-thread.org/uploads/20211105/174b4077fd882356c8b98565564134f9.png) ![image.png](https://oss-club.rt-thread.org/uploads/20211105/b8d4287507606532a34d84fdf73592e4.png) **总结一下:这里是将命令的名字和描述的值放在了`.rodata.name`段,将它们的地址和函数的地址一起组成结构体(也就是一段连续的地址)放在了`FSymTab`段,最后供其他程序从段中寻找地址去调用对应的函数,这就是`MSH_CMD_EXPORT`宏定义要做的事** ## 命令存放的起始地址 上边提到了`FSymTab`段,说把终端命令的信息放在了这里,那么我们该怎么获得这个地址呢 首先设置`FSymTab`段,*`FSymTab$a`表示start,`FSymTab$z`表示end*, ```c #pragma section("FSymTab$a", read) const char __fsym_begin_name[] = "__start"; const char __fsym_begin_desc[] = "begin of finsh"; __declspec(allocate("FSymTab$a")) const struct finsh_syscall __fsym_begin = { __fsym_begin_name, __fsym_begin_desc, NULL }; #pragma section("FSymTab$z", read) const char __fsym_end_name[] = "__end"; const char __fsym_end_desc[] = "end of finsh"; __declspec(allocate("FSymTab$z")) const struct finsh_syscall __fsym_end = { __fsym_end_name, __fsym_end_desc, NULL }; ``` 但你要说这个段的开始地址和结束地址的具体值到底怎么来的,我感觉是`link.lds`文件中这段代码在最后链接的时候堆积出来的,这些就不深究了 ```c . = ALIGN(4); __fsymtab_start = .; KEEP(*(FSymTab)) __fsymtab_end = .; ``` 在`finsh_system_init`函数中有一下的代码 ```c unsigned int *ptr_begin, *ptr_end; ptr_begin = (unsigned int *)&__fsym_begin; ptr_begin += (sizeof(struct finsh_syscall) / sizeof(unsigned int)); while (*ptr_begin == 0) ptr_begin ++; ptr_end = (unsigned int *) &__fsym_end; ptr_end --; while (*ptr_end == 0) ptr_end --; finsh_system_function_init(ptr_begin, ptr_end); ``` ```c void finsh_system_function_init(const void *begin, const void *end) { _syscall_table_begin = (struct finsh_syscall *) begin; _syscall_table_end = (struct finsh_syscall *) end; } ``` 通过这两段代码来把`FSymTab`段的起始地址和结束地址分别保存在`_syscall_table_begin`和`_syscall_table_end`变量中以供下边查找使用 ## 下面来看一下`finsh_thread_entry`函数 还以`version`这个命令为例 当我们在终端按下键盘按键的时候,都会通过`void finsh_thread_entry(void *parameter)`函数中的`ch = finsh_getchar();`去获取字符,然后去判断该字符是一些输入值(字母类的),还是一些属性值(回车、tab、上下左右键等)(这是我自己的分类,RT-Thread内部实现没有区分) 当我们输入`version`会发生什么呢,RT-Thread内部是单个字母进行操作的,当我们每输入一个字符都会存在shell->line数组中 由于键盘左右键可以控制添加你漏输的字符,所以在字符输入分为两种情况,(字符删除是类似的,这里就不在说明) ```c //shell->echo_mode 这个变量是控制命令是否回显的 /* normal character */ if (shell->line_curpos < shell->line_position) { int i; /* 将光标后面的字符整体后移一位,将新获取的字符插入,并在回显状态下输出 */ rt_memmove(&shell->line[shell->line_curpos + 1], &shell->line[shell->line_curpos], shell->line_position - shell->line_curpos); shell->line[shell->line_curpos] = ch; if (shell->echo_mode) rt_kprintf("%s", &shell->line[shell->line_curpos]); /* move the cursor to new position */ /* 退格到原来的光标 */ for (i = shell->line_curpos; i < shell->line_position; i++) rt_kprintf("\b"); } else { /* 当光标在最后时,直接在shell->line数组最后增加就行 */ shell->line[shell->line_position] = ch; if (shell->echo_mode) rt_kprintf("%c", ch); } ``` 因为RT-Thread是支持tab键补全的,所以我们只需要输入`v`就可以使用tab键去补全,可以有效的防止输错的情况 ### tab键操作 #### `finsh_thread_entry`函数 这是判断tab键的部分,其中`line_curpos`表示当前已输入命令的光标位置 。`line_position`表示当前已输入命令的总长度 ,下面就是通过`shell_auto_complete(&shell->line[0]);`来把终端的命令补全,并将补全的命令长度更新到光标位置`shell->line_curpos`和命令总长度`shell->line_position` ```c /* handle tab key */ else if (ch == '\t') { int i; /* move the cursor to the beginning of line */ for (i = 0; i < shell->line_curpos; i++) rt_kprintf("\b"); /* auto complete */ shell_auto_complete(&shell->line[0]); /* re-calculate position */ shell->line_curpos = shell->line_position = strlen(shell->line); continue; } ``` #### `shell_auto_complete`函数 下面就来看一下`shell_auto_complete`函数的具体功能,首先通过`msh_is_used()`来判断当前的命令行终端模式,这里使用的就是msh模式,执行完`msh_auto_complete`函数后就输出`msh />`标志还有对应的补全命令(`FINSH_PROMPT`的内部实现就不解释了,在没有进入文件系统目录输出的就是`msh />`,如果进入了文件系统的路径,还会将对应的路径输出) ```c static void shell_auto_complete(char *prefix) { rt_kprintf("\n"); if (msh_is_used() == RT_TRUE) { msh_auto_complete(prefix); } else { extern void list_prefix(char * prefix); list_prefix(prefix); } rt_kprintf("%s%s", FINSH_PROMPT, prefix); } ``` #### `msh_auto_complete`函数 下面看一下`msh_auto_complete`函数的实现 这段代码定义了一个 `index` 结构体指针,当指针的范围在 `_syscall_table_begin` 和 `_syscall_table_end` 范围之内时就对该指针进行解引用,这里的指针就是命令导出时放的结构体指针,所以这里就是把对应的结构体中的命令名字取出来与已经输入的命令做对比,并把全部匹配的输出,而且还在这些匹配的命令中找到最大的公共部分,作为已补全的命令,更新到`shell->line`数组中 ```c struct finsh_syscall *index; /* checks in internal command */ { /* 从_syscall_table_begin到_syscall_table_end地址中依次把cmd名称取出来比较 * 也就是上述宏定义封装中放在FSymTab段和.rodata.name段的数据 */ for (index = _syscall_table_begin; index < _syscall_table_end; FINSH_NEXT_SYSCALL(index)) { /* skip finsh shell function */ if (strncmp(index->name, "__cmd_", 6) != 0) continue; cmd_name = (const char *) &index->name[6]; if (strncmp(prefix, cmd_name, strlen(prefix)) == 0) { if (min_length == 0) { /* set name_ptr */ name_ptr = cmd_name; /* set initial length */ min_length = strlen(name_ptr); } /* 比较两次段中的名字的最大相同长度 */ length = str_common(name_ptr, cmd_name); /* 将最短的相同长度记录到min_length */ if (length < min_length) min_length = length; rt_kprintf("%s\n", cmd_name); } } } /* auto complete string */ if (name_ptr != NULL) { /* 将min_length长度所对应的字符赋给prefix */ rt_strncpy(prefix, name_ptr, min_length); } ``` ### 回车键 这个键的实现是最复杂了,也是整个终端命令的精髓所在,键盘上下键的储存命令也是在这里实现的 #### 储存命令 这段代码首先判断了当前已存指令数量`shell->history_count`和预设的最大储存数量`FINSH_HISTORY_LINES`,如果已经满了的话,则需要抛弃最早储存的指令,使储存数组向前覆盖,以储存最新的指令,如果没满的话直接将指令放在指令数量的数组`shell->cmd_history[shell->history_count]`里即可 当然两者都判断这次输入的指令和最近一次保存的指令是否一致, 防止出现同一个指令的连续存储(第九行和第二十八行) ```c static void shell_push_history(struct finsh_shell *shell) { if (shell->line_position != 0) { /* push history */ if (shell->history_count >= FINSH_HISTORY_LINES) { /* if current cmd is same as last cmd, don't push */ if (memcmp(&shell->cmd_history[FINSH_HISTORY_LINES - 1], shell->line, FINSH_CMD_SIZE)) { /* move history */ int index; for (index = 0; index < FINSH_HISTORY_LINES - 1; index ++) { memcpy(&shell->cmd_history[index][0], &shell->cmd_history[index + 1][0], FINSH_CMD_SIZE); } memset(&shell->cmd_history[index][0], 0, FINSH_CMD_SIZE); memcpy(&shell->cmd_history[index][0], shell->line, shell->line_position); /* it's the maximum history */ shell->history_count = FINSH_HISTORY_LINES; } } else { /* if current cmd is same as last cmd, don't push */ if (shell->history_count == 0 || memcmp(&shell->cmd_history[shell->history_count - 1], shell->line, FINSH_CMD_SIZE)) { shell->current_history = shell->history_count; memset(&shell->cmd_history[shell->history_count][0], 0, FINSH_CMD_SIZE); memcpy(&shell->cmd_history[shell->history_count][0], shell->line, shell->line_position); /* increase count and set current history position */ shell->history_count ++; } } } shell->current_history = shell->history_count; } ``` #### 运行导出的命令 那么在我们得到`shell->line`数组之后是怎么运行我们的程序的呢 ```c #ifdef FINSH_USING_MSH if (msh_is_used() == RT_TRUE) { if (shell->echo_mode) rt_kprintf("\n"); msh_exec(shell->line, shell->line_position); } else #endif { #ifndef FINSH_USING_MSH_ONLY /* add ';' and run the command line */ shell->line[shell->line_position] = ';'; rt_kprintf("FINSH_USING_MSH_ONLY\n"); if (shell->line_position != 0) finsh_run_line(&shell->parser, shell->line); else if (shell->echo_mode) rt_kprintf("\n"); #endif } rt_kprintf(FINSH_PROMPT); memset(shell->line, 0, sizeof(shell->line)); shell->line_curpos = shell->line_position = 0; ``` 上述代码除去一些`msh`之外的,可以简化成下面的形式,也就是`msh_exec`,加上终端提示符的输出,以及对shell结构体的清空 ```c msh_exec(shell->line, shell->line_position); rt_kprintf(FINSH_PROMPT); memset(shell->line, 0, sizeof(shell->line)); shell->line_curpos = shell->line_position = 0; ``` ##### `msh_exec`函数理解 重点还是要看`msh_exec`函数的实现 ```c int msh_exec(char *cmd, rt_size_t length) { int cmd_ret; /* strim the beginning of command */ while (*cmd == ' ' || *cmd == '\t') { cmd++; length--; } if (length == 0) return 0; /* Exec sequence: * 1. built-in command * 2. module(if enabled) */ if (_msh_exec_cmd(cmd, length, &cmd_ret) == 0) { return cmd_ret; } #ifdef RT_USING_DFS #ifdef DFS_USING_WORKDIR if (msh_exec_script(cmd, length) == 0) { return 0; } #endif #ifdef RT_USING_MODULE if (msh_exec_module(cmd, length) == 0) { return 0; } #endif #ifdef RT_USING_LWP if (_msh_exec_lwp(cmd, length) == 0) { return 0; } #endif #endif /* truncate the cmd at the first space. */ { char *tcmd; tcmd = cmd; while (*tcmd != ' ' && *tcmd != '\0') { tcmd++; } *tcmd = '\0'; } rt_kprintf("%s: command not found.\n", cmd); return -1; } ``` 同样的除去一些`msh`之外的,可以简化为如下代码(一些简单的逻辑已经去除,只说一些关键的东西) 下面的代码通过`while`,把命令前面空格和"\t"去掉,然后去执行`_msh_exec_cmd`函数,如果没有找到这个命令的话将会执行下面的代码,去截取第一个空格前的指令,并输出具体信息,`%s: command not found.` ```c int msh_exec(char *cmd, rt_size_t length) { int cmd_ret; /* strim the beginning of command */ while (*cmd == ' ' || *cmd == '\t') { cmd++; length--; } if (_msh_exec_cmd(cmd, length, &cmd_ret) == 0) { return cmd_ret; } /* truncate the cmd at the first space. */ { char *tcmd; tcmd = cmd; while (*tcmd != ' ' && *tcmd != '\0') { tcmd++; } *tcmd = '\0'; } rt_kprintf("%s: command not found.\n", cmd); return -1; } ``` 那么下面就看一下`_msh_exec_cmd`函数的具体实现 ##### `_msh_exec_cmd`函数理解 `RT_ASSERT(cmd);`判断指针`cmd`是否为空,当为空指针时进入断言 然后通过`while`循环得到指令名字的长度,再使用`msh_get_cmd`获取其对应的函数指针,使用`msh_split`将命令连带的参数信息放在`argc`指针数组中,使用`cmd_func(argc, argv);`调用运行,就相当于运行了`MSH_CMD_EXPORT`导出的函数 ```c static int _msh_exec_cmd(char *cmd, rt_size_t length, int *retp) { int argc; rt_size_t cmd0_size = 0; cmd_function_t cmd_func; char *argv[FINSH_ARG_MAX]; RT_ASSERT(cmd); RT_ASSERT(retp); /* find the size of first command */ while ((cmd[cmd0_size] != ' ' && cmd[cmd0_size] != '\t') && cmd0_size < length) cmd0_size ++; if (cmd0_size == 0) return -RT_ERROR; cmd_func = msh_get_cmd(cmd, cmd0_size); if (cmd_func == RT_NULL) return -RT_ERROR; /* split arguments */ memset(argv, 0x00, sizeof(argv)); argc = msh_split(cmd, length, argv); if (argc == 0) return -RT_ERROR; /* exec this command */ *retp = cmd_func(argc, argv); return 0; } ``` 分析到这里终于运行起来了`MSH_CMD_EXPORT`导出的函数,但是还不知道`msh_get_cmd`是怎么获取到对应函数的指针的 ##### `msh_get_cmd`函数理解 在找到段的首地址之后,通过对比终端输入的值`cmd`和 `FSymTab`段中以结构体访问的命令的名字地址去访问`.rodata.name`段的名字值去做对比,找到命令后返回对应结构体中函数的指针 ```c static cmd_function_t msh_get_cmd(char *cmd, int size) { struct finsh_syscall *index; cmd_function_t cmd_func = RT_NULL; for (index = _syscall_table_begin; index < _syscall_table_end; FINSH_NEXT_SYSCALL(index)) { /*判断是否有__cmd_前缀,在MSH_CMD_EXPORT的第一层封装中加入了这个前缀*/ if (strncmp(index->name, "__cmd_", 6) != 0) continue; if (strncmp(&index->name[6], cmd, size) == 0 && index->name[6 + size] == '\0') { /*将cmd对应的命令的函数地址赋给函数指针cmd_func*/ cmd_func = (cmd_function_t)index->func; break; } } return cmd_func; } ``` ##### `FINSH_NEXT_SYSCALL(index)`宏定义理解 ```c #define FINSH_NEXT_SYSCALL(index) index=finsh_syscall_next(index) struct finsh_syscall* finsh_syscall_next(struct finsh_syscall* call) { unsigned int *ptr; ptr = (unsigned int*) (call + 1); while ((*ptr == 0) && ((unsigned int*)ptr < (unsigned int*) _syscall_table_end)) ptr ++; return (struct finsh_syscall*)ptr; } ``` 这里四字节四字节去查找是非为空的原因是: - 当关闭`FINSH_USING_DESCRIPTION`或`FINSH_USING_SYMTAB`时,字节数会由12字节减少为8字节 ```c struct finsh_syscall { const char* name; /* the name of system call */ #if defined(FINSH_USING_DESCRIPTION) && defined(FINSH_USING_SYMTAB) const char* desc; /* description of system call */ #endif syscall_func func; /* the function address of system call */ }; ``` ### 方向键 这部分的代码也是在`void finsh_thread_entry(void *parameter)`函数中去实现的,同样是使用`ch = finsh_getchar();`去获取,但是不同的是方向键是由三位组成的,所以要连续的去判断对应的值 ```c /* * handle control key * up key : 0x1b 0x5b 0x41 * down key: 0x1b 0x5b 0x42 * right key:0x1b 0x5b 0x43 * left key: 0x1b 0x5b 0x44 */ if (ch == 0x1b) { shell->stat = WAIT_SPEC_KEY; continue; } else if (shell->stat == WAIT_SPEC_KEY) { if (ch == 0x5b) { shell->stat = WAIT_FUNC_KEY; continue; } shell->stat = WAIT_NORMAL; } else if (shell->stat == WAIT_FUNC_KEY) { shell->stat = WAIT_NORMAL; ``` 上下键是类似的,都是通过读取回车储存在`shell->cmd_history`数组中的值去写入到当前的命令行数组`shell->line`中,通过`shell_handle_history(shell);`输出去覆盖当前终端上的值 ```c if (ch == 0x41) /* up key */ { #ifdef FINSH_USING_HISTORY /* prev history */ if (shell->current_history > 0) shell->current_history --; else { shell->current_history = 0; continue; } /* copy the history command */ memcpy(shell->line, &shell->cmd_history[shell->current_history][0], FINSH_CMD_SIZE); shell->line_curpos = shell->line_position = strlen(shell->line); shell_handle_history(shell); #endif continue; } ``` 左右键的话,就更简单了,没什么好说的 ```c else if (ch == 0x44) /* left key */ { if (shell->line_curpos) { rt_kprintf("\b"); shell->line_curpos --; } continue; } else if (ch == 0x43) /* right key */ { if (shell->line_curpos < shell->line_position) { rt_kprintf("%c", shell->line[shell->line_curpos]); shell->line_curpos ++; } continue; } ```
1
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
zhkag
这家伙很懒,什么也没写!
文章
12
回答
482
被采纳
66
关注TA
发私信
相关文章
1
rtthread-studio v1.1.4打开串口终端失败
2
终端打印中文发生乱码/错码/叠字/缺字问题
3
rt-thread studio 终端连不上,没有找到端口com1和com2。
4
终端出现错误 function:rt_object_init,
5
RT-Thread Studio 的终端打印能否加入时间戳?
6
如何在串口终端打印自己想要的logo?
7
stm32F407 ,串口终端不能使用
8
串口终端无法输入指令
9
RT-Studio 串口终端无法打印
10
普通的串口调试助手kprintf 会多一行空格
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
UART
WIZnet_W5500
ota在线升级
freemodbus
PWM
flash
cubemx
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
编译报错
Debug
rt_mq_消息队列_msg_queue
SFUD
msh
keil_MDK
ulog
C++_cpp
MicroPython
本月问答贡献
踩姑娘的小蘑菇
4
个答案
1
次被采纳
红枫
4
个答案
1
次被采纳
张世争
4
个答案
1
次被采纳
Ryan_CW
4
个答案
1
次被采纳
xiaorui
1
个答案
1
次被采纳
本月文章贡献
catcatbing
3
篇文章
5
次点赞
qq1078249029
2
篇文章
2
次点赞
xnosky
2
篇文章
1
次点赞
Woshizhapuren
1
篇文章
5
次点赞
YZRD
1
篇文章
2
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部