从0开始写出一个最简单的shell(基于x210)


前言

最近跟着朱有鹏老师学习了linux核心课程,学完了他的裸机部分,在此给自己做个小小的总结。当然自身还有很多不足的地方有待改进

一、什么是shell?

以下总结摘自朱老师的裸机课程部分笔记,我个人总结不了这么全面顾拿来使用。

1.shell是什么鬼

壳与封装
(1)shell就是壳的意思,在计算机中经常提到shell是用户操作接口的意思。
(2)因为计算机程序本身很复杂,里面的实现和外面的调用必须分开。接口本身就是对内部复杂的实现的一种封装,外部只需要通过接口就可以很容易的实现效果,但是却不用理会内部实现的复杂性和原理。

2.程序或操作系统的用户接口

(1)操作系统运行起来后都会给用户提供一个操作界面,这个操作界面就叫shell。用户可以通过shell来调用操作系统内部的复杂实现。
(2)shell编程就是在shell层次上进行编程。譬如linux中的脚本编程、windows中的批处理。

3.两种shell:GUI和cmdline

(1)GUI(图形用户界面),特点是操作简单、易学易用,适合使用电脑来工作的人。
(2)cmdline(命令行界面),譬如linux的终端和windows的cmd,特点是不易用易学,优点是可以进行方便的shell编程,适合做开发的人。
(3)展望:将来的shell应该是声音图像等接口的。

4.shell的运行原理:由消息接收、解析、执行构成的死循环

(1)我们主要分析命令行shell的运行原理。
(2)命令行shell其实就是一个死循环。这个死循环包含3个模块,这3个模块是串联的,分别是命令接收、命令解析、命令执行。
(3)命令行有一个标准命令集,用户在操作的时候必须知道自己想要的操作用通过哪个命令来实现,不能随便输入命令。如果用户输入了一个不是标准命令的命令(不能识别的命令),提示用户这不是一个合法命令,然后重新回到命令行让用户输入下一个命令。
(4)用户输入命令的界面是一个命令行,命令行的意思就是用户输入的命令是以行为单位的,更好理解的说用户输入的命令在用户按下回车键之后就算是结束了,shell可以开始接收了。

5.shell举例:uboot、linux终端、Windows图形界面等

(1)常见的shell,uboot就是一个裸机程序构成的shell(本课程要完成的shell也是裸机的),clinux中断和windows的cmd是操作系统下的命令行shell。windows图形界面、ubuntu图形界面、android的图形界面这些都是图形界面的shell。突然想到另一个类型的shell,网页类型的shell,典型代表就是路由器。

二、我们这个简单shell要实现个什么功能?

以下开始正文
实现类似于linux中的shell界面,输入命令可以自动识别解析命令,做出对应的响应,比如,我敲入led on那么对应的x210上面的led就亮(值得一提的是我这里的硬件都是自己进行手动移植的,所以如果你也按照这篇文章进行了实验,但是却没有成功,那么你除了要好好对照本文的代码之外,还需要看看自己的硬件移植方面是否出现了问题,检查硬件的移植问题也很简单,对照着数据手册看就可以了!)

三、具体实现的步骤


实现类似于linux中的shell界面,输入命令可以自动识别解析命令,做出对应的响应,比如,我敲入led on那么对应的x210上面的led就亮(值得一提的是我这里的硬件都是自己进行手动移植的,所以如果你也按照这篇文章进行了实验,但是却没有成功,那么你除了要好好对照本文的代码之外,还需要看看自己的硬件移植方面是否出现了问题,检查硬件的移植问题也很简单,对照着数据手册看就可以了!)

第一步:实现的shell回显功能以及定义简单命令并解析

基本思路:
1.打印命令行提示符
2.获取用户输入的命令
3.解析用户输入的命令
(1)解析成功即打印出对于命令
(2)解析失败则打印出NULL

上代码:

#include<stdio.h>
#include<string.h>
#define max_command_length 256 //定义命令行的长度

/*定义命令*/
#define led       "led"
#define pwm       "pwm"
#define lcd       "lcd"

#define cmd_num     3   //定义命令的个数

/*初始化命令*/
char g_cmdset[cmd_num][max_command_length]; 
static void init_cmd_set(void){
    memset(g_cmdset, 0, sizeof(g_cmdset));		// 先全部清零
	strcpy(g_cmdset[0], led);
	strcpy(g_cmdset[1], lcd);
	strcpy(g_cmdset[2], pwm);
}

int main(){
   char str[max_command_length];    //用来存放用户输入的命令内容
   int i = 0;
   init_cmd_set();//调用初始化命令
    while(1){   //shell本质上就是一个死循环在不停的解析用户输入的命令
        printf("easy_shell#");//打印命令行提示符,注意不能加换行
        memset(str,0,sizeof(str));//清除str数组以存放新的字符串
        scanf("%s",str);    //获取用户输入的命令
        /*遍历一遍命令,挨个解析用户输入命令*/
        for(i = 0;i<cmd_num;i++){
            /*如果成功匹配到对应的命令则打印出其命令,strcmp的两个字符串相等就返回0所以要加!*/
            if(!strcmp(str,g_cmdset[i])){
                printf("%s\n",g_cmdset[i]);
                break;
            }
        }
        /*如果所有命令都遍历完了还没有对应的命令,就打印出NULL*/
        if(i>=cmd_num){
            printf("NULL\n");
        }
    }
    return 0;    

}

实现效果如下图
请添加图片描述
是不是和Linux中的shell有那么一丢丢相似了,别急我们一步步完善它

第二步:将简单shell移植到开发板上

基本思路:
1.打印命令行提示符
2.获取用户输入的命令
(1)将用户输入的命令打印到终端上显示出来
(2)增加用户输入时按backspace可以删除的操作
实现流程:
将代码在windows的Notepad上实现,然后用共享文件夹移动到ubantu上进行交叉编译(中途有错就在windows的Notepad上进行修改),将交叉编译出来的uart.bin文件通过dnw下载烧录到x210中,然后通过串口打印secureCRT显示出代码实现的效果
代码分文件处理:所用得到代码及其作用有
clock.c :设置时钟
led.c :设置led
link.lds :链接脚本
main.c :主函数
Makefile :编译脚本
mkv210_image.c:用于生成SD卡烧录的文件
start.S :汇编文件,各种初始化
stdio.c :自己写的输出函数
uart.c:串口程序
如图:其中clock.c ,led.c,link.lds,Makefile ,mkv210_image.c,start.S,uart.c这些文件是底层硬件的配置及其相关的文件,并不是我们本文的重点,这里就不过多的赘述了,我们主要看main.c与stdio.c
注:因为要移植到x210的开发板上所以配置文件(clock.c ,led.c,link.lds,Makefile ,mkv210_image.c,start.S,uart.c)必不可少接下来都需要用到,但是又不是重点,所以在这里说一下下面的步骤中就会省略掉了,如果你并不是需要移植到开发板上或者与我的开发板并不相同,那么只需要关注main.c与stdio.c这两个文件即可
请添加图片描述
main.c与stdio.c实现的功能:
main.c:将用户输入的命令打印到终端上显示出来
stdio.c:增加用户输入时按backspace可以删除的操作

上代码(main.c,stdio.c):
以下main.c

void puts(const char *p);
char *gets(char *p);
void uart_init(void);
// C标准库中也有个memset函数,但是我们这里用的是自己写的,没用标准库
void memset(char *p, int val, int length)
{
	int i;
	for (i=0; i<length; i++)
	{
		p[i] = val;
	}
}
int main(void)
{ 
	char buf[100] = {0};		// 用来暂存用户输入的命令
	uart_init();//x210串口初始化,硬件相关
	puts("this is x210 easy_shell: \n");
	while(1)
	{	
		puts("easy_shell# ");
		memset(buf, 0, sizeof(buf));		// buf弄干净好存储这次用户输入
		gets(buf);							// 读取用户输入放入buf中
		puts("您输入的是:");
		puts(buf);
		puts("\n");
	}
	return 0;
}

以下stdio.c

void uart_putc(char c);
char uart_getc(void);
/*因为要用到串口所以上面两个是串口的声明*/

// 从stdio输出一个字符c
void putchar(char c)
{
	// 碰到用户输出'\n'时,实际输出"\r\n"
	// windows中按下回车键等效于"\r\n",在linux中按下回车键等效于'\n'
	//也就是说,我在windows(secureCRT)中按下回车键的时候,是同时输入了\r\n,所以需要补一个\r让输出变成\r\n这样才能正确输出换行
	if (c == '\n')
		uart_putc('\r');	//这个uart_putc是底层串口输出硬件相关的代码在start.S中
	uart_putc(c);
}

// 从stdio输出一个字符串p
void puts(const char *p)
{
	while (*p != '\0')
	{
		/*输出一个字符串,然后向后移动一格,直到*p == '\0'*/
		putchar(*p);
		p++;
	}
}

// 从stdio输入一个字符
char getchar(void)
{
	char c;
	c = uart_getc();
	if (c == '\r')
	{
		return '\n';
	}
	
	return c;
}

// 从stdio输入一个字符串
// 返回值指向传进来的数组的首地址的,目的是实现函数的级联调用
char *gets(char *p)
{
	char *p1 = p;
	char ch;
	
	// 用户的一次输入是以'\n'为结束标志的
	while ((ch = getchar()) != '\n')
	{
		// 回显
		if (ch != '\b')		//'\b'表示的就是退格键也就是键盘上的删除键
		{
			// 用户输入的不是退格键
			putchar(ch);				// 回显
			*p++ = ch;					// 存储ch,等效于 *p = ch; p++;
		}
		else
		{
			// 用户输入的是退格键
			// \b只会让secureCRT终端输出指针向后退一格,但是那个要删掉的字符还在
			// 删掉的方法就是下面3行
			if (p > p1)//保证终端界面上还有用户输入的 数
			{
				/*先输出一个退格键,也就是让光标往后移动一格,这时候光标指向的是最末端的那个字符i,然后输出空格,用空格代替最末端的字符i,这样视觉效果上就是删除了最末端的字符i,但是要清楚在输出空格之后光标是自动会向后再移动一格的,所以需要再输出一个退格键,让光标向前移动到指向空格的位置,在视觉效果上就是指向了第i-1个字符的后面*/
				putchar('\b');
				putchar(' ');
				putchar('\b');				
				/*在上述三步完成之后就是删除了一个字符之后,让p这个指针向前移动指向要删除的那个字符(因为指针可以看作一开始是指向字符串的最末尾的'\0'的),然后补上'\0'*/
				
				/*注意上面三步是处理回显,下面是处理存储指针,不要搞混了!*/
				
				p--;						//向前移动一个,指向要删除的字符
				*p = '\0';					//用'\0'覆盖掉,就相当于删除
			}	 
		}
		
	}
	// 遇到'\n'行结束,添加'\0'作为字符串结尾。
	*p = '\0';
	putchar('\n');
	
	return p1;
}

实现效果如下图:
请添加图片描述

第三步:将第一步和第二步结合起来实现标准命令的解析


我们第一步是实现了命令的解析,第二步是将easy_shell移植到了开发板上,现在我们将两部分结合起来:
因为第一步我们用到了一些函数,是调用标准io库的,但是第二步我们决定直接写标准io库的函数,所以第三步新增了string.c是自己写的一些字符串函数
为了使代码更具有调理我们用shell.h来将所有.c文件公用的函数进行统一声明,这样我们在别的函数中直接调用shell.h文件即可,避免了不必要的声明

代码文件(重要):
main.c :主要作用初始化以及调用功能函数
shell.h :将所有.c文件公用的函数进行统一声明
stdio.c :较上一步没有变,这里就不过多赘述了
cmd.c :包含第一步中命令的解析cmd_parser与新增的命令执行cmd_exec
string.c:自己写的用到的字符串函数

上代码
以下main.c

#include "shell.h"


static void shell_init(void)
{
	// shell init
	init_cmd_set();
	uart_init();
	puts("this is x210 easy_shell:\n");		// shell logo
}

int main(void)
{
	char buf[MAX_LINE_LENGTH] = {0};		// 用来暂存用户输入的命令
	
	shell_init();

	while(1)
	{
		// 第1步:命令获取
		puts("easy_shell# ");
		memset(buf, 0, sizeof(buf));		// buf弄干净好存储这次用户输入
		gets(buf);							// 读取用户输入放入buf中
		//puts("您输入的是:");
		//puts(buf);
		//puts("\n");
		// 第2步:命令解析
		cmd_parser(buf);
		// 第3步:命令执行
		cmd_exec();
	}
	
	return 0;
}

以下shell.h

#ifndef __SHELL_H__
#define __SHELL_H__

// 硬件相关函数声明
void puts(const char *p);
char *gets(char *p);
void uart_init(void);

// 命令解析/执行相关
void init_cmd_set(void);
void cmd_parser(char *str);
void cmd_exec(void);

// 字符串函数相关
void memset(char *p, int val, int length);
void strcpy(char *dst, const char *src);
int strcmp(const char *cs, const char *ct);

// 宏定义
#define MAX_LINE_LENGTH		256			// 命令行长度,命令不能超过这个长度

// 宏定义一些标准命令
#define led					"led"
#define lcd					"lcd"
#define pwm					"pwm"
#define CMD_NUM				3			// 当前系统定义的命令数 

// 全局变量声明
extern char g_cmdset[CMD_NUM][MAX_LINE_LENGTH];

#endif

以下cmd.c

// 命令解析和命令执行相关的函数
#include "shell.h"


char g_cmdset[CMD_NUM][MAX_LINE_LENGTH];


// 初始化命令列表
void init_cmd_set(void)
{
	memset((char *)g_cmdset, 0, sizeof(g_cmdset));		// 先全部清零
	strcpy(g_cmdset[0], led);
	strcpy(g_cmdset[1], lcd);
	strcpy(g_cmdset[2], pwm);
}

// 解析命令
void cmd_parser(char *str)
{
	int i;
	
	for (i=0; i<CMD_NUM; i++)
		{
			if (!strcmp(str, g_cmdset[i]))
			{
				// 相等,找到了这个命令,就去执行这个命令所对应的动作。
				puts("您输入的命令是:");
				puts(str);
				puts("\n");
				break;
			}	
		}
		if (i >= CMD_NUM)
		{
			// 找遍了命令集都没找到这个命令
			puts(str);
			puts("不是一个内部合法命令,请重新输入\n");
			puts("\n");
		}
}

// 执行命令
void cmd_exec(void)
{
	
}

以下string.c

//#include "shell.h"

// C标准库中也有个memset函数,但是我们这里用的是自己写的,没用标准库
void memset(char *p, int val, int length)
{
	int i;
	
	for (i=0; i<length; i++)
	{
		p[i] = val;
	}
}

int strcmp(const char *cs, const char *ct)
{
	unsigned char c1, c2;

	while (1) {
		c1 = *cs++;
		c2 = *ct++;
		if (c1 != c2)
			return c1 < c2 ? -1 : 1;
		if (!c1)
			break;
	}
	return 0;
}

void strcpy(char *dst, const char *src)
{
	while (*src != '\0')
	{
		*dst++ = *src++;
	}
}

实现效果如下图: 既可以有第一步的命令解析(解析成功打印出对应命令。失败提示不是合法命令)也有第二步的删除操作

请添加图片描述

第四步:在第三步的基础上添加一些命令的解析

我们上一步只是实现了普通的命令识别,怎么样去识别led on与led off这样的命令呢?列如我想让led on表示led亮,led off表示led灭

我的方法是设定一个字符串数组cmd[主命令][次命令],将led on这一个命令解析成两个命令led和led on,其中led作为主命令,led on作为次命令,也就是说一个led主命令下面可以对接多个led次命令列如led on,led off等
所以具体实现的解析思路就是:定义一个二维数组cmd,第一维cmd[0]表示主命令led,cmd[1]表示次命令on/off,这样就把led on/off拆分成了两块,第二维cmd[][]就是表示这个命令有多长,列如led有3个长度,on有两个长度。然后将cmd[0]也就是主命令与cmdset(这个里面存着我们之前定义好的命令)对比,看是否存在我们定义的命令
然后定义一个cmd_index,当成功解析一个命令之后就++,就对应着我们前面define的命令集的位数
然后cmd_exec执行命令函数通过cmd_index的值来判断执行哪一个命令

综上所述:我们需要再写一个字符串分割函数,优化cmd_parser解析函数

ps:cmd的大小是事先就规定好大小范围的,这个待优化

代码文件(重要):
main.c :和上一步没有变化
shell.h :新增宏定义及声明
stdio.c :和上一步没有变化
cmd.c :优化cmd_parser解析函数
string.c:新增字符串分割函数
上代码:
以下shell.h

#ifndef __SHELL_H__
#define __SHELL_H__

// 宏定义
#define MAX_LINE_LENGTH		256			// 命令行长度,命令不能超过这个长度

// 宏定义一些标准命令的第一部分
#define led					"led"  //第0个指令
#define lcd					"lcd"  //第1个指令
#define pwm					"pwm"  //第2个指令
#define CMD_NUM				3			// 当前系统定义的命令数 

// 宏定义一个命令相关信息
#define MAX_CMD_PART		5			// 一个命令最多包含几部分			
#define MAX_LEN_PART		20			// 命令的分部分最大长度

// 全局变量声明
extern char g_cmdset[CMD_NUM][MAX_LINE_LENGTH];

// 硬件相关函数声明
void puts(const char *p);
char *gets(char *p);
void uart_init(void);

// 命令解析/执行相关
void init_cmd_set(void);
void cmd_parser(char *str);
void cmd_exec(void);

// 字符串函数相关
void memset(char *p, int val, int length);
void strcpy(char *dst, const char *src);
int strcmp(const char *cs, const char *ct);
void cmdsplit(char cmd[][MAX_LEN_PART], const char *str);

#endif

以下cmd.c

// 命令解析和命令执行相关的函数
#include "shell.h"


char g_cmdset[CMD_NUM][MAX_LINE_LENGTH];		// 命令集,存主命令
char cmd[MAX_CMD_PART][MAX_LEN_PART];			// 当前解析出来的命令
int cmd_index = -1;								// 存储解析到的命令是第几个主命令

// 初始化命令列表
void init_cmd_set(void)
{
	memset((char *)g_cmdset, 0, sizeof(g_cmdset));		// 先全部清零
	strcpy(g_cmdset[0], led);
	strcpy(g_cmdset[1], lcd);
	strcpy(g_cmdset[2], pwm);
	
	memset((char *)cmd, 0, sizeof(cmd));	
}

// 解析命令
void cmd_parser(char *str)
{
	int i;
	
	// 第一步,先将用户输入的次命令字符串分割放入cmd中
	cmdsplit(cmd, str);
	
	// 第二步,将cmd中的次命令第一个字符串和cmdset对比
	cmd_index = -1;
	for (i=0; i<CMD_NUM; i++)
	{
		// cmd[0]就是次命令中的第一个字符串,也就是主命令
		if (!strcmp(cmd[0], g_cmdset[i]))
		{
			// 相等,找到了这个命令,就去执行这个命令所对应的动作。
			//puts("您输入的命令是:");
			//puts(str);
			//puts("\n");
			cmd_index = i;
			
			break;
		}
	}			
/*		
		if (i >= CMD_NUM)
		{
			// 找遍了命令集都没找到这个命令
			cmd_index = -1;	
		}
*/	
}

// 命令没找到处理方法
void do_cmd_notfound(void)
{
	puts(cmd[0]);
	puts("不是一个内部合法命令,请重新输入\n");
}

// led命名的处理方法
void do_cmd_led(void)
{
	puts("led cmd\n");//表示成功执行了这个函数
}

// 执行命令
void cmd_exec(void)
{
	switch (cmd_index)
	{
		case 0://执行#define定义的第0个命令
			do_cmd_led();	/*电亮led*/		break;
		case 1:
		case 2:
		default:
			do_cmd_notfound();		break;
	}
}

以下string.c

#include "shell.h"

// C标准库中也有个memset函数,但是我们这里用的是自己写的,没用标准库
void memset(char *p, int val, int length)
{
	int i;
	
	for (i=0; i<length; i++)
	{
		p[i] = val;
	}
}

int strcmp(const char *cs, const char *ct)
{
	unsigned char c1, c2;

	while (1) {
		c1 = *cs++;
		c2 = *ct++;
		if (c1 != c2)
			return c1 < c2 ? -1 : 1;
		if (!c1)
			break;
	}
	return 0;
}

void strcpy(char *dst, const char *src)
{
	while (*src != '\0')
	{
		*dst++ = *src++;
	}
}

// 将用户输入的字符串命令str按照空格分隔成多个字符串,依次放入cmd二维数组中
void cmdsplit(char cmd[][MAX_LEN_PART], const char *str)
{
	int m = 0, n = 0;	// m表示二位数组第一维,n表示第二维
	while (*str != '\0')
	{
		if (*str != ' ')
		{
			cmd[m][n] = *str;
			n++;
		}
		else
		{
			cmd[m][n] = '\0';
			n = 0;
			m++;
		}
		str++;
	}
	cmd[m][n] = '\0';
}

实现效果如下,这样我们就成功添加了led on与led off这样的命令
请添加图片描述

第五步:在第四步的基础上添加一些命令

第四步中我们解析了命令成功之后只是打印出对应的命令,没有做实际性的操作,这一步我们来写入实际性的操作(cmd_exec)
例如加入led on代表led亮,led off 代表led灭,buzzer on代表蜂鸣器响,buzzer off代表蜂鸣器关
实现思路:根据上一步的实现,我们的cmd[0]中存放的是主指令len/buzzer,我们的cmd[1]中存放的是次指令on/off就用字符串比较函数比较cmd[1]中存放的值与on/off进行比较即可实现

代码文件(重要):
main.c :和上一步没有变化
shell.h :新增宏定义及声明
stdio.c :和上一步没有变化
cmd.c :补全cmd_exec执行函数
string.c:没有变化
上代码:
以下shell.h

#ifndef __SHELL_H__
#define __SHELL_H__
// 宏定义
#define MAX_LINE_LENGTH		256			// 命令行长度,命令不能超过这个长度

// 宏定义一些标准命令的第一部分
#define led					"led"
#define lcd					"lcd"
#define pwm					"buzzer"
#define CMD_NUM				3			// 当前系统定义的命令数 

// 宏定义一个命令相关信息
#define MAX_CMD_PART		5			// 一个命令最多包含几部分			
#define MAX_LEN_PART		20			// 命令的分部分最大长度
// 全局变量声明
extern char g_cmdset[CMD_NUM][MAX_LINE_LENGTH];
// 硬件相关函数声明
void puts(const char *p);
char *gets(char *p);
void uart_init(void);

// 命令解析/执行相关
void init_cmd_set(void);
void cmd_parser(char *str);
void cmd_exec(void);

// 字符串函数相关
void memset(char *p, int val, int length);
void strcpy(char *dst, const char *src);
int strcmp(const char *cs, const char *ct);
void cmdsplit(char cmd[][MAX_LEN_PART], const char *str);

// 各硬件操作的命令相关的函数
// 第一个命令:led
void led_init();
void led_on(void);
void led_off(void);

// 第三个命令:buzzer
void timer2_pwm_init(void);
void buzzer_on(void);
void buzzer_off(void);
#endif

以下cmd.c

// 命令解析和命令执行相关的函数
#include "shell.h"
char g_cmdset[CMD_NUM][MAX_LINE_LENGTH];		// 命令集,存主命令
char cmd[MAX_CMD_PART][MAX_LEN_PART];			// 当前解析出来的命令
int cmd_index = -1;								// 存储解析到的命令是第几个主命令

/****************************具体硬件操作命令处理函数*************************/

// 命令没找到处理方法
void do_cmd_notfound(void)
{
	puts(cmd[0]);
	puts("不是一个内部合法命令,请重新输入\n");

	puts("\n");
}

// led命令的处理方法
void do_cmd_led(void)
{
	int flag = -1;
	//puts("led cmd");
	// 真正的led命令的操作实现
	// 目前支持的命令有led on | led off 
	// cmd[0]里面是led,cmd[1]里面是on|off
	if (!strcmp(cmd[1], "on"))
	{
		// led on
		led_on();
		flag = 1;
	}
	if (!strcmp(cmd[1], "off"))
	{
		// led off
		led_off();
		flag = 1;
	}
	// ..... 还可以继续扩展
	
	if (-1 == flag)
	{
		// 如果一个都没匹配,则打印使用方法
		puts("command error, try: led on | led off");
		puts("\n");
	}
}

// 蜂鸣器命令处理方法
void do_cmd_buzzer(void)
{
	int flag = -1;
	//puts("led cmd");
	// 真正的buzzer命令的操作实现
	// 目前支持的命令有buzzer on | buzzer off 
	// cmd[0]里面是buzzer,cmd[1]里面是on|off
	if (!strcmp(cmd[1], "on"))
	{
		// buzzer on
		buzzer_on();
		flag = 1;
	}
	if (!strcmp(cmd[1], "off"))
	{
		// buzzer off
		buzzer_off();
		flag = 1;
	}
	// ..... 还可以继续扩展
	
	if (-1 == flag)
	{
		// 如果一个都没匹配,则打印使用方法
		puts("command error, try: buzzer on | buzzer off");
		puts("\n");
	}
}

// lcd命令处理方法

// ADC命令处理方法
/*********************************shell 命令解析执行框架***********************/

// 初始化命令列表
void init_cmd_set(void)
{
	memset((char *)g_cmdset, 0, sizeof(g_cmdset));		// 先全部清零
	strcpy(g_cmdset[0], led);
	strcpy(g_cmdset[1], lcd);
	strcpy(g_cmdset[2], pwm);
	
	memset((char *)cmd, 0, sizeof(cmd));	
}

// 解析命令
void cmd_parser(char *str)
{
	int i;
	
	// 第一步,先将用户输入的次命令字符串分割放入cmd中
	cmdsplit(cmd, str);
	
	// 第二步,将cmd中的次命令第一个字符串和cmdset对比
	cmd_index = -1;
	for (i=0; i<CMD_NUM; i++)
	{
		// cmd[0]就是次命令中的第一个字符串,也就是主命令
		if (!strcmp(cmd[0], g_cmdset[i]))
		{
			// 相等,找到了这个命令,就去执行这个命令所对应的动作。
			//puts("您输入的命令是:");
			//puts(str);
			//puts("\n");
			cmd_index = i;
			
			break;
		}
	}			
/*		
		if (i >= CMD_NUM)
		{
			// 找遍了命令集都没找到这个命令
			cmd_index = -1;	
		}
*/	
}

// 执行命令
void cmd_exec(void)
{
	switch (cmd_index)
	{
		case 0:		// led
			do_cmd_led();			break;
		case 1:		// lcd
		case 2:		// buzzer
			do_cmd_buzzer();		break;
		default:
			do_cmd_notfound();		break;
	}

实现效果如下:如果led或者buzzer没有响应的话要去检查自己的驱动是否是对的,我这里已经 成功响应,就不做过多的演示了
请添加图片描述

ps:因为我用的是x210然后pwm控制蜂鸣器这里有个现象要记录一下(与本文没有关系)
引脚直接配置成输出模式,然后输出高电平,buzzer叫了。输出低电平就关了
优点就是SOC不用有pwm功能,只要能输出高低电平就能控制蜂鸣器
缺点是蜂鸣器的鸣叫的频率无法改动,如果想要控制输出模式还需要将其设置为pwm模式

注:
如果再进行添加lcd显示就会牵扯到很多硬件底层相关的东西太过于繁琐也有违我们本文的原本意图,因为前面的五步就已经将框架搭建好了,接下来软件层面只需要依照框架添加就可以了,所以我们这边就不扩充lcd的命令或是其他对应硬件的命令了,有兴趣大家可以自行添加

四、扩展补充


1.实现像uboot一样的开机倒计时

众所周知,uboot是一种bootloader,一般开机过程中,会有倒数三秒然后会进入操作系统,如果再倒数三秒没有完成的时候就将它打断的话,就会进入uboot(uboot就类似于windows的BIOS),uboot进入之后的界面也类似一个shell,可以让我们通过uboot特有的命令进行操作
这里简单拓展以下为什么要有uboot(也就是说,为什么开机的时候要有一种开机程序BootLoader,要分清楚,BootLoader就是一种开机程序,uboot就是一种BootLoader,也就是一种开机程序,我刚开始学的时候一直不懂什么是uboot,所以在这里提一下),操作系统开机需要uboot的原因其实很简单,可以理解为操作系统太大了,里面就没有装开机的程序,就是说操作系统和硬件没有直接相关联,硬件按下启动键之后是先启动了uboot然后再由uboot启动操作系统,没有uboot就启动不了操作系统,可以理解为uboot就是硬件启动操作系统的桥梁

我们这里就来模拟一下uboot启动的开头3秒情景:
目标:开头启动,打印出倒计时,然后在倒计时结束时自动启动我们自己的shell,或者是在倒计时时外部打断来启动我们的shell

思路:就基于上面的简单shell程序进行改动

既然要开头倒计时,那么我们就需要用到定时器,那么在210中有四种定时器(这个定时器和具体用法查数据手册),pwm定时器(用来控制pwm波形,但是没有定时器只能用来产生pwm波形),系统定时器(用于延时,与pwm定时器的差别在于其没有pwm波形产生器,反而多了一个中断),看门狗定时器(用来防止系统出现故障,在系统出现故障不能按时复位(喂狗)时进行rest,也有中断功能),RTC定时器(就是个BCD计数器,包含秒、分、时、日、月、年,以 二进制编码的十进制格式表示(BCD)。)

我们用看门狗进行定时器中断(之前写过拿来用也方便一点,一般用系统定时器):
在你的看门狗定时器(或者是系统定时器)中的中断处理程序函数中实现计时,然后时间没到的时候在屏幕上打印倒数计数,时间(时间须设置为1s)到了自动执行命令,执行完命令进入shell的死循环
伪代码:

// wdt的中断处理程序
void isr_wdt(void)
{
	static int i = 0;
	// 看门狗定时器时间到了时候应该做的有意义的事情
	//printf("wdt interrupt, i = %d...", i++);
	// 计时,然后时间没到的时候在屏幕上打印倒数计数,时间到了自动执行命令
	// 执行完命令进入shell的死循环
	g_bootdelay--;  //等待时间初始值设为3
	putchar('\b');  //输出backspace相当于每次进来中断就减一
	printf("%d", g_bootdelay);//然后打印出来,就实现了3.2.1.倒数
	
	if (g_bootdelay == 0) //如果倒数结束
	{
		g_isgo = 1;
		
		// 把要自动执行的命令添加到这里,但是这里是中断处理程序,不适合执行长代码
		// 所以放在外面要好一些
		
		//printf("g_isgo = 1.\n");
		// 关闭wdt
		//intc_disable(NUM_WDT);
	}

	// 清中断
	intc_clearvectaddr();
	rWTCLRINT = 1;
}

在mian.c中添加定时器的初始化以及倒计时处理
伪代码:

#include "shell.h"
#include "stdio.h"
#include "int.h"

int g_isgo = 0;				// 如果等于0则不能继续执行,等于1则可以继续执行了
int g_bootdelay = 3;		// 等待时间

static void shell_init(void)
{
	// shell init
	init_cmd_set();
	uart_init();
	hardware_init();
	wdt_timer_init();					// 初始化定时器

	puts("x210 simple shell:\n");		// shell logo
}

int main(void)
{
	shell_init();
	
	// 自动倒数执行默认命令
	// 在这里等待用户按按键,如果没按就倒计时,如果按了就结束倒计时和自动执行直接
	// 进入shell死循环。如果一直没按按键时间到了也进入shell死循环
	puts("aston#");
	printf("%d", g_bootdelay);
	while ((!g_isgo) && (!is_key_press()));
	//while (!((g_isgo) || (is_key_press())));
	
	intc_disable(NUM_WDT);//当倒计时结束或者被外部打断,就结束中断
	// 也可以在这里通过判断g_isgo的值来判断是不是倒数结束,执行自动命令
	if (g_isgo)
	{
		lcd_test();
	}

	// 执行shell死循环
	shell_loop();
	return 0;
}


串口程序中添加,按下按键的响应,用于打断中断
伪代码:

// 有人按下按键返回1,没人按按键返回0
int is_key_press(void)
{
	if ((rUTRSTAT0 & (1<<0)))
		return 1;
	else
		return 0;
}

五、后记


由于本人能力有限,现在只学到这么多,如果有人看到这篇文章对裸机部分有兴趣的话可以去找朱有鹏老师学习课程,毕竟裸机是驱动的前身,学好裸机再学驱动事半功倍

值得一提的是,我们这里的打印出数字用的是printf,但是之前都是自己写的,现在要用标准库里面的printf,那么就和自己写的起了冲突,所以两种办法,第一:修改自己写的功能函数的名字,第二:移植一个printf过来,我这里选择的是移植一个printf,所以我又基于友善之臂的printf修修改改移植了过来,但是由于能力有限即便是移植过来的也有和自己写的有重复的,所以…

关于移植printf:移植printf可以通过内核中的printf,uboot中的printf,别人移植过的printf这三个方面进行移植。
printf函数工作时内部实际调用了2个关键函数:一个是vsprintf函数(主要功能是格式化打印信息,最终得到纯字符串格式的打印信息等待输出),另一个就是真正的输出函数putc(操控标准输出的硬件,将信息发送出去)
vsprintf函数的作用是按照我们的printf传进去的格式化标本,对变参进行处理,然后将之格式化后缓存在一个事先分配好的缓冲区中。
printf后半段调用putc函数将缓冲区中格式化好的字符串直接输出到标准输出。

还有一点要注意的是,用SD卡下载的时候,因为SD卡是分为BL1和BL2进行初始化的,BL1一般大小在16KB左右,所以如果你用sd卡下载,你的程序恰好超过了16KB,那么也许不是你的程序出错了而是超出了范围需要对SD卡进行BL1和BL2的划分了

关于BL1与BL2,就是将一部分代码放入BL1中然后先运行BL1,通过运行BL1来引导BL2,设置好之后需要在linux下用dd命令进行烧写

注意:本文多次会用到C语言声明,C语言中声明全局变量时不能加初始化,如果加了编译器就会把这个声明当作定义,而且用到全局变量的时候要注意在start.S(也就是那个初始搭建C语言环境的时候)中设置bss段清零

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值