12 - 使用任務通知實現命令行解釋器

  • 雖然這是介紹FreeRTOS系列的文章,但這篇文章偏重於命令行解釋器的實現。這一方面是因爲任務通知使用起來非常簡單,另一方面也因爲對於嵌入式程序來說,使用命令行解釋器來輔助程序調試是非常有用的。程序調試是一門技術,基本上我們需要兩種調試手段,一種是可以單步仿真的硬件調試器,另外一種是可以長期監視程序狀態的狀態輸出,可以通過串口、顯示屏等等手段輸出異常信息或者某些關鍵點。這裏的命令行解釋器就屬於後者。
  • 本文實現的命令行解釋器具有以下特性:
  1. 支持十進制參數,識別負號;
  2. 支持十六進制參數,十六進制以‘0x’開始;
  3. 命令名長度可定義,默認最大20個字符;
  4. 參數數目可定義,默認最多8個參數;
  5. 命令名和參數之間以空格隔開,空格個數任意;
  6. 整條命令以回車換行符結束;
  7. 整條命令最大長度可定義,默認64字節,包括回車換行符;
  8. 如果使用SecureCRT串口工具(推薦),支持該軟件的控制字符,比如退格鍵、左移鍵、右移鍵等。
  • 一個帶參數的命令格式如下所示:

      參數名 <參數1> <參數2> … <參數3>[回車換行符]
    

編碼風格

  • FreeRTOS的編碼標準及風格見《FreeRTOS系列第4篇—FreeRTOS編碼標準及風格指南》,但我自己的編碼風格跟FreeRTOS並不相同,並且我也不打算改變我當前堅持使用的編碼風格。所以在這篇或者以後的文章中可能會在一個程序中看到兩種不同的編碼風格,對於涉及FreeRTOS的代碼,我儘可能使用FreeRTOS建議的編碼風格,與FreeRTOS無關的代碼,我仍然使用自己的編碼風格。我可以保證,兩種編碼風格決不會影響程序的可讀性,編寫良好可讀性的代碼,是我一直注重並堅持的。

一些準備工作

串口硬件驅動

命令行解釋器使用一個硬件串口,需要外部提供兩個串口底層函數:一個是串口初始化函數init_cmd_uart(),用於初始化串口波特率、中斷等事件;另一個是發送單個字符函數my_putc()。此外,命令行爲串口接收中斷服務程序提供函數fill_rec_buf(),用於保存接收到的字符,當收到回車換行符後,該函數向命令行分析任務發送通知。

一個類printf函數

  • 類printf函數用來格式化輸出,我一般用來輔助調試,爲了方便的將調試代碼從程序中去除,需要將類printf函數進行封裝。我的文章《編寫優質嵌入式C程序》第5.2節給出了一個完整的類printf函數實現和封裝代碼,最終我們使用到的類printf函數是如下形式的宏:
 MY_DEBUGF(CMD_LINE_DEBUG,("第%d個參數:%d\n",i+1,arg[i]));    

使用任務通知

  • 我們將會創建一個任務,用來分析接收到的命令,如果命令有效則調用命令實現函數。這個任務名字爲vTaskCmdAnalyze()。串口接收中斷用於接收命令,如果接收到回車換行符,則向任務vTaskCmdAnalyze()發送任務通知,表明已經接收到一條完整命令,任務可以去處理了。
  • 示意框圖如圖3-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位的整形參數,這在絕大多數嵌入式場合是足夠的。

與分析命令有關數據結構

  • 定義如下:
#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;
  • 緩衝區的大小使用宏來定義,通過更改相應的宏定義,可以設置整條命令的最大長度、命令參數最大數目等。

串口接收中斷處理函數

  • 本文使用的串口軟件是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;
       }
    }    
}

命令行分析任務

命令行分析任務大部分時間都會因爲等待任務通知而處於阻塞狀態。當接收到一個通知後,任務首先去除命令行中的無效字符和控制字符,然後找出命令名並分析參數數目、將參數轉換成十六進制數並保存到參數緩衝區中,最後檢查命令名和參數是否合法,如果合法則調用命令回調函數處理本條命令。

去除無效字符和控制字符

  • 串口軟件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;
}

參數分析

  • 接收到的命令中可能帶有參數,我們需要知道參數的數目,還需要把字符型的參數轉換成整形數並保存到參數緩衝區(這是因爲命令回調函數需要這兩個參數)。這個工作由函數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 ;
}

定義命令回調函數

  • 我們舉兩個例子:第一個是不帶參數的例子,輸入命令後,函數返回一個“Helloworld!”字符串;第二個是帶參數的例子,我們輸入命令和參數後,函數返回每一個參數值。我們在講數據結構的時候特別提到過命令回調函數的原型,這裏要根據這個函數原型來聲明命令回調函數。
不帶參數的命令回調函數舉例
/*打印字符串:Hello world!*/
void printf_hello(int32_t argc,void *cmd_arg)
{
   MY_DEBUGF(CMD_LINE_DEBUG,("Hello world!\n"));
}
帶參數的命令行回調函數舉例
/*打印每個參數*/
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]));
       }
    }
}

定義命令表

  • 在講數據結構的時候,我們定義了與命令有關的數據結構。每條命令需要包括命名名、最大參數、命令回調函數、幫助等信息,這裏要將每條命令組織成列表的形式。
/*命令表*/
const cmd_list_struct cmd_list[]={
/*   命令    參數數目    處理函數        幫助信息                         */   
{"hello",   0,      printf_hello,   "hello                      -打印HelloWorld!"},
{"arg",     8,      handle_arg,      "arg<arg1> <arg2> ...      -測試用,打印輸入的參數"},
};
  • 如果要定義自己的命令,只需要按照6.3節的格式編寫命令回調函數,然後將命令名、參數數目、回調函數和幫助信息按照本節格式加入到命令表中即可。

命令行分析任務實現

  • 有了上面的基礎,命令行分析任務實現起來就非常輕鬆了,源碼如下:
/*命令行分析任務*/
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"));
       }
    }
}

使用的串口工具

  • 推薦使用SecureCRT軟件,這是我覺得最適合命令行交互的串口工具。此外,這個軟件非常強大,除了支持串口,還支持SSH、Telnet等。對於串口,SecureCRT工具還支持文件發送協議:Xmodem、Ymodem和Zmodem。這在使用串口遠程升級時很有用,可以用來發送新的程序二進制文件。我曾經使用Ymodem做過遠程升級,以後有時間再詳細介紹SecureCRT的Ymodem功能細節。
  • 要用於本文介紹的命令行解釋器,要對SecureCRT軟件做一些設置。

設置串口參數

  • 選擇Serial功能、設置端口、波特率、校驗等,特別要注意的是不要勾選任何流控制選項,如圖2-1所示。

設置串口參數
圖2-1:設置串口參數

設置新行模式

  • 依次點擊菜單欄的“選項”—“會話選項”,在彈出的“會話選項”界面中,點擊左邊樹形菜單的“終端”—“仿真”—“模式”,在右邊的仿真模式區域選中“換行”和“新行模式”,如圖2-2所示。

設置新行模式
圖2-2:設置新行模式

設置本地回顯

  • 依次點擊菜單欄的“選項”—“會話選項”,在彈出的“會話選項”界面中,點擊左邊樹形菜單的“終端”—“仿真”—“高級”,在右邊的“高級仿真”區域,選中“本地回顯”,如圖2-3所示。

設置本地回顯
圖2-3:設置本地回顯

測試

  • 我們通過定義了兩個命令,第一條命令的名字爲”hello”,這是一個無參數命令,直接輸出字符串”Hello world!”。第二條命令的名字爲”arg”,是一個帶參數命令,輸出每個參數的值。下面對這兩個命令進行測試。

無參數命令測試

  • 設置好SecureCRT軟件,輸入字符”hello”後,按下回車鍵,設備會返回字符串”Hello world!”。如圖8-1所示。

無參數命令測試

圖8-1:無參數命令測試

帶參數命令測試

  • 設置好SecureCRT軟件,輸入字符”arg 1 2 -3 0x0a”後,按下回車鍵,設備會返回每個參數值。如圖8-2所示。

帶參數命令測試
圖8-2:帶參數命令測試

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章