虽然这是介绍FreeRTOS系列的文章,但这篇文章偏重于命令行解释器的实现。这一方面是因为任务通知使用起来非常简单,另一方面也因为对于嵌入式程序来说,使用命令行解释器来辅助程序调试是非常有用的。程序调试是一门技术,基本上我们需要两种调试手段,一种是可以单步仿真的硬件调试器,另外一种是可以长期监视程序状态的状态输出,可以通过串口、显示屏等等手段输出异常信息或者某些关键点。这里的命令行解释器就属于后者。

本文实现的命令行解释器具有以下特性:

  • 支持十进制参数,识别负号;
  • 支持十六进制参数,十六进制以‘0x’开始;
  • 命令名长度可定义,默认最大20个字符;
  • 参数数目可定义,默认最多8个参数;
  • 命令名和参数之间以空格隔开,空格个数任意;
  • 整条命令以回车换行符结束;
  • 整条命令最大长度可定义,默认64字节,包括回车换行符;
  • 如果使用SecureCRT串口工具(推荐),支持该软件的控制字符,比如退格键、左移键、右移键等。

一个带参数的命令格式如下所示:

                                      参数名 <参数1> <参数2> … <参数3>[回车换行符]

1.编码风格

FreeRTOS的编码标准及风格见《FreeRTOS系列第4篇---FreeRTOS编码标准及风格指南》,但我自己的编码风格跟FreeRTOS并不相同,并且我也不打算改变我当前坚持使用的编码风格。所以在这篇或者以后的文章中可能会在一个程序中看到两种不同的编码风格,对于涉及FreeRTOS的代码,我尽可能使用FreeRTOS建议的编码风格,与FreeRTOS无关的代码,我仍然使用自己的编码风格。我可以保证,两种编码风格决不会影响程序的可读性,编写良好可读性的代码,是我一直注重并坚持的。

2.一些准备工作

2.1串口硬件驱动

命令行解释器使用一个硬件串口,需要外部提供两个串口底层函数:一个是串口初始化函数init_cmd_uart(),用于初始化串口波特率、中断等事件;另一个是发送单个字符函数my_putc()。此外,命令行为串口接收中断服务程序提供函数fill_rec_buf(),用于保存接收到的字符,当收到回车换行符后,该函数向命令行分析任务发送通知。

2.2一个类printf函数

类printf函数用来格式化输出,我一般用来辅助调试,为了方便的将调试代码从程序中去除,需要将类printf函数进行封装。我的文章《编写优质嵌入式C程序》第5.2节给出了一个完整的类printf函数实现和封装代码,最终我们使用到的类printf函数是如下形式的宏:

 MY_DEBUGF(CMD_LINE_DEBUG,("第%d个参数:%d\n",i+1,arg[i]));    

3.使用任务通知

我们将会创建一个任务,用来分析接收到的命令,如果命令有效则调用命令实现函数。这个任务名字为vTaskCmdAnalyze()。串口接收中断用于接收命令,如果接收到回车换行符,则向任务vTaskCmdAnalyze()发送任务通知,表明已经接收到一条完整命令,任务可以去处理了。

示意框图如图3-1所示。

4.数据结构

命令行解释器程序需要涉及两个数据结构:一个与命令有关,包括命令的名字、命令的最大参数数目、命令的回调函数类型、命令帮助信息等;另一个与分析命令有关,包括接收命令字符缓冲区、存放参数缓冲区等。

4.1与命令有关的数据结构

定义如下:

typedef struct {
        char const *cmd_name;                        //命令字符串
        int32_t max_args;                            //最大参数数目
        void (*handle)(int argc,void * cmd_arg);     //命令回调函数
        char  *help;                                 //帮助信息
    }cmd_list_struct;

需要说明一下命令回调函数的参数,argc保存接收到的参数数目,cmd_arg指向参数缓冲区,目前只支持32位的整形参数,这在绝大多数嵌入式场合是足够的。

4.2与分析命令有关数据结构

定义如下:

#define ARG_NUM     8          //命令中允许的参数个数
#define CMD_LEN     20         //命令名占用的最大字符长度
#define CMD_BUF_LEN 60         //命令缓存的最大长度
 
typedef struct {
    char rec_buf[CMD_BUF_LEN];            //接收命令缓冲区
    char processed_buf[CMD_BUF_LEN];      //存储加工后的命令(去除控制字符)
    int32_t cmd_arg[ARG_NUM];             //保存命令的参数
}cmd_analyze_struct;

缓冲区的大小使用宏来定义,通过更改相应的宏定义,可以设置整条命令的最大长度、命令参数最大数目等。

5.串口接收中断处理函数

本文使用的串口软件是SecureCRT,在这个软件下敲击的任何键盘字符,都会立刻通过串口硬件发送出去,这与Telnet类似。所以我们无需使用串口的FIFO,每接收到一个字符就产生一次中断。串口中断与硬件关系密切,所以命令行解释器提供了一个与硬件无关的函数fill_rec_buf(),每当串口中断接收到一个字符,就以收到的字符为参数调用这个函数。       fill_rec_buf()函数主要操作变量cmd_analyze,变量的声明原型为:

       cmd_analyze_struct cmd_analyze;

函数fill_rec_buf()的实现代码为:

/*提供给串口中断服务程序,保存串口接收到的单个字符*/
void fill_rec_buf(char data)
{
    //接收数据 
    static uint32_t rec_count=0;
   
   cmd_analyze.rec_buf[rec_count]=data;
    if(0x0A==cmd_analyze.rec_buf[rec_count] && 0x0D==cmd_analyze.rec_buf[rec_count-1])
    {
       BaseType_t xHigherPriorityTaskWoken = pdFALSE;
       rec_count=0;
       
       /*收到一帧数据,向命令行解释器任务发送通知*/
       vTaskNotifyGiveFromISR (xCmdAnalyzeHandle,&xHigherPriorityTaskWoken);
       
       /*是否需要强制上下文切换*/
       portYIELD_FROM_ISR(xHigherPriorityTaskWoken );
    }
    else
    {
       rec_count++;
       
       /*防御性代码,防止数组越界*/
       if(rec_count>=CMD_BUF_LEN)
       {
           rec_count=0;
       }
    }    
}

6.命令行分析任务

命令行分析任务大部分时间都会因为等待任务通知而处于阻塞状态。当接收到一个通知后,任务首先去除命令行中的无效字符和控制字符,然后找出命令名并分析参数数目、将参数转换成十六进制数并保存到参数缓冲区中,最后检查命令名和参数是否合法,如果合法则调用命令回调函数处理本条命令。

6.1去除无效字符和控制字符

串口软件SecureCRT支持控制字符。比如在输入一串命令的时候,发现某个字符输入错误,就要使用退格键或者左右移动键定位到错误的位置进行修改。这里的退格键和左右移动键都属于控制字符,比如退格键的键值为0x08、左移键的键值为0x1B0x5B 0x44。我们之前也说过,在软件SecureCRT中输入字符时,每敲击一个字符,该字符立刻通过串口发送给我们的嵌入式设备,也就是所有键值都会按照敲击键盘的顺序存入到接收缓冲区中,但这里面可能有我们不需要的字符,我们首先需要利用控制字符将不需要的字符删除掉。这个工作由函数get_true_char_stream()实现,代码如下所示:

/**
* 使用SecureCRT串口收发工具,在发送的字符流中可能带有不需要的字符以及控制字符,
* 比如退格键,左右移动键等等,在使用命令行工具解析字符流之前,需要将这些无用字符以
* 及控制字符去除掉.
* 支持的控制字符有:
*   上移:1B 5B 41
*   下移:1B 5B 42
*   右移:1B 5B 43
*   左移:1B 5B 44
*   回车换行:0D 0A
*  Backspace:08
*  Delete:7F
*/
static uint32_t get_true_char_stream(char *dest,const char *src)
{
   uint32_t dest_count=0;
   uint32_t src_count=0;
   
    while(src[src_count]!=0x0D && src[src_count+1]!=0x0A)
    {
       if(isprint(src[src_count]))
       {
           dest[dest_count++]=src[src_count++];
       }
       else
       {
           switch(src[src_count])
           {
                case    0x08:                          //退格键键值
                {
                    if(dest_count>0)
                    {
                        dest_count --;
                    }
                    src_count ++;
                }break;
                case    0x1B:
                {
                    if(src[src_count+1]==0x5B)
                    {
                        if(src[src_count+2]==0x41 || src[src_count+2]==0x42)
                        {
                            src_count +=3;              //上移和下移键键值
                        }
                        else if(src[src_count+2]==0x43)
                        {
                            dest_count++;               //右移键键值
                            src_count+=3;
                        }
                        else if(src[src_count+2]==0x44)
                        {
                            if(dest_count >0)           //左移键键值
                            {
                                dest_count --;
                            }
                           src_count +=3;
                        }
                        else
                        {
                            src_count +=3;
                        }
                    }
                    else
                    {
                        src_count ++;
                    }
                }break;
                default:
                {
                    src_count++;
                }break;
           }
       }
    }
   dest[dest_count++]=src[src_count++];
    dest[dest_count++]=src[src_count++];
    return dest_count;
}

6.2参数分析

接收到的命令中可能带有参数,我们需要知道参数的数目,还需要把字符型的参数转换成整形数并保存到参数缓冲区(这是因为命令回调函数需要这两个参数)。这个工作由函数cmd_arg_analyze()实现,代码如下所示:

/**
* 命令参数分析函数,以空格作为一个参数结束,支持输入十六进制数(如:0x15),支持输入负数(如-15)
* @param rec_buf   命令参数缓存区
* @param len       命令的最大可能长度
* @return -1:       参数个数过多,其它:参数个数
*/
static int32_t cmd_arg_analyze(char *rec_buf,unsigned int len)
{
   uint32_t i;
   uint32_t blank_space_flag=0;    //空格标志
   uint32_t arg_num=0;             //参数数目
   uint32_t index[ARG_NUM];        //有效参数首个数字的数组索引
   
    /*先做一遍分析,找出参数的数目,以及参数段的首个数字所在rec_buf数组中的下标*/
    for(i=0;i<len;i++)
    {
       if(rec_buf[i]==0x20)        //为空格
       {
           blank_space_flag=1;              
           continue;
       }
        else if(rec_buf[i]==0x0D)   //换行
       {
           break;
       }
       else
       {
           if(blank_space_flag==1)
           {
                blank_space_flag=0; 
                if(arg_num < ARG_NUM)
                {
                   index[arg_num]=i;
                    arg_num++;         
                }
                else
                {
                    return -1;      //参数个数太多
                }
           }
       }
    }
   
    for(i=0;i<arg_num;i++)
    {
        cmd_analyze.cmd_arg[i]=string_to_dec((unsigned char *)(rec_buf+index[i]),len-index[i]);
    }
    return arg_num;
}

在这个函数cmd_arg_analyze()中,调用了字符转整形函数string_to_dec()。我们只支持整形参数,这里给出一个字符转整形函数的简单实现,可以识别负号和十六进制的前缀’0x’。在这个函数中调用了三个C库函数,分别是isdigit()、isxdigit()和tolower(),因此需要包含头文件#include <ctype.h>。函数string_to_dec()实现代码如下:

/*字符串转10/16进制数*/
static int32_t string_to_dec(uint8_t *buf,uint32_t len)
{
   uint32_t i=0;
   uint32_t base=10;       //基数
   int32_t  neg=1;         //表示正负,1=正数
   int32_t  result=0;
   
    if((buf[0]=='0')&&(buf[1]=='x'))
    {
       base=16;
       neg=1;
       i=2;
    }
    else if(buf[0]=='-')
    {
       base=10;
       neg=-1;
       i=1;
    }
    for(;i<len;i++)
    {
       if(buf[i]==0x20 || buf[i]==0x0D)    //为空格
       {
           break;
       }
       
       result *= base;
       if(isdigit(buf[i]))                 //是否为0~9
       {
           result += buf[i]-'0';
       }
       else if(isxdigit(buf[i]))           //是否为a~f或者A~F
       {
            result+=tolower(buf[i])-87;
       }
       else
       {
           result += buf[i]-'0';
       }                                        
    }
   result *= neg;
   
    return result ;
}

6.3定义命令回调函数

我们举两个例子:第一个是不带参数的例子,输入命令后,函数返回一个“Helloworld!”字符串;第二个是带参数的例子,我们输入命令和参数后,函数返回每一个参数值。我们在讲数据结构的时候特别提到过命令回调函数的原型,这里要根据这个函数原型来声明命令回调函数。

6.3.1不带参数的命令回调函数举例

/*打印字符串:Hello world!*/
void printf_hello(int32_t argc,void *cmd_arg)
{
   MY_DEBUGF(CMD_LINE_DEBUG,("Hello world!\n"));
}

6.3.2带参数的命令行回调函数举例

/*打印每个参数*/
void handle_arg(int32_t argc,void * cmd_arg)
{
   uint32_t i;
   int32_t  *arg=(int32_t *)cmd_arg;
   
    if(argc==0)
    {
       MY_DEBUGF(CMD_LINE_DEBUG,("无参数\n"));
    }
    else
    {
       for(i=0;i<argc;i++)
       {
           MY_DEBUGF(CMD_LINE_DEBUG,("第%d个参数:%d\n",i+1,arg[i]));
       }
    }
}

6.4定义命令表

在讲数据结构的时候,我们定义了与命令有关的数据结构。每条命令需要包括命名名、最大参数、命令回调函数、帮助等信息,这里要将每条命令组织成列表的形式。

/*命令表*/
const cmd_list_struct cmd_list[]={
/*   命令    参数数目    处理函数        帮助信息                         */   
{"hello",   0,      printf_hello,   "hello                      -打印HelloWorld!"},
{"arg",     8,      handle_arg,      "arg<arg1> <arg2> ...      -测试用,打印输入的参数"},
};

如果要定义自己的命令,只需要按照6.3节的格式编写命令回调函数,然后将命令名、参数数目、回调函数和帮助信息按照本节格式加入到命令表中即可。

6.5命令行分析任务实现

有了上面的基础,命令行分析任务实现起来就非常轻松了,源码如下:

/*命令行分析任务*/
void vTaskCmdAnalyze( void *pvParameters )
{
   uint32_t i;
   int32_t rec_arg_num;
    char cmd_buf[CMD_LEN];      
   
    while(1)
    {
       uint32_t rec_num;
       
       ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
    rec_num=get_true_char_stream(cmd_analyze.processed_buf,cmd_analyze.rec_buf);
       
       /*从接收数据中提取命令*/
       for(i=0;i<CMD_LEN;i++)
       {
           if((i>0)&&((cmd_analyze.processed_buf[i]==' ')||(cmd_analyze.processed_buf[i]==0x0D)))
           {
                cmd_buf[i]='\0';        //字符串结束符
                break;
           }
           else
           {
                cmd_buf[i]=cmd_analyze.processed_buf[i];
           }
       }
       
       rec_arg_num=cmd_arg_analyze(&cmd_analyze.processed_buf[i],rec_num);
       
       for(i=0;i<sizeof(cmd_list)/sizeof(cmd_list[0]);i++)
       {
           if(!strcmp(cmd_buf,cmd_list[i].cmd_name))       //字符串相等
           {
                if(rec_arg_num<0 || rec_arg_num>cmd_list[i].max_args)
                {
                    MY_DEBUGF(CMD_LINE_DEBUG,("参数数目过多!\n"));
                }
                else
                {
                    cmd_list[i].handle(rec_arg_num,(void *)cmd_analyze.cmd_arg);
                }
                break;
           }
           
       }
       if(i>=sizeof(cmd_list)/sizeof(cmd_list[0]))
       {
           MY_DEBUGF(CMD_LINE_DEBUG,("不支持的指令!\n"));
       }
    }
}

7.使用的串口工具

推荐使用SecureCRT软件,这是我觉得最适合命令行交互的串口工具。此外,这个软件非常强大,除了支持串口,还支持SSH、Telnet等。对于串口,SecureCRT工具还支持文件发送协议:Xmodem、Ymodem和Zmodem。这在使用串口远程升级时很有用,可以用来发送新的程序二进制文件。我曾经使用Ymodem做过远程升级,以后有时间再详细介绍SecureCRT的Ymodem功能细节。

要用于本文介绍的命令行解释器,要对SecureCRT软件做一些设置。

7.1设置串口参数

选择Serial功能、设置端口、波特率、校验等,特别要注意的是不要勾选任何流控制选项,如图2-1所示。

图2-1:设置串口参数

7.2设置新行模式

依次点击菜单栏的“选项”---“会话选项”,在弹出的“会话选项”界面中,点击左边树形菜单的“终端”---“仿真”---“模式”,在右边的仿真模式区域选中“换行”和“新行模式”,如图2-2所示。

图2-2:设置新行模式

7.3设置本地回显

依次点击菜单栏的“选项”---“会话选项”,在弹出的“会话选项”界面中,点击左边树形菜单的“终端”---“仿真”---“高级”,在右边的“高级仿真”区域,选中“本地回显”,如图2-3所示。

图2-3:设置本地回显

8.测试

我们通过6.3节和6.4接定义了两个命令,第一条命令的名字为”hello”,这是一个无参数命令,直接输出字符串”Hello world!”。第二条命令的名字为”arg”,是一个带参数命令,输出每个参数的值。下面对这两个命令进行测试。

8.1无参数命令测试

设置好SecureCRT软件,输入字符”hello”后,按下回车键,设备会返回字符串”Hello world!”。如图8-1所示。

图8-1:无参数命令测试

8.2带参数命令测试

设置好SecureCRT软件,输入字符”arg 1 2 -3 0x0a”后,按下回车键,设备会返回每个参数值。如图8-2所示。

图8-2:带参数命令测试

(十五)使用任务通知实现命令行解释器相关推荐

  1. Linux 下五个顶级的开源命令行 Shell

    这个世界上有两种 Linux 用户:敢于冒险的和态度谨慎的. 其中一类用户总是本能的去尝试任何能够戳中其痛点的新选择.他们尝试过不计其数的窗口管理器.系统发行版和几乎所有能找到的桌面插件. 另一类用户 ...

  2. cad命令栏还原默认_CAD十五个必学的命令 掌握后能走天下了

    时常有人问,怎样学CAD. 这个问题实在是太难回答了,认识界面是学,二维设计也是学,三维设计也是学,二次开发也是学... 如果按照普遍所理解的那样,懂绘图就算学会了,那就事情就容易办了. 绘图用得最多 ...

  3. 操作系统课程设计---实验十 简单shell命令行解释器的设计与实现

    实验十 简单shell命令行解释器的设计与实现 完整课程设计源码及其报告查看:陈陈的操作系统课程设计 1.实验目的 本实验主要目的在于进一步学会如何在 Linux 系统下使用进程相关的系统调用,了解 ...

  4. 操作系统课设之简单 shell 命令行解释器的设计与实现

    前言 课程设计开始了,实验很有意思,写博客总结学到的知识 白嫖容易,创作不易,学到东西才是真 本文原创,创作不易,转载请注明!!! 本文链接 个人博客:https://ronglin.fun/arch ...

  5. linux初始:命令行解释器(shell)、权限

    目录 命令行解释器 什么是命令行解释器(shell) 命令行解释器的作用 权限 权限的种类 结合用户去理解权限 如何查看权限 用户和用户组 如何看懂权限 如何更改权限 权限对于文件或文件夹的影响 权限 ...

  6. GitHub 五万星登顶,命令行的艺术!

    今天给大家推荐一个GitHub开源项目<The Art of Command Line(命令行的艺术)>,这个开源项目雄踞了 GitHub TOP 周榜,直接以 53972 Star 登上 ...

  7. 百战RHCE(第十五战:Linux进阶命令十二-主机名和域名解析极简管理)

    哈喽哈喽哈喽,大家好啊,很高兴大家能看到这篇文章! 首先,本人目前是计算机专业的大一学生,基于对Linux操作系统的爱好,参与了RHCE的培训班,而我这次编写的 <百战RHCE>文章,是基 ...

  8. 芝诺数解|「十五」考研路漫漫,行则将至——重庆考研分析报告

    前言 又一年的考研大战即将拉开帷幕,9月下旬报名,11月上旬陆续开始现场确认,2020年考研倒计时不足40天.作为求学生涯中的又一重要考试,考研受到广大学子的普遍关注.数据显示,近10年考研人数年年增 ...

  9. C shell命令行解释器

    实现简单的shell命令解释器: <blockquote> 1)shell内部命令处理:cd,exit等 2)shell外部命令处理 3)I/O重定向: 4)管道: 5)其他功能 /*Au ...

  10. linux 复制指定类型,用Linux命令行实现删除和复制指定类型的文件

    (一)Linux 删除当前目录及子目录中所有某种类型的文件 方法1 : 此方法不能处理目录中带空格的那些. rm -rf `find . -name "*.example"` Li ...

最新文章

  1. 嵌入式linux开发中常见的虚拟机和主机的文件共享问题
  2. 此任务要求应用程序具有提升的权限
  3. 什么是typora,什么是markdown?利用typora编写markdown文本
  4. static、final、static final 用法
  5. ubuntu18.10终端的方块改成竖线
  6. SiameseRPN++分析
  7. 使用mocha进行测试 区块链
  8. MAC使用homeBrew安装Redis
  9. 还是原来的配方和味道!《英雄联盟》手游界面再曝光...
  10. 写在前面--点燃酱爆心中的那团火
  11. “我曾经的小项目比我在软件行业十年产生的影响还要大”
  12. linux设备模型之tty驱动架构分析,linux设备模型之uart驱动架构分析
  13. 拓端tecdat|Matlab马尔可夫链蒙特卡罗法(MCMC)估计随机波动率(SV,Stochastic Volatility) 模型
  14. 7.3 超标量流水线
  15. 数字货币智能合约:分析以太坊信标链
  16. 无法获得 VMCI 驱动程序的版本: 句柄无效解决方法
  17. 如何证明pi是无理数
  18. linux find命令 括号,Linux中find命令细节详解
  19. 如何利用SFTP在远程服务器中保障文件传输安全
  20. LFY-SpringBoot1【课程概述、springboot2概述】

热门文章

  1. IDEA项目名称的中文和数字乱码文字
  2. PageOffice常用功能之-OA系统中的文档在线编辑及流转
  3. 信用风险频发背后:11月约600亿信用债发行取消
  4. 四字母net域名值钱吗?四字母域名取名有什么技巧?
  5. 滴滴裁员2000人:老板辞退你,从来都不是因为钱
  6. 中南大学计算机学院2021复试名单,2021年中南大学研究生拟录取名单整理汇总(各学院)...
  7. 如果你对未来还有点迷茫不妨来看一下,必看的软件测试指引!!!
  8. chrome插件之网页翻译插件
  9. [研一上]人脸属性迁移文献梳理(1)
  10. 腾讯QQ会员中心g_tk32算法【C#版】