1. 程式人生 > >【OK6410裸機程式】移植printf和scanf

【OK6410裸機程式】移植printf和scanf

在學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 [email protected]
	
%.o : %.S
	arm-linux-gcc ${CFLAGS} -c $^ -o [email protected]

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是把編譯好的檔案連線成一個有機整體,把單獨編譯的檔案內使用的相互關聯的變數,函式等地址放入相應位置.

頂層目錄的makefile
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 [email protected]  $^

%.o : %.S
	arm-linux-gcc  -c $< -o [email protected]
	
%.o : %.c
	arm-linux-gcc  $(CFLAGS) -c $< -o [email protected]

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命令,返回的值當前目錄的絕對目錄