这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战
李迟注: 这几篇文章写于2012年底,因故未发表,前不久,音视频群里的树哥询问一个技术方案,想到以前曾经实现过,就把工程发给他。现在发表出来,除修正个别严重的病句外,其它没有修改。从行文看,还有很大提高水平,但文中的编码风格却一直保持着。
本系列文章将完成一个类似DOS或Linux或busybox或u-boot的命令终端。题目的“命令终端”之所以加引号,一来表示它不是真正意义上的终端,二来也可以说明并非自己一字一字写出来的代码。——本程序所用的原型来自u-boot2010.09,这个版本陪了我很久,使我一直不能忘怀。如今重拾代码,也了却心头所念。 所谓工欲善其事,必先利其器,本文便是该工程的前期准备。包括如下内容:检测按键,接收终端字符,将字符发送到终端,printf函数的实现,等等。
以下分别给出与“终端”交互的各个函数。由于u-boot面向的是串口终端,而自己的实现的程序是操作系统中的终端。所以下面先介绍了u-boot中的实现(以SMDK2440为例),再介绍自己的实现。
一、检测按键
u-boot 的实现如下(注:已作了修改,下同):
int tstc(void)
{
return serial_tstc();
}
int serial_tstc()
{
struct s3c24x0_uart *uart = s3c24x0_get_base_uart(dev_index);
return readl(&uart->UTRSTAT) & 0x1;
}
复制代码
可以看到,这个实现最终是读取串口的状态寄存的某个位。具体可以查看芯片手册。 自己的实现如下:
/* 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实现如下:
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库中倒是有一个,但这里没必要使用这个库,网上有相应的实现函数,这里按“拿来主义”,为保其完整性,连注释也不修改。如下:
/* 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的实现如下:
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');
}
复制代码
上面提到的是发送单个字符,发送字符串就循环调用该函数,如下:
void serial_puts(const char * s)
{
while (*s) {
serial_putc(*s++);
}
}
复制代码
标准C库里面有putchar函数,直接使用之,如下:
void myputc(const char c)
{
if (c == '\n')
myputc('\r');
putchar(c);
}
void myputs(const char *s)
{
while (*s)
myputc(*s++);
}
复制代码
四、printf实现
printf的实现主要调用vsprintf,该函数实现如下:
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的代码,将这一套“终端”应用于某芯片平台上的。