大家好,这几天在各个论坛上,经常就有人在向我咨询基于FPGA的串口通信代码,大部分都是在网上下载一个现成的代码,但是在使用中就遇到了各种问题,于是就发到了论坛上来求助。在阅读了他们的代码之后,我发现几乎出自同一个版本(目前确定为特权同学的基于EPM240入门实验的代码)。他们在调试这个代码的时候,经常存在这样几个问题:1、部分人对该串口通讯模块完全不理解,对每句话,甚至每个模块的功能都不理解;2、部分人采用最原始的画波形的方式来对该模块进行仿真,结果无法得到仿真结果;3、部分人不会使用modelsim对该设计进行仿真;4、绝大部分人不会编写
testbench;5、下板测试无法进行正确的字符串收发。在公司内部,我将这种现象和几位老师交流之后,夏宇闻老师建议我专门针对该代码写一个由原理到代码,由仿真到板级的调试笔记。争取用最通俗,也是最笨的办法,手把手的教会大家来调试这个代码。 本调试笔记主要由五个部分组成:原始代码分析;原始代码验证;对原始代码进行修改;对修改后的代码进行验证;对修改后的设计进行板级验证。每个部分,小梅哥都会用图文结合的方式,教大家一步一步的来进行。 一、 原始代码分析
该代码来自小梅哥最崇拜的大神,特权同学。当时小梅哥也是看着特权同学的书和视频教程一步一步走过来的。特权同学的代码实现了单字节的收发测试,没有对连续字节的收发进行测试。特权同学当时也说过,这个只是一个简单的实验,离实际工业应用还有一定的距离。考虑到论坛上很多小伙伴都希望能够实现连续字节的收发功能,因此小梅哥就在特权同学的代码上进行了修改。修改后的代码,输入时钟可以在一定范围内选择任意频率,目前已经支持5种波特率选择(9600、19200、38400、57600、115200),实际小梅哥还做过更高波特率的测试,目前实测在115200波特率的速率下可以实现超过9999999次连续无间断的收发。这里,小梅哥首先将特权同学设计架构在这里列出来,以给读者一个直观的印象。 rs232_rx clk rst_n clk rst_n clk rst_n bps_start1 clk_bps1 clk rst_n rx_int rs232_tx my_uart_rx rx_data clk_bps2 my_uart_tx speed_rx clk rst_n bps_start2 speed_tx
由上图可知,特权同学的UART串口设计主要包含了四个模块:串口发送模块
(my_uart_tx)、串口接收模块(my_uart_rx)、串口接收波特率发生器(speed_rx)和串口发送波特率发生器(speed_tx),其中,串口发送波特率发生器主要用来产生串口发送模块发送数据时所需的波特率时钟,串口接收波特率发生器主要用来产生串口接收模块接收数据时的波特率时钟,串口发送模块主要负责在指定波特率的速率下将待发送字节发送出去,串口接收模块则主要负责接收来自其他设备发送过来的串口数据。my_uart_top模块即串口收发顶层模块实现了各个模块间的信号连接功能,通过该顶层模块的连接,实现了将串口接收到的数据再发送出去的功能,即我们测试串口通信最常用的一种方式——回环测试。特权同学该实验的各个端口和内部信号的意义如表1所示: 所属模块
端口/信号名 端口/信号意义 clk 系统时钟,本设计中,固定为50M频率的时钟 rst_n 系统复位,低电平复位 rs232_rx rs232_tx clk rst_n rs232_rx
接收模块
my_uart_rx
rx_data rx_int clk_bps bps_start clk rst_n rx_data
发送模块 my_uart_tx
rx_int rs232_tx clk_bps bps_start
收发波特率发生模
clk
rst_n
块
speed_tx/speed_rx clk_bps
串口接收端口 串口发送端口
系统时钟,本设计中,固定为50M频率的时钟 系统复位,低电平复位 串口接收端口
串口接收模块接收到的字节数据
串口接收成功中断请求信号,为一表示正在接收数据 波特率时钟
波特率发生模块使能信号
系统时钟,本设计中,固定为50M频率的时钟 系统复位,低电平复位
待发送的字节数据,来自串口接收模块 串口接收成功中断请求信号 串口发送端口 波特率时钟
波特率发生模块使能信号
系统时钟,本设计中,固定为50M频率的时钟 系统复位,低电平复位 波特率时钟
顶层模块 my_uart_top
bps_start
波特率发生模块使能信号
该实验的内容为,串口接收模块在检测到发送设备发送过来的数据起始位时,接收中断信号置1,该信号则作为启动串口接收波特率发送器的控制信号,然后在每个波特率时钟上升沿到来时读取串口接收端口(rs232_rx)上的数据。一帧(字节)数据接收完成后,接收中断信号拉低,停止波特率发生器工作,接收完成,系统进入等待状态,等待下一次的数据到来。
同时,接收中断信号的下降沿也作为串口发送模块的发送使能信号,因为一旦接收中断信号的下降沿出现,就表明接收模块完成了一次数据的接收,此时,就开始使能串口发送波特率发生器,串口发送模块则在波特率时钟的上升沿到来时依次将接收模块接收到的数据的每一位(发送模块自动添加起始位和停止位)依次发送出去,当数据发送完成之后,停止串口发送波特率发生器的使能,模块进入等待状态,等待下一次接收中断信号下降沿的到来。
这里,我们首先对该设计的波特率发生器模块进行分析。该模块相对简单,代码如下所示:
1 module speed_select ( 2 clk, rst_n ,
3 bps_start , clk_bps 4 ); 5
6 input clk; // 50MHz 7 input rst_n ; // 8 input bps_start ; //
9 output clk_bps ; // clk_bps 10 11 /*
12 parameter bps9600 = 5207, // 9600bps 13 bps19200 = 2603, // 19200bps 14 bps38400 = 1301, // 38400bps 15 bps57600 = 867, // 57600bps 16 bps115200 = 433; // 115200bps 17
18 parameter bps9600_2 = 2603, 19 bps19200_2 = 1301, 20 bps38400_2 = 650,
21 bps57600_2 = 433, 22 bps115200_2 = 216; 23 */ 24 25 //
26 `define BPS_PARA 5207 // 9600 27 `define BPS_PARA_2 2603 // 9600 28
29 reg[ 12 : 0] cnt ; // 30 reg clk_bps_r ; // 31
32 //--------------------------------------------------------- 33 reg[ 2 : 0] uart_ctrl ; // uart
34 //--------------------------------------------------------- 35
36 always @ ( posedge clk or negedge rst_n ) 37 if(! rst_n ) cnt <= 13'd0 ;
38 else if(( cnt == `BPS_PARA ) || ! bps_start ) cnt <= 13'd0 ; 39 else cnt <= cnt+1'b1 ; // 40
41 always @ ( posedge clk or negedge rst_n ) 42 if(! rst_n ) clk_bps_r <= 1'b0 ;
43 else if( cnt == `BPS_PARA_2 ) clk_bps_r <= 1'b1 ; //clk_bps_r ,
44 else clk_bps_r <= 1'b0 ; 45
46 assign clk_bps = clk_bps_r ; 47
48 endmodule
该代码的12-22行用注释的方式告诉了我们,获得不同波特率时波特率分频计数器的值应该为多少,以及波特率计数器计数到一半时的值为多少(该值作为对信号的采样点,因为一般情况下,一位数据,在该位数据保持时间的中间时刻是最稳定的)。26行和27行定义的两个参数BPS_PARA和BPS_PARA_2分别就是波特率分频计数器的最大值和中间值。实际使用时,只需要根据你所需要的波特率,更改这两个参数的值即可 。例如,
选择波特率为9600bps时,设定BPS_PARA=5207,BPS_PARA_2=2603。关于这个值的计算,这里暂时不提,后文会有交代。
36行至39行为波特率分频计数器的计数进程,即波特率发生模块没有被使能(! bps_start)或者计数器计数到BPS_PARA后则将计数器清零,其它情况下则每来一个时钟上升沿计数器自加1。通过此进程,则可得到相对精准的波特率定时。
41行至44行为数据采样时刻的生成,上面提到过“一般情况下,一位数据,在该位数据保持时间的中间时刻是最稳定的”,因此这里我们在计数器计数到一半时,产生一个时钟周期的高脉冲,此脉冲作为采样数据的使能信号。
以上为波特率发生器的代码及分析,波特率发生模块在例化时被分别例化为串口发送波特率发生器和串口接收波特率发生器。接下来我们再来分析串口接收模块的代码。
1 module my_uart_rx ( 2 clk, rst_n ,
3 rs232_rx , rx_data , rx_int , 4 clk_bps , bps_start 5 ); 6
7 input clk; // 50MHz 8 input rst_n ; //
9 input rs232_rx ; // RS232 10 input clk_bps ; // clk_bps 11 output bps_start ; //
12 output [ 7: 0] rx_data ; // 13 output rx_int ; // , 14
15 //--------------------------------------------------------- 16 reg rs232_rx0 , rs232_rx1 , rs232_rx2 , rs232_rx3 ; // 17 wire neg_rs232_rx ; // 18
19 always @ ( posedge clk or negedge rst_n ) begin 20 if(! rst_n ) begin 21 rs232_rx0 <= 1'b0 ; 22 rs232_rx1 <= 1'b0 ; 23 rs232_rx2 <= 1'b0 ; 24 rs232_rx3 <= 1'b0 ; 25 end
26 else begin 27 rs232_rx0 <= rs232_rx ; 28 rs232_rx1 <= rs232_rx0 ; 29 rs232_rx2 <= rs232_rx1 ; 30 rs232_rx3 <= rs232_rx2 ; 31 end 32 end
33 // <20ns-40ns ( ) 34 //
35 // 40ns
36 assign neg_rs232_rx = rs232_rx3 & rs232_rx2 & ~rs232_rx1 &
~rs232_rx0; // neg_rs232_rx 37
38 //--------------------------------------------------------- 39 reg bps_start_r ; 40 reg[ 3: 0] num; // 41 reg rx_int ; // , 42
43 always @ ( posedge clk or negedge rst_n ) 44 if(! rst_n ) begin 45 bps_start_r <= 1'bz ; 46 rx_int <= 1'b0 ; 47 end
48 else if( neg_rs232_rx ) begin // rs232_rx 49 bps_start_r <= 1'b1 ; // 50 rx_int <= 1'b1 ; // 51 end
52 else if( num==4'd12 ) begin // 53 bps_start_r <= 1'b0 ; // 54 rx_int <= 1'b0 ; // 55 end 56
57 assign bps_start = bps_start_r ; 58
59 //--------------------------------------------------------- 60 reg[ 7 : 0] rx_data_r ; //
61 //--------------------------------------------------------- 62
63 reg[ 7 : 0] rx_temp_data ; // 64
65 always @ ( posedge clk or negedge rst_n ) 66 if(! rst_n ) begin 67 rx_temp_data <= 8'd0 ; 68 num <= 4'd0 ; 69 rx_data_r <= 8'd0 ; 70 end
71 else if( rx_int ) begin // 72 if( clk_bps ) begin // , 8bit 1 2 73 num <= num+1'b1 ; 74 case ( num) 75 4'd1:rx_temp_data[0] <= rs232_rx; // 0bit 76 4'd2:rx_temp_data [1] <= rs232_rx; // 1bit 77 4'd3:rx_temp_data [2] <= rs232_rx; // 2bit 78 4'd4:rx_temp_data [3] <= rs232_rx; // 3bit 79 4'd5:rx_temp_data [4] <= rs232_rx; // 4bit 80 4'd6:rx_temp_data [5] <= rs232_rx; // 5bit 81 4'd7:rx_temp_data [6] <= rs232_rx; // 6bit 82 4'd8:rx_temp_data [7] <= rs232_rx; // 7bit 83 default : ; 84 endcase 85 end 86 else if( num == 4'd12 ) begin // 1+8+1(2)=11bit 87 num <= 4'd0 ; // STOP ,num
88 rx_data_r <= rx_temp_data ; // rx_data 89 end 90 end 91
92 assign rx_data = rx_data_r ; 93
94 endmodule
第19行到第36行为起始位检测部分,19到32行,实现了对rs232_rx端口上电平的连续四个时钟周期的寄存,第36行则对这连续4个时钟上升沿时的rs232_rx端口电平进行逻辑操作,得出rs232_rx端口信号下降沿的到来。neg_rs232_rx = rs232_rx3 & rs232_rx2 & ~rs232_rx1 & ~rs232_rx0,即后两次寄存的状态为低电平而前两次寄存的装填为高电平,则表明该端口上的信号发生了1->0的跳变,即有下降沿出现。neg_rs232_rx信号会产生一个周期的高脉冲。
第43行至第55行则根据neg_rs232_rx和num计数值来控制串口接收波特率发生器的工作和接收中断信号。第65行至第90行则采用线性序列机的设计方式,进行一个字节的数据的接收。
以上为对串口接收模块的一个简单分析,接下来,再进行串口发送模块的分析。
1 module my_uart_tx ( 2 clk, rst_n ,
3 rx_data , rx_int , rs232_tx , 4 clk_bps , bps_start 5 ); 6
7 input clk; // 50MHz 8 input rst_n ; //
9 input clk_bps ; // clk_bps_r , 10 input [ 7 : 0] rx_data ; // 11 input rx_int ;
12 output rs232_tx ; // RS232 13 output bps_start ; // 14
15 //--------------------------------------------------------- 16 reg rx_int0 , rx_int1 , rx_int2 ; //rx_int 17 wire neg_rx_int ; // rx_int 18
19 always @ ( posedge clk or negedge rst_n ) begin 20 if(! rst_n ) begin 21 rx_int0 <= 1'b0 ; 22 rx_int1 <= 1'b0 ; 23 rx_int2 <= 1'b0 ; 24 end
25 else begin 26 rx_int0 <= rx_int ; 27 rx_int1 <= rx_int0 ;
28 rx_int2 <= rx_int1 ; 29 end 30 end 31
32 assign neg_rx_int = ~rx_int1 & rx_int2 ; // neg_rx_int 33
34 //--------------------------------------------------------- 35 reg[ 7 : 0] tx_data ; //
36 //--------------------------------------------------------- 37 reg bps_start_r ; 38 reg tx_en ; // 39 reg[ 3: 0] num; 40
41 always @ ( posedge clk or negedge rst_n ) begin 42 if(! rst_n ) begin 43 bps_start_r <= 1'bz ; 44 tx_en <= 1'b0 ; 45 tx_data <= 8'd0 ; 46 end
47 else if( neg_rx_int ) begin // 48 bps_start_r <= 1'b1 ; 49 tx_data <= rx_data ; // 50 tx_en <= 1'b1 ; // 51 end
52 else if( num==4'd11 ) begin // 53 bps_start_r <= 1'b0 ; 54 tx_en <= 1'b0 ; 55 end 56 end 57
58 assign bps_start = bps_start_r ; 59
60 //--------------------------------------------------------- 61 reg rs232_tx_r ; 62
63 always @ ( posedge clk or negedge rst_n ) begin 64 if(! rst_n ) begin 65 num <= 4'd0 ; 66 rs232_tx_r <= 1'b1 ; 67 end
68 else if( tx_en ) begin 69 if( clk_bps ) begin 70 num <= num+1'b1 ; 71 case ( num) 72 4'd0 : rs232_tx_r <= 1'b0 ; // 73 4'd1 : rs232_tx_r <= tx_data [0]; // bit0 74 4'd2 : rs232_tx_r <= tx_data [1]; // bit1 75 4'd3 : rs232_tx_r <= tx_data [2];// bit2 76 4'd4 : rs232_tx_r <= tx_data [3]; // bit3 77 4'd5 : rs232_tx_r <= tx_data [4];// bit4 78 4'd6 : rs232_tx_r <= tx_data [5]; // bit5 79 4'd7 : rs232_tx_r <= tx_data [6]; // bit6 80 4'd8 : rs232_tx_r <= tx_data [7]; // bit7 81 4'd9 : rs232_tx_r <= 1'b1 ; // 82 default : rs232_tx_r <= 1'b1 ; 83 endcase
84 end 85 else if( num==4'd11 ) num <= 4'd0 ; // 86 end 87 end 88
89 assign rs232_tx = rs232_tx_r ; 90
91 endmodule
代码19行到30行对串口接收模块的接收中断信号进行了3次寄存,第32行则通过对连续两次寄存结果的判断,来检测接收中断信号rx_int的下降沿。如果有下降沿到来,neg_rx_int信号则会产生一个时钟周期的高脉冲信号,第47行则通过对该信号的状态判断,来确定是否启动发送波特率发生器模块。如果检测到了该高脉冲,则使能串口发送(tx_en <= 1'b1),同时将待发送的字节数据(rx_data)寄存到发送寄存器(tx_data)中。第52行,即计数到11后,表明一个字节的数据发送完成,此时停止发送波特率发生器的工作,同时禁止发送模块的工作(tx_en <= 1'b0),即完成一个字节的数据的发送。
第63行至第87行采用线性序列机设计方式,实现一个字节(自动添加起始位和停止位)的发送操作。
以上就是对特权同学的串口代码的一个简单分析,顶层模块就只进行了一个简单的例化和连线作用,因此小梅哥就不做分析了。小梅哥个人能力有限,可能存在理解不到位或者表述不专业的地方,不妥之处还望各位多多指出,以促进共同学习。可以说,相对于淘宝网上漫天飞舞的开发板和五花八门的串口例程,特权同学的设计代码相当规范,架构合理,思路清楚,确实是值得我们每一个FPGA爱好者学习。
二、 原始代码验证
前面,通过对设计代码的一个简单分析,弄清楚了特权同学设计代码的基本架构和思路。那么看过特权同学教学视频的都知道,该代码能够实现一个字节的数据收发测试。那么这里,小梅哥就先对该设计进行一个仿真,通过仿真来分析此设计的性能。
仿真的思路很简单,就是通过模拟串口发送过程,给该设计模块发送数据,由前面分析可知,该设计模块接收到数据后,会立即将数据发送出去,因此我们还需要对串口发送出来的数据进行分析,这里,熟悉Uart协议的,我们可以直接观察发送波形。当然,为了更加直观,我们也可以设计一个模拟串口接收数据的仿真模型,通过该模块来读取串口发送出来的数据。考虑到看这篇文章的大多是初学者,为了让大家能够更好的查看我们的仿真结果,同时教大家进行仿真模型的设计,小梅哥还是自己编写了一个虚拟的串口仿真模型。验证时,只需要将该仿真模型挂接到串口模块上,则该模型便能够自动
的给串口模块发送数据,同时接收串口发送过来的数据。并会实时的将发送的数据和接收的数据打印出来,实际在观察仿真结果时,我们便只需要观看打印的结果就可以了。该串口仿真模型的代码如下所示:
1 `timescale 1ns/1ps 2
3 module Uart_module ( uart_rx , uart_tx , send_state ); 4
5 input uart_rx ;
6 output reg uart_tx ; 7 output reg send_state ; 8
9 reg Clk; 10 reg Rst_n ; 11
12 wire Mid_Flag_send ; 13 wire Mid_Flag_Receive ; 14
15 reg Receive_Baud_Start ; 16 reg [ 7 : 0] rx_data ; 17
18 initial Clk = 1;
19 always #10 Clk = ~Clk; 20
21 speed_select speed_select_Send ( 22 . clk( Clk), 23 . rst_n ( Rst_n ), 24 . bps_start ( 1'b1 ), 25 . clk_bps ( Mid_Flag_send ) 26 ); 27
28 speed_select speed_select_receive ( 29 . clk( Clk), 30 . rst_n ( Rst_n ), 31 . bps_start ( Receive_Baud_Start ), 32 . clk_bps ( Mid_Flag_Receive ) 33 ); 34
35 initial begin 36 Rst_n = 0; 37 uart_tx = 1; 38 send_state = 0; 39 #300 Rst_n = 1; 40
41 $display ( \"Set Baud As 9600bps\" ); 42 #200 ; Uart_Send ( 8'hb6 ); 43 #20 ; Uart_Send ( 8'he7 ); 44 #80 ; Uart_Send ( 8'hf0 ); 45 #500 ; Uart_Send ( 8'h02 ); 46 #300 ; Uart_Send ( 8'hb4 ); 47 #40 ; Uart_Send ( 8'he5 ); 48 #90 ; Uart_Send ( 8'hb0 ); 49 #1000 ; Uart_Send ( 8'h32 );
50 #2000000 ; 51 $stop ; 52 end 53
54 task Uart_Send ; 55 input [ 7: 0] Data ; 56 begin 57 send_state = 1; 58 @( posedge Mid_Flag_send) #0.1 uart_tx = 0; 59 @( posedge Mid_Flag_send) #0.1 uart_tx = Data [0]; 60 @( posedge Mid_Flag_send) #0.1 uart_tx = Data [1]; 61 @( posedge Mid_Flag_send) #0.1 uart_tx = Data [2]; 62 @( posedge Mid_Flag_send) #0.1 uart_tx = Data [3]; 63 @( posedge Mid_Flag_send) #0.1 uart_tx = Data [4]; 64 @( posedge Mid_Flag_send) #0.1 uart_tx = Data [5]; 65 @( posedge Mid_Flag_send) #0.1 uart_tx = Data [6]; 66 @( posedge Mid_Flag_send) #0.1 uart_tx = Data [7]; 67 @( posedge Mid_Flag_send) #0.1 uart_tx = 1; 68 $display ( \"Uart_Send Data = %0h\" , Data ); 69 send_state = 0; 70 end 71 endtask 72
73 initial begin 74 forever begin
75 @( negedge uart_rx ) 76 begin 77 Receive_Baud_Start = 1; 78 @( posedge Mid_Flag_Receive); 79 @( posedge Mid_Flag_Receive) rx_data [0] = uart_rx ; 80 @( posedge Mid_Flag_Receive) rx_data [1] = uart_rx ; 81 @( posedge Mid_Flag_Receive) rx_data [2] = uart_rx ; 82 @( posedge Mid_Flag_Receive) rx_data [3] = uart_rx ; 83 @( posedge Mid_Flag_Receive) rx_data [4] = uart_rx ; 84 @( posedge Mid_Flag_Receive) rx_data [5] = uart_rx ; 85 @( posedge Mid_Flag_Receive) rx_data [6] = uart_rx ; 86 @( posedge Mid_Flag_Receive) rx_data [7] = uart_rx ; 87 @( posedge Mid_Flag_Receive) Receive_Baud_Start = 0; 88 $display ( \"Uart_receive Data = %0h\" , rx_data ); 89 end 90 end 91 end 92
93 endmodule 94
因为在将代码复制到word的过程中,会有一定的格式兼容问题,所以文中部分格式不是太规范,望各位理解,另外,完整的代码,小梅哥也以pdf的形式提供了,感兴趣的朋友可以下载学习。
本仿真模型的第一句话“`timescale 1ns/1ps”为仿真精度及时间的说明,定义时间精度为1ps,时间单位为1ns,那么我们在代码编写的过程中,如果写成“#200”则表示延时200ns,因为时间精度为1ps,因此我们还可以进一步提高延时精度,如
“#200.1”表示延时200.1ns。一般的测试文件(testbench)中,这句话作为第一句话,必写,当然,时间精度和单位我们可以根据自己的需求更改,如写成“`timescale 1us/1ns”或者“`timescale 1ns/1ns”等都是可以的。 该模块作为一个仿真模型,就是虚拟了一个串口收发仪器,既然是一个串口收发仪器,则必然有串口发送端口和串口接收端口,因此在模块名后面定义了三个端口。这与一般的testbench不同,一般的testbench作为仿真时的顶层,不需要端口,因此模块名后就直接以“;”结束。该模块的三个端口“uart_rx , uart_tx ,
send_state”分别为串口接收端口、串口发送端口、串口发送状态信号。串口收发端口不用说,大家也已经知道了,串口发送状态信号主要作为指示信号,指示当前仿真模型正在进行数据的发送过程。
第9行至第16行为测试文件中信号的定义,以前我们总是理解说这些信号就是待测试模块的端口,需要在测试文件中定义。那么这里小梅哥更喜欢换一种方式来理解:我们自己的设计,本设计中即特权的串口模块,是一个功能未知的黑盒子,这个黑盒子有一些信号线引出,有的信号线是作为输入的,即需要外部输入一定的信号作为激励,而有的信号线是作为输出的,能够输出一些数据,当然还有一些信号线是既能够作为输入,又能够作为输出的,即三态。我们要想知道这个黑盒子的功能,就需要给这个黑盒子的输入信号线接上信号源,通过给这些输入信号线一定的激励,观察其输出端口上的响应,从而获知该黑盒子的功能。那么在这里,对于待测试模块的输入端口,我们就接上信号发生器,对于输出端口,我们就接上示波器或者逻辑分析仪,这样,我们就能够通过信号发生器给输入端口产生一定的激励,然后通过示波器观察输出端口的输出了。即如下图所示:
那么,我们的testbench主要实现信号发生器的功能,既然是信号发生器那么就一定有数据信号输出,这个数据信号输出就可以连接到我们的待测模块上。待测试模块的输出端口,连接到我们的示波器或者逻辑分析仪的探头上,这样就实现了一个完整的测试系统,那么我们信号发生器的信号源,可能命名叫做,a,b,c,d,e……. 而我们示波器的探头则命名为探头1,探头2……接下来就好理解了,在testbench,我们将信号发生器的输出信号定义为reg型,而示波器的探头定义为wire型。我们信号发生器的输出信号线和示波器的探头线都可以任意命名,实际使用时一一对应连接到待测试模块的
信号发生器 待测模块 示波器/逻辑分析仪 端口上,也可以就直接与待测模块的各个端口名保持一致。本设计中,小梅哥让testbench中的信号与待测模块的端口保持一致。 第18行和19行为产生50MHz时钟的语句。
因为本仿真模型实质上就是一个串口收发模块,因此也需要有收发波特率发生器,这里小梅哥为了省事,直接调用了特权同学的波特率发生器模块,来作为我仿真模型的波特率发生器。因为该波特率发生器本身也属于待测试部分,小梅哥之所以敢放心的调用,是因为事先我已经通过仿真,确定了该波特率发生器功能的正确性。第21行至33行为分别例化得到发送波特率发生器和接收波特率发生器的代码。
第54行至71行为发送一个完整字节的数据(自动添加起始位和停止位)的代码,该部分写成任务的形式,方便调用。当我们需要发送一个字节的数据时,例如,发送8'hb6,只需要写“Uart_Send ( 8'hb6 )”即可,该任务便将自动执行,将数据发送出去。在一个字节的数据发送完成后,同时使用系统任务$display来打印当前发送的数据是多少,以方便我们直观的观察仿真运行过程。至于$display这个系统任务中各个部分的含义,请读者自行阅读verilog的语法书。代码的42至49行便是调用此任务进行了多次数据的发送。
73行至91行为模拟串口接收部分,通过对串口模块发送出来的数据进行接收,并将接收到的数据用$display函数打印出来。我们只需要阅读发送数据和接收数据后打印出来的信息,即可判断通信是否成功,待测模块功能是否正常。
这里需要注意的是,打印出来的接收数据和发送数据是针对仿真模型来说的,send data是仿真模型发送出去的数据,对应待测模块应该接收到的数据。receive data则是仿真模型接收到的数据,对应待测模块发送的数据。
我们所编写的测试文件,一定要是可控的,即在所有事务完成后,将仿真停下来,否则,仿真会一直进行下去,导致出现大量冗余波形,影响我们对仿真结果的分析。因此在第51行,当所有测试已经完成后,使用系统任务$stop将仿真停下来。
以上对小梅哥写的串口仿真模型进行了介绍,在实际使用中,只需要将该模型与待测模块按照如下图所示的方式连接起来即可。
clk rst_n rs232_tx uart_rx my_uart_top rs232_rx uart_tx Uart_module send_state
这里,小梅哥使用一个testbench文件作为顶层,将这两个部分连接起来,同时产生my_uart_top工作所需的时钟和复位信号。该文件详细代码如下:
1 `timescale 1ns /1ns 2
3 module Uart_tb ; 4
5 reg Clk; 6 reg Rst_n ; 7
8 wire uart_rx ; 9 wire uart_tx ; 10 wire send_state ; 11
12 my_uart_top u1 ( 13 . clk( Clk), 14 . rst_n ( Rst_n ), 15 . rs232_rx ( uart_tx ), 16 . rs232_tx ( uart_rx ) 17 ); 18
19 Uart_module u2 ( 20 . uart_rx ( uart_rx ), 21 . uart_tx ( uart_tx ), 22 . send_state ( send_state ) 23 ); 24
25 initial begin 26 Clk = 1; 27 Rst_n = 0; 28 #200 ; 29 Rst_n = 1; 30 end 31
32 always #10 Clk = ~Clk; 33
34 endmodule 35
该代码实在简单,只是实现了一个启动时的初始化和50MHz时钟的产生,因此小梅哥就不做任何分析了。
以上为小梅哥为了对特权同学的串口收发模块进行测试所展开的部分工作,到这里,仿真测试所需要的准备工作我们就做好了,接下来将实际进行仿真,通过仿真来分析该模块的性能。
这里极力推荐大家使用modelsim进行仿真,因为quartusII自带的仿真工具灵活性和功能都赶modelsim相差甚远。Modelsim作为一款强大的仿真软件,在业界被广泛使用。同时,modelsim针对不同的EDA厂家,也推出了OEM版本,modelsim-altera就是为Altera公司开发的OEM版本,此版本针对Altera公司的器件预先做了许多的工作,使我
们使用的时候能够更加的快捷方便,这里,小梅哥就使用modelsim-Altera版本来仿真这个设计。
我们可以使用modelsim-Altera,通过完全手动化的方式来建立仿真工程,添加仿真库、编译文件,添加波形,运行仿真,当然也可以使用Nativelink的方式,通过quartus II软件实现一键调用,实现自动化的仿真过程。这里,对于初学者,小梅哥还是推荐采用这种自动化的方式,因为简单,不需要过多的手动操作,且不容易出错。这里,小梅哥也使用这种方式来进行仿真。
要使用这种方式仿真,我们需要在QuartusII软件中进行EDA Tools的设置。在quartus II软件中,依次点击“tools – options ”,在打开的选项卡中选择“EDA tool options”,在modelsim-altera一栏中输入你的modelsim-Altera的安装路径(部分在安装的时候就已经自动设置好了,就不用去管),如下图所示:
这里设置完成后,点击OK即可。
当确认这一步没有问题后,我们就可以开始进行Nativelink的设置了。Nativelive就是一个链接的过程,通过设置Nativelink,让Quartus II软件能够自动的调用modelsim-altera软件,并建立仿真工程,添加仿真库、编译文件,添加波形,运行仿真。接下来我们就来进行Nativelink的设置
第一步:依次点击“assignments – settings”,
在弹出的选项卡中,选中“EDA Tool Settings”(红色标号1处),在子选项中选择“Simulation”(红色标号2处),注意红色标号5处和6处应分别选择为modelsim-altera和Verilog HDL,如果不是,请通过下拉列表选择为上述选项。点击“Compile test bench” (红色标号3处),然后点击最右侧的“Test Benchs” (红色标号4处),就会弹出如下所示的界面:
点击“New” (红色标号1处),接着会弹出“New Test Bench Settings”界面,如下图所示:
首先我们点击红色标号1处的三个小点,在弹出的文件选择界面中,选中我们需要加入的testbench文件,如下所示:
这里我们选择“Uart_tb.v”和“Uart_module.v”,点击“Open”打开。这里,“Uart_tb.v”为仿真顶层文件,“Uart_module.v”为串口仿真模型。 文件选择好之后,回到New Test Bench Settings界面,如下图所示,
在顶端“test bench name”(红色标号2)处输入我们的仿真顶层文件名,即Uart_tb,注意,不要加“.v”后缀。于是,”Top level module in test bench”会自动与“Test bench name”保持一致。
至此,我们的Nativelink设置就完成了,一路选择OK下去,直到回到Quartus II软件的主界面。此时,我们已经完成了仿真需要的所有设置,我们直接点击Quartus II软件上的RTL Simulation图标
即可启动仿真,也可以通过“Tools – Run Simulation Tool – RTL Simulation”启动仿真。 接下来,我们需要做的就是大约20秒左右的等待,这个过程中,Quartus II会自动启动modelsim-altera软件,建立仿真工程,添加仿真库、编译文件,添加波形,运行仿真。最后停留在如下所示的状态:
这里,右侧深色窗口为波形窗口,下方为副本界面,打印了软件运行过程中的信息,包括添加文件、编译文件、添加波形、运行等,同时还打印了testbench中需要打印输出的信息。具体的打印信息如下:
由此打印信息可知,串口仿真模型总共进行了8次数据发送,却只收到了6次数据,因此有两次发送给串口模块的数据丢失或者串口模块发送的数据不正确。同时,第一次发送出去的数据接收回来还是正确的,但是第二次发送的数据再接收回来,就错误了。这两次数据发送之间的时间间隔为40ns。考虑可能是发送间隔太短导致,但是当第4次发送的数据(s4)被正确接收后(r3),紧接着第5次发送的数据在发回的时候,再次出错,而s4和s5之间间隔了300ns,因此可知,该串口收发模块在连续两次发送间隔很短的情况下,很容易出错。那么,怎么样才能保证连续两次发送之间,即使很短的间隔,
也不出错误呢?小梅哥通过对仿真波形的分析发现:一次接收数据,总共有12个波特率脉冲,如下图所示:
这里小梅哥就有点疑惑了,我们一般的应用中,串口一帧的数据为十位,包含1位起始位、8位数据位、一位停止位,一般不含校验位。因此,这里明显多了两个波特率周期,那么,问题很有可能就出在这里。回到这部分的代码:
43 always @ ( posedge clk or negedge rst_n ) 44 if(! rst_n ) begin 45 bps_start_r <= 1'bz ; 46 rx_int <= 1'b0 ; 47 end
48 else if( neg_rs232_rx ) begin // rs232_rx 49 bps_start_r <= 1'b1 ; // 50 rx_int <= 1'b1 ; // 51 end
52 else if( num==4'd12 ) begin // 53 bps_start_r <= 1'b0 ; // 54 rx_int <= 1'b0 ; // 55 end 56
57 assign bps_start = bps_start_r ; 58
59 //--------------------------------------------------------- 60 reg[ 7 : 0] rx_data_r ; //
61 //--------------------------------------------------------- 62
63 reg[ 7 : 0] rx_temp_data ; // 64
65 always @ ( posedge clk or negedge rst_n ) 66 if(! rst_n ) begin 67 rx_temp_data <= 8'd0 ; 68 num <= 4'd0 ; 69 rx_data_r <= 8'd0 ; 70 end
71 else if( rx_int ) begin // 72 if( clk_bps ) begin // , 8bit 1 2 73 num <= num+1'b1 ; 74 case ( num) 75 4'd1:rx_temp_data[0] <= rs232_rx; // 0bit 76 4'd2:rx_temp_data [1] <= rs232_rx; // 1bit 77 4'd3:rx_temp_data [2] <= rs232_rx; // 2bit 78 4'd4:rx_temp_data [3] <= rs232_rx; // 3bit 79 4'd5:rx_temp_data [4] <= rs232_rx; // 4bit 80 4'd6:rx_temp_data [5] <= rs232_rx; // 5bit 81 4'd7:rx_temp_data [6] <= rs232_rx; // 6bit
82 83 84 85 4'd8:rx_temp_data [7] <= rs232_rx; // 7bit default : ; endcase end
else if( num == 4'd12 ) begin//我们的标准接收模式下只有
86
1+8+1(2)=11bit的有效数据 87 num <= 4'd0 ; // STOP ,num 88 rx_data_r <= rx_temp_data ; // rx_data 89 end 90 end
那么,特权同学在这里有通过注释解释为什么是12个波特率周期:“我们的标准接收模式下只有1+8+1(2)=11bit的有效数据”。即使是11位,小梅哥还是无法理解为什么需要12个波特率时钟。我们在实际使用的时候,一般不去考虑校验位的问题,因此总共只有10位,也就是说,我写的10位数据位的仿真模型和特权的11位方式不兼容。考虑到大多数的应用过程中是10位,因此小梅哥在特权大神的代码上稍作修改,以适应10位模式。这里,将第52行代码“else if( num==4'd12 ) begin ”改为“else if( num==4'd10 ) begin ”,将第86行代码“else if( num == 4'd12 ) begin”改为“else if( num == 4'd10 ) begin”。通过这样更改,就能够适应1bit的数据接收。同时,小梅哥在实际调试中发现,第45行的“bps_start_r <= 1'bz ;”代码也存在问题,在复位时,给了bps_start_r一个高阻态,这样在实际仿真时,会导致错误,因此小梅哥在这里,将这一行代码进行了修改,复位时将bps_start_r置为0,即修改为“bps_start_r <= 1'b0 ;”。 以上为接收部分的修改。发送部分的波特率波形如下所示:
同样有11个波特率时钟,因此,小梅哥将发送部分的代码稍作修改,以适应10bit的数据发送。特权同学原始代码如下: 41 42 43 44 45 46 47 48 49 50 51 52 53
always @ ( posedge clk or negedge rst_n ) begin if(! rst_n ) begin bps_start_r <= 1'bz ; tx_en <= 1'b0 ; tx_data <= 8'd0 ; end
else if( neg_rx_int ) begin // bps_start_r <= 1'b1 ; tx_data <= rx_data ; // tx_en <= 1'b1 ; // end
else if( num==4'd11 ) begin // bps_start_r <= 1'b0 ;
54 tx_en <= 1'b0 ; 55 end 56 end 57
58 assign bps_start = bps_start_r ; 59
60 //--------------------------------------------------------- 61 reg rs232_tx_r ; 62
63 always @ ( posedge clk or negedge rst_n ) begin 64 if(! rst_n ) begin 65 num <= 4'd0 ; 66 rs232_tx_r <= 1'b1 ; 67 end
68 else if( tx_en ) begin 69 if( clk_bps ) begin 70 num <= num+1'b1 ; 71 case ( num) 72 4'd0 : rs232_tx_r <= 1'b0 ; // 73 4'd1 : rs232_tx_r <= tx_data [0]; // bit0 74 4'd2 : rs232_tx_r <= tx_data [1]; // bit1 75 4'd3 : rs232_tx_r <= tx_data [2];// bit2 76 4'd4 : rs232_tx_r <= tx_data [3]; // bit3 77 4'd5 : rs232_tx_r <= tx_data [4];// bit4 78 4'd6 : rs232_tx_r <= tx_data [5]; // bit5 79 4'd7 : rs232_tx_r <= tx_data [6]; // bit6 80 4'd8 : rs232_tx_r <= tx_data [7]; // bit7 81 4'd9 : rs232_tx_r <= 1'b1 ; // 82 default : rs232_tx_r <= 1'b1 ; 83 endcase 84 end 85 else if( num==4'd11 ) num <= 4'd0 ; // 86 end 87 end 88
这里,我们将第52行代码“else if( num==4'd11 ) begin”修改为“else if( num==4'd10 ) begin”,将第85行代码“else if( num==4'd11 ) num <= 4'd0 ;”修改为“else if( num==4'd10 ) num <= 4'd0 ;”同样,第43行,针对“bps_start_r <= 1'bz;”也进行了与接收部分相同的修改,即修改为“bps_start_r <= 1'b0 ;”其他部分不变,然后再进行仿真编译测试。仿真结果如下:
由以上仿真结果可知,通过修改,该串口模块已经能够实现正确的收发了。接下来,小梅哥将通过板级验证来对该模块进行测试。
这里,小梅哥使用至芯科技提供的一块入门级FPGA开发板来完成该模块的板级验证。以下为该开发板的照片:
该开发板上集成了一枚PL2303的USB转串口芯片,因此,我们不再需要笨拙的9针串口线,只需要一根USB线将开发板与电脑的USB口相连,再安装PL2303的驱动,即可在PC端模拟出一个串口端口,该串口端口的使用和传统串口端口没有任何差别。具体的串口驱动安装过程如下图所示:
通过以上步骤,我们就实现了PL2303型USB转串口芯片驱动的安装。接下来,我们需要根据各自使用的开发板的电路分配引脚,这里小梅哥先用图文的形式介绍一下针对ZX2开发板的引脚分配及最终将配置文件下载到开发板的流程:
通过以上步骤,电路就已经成功的配置到了我们的开发板上并运行起来了,这个时候,我们就需要通过调试软件来进行调试,以验证我们修改后的UART收发模块能否稳定运行。要调试我们的设计,我们还需要一个串口调试工具,通过串口调试工具,对我们编写的串口模块持续发送数据,然后接收串口模块发送回来的数据,通过对比发送与接收
的数据,如果相同,则表明我们的模块是能够稳定工作的,如果出现丢码或者乱码,表明收发过程中出现了错误,则表明我们设计的模块存在工作不稳定的情况,需要进一步修改优化。
这里,我们使用程序匠人前辈编写的强大串口调试软件——串口猎人。该软件的下载和安装小梅哥就不多说了,当我们启动串口猎人软件后,会显示以下界面,接下来小梅哥就直接以图片配文字的形式来一步一步演示该软件的使用:
通过以上测试,我们知道了,经过小梅哥的修改,该串口模块目前能够以连续无间隔的发码速度进行收发,而且不存在任何误码和丢码。因此,可以说,小梅哥的修改是成功的。
这一篇,主要带领大家一起进行了串口代码的分析、仿真模型的设计(testbench)、Quartus II软件下使用Nativelink调用Modelsim-altera软件进行仿真、仿真结果简单分析、串口代码的修改、串口模块的板级验证。相信通过此文,初学者能够学到部分知识。 这是该系列文档的第一篇,主要对特权大神的UART代码进行了简单的修改,以适应小梅哥自己的用途。下一篇,小梅哥将详细介绍基于此模块的扩展设计,其实扩展也没多少内容,主要就是加上可变波特率功能,顺便介绍一下代码中一些增加设计灵活性的技巧。第三篇,则是小梅哥开始手把手教大家从原理入手,设计我们自己的UART模块,设计中会加入收发FIFO,并使用标准的Avalon ST接口协议,以形成IP核的形式。 需要小梅哥原版工程的请前往
http://www.fpgaw.com/forum.php?mod=viewthread&tid=78763&fromuid=28191 (出处: fpga论坛|fpga设计论坛)
或者http://bbs.elecfans.com:88/jishu_463390_1_1.html下载
因篇幅问题不能全部显示,请点此查看更多更全内容