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个元素的数组并不大,但是头插法就感觉很糟糕

总结

  • 大概差不多就是这样了,控制台组件的话
posted @ 2020-09-26 11:57  JoyooO  阅读(867)  评论(1编辑  收藏  举报