UART原理说明:
通用异步收发器简称UART,即"Universal Asynchronous Receiver Transmitter",它用来传输串行数据:发送数据时,CPU将并行数据写入UART,UART按照一定的格式在一根电线上串行发出;接收数据时,UART检测另一根电线上的信号,将串行收集放在缓冲区中,CPU即可读取UART获得这些数据。UART之间以全双工方式传输数据,最精简的连线方法只有三根电线:TxD用于发送数据,RxD用于接收数据,Gnd用于给双方提供参考电平,连线如图1所示:
图1. UART连线图
UART使用标准的TTL/CMOS逻辑电平(0~5V、0~3.3V、0~2.5V或0~1.8V四种)来表示数据,高电平表示1,低电平表示0。为了增强数据的抗干扰能力、提高传输长度,通常将TTL/CMOS逻辑电平转换为RS-232逻辑电平,3~12V表示0,-3~-12V表示1。
TxD、RxD数据线以"位"为最小单位传输数据。帧(frame)由具有完整意义的、不可分割的若干位组成,它包含开始位、数据位、较验位(需要的话)和停止位。发送数据之前,UART之间要约定好数据的传输速率(即每位所占据的时间,其倒数称为波特率)、数据的传输格式(即有多少个数据位、是否使用较验位、是奇较验还是偶较验、有多少个停止位)。
数据传输流程如下:
(1)平时数据线处于"空闭"状态(1状态)。
(2)当要发送数据时,UART改变TxD数据线的状态(变为0状态)并维持1位的时间──这样接收方检测到开始位后,再等待1.5位的时间就开始一位一位地检测数据线的状态得到所传输的数据。
(3)UART一帧中可以有5、6、7或8位的数据,发送方一位一位地改变数据线的状态将它们发送出去,首先发送最低位(LSB)。
(4)如果使用较验功能,UART在发送完数据位后,还要发送1个较验位。有两种较验方法:奇较验、偶较验──数据位连同较验位中,"1"的数目等于奇数或偶数。
(5)最后,发送停止位,数据线恢复到"空闭"状态(1状态)。停止位的长度有3种:1位、1.5位、2位。
图2演示了UART使用7个数据位、偶较验、2个停止位的格式传输字符"A"(二进制值为0b1000001)时,TTL/CMOS逻辑电平对应的波形。
图2. TTL/CMOS逻辑电平下,传输"A"时的波形
UART还有其他功能,比如流量控制等,想深入了解的读者请自行查阅相关资料。
Exynos4412的UART特性:
Exynos4412中UART,有4个独立的通道,每个通道都可以工作于中断模式或DMA模式,即UART可以发出中断或DMA请求以便在UART、CPU间传输数据。UART由波特率发生器、发送器、接收器和控制逻辑组成。
使用系统时钟时,Exynos4412的UART波特率可以达到4Mbps。波特率可以通过编程进行控制。
Exynos4412 UART的通道0有256字节的发送FIFO和256字节的接收FIFO;通道1、4有64字节的发送FIFO和64字节的接收FIFO;通道2、3有16字节的发送FIFO和16字节的接收FIFO。发送数据时,CPU先将数据写入发送FIFO中,然后UART会自动将FIFO中的数据复制到"发送移位器"(Transmit Shifter)中,发送移位器将数据一位一位地发送到TxDn数据线上(根据设定的格式,插入开始位、较验位和停止位)。接收数据时,"接收移位器"(Receive Shifter)将RxDn数据线上的数据一位一位接收进来,然后复制到接收FIFO中,CPU即可从中读取数据。
Exynos4412 UART的每个通道支持的停止位有1位、2位,数据位有5、6、7或8位,支持较验功能,另外还有红外发送/接收功能。Exynos4412 UART结构如图3所示:
图3. Exynos4412 UART结构图
Exynos4412 UART的使用:
图4. 设置Serial连接图
如图4所示,我们在使用UART与PC通信的时候,PC端需要设置波特率、数据位、是否使用校验位、有多少个停止位、是否使用流控等。UART是工作在异步模式下,接收器自身实现帧的同步,因此要实现通信,Exynos4412的UART也要作相同的设置,另外还要选择所涉及管脚为UART功能、选择UART通道的工作模式为中断模式或DMA模式。设置好之后,往某个寄存器写入数据即可发送,读取某个寄存器即可得到接收到的数据。在中断模式(查询模式)下,我们一般通过查询状态寄存器或设置中断来获知数据是否已经发送完成、是否已经接收到数据。下面详细讲解上述设置过程。
1.将所涉及的UART通道管脚设为UART功能
比如UART通道0中,GPA0_0、GPA0_1分别用作RXD0、TXD0,要使用UART通道0时,先设置GPA0CON寄存器将GPA0_0、GPA0_1引脚的功能设为RXD0、TXD0。
2.选择UART的时钟源
图5. UART时钟
Exynos4412 UART的时钟源有八种选择:XXTI、XusbXTI、SCLK_HDMI24M、SCLK_USBPHY0、SCLK_HDMIPHY、SCLKMPLL_USER_T、SCLKEPLL、SCLKVPLL,由CLK_SRC_PERIL0寄存器控制。
选择好时钟源后,还可以通过DIVUART0~4设置分频系数,由CLK_DIV_PERIL0寄存器控制。从分频器得到的时钟被称为SCLK_UART。
SCLK_UART经过图5中的"UCLK Generator"后,得到UCLK,它的频率就是UART的波特率。"UCLK Generator"通过这2个寄存器来设置:UBRDEVn、UFRACVALn(在下面描述)。
表1 CLK_SRC_PERIL0的寄存器格式
由于在iROM的代码中设置了相关时钟的寄存器,我们在试验的使用选择XusbXTI作为时钟源。
表2 CLK_DIV_PERIL0的寄存器格式
3.设置波特率:UBRDIVn寄存器(UART BAUD RATE DIVISOR)、UFRACVALn寄存器
根据给定的波特率、所选择的时钟源的频率,可以通过以下公式计算UBRDIVn 寄存器(n为0~4,对应5个UART通道)的值:
UBRDIVn = (int)(UART clock/(buad rate x 16)) – 1
上式计算出来的UBRDIVn寄存器值不一定是整数,UBRDIVn寄存器取其整数部分,小数部分由UFRACVALn寄存器设置,UFRACVALn寄存器的引入,使产生的波特率更加精确。
例如,当UART clock为100MHz时,要求波特率为115200bps,则:
100000000/(115200 x 16) – 1 = 54.25 – 1 = 53.25
UBRDIVn = 整数部分 = 53
UFRACVALn/16 = 小数部分 = 0.25
UFRACVALn = 4
4.设置传输格式:ULCONn寄存器(UART LINE CONTROL)
ULCONn寄存器(n为0~4)格式如表3所示。
表3 ULCONn寄存器格式
我们这里设置为,普通模式、无校验位、一帧中有一个停止位和8位的数据位。
UART通道被设为红外模式时,其串行数据的波形与正常模式稍有不同,有兴趣的读者请自行阅读数据手册。
5.设置UART工作模式:UCONn寄存器(UART CONTROL)
Exynos4412的UCONn寄存器格式,如表4所示。
表4 UCONn寄存器格式
我们写程序时使用中断或查询方式。
6.UFCONn寄存器(UART FIFO CONTROL)、UFSTATn寄存器(UART FIFO STATUS)
UFCONn寄存器用于设置是否使用FIFO,设置各FIFO的触发阀值,即发送FIFO中有多少个数据时产生中断、接收FIFO中有多少个数据时产生中断。并可以通过设置UFCONn寄存器来复位各个FIFO。
读取UFSTATn寄存器可以知道各个FIFO是否已经满、其中有多少个数据。
不使用FIFO时,可以认为FIFO的深度为1,使用FIFO时Exynos4412的FIFO深度最高可达到256。这两类寄存器各位的含义请读者查阅数据手册。
7.UMCONn寄存器(UART MODEM CONTROL)、UMSTATn寄存器(UART MODEM STATUS)
这两类寄存器用于流量控制,这里不介绍。
8.UTRSTATn寄存器(UART TX/RX STATUS)
UTRSTATn寄存器用来表明数据是否已经发送完毕、是否已经接收到数据,格式如下表4所示。下面说的"缓冲区",其实就是图3中的FIFO,不使用FIFO功能时可以认为其深度为1。
表5 UTRSTATn寄存器格式
9.UERSTATn寄存器(UART ERROR STATUS)
用来表示各种错误是否发生,位[0]至位[3]为1时分别表示溢出错误、较验错误、帧错误、检测到"break"信号。读取这个寄存器时,它会自动清0。
需要注意的是,接收数据时如果使用FIFO,则UART内部会使用一个"错误 FIFO"来表明接收FIFO中哪个数据在接收过程中发生了错误。CPU只有在读出这个错误的数据时,才会觉察到发生了错误。要想清除"错误 FIFO",则必须读出错误的数据,并读出UERSTATn寄存器。
10.UTXHn寄存器(UART TRANSMIT BUFFER REGISTER)
CPU将数据写入这个寄存器,UART即会将它保存到缓冲区中,并自动发送出去。
11.URXHn寄存器(UART RECEIVE BUFFER REGISTER)
当UART接收到数据时,CPU读取这个寄存器,即可获得数据。
示例程序详解,主要涉及到三个文件,start.S文件主要设置栈等基本的功能;uart.c文件主要就是初始化串口和定义操作串口用到的函数。main.c文件主要做相关的操作。
start.S文件内容如下:
.text
.global _start
_start:
ldr sp, =0x02027400 //调用C函数之前必须设置栈,栈用于保存运行环境,给局部变量分配空间;
//参考ROM手册P14,我们把栈指向BL2的最上方;
//即:0x02020000(iROM基地址)+5K(iROM代码用)+8K(BL1用)+16K(BL2用)
bl main //跳转到C函数中执行
halt: //死循环
b halt
uart.c文件的内容如下,已经在里面做了详细的注释,这里不再做详细的介绍。
//串口0使用的引脚
#define GPA0CON (*(volatile unsigned int *)0x11400000)
//选择时钟源
#define CLK_SRC_PERIL0 (*(volatile unsigned int *)0x1003C250)
//设置uart0的分频系数
#define CLK_DIV_PERIL0 (*(volatile unsigned int *)0x1003C550)
#define UBRDIV0 (*(volatile unsigned int *)0x13800028)
#define UFRACVAL0 (*(volatile unsigned int *)0x1380002C)
#define UFCON0 (*(volatile unsigned int *)0x13800008)
#define ULCON0 (*(volatile unsigned int *)0x13800000)
#define UCON0 (*(volatile unsigned int *)0x13800004)
#define UTXH0 (*(volatile unsigned int *)0x13800020)
#define URXH0 (*(volatile unsigned int *)0x13800024)
#define UTRSTAT0 (*(volatile unsigned int *)0x13800010)
void uart0_init()
{
//配置GPA0CON的引脚为串口功能;
GPA0CON = 0x22222222;
//设置串口0使用的时钟源;使用XusbXTI作为时钟源
CLK_SRC_PERIL0 &= ~(0xF); //低四位清零
CLK_SRC_PERIL0 |= (0x1); //低四位设置为0b0001 MOUTUART0 = 24MHz
//uart0的时钟源为24MHz;设置分频系数
CLK_DIV_PERIL0 &= ~(0xF); //低四位清零
CLK_DIV_PERIL0 |= (0x2); //SCLK_UART0 = MOUTUART0/(UART0_RATIO + 1)
//MOUTUART0=24,UART0_RATIO=2;所以,SCLK_UART0=8MHz
//程序走到这里SCLK_UART0(8MHz)时钟已经获得了。
/*
* 使能FIFO;Rx FIFO:64 bytes Tx FIFO:32 bytes
*/
UFCON0 =0x111;
/* 设置数据格式: 8n1, 即8个数据位,没有较验位,1个停止位 */
ULCON0 = 0x3;
/* 工作于中断/查询模式
* 另一种是DMA模式,本章不使用
*/
UCON0 = 0x5;
//SCLK_UART0=8MHz;设置UBRDIV0和UFRACVAL0寄存器;波特率是115200
UBRDIV0 = 0x3;
UFRACVAL0 = 0x5;
}
//从串口获得一个字符
char getc(void)
{
char c;
/* 查询状态寄存器,直到有有效数据 */
while (!(UTRSTAT0 & (1<<0)));
c = URXH0; /* 读取接收寄存器的值 */
return c;
}
//输出一个字符
void putc(char c)
{
/* 查询状态寄存器,直到发送缓存为空 */
while (!(UTRSTAT0 & (1<<2)));
UTXH0 = c; /* 写入发送寄存器 */
return;
}
//打印字符串
void puts(char *s)
{
while (*s)
{
putc(*s);
s++;
}
}
//将数字按照十六进制格式打印
void puthex(unsigned long val)
{
/* val = 0x1234ABCD */
unsigned char c;
int i = 0;
putc('0');
putc('x');
for (i = 0; i < 8; i++)
{
c = (val >> ((7-i)*4)) & 0xf;
if ((c >= 0) && (c <= 9))
{
c = '0' + c;
}
else if ((c >= 0xA) && (c <= 0xF))
{
c = 'A' + (c - 0xA);
}
putc(c);
}
putc('\n');
putc('\r');
}
main.c文件主要做使用试验使用,内容如下:
int main()
{
char c;
uart0_init();
puts("Test uart by Haitao Cai!\n\r");
puthex(100); //打印十六进制数
while(1)
{
c = getc(); //从串口获得一个字符
if(c == '\r')
{
puts("Test over!");
break;
}
putc(c);
putc(':');
putc(c+1); //将字符加1输出
putc('\n');
putc('\r');
}
return 0;
}
将文件上传,编译并烧写SD卡;开机试验过程如下:
本文完毕!