文章目錄
- 前言
- 一、什么是shell?
- 1.shell是什么鬼
- 2.程式或作業系統的用戶介面
- 3.兩種shell:GUI和cmdline
- 4.shell的運行原理:由訊息接收、決議、執行構成的死回圈
- 5.shell舉例:uboot、linux終端、Windows圖形界面等
- 二、我們這個簡單shell要實作個什么功能?
- 三、具體實作的步驟
- 第一步:實作的shell回顯功能以及定義簡單命令并決議
- 第二步:將簡單shell移植到開發板上
- 第三步:將第一步和第二步結合起來實作標準命令的決議
- 第四步:在第三步的基礎上添加一些命令的決議
- 第五步:在第四步的基礎上添加一些命令
- 四、擴展補充
- 1.實作像uboot一樣的開機倒計時
- 五、后記
前言
最近跟著朱有鵬老師學習了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段清零
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/294555.html
標籤:其他
下一篇:IOTOS驅動詳解-引數的上傳
