2.5 UART串口通信设计实例(1)接下来用刚才采用的方法设计一个典型实例。
在一般的嵌入式开发和FPGA设计中,串口UART是使用非常频繁的一种调试手段。
下面我们将使用Verilog RTL编程设计一个串口收发模块。
这个实例虽然简单,但是在后续的调试开发中,串口使用的次数比较多,这里阐明它的设计方案,不仅仅是为了讲解RTL编程,而且为了后续使用兼容ARM9内核实现嵌入式开发。
串口在一般的台式机上都会有。
随着笔记本电脑的使用,一般会采用USB转串口的方案虚拟一个串口供笔记本使用。
图2-7为UART串口的结构图。
串口具有9个引脚,但是真正连接入FPGA开发板的一般只有两个引脚。
这两个引脚是:发送引脚TxD和接收引脚RxD。
由于是串行发送数据,因此如果开发板发送数据的话,则要通过TxD线1 bit接着1 bit 发送。
在接收时,同样通过RxD引脚1 bit接着1 bit接收。
再看看串口发送/接收的数据格式(见图2-8)。
在TxD或RxD这样的单线上,是从一个周期的低电平开始,以一个周期的高电平结束的。
它中间包含8个周期的数据位和一个周期针对8位数据的奇偶校验位。
每次传送一字节数据,它包含的8位是由低位开始传送,最后一位传送的是第7位。
这个设计有两个目的:一是从串口中接收数据,发送到输出端口。
接收的时候是串行的,也就是一个接一个的;但是发送到输出端口时,我们希望是8位放在一起,成为并行状态(见图2-10)。
我们知道,串口中出现信号,是没有先兆的。
如果出现了串行数据,则如何通知到输出端口呢?我们引入“接收有效”端口。
“接收有效”端口在一般情况下都是低电平,一旦有数据到来时,它就变成高电平。
下一个模块在得知“接收有效”信号为高电平时,它就明白:新到了一个字节的数据,放在“接收字节”端口里面。
二是发送数据到串口。
发送数据的时候,我们也希望输入端口能够给出一个简单的形式。
我们引入“发送有效”信号,它为高电平,表示我们希望把“发送字节”送入TxD发送出去。
但是“发送有效”信号是否生效是有限制的,也就是正在发送的时候,是不能接收新的数据并发送的。
所以,我们引入一个“发送状态”信号,它标识当前的“发报机”是否处于忙碌状态。
如果“发报机”处于忙碌状态,则它拒绝“发送有效”信号,不予执行。
根据上面的分析,可以确定端口信号如下:1.module rxtx (2. clk,3. rst,4. rx,5. tx_vld,6. tx_data,7.8. rx_vld,9. rx_data,10. tx,11. txrdy12. );13.input clk;14.input rst;15.input rx;16.input tx_vld;17.input [7:0] tx_data;18.19.output rx_vld;20.output [7:0] rx_data;21.output tx;22.output txrdy;rx对应RxD,tx对应TxD。
rx_vld就是“接收有效”信号,rx_data则是“接收字节”信号。
tx_vld是“发送有效”信号,tx_data是“发送字节”信号。
txrdy是“发送状态”信号,它是低电平表示正处于发送状态,不接收新的字节而进行发送。
我们知道,串行数据的频率是9600Hz,而FPGA开发板的频率却是非常高的。
这里,我们假定FPGA的工作频率是25MHz,则串口发送1位信息,则FPGA上的clk需要计数:25 000 000/9600=2604次。
我们知道rx一旦变化,不论是从0到1,还是从1到0,都表示1位信息的传递开始。
因此,我们在设计一个最大计数值为2604的计数器的时候,rx的变化都将导致这个计数器重新开始计数。
如果计数到2604附近,rx发生变化,那么又将导致计数器清零;如果rx没有发生变化,没关系,计数器在计数到2604时,自动清零。
因此,rx的变化会不断调整计数器的计数。
在这个计数器计算到中间,也就是1302时,是最佳采样时刻,这个时候,rx的电平是我们需要知道的位信息。
对于rx的接收,需要2~3个寄存器同步,来消除异步传送的不确定性。
下面描述的rx1、rx2、rx3、rxx只是对rx进行延时,消除异步效果。
1.reg rx1,rx2,rx3,rxx;2.always @ ( posedge clk ) begin3. rx1 <= rx;4. rx2 <= rx1;5. rx3 <= rx2;6. rxx <= rx3;7. end对于rxx,我们将检测它的变化,这个变化将作为置位计数器的标志。
rx_change表示rxx 发生了改变,它比较了rx_dly和rxx的差别。
1.reg rx_dly;2.always @ ( posedge clk )3. rx_dly <= rxx;4.5.wire rx_change;6.assign rx_change = (rxx != rx_dly );下面将实现一个计数器,它将以2604为周期进行计数。
如果rx保持长时间不变,比如传送多个1或多个0,则计数器以2604为周期计数,可以计算到底有多少个1或0传送—因为在传送多个1或0时,rx是不会发生变化的,但是计数器会从2604恢复到0,可以计算传递了多少位。
rx_en是我们提取rx的标志时刻,这时候计数器位于串行数据的中间—计数到1302时。
1.reg [13:0] rx_cnt;2.always @ ( posedge clk or posedge rst )3.if ( rst )4. rx_cnt <= 0;5.else if ( rx_change | ( rx_cnt==14'd2603 ) )6. rx_cnt <= 0;7.else8. rx_cnt <= rx_cnt + 1'b1;9.10.wire rx_en;11.assign rx_en = ( rx_cnt==14'd1301 );如果在RxD检测到0,即在rx_en等于1时,检测到rxx等于1'b0,我们知道探测到一个字节的传送开始。
这标志着后续将传送8位数据、1个奇偶校验位和1个停止位。
因此,在rx_en==1'b1,rxx==1'b0时,启动一个以10为周期的计数器。
以10为周期的计数器递进的标志是rx_en==1'b1—这是传送1个位的标志。
当计数到9时,计数终止,计数器清0,此时一个字节的数据接收完毕。
在第二次探测到rxx在rx_en有效时等于0,又将重复第二次计数,如此周而复始。
1.reg data_vld;2.always @ ( posedge clk or posedge rst )3.if ( rst )4. data_vld <= 1'b0;5.else if ( rx_en & ~rxx & ~data_vld )6. data_vld <= 1'b1;7.else if ( data_vld & ( data_cnt==4'h9 ) & rx_en )8. data_vld <= 1'b0;9.else;10.11.reg [3:0] data_cnt;12.always @ ( posedge clk or posedge rst )13.if ( rst )14. data_cnt <= 4'b0;15.else if ( data_vld )16. if ( rx_en )17. data_cnt <= data_cnt + 1'b1;18. else;19.else20.data_cnt <= 4'b0;我们在前面已经用到了这个计数器形式。
在这里,使用这种类型计数器,就是通过探测到传送开始的0位,启动一个以10为周期的计数器进行对后续位的接收,并在接收完毕后,自动恢复到初态。
在data_vld为高电平时,对应8位数据、1个奇偶校验位以及1个停止位的接收。
所以data_cnt从0到7计数时,我们可以依次接收rxx的数据。
因为rxx是从低位到高位传递,所以向右移位。
1.reg [7:0] rx_data;2.always @ ( posedge clk or posedge rst )3.if ( rst )4. rx_data <= 7'b0;5.else if ( data_vld & rx_en & ~data_cnt[3] )6. rx_data <= {rxx,rx_data[7:1]};7.else;同理,在data_vld计数到停止位时,我们认为该字节接收完毕,发送一个周期的高电平信号,通知给其他模块:表示已经接收到1字节,位于rx_data内。
其他模块在探测到rx_vld 为高电平时,取出rx_data。
1.always @ ( posedge clk or posedge rst )2.if ( rst )3. rx_vld <= 1'b0;4.else5.rx_vld <= data_vld & rx_en & ( data_cnt==4'h9);以上就是串口接收数据的设计代码。
在发送时,我们也希望能够利用rx_en这个定时信息,使用它来发送数据。
首先,我们在tx_vld==1'b1时,保存tx_data,用来发送。
tx_rdy_data 就是用来暂存tx_data的。
只有在txrdy等于1的情况下,也就是发送单元处于空闲状态时,tx_data才能保存入tx_rdy_data。
1.reg [7:0] tx_rdy_data;2.always @ ( posedge clk or posedge rst )3.if ( rst )4. tx_rdy_data <= 8'b0;5.else if ( tx_vld & txrdy )6. tx_rdy_data <= tx_data;7.else;当tx_vld有效时,会触发一个发送过程。
在发送时,tx会发送起始0位、8位数据、1个奇偶校验位和1个停止位,总共是11位。
因此,tx_vld触发了一个以11为周期的计数器,在每计数到一个数以后,会发送相应的位信息。
在发送完毕后,计数器清0。
1.reg tran_vld;2.always @ ( posedge clk or posedge rst )3.if ( rst )4. tran_vld <= 1'b0;5.else if ( tx_vld )6. tran_vld <= 1'b1;7.else if ( tran_vld & rx_en & ( tran_cnt== 4'd10 ) )8. tran_vld <= 1'b0;9.else;10.11.reg [3:0] tran_cnt;12.always @ ( posedge clk or posedge rst )13.if ( rst )14. tran_cnt <= 4'b0;15.else if ( tran_vld )16. if( rx_en )17. tran_cnt <= tran_cnt + 1'b1;18. else;19.else20.tran_cnt <= 4'b0;在上面,我们用到了同类的计数器。