参考:1.通过串口实现printf和scanf函数 2.【OK6410裸机程序】串口初始化
1. 源码printf.rar
在学c语言的时候,经典的hello world程序,是通过printf函数实现了。有了这个函数,就可以随意的向屏幕打印数据了。在嵌入式中,其实也是可以用printf函数的,不过需要稍微麻烦点的移植。毕竟,在嵌入式中,所有实现的都要自己来弄,不在向PC程序开发一样,很多库函数,操作系统已经搞好,就用就行了。首先,是要去下载能实现printf的源代码。这里用的是国嵌提供的。有两个文件夹,一个include,里面一些头文件,另外一个lib,实现printf的需要的额外的程序。
2. 浅析va宏
typedef char * va_list;
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
#define va_start(ap,v) (ap = (va_list)&v + _INTSIZEOF(v))
#define va_arg(ap,t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
#define va_end(ap) (ap = (va_list)0)
(1) _INTSIZEOF(n)宏的作用
这里涉及到内存对齐(alignment)问题,内存对齐跟具体使用的硬件平台有密切关系,比如大家熟知的32位x86平台规定所有的变量地址必须是4的倍数(sizeof(int) = 4)。va机制中用宏_INTSIZEOF(n)来解决这个问题,没有这些宏,va的可移植性无从谈起。
具体来说,如果sizeof(n)是sizeof(int)的倍数,则保持不变,否则返回最小的且大于sizeof(n)的sizeof(int)的倍数。比如sizeof(int)是4,那么sizeof(n)是1-4的时候返回4,5-8的时候返回8,以此类推。首先sizeof(int)写成2进制是1后面若干个0,假设是n个0。sizeof(int) - 1 就是n个1,取非再和前面的数取与就是清除掉前面那个数的后面n比特。如果sizeof(n) 后面n比特都是0,那么加sizeof(int)-1不进位,相当于加上再清除掉。如果sizeof(n) 后面n比特至少有1位是1,那么加了之后会往前进1,再与掉末尾的n位,相当于sizeof(int)右移1位。
(2)va_start宏的作用 :
v是第一个参数,通过前面我们知道,第一个参数就是用来表明有几个参数,它不是我们实际需要的参数。我们通过它来计算出,第一个实际参数的地址,注意是实际参数,可不是第一个表明参数个数的参数地址,让ap指针变量保存。
(3)va_arg宏的作用:
通过va_start,我们的ap的指针已经指向了第一个实际参数。va_arg两个作用:返回ap指针当前指向的数据,更新ap指针指向下一个参数。可以看到的是ap指针先更新了,然后又减了一个值,最终把这个值返回。这里面的t代表即将获得参数的类型。可以看出,通过va_arg宏我们获得每个实际参数的值。
实际上,格式化的转换有现成的函数可以调用,例如:vsprintf()和vsscanf()内部都是调用va_start实现的。
(4)va_end宏的作用
va_end很简单,仅仅是把指针作废而已
3.程序架构
顶层目录:
dev目录:
lib目录:
include目录:
4.移植工作
4.1 添加printf.c文件。inlucde目录不需要修改,lib目录下添加printf.c文件
#include "vsprintf.h"
#include "string.h"
#include "printf.h"
extern void putchar(unsigned char c);
extern unsigned char getchar(void);
#define OUTBUFSIZE 1024
#define INBUFSIZE 1024
static unsigned char g_pcOutBuf[OUTBUFSIZE];
static unsigned char g_pcInBuf[INBUFSIZE];
int printf(const char *fmt, ...)
{
int i;
int len;
va_list args;
va_start(args, fmt);
len = vsprintf(g_pcOutBuf,fmt,args);
va_end(args);
for (i = 0; i < strlen(g_pcOutBuf); i++)
{
putchar(g_pcOutBuf[i]);
}
return len;
}
int scanf(const char * fmt, ...)
{
int i = 0;
unsigned char c;
va_list args;
while(1)
{
c = getchar();
//putchar(c);
if((c == 0x0d) || (c == 0x0a))
{
g_pcInBuf[i] = '\0';
break;
}
else
{
g_pcInBuf[i++] = c;
}
}
va_start(args,fmt);
i = vsscanf(g_pcInBuf,fmt,args);
va_end(args);
return i;
}
4.2 Makefile修改。
dev目录下的Makefile
objs := clock.o uart.o
all : $(objs)
arm-linux-ld -r -o dev.o $^
%.o : %.c
arm-linux-gcc ${CFLAGS} -c $^ -o $@
%.o : %.S
arm-linux-gcc ${CFLAGS} -c $^ -o $@
clean:
rm -f *.o *.bak
lib目录下的Makefile
objs := div64.o lib1funcs.o ctype.o muldi3.o printf.o string.o vsprintf.o
all : $(objs)
arm-linux-ld -r -o lib.o $^
%.o : %.c
arm-linux-gcc ${CFLAGS} -c $^
%.o : %.S
arm-linux-gcc ${CFLAGS} -c $^
clean:
rm -f *.o
在底层Makefile中,将各个外设的代码给编译链接成xxx.o文件,供外部使用。这里是用ld链接起来的,有的是用ar -r 链接起来的。两种方法的区别如下:
ar只是把你的多个文件归档成一个文件,不检查文件之间的相互关系.
而ld是把编译好的文件连接成一个有机整体,把单独编译的文件内使用的相互关联的变量,函数等地址放入相应位置.
objs := start.o main.o dev/dev.o lib/lib.o
#CPPFLAGS := -nostdinc -Wall
CFLAGS := -fno-builtin -I$(shell pwd)/include -I$(shell pwd)/dev -Os -W
export CFLAGS
all: uart.elf
arm-linux-objcopy -O binary -S $^ uart.bin
arm-linux-objdump -D uart.elf > uart.dis
uart.elf:$(objs)
arm-linux-ld -Tstart.lds -o $@ $^
%.o : %.S
arm-linux-gcc -c $< -o $@
%.o : %.c
arm-linux-gcc $(CFLAGS) -c $< -o $@
dev/dev.o:
make -C dev all
lib/lib.o:
make -C lib all
.PHONY: clean
clean:
-rm *.o *.elf *.bin *.dis *.bak
make -C lib clean
make -C dev clean
CFLAGS 这个是顶层Makefile定义的变量,定义的参数是编译选项,通过export传递给底层的Makefile.
-fno-builtin是说函数不使用内建的函数,当我们写的函数和编译器的内建函数的名字是一样的时候使用我们自己定义的函数。
-I指的是搜索的头文件的目录,这里指定include目下和dev目录下。因为在实现printf的时候,有调用include中的头文件,所以需要告诉编译器这些头文件在什么地方。在main.c中有包含uart.h和clock.h头文件。
$(shell pwd)这个是shell中的一些用法,调用pwd命令,返回的值当前目录的绝对目录。
5. 工程源码 uart_stdio.rar