本篇博文设计思想及代码规范均借鉴明德扬至简设计法,加上些自己的理解和灵活应用,希望对自己和大家都有所帮助。核心要素依然是计数器和状态标志位逻辑相配合的设计方式。在最简单的串口收发一字节数据功能基础上,实现字符串收发。
上一篇博文中详细设计了串口发送模块,串口接收模块设计思想基本相同,只不过将总线的下降沿作为数据接收的开始条件。需要注意有两点:其一,串口接收中读取每一位bit数据时,最好在每一位的中间点取值,这样数据较为准确。第二,串口接收的比特数据属于异步数据,因此需要打两拍做同步处理,避免亚稳态的出现。关于串口接收的设计细节这里不再赘述,不明之处请参考串口发送模块设计思路。串口接收代码如下:
1 `timescale 1ns / 1ps 2 3 module uart_rx( 4 input clk, 5 input rst_n, 6 input [2:0] baud_set, 7 input din_bit, 8 9 output reg [7:0] data_byte, 10 output reg dout_vld 11 ); 12 13 reg din_bit_sa,din_bit_sb; 14 reg din_bit_tmp; 15 reg add_flag; 16 reg [15:0] div_cnt; 17 reg [3:0] bit_cnt; 18 reg [15:0] CYC; 19 20 wire data_neg; 21 wire add_div_cnt,end_div_cnt; 22 wire add_bit_cnt,end_bit_cnt; 23 wire prob; 24 25 //分频计数器 26 always@(posedge clk or negedge rst_n)begin 27 if(!rst_n) 28 div_cnt <= 0; 29 else if(add_div_cnt)begin 30 if(end_div_cnt) 31 div_cnt <= 0; 32 else 33 div_cnt <= div_cnt + 1'b1; 34 end 35 end 36 37 assign add_div_cnt = add_flag; 38 assign end_div_cnt = add_div_cnt && div_cnt == CYC - 1; 39 40 //bit计数器 41 always@(posedge clk or negedge rst_n)begin 42 if(!rst_n) 43 bit_cnt <= 0; 44 else if(add_bit_cnt)begin 45 if(end_bit_cnt) 46 bit_cnt <= 0; 47 else 48 bit_cnt <= bit_cnt + 1'b1; 49 end 50 end 51 52 assign add_bit_cnt = end_div_cnt; 53 assign end_bit_cnt = add_bit_cnt && bit_cnt == 9 - 1; 54 55 //波特率查找表 56 always@(*)begin 57 case(baud_set) 58 3'b000: CYC <= 20833;//9600 59 3'b001: CYC <= 10417;//19200 60 3'b010: CYC <= 5208;//38400 61 3'b011: CYC <= 3472;//57600 62 3'b100: CYC <= 1736;//115200 63 default:CYC <= 20833;//9600 64 endcase 65 end 66 67 //同步处理 68 always@(posedge clk or negedge rst_n)begin 69 if(!rst_n)begin 70 din_bit_sa <= 1; 71 din_bit_sb <= 1; 72 end 73 else begin 74 din_bit_sa <= din_bit; 75 din_bit_sb <= din_bit_sa; 76 end 77 end 78 79 //下降沿检测 80 always@(posedge clk or negedge rst_n)begin 81 if(!rst_n) 82 din_bit_tmp <= 1; 83 else 84 din_bit_tmp <= din_bit_sb; 85 end 86 87 assign data_neg = din_bit_tmp == 1 && din_bit_sb == 0; 88 89 //检测到下降沿说明有数据起始位有效,计数标志位拉高 90 always@(posedge clk or negedge rst_n)begin 91 if(!rst_n) 92 add_flag <= 0; 93 else if(data_neg) 94 add_flag <= 1; 95 else if(end_bit_cnt) 96 add_flag <= 0; 97 end 98 99 //bit位中点采样数据 100 always@(posedge clk or negedge rst_n)begin 101 if(!rst_n) 102 data_byte <= 0; 103 else if(prob) 104 data_byte[bit_cnt - 1] <= din_bit_sb; 105 end 106 107 assign prob = bit_cnt !=0 && add_div_cnt && div_cnt == CYC / 2 - 1; 108 109 110 //输出数据设置在接收完成是有效 111 always@(posedge clk or negedge rst_n)begin 112 if(!rst_n) 113 dout_vld <= 0; 114 else if(end_bit_cnt) 115 dout_vld <= 1; 116 else 117 dout_vld <= 0; 118 end 119 120 endmodule
由于思路代码与串口发送非常详尽,这里省去仿真,单独在线调试的过程,将验证工作放在总体设计中。到目前为止,串口的一字节数据发送和接收功能已经实现。下面我们在此基础上做一个完整的小项目。功能定为:FPGA每隔3s向PC发送一个准备就绪(等待)指令“wait”,再等待区间内PC端可以发送一个由#号结尾且长度小于等于10个字符的字符串,当FPGA在等待区间内收到了全部字符串,即收到#号,则等待时间到达后转而发送收到的字符串实现环回功能。之后如果没有再收到字符串再次发送“wait”字符串,循环往复。
现在串口发送接收8位数据的功能已经实现,而一个字符即为8位数据(详见ASCII码表),那么现在的工作重心已将从发送接收字符转到如何实现字符串的收发和切换上。很明显,需要一个控制模块完成上述逻辑,合理调配它的部下:串口接收模块和串口发送模块。我们来一起分析控制模块的实现细节:
先来说发送固定字符串的功能,字符串即是多个字符的集合,所以这里需要一个字符发送计数器,在每次串口发送模块发送完一个字符后加1,从而索引存储在FPGA内部的字符串。说到存储字符串,我们需要一个存储结构,它能将多个比特作为一个整体进行索引,这样才能通过计数器找到一整个字符,所以要用到存储器的结构。上面说要每隔一段时间发送一个字符串,很明显需要等待时间计数器和相应的标志位来区分等待区间和发送区间。至于字符串的接收,其实是一个道理:当然也需要对接收数据计数,这样才能知道接收到字符串的长度。等待区间内若收到结束符#号,则在等待结束后由发送固定字符转而将接收的字符发送出去。其关键也是在于通过接收计数器对接收缓存进行索引。至此,控制模块已设计完毕。你会发现,上述功能仅仅需要几个计数器和一些标志位之间的逻辑即可完成,如此简单的流程不需要使用的状态机。之前的按键检测模块等下也用这种设计思想加以化简。废话不多说,上代码:
1 `timescale 1ns / 1ps 2 3 module uart_ctrl( 4 input clk, 5 input rst_n, 6 input key_in, 7 8 input [7:0] data_in, 9 input data_in_vld, 10 input tx_finish, 11 output reg [2:0] baud, 12 output reg [7:0] data_out, 13 output reg tx_en 14 ); 15 16 parameter WAIT_TIME = 600_000_000;//3s 17 integer i; 18 19 reg [7:0] store [4:0];//发送存储 20 reg [7:0] str_cnt; 21 reg [7:0] N; 22 reg [7:0] rx_cnt; 23 reg [7:0] rx_cnt_tmp; 24 reg [7:0] rx_num; 25 reg [31:0] wait_cnt; 26 (*mark_debug = "true"*)reg wait_flag; 27 reg rec_flag; 28 reg [7:0] rx_buf [9:0]; 29 30 wire add_str_cnt,end_str_cnt; 31 wire add_wait_cnt,end_wait_cnt; 32 wire add_rx_cnt,end_rx_cnt; 33 wire end_signal; 34 wire din_vld; 35 36 //按键实现波特率的切换 37 always@(posedge clk or negedge rst_n)begin 38 if(!rst_n) 39 baud <= 3'b000; 40 else if(key_in)begin 41 if(baud == 3'b100) 42 baud <= 3'b000; 43 else 44 baud <= baud + 1'b1; 45 end 46 end 47 48 always@(posedge clk or negedge rst_n)begin 49 if(!rst_n)begin 50 store[0] <= 0; 51 store[1] <= 0; 52 store[2] <= 0; 53 store[3] <= 0; 54 store[4] <= 0; 55 end 56 else begin 57 store[0] <= "w";//8'd119;//w 58 store[1] <= "a";//8'd97;//a 59 store[2] <= "i";//8'd105;//i 60 store[3] <= "t";//8'd116;//t 61 store[4] <= " ";//8'd32;//空格 62 end 63 end 64 65 //发送计数器区分发送哪一个字符 66 always@(posedge clk or negedge rst_n)begin 67 if(!rst_n) 68 str_cnt <= 0; 69 else if(add_str_cnt)begin 70 if(end_str_cnt) 71 str_cnt <= 0; 72 else 73 str_cnt <= str_cnt + 1'b1; 74 end 75 end 76 77 assign add_str_cnt = tx_finish; 78 assign end_str_cnt = add_str_cnt && str_cnt == N - 1; 79 80 //接收计数器 81 always@(posedge clk or negedge rst_n)begin 82 if(!rst_n) 83 rx_cnt <= 0; 84 else if(add_rx_cnt)begin 85 if(end_rx_cnt) 86 rx_cnt <= 0; 87 else 88 rx_cnt <= rx_cnt + 1'b1; 89 end 90 end 91 92 assign add_rx_cnt = din_vld; 93 assign end_rx_cnt = add_rx_cnt && ((rx_cnt == 10 - 1) || data_in == "#");//接收到的字符串最长为10个 94 95 96 assign din_vld = data_in_vld && wait_flag; 97 98 //计数器计时等待时间1s 99 always@(posedge clk or negedge rst_n)begin 100 if(!rst_n) 101 wait_cnt <= 0; 102 else if(add_wait_cnt)begin 103 if(end_wait_cnt) 104 wait_cnt <= 0; 105 else 106 wait_cnt <= wait_cnt + 1'b1; 107 end 108 end 109 110 assign add_wait_cnt = wait_flag; 111 assign end_wait_cnt = add_wait_cnt && wait_cnt == WAIT_TIME - 1; 112 113 //等待标志位 114 always@(posedge clk or negedge rst_n)begin 115 if(!rst_n) 116 wait_flag <= 1; 117 else if(end_wait_cnt) 118 wait_flag <= 0; 119 else if(end_str_cnt) 120 wait_flag <= 1; 121 end 122 123 always@(posedge clk or negedge rst_n)begin 124 if(!rst_n) 125 rx_num <= 0; 126 else if(end_signal) 127 rx_num <= rx_cnt + 1'b1; 128 end 129 130 assign end_signal = add_rx_cnt && data_in == "#"; 131 132 //接收缓存 133 always@(posedge clk or negedge rst_n)begin 134 if(!rst_n) 135 for(i = 0;i < 10;i = i + 1)begin 136 rx_buf[i] <= 0; 137 end 138 else if(din_vld && !end_signal) 139 rx_buf[rx_cnt] <= data_in; 140 else if(end_wait_cnt) 141 rx_buf[rx_num - 1] <= " "; 142 else if(end_str_cnt) 143 for(i = 0;i < 10;i = i + 1)begin 144 rx_buf[i] <= 0; 145 end 146 end 147 148 //检测有效数据 149 always@(posedge clk or negedge rst_n)begin 150 if(!rst_n) 151 rec_flag <= 0; 152 else if(end_signal) 153 rec_flag <= 1; 154 else if(end_str_cnt) 155 rec_flag <= 0; 156 end 157 158 always@(*)begin 159 if(rec_flag) 160 N <= rx_num; 161 else 162 N <= 5; 163 end 164 165 //发送数据给串口发送模块 166 always@(*)begin 167 if(rec_flag) 168 data_out <= rx_buf[str_cnt]; 169 else 170 data_out <= store[str_cnt]; 171 end 172 173 //等待结束后发送使能有效 174 always@(posedge clk or negedge rst_n)begin 175 if(!rst_n) 176 tx_en <= 0; 177 else if(end_wait_cnt || (add_str_cnt && str_cnt < N - 1 && !wait_flag)) 178 tx_en <= 1; 179 else 180 tx_en <= 0; 181 end 182 183 endmodule
控制模块设计结束,我们通过仿真验证预期功能是否实现。这里仅测试最重要的控制模块,由于需要用到发送模块的tx_finish信号,在测试文件中同时例化控制模块和串口发送模块。需要注意在仿真前将控制模块设为顶层。测试文件:
1 `timescale 1ns / 1ps 2 3 module uart_ctrl_tb; 4 5 reg clk,rst_n; 6 reg key_in; 7 reg [7:0] data_in; 8 reg data_in_vld; 9 10 wire tx_finish; 11 wire [2:0] baud; 12 wire [7:0] data_tx; 13 wire tx_en; 14 15 uart_ctrl uart_ctrl( 16 .clk(clk), 17 .rst_n(rst_n), 18 .key_in(key_in), 19 20 .data_in(data_in), 21 .data_in_vld(data_in_vld), 22 .tx_finish(tx_finish), 23 .baud(baud), 24 .data_out(data_tx), 25 .tx_en(tx_en) 26 ); 27 28 uart_tx_module uart_tx_module( 29 .clk(clk), 30 .rst_n(rst_n), 31 .baud_set(baud), 32 .send_en(tx_en), 33 .data_in(data_tx), 34 35 .data_out(), 36 .tx_done(tx_finish) 37 ); 38 39 40 integer i; 41 42 parameter CYC = 5, 43 RST_TIME = 2; 44 45 defparam uart_ctrl.WAIT_TIME = 2000_000; 46 47 initial begin 48 clk = 0; 49 forever #(CYC / 2.0) clk = ~clk; 50 end 51 52 initial begin 53 rst_n = 1; 54 #1; 55 rst_n = 0; 56 #(CYC * RST_TIME); 57 rst_n = 1; 58 end 59 60 61 initial begin 62 #1; 63 key_in = 0; 64 data_in = 0; 65 data_in_vld = 0; 66 #(CYC * RST_TIME); 67 #10_000; 68 #5_000_000; 69 data_in = 8'h80; 70 repeat(4)begin 71 data_in_vld = 1; 72 data_in = data_in + 1; 73 #(CYC * 1); 74 data_in_vld = 0; 75 end 76 data_in_vld = 1; 77 data_in = 8'd32; 78 #(CYC * 1); 79 data_in_vld = 0; 80 #10_000; 81 $stop; 82 end 83 84 endmodule
本次设计先采用VIVADO自带仿真工具Vivado Simulator。虽然速度有些慢,不过对简单的设计来说体验区别不明显,而且用起来很方便简单,适合新手。观察行为仿真波形:
可以看到波形符合预期功能,成功将串口接收到的129 130 131 132 32五个数据通过串口环回,在没有收到有效字符串时发送“wait”字符串对应的ASCII码十进制数值。如代码有问题修改代码并保存后只需按下仿真界面上方仿真工具栏中重新Relaunch Simulation按钮,开发工具将自动将修改后的代码更新到仿真环境中并重新开始运行仿真:
在上述控制模块中,我加入了根据按键按下次数调整常用波特率的功能,因此需要例化按键消抖模块。剩下的工作只需建立顶层文件,把各个模块之间信号连接起来。好像没什么可说的了,相信大家都能看懂,以下是顶层模块
1 `timescale 1ns / 1ps 2 3 module send_data_top( 4 input sys_clk_p, 5 input sys_clk_n, 6 input rst_n, 7 input key, 8 9 output bit_tx, 10 output tx_finish_led, 11 12 input bit_rx, 13 output rx_finish_led 14 ); 15 16 wire tx_done,rx_done; 17 (*mark_debug = "true"*)wire data_rx_vld; 18 (*mark_debug = "true"*)wire [7:0] data_rx_byte; 19 wire key_signal; 20 wire [2:0] baud; 21 wire [7:0] data_tx; 22 (*mark_debug = "true"*)wire send_start; 23 24 // 差分时钟转单端时钟 25 // IBUFGDS是IBUFG差分形式,当信号从一对差分全局时钟引脚输入时,必须使用IBUFGDS作为全局时钟输入缓冲 26 wire sys_clk_ibufg; 27 IBUFGDS # 28 ( 29 .DIFF_TERM ("FALSE"), 30 .IBUF_LOW_PWR ("FALSE") 31 ) 32 u_ibufg_sys_clk 33 ( 34 .I (sys_clk_p), //差分时钟的正端输入,需要和顶层模块的端口直接连接 35 .IB (sys_clk_n), // 差分时钟的负端输入,需要和顶层模块的端口直接连接 36 .O (sys_clk_ibufg) //时钟缓冲输出 37 ); 38 39 key_jitter key_jitter 40 ( 41 .clk(sys_clk_ibufg), 42 .rst_n(rst_n), 43 44 .key_i(key), 45 .key_vld(key_signal) 46 ); 47 48 uart_ctrl uart_ctrl( 49 .clk(sys_clk_ibufg), 50 .rst_n(rst_n), 51 .key_in(key_signal), 52 53 .data_in(data_rx_byte), 54 .data_in_vld(data_rx_vld), 55 .tx_finish(tx_done), 56 .baud(baud), 57 .data_out(data_tx), 58 .tx_en(send_start) 59 ); 60 61 62 uart_tx uart_tx( 63 .clk(sys_clk_ibufg), 64 .rst_n(rst_n), 65 .baud_set(baud),//[2:0] 66 .send_en(send_start), 67 .data_in(data_tx),//[7:0] 68 69 .data_out(bit_tx), 70 .tx_done(tx_done)); 71 72 assign tx_finish_led = !tx_done; 73 74 uart_rx uart_rx( 75 .clk(sys_clk_ibufg), 76 .rst_n(rst_n), 77 .baud_set(baud), 78 .din_bit(bit_rx), 79 80 .data_byte(data_rx_byte), 81 .dout_vld(data_rx_vld) 82 ); 83 84 assign rx_finish_led = !data_rx_vld; 85 86 endmodule
看下整体结构图吧,很清晰,也确认信号连接没有犯低级错误
确认功能没有问题之后添加约束文件:
然后步骤同上一篇博文,添加调试IP核,综合、布局布线、生成bit流。打开硬件管理器下载bit流,使用调试界面观察芯片内部波形数据,先来看看接收有没有问题,串口调试助手发送“good#”,观察接收有效指示信号和接收数据:
成功接收到了good字符串,并且串口调试助手收到了发送的字符,在没有发送字符时每隔3s收到一个“wait”字符串:
串口收到数据的工程到这里告一段落,以后可以进一步改进和做些更具应用性的工程。经过三篇博文,提高了VIVADO开发环境的基本操作熟练度,对串口协议有了深层次的认识。最重要的是时序设计能力有了一定的提升。