嵌入式知识-ARM裸机-学习笔记(6):串口通信详解与基于S5PV210的uartstdio移植

嵌入式知识-ARM裸机-学习笔记(6):串口通信详解与基于S5PV210的uartstdio移植

一、串口通信原理

1. 串口通信基本概念

通信中最重要的三个方面: 信息表示(编码)、解析方法(解码)、信息的传输方法。通信双方事先需要约定好信息的表示方法和解析方法,做到一致,否则信息不能有效传递。信号的传输方法是指经过编码后的通信信息如何在传输介质上传输的过程,传输方法与编解码方法无关。
在这里插入图片描述
通信的过程:首先发送方先按照信息编码方式对有效信息进行编码(编程成可以在通信线路上传输的信号形态),编码后的信息在传输介质上进行传输,输送给接收方;最后接收方接收到编码信息后进行解码,解码后得到可以理解的有效信息。

同步通信和异步通信
在这里插入图片描述
(1)同步通信:通信双方按照统一节拍工作,一般需要发送方给接收方发送信息同时发送时钟信号,接收方根据发送方给它的时钟信号来安排自己的节拍。(同步通信用在通信双方信息交换频率固定,或者经常通信时)
(2)异步通信:又叫异步通知。异步通信时接收方不必一直等待发送方,发送方需要发送信息时会首先给接收方一个信息开始的起始信号,接收方接收到起始信号后就认为后面紧跟着的就是有效信息,才会开始注意接收信息,直到收到发送方发过来的结束标志(异步通信用在双方通信的频率不固定时)
(3)同步和异步的区别:发送方和接收方按照同一个时钟节拍工作就叫同步,发送方和接收方没有统一的时钟节拍、而各自按照自己的节拍工作就叫异步,异步一般带有一根时钟线,负责传输时钟,控制接收方的节拍(大部分情况下的判断依据)

电平信号和差分信号:
(1)电平信号和差分信号是用来描述通信线路传输方式的,即通过什么方式在通信线路上来表达逻辑0或1。
(2)电平信号的传输线中有一个参考电平线(一般是GND),通过信号线上的信号电平和参考电平线的电压差决定信息表达是0或1。这种方式的传播一般容易受到外界干扰。
(3)差分信号的传输线中没有参考电平,所有都是信号线。通过信号线之间的电压差来表达逻辑0或1。由于两条信号线受到干扰环境的影响基本相同,因此两者之间受到的干扰可以在通过电压差计算时相互抵消。

串行通信和并行通信:
(1)串行、并行主要是考虑通信线的根数,就是发送方和接收方同时可以传递的信息量的多少。例如同时发送8位二进制数,对于电平信号,需要9根线(8根信号线+1根参考线);对于差分信号,需要16根线(2根组成1对,一共8对)。
(2)在实际使用时,串行接口应用更广泛,因为更省信号线,而且对传输线的要求更低、成本更低;而且串行时可以通过提高通信速度来提高总体通信性能,不一定非得要并行。2条线的串行通信方式每次只能传输1个二进制位。
最常用的搭配方式为:异步、串行、差分,譬如USB和网络通信。

波特率(bandrate):
指的是串口通信的速率,也就是串口通信时每秒钟可以传输多少个二进制位。譬如每秒种可以传输9600个二进制位(传输一个二进制位需要的时间是1/9600秒,也就是104us),波特率就是9600。一般最常见的波特率是9600或者115200(低端单片机如51常用9600,高端单片机和嵌入式SoC一般用115200)。通信双方必须事先设定相同的波特率这样才能成功通信,如果发送方和接收方按照不同的波特率通信则根本收不到。

起始位、数据位、奇偶校验位、停止位
在这里插入图片描述
(1)串口通信时,收发是一个周期一个周期进行的,每周期传输n个二进制位。这一个周期就叫做一个通信单元,一个通信单元是由:起始位+数据位+奇偶校验位+停止位组成的(又叫做一帧)
(2)起始位表示发送方要开始发送一个通信单元,起始位是串口通信标准事先指定的,是由通信线上的电平变化来反映的,也就是时序数据位是一个通信单元中发送的有效信息位,也就是本次通信真正要发送的有效数据,大部分情况下数据位是8位,因为通过串口发送的文字信息都是ASCII码编码的,而ASCII码中一个字符刚好编码为8位。奇偶校验位是用来校验数据位,把待校验的有效数据逐个位的加起来,总和为奇数奇偶校验位就为1,总和为偶数奇偶校验位就为0。停止位是发送方用来表示本通信单元结束标志的,一般为1位。
总结:串口通信时因为是异步通信,所以通信双方必须事先约定好通信参数,这些通信参数包括:波特率、数据位、奇偶校验位、停止位(串口通信中起始位定义是唯一的,所以一般不用选择)。

通信方式(单工、双工、半双工)
(1)单工就是单方向传输,表示只能A发B收。
(2)双工就是双方同时收发,A发B收的同时也能B发A收。
(3)半双工就是只能单方向但是方向可以改变,A发B收或者B发A收(两个方向不能同时)。

2. 常见的电平标准

TTL电平
供电范围在0~5V。
对输出:大于2.4V是高电平;小于0.4V是低电平。
对输入:大于2V是高电平;小于0.8V是低电平。

在这里插入图片描述
RS232
对输出:输出“1”时的电平应在-3~-15 V之间,输出“0”时的电平应在+3~+15 V之间。
对输入:输入电平在-3~-15 V之间被认为“1”,在+3~+15 V之间被认为“0”。
当线路上不传送数据(空闲)时,发送器输出为“1”。
双向传输,全双工通信,最高传输速率20kbps。

RS485
对输出:逻辑"1"以两线间的电压差为+2v ~ +6表示;逻辑"0"以两线间的电压差为-2V ~ -6V 表示。
对输入:A比B高200mV以上即认为是逻辑"1",A 比B 低200mV 以上即认为是逻辑"0"。
双向差分传输,半双工通信,最高传输速率10Mbps。

RS422
与RS485的电平标准相同,发送口与接收口不同,如若将其并连就变成了RS485。
相当于两个半双工的RS485构成了一个全双工通信,最高速率10Mbps。

3. 串口通信基本原理

通信线(RX TX GND)
(1)任何通信都要有信息传输载体,或者是有线的或者是无线的。串口通信是有线通信,是通过串口线来通信的。
(2)串口通信线最少需要2根(GND和信号线),可以实现单工通信,也可以使用3根通信线(Tx、Rx、GND)来实现全双工。一般开发板都会引出SoC上串口引脚直接输出的TTL电平的串口(X210开发板没有),插座用插针式插座,每个串口引出的都有3个线(Tx、Rx、GND),可以用这些插座直接连接外部的TTL电平的串口设备。

信息在信道上传输方式
串口通信的发送方每隔一定时间(时间固定为1/波特率,单位是秒)将有效信息(1或者0)放到通信线上去,逐个二进制位的进行发送。
接收方通过定时(起始时间由读到起始位标志开始,间隔时间由波特率决定)读取通信线上的电平高低来区分发送给我的是1还是0。依次读取数据位、奇偶校验位、停止位,停止位就表示这一个通信单元(帧)结束,然后中间是不定长短的非通信时间(发送方有可能紧接着就发送第二帧,也可能半天都不发第二帧,这就叫异步通信),接着发送第二帧·····

FIFO模式及其作用
对于典型的串口设计,发送/接收缓冲区只有1字节,因此每次发送/接收时只能处理1帧数据。在复杂的SoC中CPU的时钟远高于串口发送端,会导致CPU需要不断切换上下文(每发完1帧数据就需要重新切换回发送端),大大降低了速率。
如何像icache一样提供一个解决两者速率差异较大的方法?解决方案就是想办法扩展串口控制器的发送/接收缓冲区,例如将发送/接收缓冲器设置为64字节,CPU一次过来直接给发送缓冲区64字节的待发送数据,然后transmitter慢慢发,发完再找CPU再要64字节。但是串口控制器本来的发送/接收缓冲区是固定的1字节长度的,所以做了个变相的扩展,就是FIFO(first in first out),先进入缓冲区的先出来,从而不影响顺序
CPU来一次直接给FIFO了64字节的内容,然后FIFO一个字节一个字节的给发送缓冲区,此时就不需要CPU的参与,大大提高了效率。

DMA模式及其作用
DMA direct memory access,直接内存访问。 DMA本来是DSP中的一种技术,DMA技术的核心就是在交换数据时不需要CPU参与,模块可以自己完成。DMA模式要解决的问题和上面FIFO模式是同一个问题,就是串口发送/接收要频繁的折腾CPU造成CPU反复切换上下文导致系统效率低下。
传统的串口工作方式(无FIFO无DMA)效率是最低的,适合低端单片机;高端单片机上CPU事物繁忙所以都需要串口能够自己完成大量数据发送/接收。这时候就需要FIFO或者DMA模式。FIFO模式是一种轻量级的解决方案,DMA模式适合大量数据迸发式的发送/接收时。

二、S5PV210串口配置过程

1. 配置前的分析

在这里插入图片描述
首先说明一下为什么串口叫UART,universal asynchronous reciver and transmitter,通用异步收发器,即可知UART的通信方式是异步的。

(1)从图中可以看出,整个串口控制器包含transmitterreceiver两部分,两部分功能彼此独立,transmitter负责210向外部发送信息,receiver负责从外部接收信息到210内部。
(2)从之前的时钟部分可知,串口控制器是接在APB总线上的。对我们编程有影响的是:将来计算串口控制器的源时钟时是以APB总线来计算的。
(3)transmitter由发送缓冲区和发送移位器构成。 在发送信息时,首先将信息进行编码成二进制流,然后将一帧数据写入发送缓冲区,发送移位器会自动从发送缓冲区中读取一帧数据,然后自动移位(移位的目的是将一帧数据的各个位分别拿出来)将其发送到TX(发送端)通信线上。
(4)receiver由接收缓冲区和接收移位器构成。 当有人通过串口线向我发送信息时,信息通过RX(接收端)通信线进入我的接收移位器,然后接收移位器自动移位将该二进制位保存到我的接收缓冲区,接收完一帧数据后receiver会产生一个中断给CPU,CPU收到中断后即可知道receiver接收满了一帧数据,就会来读取这帧数据。
总结:发送缓冲区和接收缓冲区是关键。发送移位器和接收移位器的工作都是自动的,不用编程控制的,所以我们写串口的代码就是:首先初始化串口控制器(初始化的实质是读写寄存器,包括发送控制器和接收控制器),然后将要发送信息时直接写入发送缓冲区,要接收信息时直接去接收缓冲区读取即可。

串口通信分为发送/接收2部分。发送方一般不需要(也可以使用)中断即可完成发送,接收方必须(一般来说必须,也可以轮询方式接收)使用中断来接收。本实验采用轮询的方式接收,通过状态寄存器中有一个位叫发送缓冲区空标志,transmitter发送完成(发送缓冲区空了)就会给这个标志位置位,CPU就是通过不断查询这个标志位为1还是0来知道发送是否已经完成的。

2. 时钟分析

从图中可以看出波特率的产生需要时钟的提供(Clock Source),所以transmitter和receiver都需要一个时钟信号。
由上述可得,源时钟信号是外部APB总线(PCLK_PSYS,66MHz)提供给串口模块的,然后进到串口控制器内部后给波特率发生器(实质上是一个分频器),在波特率发生器中进行分频,分频后得到一个低频时钟,这个时钟就是给transmitter和receiver使用的。

3. 编程详解

C语言代码(uart.c文件):

#define GPA0CON		0xE0200000
#define UCON0 		0xE2900004
#define ULCON0 		0xE2900000
#define UMCON0 		0xE290000C
#define UFCON0 		0xE2900008
#define UBRDIV0 	0xE2900028
#define UDIVSLOT0	0xE290002C
#define UTRSTAT0	0xE2900010
#define UTXH0		0xE2900020	
#define URXH0		0xE2900024	

#define rGPA0CON	(*(volatile unsigned int *)GPA0CON)
#define rUCON0		(*(volatile unsigned int *)UCON0)
#define rULCON0		(*(volatile unsigned int *)ULCON0)
#define rUMCON0		(*(volatile unsigned int *)UMCON0)
#define rUFCON0		(*(volatile unsigned int *)UFCON0)
#define rUBRDIV0	(*(volatile unsigned int *)UBRDIV0)
#define rUDIVSLOT0	(*(volatile unsigned int *)UDIVSLOT0)
#define rUTRSTAT0		(*(volatile unsigned int *)UTRSTAT0)
#define rUTXH0		(*(volatile unsigned int *)UTXH0)
#define rURXH0		(*(volatile unsigned int *)URXH0)

// 串口初始化程序
void uart_init(void)
{
	// 初始化Tx Rx对应的GPIO引脚
	rGPA0CON &= ~(0xff<<0);			// 把寄存器的bit0~7全部清零
	rGPA0CON |= 0x00000022;			// 0b0010, Rx Tx
	
	// 几个关键寄存器的设置
	rULCON0 = 0x3;	//0校验位,8数据位,1停止位
	rUCON0 = 0x5;	//轮询模式
	rUMCON0 = 0;
	rUFCON0 = 0;
	
	// 波特率设置	DIV_VAL = (PCLK / (bps x 16))-1
	// PCLK_PSYS用66MHz算		余数0.8
	//rUBRDIV0 = 34;	
	//rUDIVSLOT0 = 0xdfdd;
	
	// PCLK_PSYS用66.7MHz算		余数0.18
	// DIV_VAL = (66700000/(115200*16)-1) = 35.18
	rUBRDIV0 = 35;
	// (rUDIVSLOT中的1的个数)/16=上一步计算的余数=0.18
	// (rUDIVSLOT中的1的个数 = 16*0.18= 2.88 = 3
	rUDIVSLOT0 = 0x0888;		// 3个1,查官方推荐表得到这个数字
}

// 串口发送程序,发送一个字节
void uart_putc(char c)
{                  	
	// 串口发送一个字符,其实就是把一个字节丢到发送缓冲区中去
	// 因为串口控制器发送1个字节的速度远远低于CPU的速度,所以CPU发送1个字节前必须
	// 确认串口控制器当前缓冲区是空的(意思就是串口已经发完了上一个字节)
	// 如果缓冲区非空则位为0,此时应该循环,直到位为1
	while (!(rUTRSTAT0 & (1<<1)));
	rUTXH0 = c;
}

// 串口接收程序,轮询方式,接收一个字节
char uart_getc(void)
{
	while (!(rUTRSTAT0 & (1<<0)));
	return (rURXH0 & 0x0f);	//返回这个值得0到7bit位
}

整个串口通信主要由3个函数组成:uart_init负责初始化串口,uart_putc负责发送一个字节,uart_getc负责接收一个字节。

(1)uart_init()函数用来初始化串口,其中包括引脚的选择以及一些关键参数的设置:
在这里插入图片描述
通过原理图可以得到,若想使用串口UART0,即需要设置收发引脚RXD0/TXD0,对应SoC引脚为GPA0_0和GPA0_1,找到对应的寄存器GPA0CON(Address = 0xE020_0000)。
在这里插入图片描述
通过设置GPA0CON寄存器的[7:0]为0x22,即可配置UART0的输入输出端口。

接着需要设置数据的接口模式、数据位、校验方式、停止位,找到对应的寄存器ULCON0(Address = 0xE290_0000)。
在这里插入图片描述
通过设置ULCON0寄存器的[6:0]为0x3,即将串口配置为:普通模式、无校验、1个停止位、8个数据位。

接着需要设置传输方式,找到对应的寄存器UCON0(Address = 0xE290_0004)。
在这里插入图片描述
通过设置UCON0寄存器的[5:0]为0x5,即将串口配置为:PCLK为时钟源,中断全关,polling mode(轮询模式)。

设置控制流控,找到对应的寄存器UMCON0(Address = 0xE290_000C),将其设置为0x0全部禁止。

设置是否使用FIFO,找到对应的寄存器UFCON0(Address = 0xE290_0008),将其设置为0x0全部禁止。

最后是设置波特率,找到对应寄存器UBRDIV0(Address = 0xE290_0028)和UDIVSLOT0(Address = E290_002C)。
在这里插入图片描述
在这里插入图片描述
已知PCLK=66.7Mhz,根据公式可得DIV_VAL = (66700000/(11520016)-1) = 35.18,即为35;UDIVSLOT中的1的个数 = 160.18= 2.88 = 3,查找3对应的码为0x0888,波特率配置完成。

(2)uart_putc()函数用来发送数据,通过调用来发送一字节的内容。其实就是向发送端缓冲区写内容,发送端缓冲区寄存器为UTXH0(Address = 0xE290_0020),一次写一个字节,因为是char型。
在这里插入图片描述
但是要注意,由于CPU的速度远高于串口,因此CPU给缓冲区发完数据以后就去干别的工作了,所以需要一个标志来表示缓冲区已发送完毕。这就需要用到缓冲区状态寄存器UTRSTAT0(Address = 0xE290_0010),当发送端缓冲区为1时为空,因此可以设置一个条件,当UTRSTAT0=0x10时,则请求CPU发送下一字节。
在这里插入图片描述
(3)uart_getc()函数的原理与uart_putc()类似,这里不再赘述。

main.c文件:

void main(void)
{
	uart_init();
	
	while(1)
	{
		uart_putc('b');
		delay();		//这里的delay函数来自于led.c的延时函数
		
	}
}

通过调用uart_putc函数来一直发字符b。

start.s文件:
在这里插入图片描述
在这里调用main.c文件中的main函数。

Makefile文件中添加uart.c文件和main.c文件:
在这里插入图片描述

三、S5PV210的uartstdio移植过程

由于我们第二章设计出的putc函数只能打印1个字符,而我们想通过串口实现printf打印功能,这就用到了文件系统移植的部分。

1. 移植前的准备

首先介绍一下要移植的头文件stdio(standard input output,标准输入输出),在使用时要通过 #include <stdio.h> 来实现头文件的包含。标准输入输出就是操作系统定义的默认的输入和输出通道。一般在PC机的情况下,标准输入指的是键盘,标准输出指的是屏幕。
printf函数和scanf函数可以和底层输入/输出函数绑定,然后这两个函数就可以和stdio绑定起来。也就是说我们直接调用printf函数输出,内容就会被从标准输出出去。
我们希望在我们的开发板上使用printf函数进行(串口)输出,使用scanf函数进行(串口)输入,就像在PC机上用键盘和屏幕进行输入输出一样。即将标准输入输出改为串口。

移植思路: 通过别人移植好的printf函数来移植到我们所需的板卡上。
在这里插入图片描述
在这里插入图片描述
我们要做的就是将别人编写好的printf函数中的putc()函数与我们编写的驱动串口打印的uart_putc()函数相关联起来,即可实现通过printf实现串口打印。

2. 移植过程

在lib文件夹中,我们可以看到有一个Makefile,这个Makefile即是用来联系lib文件夹中的各文件,因此我们需要通过自己的Makefile文件来支持这个子Makefile文件
介绍Makefile文件:
在这里插入图片描述
(1)通过子Makefile文件内容可以发现,它通过${CC}的形式来调用编译器,因此我们需要将我们的编译器进行名字的简化(统一化),再通过export将其导出,以使得外部的文件可以调用,这样在子Makefile中即可实现交叉编译工具的调用
(2)通过objs := 的方式实现文件名的统一管理,在之后只需 $(obj)的方式,代表之间的所有文件。
(3)在母Makefile文件中需要对子Makefile进行包含,首先需要objs += lib/libc.a 使得子Makefile中的规则libc.a成为生成uart.bin的一个条件,然后我们还需要执行该规则的执行方式为cd lib; make; cd … (cd lib表示进入到lib文件夹中,make表示编译该目录下的Makefile,cd …退回到之前的目录)。

(4)CPPFLAGS是C预编译器的flag,它包含的可选项有 -nostdlib (不使用标准库,stdio.h使用我们自己的); -nostdinc(不用标准的头文件) ;-I$(INCDIR)/include (指定头文件目录,这里的INCDIR表示Makefile所在的当前目录,即当前目录显得include文件夹中)。
(5)CFLAGS 是C编译器的flag,它包含的可选项有 -Wall (警告信息);-O2 (编译器的优化选项,2是一个等级);-fno-builtin (全用我们现有的文件不用标准的)。
(6)最后要在clean中包括对子Makefile的管理。

介绍main.c文件:
在这里插入图片描述
此时通过printf即可实现串口打印,接下来即需要将uart_putc()函数与putc()函数进行绑定。

介绍uart.c文件:
在这里插入图片描述
最终我们移植完成,uart.c文件中的putc函数被printf.c文件所调用,因此在printf.c文件中,执行打印函数print时,即将信息发送到了串口的输出缓冲区中;main.c文件通过调用printf函数实现打印功能。母Makefile文件调用子Makefile文件,两者各自管理所涉及范围,从而实现协同编译,即完成了文件系统移植。

在ubuntu中进行编译:
在这里插入图片描述
将SecureCRT中的波特率设置与UART0串口相同,将bin文件烧录到板卡中,即可实现main.c文件中的打印信息。
在这里插入图片描述

发布了65 篇原创文章 · 获赞 83 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/qq_42826337/article/details/104612225