S3C2440—6.串口的printf實現

一.框架

在之前STM32的學習中,我在串口輸出調試信息的時候,經常採用printf()函數作爲串口輸出函數,這樣不僅方便調試而且代碼易讀。

在S3C2440的學習中,對於UART同樣需要對串口輸出信息進行調試,那麼在這裏可不可以使用printf函數呢?

當然是可以的,不過相比於STM32中簡單的配置,S3C2440中對printf的使用要深入到printf函數的原理,將printf函數進行徹頭徹尾的刨析,然後結合底層的UART輸出,使用printf打印調試信息,其實現的前提是:

  • UART底層的putchar函數
  • 瞭解printf函數中,固定參數和可變參數
  • 會利用固定參數以及格式字符推出可變參數
  • 根據不同的格式字符輸出可變參數

其中,UART底層的putchar函數在上一篇博客中以及實現了,這裏主要介紹對printf函數的刨析,以及對可變參數的推導。

對printf函數的刨析,由my_printf.c實現,由my_printf.c調用UART底層的putchar函數即可實現UART的printf輸出。

二.printf函數原理

2.1 printf的聲明

在標準庫<stdio.h>中定義了printf函數,其聲明如下:

*int printf(const char format, …);

在這裏插入圖片描述

2.2 參數解讀

可以看出printf的參數爲:(const char *format, …)

其中:format代表固定參數,…代表可變參數,固定參數中含有格式字符

(聽起來有點懵逼,下面解讀一下參數的內容)

顧名思義,固定參數就是一串固定的字符串, *format就是字符串的首地址,根據首地址我們可以將字符串的全部內容輸出。欸,可是固定參數中也有特殊的字符啊,那就是格式字符,格式字符用來說明可變參數的數據類型和個數,根據固定參數中的格式字符,可以將可變參數按照格式打印出來。**所以利用printf打印信息的主要步驟就是解析固定參數以及固定參數中的格式字符,將可變參數在格式字符位置輸出,直到固定參數解析完畢!**這樣就可以將參數中像表達的內容完整地打印出來。

格式字符:

在這裏插入圖片描述

例如:

printf("I'm %s, my ID is %d ,my score is %.2f !!!\n","Bob",25,98.2);

運行結果如下:

在這裏插入圖片描述

printf是從固定參數”I’m %s, my ID is %d ,my score is %.2f !!!\n”的首地址開始解析的,逐個輸出字符,直到第一個格式字符%s,對應了後面可變參數的“Bob”字符串,在格式字符的位置上打印可變參數,然後繼續解析固定參數繼續打印字符,直到第二個格式字符%d,在%d的位置打印可變參數25,然後繼續解析直到第三個格式字符%.2f,根據格式字符打印出98.20,然後繼續解析,直到最後一個轉義字符“\n”,結束了固定參數的解析,也完成了數據的打印輸出。

沒遇到%就直接輸出,遇到%根據格式符前導碼分情況處理。

2.3 如何得到可變參數的值

我們瞭解了參數組成,以及printf打印輸出的流程,那麼問題來了,固定參數的首地址是直接傳進來的,那可變參數的地址怎麼得到呢???

實際上,參數都存儲在棧上的,而且固定參數和可變參數的地址是連續的,對!!!因爲是連續的,所以我們就可以通過指針的移動和取值,由固定參數得到可變參數的地址,從而打印出可變參數。

下面有一段代碼來解釋獲取可變參數的過程:

(參考自百問科技代碼)

 
#include <stdio.h>

struct  person{
	char *name;
	int  age;
	char score;
	int  id;
};
/* 
 *int printf(const char *format, ...); 
 *依據:x86平臺,函數調用時參數傳遞是使用堆棧來實現的 
 *目的:將所有傳入的參數全部打印出來 
 */ 
int push_test(const char *format, ...)
{
	char *p = (char *)&format;
	int i;
	struct  person per;  
	char c;
	double d;
	
	printf("arg1 : %s\n",format);	 

	p = p + sizeof(char *);
	
	/*指針對連續空間操作時: 1) 取值  2)移動指針*/  
	i = *((int *)p);
	p = p + sizeof(int); 	
	printf("arg2 : %d\n",i);   
        
	/*指針對連續空間操作時: 1) 取值  2)移動指針*/    
 	per = *((struct  person *)p); 
	p = p + sizeof(struct person);	
	printf("arg3: .name = %s, .age = %d, .socre=%c  .id=%d\n",\
		          per.name,   per.age,   per.score, per.id);   

	/*指針對連續空間操作時: 1) 取值  2)移動指針*/
	c = *((char *)p);
	p = p + ((sizeof(char) + 3) & ~3);	
	printf("arg4: %c\n",c);
   
	/*指針對連續空間操作時: 1) 取值  2)移動指針*/
	d = *((double *)p);
	p = p + sizeof(double);
	
	printf("arg5: %f\n",d);
	
	return 0;
}

int main(void)
{

    push_test("abcd",123,per,'c',2.79); 	
	 		
	return 0;
}	



這是已知可變參數情況下的輸出,目的就是理解如何通過固定參數得到可變參數,通過棧空間的移動取值就可以!

2.4 解決變參的宏定義

在stdarg.h中,有解決變參問題的一些宏定義,改進後作註釋如下:

/* 參數指針 */ 
typedef char *  va_list;

/* 考慮到地址對齊,對齊大小爲sizeof(int) */
/* 比如,如果sizeof(n)在1-4之間,那麼_INTSIZEOF(n)=4;如果sizeof(n)在5-8之間,那麼 _INTSIZEOF(n)=8。 */ 
/* & ~(sizeof(int) - 1)相當於將0~3掩蓋掉   sizeof(int) - 1 相當於增加0~3  如此,就只取決於sizeof(n)的大小 */ 
#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

/* 得到第一個可變參數的起始地址,傳給ap */
#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )

/* 移動指針到下一個參數,取值 */
#define va_arg(ap,t)    (*(t *)(ap = ap + _INTSIZEOF(t), ap - _INTSIZEOF(t)))

/* 結束,防止ap成爲野指針 */
#define va_end(ap)      ( ap = (va_list)0 )	

在這裏插入圖片描述

2.5 完成printf函數的封裝

解決了變參問題後,剩下的就是對printf函數的封裝了,封裝引出的接口就是:outc()輸出一個字符、outs()輸出字符串,到時候只要將這倆個接口和UART的putchar()函數、puts()函數對應起來,就可以實現UART使用printf了。

printf輸出打印的核心部分:輸出固定參數及可變參數的函數

/*reference :   int vprintf(const char *format, va_list ap); */
/* 輸出固定參數及可變參數的函數 */
static int my_vprintf(const char *fmt, va_list ap) 
{
	char lead=' ';
	int  maxwidth=0;
	
	 for(; *fmt != '\0'; fmt++)
	 {
		if (*fmt != '%') 
		{
			outc(*fmt);  //outc()就是輸出一個字符
			continue;
		}
			
		//format : %08d, %8d,%d,%u,%x,%f,%c,%s 
	    fmt++;
		if(*fmt == '0')
		{
			lead = '0';
			fmt++;	
		}

		lead=' ';
		maxwidth=0;
		
		while(*fmt >= '0' && *fmt <= '9')
		{
			maxwidth *=10;
			maxwidth += (*fmt - '0');
			fmt++;
		}
		
			switch (*fmt)
			{
			case 'd': out_num(va_arg(ap, int),          10,lead,maxwidth); break;
			case 'o': out_num(va_arg(ap, unsigned int),  8,lead,maxwidth); break;				
			case 'u': out_num(va_arg(ap, unsigned int), 10,lead,maxwidth); break;
			case 'x': out_num(va_arg(ap, unsigned int), 16,lead,maxwidth); break;
			case 'c': outc(va_arg(ap, int   )); break;		
			case 's': outs(va_arg(ap, char *)); break;		  		
				
			default:  
				outc(*fmt);
				break;
			}
	}
	return 0;
}

裏面的out_num()函數是根據格式字符輸出不同形式的數字,其函數定義如下:

static int out_num(long n, int base,char lead,int maxwidth) 
{
	unsigned long m=0;
	char buf[MAX_NUMBER_BYTES], *s = buf + sizeof(buf);
	int count=0,i=0;	

	*--s = '\0';
	
	if (n < 0)
		m = -n;
	else
		m = n;
	
	do
    {
		*--s = hex_tab[m%base];
		count++;
	}
    while ((m /= base) != 0);
	
	if( maxwidth && count < maxwidth)
    {
		for (i=maxwidth - count; i; i--)	
			*--s = lead;
    }

	if (n < 0)
		*--s = '-';
	
	return outs(s);
}

所以,printf函數的封裝就如下:

int printf(const char *fmt, ...) 
{	
    /* 參數指針 */
	va_list ap;
	/* 有固定參數得到起始地址 */
	va_start(ap, fmt);
    /* 輸出打印 */
	my_vprintf(fmt, ap);
    /* 結束 */
	va_end(ap);
	return 0;
}

三.結合UART實現

上面我們知道,對printf函數的刨析,由my_printf.c實現,由my_printf.c調用UART底層的putchar函數即可實現UART的printf輸出,調用接口就是outc()、outs()函數,只要如下設置就ok:

static int outc(int c) 
{
	putchar(c);
	return 0;
}

static int outs (const char *s)
{
	while (*s != '\0')	
		putchar(*s++);
	return 0;
}

對UART的配置已經在之前的博客中有詳細介紹了。

有問題可以q我,一起學習:2723808286

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章