串口UART
1. 背景知识
1.1 串行通信和并行通信
-
串行通信:使用一根数据线,将数据一位一位地依次传输。简称:逐位收发。特点:只有1根数据线。
-
并行通信:使用多根数据线,一次发送多位数据。缺点:线与线存在信号干扰。
1.2 单工通信和双工通信
-
单工:只支持信号在一个方向上传输,任何时候不能改变信号的传输方向。
如上图所示,数据只能从发送器发送到接收器,无法从接收器发送到发送器,数据是单向传输的。
-
双工:双工通信可以分为全双工和半双工两种模式。
2.1 半双工:允许信号在两个方向上传输,但某一时刻只允许信号在一个信道上单向传输。特点:只有1根数据线。
半双工通信实际上是一种可切换方向的单工通信。
如上图所示,左边的发送器向右边的接收器发送数据时,右边的发送器无法向左边的接收器发送数据。因为只有1根数据线,只能等待一方的发送器发送完数据,另一方的发送器才能发送。
2.2 全双工:允许数据同时在两个方向上传输,因此可以同时在两个信道上进行双向传输。特点:有2根数据线。
全双工通信可以看作是是两个单工通信方式的结合。
如上图所示,双方的发送器可以独立发送数据,因为有2根数据线,每个发送器占用1根,两者互不影响。
1.3 波特率和比特率
-
波特率(单位:Baud):每秒传送码元符号的个数。不同的调制方式,可以在一个码元符号上负载多个bit位信号。在二进制下,波特率=比特率。
-
比特率(单位:bps):每秒传送bit的数量,用于描述UART通信时的通信速度。
1.4 同步和异步
同步通信:收发双方在通信时,有同步时钟信号,该时钟信号又叫SCK。时钟信号确定了通信的节奏。
异步通信:收发双方在通信时,没有同步时钟信号,收发双方依靠各自的时钟,因此收发双方必须事先约定好传输速率才能开始收发数据。
2. 串口简介
串口英文简称:UART,全称:Universal Asynchronous Receiver Transmitter
即通用异步收发器。
串口是一种通用的串行、异步通信总线。该总线有2条数据线(TXD RXD),可以实现全双工的发送和接收。在嵌入式系统中常用于主机与辅助设备之间的通信。
串口:点对点、串行、全双工、异步
3. 串口帧格式
-
起始位为低位,停止位为高位。拉低为始,拉高为止。
-
数据线空闲时,一定为高电平。
- 两个器件通讯时,一定要有起始信号和停止信号,表示通讯的开始和结束。
- 有了起始信号才能确定后面紧跟的是有效的数据,收发双发才会做好准备进行处理。
- 串口必须要有停止信号,因为其是异步通信,收发双方完全依靠各自的时钟来收发数据,时钟之间必然存在误差,有的时钟快一点,有的时钟慢一点,短时间影响不大,长时间就会出现累计误差。比如:
01
和0011
,它两的电平从大体上来看是一样的,只能通过时钟来确定有几个有效的数据,因为收发双方的时钟存在误差,时间一长必然会导致累计误差,因此必须要有停止位。 - 发送n个有效的数据必须要发送n个停止位和n个起始位,如果只有1个停止位和1个起始位,就会产生上面3中描述错位的现象。
- 数据线空闲时为高电平是为了和起始信号的低电平进行区分。
-
串口是先发低位,再发高位,一次发送一个字节。
-
校验位只能起到校验的作用,无法修正错误。
4. 硬件设置
如上图所示,简而言之就是A器件的TXD接B器件的RXD,同时两者的GND也要连接在一起(图中未给出)。
在芯片中,引脚是有多种功能的。例如下图的PA2,可以作为串口2的TXD引脚、定时器5的通道3引脚、ADC1、2以及3的2号输入引脚以及定时器2的通道3引脚。在使用引脚前,需要先设置引脚的功能。
设置引脚功能的实质是让引脚在芯片内部连接到某一个对应的控制器上。类似于单刀多掷开关的效果,如下图所示:
5. 串口的发送过程
串口收发过程类似,以发送为例来进行说明。
串口的发送器和接收器都有1个队列(FIFIO)和1个移位器。队列的特性是先进先出。
以下图为例说明发送的过程,数字5先进入到队列当中,1最后进入到队列中,所以先发送5;将5拷贝到移位器中。因为发送的是0或者1这种二进制的电平信号,移位器存放的是数据的二进制表示形式。5的二进制是101,因此下图中移位器的数据是 00000101
,移位器的数据通过TXD引脚一位一位发送出去,先发低位,再发高位。
接收的过程类似,只是方向相反。
6. 串口的整体发送过程
下图是数据的整体发送路径:
CPU向发送寄存器写入数据,发生寄存器的数据会通过发送器发送出去。CPU每秒能执行10亿条指令,而发送器发送的速率比较低,例如:115200。这就要求速率要匹配。
假如要不断发送ABCD这4个数据,当CPU向发送寄存器写入A后,发送器开始发送A,而发送器还没发送完A,CPU又向寄存器写了B,此时发送器没发送完,B不会被发送出去;此时CPU又向发送寄存器写入C,此时B被覆盖掉了,造成了数据遗漏,一直到发送器里面的数据彻底发送出去了,才会将发送寄存器里面的最新数据发送出去。
所以要进行速率的匹配:一直等到发送器发送完数据后,CPU才向发生寄存器写入新的数据,让发送器继续发送新的数据。在发送器发送时,CPU可以忙其他的事情,充分利用资源,避免资源占用!
//速率不匹配的案例
void UART_Init(void)
{
/*1.将GPA1_0和GPA1_1设置成UART2的接收和发送引脚 GPA1CON[7:0]*/
GPA1.CON = GPA1.CON & (~(0xFF << 0)) | (0x22 << 0);
/*2.设置UART2的帧格式 8位数据位 1位停止位 无校验 正常模式 ULCON2[6:0]*/
UART2.ULCON2 = UART2.ULCON2 & (~(0x7F << 0)) | (0x3 << 0);
/*3.设置UART2的接收和发送模式为轮询模式 UCON2[3:0]*/
UART2.UCON2 = UART2.UCON2 & (~(0xF << 0)) | (0x5 << 0);
/*4.设置UART2的波特率为115200 UBRDIV2/UFRACVAL2*/
UART2.UBRDIV2 = 53;
UART2.UFRACVAL2 = 4;
}
int main()
{
UART_Init();
while (1)
{
UART2.UTXH2 = 'A';
UART2.UTXH2 = 'B';
UART2.UTXH2 = 'C';
UART2.UTXH2 = 'D';
}
}
上图为接收到的数据,本想发送不断 ABCD
,结果接收到这么一串数据。看似有规律可言,但实际上接收的是没有规律的字符,具体原因上面分析过了,主要还是由于速率不匹配造成的。
解决办法:等待发送寄存器为空,再写入新的数据。
//速率不匹配的案例
void UART_Init(void)
{
/*1.将GPA1_0和GPA1_1设置成UART2的接收和发送引脚 GPA1CON[7:0]*/
GPA1.CON = GPA1.CON & (~(0xFF << 0)) | (0x22 << 0);
/*2.设置UART2的帧格式 8位数据位 1位停止位 无校验 正常模式 ULCON2[6:0]*/
UART2.ULCON2 = UART2.ULCON2 & (~(0x7F << 0)) | (0x3 << 0);
/*3.设置UART2的接收和发送模式为轮询模式 UCON2[3:0]*/
UART2.UCON2 = UART2.UCON2 & (~(0xF << 0)) | (0x5 << 0);
/*4.设置UART2的波特率为115200 UBRDIV2/UFRACVAL2*/
UART2.UBRDIV2 = 53;
UART2.UFRACVAL2 = 4;
}
int main()
{
UART_Init();
while (1)
{
/*等待发送寄存器为空,即上一个数据已经发送完成 UTRSTAT2[1]*/
while(!(UART2.UTRSTAT2 & (1 << 1)));
UART2.UTXH2 = 'A';
/*等待发送寄存器为空,即上一个数据已经发送完成 UTRSTAT2[1]*/
while(!(UART2.UTRSTAT2 & (1 << 1)));
UART2.UTXH2 = 'B';
/*等待发送寄存器为空,即上一个数据已经发送完成 UTRSTAT2[1]*/
while(!(UART2.UTRSTAT2 & (1 << 1)));
UART2.UTXH2 = 'C';
/*等待发送寄存器为空,即上一个数据已经发送完成 UTRSTAT2[1]*/
while(!(UART2.UTRSTAT2 & (1 << 1)));
UART2.UTXH2 = 'D';
}
}
7. 输入输出重定向 - printf
浅聊 printf
- 来源
- C语言库函数
- 自己写,但是无法处理浮点型数据(原因:无相应代码,难以处理)
- 输出重定向
- 屏幕(最常见 -> 在屏幕上显示输出的数据)
- UART(将数据通过串口输出)
裸机开发,没有安装操作系统,就没有C语言的库函数,所以只能自己写 printf
函数。同时,要将内容通过串口发送,而不是通过屏幕显示出来,因此要重新确定 printf
输出的方向,简称重定向。
下面是自己写的printf
函数,其核心是vsprintf
函数,它是对数据格式的处理;最后一行的 puts
函数里面调用的putc
函数包含了数据重定向的作用。
void printf (const char *fmt, ...)
{
va_list args;
unsigned int i;
char printbuffer[100];
va_start (args, fmt);
/* For this to work, printbuffer must be larger than
* anything we ever want to print.
*/
i = vsprintf (printbuffer, fmt, args); //核心
va_end (args);
puts (printbuffer); //重定向
}
vsprintf
函数的作用是对格式进行解析,并没有重定向的作用
int vsprintf(char *buf, const char *fmt, va_list args)
{
int len;
#ifdef CFG_64BIT_VSPRINTF
unsigned long long num;
#else
unsigned long num;
#endif
int i, base;
char * str;
const char *s;
int flags; /* flags to number() */
int field_width; /* width of output field */
int precision; /* min. # of digits for integers; max
number of chars for from string */
int qualifier; /* 'h', 'l', or 'q' for integer fields */
for (str=buf ; *fmt ; ++fmt) {
if (*fmt != '%') {
*str++ = *fmt;
continue;
}
/* process flags */
flags = 0;
repeat:
++fmt; /* this also skips first '%' */
switch (*fmt) {
case '-': flags |= LEFT; goto repeat;
case '+': flags |= PLUS; goto repeat;
case ' ': flags |= SPACE; goto repeat;
case '#': flags |= SPECIAL; goto repeat;
case '0': flags |= ZEROPAD; goto repeat;
}
/* get field width */
field_width = -1;
if (is_digit(*fmt))
field_width = skip_atoi(&fmt);
else if (*fmt == '*') {
++fmt;
/* it's the next argument */
field_width = va_arg(args, int);
if (field_width < 0) {
field_width = -field_width;
flags |= LEFT;
}
}
/* get the precision */
precision = -1;
if (*fmt == '.') {
++fmt;
if (is_digit(*fmt))
precision = skip_atoi(&fmt);
else if (*fmt == '*') {
++fmt;
/* it's the next argument */
precision = va_arg(args, int);
}
if (precision < 0)
precision = 0;
}
/* get the conversion qualifier */
qualifier = -1;
if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L' ||
*fmt == 'Z' || *fmt == 'z' || *fmt == 't' ||
*fmt == 'q' ) {
qualifier = *fmt;
if (qualifier == 'l' && *(fmt+1) == 'l') {
qualifier = 'q';
++fmt;
}
++fmt;
}
/* default base */
base = 10;
switch (*fmt) {
case 'c':
if (!(flags & LEFT))
while (--field_width > 0)
*str++ = ' ';
*str++ = (unsigned char) va_arg(args, int);
while (--field_width > 0)
*str++ = ' ';
continue;
case 's':
s = va_arg(args, char *);
if (!s)
s = "<NULL>";
len = strnlen(s, precision);
if (!(flags & LEFT))
while (len < field_width--)
*str++ = ' ';
for (i = 0; i < len; ++i)
*str++ = *s++;
while (len < field_width--)
*str++ = ' ';
continue;
case 'p':
if (field_width == -1) {
field_width = 2*sizeof(void *);
flags |= ZEROPAD;
}
str = number(str,
(unsigned long) va_arg(args, void *), 16,
field_width, precision, flags);
continue;
case 'n':
if (qualifier == 'l') {
long * ip = va_arg(args, long *);
*ip = (str - buf);
} else {
int * ip = va_arg(args, int *);
*ip = (str - buf);
}
continue;
case '%':
*str++ = '%';
continue;
/* integer number formats - set up the flags and "break" */
case 'o':
base = 8;
break;
case 'X':
flags |= LARGE;
case 'x':
base = 16;
break;
case 'd':
case 'i':
flags |= SIGN;
case 'u':
break;
default:
*str++ = '%';
if (*fmt)
*str++ = *fmt;
else
--fmt;
continue;
}
#ifdef CFG_64BIT_VSPRINTF
if (qualifier == 'q') /* "quad" for 64 bit variables */
num = va_arg(args, unsigned long long);
else
#endif
if (qualifier == 'l') {
num = va_arg(args, unsigned long);
} else if (qualifier == 'Z' || qualifier == 'z') {
num = va_arg(args, size_t);
} else if (qualifier == 't') {
num = va_arg(args, long);
} else if (qualifier == 'h') {
num = (unsigned short) va_arg(args, int);
if (flags & SIGN)
num = (short) num;
} else if (flags & SIGN)
num = va_arg(args, int);
else
num = va_arg(args, unsigned int);
str = number(str, num, base, field_width, precision, flags);
}
*str = '\0';
return str-buf;
}
重定向的内容在putc
函数实现的。 puts
函数的作用是:输出一个字符串。
void putc(const char data)
{
while(!(UART2.UTRSTAT2 & 0X2)); //等待发送寄存器为空
UART2.UTXH2 = data; //输出重定向
if (data == '\n')
putc('\r');
}
//输出一个字符串
void puts(const char *pstr)
{
while(*pstr != '\0')
putc(*pstr++);
}
各函数流程和主要功能如下图所示:
有时putc
也写作fputc
;有时puts
也写作fputs
;重定向,就得找putc
函数。