ARM如果能使用C函数库自带的printf函数格式输出,那多方便,但是默认的printf都是定位到stdout终端,而不是串口,本文章讲述的是如何定位到ARM的串口。
1.1.1 函数主要代码
有在Mini2440开发板上验证过
//*****************main.c*******************************
#include"serial.h"
int Main()
{
unsignedint plck_val = 50000000;
unsignedint buad_val = 115200;
unsignedint ch_val = 0;
char*string="Hello,http://blog.csdn.net/wfq0624";
uart_init();
uart_printf("\n\r%s\n",string);
uart_printf("\rUse uart0\n\rParameter:PCLK is %d,buad is %d,uart_port is %d \n",plck_val,buad_val,ch_val);
uart_printf("\r该函数不能打印浮点数!");
return 0;
}
//***********************serial.c**************************
#include"s3c2440.h"
#include"serial.h"
#include <stdarg.h> //需要包含此stdarg.h头文件
#include <stdio.h> //需要包涵此stdio.h头文件
#define TXD0READY (1<<2)
#defineRXD0READY (1)
#definePCLK 50000000 // init.c中的clock_init函数设置PCLK为50MHz
#defineUART_CLK PCLK // UART0的时钟源设为PCLK
#defineUART_BAUD_RATE 115200 // 波特率
#defineUART_BRD ((UART_CLK / (UART_BAUD_RATE * 16)) - 1)
voiduart_init()
{ //UART初始化:端口使能、功能设定、波特率、设置数据格式
GPHCON = (GPHCON & ~(0xfff<<4)) |(0xaaa<<4);//端口配置成uart0、uart1,uart3
GPHUP = 0x38; //端口GPH禁止上拉
UFCON0 = 0x0; //禁止FIFO
UMCON0 = 0x0; //禁止AutoFlow Control
//Normal:No parity:One stop:8-bits 中断响应 UART clock: PCLK
ULCON0 = 0x03; // 8N1(8个数据位,无较验,1个停止位)
UCON0 = 0x05; // 查询方式,UART时钟源为PCLK
UBRDIV0 = UART_BRD; // 波特率为115200
}
voiduart_send_byte(char data)
{
while (!(UTRSTAT0 & TXD0READY));
UTXH0 = data;
}
voiduart_send_string(char *string)
{
while(*string)
{
uart_send_byte(*string++);
}
}
void uart_printf(char *fmt,...) //这个才是本文重点
{
va_listap;
charstring[256];
va_start(ap,fmt);
vsprintf(string,fmt,ap);
uart_send_string(string);
va_end(ap);
}
//***************************************************
1.1.2 uart_printf分析
这里涉及到一个重要概念,变参函数,比如C库函数int printf(char *fmt, ...),就是一个典型的变参函数。
我们即将编写的uart_printf毫无疑问,也是一个变参函数。
可变参数入栈顺序
在进程中,堆栈地址是从高到低分配的.当执行一个函数的时候,将参数列表入栈,压入堆栈的高地址部分,然后入栈函数的返回地址,接着入栈函数的执行代码,这个入栈过程,堆栈地址不断递减,一些黑客就是在堆栈中修改函数返回地址,执行自己的代码来达到执行自己插入的代码段的目的.
总之,函数在堆栈中的分布情况是:地址从高到低,依次是:函数参数列表,函数返回地址,函数执行代码段.
堆栈中,各个函数的分布情况是倒序的.即最后一个参数在列表中地址最高部分,第一个参数在列表地址的最低部分.参数在堆栈中的分布情况如下:
最后一个参数
倒数第二个参数
...
第一个参数
函数返回地址
函数代码段
va_list/ va_start/ va_arg/va_end 介绍
C语言标准库中头文件stdarg.h索引的接口包含了一组能够遍历变参数列表的宏。主要包含下面几个:
1.va_list:在函数里定义一个va_list型的变量,这个变量是指向参数的指针
2.va_start:用va_start宏初始化刚定义的va_list变量,让它指向可变参数表里面的第一个参数,
3.va_arg:每次调用时都会返回当前指针指向的变量,并将指针挪至下一个位置,参数的类型需要在这个调用的第二个参数来指定,va_arg也是根据这个参数来判断偏移的距离。
4.va_end:获取所有的参数之后,我们有必要将这个指针关掉,以免发生危险,方法是调用 va_end,置为 NULL,应该养成获取完参数表之后关闭指针的习惯。
void uart_printf(char *fmt,...) //这个才是本文重点
{
va_listap; //定义一个 va_list 指针来访问参数表
charstring[256];
va_start(ap,fmt); //初始化 ap,让它指向第一个变参[也就是fmt参数后面的参数]
vsprintf(string,fmt,ap); //将带参数的字符串按照参数列表格式化到string中
uart_send_string(string);
va_end(ap); //结束变量列表,和va_start成对使用
}
可变参数在编译器中的处理
va_list ,va_start,va_arg,va_end是在stdarg.h中被定义成宏的,以VC++中stdarg.h里x86平台的宏定义摘录如
=================================================================
typedef char * va_list;
#define_INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
#defineva_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#defineva_arg(ap,t) ( *(t *)((ap +=_INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0
=================================================================
è#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统
我们知道对于X86,sizeof(int)一定是4的整数倍,所以~(sizeof(int) - 1) )的值一定是右面[sizeof(n)-1]/2位为0,整个这个宏也就是保证了右面[sizeof(n)-1]/2位为0,其余位置为1,所以_INTSIZEOF(n)的值只有可能是4,8,16,......等等,实际上是实现了内存对齐。
举例:#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
目的在于把sizeof(n)的结果变成至少是sizeof(int)的整倍数,这个一般用来在结构中实现按int的倍数对齐。
如果sizeof(int)是4,那么,当sizeof(n)的结果在1~4之间是,_INTSIZEOF(n)的结果会是4;当sizeof(n)的结果在5~8时,_INTSIZEOF(n)的结果会是8;当sizeof(n)的结果在9~12时,_INTSIZEOF(n)的结果会是12;……总之,会是sizeof(int)的倍数。
è#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
ap指向参数v之后的那个参数的地址,即 ap指向第一个可变参数在堆栈的地址。
è#define va_arg(ap,t) ( *(t *)((ap +=_INTSIZEOF(t)) - _INTSIZEOF(t)) )
取出当前ap指针所指的值,并使ap指向下一个参数。
用va_arg返回可变的参数,va_arg的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用va_arg获取各个参数
è#define va_end(ap) (ap = (va_list)0
清空va_list ap.
使用VA_LIST应该注意的问题:
(1)因为va_start,va_arg, va_end等定义成宏,并不能智能地识别不同参数的个数和类型. 也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的.这个方法存在漏洞:输入参数的类型随意性,使得参数很容易以一个不正确的类型获取一个值(譬如输入一个float,却以int型去获取他),这样做会出现莫名其妙的运行结果
(2)另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.不利于我们写出高质量的代码。
(3)由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量,或作为函数或数组类型。