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位。
(点击查看大图)图2-8 串口发送串行数据的格式示意图 上述格式只是发送串口数据最通常的格式。打开Windows自带的串口收发软件:超级终端,可以配置相关的选项。比如,每秒位数,可以设定一个周期的长度:如果我们设定为9600,则上图中1位持续的时间是:1/9600s。数据位也可以从5、6、7、8中选一位。奇偶校验可以从偶校验、奇校验、无、标记、空格中任选一个。停止位可以是:1、1.5、2。在这里,我们选择最通常的配置,如图2-9所示。
这个设计有两个目的:一是从串口中接收数据,发送到输出端口。接收的时候是串行的,也就是一个接一个的;但是发送到输出端口时,我们希望是8位放在一起,成为并行状态(见图2-10)。我们知道,串口中出现信号,是没有先兆的。如果出现了串行数据,则如何通知到输出端口呢?我们引入“接收有效”端口。“接收有效”端口在一般情况下都是低电平,一旦有数据到来时,它就变成高电平。下一个模块在得知“接收有效”信号为高电平时,它就明白:新到了一个字节的数据,放在“接收字节”端口里面。
图2-10 串口接收和发送数据时进行串并转换的示意图 二是发送数据到串口。发送数据的时候,我们也希望输入端口能够给出一个简单的形式。我们引入“发送有效”信号,它为高电平,表示我们希望把“发送字节”送入TxD发送出去。但是“发送有效”信号是否生效是有的,也就是正在发送的时候,是不能接收新的数据并发送的。所以,我们引入一个“发送状态”信号,它标识当前的“发报机”是否处于忙碌状态。如果“发报机”处于忙碌状态,则它拒绝“发送有效”信号,不予执行。
根据上面的分析,可以确定端口信号如下:
1. 2. 3. 4. 5. 6. 7. 8. 9.
module rxtx (
clk, rst, rx, tx_vld, tx_data,
rx_vld, rx_data,
10. tx, 11. txrdy 12. ); 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. 2. 3. 4. 5. 6. 7.
reg rx1,rx2,rx3,rxx;
always @ ( posedge clk ) begin rx1 <= rx; rx2 <= rx1; rx3 <= rx2; rxx <= rx3; end
对于rxx,我们将检测它的变化,这个变化将作为置位计数器的标志。rx_change表示rxx发生了改变,它比较了rx_dly和rxx的差别。
1. 2. 3. 4. 5.
reg rx_dly;
always @ ( posedge clk ) rx_dly <= rxx;
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. 2. 3. 4. 5. 6. 7. 8. 9.
reg [13:0] rx_cnt;
always @ ( posedge clk or posedge rst ) if ( rst ) rx_cnt <= 0;
else if ( rx_change | ( rx_cnt==14'd2603 ) ) rx_cnt <= 0; else
rx_cnt <= rx_cnt + 1'b1;
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. 2. 3. 4. 5. 6. 7. 8. 9.
reg data_vld;
always @ ( posedge clk or posedge rst ) if ( rst )
data_vld <= 1'b0;
else if ( rx_en & ~rxx & ~data_vld ) data_vld <= 1'b1;
else if ( data_vld & ( data_cnt==4'h9 ) & rx_en ) data_vld <= 1'b0; 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. else
20. data_cnt <= 4'b0;
我们在前面已经用到了这个计数器形式。在这里,使用这种类型计数器,就是通过探测到传送开始的0位,启动一个以10为周期的计数器进行对后续位的接收,并在接收完毕后,自动恢复到初态。在data_vld为高电平时,对应8位数据、1个奇偶校验位以及1个停止位的接收。所以data_cnt从0到7计数时,我们可以依次接收rxx的数据。因为rxx是从低位到高位传递,所以向右移位。
1. 2. 3. 4. 5. 6. 7.
reg [7:0] rx_data;
always @ ( posedge clk or posedge rst ) if ( rst )
rx_data <= 7'b0;
else if ( data_vld & rx_en & ~data_cnt[3] ) rx_data <= {rxx,rx_data[7:1]}; else;
同理,在data_vld计数到停止位时,我们认为该字节接收完毕,发送一个周期的高电平信号,通知给其他模块:表示已经接收到1字节,位于rx_data内。其他模块在探测到rx_vld为高电平时,取出rx_data。
1. 2. 3. 4. 5.
always @ ( posedge clk or posedge rst ) if ( rst )
rx_vld <= 1'b0; else
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. 2. 3. 4. 5. 6. 7.
reg [7:0] tx_rdy_data;
always @ ( posedge clk or posedge rst ) if ( rst )
tx_rdy_data <= 8'b0; else if ( tx_vld & txrdy ) tx_rdy_data <= tx_data; else;
当tx_vld有效时,会触发一个发送过程。在发送时,tx会发送起始0位、8位数据、1个奇偶校验位和1个停止位,总共是11位。因此,tx_vld触发了一个以11为周期的计数
器,在每计数到一个数以后,会发送相应的位信息。在发送完毕后,计数器清0。
1. 2. 3. 4. 5. 6. 7. 8. 9.
reg tran_vld;
always @ ( posedge clk or posedge rst ) if ( rst )
tran_vld <= 1'b0; else if ( tx_vld ) tran_vld <= 1'b1;
else if ( tran_vld & rx_en & ( tran_cnt== 4'd10 ) ) tran_vld <= 1'b0; 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. else
20. tran_cnt <= 4'b0;
在上面,我们用到了同类的计数器。这类计数器通常由一个电平触发,然后持续固定的时间,这样就能做到收发自如,可控性比较好。下面,我们根据计数器计数值发出tx信息。tx的电平会在每次rx_en有效的时候改变。
1. 2. 3. 4. 5. 6. 7. 8. 9.
reg tx;
always @ ( posedge clk or posedge rst ) if ( rst ) tx <= 1'b1; else if ( tran_vld ) if ( rx_en )
case ( tran_cnt ) 4'd0 : tx <= 1'b0;
4'd1 : tx <= tx_rdy_data[0];
10. 4'd2 : tx <= tx_rdy_data[1]; 11. 4'd3 : tx <= tx_rdy_data[2]; 12. 4'd4 : tx <= tx_rdy_data[3]; 13. 4'd5 : tx <= tx_rdy_data[4]; 14. 4'd6 : tx <= tx_rdy_data[5]; 15. 4'd7 : tx <= tx_rdy_data[6]; 16. 4'd8 : tx <= tx_rdy_data[7];
17. 4'd9: tx <= ^tx_rdy_data; 18. 4'd10: tx <= 1'b1; 19. default: tx <= 1'b1; 20. endcase 21. else; 22. else 23. tx<= 1'b1;
最后,给出txrdy的描述。txrdy对外界提供一个信息:不能在txrdy为低电平时传送新的数据,因为发送单元正处于传送数据的过程中,不能接收新的数据了。
1.
assign txrdy = ~tran_vld;
如果在上面的代码的基础上再加上endmodule,就是一个完整的Verilog RTL设计了。