李迟注: 这几篇文章写于2012年底,因故未发表,前不久,音视频群里的树哥询问一个技术方案,想到以前曾经实现过,就把工程发给他。现在发表出来,除修正个别严重的病句外,其它没有修改。从行文看,还有很大提高水平,但文中的编码风格却一直保持着。
本系列文章将完成一个类似DOS或Linux或busybox或u-boot的命令终端。题目的“命令终端”之所以加引号,一来表示它不是真正意义上的终端,二来也可以说明并非自己一字一字写出来的代码。——本程序所用的原型来自u-boot2010.09,这个版本陪了我很久,使我一直不能忘怀。如今重拾代码,也了却心头所念。 所谓工欲善其事,必先利其器,本文便是该工程的前期准备。包括如下内容:检测按键,接收终端字符,将字符发送到终端,printf函数的实现,等等。
以下分别给出与“终端”交互的各个函数。由于u-boot面向的是串口终端,而自己的实现的程序是操作系统中的终端。所以下面先介绍了u-boot中的实现(以SMDK2440为例),再介绍自己的实现。
一、检测按键 u-boot 的实现如下(注:已作了修改,下同):
1 2 3 4 5 6 7 8 9 10 11 int tstc(void) { return serial_tstc(); } int serial_tstc() { struct s3c24x0_uart *uart = s3c24x0_get_base_uart(dev_index); return readl(&uart->UTRSTAT) & 0x1; }
可以看到,这个实现最终是读取串口的状态寄存的某个位。具体可以查看芯片手册。 自己的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 /* implement of getch() */ #ifdef WIN32 #include <conio.h> /** * return non-zero if a key pressed, zero if not. * */ int mytstc(void) { return kbhit(); } #else #include <termios.h> /* for tcxxxattr, ECHO, etc */ #include <unistd.h> /* for STDIN_FILENO */ #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> int mytstc(void) { struct timeval tv; fd_set rdfs; int ch; struct termios oldt, newt; // get terminal input's attribute tcgetattr(STDIN_FILENO, &oldt); newt = oldt; //set termios' local mode newt.c_lflag &= ~(ECHO|ICANON); tcsetattr(STDIN_FILENO, TCSANOW, &newt); tv.tv_sec = 0; tv.tv_usec = 100; FD_ZERO(&rdfs); FD_SET (STDIN_FILENO, &rdfs); select(STDIN_FILENO+1, &rdfs, NULL, NULL, &tv); ch = FD_ISSET(STDIN_FILENO, &rdfs); //recover terminal's attribute tcsetattr(STDIN_FILENO, TCSANOW, &oldt); return ch; }
二、接收终端字符 u-boot实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 int getc(void) { return serial_getc(); } int serial_getc() { struct s3c24x0_uart *uart = s3c24x0_get_base_uart(dev_index); while (!(readl(&uart->UTRSTAT) & 0x1)) /* wait for character to arrive */ ; return readb(&uart->URXH) & 0xff; }
原理很简单,就是判断接收缓冲区是否有数据,没有的话就一直等,否则就返回数据接收寄存中的值。 在Windows中有getch可用,但Linux无此函数,在curses库中倒是有一个,但这里没必要使用这个库,网上有相应的实现函数,这里按“拿来主义”,为保其完整性,连注释也不修改。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 /* implement of getch() */ #ifdef WIN32 #include <conio.h> int mygetc(void) { return getch(); } #else #include <termios.h> /* for tcxxxattr, ECHO, etc */ #include <unistd.h> /* for STDIN_FILENO */ /*simulate windows' getch(), it works!!*/ int mygetc(void) { int ch; struct termios oldt, newt; // get terminal input's attribute tcgetattr(STDIN_FILENO, &oldt); newt = oldt; //set termios' local mode newt.c_lflag &= ~(ECHO|ICANON); tcsetattr(STDIN_FILENO, TCSANOW, &newt); //read character from terminal input ch = getchar(); //recover terminal's attribute tcsetattr(STDIN_FILENO, TCSANOW, &oldt); return ch; } #endif
三、发送字符到终端 u-boot的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void putc(const char c) { serial_putc(c); } void serial_putc(const char c, const int dev_index) { struct s3c24x0_uart *uart = s3c24x0_get_base_uart(dev_index); while (!(readl(&uart->UTRSTAT) & 0x2)) /* wait for room in the tx FIFO */ ; writeb(c, &uart->UTXH); /* If \n, also do \r */ if (c == '\n') serial_putc('\r'); }
上面提到的是发送单个字符,发送字符串就循环调用该函数,如下:
1 2 3 4 5 6 void serial_puts(const char * s) { while (*s) { serial_putc(*s++); } }
标准C库里面有putchar函数,直接使用之,如下:
1 2 3 4 5 6 7 8 9 10 11 12 void myputc(const char c) { if (c == '\n') myputc('\r'); putchar(c); } void myputs(const char *s) { while (*s) myputc(*s++); }
四、printf实现 printf的实现主要调用vsprintf,该函数实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 int myprintf(const char *fmt, ...) { va_list args; int i; char printbuffer[512]; va_start(args, fmt); /* For this to work, printbuffer must be larger than * anything we ever want to print. */ i = myvsprintf(printbuffer, fmt, args); va_end(args); /* Print the string */ myputs(printbuffer); return i; } /** 简单版本 仅支持:%d %x %s %c */ int myvsprintf1(char *buf, const char *fmt, va_list args) { char* s = NULL; char* p = NULL; int d = 0; int len = 0; int i = 0; s = buf; for (; *fmt; ++fmt) { if (*fmt != '%') { // string within there *s++ = *fmt; continue; } ++fmt; // no flags // no field width // no precision // no length // specifier switch (*fmt) { case 's': p = va_arg(args, char*); break; case 'x': d = va_arg(args, int); p = myitoa(d, 16); while (*p) *s++ = *p++; break; case 'd': d = va_arg(args, int); p = myitoa(d, 10); while (*p) *s++ = *p++; break; case 'c': d = va_arg(args, int); // cannot be 'char' *s++ = d; break; default: *s++ = *fmt; break; } } *s = '\0'; return (int)(s - buf); }
其中myitoa函数是itoa的实现,可参考笔者前段时间写的文章。vsprintf函数如其注释所示,支持的格式化十分有限。当然,网上已经有人实现了功能十分强大的vsprintf,可参阅文后链接。
注: 关于串口的操作,大部分芯片原理是相似的,具体到某款芯片,只要按照其数据手册中寄存器说明来编写代码就OK了。笔者就是参考u-boot的代码,将这一套“终端”应用于某芯片平台上的。
参考资料: http://blog.csdn.net/summerhust/article/details/6615785 http://www.jbox.dk/sanos/source/lib/vsprintf.c.html
李迟 2020.9.30