RT-Thread之控制台线程工作流程学习记录
RT-Thread控制台工作流程
要使用RT-Thread需要在rtconfig.h头文件添加如下宏定义,RT-Thread官方定义的,不想深究这个
/* Command shell */
#define RT_USING_FINSH
#define FINSH_THREAD_NAME "tshell"
#define FINSH_USING_HISTORY
#define FINSH_HISTORY_LINES 5
#define FINSH_USING_SYMTAB
#define FINSH_USING_DESCRIPTION
#define FINSH_THREAD_PRIORITY 20
#define FINSH_THREAD_STACK_SIZE 512
#define FINSH_CMD_SIZE 80
#define FINSH_USING_MSH
#define FINSH_USING_MSH_DEFAULT
#define FINSH_USING_MSH_ONLY
创建线程
从finsh_system_init(void)开始
...
#ifdef RT_USING_HEAP
/* create or set shell structure */
shell = (struct finsh_shell *)rt_calloc(1, sizeof(struct finsh_shell)); //<------创建1个长度为sizeof(struct finsh_shell)的连续空间,返回这个空间的首地址。与malloc不同之处是calloc会把这个空间初始化为0
if (shell == RT_NULL)
{
rt_kprintf("no memory for shell\n");
return -1;
}
tid = rt_thread_create(FINSH_THREAD_NAME,
finsh_thread_entry, RT_NULL,
FINSH_THREAD_STACK_SIZE, FINSH_THREAD_PRIORITY, 10);
#else
shell = &_shell;
tid = &finsh_thread;
result = rt_thread_init(&finsh_thread,
FINSH_THREAD_NAME,
finsh_thread_entry, RT_NULL,
&finsh_thread_stack[0], sizeof(finsh_thread_stack),
FINSH_THREAD_PRIORITY, 10);
#endif /* RT_USING_HEAP */
rt_sem_init(&(shell->rx_sem), "shrx", 0, 0);
if (tid != NULL && result == RT_EOK)
rt_thread_startup(tid);
...
- 这个函数的主要功能就是做finsh的初始化,前面是获取系统或用户命令的首尾地址,之前分析过了。
- 然后主要就是创建控制台线程。
- 创建一个信号量。
- 接着调用rt_thread_startup(tid)。
finsh_shell结构体
这里有一个结构体finsh_shell,几乎是最重要的东西了
struct finsh_shell
{
struct rt_semaphore rx_sem;
enum input_stat stat;
rt_uint8_t echo_mode:1;
#ifdef FINSH_USING_HISTORY
rt_uint16_t current_history;
rt_uint16_t history_count;
char cmd_history[FINSH_HISTORY_LINES][FINSH_CMD_SIZE];
#endif
#ifndef FINSH_USING_MSH_ONLY
struct finsh_parser parser;
#endif
char line[FINSH_CMD_SIZE];
rt_uint8_t line_position;
rt_uint8_t line_curpos;
#ifndef RT_USING_POSIX
rt_device_t device;
#endif
#ifdef FINSH_USING_AUTH
char password[FINSH_PASSWORD_MAX];
#endif
};
- 可以看到这个结构体定义了一些控制台组件的参数
- struct rt_semaphore rx_sem; 作用应该是控制台的串口接收到消息就释放信号的
- enum input_stat stat; 用来判断命令有没有输完,比如说没有\n作为结束符,命令是不算数的
- rt_uint8_t echo_mode:1; 位域,只能表示0和1,这个的作用是代表是否开启回显模式
- 下面3个数据成员应该是用于像linux终端那样,可以用上键查找上一个信息用的
- struct finsh_parser parser;是语法解析器,我的工程没有用到。只使用msh功能。用C-Style模式能用到,具体看文档了
- char line[FINSH_CMD_SIZE]; 应该是存放一次性命令行的,命令行长度最大是80字节
- 接着两个变量是辅助上面line的
- rt_device_t device; 就是指向控制台串口的设备信息了,关于设备框架慢慢再看
- 最后char password[FINSH_PASSWORD_MAX];这个是要求使用控制台之前输入密码的,暂时用不上
线程内finsh_shell数据初始化
至此可以进入控制台线程中看看它干了什么
struct finsh_shell *shell;
- shell.c的开头处先定义了finsh_shell类型的全局变量shell
...
/* normal is echo mode */
#ifndef FINSH_ECHO_DISABLE_DEFAULT
shell->echo_mode = 1;
#else
shell->echo_mode = 0;
#endif
#ifndef FINSH_USING_MSH_ONLY
finsh_init(&shell->parser);
#endif
#ifndef RT_USING_POSIX
/* set console device as shell device */
if (shell->device == RT_NULL)
{
rt_device_t console = rt_console_get_device();
if (console)
{
finsh_set_device(console->parent.name);
}
}
#endif
...
- while(1)部分之前,它把初始化shell结构体的工作放到这里来了
- 比较重要的是绑定设备的部分,设备框架之后会看,暂时不在这里看了
- 获得设备框架的结构体数据后调用finsh_set_device(console->parent.name);
- 传入的参数是一个const char*类型的数据,也就是描述设备名称的字符串
- 下面是finsh_set_device的内部
rt_device_t dev = RT_NULL;
RT_ASSERT(shell != RT_NULL);
dev = rt_device_find(device_name);
if (dev == RT_NULL)
{
rt_kprintf("finsh: can not find device: %s\n", device_name);
return;
}
/* check whether it's a same device */
if (dev == shell->device) return;
/* open this device and set the new device in finsh shell */
if (rt_device_open(dev, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_INT_RX | \
RT_DEVICE_FLAG_STREAM) == RT_EOK)
{
if (shell->device != RT_NULL)
{
/* close old finsh device */
rt_device_close(shell->device);
rt_device_set_rx_indicate(shell->device, RT_NULL);
}
/* clear line buffer before switch to new device */
memset(shell->line, 0, sizeof(shell->line));
shell->line_curpos = shell->line_position = 0;
shell->device = dev;
rt_device_set_rx_indicate(dev, finsh_rx_ind);
}
- 前面是判断传进来的这个设备是否被正确加载到设备框架中了
- 因为shell已经绑定了这个设备了,就无需再绑定了所以 if (dev == shell->device) return;
- 下面是打开设备。其过程是先判断shell被旧设备占用,先关闭它,再重新绑定新设备。具体就是设置串口接收消息回调函数
- rt_device_set_rx_indicate(dev, finsh_rx_ind); 这个函数是设备框架的东西,先不看
- finsh_rx_ind(),是个信号量
static rt_err_t finsh_rx_ind(rt_device_t dev, rt_size_t size)
{
RT_ASSERT(shell != RT_NULL);
/* release semaphore to let finsh thread rx data */
rt_sem_release(&shell->rx_sem);
return RT_EOK;
}
- 可以看到,接收信息回调函数触发后释放shell结构体定义的rx_sem信号量,让阻塞的线程知道收到信息,从而开始下一步动作
线程死循环前段
接下来进入死循环的代码,主要逻辑代码部分
- 第一行就是一个阻塞
ch = finsh_getchar(); // ch的定义在前面,忽略了
|
|
v
static char finsh_getchar(void)
{
#ifdef RT_USING_POSIX
return getchar();
#else
char ch;
RT_ASSERT(shell != RT_NULL);
while (rt_device_read(shell->device, -1, &ch, 1) != 1)
rt_sem_take(&shell->rx_sem, RT_WAITING_FOREVER);
return ch;
#endif
}
- 这个函数只返回一个字符,while那两行是rtthread官方文档推荐的写法
- 在没消息到时,while为真,rt_sem_tack阻塞住了
- 直到有消息到,回调函数释放信号量,rt_sem_tack获得信号量,就返回成功一个字符
- 接下来继续往下看while里的代码
- 前面很长一段都是用ifelse来处理特殊字符的情况,忽略不看了。。
- 从shell.c源码602行开始,描述的是普通命令的情况,主要就看下面的了
/* handle end of line, break */
if (ch == '\r' || ch == '\n')
{
#ifdef FINSH_USING_HISTORY
shell_push_history(shell);
#endif
#ifdef FINSH_USING_MSH
if (msh_is_used() == RT_TRUE)
{
if (shell->echo_mode)
rt_kprintf("\n");
msh_exec(shell->line, shell->line_position);
}
#endif
收到完整命令的情况
- 这里描述的当接收到\r或者\n符号的时候,代表控制台接收到完整的命令格式的信息了
- 既然程序认为一个完整命令已经被接收到了,那不管对错,都把它先存放到shell结构体的存放历史信息那里。所以调用shell_push_history,不深究了
- 然后做个判断msh是否正常工作呀,在只使用msh的模式下,源码直接就return true了...所以等于必执行进去
- 进入msh_exec(shell->line, shell->line_position),由于宏定义没用到的代码就不贴了...
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)
* 3. chdir to the directry(if possible)
*/
if (_msh_exec_cmd(cmd, length, &cmd_ret) == 0)
{
return cmd_ret;
}
- 第一个参数传进一个字符串,第二个参数是字符串长度(shell->line,shell->line_position)line_position在前面用strlen(shell->line)赋值过了
- 第一个while,很贴心的把命令前的空格字符给忽略掉了
- 如果命令是正确的,_msh_exec_cmd(cmd, length, &cmd_ret) == 0为真,就返回cmd_ret,进去看看那是什么吧
- 不过在此之前先把命令是错误的讲完,_msh_exec_cmd挺长的
- 下面的代码在源码中紧接着上面的代码
/* 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;
- 上面已经返回0了,也就是找不到这条命令的情况,跑到这里来,看看是怎么处理的
- 中括号表示一个作用域,char* tcmd的生命周期只在此作用域内
- 然后,由于可能命令是带参数的,参数也许很长,所以用指针tcmd扫描cmd字符串第一个出现空格的位置
- 让这个位置等于'\0'表示字符串的结束,从而只保留命令的主体,舍弃后面的参数
- 然后,贴心的打印出提示信息,没有找到该命令啊,然后把命令贴出来告诉你是哪条命令。
ok,现在回到_msh_exec_cmd函数中去,看看它里面是如何实现,直接贴上整个函数体代码
- 三个参数分别是命令字符串,字符串长度,一个状态量的赋值指针(但其实源码中并没有找到数据接收这个msh_exec的返回值)
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[RT_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;
...
- 第一个while就是查找命令的主体的大小,边界条件是遇到' '或'\t'或cmd0size == length
- 第一个主体就是主要命令,如果还没结束,空格后面的就是参数,分开处理它
- cmd_func = msh_get_cmd(cmd, cmd0_size);
- cmd_func是一个函数指针------>typedef int (*cmd_function_t)(int argc, char **argv);
- msh_get_cmd(cmd,cmd0_size)的作用是找到这个字符串所对应的在FSymTab段中的函数的地址,然后返回给cmd_func
- 它的具体实现:
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))
{
if (strncmp(index->name, "__cmd_", 6) != 0) continue;
if (strncmp(&index->name[6], cmd, size) == 0 &&
index->name[6 + size] == '\0')
{
cmd_func = (cmd_function_t)index->func;
break;
}
}
return cmd_func;
}
- 创建一个finsh_syscall*类型局部变量用于迭代
- 创建一个返回值存放结果
- for循环遍历FSymTab段中的地址,这个段的生成在第一篇rtthread的记录中详细讲过了
- 里面的具体实现就是匹配字符串,他有自己的一套命名规则
- 最后找到就返回那个代码执行的地址,找不到就返回RT_NULL
- 返回RT_NULL的话_msh_exec_cmd会返回错误的
- 接着继续_msh_exec_cmd的代码
//前面定义了int argc和char *argv[RT_FINSH_ARG_MAX];
//RT_FINSH_ARG_MAX = 10
/* 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;
- 跑到这里,是等于已经成功解析到命令的存在了,接下来是处理后面的参数
- memset先把argv清0,到这里才调用它,应该是避免了每次先清理,损失性能。因为不一定到这里
- argc接收msh_split的返回值,msh_split三个参数分别是命令行的字符串,字符串长度,还有argv参数二维数组
- 懒得一行行看msh_split了,功能肯定是把命令行后面的参数分别解析出来然后放进argv二维数组里去,然后统计参数个数,返回给argc
- 然后就执行cmd_func(argc,argv)这个函数了,命令行函数的真正入口---->至此,一个正确的命令,被执行完毕。
还没收到完整命令的情况
然后又回到线程执行函数里,继续往下走...
#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] = ';';
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;
continue;
}
- else的代码是C-Style模式的东西,不讨论它了
- 下面rt_kprintf是回车换行,然后输出个msh>的字符
- 然后把shell->line和相关的数据清空,continue,进入下一次循环
后面还有几行代码,则是还没有收到命令行结束符的情况,按理这里应该是调用次数最多的
/* it's a large line, discard it */
if (shell->line_position >= FINSH_CMD_SIZE)
shell->line_position = 0;
//判断,如果shell->line的长度大于80的话,不符合设定了,直接清空作废
/* normal character */
if (shell->line_curpos < shell->line_position) //如果数组不为空,调用rt_memove,把数组从0到n的数都往后平移一格,空出line[0]。再把ch填进line[0]
{
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 //把ch填进shell->line[0]里
{
shell->line[shell->line_position] = ch;
if (shell->echo_mode)
rt_kprintf("%c", ch);
}
ch = 0;
shell->line_position ++;
shell->line_curpos++;
if (shell->line_position >= FINSH_CMD_SIZE)//再次判断是否越界
{
/* clear command line */
shell->line_position = 0;
shell->line_curpos = 0;
}
} /* end of device read */
- 这段代码的作用是把接收到的字符填到shell->line这个数组里
- 只要数组没满或者没有收到符合结束条件的字符,就会一直填下去
- 当shell->line是空的时候,程序跑到else里,如果它不是空的,if()会为真。具体区别看代码的加的注释
- 感觉这part源码写得不太好..虽说80个元素的数组并不大,但是头插法就感觉很糟糕
总结
- 大概差不多就是这样了,控制台组件的话