串口通信的基本原理
单工通信和双工通信
(1)单工就是单方向,双工就是双方同时收发,同时只能但方向但是方向可以改变叫半双工
(2)如果只能A发B收则单工,A发B收或者B发A收(两个方向不能同时)叫半双工,A发B收同时B发A收叫全双工。
三根通信线:Rx Tx GND
(1)任何通信都要有信息传输载体,或者是有线的或者是无线的。
(2)串口通信是有线通信,是通过串口线来通信的。
(3)串口通信线最少需要2根(GND和信号线),可以实现单工通信,也可以使用3根通信线(Tx、Rx、GND)来实现全双工。
(4)一般开发板都会引出SoC上串口引脚直接输出的TTL电平的串口(X210开发板没有),插座用插针式插座,每个串口引出的都有3个线(Tx、Rx、GND),可以用这些插座直接连接外部的TTL电平的串口设备。
收发双方事先规定好通信参数(波特率、数据位、奇偶校验位、停止位等)
(1)串口通信属于基层基本性的通信规约,它自己本身不会去协商通信参数,需要通信前通信双方事先约定好通信参数(一般4个最重要的)
(2)串口通信的任何一个关键参数设置错误,都会导致通信失败。譬如波特率调错了,发送方发送没问题,接收方也能接收,但是接收到全是乱码···
S5PV210的串口控制器工作原理框图
《S5PV210_UM_REV1.1》853页
(1)S5PV210的数据手册中串口控制器在section8.1
(2)串口的官方名称叫:universal asynchronous reciver and transmitter,通用异步收发器
英文缩写是uart,中文简称串口。
将来计算串口控制器的源时钟时是以APB总线来计算的。
整个串口控制器包含transmitter和receiver两部分,两部分功能彼此独立,transmitter负责210向外部发送信息,receiver负责从外部接收信息到210内部。
Transmitter(发送器)
组成:由发送缓冲区和发送移位器两部分构成。
工作流程优化描述:
信息编码:首先,待发送的信息被高效编码为ASCII码(或根据需求选择其他适合的编码方式)形式的二进制流。
缓冲区写入:随后,这一串二进制流被组织成帧,每帧通常包含8位(或其他预定义长度),并自动写入发送缓冲区。此过程无缝衔接,确保数据流的连续性。
自动发送:一旦数据被安全存入缓冲区,发送移位器即自动介入,无需CPU额外干预。它逐位地从缓冲区中读取数据帧,并通过精心设计的移位机制将每位数据依次发送到Tx(发送)通信线上。这一过程极大减轻了CPU的负担,提高了数据传输的效率和稳定性。
Receiver(接收器)
组成:由接收缓冲区和接收移位器两部分组成。
工作流程优化描述:
数据接收:当外部设备通过串口Rx(接收)通信线发送数据时,这些数据首先被接收移位器捕获。移位器迅速而准确地处理每一位进入的数据,通过内部移位操作将它们整理成有序的二进制位序列。
缓冲存储:整理好的二进制位序列被自动存储到接收缓冲区中,直到一帧完整的数据被接收完毕。此过程确保了数据的完整性和准确性。
中断通知:一旦一帧数据被完全接收并存储在缓冲区中,接收器会立即生成一个中断信号给CPU。这一设计允许CPU在需要时才介入处理数据,从而实现了资源的有效分配和系统的低功耗运行。CPU在接收到中断后,将响应该信号,从接收缓冲区中读取数据帧,进行后续处理。
串口控制器中有一个波特率发生器,作用是产生串口发送/接收的节拍时钟。波特率发生器其实就是个时钟分频器,它的工作需要源时钟(APB总线来),然后内部将源时钟进行分频(软件设置寄存器来配置)得到目标时钟,然后再用这个目标时钟产生波特率(硬件自动的)。
基本知识
自动流控(AFC:Auto flow control)是一种在数据传输过程中用于控制数据流速和确保数据完整性的机制。它主要用于串口通信中,特别是在发送方速率可能超过接收方处理能力的情况下,通过自动调整数据传输的速率来防止数据丢失。
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模式,开启红外模式后,我们只需要向串口写数据,这些数据就会以红外光的方式向外发射出去(当然是需要一些外部硬件支持的),然后接收方接收这些红外数据即可解码得到我们的发送信息。
串行通信与中断的关系
(1)串口通信分为发送/接收2部分。发送方一般不需要(也可以使用)中断即可完成发送,接收方必须(一般来说必须,也可以轮询方式接收)使用中断来接收。
(2)发送方可以选择使用中断,也可以选择不使用中断。使用中断的工作情景是:发送方先设置好中断并绑定一个中断处理程序,然后发送方丢一帧数据给transmitter,transmitter发送耗费一段时间来发送这一帧数据,这段时间内发送方CPU可以去做别的事情,等transmitter发送完成后会产生一个TXD中断,该中断会导致事先绑定的中断处理程序执行,在中断处理程序中CPU会切换回来继续给transmitter放一帧数据,然后CPU切换离开;不使用中断的工作情景是:发送方事先禁止TXD中断(当然也不需要给相应的中断处理程序了),发送方CPU给一帧数据到transmitter,然后transmitter耗费一段时间来发送这帧数据,这段时间CPU在这等着(CPU没有切换去做别的事情),待发送方发送完成后CPU再给它一帧数据继续发送直到所有数据发完。CPU是怎么知道transmitter已经发送完了?原来是有个状态寄存器,状态寄存器中有一个位叫发送缓冲区空标志,transmitter发送完成(发送缓冲区空了)就会给这个标志位置位,CPU就是通过不断查询这个标志位为1还是0来指导发送是否已经完成的。
(3)因为串口通信是异步的,异步的意思就是说发送方占主导权。也就是说发送方随时想发就能发,但是接收方只有时刻等待才不会丢失数据。所以这个差异就导致发送方可以不用中断,而接收方不得不使用中断模式。
210串行通信接口的时钟设计
(1)串口通信为什么需要时钟?因为串口通信需要一个固定的波特率,所以transmitter和receiver都需要一个时钟信号。
(2)时钟信号从哪里来?源时钟信号是外部APB总线(PCLK_PSYS,66MHz)提供给串口模块的(这就是为什么我们说串口是挂在APB总线上的),然后进到串口控制器内部后给波特率发生器(实质上是一个分频器),在波特率发生器中进行分频,分频后得到一个低频时钟,这个时钟就是给transmitter和receiver使用的。
(3)串口通信中时钟的设置主要看寄存器设置。重点的有:寄存器源设置(为串口控制器选择源时钟,一般选择为PCLK_PSYS,也可以是SCLK_UART),还有波特率发生器的2个寄存器。
(4)波特率发生器有2个重要寄存器:UBRDIVn和UDIVSLOTn,其中UBRDIVn是主要的设置波特率的寄存器,UDIVSLOTn是用来辅助设置的,目的是为了校准波特率的。
代码实现
始化串口的Tx和Rx引脚所对应的GPIO(查原理图可知Rx和Rx分别对应GPA0_1和GPA0_0)
主要的几个寄存器
(1)ULCON0(ULCON0, R/W, Address = 0xE290_0000) = 0x3 // 0校验位、8数据位、1停止位
(2)UCON0(R/W, Address = 0xE290_0004) = 0x5 // 发送和接收都是polling mode
(3)UMCON0(UMCON0, R/W, Address = 0xE290_000C) = 0x0 // 禁止modem、afc
(4)UFCON0(UFCON0, R/W, Address = 0xE290_0008) = 0x0 // 禁止FIFO模式
(5)UBRDIV0(UBRDIV0, R/W, Address = 0xE290_0028)和UDIVSLOT0(UDIVSLOT0, R/W, Address = 0xE290_002C)和波特率有关,要根据公式去算的
#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
GPA0CON:
用于配置端口A的第0组引脚的功能(如输入、输出、特殊功能等)。
UCON0:
UART控制寄存器0的地址。这个寄存器用于控制UART0的基本操作,如设置UART的模式(如中断模式、轮询模式)、设置波特率生成器、使能UART接收/发送等。
ULCON0:
UART线路控制寄存器0的地址。这个寄存器用于设置UART的线路控制特性,如数据位长度、停止位数量、奇偶校验位等。
UMCON0:
UART调制控制寄存器0的地址。这个寄存器可能用于控制UART的流控制(如软件流控制RTS/CTS)或其他调制相关的功能。
UFCON0:
UART FIFO控制寄存器0的地址。FIFO(先进先出)是一种用于缓冲数据的队列结构,这个寄存器用于控制UART的FIFO缓冲区,如使能/禁用FIFO、设置FIFO触发级别等。
UBRDIV0:
UART波特率除数寄存器0的地址。这个寄存器用于设置UART的波特率除数,通过调整这个值,可以调整UART通信的波特率。
UDIVSLOT0:
UART除法槽寄存器0的地址。这个寄存器可能用于更精细地调整UART的波特率,特别是在需要非常精确的波特率设置时。
UTRSTAT0:
UART传输/接收状态寄存器0的地址。这个寄存器提供了UART传输和接收操作的状态信息,如是否有数据可读、发送缓冲区是否空等。
UTXH0:
UART发送缓冲区寄存器0的地址。这个寄存器用于存放要发送的数据,当UART发送器准备好时,它会从这个寄存器中读取数据并发送出去。
URXH0:
UART接收缓冲区寄存器0的地址。这个寄存器用于存放接收到的数据,当UART接收器接收到数据时,它会将数据存放在这个寄存器中,供CPU读取。
波特率的计算
DIV_VAL = (PCLK / (bps x 16)) −1
余数决定UDIVSLOTn
0.816 = 12.8 选 0xDFDD
0.1816 =2.88 选0x0888
//初始化
void uart_init(void)
{
// 初始化 Tx Rx 对应的引脚
rGPA0CON &= ~(0xff<<0); //把寄存器的bit0~7全部清零
rGPA0CON |= 0x00000022; // 0b0010, Rx Tx
// 关键寄存器配置
rULCON0 = 0x3;// 0校验位、8数据位、1停止位
rUCON0 = 0x5;// 发送和接收都是polling mode
rUMCON0 = 0;// 禁止modem、afc
rUFCON0 = 0;// 禁止FIFO模式
// 波特率公式 DIV_VAL = (PCLK / (bps x 16))-1
// PCLK_PSYS=66MHz 余数0.8
// DIV_VAL = (66000000/(115200*16)-1) = 34.80
//rUBRDIV0 = 34;
//rUDIVSLOT0 = 0xdfdd;//0.8*16 = 12.8 选 0xDFDD
// PCLK_PSYS=66.7MHz 余数0.18
// DIV_VAL = (66700000/(115200*16)-1) = 35.18
rUBRDIV0 = 35;
//例子 (num of 1's in UDIVSLOTn)/16 = 0.7 -》UDIVSLOTn= 余数*19=6按照表格找值
rUDIVSLOT0 = 0x0888; // 0.18*16 =2.88 选0x0888
}
波特率的计算和设置
(1)第一步,用PCLK_PSYS和目标波特率去计算DIV_VAL: DIV_VAL = (PCLK / (bps x 16)) -1
(2)第二步,UBRDIV0寄存器中写入DIV_VAL的整数部分
(3)第三步,用小数部分*16得到1个个数,查表得uBDIVSLOT0寄存器的设置值
串口发送和接收函数的编写
(1)写发送函数,主要发送前要用while循环等待发送缓冲区为空才能发送。
//发送一个字节
void uart_putc(char c)
{
//因为串口控制器发送1个字节的速度远低于CPU的速度,所以CPU发送1个字节前必须
//确认串口控制器当前缓冲区是空的(意思就是串口已经发完了上一个字节
//如果缓冲区非空则位为0,此时应该循环,直到bit1位为1
while (!(rUTRSTAT0 & (1<<1)));//硬件判断是否发送结束 bit1
rUTXH0 = c; //串口发送一个字符,其实就是把一个字节丢到发送给冲区中去
}
下载程序验证成功
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;更简单的方法就是直接使用别人移植好的。
(3)我们课程中使用第三种方法,别人移植好的printf函数来自于友善之臂的Tiny210的裸机教程中提供的。
主要是将修改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
#导出这些变量到全局,其实就是给子文件夹下面的Makefile使用
export CC LD OBJCOPY OBJDUMP AR CPPFLAGS CFLAGS
objs := start.o led.o clock.o uart.o main.o
objs += lib/libc.a
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
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
cd lib; make clean; cd ..
学习记录,侵权联系删除。
来源:朱老师物联网大课堂