FPGA(二)串口通信

UART串口通信

  • 前言
  • 一、什么是串口?
    • 1.同步串行通信
    • 2.异步串行通信
        • 总结:
  • 二、串口的协议层面与物理层面
    • 1.协议层
      • (1)传输格式
      • (2)传输速率
    • 2.物理层
      • (1)接口类型
      • (2)电平标准
        • ①传输方向
        • ②接口标准
  • 三、使用FPGA编写串口回环
    • 1.数据发送
    • 2.数据接收
    • 3.FPGA程序
      • (1) 串口接收
      • (2) 串口发送
      • (3) 串口环回模块
  • 结尾


前言

通信过程分为3个步骤:首先,发送方按照信息编码方式对有效信息进行编码(编成可以在通信线路上传输的信号形态);然后,编码后的信息在传输介质上进行传输,输送给接收方;最后,接收方接到编码信息后进行解码,解码后得到可以理解的有效信息。


一、什么是串口?

串口是“串行接口(serial port)”的简称,即采用串行通信方式的接口。串行通信将数据字节分成一位一位的形式在一条数据线上逐个传送,其特点是通信线路简单,但传输速度较慢。因此串口广泛应用于嵌入式、工业控制等领域中对数据传输速度要求不高的场合。
FPGA(二)串口通信_第1张图片
与串行对应的呢,就是并行了。并行接口,指采用并行传输方式来传输数据的接口标准。从最简单的一个并行数据寄存器或专用接口集成电路芯片如8255、6820等,一直至较复杂的SCSI或IDE并行接口,种类有数十种。一个并行接口的接口特性可以从两个方面加以描述:1. 以并行方式传输的数据通道的宽度,也称接口传输的位数;2. 用于协调并行数据传输的额外接口控制线或称交互信号的特性。 数据的宽度可以从1~128位或者更宽,最常用的是8位,可通过接口一次传送8个数据位。在计算机领域最常用的并行接口是通常所说的LPT接口主要用于打印机和绘图仪
FPGA(二)串口通信_第2张图片

  • 在数据输入过程中:输入设备将数据送给接口同时使"数据输入准备好"有效。接口把数据送给输入缓冲寄存器时,使"数据输入回答"信号有效,当外设收到应答信号后,就撤消"数据输入准备好"和数据信号。同时,状态寄存器中的相应位(“数据输入准备好”)有效,以供CPU查询。当然,也可采用中断方式,向CPU发出中断请求。CPU在读取数据后,接口会自动将状态寄存器中的"数据输入准备好"位复位。然后,CPU进入下一个输入过程。

  • 在数据输出过程中:当CPU输出的数据送到数据输出缓冲寄存器后,接口会自动清除状态寄存器中的"输出准备好"状态位,并且把数据送给输出设备,输出设备收到数据后,向接口发一个应答信号,告诉接口数据已收到,接口收到信号后,将状态寄存器中的"输出准备好"状态位置"1"。然后,CPU进入下一个输出过程。

  • 并行接口是指数据的各位同时进行传送,其特点是传输速度快,但当传输距离较远、位数又多时,就导致通信线路复杂成本提高。串口形容一下就是:一条车道,而并口就是有8个车道同一时刻能传送8位(一个字节)数据。但是并不是并口快。由于8位通道之间的互相干扰,传输时速度就受到了限制。而且当传输出错时,要同时重新传8个位的数据。而串口没有干扰,传输出错后重发一位就可以了,所以要比并口快。串口硬盘就是这样被人们重视的。

所以呢,区分串行还是并行,看数据线的个数就好了。串行就一根数据线,把要发送的数据一位一位地发送出去,并行有若干根数据线,常用的是8根,数据就是8位8位地发送出去。

在这里呢,主要讲的是串行。

串行通信分为两种方式:同步串行通信和异步串行通信。 同步串行通信需要通信双方在同一时钟的控制下,进行传输数据;异步串行通信是指通信双方使用各自的时钟控制数据的发送和接收过程。

1.同步串行通信

同步通信(SYNC:synchronous data communication)是指在约定的通信速率下,发送端和接收端的时钟信号频率和相位始终保持一致(同步),这样就保证了通信双方在发送和接收数据时具有完全一致的定时关系。

同步通信把许多字符组成一个信息组(信息帧),每帧的开始用同步字符来指示,一次通信只传送一帧信息。在传输数据的同时还需要传输时钟信号,以便接收方可以用时针信号来确定每个信息位。同时,要求在传输线路上始终保持连续的字符位流,若计算机没有数据传输,则线路上要用专用的"空闲"字符或同步字符填充。

同步通信的优点是传送信息的位数几乎不受限制,一次通信传输的数据有几十到几千个字节,通信效率较高。同步通信的缺点是要求在通信中始终保持精确的同步时钟,即发送时钟和接收时钟要严格的同步(常用的做法是两个设备使用同一个时钟源),所以其发送器和接收器比较复杂,成本也较高,一般用于传送速率要求较高的场合。

2.异步串行通信

异步通信(ASYNC:asynchronous data communication),又称为起止式异步通信,是以字符为单位进行传输的,字符之间没有固定的时间间隔要求,而每个字符中的各位则以固定的时间传送。

在异步通信中,收发双方取得同步是通过在字符格式中设置起始位和停止位的方法来实现的。具体来说就是,在一个有效字符正式发送之前,发送器先发送一个起始位,然后发送有效字符位,在字符结束时再发送一个停止位,起始位至停止位构成一帧。停止位至下一个起始位之间是不定长的空闲位,并且规定起始位为低电平(逻辑值为0),停止位和空闲位都是高电平(逻辑值为1),这样就保证了起始位开始处一定会有一个下跳沿,由此就可以标志一个字符传输的起始。而根据起始位和停止位也就很容易的实现了字符的界定和同步。

显然,采用异步通信时,发送端和接收端可以由各自的时钟来控制数据的发送和接收,这两个时钟源彼此独立,可以互不同步。

总结:

通信接口中有并行也有串行,有同步也有异步,在多年的发展中,用途相对较多的是异步串行通信。常用的串行接口有:
FPGA(二)串口通信_第3张图片
想要了解更多,可以查看UART、I2C、SPI、TTL、RS232、RS422、RS485、CAN、USB、SD卡、1-WIRE、Ethernet

二、串口的协议层面与物理层面

其实串口是一个泛称,我们将使用这一类通信时序协议的接口,都称为串口,比如说RS232口、RS485口这样的。但是,提到RS-232我们又会想到TTL,这些之间又有什么区别和联系呢?接下来就要好好地梳理梳理了。

1.协议层

上面说的同步串行通信、异步串行通信,就是一种通信协议,在这样通信协议的加持下,可以演变成不同的接口类型,而UART口就是其中之一。

(1)传输格式

异步通信规定传输的数据格式由起始位(start bit)、数据位(data bit)、奇偶校验位(parity bit)和停止位(stop bit)组成:
FPGA(二)串口通信_第4张图片
(1)起始位:起始位必须是持续一个比特时间的逻辑0电平,标志传输一个字符的开始,接收方可用起始位使自己的接收时钟与发送方的数据同步。

(2)数据位:数据位紧跟在起始位之后,是通信中的真正有效信息。数据位的位数可以由通信双方共同约定,一般可以是5位、7位或8位,标准的ASCII码是0-127(7位),扩展的ASCII码是0-255(8位)。传输数据时先传送字符的低位,后传送字符的高位。

(3)奇偶校验位:奇偶校验位仅占一位,用于进行奇校验或偶校验,奇偶检验位不是必须有的。如果是奇校验,需要保证传输的数据总共有奇数个逻辑高位;如果是偶校验,需要保证传输的数据总共有偶数个逻辑高位。

举例来说,假设传输的数据位为01001100,如果是奇校验,则奇校验位为0(要确保总共有奇数个1),如果是偶校验,则偶校验位为1(要确保总共有偶数个1)。

由此可见,奇偶校验位仅是对数据进行简单的置逻辑高位或逻辑低位,不会对数据进行实质的判断,这样做的好处是接收设备能够知道一个位的状态,有可能判断是否有噪声干扰了通信以及传输的数据是否同步。

(4)停止位:停止位可以是是1位、1.5位或2位,可以由软件设定。它一定是逻辑1电平,标志着传输一个字符的结束。

(5)空闲位:空闲位是指从一个字符的停止位结束到下一个字符的起始位开始,表示线路处于空闲状态,必须由高电平来填充。

(2)传输速率

串口通信的传输速率用波特率来表示——每秒钟传送的码元符号的个数,是衡量数据传送速率的指标,它用单位时间内载波调制状态改变的次数来表示。单位应该是Bd,我查了好多资料,那些bps、b/s是比特率的单位。在信息传输通道中,携带数据信息的信号单元叫码元,每秒钟通过信道传输的码元数称为码元传输速率,简称波特率。

单位“波特”本身就已经是代表每秒的调制数,以“波特每秒”(Baud per second)为单位是一种常见的错误。由于翻译和中文的原因,波特(Baud)本身就已经代表速率了,所以翻译回去的时候不是Baud Rate,而是Baud。我们给本身就代表速率的波特后面加了个“率”大概也是中文习惯吧。

前面说的很官方,简单解释一下:上面说了调制编码,那举个简单的例子:01001111这串信号有8位(比特),如果一位一位的来看,那就是二电平信号0和1。但是假设现在经过调制,我们现在认为每两个bit作为“码元符号”,也就是两位两位的来看,即把这串信号这样看:01 00 11 11。这样调制的码元就是2个bit为一个单位,即四电平信号:00 01 10 11,换种说法就是0 1 2 3,此时1波特就是2比特。在二电平信号中,1波特包含1个bit信息,那此时波特率和比特率数值相等,但意义并不相同,即1秒钟传输了多少个比特数(如果波特率为9600的话,就是1s内传输9600个比特数)。

下面这个图我觉得还挺合适的,就拿来放在这里了(当比特率遇上波特率),如果存在侵权问题,请及时联系我,我将其删除。
FPGA(二)串口通信_第5张图片

回归正确的波特率哈。1波特即指每秒传输1个码元符号(通过不同的调制方式,可以在一个码元符号上负载多个bit位信息),1比特每秒是指每秒传输1比特(bit)。当波特率为9600时,就意味着这段通信1s的时间可以发送和接收9600个码元符号,那么每一个符号所需的时间就是 系统时钟/波特率,以50M系统时钟为例,每一符号占用 50_000_000/9600=5208个时钟周期,假使1个时钟周期是20ns,那么一个符号的时间就是5208*20ns=104160ns.

我们在使用波特率的时候,都是按照一些常用数字来规定的,比如9600、115200这样的,在串口调试助手上选择的数字也都是1200的整数倍,这里应该着重强调的是选择,而不是自己填写。照理说,我在串口调试助手上写一个波特率,然后在我的下位机上设置一个相同的波特率应该是可以的,但是为什么不行呢?其实应该是可以的(个人想法)!我们在做硬件之间的通信时,只要我们自己设置好分频系数、计算好误差,让接收方与发送方的波特率完全契合,那么两者之间也是可以完成通信的,并且彼此的时钟频率可以是不同的。而我们常用的波特率,我想是因为可以和时钟频率进行契合,经过验证没有误差,可以进行良好的通信吧。

我从网上没有找到觉得特别好的说法,但还是找了一个觉得不错的,放在这里仅供参考:
电工之家:串口通信为什么要设置波特率?串口通信波特率计算

说到波特率,有人就会和比特率进行混淆,就顺便说一说比特率:指每秒传送的比特(bit)数。单位为 b/s或bps(Bit Per Second),比特率越高,传送数据速度越快。在数据传输这一概念中,比特率和波特率在数值上存在这样一种关系:比特率 = 波特率 * log2(M),M表示码元进制数。比如上面写的二电平信号,M=2,四电平信号中M=4.

在很多的资料中,把比特率和波特率都给弄混了,我甚至都去知网上查文献了,最终经过自己的思考与判断,写下了这一节波特率和比特率的区别。

关于波特率和比特率,我觉得这个讲的还不错:
《通信原理》学习笔记No.8:什么比特率、什么是波特率,二者有什么区别?

2.物理层

在我们通常的称呼中,串口、UART口、COM口、USB口是指的物理接口形式(硬件),而TTL、RS-232、RS-485是指的电平标准(电信号)。UART、TTL、RS-232、RS-485都遵循串行通信的时序协议,因此都被通称为串口。

(1)接口类型

UART 是一种采用异步串行通信方式的通用异步收发传输器(Universal Asynchronous Receiver-Transmitter), 它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据。UART是串口收发的逻辑电路,这部分可以独立成芯片,也可以作为模块嵌入到其他芯片里,单片机、SOC、PC里都会有UART模块。UART 串口通信需要两根信号线来实现, 一根用于串口发送,另外一根负责串口接收。

COM口:特指台式计算机或一些电子设备上的D-SUB外形(一种连接器结构,VGA接口的连接器也是D-SUB)的串行通信口,应用了串口通信时序和RS-232的逻辑电平。

USB口:通用串行总线,和串口完全是两个概念。虽然也是串行方式通信,但由于USB的通信时序和信号电平都和串口完全不同,因此和串口没有任何关系。USB是高速的通信接口,用于PC连接各种外设,U盘、键鼠、移动硬盘、当然也包括“USB转串口”的模块。(USB转串口模块,就是USB接口的UART模块)。

(2)电平标准

电平信号是用信号线电平减去参考线电平得到的电压差,这个电压差决定了传输值是1还是0。在仔细说电平标准之前,先来简单介绍一下信号的传输方向。

①传输方向

  • 单工:数据只能沿一个方向传输:
  • 全双工:数据可以同时进行双向传输;
  • 半双工:数据传输可以沿两个方向,但需要分别进行。
    FPGA(二)串口通信_第6张图片

②接口标准

通常我们对于视频服务器、录像机、切换台等直接播出、切换控制主要使用串口进行,主要使用到RS-232、RS-422与RS-485三种接口控制。下面就串口的接口标准以及使用和外部插件和电缆进行探讨。 RS指“推荐标准”。

RS-232、RS-422与RS-485标准只对接口的电气特性做出规定,而不涉及接插件、电缆或协议,在此基础上用户可以建立自己的高层通信协议。例如:视频服务器都带有多个RS-422串行通讯接口,每个接口均可通过RS-422通讯线由外部计算机控制实现记录与播放。 视频服务器除提供各种控制硬件接口外,还提供协议接口,如RS-422接口除支持RS-422的Profile协议外,还支持 Louth、Odetics 、BVW等通过RS-422控制的协议。

RS-232、RS-422与RS-485都是串行数据接口标准,都是由电子工业协会(EIA)制订并发布的,RS-232在1962年发布。RS-422由RS-232发展而来,为改进RS-232通信距离短、速率低的缺点,RS-422定义了一种平衡通信接口,将传输速率提高到10Mbps,传输距离延长到4000英尺(速率低于100Kbps时),并允许在一条平衡总线上连接最多10个接收器。RS-422是一种单机发送、多机接收的单向、平衡传输规范,被命名为TIA/EIA-422-A标准。为扩展应用范围,EIA又于1983年在RS-422基础上制定了RS-485标准,增加了多点、双向通信能力,即允许多个发送器连接到同一条总线上,同时增加了发送器的驱动能力和冲突保护特性,扩展了总线共模范围,后命名为TIA/EIA-485-A标准。 

看了上面的话,我很想知道TTL是不是也是制定的什么标准,或者是与RS-232等有什么本质上的区别,但我查了半天,并没有查到,反而只总结出这么两句话:TTL与RS232在软件协议层面是一样的;RS232和TTL唯一不同在于硬件——电平表示的逻辑含义不同(相反)

下面部分参考博客串口、COM口、UART口, TTL、RS-232、RS-485区别详解

TTL指双极型三极管逻辑电路,市面上很多“USB转TTL”模块,实际上是“USB转TTL电平的串口”模块。这种信号0对应0V,1对应3.3V或者5V。与单片机、SOC的IO电平兼容。不过实际也不一定是TTL电平,因为现在大部分数字逻辑都是CMOS工艺做的,只是沿用了TTL的说法。我们进行串口通信的时候 从单片机直接出来的基本是都 是 TTL 电平。
全双工(逻辑1: 2.4V–5V 逻辑0: 0V–0.5V)

TTL用于两个MCU间通信
FPGA(二)串口通信_第7张图片
TTL的‘0’和‘1’表示
FPGA(二)串口通信_第8张图片

RS-232是电子工业协会(Electronic Industries Association,EIA) 制定的异步传输标准接口,同时对应着电平标准和通信协议(时序),其电平标准:+3V~+15V对应0,-3V~-15V对应1。RS-232 的逻辑电平和TTL 不一样但是协议一样。
全双工(逻辑1:-15V–5V 逻辑0:+3V–+15V)

RS-232是现在主流的串行通信接口之一,被广泛应用于计算机串行接口外设连接。由于RS-232接口标准出现较早,难免有不足之处,主要有以下四点:

  1. 接口的信号电平值较高,易损坏接口电路的芯片。RS232接口任何一条信号线的电压均为负逻辑关系。即:逻辑“1”为-3—-15V;逻辑“0”:+3—+15V,噪声容限为2V。即要求接收器能识别高于+3V的信号作为逻辑“0”,低于-3V的信号作为逻辑“1”,TTL电平为5V为逻辑正,0为逻辑负。与TTL电平不兼容故需使用电平转换电路方能与TTL电路连接;
  2. 传输速率较低,在异步传输时,比特率为20Kbps;因此在51CPLD开发板中,综合程序波特率只能采用19200,也是这个原因;
  3. 接口使用一根信号线和一根信号返回线而构成共地的传输形式,这种共地传输容易产生共模干扰,所以抗噪声干扰性弱;
  4. 传输距离有限,最大传输距离标准值为50英尺,实际上也只能用在15米左右。

RS-232的通信示意图,其中TTL用于MCU与PC之间的通信
FPGA(二)串口通信_第9张图片
RS-232的‘0’和‘1’表示
FPGA(二)串口通信_第10张图片
RS-232 DB9接口定义
FPGA(二)串口通信_第11张图片

RS-485是一种串口接口标准,为了长距离传输采用差分方式传输,传输的是差分信号,抗干扰能力比RS232强很多。两线压差为-(2~ 6)V表示0,两线压差为+(2~6)V表示1。与RS-422电路原理基本相同,区别就是RS422是4线全双工。
半双工(逻辑1:+2V~+6V 逻辑0: -6V ~-2V)这里的电平指A B两线间的电压差。

在要求通信距离为几十米到上千米时,广泛采用RS-485串行总线。RS-485采用平衡发送和差分接收,因此具有抑制共模干扰的能力。加上总线收发器具有高灵敏度,能检测低至200mV的电压,故传输信号能在千米以外得到恢复。

RS-485用于多点互连时非常方便,可以省掉许多信号线。应用RS-485可以联网构成分布式系统,其允许最多并联32台驱动器和32台接收器。针对RS-232-C的不足,新标准RS-485具有以下特点:

  1. RS-485的电气特性:逻辑“1”以两线间的电压差+2V~ +6V表示,逻辑“0”以两线间的电压差-6V~-2V表示。接口信号电平比RS-232-C降低了,就不容易损坏接口电路芯片,且该电平与TTL电平兼容,刻方便与TTL电路连接;
  2. 数据最高传输速率为:10Mbps;
  3. RS-485接口采用平衡驱动器和差分接收器的组合,抗共模干扰能力强,即抗噪声性能好;
  4. RS-485接口的最大传输距离标准值4000英尺,实际上可达3000米;
  5. RS-232-C接口在总线上只允许连接一个收发器,即单站能力;而RS-485接口在总线上只允许连接多达128个收发器,即具有多站能力,这样用户可以利用单一的RS-485接口方便地建立设备网络。

RS-485通讯网络示意图
FPGA(二)串口通信_第12张图片
RS485的‘0’和‘1’表示
FPGA(二)串口通信_第13张图片

RS-422标准全称是“平衡电压数字接口电路的电气特性”,它定义了接口电路的特性。实际上还有一根信号地线,共5根线。由于接收器采用高输入阻抗和发送驱动器比RS-232更强的驱动能力,故允许在相同传输线上连接多个接收节点,最多可接10个节点。一个主设备(Master),其余为从设备(Slave),从设备之间不能通信,所以RS-422支持点对多的双向通信。接收器输入阻抗为4k,故发端最大负载能力是10 & TImes;4k+100Ω(终接电阻)。

RS-422和RS-485电路原理基本相同,都是以差动方式发送和接受,不需要数字地线。差动工作是同速率条件下传输距离远的根本原因,这正是二者与RS-232的根本区别,因为RS232是单端输入输出,双工工作时至少需要数字地线。发送线和接受线三条线(异步传输),还可以加其它控制线完成同步等功能。

RS-422通过两对双绞线可以全双工工作收发互不影响,而RS485只能半双工工作,发收不能同时进行,但它只需要一对双绞线。RS-422和RS-485在19kpbs下能传输1200米。用新型收发器线路上可连接台设备。

RS-422的电气性能与RS-485完全一样。主要的区别在于:RS-422有4根信号线:两根发送(Y、Z)、两根接收(A、B)。由于RS-422的收与发是分开的所以可以同时收和发(全双工);RS-485有2根信号线:发送和接收。

RS-422四线接口由于采用单独的发送和接收通道,因此不必控制数据方向,各装置之间任何必须的信号交换均可以按软件方式(XON/XOFF握手)或硬件方式(一对单独的双绞线)。RS-422的最大传输距离为4000英尺(约1219米),最大传输速率为10Mb/s。其平衡双绞线的长度与传输速率成反比,在100kb/s速率以下,才可能达到最大传输距离。只有在很短的距离下才能获得最高速率传输。一般100米长的双绞线上所能获得的最大传输速率仅为1Mb/s。

RS-422需要一终接电阻,要求其阻值约等于传输电缆的特性阻抗。在短距离传输时可不需终接电阻,即一般在300米以下不需终接电阻。终接电阻接在传输电缆的最远端。

RS-422方式接线图
FPGA(二)串口通信_第14张图片
RS-422/RS-485 DB9接口定义
FPGA(二)串口通信_第15张图片

对上面的四种接口标准做一个表格概括一下:
FPGA(二)串口通信_第16张图片

三、使用FPGA编写串口回环

1.数据发送

前面我们弄清楚了异步通信的数据格式之后,就可以按照指定的数据格式发送数据了,发送数据的具体步骤如下:

(1)初始化后或者没有数据需要发送时,发送端输出逻辑1,可以有任意数量的空闲位。

(2)当需要发送数据时,发送端首先输出逻辑0,作为起始位。

(3)接着就可以开始输出数据位了,发送端首先输出数据的最低位D0,然后是D1,最后是数据的最高位。

(4)如果设有奇偶检验位,发送端输出检验位。

(5)最后,发送端输出停止位(逻辑1)。

(6)如果没有信息需要发送,发送端输出逻辑1(空闲位),如果有信息需要发送,则转入步骤(2)。

2.数据接收

在异步通信中,接收端以接收时钟和波特率因子决定每一位的时间长度。下面以波特率因子等于16(接收时钟每16个时钟周期使接收移位寄存器移位一次)为例来说明。

(1)开始通信,信号线为空闲(逻辑1),当检测到由1到0的跳变时,开始对接收时钟计数。

(2)当计到8个时钟的时候,对输入信号进行检测,若仍然为低电平,则确认这是起始位,而不是干扰信号。

(3)接收端检测到起始位后,隔16个接收时钟对输入信号检测一次,把对应的值作为D0位数据。

(4)再隔16个接收时钟,对输入信号检测一次,把对应的值作为D1位数据,直到全部数据位都输入。

(5)检验奇偶检验位。

(6)接收到规定的数据位个数和校验位之后,通信接口电路希望收到停止位(逻辑1),若此时未收到逻辑1,说明出现了错误,在状态寄存器中置“帧错误”标志;若没有错误,对全部数据位进行奇偶校验,无校验错时,把数据位从移位寄存器中取出送至数据输入寄存器,若校验错,在状态寄存器中置“奇偶错”标志。

(7)本帧信息全部接收完,把线路上出现的高电平作为空闲位。

(8)当信号再次变为低时,开始进入下一帧的检测。

以上就是异步通信中数据发送和接收的全过程了。

3.FPGA程序

下面的程序是用于UART串口通讯:

(1) 串口接收

串口接收时序
FPGA(二)串口通信_第17张图片

写程序的思路基本上就是一条线一个时序约束,从上往下即可。

串口开始接收的标志是start_flag变为1的时候,而start_flag怎么变成1呢?根据常用协议,一般将数据接收线uart_rxd拉高,而在捕获到uart_rxd由高置低时,start_flag就变为1了。而这样如何捕获呢,就需要设置两个捕获标志位——uart_rxd_d0与uart_rxd_d1,一直捕捉uart_rxd上的电位,当uart_rxd_d0与uart_rxd_d1产生差异的时候,start_flag就变为1了。

reg        uart_rxd_d0;
reg        uart_rxd_d1;

wire       start_flag;

//捕获接收端口下降沿(起始位),得到一个时钟周期的脉冲信号
assign  start_flag = uart_rxd_d1 & (~uart_rxd_d0);    

//对UART接收端口的数据延迟两个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin 
    if (!sys_rst_n) begin 
        uart_rxd_d0 <= 1'b0;
        uart_rxd_d1 <= 1'b0;          
    end
    else begin
        uart_rxd_d0  <= uart_rxd;                   
        uart_rxd_d1  <= uart_rxd_d0;
    end   
end

上面的代码是一个边沿检测电路。在系统复位没有生效时,uart_rxd_d1总是延迟于uart_rxd_d0一个时钟周期的,所以可以捕获这接收端口的下降沿。

在复位按键不被触发的情况下,rx_flag在start_flag变为1后拉高,并在数据接收停止位中置低。

parameter  CLK_FREQ = 50_000_000;                //系统时钟频率
parameter  UART_BPS = 9600;                    //串口波特率
localparam  BPS_CNT  = CLK_FREQ/UART_BPS;       //为得到指定波特率,
                                                //需要对系统时钟计数BPS_CNT次

reg [15:0] clk_cnt;                             //系统时钟计数器
reg [ 3:0] rx_cnt;                              //接收数据计数器
reg        rx_flag;                             //接收过程标志信号

//当脉冲信号start_flag到达时,进入接收过程           
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n)                                  
        rx_flag <= 1'b0;
    else begin
        if(start_flag)                          //检测到起始位
            rx_flag <= 1'b1;                    //进入接收过程,标志位rx_flag拉高
                                                //计数到停止位中间时,停止接收过程
        else if((rx_cnt == 4'd9) && (clk_cnt == BPS_CNT/2))
            rx_flag <= 1'b0;                    //接收过程结束,标志位rx_flag拉低
        else
            rx_flag <= rx_flag;
    end
end

其中,不必计数完整的停止位,可以根据自己的需求确定计数到停止位的中间某个地方。上面clk_cnt == BPS_CNT/2是计数到停止位的中间,是因为计数到数据中间时的采样结果最稳定。详细理解就是,虽然此时一帧数据传输还没有完成(停止位只传送到一半),但是数据位已经寄存完毕。而在连续接收数据时,提前半个波特率周期结束接收过程可以为检测下一帧数据的起始位留出充足的时间。

然后需要注意的是要定义一个接收数据寄存器,用于将接受过程中获取的数据存储起来,并在接收到停止位后直接传递给uart_data:

output  reg [7:0] uart_data                     //接收的数据

reg [ 7:0] rxdata;                              //接收数据寄存器

//根据接收数据计数器来寄存uart接收端口数据
always @(posedge sys_clk or negedge sys_rst_n) begin 
    if ( !sys_rst_n)  
        rxdata <= 8'd0;                                     
    else if(rx_flag)                            //系统处于接收过程
        if (clk_cnt == BPS_CNT/2) begin         //判断系统时钟计数器计数到数据位中间
            case ( rx_cnt )
             4'd1 : rxdata[0] <= uart_rxd_d1;   //寄存数据位最低位
             4'd2 : rxdata[1] <= uart_rxd_d1;
             4'd3 : rxdata[2] <= uart_rxd_d1;
             4'd4 : rxdata[3] <= uart_rxd_d1;
             4'd5 : rxdata[4] <= uart_rxd_d1;
             4'd6 : rxdata[5] <= uart_rxd_d1;
             4'd7 : rxdata[6] <= uart_rxd_d1;
             4'd8 : rxdata[7] <= uart_rxd_d1;   //寄存数据位最高位
             default:;                                    
            endcase
        end
        else 
            rxdata <= rxdata;
    else
        rxdata <= 8'd0;
end

下面给出串口接收模块的完整代码:

module uart_recv(
    input			  sys_clk,                  //系统时钟
    input             sys_rst_n,                //系统复位,低电平有效
    
    input             uart_rxd,                 //UART接收端口
    output  reg       uart_done,                //接收一帧数据完成标志
    output  reg [7:0] uart_data                 //接收的数据
    );
    
//parameter define
parameter  CLK_FREQ = 50_000_000;                //系统时钟频率
parameter  UART_BPS = 9600;                    //串口波特率
localparam  BPS_CNT  = CLK_FREQ/UART_BPS;       //为得到指定波特率,
                                                //需要对系统时钟计数BPS_CNT次
//reg define
reg        uart_rxd_d0;
reg        uart_rxd_d1;
reg [15:0] clk_cnt;                             //系统时钟计数器
reg [ 3:0] rx_cnt;                              //接收数据计数器
reg        rx_flag;                             //接收过程标志信号
reg [ 7:0] rxdata;                              //接收数据寄存器

//wire define
wire       start_flag;

//*****************************************************
//**                    main code
//*****************************************************
//捕获接收端口下降沿(起始位),得到一个时钟周期的脉冲信号
assign  start_flag = uart_rxd_d1 & (~uart_rxd_d0);    

//对UART接收端口的数据延迟两个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin 
    if (!sys_rst_n) begin 
        uart_rxd_d0 <= 1'b0;
        uart_rxd_d1 <= 1'b0;          
    end
    else begin
        uart_rxd_d0  <= uart_rxd;                   
        uart_rxd_d1  <= uart_rxd_d0;
    end   
end

//当脉冲信号start_flag到达时,进入接收过程           
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n)                                  
        rx_flag <= 1'b0;
    else begin
        if(start_flag)                          //检测到起始位
            rx_flag <= 1'b1;                    //进入接收过程,标志位rx_flag拉高
                                                //计数到停止位中间时,停止接收过程
        else if((rx_cnt == 4'd9) && (clk_cnt == BPS_CNT/2))
            rx_flag <= 1'b0;                    //接收过程结束,标志位rx_flag拉低
        else
            rx_flag <= rx_flag;
    end
end

//进入接收过程后,启动系统时钟计数器
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n)                             
        clk_cnt <= 16'd0;                                  
    else if ( rx_flag ) begin                   //处于接收过程
        if (clk_cnt < BPS_CNT - 1)
            clk_cnt <= clk_cnt + 1'b1;
        else
            clk_cnt <= 16'd0;               	//对系统时钟计数达一个波特率周期后清零
    end
    else                              				
        clk_cnt <= 16'd0;						//接收过程结束,计数器清零
end

//进入接收过程后,启动接收数据计数器
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n)                             
        rx_cnt  <= 4'd0;
    else if ( rx_flag ) begin                   //处于接收过程
        if (clk_cnt == BPS_CNT - 1)				//对系统时钟计数达一个波特率周期
            rx_cnt <= rx_cnt + 1'b1;			//此时接收数据计数器加1
        else
            rx_cnt <= rx_cnt;       
    end
	 else
        rx_cnt  <= 4'd0;						//接收过程结束,计数器清零
end

//根据接收数据计数器来寄存uart接收端口数据
always @(posedge sys_clk or negedge sys_rst_n) begin 
    if ( !sys_rst_n)  
        rxdata <= 8'd0;                                     
    else if(rx_flag)                            //系统处于接收过程
        if (clk_cnt == BPS_CNT/2) begin         //判断系统时钟计数器计数到数据位中间
            case ( rx_cnt )
             4'd1 : rxdata[0] <= uart_rxd_d1;   //寄存数据位最低位
             4'd2 : rxdata[1] <= uart_rxd_d1;
             4'd3 : rxdata[2] <= uart_rxd_d1;
             4'd4 : rxdata[3] <= uart_rxd_d1;
             4'd5 : rxdata[4] <= uart_rxd_d1;
             4'd6 : rxdata[5] <= uart_rxd_d1;
             4'd7 : rxdata[6] <= uart_rxd_d1;
             4'd8 : rxdata[7] <= uart_rxd_d1;   //寄存数据位最高位
             default:;                                    
            endcase
        end
        else 
            rxdata <= rxdata;
    else
        rxdata <= 8'd0;
end

//数据接收完毕后给出标志信号并寄存输出接收到的数据
always @(posedge sys_clk or negedge sys_rst_n) begin        
    if (!sys_rst_n) begin
        uart_data <= 8'd0;                               
        uart_done <= 1'b0;
    end
    else if(rx_cnt == 4'd9) begin               //接收数据计数器计数到停止位时           
        uart_data <= rxdata;                    //寄存输出接收到的数据
        uart_done <= 1'b1;                      //并将接收完成标志位拉高
    end
    else begin
        uart_data <= 8'd0;                                   
        uart_done <= 1'b0; 
    end    
end

endmodule	

(2) 串口发送

串口发送时序
FPGA(二)串口通信_第18张图片

和串口接收模块一样,也是根据时序图来一根线一根线的进行约束。

在发送模块,也有一个开始的使能信号en_flag,由数据使能信号uart_en从低拉高触发。同样经过边沿检测电路获取uart_en的上升沿,得到一个开始进行传输的标志——en_flag的脉冲:

reg        uart_en_d0; 
reg        uart_en_d1;  

wire       en_flag;

//捕获uart_en上升沿,得到一个时钟周期的脉冲信号
assign en_flag = (~uart_en_d1) & uart_en_d0;

//对发送使能信号uart_en延迟两个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n) begin
        uart_en_d0 <= 1'b0;                                  
        uart_en_d1 <= 1'b0;
    end                                                      
    else begin                                               
        uart_en_d0 <= uart_en;                               
        uart_en_d1 <= uart_en_d0;                            
    end
end

接下来在触发使能产生脉冲后,将传输标志位tx_flag拉高同时将接收数据线上的数据传递给中间变量——寄存发送数据tx_data,用于启动发送数据寄存器后给uart发送端口赋值:

reg        tx_flag;                         //发送过程标志信号
reg [ 7:0] tx_data;                         //寄存发送数据

//当脉冲信号en_flag到达时,寄存待发送的数据,并进入发送过程          
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n) begin                                  
        tx_flag <= 1'b0;
        tx_data <= 8'd0;
    end 
    else if (en_flag) begin                 //检测到发送使能上升沿                      
            tx_flag <= 1'b1;                //进入发送过程,标志位tx_flag拉高
            tx_data <= uart_din;            //寄存待发送的数据
        end
                                            //计数到停止位结束时,停止发送过程
        else if ((tx_cnt == 4'd9) && (clk_cnt == BPS_CNT -(BPS_CNT/16))) begin                                       
            tx_flag <= 1'b0;                //发送过程结束,标志位tx_flag拉低
            tx_data <= 8'd0;
        end
        else begin
            tx_flag <= tx_flag;
            tx_data <= tx_data;
        end 
end

需要提醒的是,上述代码将tx_flag提前1/16个停止位拉低,是为了确保发送模块发送数据的时间略小于接收模块接收数据的时间,否则当连续传输大量数据时,发送数据的时间会不断累积,最终导致在做串口环回实验时丢失数据。尽管串口发送数据只是接收数据的反过程,理论上在传输的时间上是一致的,考虑到我们模块里计算波特率会有较小的偏差,并且串口对端的通信设备(如电脑等)收发数据的波特率同样可能会出现较小的偏差,因此这里为了确保环回实验的成功,这里将发送模块的停止位略微提前结束。

下面给出发送模块全部代码:

module uart_send(
    input	      sys_clk,                  //系统时钟
    input         sys_rst_n,                //系统复位,低电平有效
    
    input         uart_en,                  //发送使能信号
    input  [7:0]  uart_din,                 //待发送数据
    output        uart_tx_busy,             //发送忙状态标志      
    output  reg   uart_txd                  //UART发送端口
    );
    
//parameter define
parameter  CLK_FREQ = 50000000;            //系统时钟频率
parameter  UART_BPS = 9600;                //串口波特率
localparam  BPS_CNT  = CLK_FREQ/UART_BPS;   //为得到指定波特率,对系统时钟计数BPS_CNT次

//reg define
reg        uart_en_d0; 
reg        uart_en_d1;  
reg [15:0] clk_cnt;                         //系统时钟计数器
reg [ 3:0] tx_cnt;                          //发送数据计数器
reg        tx_flag;                         //发送过程标志信号
reg [ 7:0] tx_data;                         //寄存发送数据

//wire define
wire       en_flag;

//*****************************************************
//**                    main code
//*****************************************************
//在串口发送过程中给出忙状态标志
assign uart_tx_busy = tx_flag;

//捕获uart_en上升沿,得到一个时钟周期的脉冲信号
assign en_flag = (~uart_en_d1) & uart_en_d0;

//对发送使能信号uart_en延迟两个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n) begin
        uart_en_d0 <= 1'b0;                                  
        uart_en_d1 <= 1'b0;
    end                                                      
    else begin                                               
        uart_en_d0 <= uart_en;                               
        uart_en_d1 <= uart_en_d0;                            
    end
end

//当脉冲信号en_flag到达时,寄存待发送的数据,并进入发送过程          
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n) begin                                  
        tx_flag <= 1'b0;
        tx_data <= 8'd0;
    end 
    else if (en_flag) begin                 //检测到发送使能上升沿                      
            tx_flag <= 1'b1;                //进入发送过程,标志位tx_flag拉高
            tx_data <= uart_din;            //寄存待发送的数据
        end
                                            //计数到停止位结束时,停止发送过程
        else if ((tx_cnt == 4'd9) && (clk_cnt == BPS_CNT -(BPS_CNT/16))) begin                                       
            tx_flag <= 1'b0;                //发送过程结束,标志位tx_flag拉低
            tx_data <= 8'd0;
        end
        else begin
            tx_flag <= tx_flag;
            tx_data <= tx_data;
        end 
end

//进入发送过程后,启动系统时钟计数器
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n)                             
        clk_cnt <= 16'd0;                                  
    else if (tx_flag) begin                 //处于发送过程
        if (clk_cnt < BPS_CNT - 1)
            clk_cnt <= clk_cnt + 1'b1;
        else
            clk_cnt <= 16'd0;               //对系统时钟计数达一个波特率周期后清零
    end
    else                             
        clk_cnt <= 16'd0; 				    //发送过程结束
end

//进入发送过程后,启动发送数据计数器
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n)                             
        tx_cnt <= 4'd0;
    else if (tx_flag) begin                 //处于发送过程
        if (clk_cnt == BPS_CNT - 1)			//对系统时钟计数达一个波特率周期
            tx_cnt <= tx_cnt + 1'b1;		//此时发送数据计数器加1
        else
            tx_cnt <= tx_cnt;       
    end
    else                              
        tx_cnt  <= 4'd0;				    //发送过程结束
end

//根据发送数据计数器来给uart发送端口赋值
always @(posedge sys_clk or negedge sys_rst_n) begin        
    if (!sys_rst_n)  
        uart_txd <= 1'b1;        
    else if (tx_flag)
        case(tx_cnt)
            4'd0: uart_txd <= 1'b0;         //起始位 
            4'd1: uart_txd <= tx_data[0];   //数据位最低位
            4'd2: uart_txd <= tx_data[1];
            4'd3: uart_txd <= tx_data[2];
            4'd4: uart_txd <= tx_data[3];
            4'd5: uart_txd <= tx_data[4];
            4'd6: uart_txd <= tx_data[5];
            4'd7: uart_txd <= tx_data[6];
            4'd8: uart_txd <= tx_data[7];   //数据位最高位
            4'd9: uart_txd <= 1'b1;         //停止位
            default: ;
        endcase
    else 
        uart_txd <= 1'b1;                   //空闲时发送端口为高电平
end

endmodule	      

需要指出的是,assign uart_tx_busy = tx_flag; 将tx_flag的值赋给uart_tx_busy,并通过端口输出,这样其他模块就可以通过检测 uart_tx_busy 信号是否为低电平,从而判断串口发送模块是否处于空闲状态。

(3) 串口环回模块

FPGA(二)串口通信_第19张图片
先通过边沿检测电路捕捉到recv_done信号的上升沿(即接收模块uart_done的上升沿),此时recv_done_flag 输出一个时钟周期的高电平,标志着串口接收模块接收到了一帧数据。在判断到 recv_done_flag 为高电平时,寄存接收到的数据 recv_data(接收模块的uart_data) 到 send_data(发送模块的uart_din)中;同时将 tx_ready 信号拉高,表示已经准备好了待发送的数据。另外还要将 send_en(发送模块的uart_en) 信号拉低,为接下来产生一个上升沿作准备。

环回模块完整代码如下:

module uart_loop (
    input               sys_clk     ,
    input               sys_rst_n   ,

    input               recv_done   ,                 //接收一帧数据完成标志
    input      [7:0]    recv_data,                 //接收的数据
     
    input               tx_busy     ,                   //发送忙状态标志      
    output reg          send_en     ,                   //发送使能信号
    output reg [7:0]    send_data                  //待发送数据
);

reg recv_done_d0;
reg recv_done_d1;
reg tx_ready;

wire recv_done_flag;

//捕获recv_done上升沿,得到一个时钟周期的脉冲信号
assign recv_done_flag = recv_done_d0 & (~recv_done_d1);

always @(posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n) begin
        recv_done_d0 <= 1'b0;
        recv_done_d1 <= 1'b0;
    end 
    else begin
        recv_done_d0 <= recv_done;
        recv_done_d1 <= recv_done_d0;
    end       
end

//判断接收完成信号,并在串口发送模块空闲时给出发送使能信号
always @(posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n) begin
        tx_ready <= 1'b0;
        send_en <= 1'b0;
        send_data <= 8'd0;
    end 
    else begin
        if(recv_done_flag) begin
            tx_ready <= 1'b1;      //准备启动发送过程
            send_en <= 1'b0;
            send_data <= recv_data; //寄存器串口接收的数据
        end
        else if(tx_ready && (~tx_busy)) begin   //检测串口模块发送空闲
            tx_ready <= 1'b0;           //准备过程结束
            send_en <= 1'b1;            //拉高发送使能信号
        end
    end
end
    
endmodule

结尾

这是我在学习过程中慢慢写下来的,最开始还是写给我自己看的,但一想就还是把它给发出来吧。我也是在慢慢学习,在学的过程中有不懂的我就去查资料,所以上面的内容是基于我自己的认识与网络上查阅的资料综合得出的,如果有什么不对的地方,望批评指正,共同学习。

我在文字方面也没啥造诣,所以可能存在语序错误或啰里啰唆的情况,还望见谅。

有不少东西都是网络上搜寻到的,甚至是直接复制粘贴过来的,有些直接标明了参考的网址,有些也忘了,我只是用于学习,如果存在侵权行为,请及时联系我,我会将其删除。

参考内容:

正点原子

波特率与比特率

串口通信与编程01:串口基础知识

串口通信详解

RS232/RS422/RS485通信接口区别

你可能感兴趣的:(fpga开发,学习)