推荐给好友 上一篇 | 下一篇

基于8051单片机的超级终端仿真,新年最新奉献


    超级终端,其实就一个输入输出设备,搞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)



查看全部3条评论

最新评论

  • 删除 引用 lhaz (2008-12-20 16:29:18, 评分: 0 )

    广告歌南京大同人的突然大雨后急不可耐你健康良好
  • 删除 引用 donglinpeng (2008-5-30 09:47:05, 评分: 0 )

    ggggggggggggggggggg
  • 删除 引用 yuzhen (2008-5-22 15:35:28, 评分: 0 )

    MEI QIAN A AAAAAAAAAAAAAAAAAAA
    MEI  QIAN  A   AAAAAAAAAAAAAAAAAAA
 

评分:0

我来说两句

seccode