FreeRTOS使用任務通知實現(xiàn)命令行解釋器
前言
雖然這是介紹FreeRTOS系列的文章,但這篇文章偏重于命令行解釋器的實現(xiàn)。這一方面是因為任務通知使用起來非常簡單,另一方面也因為對于嵌入式程序來說,使用命令行解釋器來輔助程序調試是非常有用的。程序調試是一門技術,基本上我們需要兩種調試手段,一種是可以單步仿真的硬件調試器,另外一種是可以長期監(jiān)視程序狀態(tài)的狀態(tài)輸出,可以通過串口、顯示屏等等手段輸出異常信息或者某些關鍵點。這里的命令行解釋器就屬于后者。
本文實現(xiàn)的命令行解釋器具有以下特性:
支持十進制參數,識別負號;
支持十六進制參數,十六進制以‘0x’開始;
命令名長度可定義,默認最大20個字符;
參數數目可定義,默認最多8個參數;
命令名和參數之間以空格隔開,空格個數任意;
整條命令以回車換行符結束;
整條命令最大長度可定義,默認64字節(jié),包括回車換行符;
如果使用SecureCRT串口工具(推薦),支持該軟件的控制字符,比如退格鍵、左移鍵、右移鍵等。
一個帶參數的命令格式如下所示:
參數名 <參數1> <參數2> … <參數3>[回車換行符]
1.編碼風格
FreeRTOS的編碼標準及風格見FreeRTOS編碼標準及風格指南,但我自己的編碼風格跟FreeRTOS并不相同,并且我也不打算改變我當前堅持使用的編碼風格。所以在這篇或者以后的文章中可能會在一個程序中看到兩種不同的編碼風格,對于涉及FreeRTOS的代碼,我盡可能使用FreeRTOS建議的編碼風格,與FreeRTOS無關的代碼,我仍然使用自己的編碼風格。我可以保證,兩種編碼風格決不會影響程序的可讀性,編寫良好可讀性的代碼,是我一直注重并堅持的。
2.一些準備工作
2.1串口硬件驅動
命令行解釋器使用一個硬件串口,需要外部提供兩個串口底層函數:一個是串口初始化函數init_cmd_uart(),用于初始化串口波特率、中斷等事件;另一個是發(fā)送單個字符函數my_putc()。此外,命令行為串口接收中斷服務程序提供函數fill_rec_buf(),用于保存接收到的字符,當收到回車換行符后,該函數向命令行分析任務發(fā)送通知。
2.2一個類printf函數
類printf函數用來格式化輸出,我一般用來輔助調試,為了方便的將調試代碼從程序中去除,需要將類printf函數進行封裝。我的文章嵌入式C程序優(yōu)質編寫教程第5.2節(jié)給出了一個完整的類printf函數實現(xiàn)和封裝代碼,最終我們使用到的類printf函數是如下形式的宏:
MY_DEBUGF(CMD_LINE_DEBUG,("第%d個參數:%d\n",i+1,arg[i]));
3.使用任務通知
我們將會創(chuàng)建一個任務,用來分析接收到的命令,如果命令有效則調用命令實現(xiàn)函數。這個任務名字為vTaskCmdAnalyze()。串口接收中斷用于接收命令,如果接收到回車換行符,則向任務vTaskCmdAnalyze()發(fā)送任務通知,表明已經接收到一條完整命令,任務可以去處理了。
示意框圖如圖3-1所示。
4.數據結構
命令行解釋器程序需要涉及兩個數據結構:一個與命令有關,包括命令的名字、命令的最大參數數目、命令的回調函數類型、命令幫助信息等;另一個與分析命令有關,包括接收命令字符緩沖區(qū)、存放參數緩沖區(qū)等。
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指向參數緩沖區(qū),目前只支持32位的整形參數,這在絕大多數嵌入式場合是足夠的。
4.2與分析命令有關數據結構
定義如下:
#define ARG_NUM 8 //命令中允許的參數個數 #define CMD_LEN 20 //命令名占用的最大字符長度 #define CMD_BUF_LEN 60 //命令緩存的最大長度 typedef struct { char rec_buf[CMD_BUF_LEN]; //接收命令緩沖區(qū) char processed_buf[CMD_BUF_LEN]; //存儲加工后的命令(去除控制字符) int32_t cmd_arg[ARG_NUM]; //保存命令的參數 }cmd_analyze_struct;
緩沖區(qū)的大小使用宏來定義,通過更改相應的宏定義,可以設置整條命令的最大長度、命令參數最大數目等。
5.串口接收中斷處理函數
本文使用的串口軟件是SecureCRT,在這個軟件下敲擊的任何鍵盤字符,都會立刻通過串口硬件發(fā)送出去,這與Telnet類似。所以我們無需使用串口的FIFO,每接收到一個字符就產生一次中斷。串口中斷與硬件關系密切,所以命令行解釋器提供了一個與硬件無關的函數fill_rec_buf(),每當串口中斷接收到一個字符,就以收到的字符為參數調用這個函數。 fill_rec_buf()函數主要操作變量cmd_analyze,變量的聲明原型為:
cmd_analyze_struct cmd_analyze;
函數fill_rec_buf()的實現(xiàn)代碼為:
/*提供給串口中斷服務程序,保存串口接收到的單個字符*/ 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; /*收到一幀數據,向命令行解釋器任務發(fā)送通知*/ vTaskNotifyGiveFromISR (xCmdAnalyzeHandle,&xHigherPriorityTaskWoken); /*是否需要強制上下文切換*/ portYIELD_FROM_ISR(xHigherPriorityTaskWoken ); } else { rec_count++; /*防御性代碼,防止數組越界*/ if(rec_count>=CMD_BUF_LEN) { rec_count=0; } } }
6.命令行分析任務
命令行分析任務大部分時間都會因為等待任務通知而處于阻塞狀態(tài)。當接收到一個通知后,任務首先去除命令行中的無效字符和控制字符,然后找出命令名并分析參數數目、將參數轉換成十六進制數并保存到參數緩沖區(qū)中,最后檢查命令名和參數是否合法,如果合法則調用命令回調函數處理本條命令。
6.1去除無效字符和控制字符
串口軟件SecureCRT支持控制字符。比如在輸入一串命令的時候,發(fā)現(xiàn)某個字符輸入錯誤,就要使用退格鍵或者左右移動鍵定位到錯誤的位置進行修改。這里的退格鍵和左右移動鍵都屬于控制字符,比如退格鍵的鍵值為0x08、左移鍵的鍵值為0x1B0x5B 0x44。我們之前也說過,在軟件SecureCRT中輸入字符時,每敲擊一個字符,該字符立刻通過串口發(fā)送給我們的嵌入式設備,也就是所有鍵值都會按照敲擊鍵盤的順序存入到接收緩沖區(qū)中,但這里面可能有我們不需要的字符,我們首先需要利用控制字符將不需要的字符刪除掉。這個工作由函數get_true_char_stream()實現(xiàn),代碼如下所示:
/** * 使用SecureCRT串口收發(fā)工具,在發(fā)送的字符流中可能帶有不需要的字符以及控制字符, * 比如退格鍵,左右移動鍵等等,在使用命令行工具解析字符流之前,需要將這些無用字符以 * 及控制字符去除掉. * 支持的控制字符有: * 上移: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參數分析
接收到的命令中可能帶有參數,我們需要知道參數的數目,還需要把字符型的參數轉換成整形數并保存到參數緩沖區(qū)(這是因為命令回調函數需要這兩個參數)。這個工作由函數cmd_arg_analyze()實現(xiàn),代碼如下所示:
/** * 命令參數分析函數,以空格作為一個參數結束,支持輸入十六進制數(如:0x15),支持輸入負數(如-15) * @param rec_buf 命令參數緩存區(qū) * @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()。我們只支持整形參數,這里給出一個字符轉整形函數的簡單實現(xiàn),可以識別負號和十六進制的前綴’0x’。在這個函數中調用了三個C庫函數,分別是isdigit()、isxdigit()和tolower(),因此需要包含頭文件#include <ctype.h>。函數string_to_dec()實現(xiàn)代碼如下:
/*字符串轉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節(jié)的格式編寫命令回調函數,然后將命令名、參數數目、回調函數和幫助信息按照本節(jié)格式加入到命令表中即可。
6.5命令行分析任務實現(xiàn)
有了上面的基礎,命令行分析任務實現(xiàn)起來就非常輕松了,源碼如下:
/*命令行分析任務*/ 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工具還支持文件發(fā)送協(xié)議:Xmodem、Ymodem和Zmodem。這在使用串口遠程升級時很有用,可以用來發(fā)送新的程序二進制文件。我曾經使用Ymodem做過遠程升級,以后有時間再詳細介紹SecureCRT的Ymodem功能細節(jié)。
要用于本文介紹的命令行解釋器,要對SecureCRT軟件做一些設置。
7.1設置串口參數
選擇Serial功能、設置端口、波特率、校驗等,特別要注意的是不要勾選任何流控制選項,如圖2-1所示。
圖2-1:設置串口參數
7.2設置新行模式
依次點擊菜單欄的“選項”---“會話選項”,在彈出的“會話選項”界面中,點擊左邊樹形菜單的“終端”---“仿真”---“模式”,在右邊的仿真模式區(qū)域選中“換行”和“新行模式”,如圖2-2所示。
圖2-2:設置新行模式
7.3設置本地回顯
依次點擊菜單欄的“選項”---“會話選項”,在彈出的“會話選項”界面中,點擊左邊樹形菜單的“終端”---“仿真”---“高級”,在右邊的“高級仿真”區(qū)域,選中“本地回顯”,如圖2-3所示。
圖2-3:設置本地回顯
8.測試
我們通過6.3節(jié)和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:帶參數命令測試
以上就是FreeRTOS使用任務通知實現(xiàn)命令行解釋器的詳細內容,更多關于FreeRTOS任務通知命令行解釋器的資料請關注腳本之家其它相關文章!
相關文章
FreeRTOS實時操作系統(tǒng)隊列的API函數講解
這篇文章主要為大家介紹了FreeRTOS實時操作系統(tǒng)隊列的API函數講解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪2022-04-04FreeRTOS實時操作系統(tǒng)的任務創(chuàng)建與任務切換
這篇文章主要為大家介紹了FreeRTOS實時操作系統(tǒng)的任務創(chuàng)建與任務切換,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪2022-04-04