2009年3月28日星期六

嵌入式系统上的简易printf

嵌入式中,调试手段通常有两种,一是远程gdb,一是直接printf。如果是调试自己玩的小板子,用gdb有点大张旗鼓了,大多数情况下printf就可以搞定。不过printf的问题是stdio
lib的size太大,稍微有点程序,加上几个常用的库,比如stdio和string,超过16k甚至32k(已经大于一些低端芯片的flash容量了)是很正常的事情,而且通常比较慢,程序越多,越麻烦。道理很简单,标准C语言库的规范中,Printf()
必须处理大量的数据格式,包括字符串、字符、(各种长度的有符号和无符号)数字,以及浮点值。而且格式字符串还要包括用于更改文本对齐、基数、间距、字段宽度和精度的调节器和指示器。符合这个规范的代码必然会是冗长和繁重的。一些嵌入式系统库倒是提供了一些之针对整数的printf,但还是有问题,首先是还是太大,其次是你没有自己的调整权限。

其实printf也就是IO的调用包装而已,我们完全可以自己写一个简易版本的printf满足自己的需要,并随时根据需要裁剪。具体来说,printf在这里要起的作用就是将调试字符串从嵌入式目标空闲的串口压出,并在运行于宿主工作站的终端模拟器上显示结果。下面就简单介绍一下,如何来自己写一个简易printf函数。

要写printf,首先要知道什么是可变参数传递,我们来看看标准库里面,是如何定义可变参数实现的:


#define _AUPBND (sizeof (acpi_native_int) - 1)
#define _ADNBND (sizeof (acpi_native_int) - 1)

#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))
#define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T,_AUPBND))) - (_bnd
(T,_ADNBND))))
#define va_end(ap) (void) 0
#define va_start(ap, A) (void) ((ap) = (((char *) &(A))
+(_bnd(A,_AUPBND))))

关于可变参数的原理,网上有一些文章,总结来说,就是我们可以通过Intel80×86机器的对齐特性来获得所有的参数,因为在Intel80×86机器上,每个变量的地址都要是sizeof(int)的倍数,这样能提升CPU运行的效率。也就是说,所有参数的首地址都要是4的倍数,就算你是char型的,那浪费3个byte也要安排你占第四个坑。

好,由于C语言传递参数时是用push指令从右到左将参数逐个压栈,因此我们通过栈指针跳4n格来访问第n个参数,不要忘了,参数的地址都是字对齐的。这里,我们用#define
_bnd(X, bnd) (((sizeof (X)) + (bnd)) &
(~(bnd)))来计算类型为X的参数在栈中占据的字对齐后的字节数。bnd是sizeof
(acpi_native_int) - 1,acpi_native_unit在32位机的定义是:

typedef u32 acpi_native_uint;

所以( ~(bnd))就是0xfffffffc 。 因此,_bnd(X,bnd) 宏在32位机下就是

( (sizeof(X) + 3)&0xfffffffc )

很明显,其作用是–倘若sizeof(X)不是4的整数倍,将其变为4的整数倍。

va_start(ap,A)
负责初始化参数指针ap,将函数参数A右边第一个参数的地址赋给ap,这个第一个参数通常就是printf里面的"%x%d%f%d"。

va_arg(ap,T)
可以获得ap指向参数的值,并使ap指向下一个参数,T用来指明当前参数类型。

在这里,上述代码还是麻烦,而且sizeof我们也不能直接用,所以我们不如干脆直接写一个不那么麻烦而有针对性的可变参数操作定义:

#define sizeof(x) ((char *)(&x+1) - (char *)(&x))
#define va_start(ap,v) ( ap = (char *)&v + sizeof(v) )
#define va_arg(ap,t) ( *(t *)((ap += sizeof(t)) - sizeof(t)) )
#define va_end(ap) ( ap = (char *)0 )

有了这几个定义,print函数就好写了,为了节省空间,这个简单的print()只支持"%s","%d"和"%c"格式的分类符,暂时不需要其他功能,比如格式对齐之类的,当然,可以根据自己的需要扩展这个函数。

int print( const char *fmt, ... )
{
const char *s;
char c;
int d;
va_list ap;
va_start(ap, fmt);
while( *fmt != '\0' )
{
if( *fmt != '%' )
{
uart_putc(*fmt++);
continue;
}
switch(*++fmt)
{
case 's':
{
s = va_arg(ap, const char *);
uart_puts(s);
break;
}
case 'd':
{
d = va_arg(ap, int);
uart_putints(d, 10);
break;
}
case 'c':
{
c = va_arg(ap, char);
uart_putc(c);
break;
}
default:
uart_putc(*fmt);
}
fmt++;
}
va_end(ap);
return 1;
}

这里面有一些函数,uart_putc是串口驱动程序,给串口送东西的,uart_puts是简单的多重putc包装。uart_putints则需要做一些atoi的转换,一个比较简单但是有效的atoi程序宏定义如下:

#define ATOI(X, result) \
do{ \
char *lptr = X; \
result = 0; \
while (1) \
{ \
if ((*lptr >= '0') && (*lptr <= '9')) \
{ \
result *= 10; \
result += *lptr - '0'; \
lptr++; \
} \
else \
{ \
break; \
} \
} \
}while(0)

没有评论: