“命令终端”的实现1-准备篇

李迟注:
这几篇文章写于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