在单片机中实现类似shell的命令行工具

如果在单片机编程过程中有一个类似linux的shell命令工具可以通过串口调试助手输入命令然后运行一些调试函数,将会为编程提供极大的帮助。

本文旨在提供一个十分便于移植和十分简单有效的shell解决方法。
在移植时只需提供shellGetChar函数和shellSendChar函数,函数编写尽量简单、高效甚至可以简单的移植到类似51单片机这样的8位处理器上。

首先编写shell最基础的东西,从串口获取到一行字符串,其特点如下。

  1. 由’\r\n’ 或者’\n’结尾。
  2. 在超级终端上输入字符需要再返回给终端
  3. 需要对退格和删除键进行特殊处理
#include "shell.h"

char    shellLine[100]                  = {SHELL_LINE_MAX_LEN}; /*用于存储从串口接收到的字符串*/
char    *shellParam[SHELL_LINE_MAX_LEN] = {0};                  /*用于存储接收到的参数(包括命令名字)*/
extern  T_ShellCmd* sysCmd[];

uint8_t shellGetChar(char *recCh)
{
	/*需要自己提供获取一个字符的函数,获取到字符返回1,反之返回0 */
	return 0;
}

void shellSendChar(char ch)
{
	/*需要提供发送一个字符的函数*/
}

/*
 *用于从串口获取一条以回车换行结尾的命令
 */
uint8_t shellGetOneLine(char *line, uint8_t maxLen)
{
    char getChar;
    static uint8_t count=0; /*用于记录除特殊字符外的其他有效字符的数量*/ 
    if(shellGetChar(&getChar))
    {
    	if(count>=maxLen) /*长度超限*/
    	{
    		count = 0; /*清零计数器以便后续使用*/
    		return 1;  /*返回有效标志*/
    	}
        line[count] = getChar; /*记录数据*/
        switch(getChar)
        {   
            case 0x08:
            case 0x7F: /*退格键或者删除键*/
            {
                if(count>0)
                {
                    count--; /*删除上一个接收到的字符*/
                }
            }break;
            
            case '\r':
            case '\n': /*接收到回车换行,证明已经收到一个完整的命令*/
            {
                line[count] = '\0'; /*添加字符串结束符,刚好可以去掉'\r'或者'\n'*/
                count = 0; /*清零计数器以便后续使用*/
                return 1; /*返回有效标志*/
            }break;
            
            default:
                count++;
        }
        shellSendChar(getChar); /*把收到的字符输出到串口*/
    }
    return 0;
}

处理好接收一行命令的函数后,再编写函数将输入的一行字符串转换成命令名和参数,需要使用sting.h 中的strtok函数,使用此函数我们可以简便的提取到相关的参数字符。

/*从命令字符串中解析到命令和其参数
 * 获取到的paramArry[0]为要允许的命令名
 * 其他的为命令参数
 * 返回值为获取到的参数的个数(包括一个命令名)
 */
uint8_t shellGetParam(char* line, char *paramArry[], uint8_t arryLen)
{
	uint8_t i,ret;
	char *ptr = NULL;
	ptr = strtok(line, " ");
	for(i=0; ptr!=NULL &&i<arryLen; i++)
	{
		paramArry[i] = ptr;
		ptr = strtok(NULL, ",");		
	}
	ret = i;
	return ret;
}

现在我们可以从串口获取到一节命令字符串,并且可以解析得到命令的名字和参数,接下来就是要去通过命令名字去查找我们预先设置好的命令数组,找到相应的命令函数,然后运行即可。

在我们学习C语言的时候,我们有时候会发现main函数会是这个样子,是带参数的。百科上面的解释argc argv百科

int main(int argc, char* argv[])
{

}

因此我定义的命令函数的原型为

typedef int (*T_ShellFun)(int argc, char*argv[]);

还需要去设计一个结构体去将命令名和命令函数连接起来

typedef struct
{
	char* name; /*命令的名字*/
	char* help; /*帮助描述*/
	T_ShellFun fun; /*命令函数*/
}T_ShellCmd;

这里并没有去定义命了的参数个数,在解析完用户输入,运行命令函数时也就不会去检查用户输入的参数的个数。以便实现以下类似功能

ls 
不输入参数显示当前文件夹下的所有文件和文件夹
ls \user\
输入了参数,显示user目录下的文件和文件夹

定义好了命令描述结构体,和命令函数模板,就可以定义命令了

extern T_ShellCmd *sysCmd[];

int helpCmdFun(int argc, char*argv[]) /*命令函数*/
{
	uint8_t i;
	for(i=0; sysCmd[i]; i++)
	{
		printf("%-15s %s\r\n",sysCmd[i]->name, sysCmd[i]->help);
	}
}
T_ShellCmd helpCmd= /*命令描述*/
{
	.name = "help",
	.help = "show all cmd list",
	.fun = helpCmdFun
};

/*例程,此例程序可以接收两个参数,int a,float b*/
int paramTestCmdFun(int argc, char *argv[]) /*命令函数*/
{
	uint8_t reti,retf;
	int vali;
	float valf;
	printf("get param num %d\r\n", argc); /*如果用户输入的参数不够cmd函数使用,将传入一个默认的地址,其内容为 '\0' */
	vali = shellStr2Int(argv[0], &reti); /*提供的字符串转int函数*/
	valf = shellStr2Float(argv[1], &retf); /**/
	printf("int[%d]:%d\r\n", reti,vali );
	printf("float[%d]:%f\r\n",retf,valf );
	return argc;
}
T_ShellCmd paramTestCmd = /*命令描述结构体*/
{
	.name= "test",
	.help = "(int a=0, float b=0) show tow param val", /*参数提示,使用函数参数书写方式,有等于号表明默认参数,无参数不提示参数项*/ 
	.fun = paramTestCmdFun
};


定义好了一个命令之后我们可以定义一个数组,专门用来存放所有的命令描述结构体

T_ShellCmd *sysCmd[] = 
{
	&helpCmd, /*只存放命令结构体的指针减少对存储的占用*/
	&paramTestCmd, /*按照help的方式建立的其他命令*/
	NULL /*用于标记命令数组的结尾*/
};

最后编写shellMain以及上面用到的shellStr2Int和shellStr2Float函数,

uint8_t shellMain(void)
{
    uint8_t paramNum = 0;
    if(shellGetOneLine(shellLine, SHELL_LINE_MAX_LEN))
    {
        paramNum = shellGetParam(shellLine, shellParam, SHELL_PARAM_MAX_NUM);
        if(paramNum)
        {
            uint8_t i=0;
            for(i=0; sysCmd[i]; i++) /*查找命令名字*/
            {
                if(strcmp(sysCmd[i]->name, shellParam[0]) == 0)
                {
                    int value = sysCmd[i]->fun(paramNum-1, &shellParam[1]); /*运行命令函数*/
                    printf("value %d = 0x%x\r\n", value, value); /*打印运行结果*/
                    return 1;
                }
            }
            if(sysCmd[i] == NULL) /*没有找到命令*/
            {
                printf("C interp: unknown symbol name \'%s\' \r\n",shellLine); /*打印错误信息*/
            }
        }
        printf("->");
    }
    return 0;
}

/*
 * 提供int字符串转int
 */
int shellStr2Int(const char *str, uint8_t* ok)
{
	int ret;
    if(str == NULL)
    {
        ok = false;
        return 0;
    }
	*ok=(uint8_t)sscanf(str,"%d", &ret);
    if(*ok != 1)
    {
        *ok = false;
        return 0;
    }
	return ret;
}

/*
 * 提供flaot字符串转浮点数功能
 */
float shellStr2Float(const char *str, uint8_t* ok)
{
	float ret;
    if(str == NULL)
    {
        ok = false;
        return 0;
    }
	*ok=(uint8_t)sscanf(str,"%f", &ret);
    if(*ok != 1)
    {
        *ok = false;
        return 0.0;
    }
	return ret;
}

/*
 * 提供16进制字符串转数字的功能
 */
int shellStr2Hex(const char *str, uint8_t* ok)
{
	int ret;
    if(str == NULL)
    {
        ok = false;
        return 0;
    }
	*ok=(uint8_t)sscanf(str,"%X", &ret);
    if(*ok != 1)
    {
        *ok = false;
        return 0;
    }
	return ret;
}

最后附一下源码shell源码
文章中的代码已经将代码讲完了,提供的源码里面增加了一个shell_cmd.c用于存放左右的用户自定义命令。
gitee上的代码xcmd

Logo

更多推荐