1 UART通信协议
1.1 UART通信的物理连接
图1 UART的物理连接
1.2 逻辑电平
用电平表示逻辑1和逻辑0,逻辑1和逻辑0用来组织计算机层面的数据。
1.3 电平标准
根据通讯使用的电平标准不同,串口通讯可分为 TTL标准及 RS-232 标准。
1.4 协议解析
通讯双方需要约定波特率,并约定一致的数据包格式才能保证正常收发数据。
1.4.1 波特率(bps)
单位时间内,发送数据的位数。
1.4.2 数据格式
串口通信一般以起始位作为一帧数据传输的开始,以结束位表示一帧数据传输的结束。每一帧数据一般由起始位、数据位、停止位、校验位组成。
起始位:由1个逻辑0的数据位表示;
停止位:由 0.5、1、1.5或 2个逻辑 1的数据位表示;
校验位(奇校验/偶校验):当为奇校验时,数据位和校验位中,逻辑1的数据位的个数为奇数个;当为偶校验时,数据位和校验位中,逻辑1的数据位的个数为偶数个。
图2 115200,8n1; send 0b01000001
如图2所示,115200bps,则1/115200spb,即每传输一位需要1/115200秒,数据在(1/115200)/2处采样。
2 printf的实现
2.1 标准库中的printf
函数原型:
int printf(const char *format, ...)
返回值:
成功返回实际输出字符数,失败返回-1;
传入参数说明:
format: 固定参数
...: 可变参数;参数的个数不确定,类型不确定;
2.2 可变参数的实现原理
调用子函数,函数的参数最终会以被压入栈中,被函数使用;通过格式控制符,实现对栈中传入参数的读取和使用。
在标准库中的实现:
typedef char* va_list;
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1) & ~(sizeof(int)-1)) //保证4字节对齐
#define va_start(ap, v) (ap = (va_list)&v + _INTSIZEOF(v)) //获取第一个变参在栈中的地址
#define va_arg(ap, t) (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t))) //获取ap所指向的数据,并把ap偏移至下一个变参的地址
#define va_end(ap) (ap = (va_list)0) //使ap指向空,避免野指针
printf(format, arg1, arg2, arg3);参数在栈中的存放
2.3.1 对_INTSIZEOF(n)分析
栈指针总是4字节对齐的,因此使用_INTSIZEOF(n),使变量的大小是4的倍数(实际变量在栈中占据的空间)。
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1) & ~(sizeof(int)-1)),对这个宏定义有一个形象的比喻:
比方说有一个箱子可以装4个瓶子,
如果我有8个瓶子 ,那么我需要2个箱子;
如果我有10个瓶子呢,我不能说我需要10除4,需要2.5个箱子吧,实际上我需要3个箱子;
那怎么求我实际需要的箱子数呢?
用一个容易理解的公式来求上述问题:
设我的瓶子数为B,我需要的箱子数为C,一个箱子最多可以装A个瓶子。
公式:C =(B+A-1)/ A (舍去余数)
因此,((sizeof(n)+sizeof(int)-1) & ~(sizeof(int)-1))相当于C * A。
2.3.2 对va_arg(ap, t)分析
#define va_arg(ap, t) (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
这个宏定义实现了两个功能:
a、ap += _INTSIZEOF(t) —— 求下一个参数的指针
b、(*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t))) —— 取当前参数值,这个宏定义将返回的就是当前参数值
附录:源代码
uart.c —— 波特率:115200
debug_printf.c