0. 前言
Uart在一个嵌入式系统中是一个非常重要的模块,他承担了CPU与用户交互的桥梁。用户输入信息给程序、CPU要打印一些信息给终端都要依赖UART。
本文将以Exynos4412的UART控制器为基础,讲解UART的原理以及驱动程序如何编写。
1. UART是什么
UART是通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),通常称作UART,是一种异步收发传输器,是设备间进行异步通信的关键模块。UART负责处理数据总线和串行口之间的串/并、并/串转换,并规定了帧格式;通信双方只要采用相同的帧格式和波特率,就能在未共享时钟信号的情况下,仅用两根信号线(Rx 和Tx)就可以完成通信过程,因此也称为异步串行通信。UART总线双向通信,可以实现全双工传输和接收。在嵌入式设计中,UART用于主机与辅助设备通信,如汽车音响与外接AP之间的通信,与PC机通信包括与监控调试器和其它器件,如EEPROM通信。
通常需要加入一个合适的电平转换器,如SP3232E、SP3485,UART还能用于RS-232、RS-485 通信,或与计算机的端口连接。UART 应用非常广泛,手机、工业控制、PC 等应用中都要用到UART。
在这里插入图片描述
2. UART通信方式
UART使用的是 异步,串行通信方式。
串行通信
串行通信是指利用一条传输线将资料一位位地顺序传送。好比是一列纵队,每个数据元素依次纵向排列。如下图所示,传输时一个比特一个比特的串行传输,每个时钟周期传输一个比特,这种传输方式相对比较简单,速度较慢,但是使用总线数较少,通常一根接收线,一根发送线即可实现串行通信。
它的缺点是要增加额外的数据来控制一个数据帧的开始和结束。特点是通信线路简单,利用简单的线缆就可实现通信,降低成本,适用于远距离通信,但传输速度慢的应用场合。
并行通信
并行通信好比一排横队,齐头并进同时传输。这种通信方式每个时钟周期传输的数据量和其总线宽度成正比,但是实现较为复杂。
在这里插入图片描述
异步通信
异步通信以一个字符为传输单位,通信中两个字符间的时间间隔多少是不固定的,然而在同一个字符中的两个相邻位间的时间间隔是固定的。
在异步通信技术中,数据发送方和数据接收方没有同步时钟,只有数据信号线,只不过发送端和接收端会按照协商好的协议(固定频率)来进行数据采样。数据发送方以每秒钟57600bits的速度发送数据,接收方也以57600bits的速度去接收数据,这样就可以保证数据的有效和正确。通常异步通信中使用波特率(Baud-Rate)来规定双方传输速度,其单位为bps(bits per second每秒传输位数)。
同步通信
在发送数据信号的时候,会同时送出一根同步时钟信号, 用来同步发送方和接收方的数据采样频率。如下图所示,同步通信时,信号线1是一根同步时钟信号线,以固定的频率进行电平的切换,其频率周期为t,在每个电平的上升沿之后进行对同步送出的数据信号线2进行采样(高电平代表1,低电平代表0),根据采样数据电平高低取得输出数据信息。如果双方没有同步时钟的话,那么接收方就不知道采样周期,也就不能正常的取得数据信息。
在这里插入图片描述
3. 帧格式
数据传送速率用波特率来表示,即每秒钟传送的二进制位数。例如数据传送速率为120字符/秒,而每一个字符为10位(1个起始位,7个数据位,1个校验位,1个结束位),则其传送的波特率为10×120=1200字符/秒=1200波特。数据通信格式如下图:
在这里插入图片描述
其中各位的意义如下:
注:异步通信是按字符传输的,接收设备在收到起始信号之后只要在一个字符的传输时间内能和发送设备保持同步就能正确接收。
下一个字符起始位的到来又使同步重新校准(依靠检测起始位来实现发送与接收方的时钟自同步的)
关于RS-232、RS-422、RS-485等标准,大家可以参考文章《一篇文章了解什么是串口,UART、RS-232、RS-422、RS-485 》
4. Exynos4412 Uart
本文讨论UART 是基于Cortex-A9架构的Exynos4412 为例。
1)特性
UART include:
每个UART还包括
2)Uart控制器
功能模块
在这里插入图片描述
每个UART包含一个波特率产生器,发送器,接收器和一个控制单元,如上图所示:
UART是以异步方式实现通信的,其采样速度由波特率决定,波特率产生器的工作频率可以由PCLK(外围设备频率),FCLK/n(CPU工作频率的分频),UEXTCLK(外部输入时钟)三个时钟作为输入频率,波特率设置寄存器是可编程的,用户可以设置其波特率决定发送和接收的频率。
发送器和接收器包含了64Byte的FIFO和数据移位器。UART通信是面向字节流的,待发送数据写到FIFO之后,被拷贝到数据移位器(1字节大小)里,数据通过发送数据管脚TXDn发出。
同样道理,接收数据通过RXDn管脚来接收数据(1字节大小)到接收移位器,然后将其拷贝到FIFO接收缓冲区里。
(1)数据发送 发送的数据帧可编程的,它的一个帧长度是用户指定的,它包括一个开始位,5~8个数据位,一个可选的奇偶校验位和1~2个停止位,数据帧格式可以通过设置ULCONn寄存器来设置。发送器也可以产生一个终止信号,它是由一个全部为0的数据帧组成。在当前发送数据被完全传输完以后,该模块发送一个终止信号。在终止信号发送后,它可以继续通过FIFO(FIFO)或发送保持寄存器(NON-FIFO)发送数据。
(2)数据接收 同样接收端的数据也是可编程的,接收器可以侦测到溢出错误奇偶校验错误,帧错误和终止条件,每个错误都可以设置一个错误标志。• 溢出错误 :在旧数据被读取到之前,新数据覆盖了旧数据 • 奇偶校验错误:接收器侦测到了接收数据校验结果失败,接收数据无效 • 帧错误 :接收到的数据没有一个有效的停止位,无法判定数据帧结束 • 终止条件 :RxDn接收到保持逻辑0状态持续长于一个数据帧的传输时间
(3)自动流控AFC(Auto Float Control) UART0和UART1支持有nRTS和nCTS的自动流控。在AFC情况下,通信双方nRTS和nCTS管脚分别连接对方的nCTS和nRTS管脚。通过软件控制数据帧的发送和接收。在开启AFC时,发送端接收发送前要判断nCTS信号状态,当接收到nCTS激活信号时,发送数据帧。该nCTS管脚连接对方nRTS管脚。接收端在准备接收数据帧前,其接收器FIFO有大于32个字节的空闲空间,nRTS管脚会发送激活信号,当其接收FIFO小于32个字节的空闲空间,nRTS必须置非激活状态。
在这里插入图片描述
3)选择时钟源
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 经过上图中的“ UCLK Generator”后,得到UCLK ,它的频率就是UART 的波特率。“ Generator UCLK Generator ”通过这 2个寄存器来设置:UBRDIVn(UART BAUD RATE DIVISOR) 、UFRACVALn 。
4)UART配置寄存器
在这里插入图片描述
ULCONn
该寄存器我们通用的配置是:
- ULCON2 = 0x3; //Normal mode, No parity,One stop bit,8 data bits
UCONn
该寄存器通用配置为:
- UCON2 = 0x5; //Interrupt request or polling mode
一般裸机情况下,采用轮询模式。
UTRSTATn
UTRSTAT n寄存器用来表明数据是否已经发送完毕、是否已经接收到数据,格式如下图所示,上面说的“缓冲区”,其实就是下图中的 FIFO ,不使用 FIFO 功能时可以认为其深度为 1。
当我们读取数据时,就轮询检查bit[0]置1之后,然后再从URXHn寄存器读取数据;当我们读取数据时,就轮询检查bit[1]置1之后,然后再向UTXHn寄存器写入数据来发送数据;
在这里插入图片描述
在这里插入图片描述
UTXHn寄存器(UART TRANSMIT BUFFER REGISTER)
CPU 将数据写入这个寄存器, UART即会将它保存到缓冲区中,并自动发送出去。
URXHn寄存器(UART RECEIVE BUFFER REGISTER)
当 UART 接收到数据时,读取这个寄存器,即可获得数据。
UFRACVALn 计算波特率
在这里插入图片描述
根据给定的波特率、所选择时钟源频率,可以通过以下公式计算 UBRDIVn 寄存器 (n 为 0~4,对应 5个 UART 通道 )的值。
- UBRDIVn = (int)( UART clock / ( buad rate x 16) ) – 1
上式计算出来的 UBRDIVn 寄存器值不一定是整数, UBRDIVn 寄存器取其整数部分,小部分由 UFRACVALn 寄存器设置, UFRACVALn 寄存器的引入,使产生波特率更加精确。「【举例】」当UART clock为100MHz时,要求波特率为115200 bps,则:
- 100000000/(115200 x 16) – 1 = 54.25 – 1 = 53.25
- UBRDIVn = 整数部分 = 53
- UFRACVALn/16 = 小数部分 = 0.25
- UFRACVALn = 4
5)电路图
外设电路图:
在这里插入图片描述
SP3232EEA 用来将TTL电平转换成RS232电平。我们使用的是COM2。
外设与核心板连接电路图
在这里插入图片描述
可见UART的收发引脚连接到了GPA上,打开exynos4412芯片手册:
在这里插入图片描述
我们只需要将GPA1 的低8位设置为0x22。
6.实例代码
裸机代码,主要实现uart_init()、putc()、getc()这三个函数。
uart_init()
该函数主要配置UART的,波特率115200,数据位:8,奇偶校验位:0,终止位:1,不设置流控。
如下图:是运行在windows下常用的串口工具配置信息,配置信息必须完全一致。
在这里插入图片描述
putc()
该函数是向串口发送一个数据data,他的实现逻辑就是轮询检查寄存器UART2.UTRSTAT2 ,判断其bite【1】是否置1,如果置1,则向UART2.UTXH2存入要发送的数据即可。
getc()
该函数是从串口接收一个数据data,他的实现逻辑就是轮询检查寄存器UART2.UTRSTAT2 ,判断其bite【0】是否置1,如果置1,说明数据准备好,则可以从寄存器UART2.URXH2取出数据。
代码
- /*
- * UART2
- */
- typedef struct {
- unsigned int ULCON2;
- unsigned int UCON2;
- unsigned int UFCON2;
- unsigned int UMCON2;
- unsigned int UTRSTAT2;
- unsigned int UERSTAT2;
- unsigned int UFSTAT2;
- unsigned int UMSTAT2;
- unsigned int UTXH2;
- unsigned int URXH2;
- unsigned int UBRDIV2;
- unsigned int UFRACVAL2;
- unsigned int UINTP2;
- unsigned int UINTSP2;
- unsigned int UINTM2;
- }uart2;
- #define UART2 ( * (volatile uart2 *)0x13820000 )
- /* GPA1 */
- typedef struct {
- unsigned int CON;
- unsigned int DAT;
- unsigned int PUD;
- unsigned int DRV;
- unsigned int CONPDN;
- unsigned int PUDPDN;
- }gpa1;
- #define GPA1 (* (volatile gpa1 *)0x11400020)
- void uart_init()
- { /*UART2 initialize*/
- GPA1.CON = (GPA1.CON & ~0xFF ) | (0x22); //GPA1_0:RX;GPA1_1:TX
- UART2.ULCON2 = 0x3; //Normal mode, No parity,One stop bit,8 data bits
- UART2.UCON2 = 0x5; //Interrupt request or polling mode
- //Baud-rate : src_clock:100Mhz
- UART2.UBRDIV2 = 0x35;
- UART2.UFRACVAL2 = 0x4;
- }
- void putc(const char data)
- { while(!(UART2.UTRSTAT2 & 0X2));
- UART2.UTXH2 = data;
- if (data == '\n')
- putc('\r');
- }
- char getc(void)
- { char data;
- while(!(UART2.UTRSTAT2 & 0x1));
- data = UART2.URXH2;
- if ((data == '\n')||(data == '\r'))
- {
- putc('\n');
- putc('\r');
- }else
- putc(data);
- return data;
- }
puts/gets
- void puts(const char *pstr)
- { while(*pstr != '\0')
- putc(*pstr++);
- }
- void gets(char *p)
- { char data;
- while((data = getc())!= '\r')
- { if(data == '\b')
- {p--;
- }
- *p++ = data;
- }
- if(data == '\r')
- *p++ = '\n';
- *p = '\0';
- }
7.如何裸机程序可以支持printf函数
首先看下文件的目录结构:
代码架构
老规矩,关注,后台回复【armprintf】,就可以得到代码。
这里我们只贴出部分文件的代码。
「cpu/start.s」改文件主要是实现异常向量表,实现各个模式的栈初始化
- .text
- .global _start
- _start:
- b reset
- ldr pc,_undefined_instruction
- ldr pc,_software_interrupt
- ldr pc,_prefetch_abort
- ldr pc,_data_abort
- ldr pc,_not_used
- ldr pc,=irq_handler
- ldr pc,_fiq
- _undefined_instruction: .word _undefined_instruction
- _software_interrupt: .word _software_interrupt
- _prefetch_abort: .word _prefetch_abort
- _data_abort: .word _data_abort
- _not_used: .word _not_used
- _irq: .word irq_handler
- _fiq: .word _fiq
- reset:
- ldr r0,=0x40008000
- mcr p15,0,r0,c12,c0,0 @ 协处理器指令设置异常向量表地址
- init_stack:
- ldr r0,stacktop /*get stack top pointer*/
- /********svc mode stack********/
- mov sp,r0
- sub r0,#128*4 /*512 byte for irq mode of stack*/
- /****irq mode stack**/
- msr cpsr,#0xd2
- mov sp,r0
- sub r0,#128*4 /*512 byte for irq mode of stack*/
- /***fiq mode stack***/
- msr cpsr,#0xd1
- mov sp,r0
- sub r0,#0
- /***abort mode stack***/
- msr cpsr,#0xd7
- mov sp,r0
- sub r0,#0
- /***undefine mode stack***/
- msr cpsr,#0xdb
- mov sp,r0
- sub r0,#0
- /*** sys mode and usr mode stack ***/
- msr cpsr,#0x10
- mov sp,r0 /*1024 byte for user mode of stack*/
- b main @跳转到c语言的main函数
- .align 4
- /**** swi_interrupt handler ****/
- /**** irq_handler ****/
- irq_handler:
- sub lr,lr,#4
- stmfd sp!,{r0-r12,lr}
- .weak do_irq @该函数可以没有定义
- bl do_irq @跳转到中断入口
- ldmfd sp!,{r0-r12,pc}^
- stacktop: .word stack+4*512 @定义栈顶
- .data
- stack: .space 4*512 @分配一块栈空间
「lib/printf.c」
该文件主要实现打印函数printf一些格式控制,一些字符串转换算数运算需要借助头文件ctype.h、stdarg.h中一些宏。其中vsprintf 具体的实现我们就不再详解,有兴趣读者自行研究。
- ……
- 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); //调用上一章我们封装的puts函数实现向串口打印书字符串
- }
「main.c」该文件可以直接调用printf()函数来打印信息了。
- void delay_ms(unsigned int num)
- {
- int i,j;
- for(i=num; i>0;i--)
- for(j=1000;j>0;j--)
- ;
- }
- /*
- * 裸机代码,不同于LINUX 应用层, 一定加循环控制
- */
- int main (void)
- {
- int i = 0;
- while (1) {
- printf("aaaaaaaaaaaaa\n");
- delay_ms(500);
- }
- return 0;
- }
「Makefile」
- CROSS_COMPILE = arm-none-eabi-
- NAME =gcd
- CFLAGS=-mfloat-abi=softfp -mfpu=vfpv3 -mabi=apcs-gnu -fno-builtin -fno-builtin-function -g -O0 -c -I ./include -I ./lib
- LD = $(CROSS_COMPILE)ld
- CC = $(CROSS_COMPILE)gcc
- OBJCOPY = $(CROSS_COMPILE)objcopy
- OBJDUMP = $(CROSS_COMPILE)objdump
- OBJS=./cpu/start.o ./driver/uart.o \
- ./driver/_udivsi3.o ./driver/_divsi3.o ./driver/_umodsi3.o main.o ./lib/printf.o
- #=============================================================================#
- all: $(OBJS)
- $(LD) $(OBJS) -T map.lds -o $(NAME).elf
- $(OBJCOPY) -O binary $(NAME).elf $(NAME).bin
- $(OBJDUMP) -D $(NAME).elf > $(NAME).dis
- %.o: %.S
- $(CC) $(CFLAGS) -c -o $@ $<
- %.o: %.s
- $(CC) $(CFLAGS) -c -o $@ $<
- %.o: %.c
- $(CC) $(CFLAGS) -c -o $@ $<
- clean:
- rm -rf $(OBJS) *.elf *.bin *.dis *.o
还记得拉手网吗?曾经辉煌一时、排行第一的团购网站拉手网传出拖欠工资、变相裁...
1. 接口描述 接口请求域名: cvm.tencentcloudapi.com 。 本接口(DescribeImage...
简单垂直条形图 GDP=[12406.8,13908.57,9386.87,9143.64] #绘图 plt.bar(range(4...
如今,数据科学家和数据分析师的需求量很大。而获得大数据认证将给IT人士的职业...
百度云 用AI赋能 驱动金融行业智能化升级?百度云:AI赋能 金融行业智能化升级?百...
本文已经过原作者 Chris Bongers 授权翻译。 复制数组 我们可以使用展开操作符复...
接下来正式开始今天的内容讲解,今天的男主角是计时器 timer。 在实际的应用工程...
做过SEO的人都知道,网站服务器是一个网站正常运营的根本所在,而对于网站优化来...
前言 前两天刚买了个腾讯服务器(CVM),这次登陆上去的时候特别卡,通过top发现负...
中国信息通信研究院(以下简称“中国信通院”)电信网络诈骗治理支撑与服务中心...