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

李迟注:
这几篇文章写于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的代码,将这一套“终端”应用于某芯片平台上的。

参考资料:

http://blog.csdn.net/summerhust/article/details/6615785
http://www.jbox.dk/sanos/source/lib/vsprintf.c.html

李迟 2020.9.30

©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页