2021-7-29 Verilog语言实现串口的收发——接收功能(含有灵活校验位设计)

文章目录

  • 前言
  • 一、串口的通信协议
  • 二、分模块设计
    • 1.检测模块
    • 2.波特率设计模块
    • 3.接收数据控制模块
    • 4.顶层联系模块
  • 总结


前言

上一篇文章中介绍了与串口相关的各电气标准,本篇文章主要是介绍如何用Verilog语言来完成串口接收的功能。在前期查询资料的过程中,在黑金动力社区-博客园也是发现了有关这部分的相关知识,其中也是给出了设计源码,详细的代码讲解感兴趣的朋友可以去查看。【连载】【FPGA黑金开发板】Verilog HDL那些事儿–串口模块(十一)。通过阅读这篇文章的讲解,自己对Verilog语言也是进一步的了解,对项目的设计格式也是有更深的感悟,本篇文章的主要目的也是记录一下学习的过程,方便后续的查阅。

一、串口的通信协议

要进行模块的设计,首先要了解该部分功能的原理。这就涉及到串口的通信协议。
在这里插入图片描述
从这个图中可以看到,在RX信号线中,空闲状态是高电平。也就是说在不传输信息的时候,信号线置高。拉低的信号就是起始信号,也就是要开始传输数据的信号。第一位是起始位,紧跟后面的是数据位,随后有校验位和停止位。
首先介绍要介绍一下的理解。一个串行通信协议,一要有信号线,二要有时钟线。但这个串口就很奇怪,没有时钟线。这就要提到了波特率,这是收发两端提前说好的传输速度,不需要时钟线,常见的波特率有9600 bps(bit per second)和115200 bps,也就是一秒钟发送9600位和115200位信号。那么一位信号所占的时间是多少那?这里用9600 bps来进行一个简单的计算。
一秒钟传输9600位信号,那么一位信号所占的时间就是:
在这里插入图片描述
得出的答案是0.000104166667 s 也就是 1/bps;
有了波特率的概念,就可以理解通信协议中一位一位的概念。
接下来详细的理解这个通信协议。

  1. 空闲状态: 高电平
  2. 起始位: 拉低并且持续一位的时间,也就是持续 1/bps 的时间
  3. 数据位: 从这位开始就是数据位,数据位的长度从5 到 8 不等。最常见的是8位数据位。
  4. 校验位: 作为数据的校验位。
  5. 停止位: 拉高持续0.5,1 或者 2个时钟,只要接收双方协议好即可。
    这就是一帧数据中包含的信息量,其中对接收方来说最重要的自然是数据位。需要准确的从中提取出数据位,来进行下一步的处理。
    其中校验位的设计又有多种选择,校验方法有奇校验(odd)、偶校验(even)、0校验(space)、1校验(mark)及无校验(noparity)。常见的就是无校验,或者奇偶校验。
    无校验位就是不设置校验位,直接跟上停止位。
    奇偶校验位根据数据位中1 的个数来设计。
    奇校验位:如果数据位中1 的个数为奇数个,则该位为0。如果数据位中1 的个数为偶数个,则该位为1。也就是要求数据位与校验位中 1 的个数为奇数。
    偶校验位:和奇校验位相反。如果数据位中1 的个数为奇数个,则该位为1。如果数据位中1 的个数为偶数个,则该位为0。也就是要求数据位与校验位中 1 的个数为偶数。

举几个例子可以更好的理解串口的通信协议:
接收 0x54,串口设置:9600 bps,无校验位,一个停止位

在这里插入图片描述
为了便于理解加上了时钟线,在实际测试并没有时钟线,这是双方协议好的波特率。从图中可以看到在信号线中传递有效数据。

接收 0x5A,串口设置:9600 bps,奇校验位,一个停止位
在这里插入图片描述
从上面两个例子中可以熟悉串口的传输协议,了解协议之后就开始进行接收模块的设计。首先要了解接收模块。它的主要功能就是定时采集,去采集有效的数据位,从而忽略无效的起始位、停止位以及校验位。定时采集,什么时候采集最合适?自然是信号线最稳定的时候。什么时候信号线会稳定?自然是信号的中心,看图我们就能有一个很清楚的理解。
在这里插入图片描述


在每个数据位变换完之后,中间去采集,这样的到的信号数据,是最稳定的。了解到要设计的模块功能之后,就可以分模块的来进行整体的设计。
本次设计将接收模块设计成三个部分。三个部分各司其职。

在这里插入图片描述

  1. 第一个就是电平检测模块,他的作用就是检测信号线的下降沿,也就是起始位的开始。
  2. 第二个是波特率定时模块,作用为产生相应的波特率以及确定采集的时刻
  3. 第三个就是接受控制模块,作用就是检测什么位该忽略,什么位要采集。接收好有效的数据位,同时产生相应的正在接受以及接受完成的信号。

二、分模块设计

这次模块的设计让我也是重新的认识到 Verilog 语言设计新的一种格式。先设计好每一个相应的模块,在合理的将每个模块联系起来,形成需要的模块。这种方式使得整体的进程可以进行的比较有条理。下面我们先来进行第一步,分模块的设计。

1.检测模块

首先第一个是检测模块。该模块的功能是检测信号线的下降沿,也就是起始位的开始。那么只要做一个下降沿来的时候,产生一个时钟的高脉冲即可。

module detect_module
(CLK,RSTn,RX_Pin_In,H2L_Sig
); input CLK;input RSTn;input RX_Pin_In;output H2L_Sig;/*************************/reg H2L_F1;reg H2L_F2;always @ (posedge CLK or negedge RSTn)if(!RSTn)beginH2L_F1 <= 1'b1;H2L_F2 <= 1'b1;endelsebeginH2L_F1 <= RX_Pin_In;H2L_F2 <= H2L_F1;end/*************************/	assign H2L_Sig = H2L_F2 & !H2L_F1; //检测开始位的到来 发送一个高脉冲/*************************/	endmodule	

其中 RX_Pin_In 就是信号输入的信号线,H2L_Sig 就是输出高脉冲的信号线。 可以画一个时序图,理解一下其中的原理。

在这里插入图片描述
其中我们使用的 CLK 时钟为 50M ,相对于串口的速度,可以说非常的快。即使有个别的时钟错位,也不会影响通信的质量。同时在停止位时,时钟会再一次调整,所以说,停止位越长,通信过程中时钟的矫正就会有更好的时间。
这样通过这个模块就可以得到 RX_Pin_In 信号线的 一帧数据中起始下降沿的变化,同时产生一个高脉冲,通过这个高脉冲我们可以开始进行对后续各个数据位的操作。

2.波特率设计模块

在设计波特率生成模块之前,要先进行简单的计算。对波特率有了基本的了解,这部分设计起来本质就是计数器的操作。
前面介绍了,9600 bps 的计算。为了加深印象这次来计算另一种比较常见的波特率设置:115200 bps 。
首先还是先来计算一下,在115200 bps 的设置下,一位的数据位间隔多长时间。

得到的结果就是 0.00000868 s 这样,如果我们使用的 50 M的时钟来计数,那么记录多少个数才能达到这样的时间间隔那?做一个简单的计算。
记一个数的间隔为:

也就是2 * 10-8。我们用 0.00000868 除于 2 * 10-8就是我们要计数的个数。过程就是:

我们要计的数量为434,可以看到最后的结果略微有以下差距,这点错位就交给停止位来矫正就好,只要差距不大,不会太影响通信的质量。

接下来我们总结一下波特率设计的计算公式:

N = (1/BPS) / (1/F) = F / BPS

其中:

N: 要设计的计数个数(取整)
BPS: 设计的串口要设置的波特率
F :用来计数的主频的频率

这样波特率设计模块中计数过程就完成了,这样 我们介绍模块中 定时采集的概念,定时已经完成一半了,还有一部分就是,什么时候才开始定时?
是一直在计数嘛?当然不是。计数开始在一帧数据的传输的开始,结束在一帧数据传输的结束。一帧数据传输的开始和结束的标志是什么?起始位和停止位,开始的信号就是上一个设计的模块,也就是起始开始的信号,那个下降沿,一直到停止位的接收完成。还是利用一个时序图来理解一下定时采集的含义。


到这里,定时的部分可以说已经完成了,接下来就是采集信号的设计。在上文写到,采集最好的时间就是数据位的中间,这时的数据最稳定。我们可以再数据传输一位的中间产生一个脉冲,利用和这个脉冲来进行采集,看一下时序可以更加理解。

在中间采集,也就是计数到 217 的时候产生一个脉冲即可。
下面就是设计代码:

module rx_bps_module
(CLK,RSTn,Count_Sig,BPS_CLK
); input CLK;input RSTn;input Count_Sig;output BPS_CLK;/*************************/reg [11:0] Count_BPS;always @( posedge CLK or negedge RSTn )if(!RSTn)Count_BPS <= 12'd0;else if(Count_BPS == 12'd434)Count_BPS <= 12'd0;else if(Count_Sig)Count_BPS <= Count_BPS + 1'b1;else Count_BPS <= 12'd0;/*************************/			assign BPS_CLK = ( Count_BPS == 12'd217 ) ? 1'b1 : 1'b0;//在数据位的中间产生采集脉冲/*************************/	endmodule				

3.接收数据控制模块

有了开始位检测模块,波特率检测模块,下面要完成就是各个位的控制模块。起始位,数据位,校验位,停止位。每个位的数据作用各不相同,要进行的处理也各不相同, 这里就需要来控制。
其中最主要的自然就是数据位的存储,这里举例最常见的8位数据位来进行解析。
这部分就要用到 Verilog 语言中的状态机设计方案。根据前文的介绍,状态机的开始自然就是起始位那个下降沿的信号,也就是 H2L_Sig 这个脉冲信号。状态机的结束自然就是处理完停止位之后,但是本次设计中,设计了一个标记信号,来表示接收已经完成的信号,所以多加了一个状态。还是使用一个时序图来了解一下整个接收数据控制模块的设计。

第一次进入状态机,就是下降沿的时候。接下来状态的改变时刻就是采集脉冲的时刻,这样就达到了在传输位中间对数据进行处理的设计。

module rx_control_module
(CLK,RSTn,H2L_Sig,RX_Pin_In,BPS_CLK,RX_En_Sig,Count_Sig,RX_Data,RX_Done_Sig); input CLK;input RSTn;input H2L_Sig;input RX_En_Sig;input RX_Pin_In;input BPS_CLK;output Count_Sig;output [7:0] RX_Data;output RX_Done_Sig;/*************************/reg [3:0]i;reg [7:0]rData;reg isCount;reg isDone;always @ (posedge CLK or negedge RSTn)if(!RSTn)begini <= 4'd0;rData <= 8'd0;isCount <= 1'd0;isDone <= 1'd0;endelse if(RX_En_Sig)case (i)4'd0 :					  //检测到下降沿  //通信开始的信号if(H2L_Sig) begin i <= i+1'b1; isCount <= 1'b1; end4'd1 :						//起始位if(BPS_CLK) begin i <= i+1'b1; end  4'd2,4'd3,4'd4,4'd5,4'd6,4'd7,4'd8,4'd9 :	  //数据位		//数据位的存储 先低位后高位	if(BPS_CLK) begin i <= i+1'b1; rData[i-2] = RX_Pin_In; end4'd10 :				  //校验位if(BPS_CLK) begin i <= i+1'b1; end4'd11 :				  //停止位if(BPS_CLK) begin i <= i+1'b1; end  4'd12 :begin i <= i+1'b1; isDone <= 1'b1; isCount <= 1'b0; end4'd13 :begin i <= 1'b0; isDone <= 1'b0; endendcase/*************************/	assign Count_Sig = isCount;assign RX_Data = rData;assign RX_Done_Sig = isDone;/*************************/	endmodule	

其中起始位就是标志位,忽略即可。接下来的八位数据位,分别存储在一个深度为八位的寄存器中即可,也就是串行转并行。
随后就是校验位,前文提到的校验位设计有多种,这里介绍其中两种比较常见的设计。
第一个就是无校验位的设计,要是不添加校验位,这个状态就要删除,直接进入到停止位,这里要注意停止位的长度,否则就会造成两帧数据传输的错位情况。
第二个就是奇校验位的设计。这里要进行一个判断:
这里要提出一个非常精妙的逻辑语言应用。就是这个符号: ^ 异或
这个符号用很多应用,这里简单介绍一下用于奇偶校验的作用。
例如:
设 a = 4‘b 1100; 令 b = ^a ;
也就是b = ^ a = 1 ^ 1 ^ 0 ^ 0 = 0 ;按位异或。用来检测数据中1的个数。
这时候校验位的代码设计就可以是:

				4'd10 :				  //校验位if(BPS_CLK) begin 	if(RX_Pin_In == (^rData)? 1'b0:1'b1 )  //满足奇校验i <= i+1'b1;else 												//不满足奇校验begin	rData <= 8'd0;    					//将数据位清零i <= i+1'b1;endend

偶校验的话,也是用到同样的设计即可。

4.顶层联系模块

三个模块设计完成之后了,要对每个模块进行联系。

从这张图中可以清晰的看出那个信号线是来自顶层,那个信号线是要联系到那个模块。其中要注意的一点就是模块与模块之间的连线要定义成 wire 类型。

module rx_module
(CLK,RSTn,RX_Pin_In,RX_En_Sig,RX_Data,RX_Done_Sig); input CLK;input RSTn;input RX_En_Sig;input RX_Pin_In;output [7:0] RX_Data;output RX_Done_Sig;/*************************/wire H2L_Sig;detect_module U1(.CLK(CLK),  	 // 顶层的 CLK.RSTn(RSTn),	 // 顶层的 复位信号.RX_Pin_In(RX_Pin_In),  //顶层的 输入信号线 .H2L_Sig( H2L_Sig )		// 发送给 rx_control_module 模块中的 开始信号);/*************************/wire BPS_CLK;rx_bps_module U2(.CLK( CLK ),		// 顶层的 CLK.RSTn( RSTn ),		// 顶层的 复位信号.Count_Sig(Count_Sig),  //来自 rx_control_module 模块中 正在接受数据的信号 (isCount).BPS_CLK(BPS_CLK)   //发送给rx_control_module 模块用于状态机变换的 信号);/*************************/wire Count_Sig;rx_control_module U3(.CLK( CLK ),		// 顶层的 CLK.RSTn( RSTn ),		// 顶层的 复位信号.H2L_Sig( H2L_Sig ),  // 来自detect_module 模块中的开始的信号.RX_En_Sig(RX_En_Sig), // 来自顶层的使能信号.RX_Pin_In( RX_Pin_In ),//顶层的 输入信号线 .BPS_CLK(BPS_CLK),		//来自rx_bps_module 模块中的 采集脉冲.Count_Sig(Count_Sig),  //发送给 rx_bps_module 模块 表示是否正在传输的 信号线.RX_Data(RX_Data),      //对数据位的数据进行串转并 之后输出的并口数据.RX_Done_Sig(RX_Done_Sig) //产生的传输完成的脉冲);	/*************************/	endmodule	

连接完成之后,我们可以打开 RTL 视角看一下连接的情况:
在这里插入图片描述
这样可以和理论图进行一个详细的对比。
这就是分模块编写,而不是全部写到一个.v文件中,写好几个always 的好处。如果写到一个.v文件中,非常的复杂,同时检查的逻辑也很麻烦,也并不能生成这样的分模块的 RTL 视图。

总结

这样串口设计中介绍模块的设计也就完成了,整体写下来可能比较乱,有的地方可能会重复,字数也比较多。但通过这次对案例代码的理解和阅读,自己对 Verilog 语言设计有了进一步的理解。对于整体模块的设计也是有了一大步的理解和提升。相较于以前的编写风格,这种模块化的设计,会让逻辑更加的清楚。便于后期的维护,希望可以通过多次练习自己能够熟练这种编写方式。


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部