ARM裸机学习笔记(六)串口通信

(一)电子通信概念

同步通信 收发双方按照同一时钟节拍工作,( 一般需要发送方给接收方发送信息同时发送时钟信号)
异步通信 发送方和接收方没有统一的时钟节拍、而各自按照自己的节拍工作就叫异步
电平信号 电平信号的传输线中有一个参考电平线(一般是GND),然后信号线上的信号值是由信号线电平和参考电平线的电压差决定。
差分信号 差分信号的传输线中没有参考电平,所有都是信号线。然后1和0的表达靠信号线之间的电压差。
并行接口 并行接口是指数据的各位同时进行传送,其特点是传输速度快,但当传输距离较远、位数又多时,导致了通信线路复杂且成本提高
串行接口 串行接口是指数据一位位地顺序传送,其特点是通信线路简单,只要一对传输线就可以实现双向通信

(二)S5PV210串行通信接口详解

  • 串口的官方名称叫:universal asynchronous reciver and transmitter,通用异步收发器
    英文缩写是uart,中文简称串口。

1.串口的一些高级功能

FIFO模式及其作用 (1)典型的串口设计,发送/接收缓冲区只有1字节,每次发送/接收只能处理1帧数据。这样在单片机中没什么问题,但是到复杂SoC中(一般有操作系统的)就会有问题,会导致效率低下,因为CPU需要不断切换上下文。 (2)解决方案就是想办法扩展串口控制器的发送/接收缓冲区,譬如将发送/接收缓冲器设置为64字节,CPU一次过来直接给发送缓冲区64字节的待发送数据,然后transmitter慢慢发,发完再找CPU再要64字节。但是串口控制器本来的发送/接收缓冲区是固定的1字节长度的,所以做了个变相的扩展,就是FIFO。 (3)FIFO就是first in first out,先进先出。fifo其实是一种数据结构,这里这个大的缓冲区叫FIFO是因为这个缓冲区的工作方式类似于FIFO这种数据结构。
DMA模式及其作用 (1)DMA direct memory access,直接内存访问。DMA本来是DSP中的一种技术,DMA技术的核心就是在交换数据时不需要CPU参与,模块可以自己完成。 (2)DMA模式要解决的问题和上面FIFO模式是同一个问题,就是串口发送/接收要频繁的折腾CPU造成CPU反复切换上下文导致系统效率低下。 (3)传统的串口工作方式(无FIFO无DMA)效率是最低的,适合低端单片机;高端单片机上CPU事物繁忙所以都需要串口能够自己完成大量数据发送/接收。这时候就需要FIFO或者DMA模式。FIFO模式是一种轻量级的解决方案,DMA模式适合大量数据迸发式的发送/接收时。
IrDA模式及其用法 (1)IrDA其实就是红外,红外就是红外线通信(电视机、空调遥控器就是红外通信的)。 (2)红外通信的原理是发送方固定间隔时间向接收方发送红外信号(表示1或0)或者不发送红外信号(表示0或者1),接收方每隔固定时间去判断有无红外线信号来接收1和0. (3)分析可知,红外通信和串口通信非常像,都是每隔固定时间发送1或者0(判断1或0的物理方式不同)给接收方来通信。因此210就利用串口通信来实现了红外发送和接收。 (4)210的某个串口支持IrDA模式,开启红外模式后,我们只需要向串口写数据,这些数据就会以红外光的方式向外发射出去(当然是需要一些外部硬件支持的),然后接收方接收这些红外数据即可解码得到我们的发送信息。

2.串行通信接口的时钟设计

  • (1)串口通信为什么需要时钟?因为串口通信需要一个固定的波特率,所以transmitter和receiver都需要一个时钟信号。
  • (2)时钟信号从哪里来?源时钟信号是外部APB总线(PCLK_PSYS,66MHz)提供给串口模块的(这就是为什么我们说串口是挂在APB总线上的),然后进到串口控制器内部后给波特率发生器(实质上是一个分频器),在波特率发生器中进行分频,分频后得到一个低频时钟,这个时钟就是给transmitter和receiver使用的。
  • (3)串口通信中时钟的设置主要看寄存器设置。重点的有:寄存器源设置(为串口控制器选择源时钟,一般选择为PCLK_PSYS,也可以是SCLK_UART),还有波特率发生器的2个寄存器。
  • (4)波特率发生器有2个重要寄存器:UBRDIVn和UDIVSLOTn,其中UBRDIVn是主要的设置波特率的寄存器,UDIVSLOTn是用来辅助设置的,目的是为了校准波特率的。

(三)串口程序分析

  • 整个串口通信相关程序包含2部分:uart_init负责初始化串口,uart_putc负责发送一个字节

  • 初始化串口的Tx和Rx引脚所对应的GPIO(查原理图可知Rx和Rx分别对应GPA0_1和GPA0_0)

    • GPA0CON(0xE0200000),bit[3:0] = 0b0010 bit[7:4] = 0b0010
  • 初始化这几个关键寄存器UCON0 ULCON0 UMCON0 UFCON0 UBRDIV0 UDIVSLOT0

    UCON0 0x03(0校验位、8数据位、1停止位)
    ULCON0 0x05(发送和接收都是polling mode)
    UMCON0 0x00(禁止modem、afc)
    UFCON0 0x00(禁止FIFO)
    UBRDIV0 波特率计算相关,需要根据情况计算
    UDIVSLOT0 波特率计算相关,需要根据情况计算
  • 波特率的计算和设置

    • (1)第一步,用PCLK_PSYS和目标波特率去计算DIV_VAL: DIV_VAL = (PCLK / (bps x 16)) -1
      • DIV_VAL = 66000000/(115200*16) - 1 = 34.8
      • UBRDIV0 = 34
    • (2)第二步,UBRDIV0寄存器中写入DIV_VAL的整数部分
    • (3)第三步,用小数部分*16得到1个个数,查表得uBDIVSLOT0寄存器的设置值
      • uBDIVSLOT0 = 0.8 * 16 = 13
  • 代码

/*
* 文件名:uart.c
* 描述:配置串口0
*/

#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;  // 发送和接收都是polling mode
	rUMCON0 = 0;   // 禁止modem、afcc
	rUFCON0 = 0;   // 禁止FIFO
	
	// 波特率设置	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 putc(char c)
{                  	
	// 串口发送一个字符,其实就是把一个字节丢到发送缓冲区中去
	// 因为串口控制器发送1个字节的速度远远低于CPU的速度,所以CPU发送1个字节前必须
	// 确认串口控制器当前缓冲区是空的(意思就是串口已经发完了上一个字节)
	// 如果缓冲区非空则位为0,此时应该循环,直到位为1
	while (!(rUTRSTAT0 & (1<<1)));
	rUTXH0 = c; //rUTXH0: 串口0 发送缓冲寄存器
}

// 串口接收程序,轮询方式,接收一个字节
char getc(void)
{
	while (!(rUTRSTAT0 & (1<<0)));
	return (rURXH0 & 0x0f);
}

(四)移植uart stdio

  • 什么是stdio

    • (1)#include <stdio.h>
    • (2)stdio:standard input output,标准输入输出
    • (3)标准输入输出就是操作系统定义的默认的输入和输出通道。一般在PC机的情况下,标准输入指的是键盘,标准输出指的是屏幕。
    • (4)printf函数和scanf函数可以和底层输入/输出函数绑定,然后这两个函数就可以和stdio绑定起来。也就是说我们直接调用printf函数输出,内容就会被从标准输出输出出去。
    • (5)在我们这里,标准输出当然不是屏幕了,而是串口。标准输出也不是键盘,而是串口。
  • printf函数的工作原理
    (1)printf函数工作时内部实际调用了2个关键函数:

    • 一个是vsprintf函数(主要功能是格式化打印信息,最终得到纯字符串格式的打印信息等待输出),
    • 另一个就是真正的输出函数putc(操控标准输出的硬件,将信息发送出去)
  • 移植printf函数的三种思路

    • (1)我们希望在我们的开发板上使用printf函数进行(串口)输出,使用scanf函数进行(串口)输入,就像在PC机上用键盘和屏幕进行输入输出一样。因此需要移植printf函数/scanf函数
    • (2)我们说的移植而不是编写,我们不希望自己完全从新编写而是想尽量借用也有的代码(叫移植)
    • (3)一般移植printf函数可以有3个途径获取printf的实现源码:最原始最原本的来源就是linux内核中的printk。难度较大、关键是麻烦;稍微简单些的方法是从uboot中移植printf;更简单的方法就是直接使用别人移植好的。
  • 我们课程中使用第三种方法,别人移植好的printf函数来自于友善之臂的Tiny210的裸机教程中提供的。

  • 在移植后的uart stdio项目中添加link.lds链接脚本,指定连接地址到0xd0020010

  • gcc可变参数及va_arg介绍

    扫描二维码关注公众号,回复: 10525456 查看本文章
    • printf函数中首先使用了C语言的可变参数va_start/va_arg/va_end;
  • vsprintf函数

    • vsprintf函数的作用是按照我们的printf传进去的格式化标本,对变参进行处理,然后将之格式化后缓存在一个事先分配好的缓冲区中。printf后半段调用putc函数将缓冲区中格式化好的字符串直接输出到标准输出。
#
#文件名:makefile
#描述:移植uart stdio时 makefile 做了一些修改
#

CC		= arm-linux-gcc
LD 		= arm-linux-ld
OBJCOPY	= arm-linux-objcopy
OBJDUMP	= arm-linux-objdump
AR		= arm-linux-ar

INCDIR	:= $(shell pwd)
# C预处理器的flag,flag就是编译器可选的选项
CPPFLAGS	:= -nostdlib -nostdinc -I$(INCDIR)/include
# C编译器的flag
CFLAGS		:= -Wall -O2 -fno-builtin  
# -Wall是所有的警告信息都要提示,这样编译出来的程序很健壮;
# -O2表示编译时使用2级优化

#导出这些变量到全局,其实就是给子文件夹下面的Makefile使用
export CC LD OBJCOPY OBJDUMP AR CPPFLAGS CFLAGS


objs := start.o sdram_init.o led.o uart.o main.o
#objs += clock.o
objs += lib/libc.a # 添加lib文件夹下的makefile依赖
uart.bin: $(objs)
	$(LD) -Tlink.lds -o uart.elf $^
	$(OBJCOPY) -O binary uart.elf uart.bin
	$(OBJDUMP) -D uart.elf > uart_elf.dis
	gcc mkv210_image.c -o mkx210
	./mkx210 uart.bin 210.bin

# 调用子makefile
lib/libc.a:
	cd lib;	make;	cd ..

%.o : %.S
	$(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< -c

%.o : %.c
	$(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< -c

clean:
	rm *.o *.elf *.bin *.dis mkx210 -f
# 进入子makefile清除子makefile 执行 makeclean
	cd lib; make clean; cd .. 

发布了21 篇原创文章 · 获赞 6 · 访问量 420

猜你喜欢

转载自blog.csdn.net/weixin_44112805/article/details/104941408
今日推荐