超级终端,其实就一个输入输出设备,搞ARM的,这个东西可能再熟悉不过了,XP下就自带了一个,在 程序->附件->通讯工具->超级终端,可以看到。
我们通常是用基于串口的超级终端,即通过串口实现输入输出,来与CPU交互,控制CPU的执行。
先看仿真工程,如图

8051.JPG
终端显示结果:下图

ht.jpg
这里似乎出现了乱码,可先别乱下结论,原来是proteus自带的串口终端根本就没有解析VT100协议,很多命令自符直接打印出来了的原因,比如,控制终端前颜色,背景色,清屏等一些命令,如果想完整仿真这个工程,建议通过虚拟串口,用XP自带的终端来实现交互,而不要用proteus的那个,如何连接虚拟串口,这个问题搜索论坛吧,有个兄弟写的教程已经很详细了,我也不多此一举了。
这里只支持4条命令,help;prompt;clear;reboot;需要可以自行添加,比如内存查看命令等等,自己也动动手修改,这样才有进步!
今天觉得无聊,详解它,代码解释
第一步,任何代码入口都从main函数开始,我们来看看:
CODE:
void main(void)
{
InitHyperTerminal();
while(1)
{
RunHyperTerminal();
}
}哇,就这么几行,也太简单了吧,某人写得流水灯好象也比这复杂啊,话是没错,解释。。。
InitHyperTerminal(); 初始化超级终端,很好理解,看看里面究竟在干什么?
CODE:
void InitHyperTerminal(void)
{
TMOD |= 0x20; /* timer1, mode 2, 8 bit reload */
SCON = 0x50; /* serial mode 1, 8 bit uart, enable receive */
PCON = 0x80; /* SMOD = 1, double baud */
TH1 = 0xFF; /* baud = 57600, fosc = 11.0592MHZ */
TL1 = 0xFF;
RI = 0; /* clear receive flag */
TI = 0; /* clear send flag */
TR1 = 1; /* start timer1 */
ES = 1; /* enable serial interrupt */
EA = 1; /* enable all interrupt */
CursorPosion = 0;
ExecCommandFlag = 0;
memset(&SerialBuffer[0],'\0',MAX_SERIAL_BUFFER_SIZE);
memcpy(&PromptBuffer[0],"-->>",MAX_PROMPT_BUFFER_SIZE);
SerialSendStr(F_LIGHTGREEN);
SerialSendStr(B_BLACK);
SerialSendStr(CLEARSCREEN);
SerialSendStr("-----------------------------\r\n");
SerialSendStr(" The 8051 Hyper Terminal,by JJJ\r\n");
SerialSendStr(" http://www.proteus.com.cn \r\n");
SerialSendStr("-----------------------------\r\n");
SerialSendStr("\r\n");
SerialSendStr(&PromptBuffer[0]);
}首先初始化串口,使用定时器1,模式2,自动装载8位,波特率57600,这样快多了,呵呵,使能串口中断,总中断,初始化全局变量,然后设置超级终端,黑底绿字,最经典的配色,清屏,向终端打印一些信息,比如版权啊什么的,尽管没人相信这东西,最后打印提示符,就跟DOS提示符一样的东西。
继续。。
while(1) 好理解,死循环。
里面就一个函数
RunHyperTerminal(); 猜也猜得到,这家伙肯定就是干活的。里面是啥呢,好奇。
CODE:
void RunHyperTerminal(void)
{
if(ExecCommandFlag)
{
ExecCommand(&SerialBuffer[0]);
SerialSendStr(&PromptBuffer[0]);
memset(&SerialBuffer[0],'\0',MAX_SERIAL_BUFFER_SIZE);
CursorPosion = 0;
ExecCommandFlag = 0;
}
}就这么点,感觉失落啊,之前可满是期待,以为长篇大论呢
解释,判断有没有命令要执行,是个全局变量标志,记得在初始化函数里还初始化过。没有就很简单了,什么也不干,有了呢?
ExecCommand(&SerialBuffer[0]);执行命令,怎么执行,等下分析。
SerialSendStr(&PromptBuffer[0]);送提示符,友好嘛。命令执行完备肯定要等待下一次命令输入的。
memset(&SerialBuffer[0],'\0',MAX_SERIAL_BUFFER_SIZE);清空缓冲区,没什么可以解释的,为了下次正确执行必须这样做,对于memset这个函数不知道干啥的兄弟,可要回去好好会会谭浩强了
CursorPosion = 0;
ExecCommandFlag = 0;
这两个也没啥好说的,复位变量。
在进入核心前,我们先看看中断服务程序,这个可不是以main函数为主线,典型的前后台系统,单任务程序的典型处理方法,以后有机会我会给大家介绍另一种处理方法,事情标志加事情驱动的思想,这个思想得益于周立功的USB接口芯片PDIUSBD12驱动源码,(不是打广告,我的公司跟周立功是竞争对手,誓不两立,别人的优点我们还是要让他放光芒的)只不过被我概括出来。
打个岔,源码是最好的老师,要想提高自己的编程水平,必须多读源码,即使你自己很能写,如果你的编程思想不好,写出来的全是垃圾,而编程思想哪里来,读别人的源码自己悟,世界上的任何一个编程天才都是这条路,可能又有人问,哪有那么多源码啊,跟别人要个源码比登天还难,我跟你说,不愿给源码的人编程水平肯定还没你强(涉及核心技术的除外),你也没必要去费神了。可能因为我工作的关系吧,推荐读linux源代码,600万行,各种芯片驱动程序都有,读吧,读完你准是高手中的高手,估计读到这篇文章的都是些初学者,那我建议2年后,你将它提上日程,在这之前有很多中小型代码可以读,vivi,三星ARM的bootloader,u-boot通用bootloader,ucos,ucgui....去开源网站看吧,切记读是读他的编程思想,编程技巧,而不是代码的功能。
串口中断服务程序
CODE:
void SerialInterrupt(void) interrupt 4 using 3
{
char SbufTemp;
if(RI)
{
RI = 0;
SbufTemp = SBUF;
switch(SbufTemp)
{
case 0x08:
case 0x06:
case 0x07:
case 0x7E:
case 0x7F:
if(CursorPosion > 0)
{
CursorPosion--;
SerialSendByte(0x08);
SerialSendByte(' ');
SerialSendByte(0x08);
}
SerialBuffer[CursorPosion] = '\0';
break;
case '\r':
case '\n':
case '\0':
SerialSendByte('\r');
SerialSendByte('\n');
ExecCommandFlag = 1;
break;
case '\t':
break;
default:
if(CursorPosion < MAX_SERIAL_BUFFER_SIZE)
{
SerialBuffer[CursorPosion] = SbufTemp;
SerialSendByte(SbufTemp);
CursorPosion++;
}
else
{
CursorPosion = 0;
memset(&SerialBuffer[0],'\0',MAX_SERIAL_BUFFER_SIZE);
SerialSendStr("\r\n Warnning:Your command string is too long!\r\n\r\n");
SerialSendStr(&PromptBuffer[0]);
}
break;
}
}
}别看这稍长,其实很好理解,如果有串口接收中断标志,代表收到一个字符,清RI标志,缓存至局部变量SbufTemp,判断字符,退格键,删除键,则将缓冲区指针减一,再更新屏幕显示,回车键,代表输入结束,置全局变量标志,有待解析字符,其余的则就是用户输入的有效字符了,注意,这里有个缓冲区溢出判断,很关键。
好了,中断服务程序理解了,即用户输入的字符都到了SerialBuffer这个数组里,那我们接下来的就是解析了
CODE:
void ParseArgs(char *argstr,char *argc_p,char **argv, char **resid)
{
char argc = 0;
char c;
PARSESTATE stackedState,lastState = PS_WHITESPACE;
while ((c = *argstr) != 0)
{
PARSESTATE newState;
if (c == ';' && lastState != PS_STRING && lastState != PS_ESCAPE)
break;
if (lastState == PS_ESCAPE)
{
newState = stackedState;
}
else if (lastState == PS_STRING)
{
if (c == '"')
{
newState = PS_WHITESPACE;
*argstr = 0;
}
else
{
newState = PS_STRING;
}
}
else if ((c == ' ') || (c == '\t'))
{
*argstr = 0;
newState = PS_WHITESPACE;
}
else if (c == '"')
{
newState = PS_STRING;
*argstr++ = 0;
argv[argc++] = argstr;
}
else if (c == '\\')
{
stackedState = lastState;
newState = PS_ESCAPE;
}
else
{
if (lastState == PS_WHITESPACE)
{
argv[argc++] = argstr;
}
newState = PS_TOKEN;
}
lastState = newState;
argstr++;
}
argv[argc] = NULL;
if (argc_p != NULL)
*argc_p = argc;
if (*argstr == ';')
{
*argstr++ = '\0';
}
*resid = argstr;
}这段代码是分析参数,即将用户输入的字符串比如“prompt 8051>>”分析出参数的个数,以及将每个参数分别存在数组里,一个二维数组,这段代码是借鉴VIVI里面的,涉及到编译原理里的一些知识。
CODE:
void ExecCommand(char *buf)
{
char argc,*argv[8],*resid,i;
COMMAND *Command = 0;
while(*buf)
{
memset(argv,0,sizeof(argv));
ParseArgs(buf, &argc, argv, &resid);
if(argc > 0)
{
for(i = 0; i < MAX_COMMAND_NUM; i++)
{
Command = &CommandList[i];
if(strncmp(Command->CommandName,argv[0],strlen(argv[0])) == 0)
break;
else
Command = 0;
}
if(Command == 0)
{
SerialSendStr(" Could not found \"");
SerialSendStr(argv[0]);
SerialSendStr("\" command\r\n");
SerialSendStr(" If you want to konw available commands, type 'help'\r\n\r\n");
}
else
{
Command->CommandFunc(argc,argv);
}
}
buf = resid;
}
}这是执行命令,即根据刚才分析的结果,运行特定功能的函数。这里涉及到一个数据类型
CODE:
typedef struct {
const char *CommandName;
void (*CommandFunc)(char argc, const char **argv);
const char *HelpString;
}COMMAND;第一个命令的名字,第二个命令的执行函数指针,带有两个参数,很像C的main函数吧,这样可以传任何命令参数,第三个简单,帮助字符串
CODE:
COMMAND CommandList[MAX_COMMAND_NUM] = {
{"help",Help," help -- Command help"},
{"prompt",Prompt," prompt <string> -- Change a prompt"},
{"clear",Clear," clear -- Clear screen"},
{"reboot",Reboot," reboot -- Reboot the MCU"}
};定义了四个命令,声明是不是很简单呢。
CODE:
void Help(char argc, const char **argv)
{
char i;
argv = argv;
switch(argc)
{
case 1:
for(i = 0; i < MAX_COMMAND_NUM; i++)
{
SerialSendStr(CommandList[i].HelpString);
SerialSendStr("\r\n");
}
SerialSendStr("\r\n");
break;
default:
SerialSendStr(" Invalid 'help' command: too many arguments\r\n");
SerialSendStr(" Usage:\r\n");
SerialSendStr(" help\r\n");
break;
}
}这是help函数,就是打印帮助信息,好简单,不说了
CODE:
void Prompt(char argc, const char **argv)
{
switch(argc)
{
case 2:
if(strlen(argv[1]) >= MAX_PROMPT_BUFFER_SIZE)
{
SerialSendStr(" Warnning:Your argument is too long!\r\n\r\n");
break;
}
memcpy(PromptBuffer,argv[1],MAX_PROMPT_BUFFER_SIZE);
SerialSendStr(" Prompt is chagned to \"");
sprintf(&SerialBuffer[0],"%s\"\r\n\r\n",&PromptBuffer[0]);
SerialSendStr(&SerialBuffer[0]);
break;
default:
SerialSendStr(" Invalid 'prompt' command: too few or many arguments\r\n");
SerialSendStr(" Usage:\r\n");
SerialSendStr(" prompt <string>\r\n");
break;
}
}改变提示符,比较人性化啊,即将要设置的提示符丢进PromptBuffer就OK了
CODE:
void Clear(char argc, const char **argv)
{
argv = argv;
switch(argc)
{
case 1:
SerialSendStr(CLEARSCREEN);
break;
default:
SerialSendStr(" Invalid 'clear' command: too many arguments\r\n");
SerialSendStr(" Usage:\r\n");
SerialSendStr(" clear\r\n");
break;
}
}清屏指令,清除超级终端上的显示,VT100的功能,SerialSendStr(CLEARSCREEN);
CODE:
void Reboot(char argc, const char **argv)
{
argv = argv;
switch(argc)
{
case 1:
(*(void(*)())0)();
break;
default:
SerialSendStr(" Invalid 'reboot' command: too many arguments\r\n");
SerialSendStr(" Usage:\r\n");
SerialSendStr(" reboot\r\n");
break;
}
}看名字意思,就知道,让单片机重启,热启动,不是硬复位,看看代码
(*(void(*)())0)(); 好晦涩啊,可能你都怀疑这能编译通过吗?事实证明,这不但能编译通过,而且还能让单片机重起。
好,我们看看,这段代码是怎么来的?
我们都知道CPU一上电就从0地址开始运行, 而一复位PC指针就指向0地址,如果我们能将PC指针设置为0,既从0地址运行,不就可以复位了吗?
用C语言去控制PC指针可没那么容易,LJMP它可没有,怎么办呢?
复习一个概念,函数,实际是一个地址,一个函数执行体的入口地址,函数指针,是指向某个函数地址的指针变量。如果函数执行体的地址是0,那么肯定就从那儿开始运行了。
看个普通的函数
void test(void)
{
}
怎么调用这个函数呢,大家都知道,这样就可以调用了
test();
没错,完全正确,但大家可能忽略了一点,这只是个简化的写法,完整的写法应该是
(*test)();
大迭眼镜了吧,老师可从来没叫我这样调用函数啊,原因很简单,因为老师也不知道,这样也很容易理解,*和&的区别想必大家都知道的吧。一个是取值,一个是取地址,test是什么呢,见函数的概念,他是一个地址,用*来取这个地址的值来运行就是执行函数了。理解了吧,不过,听到这以后,你可千万别每调用一个函数都这样干啊,人家以为你有病呢。
看到这里,大家肯定都想,要是test的地址是0,不就能完全满足我们的要求了吗?
好我们看看
(*0)();变成这样了,似乎发现编译出错,为啥出错呢,原来这里的0,编译器认为是一个值,而不是一个函数的地址,好,你不是,我把你变成是,怎么变,翻箱倒柜的想吧。
再复习一个概念,函数的指针的定义。
void (*p)(void);
定义一个函数指针p,没有参数传入,也没有返回。
这里的p是什么东西,一个名字,标识符,好我们现在来变魔术了,强制转换!!
将0变为一个函数的地址,这样写
void(*p)(void)0 这个表达式就是一个函数的地址呢,而且这个地址是0,这里的p没有任何用途,只是为了理解,我们省去,传入参数void也可以省去,则变为
void(*)()0
好,我们将它代如到一开始推出表达式则变成
(*void(*)()0)();看到了吗?就是上面的复位语句,是不是对函数,及函数指针有了更深刻的认识呢。
好进阶一下,复位我们实现了,跳转到其他地址执行,也很简单,将0改为其他值就行了,我们现在想用C语言来执行机器码,这一说,可能吓坏了好多人,这也能干?别打鼓,跟我来,给代码。
unsigned char code MyMechineCode[]={0x00,0x21,0x33,0x56};
(*(void(*)())(&MyMechineCode[0]))();
先看五秒钟,看明白的可能会心一笑了,要解释么,不要了吧,提示MyMechineCode里放的就是机器码,一直执行下去的哦,呵呵
这个东西,还有一个高论,总线病毒,想想,直接运行内存中的机器码多恐怖啊,遐想吧。。。需要统一编址哦,51是没这福分。
这些东西可不是我想出来的哦,参考《C语言陷阱与缺陷》,喜欢的话看看原著吧,那才叫原汁原味。
补充一点,这些代码只能在主程序里运行,中断里是不能运行的哦,原因是中断必须返回。
写到这里感觉已经很详细了,自己也好累了,希望大家都能从中学到东西,欢迎转载,注明来源http://www.protues.com.cn即可
[本帖最后由 jjj 于 2007-1-3 16:11 编辑]
超级终端.rar
(2007-01-03 15:51:48, Size: 89.3 kB, Downloads: 52)

最新评论
删除 引用 lhaz (2008-12-20 16:29:18, 评分: 0 )
删除 引用 donglinpeng (2008-5-30 09:47:05, 评分: 0 )
删除 引用 yuzhen (2008-5-22 15:35:28, 评分: 0 )
MEI QIAN A AAAAAAAAAAAAAAAAAAA