打印
[应用相关]

使用任务通知实现命令行解释器

[复制链接]
838|9
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
renzheshengui|  楼主 | 2018-9-13 13:02 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
原创地址为:https://blog.csdn.net/zhzht19861011/article/details/50654242

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

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


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

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

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

1.编码风格

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


沙发
renzheshengui|  楼主 | 2018-9-13 13:03 | 只看该作者
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));   

使用特权

评论回复
板凳
renzheshengui|  楼主 | 2018-9-13 13:03 | 只看该作者
3.使用任务通知

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

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



使用特权

评论回复
地板
renzheshengui|  楼主 | 2018-9-13 13:03 | 只看该作者
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
renzheshengui|  楼主 | 2018-9-13 13:04 | 只看该作者
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
renzheshengui|  楼主 | 2018-9-13 13:05 | 只看该作者
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;



  • }


使用特权

评论回复
7
renzheshengui|  楼主 | 2018-9-13 13:05 | 只看该作者
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==0x20)        //为空格



  •        {



  •            blank_space_flag=1;              



  •            continue;



  •        }



  •         else if(rec_buf==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=string_to_dec((unsigned char *)(rec_buf+index),len-index);



  •     }



  •     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==0x20 || buf==0x0D)    //为空格



  •        {



  •            break;



  •        }







  •        result *= base;



  •        if(isdigit(buf))                 //是否为0~9



  •        {



  •            result += buf-'0';



  •        }



  •        else if(isxdigit(buf))           //是否为a~f或者A~F



  •        {



  •             result+=tolower(buf)-87;



  •        }



  •        else



  •        {



  •            result += buf-'0';



  •        }                                       



  •     }



  •    result *= neg;







  •     return result ;



  • }



使用特权

评论回复
8
renzheshengui|  楼主 | 2018-9-13 13:05 | 只看该作者
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));



  •        }



  •     }



  • }



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==' ')||(cmd_analyze.processed_buf==0x0D)))



  •            {



  •                 cmd_buf='\0';        //字符串结束符



  •                 break;



  •            }



  •            else



  •            {



  •                 cmd_buf=cmd_analyze.processed_buf;



  •            }



  •        }







  •        rec_arg_num=cmd_arg_analyze(&cmd_analyze.processed_buf,rec_num);







  •        for(i=0;i<sizeof(cmd_list)/sizeof(cmd_list[0]);i++)



  •        {



  •            if(!strcmp(cmd_buf,cmd_list.cmd_name))       //字符串相等



  •            {



  •                 if(rec_arg_num<0 || rec_arg_num>cmd_list.max_args)



  •                 {



  •                     MY_DEBUGF(CMD_LINE_DEBUG,("参数数目过多!\n"));



  •                 }



  •                 else



  •                 {



  •                     cmd_list.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"));



  •        }



  •     }



  • }



使用特权

评论回复
9
renzheshengui|  楼主 | 2018-9-13 13:06 | 只看该作者
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:设置本地回显


使用特权

评论回复
10
renzheshengui|  楼主 | 2018-9-13 13:06 | 只看该作者
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:带参数命令测试


使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

78

主题

4079

帖子

2

粉丝