本文摘自网络班学员陈同学的博客:https://www.cnblogs.com/moluoqishi/p/7280191.html
明德扬FPGA网络班:http://www.mdy-edu.com/product/670.html
明德扬FPGA就业班:http://www.mdy-edu.com/product/672.html
本篇博文设计思想及代码规范均借鉴明德扬至简设计法,加上些自己的理解和灵活应用,希望对自己和大家都有所帮助。核心要素依然是计数器和状态标志位逻辑相配合的设计方式。在最简单的串口收发一字节数据功能基础上,实现字符串收发。
上一篇博文中详细设计了串口发送模块,串口接收模块设计思想基本相同,只不过将总线的下降沿作为数据接收的开始条件。需要注意有两点:其一,串口接收中读取每一位bit数据时,最好在每一位的中间点取值,这样数据较为准确。第二,串口接收的比特数据属于异步数据,因此需要打两拍做同步处理,避免亚稳态的出现。关于串口接收的设计细节这里不再赘述,不明之处请参考串口发送模块设计思路。串口接收代码如下:
由于思路代码与串口发送非常详尽,这里省去仿真,单独在线调试的过程,将验证工作放在总体设计中。到目前为止,串口的一字节数据发送和接收功能已经实现。下面我们在此基础上做一个完整的小项目。功能定为:FPGA每隔3s向PC发送一个准备就绪(等待)指令“wait”,再等待区间内PC端可以发送一个由#号结尾且长度小于等于10个字符的字符串,当FPGA在等待区间内收到了全部字符串,即收到#号,则等待时间到达后转而发送收到的字符串实现环回功能。之后如果没有再收到字符串再次发送“wait”字符串,循环往复。
现在串口发送接收8位数据的功能已经实现,而一个字符即为8位数据(详见ASCII码表),那么现在的工作重心已将从发送接收字符转到如何实现字符串的收发和切换上。很明显,需要一个控制模块完成上述逻辑,合理调配它的部下:串口接收模块和串口发送模块。我们来一起分析控制模块的实现细节:
先来说发送固定字符串的功能,字符串即是多个字符的集合,所以这里需要一个字符发送计数器,在每次串口发送模块发送完一个字符后加1,从而索引存储在FPGA内部的字符串。说到存储字符串,我们需要一个存储结构,它能将多个比特作为一个整体进行索引,这样才能通过计数器找到一整个字符,所以要用到存储器的结构。上面说要每隔一段时间发送一个字符串,很明显需要等待时间计数器和相应的标志位来区分等待区间和发送区间。至于字符串的接收,其实是一个道理:当然也需要对接收数据计数,这样才能知道接收到字符串的长度。等待区间内若收到结束符#号,则在等待结束后由发送固定字符转而将接收的字符发送出去。其关键也是在于通过接收计数器对接收缓存进行索引。至此,控制模块已设计完毕。你会发现,上述功能仅仅需要几个计数器和一些标志位之间的逻辑即可完成,如此简单的流程不需要使用的状态机。之前的按键检测模块等下也用这种设计思想加以化简。废话不多说,上代码:
`timescale 1ns / 1ps module uart_ctrl( input clk, input rst_n, input key_in, input [7:0] data_in, input data_in_vld, input tx_finish, output reg [2:0] baud, output reg [7:0] data_out, output reg tx_en ); parameter WAIT_TIME = 600_000_000;//3s integer i; reg [7:0] store [4:0];//发送存储 reg [7:0] str_cnt; reg [7:0] N; reg [7:0] rx_cnt; reg [7:0] rx_cnt_tmp; reg [7:0] rx_num; reg [31:0] wait_cnt; (*mark_debug = "true"*)reg wait_flag; reg rec_flag; reg [7:0] rx_buf [9:0]; wire add_str_cnt,end_str_cnt; wire add_wait_cnt,end_wait_cnt; wire add_rx_cnt,end_rx_cnt; wire end_signal; wire din_vld; //按键实现波特率的切换 always@(posedge clk or negedge rst_n)begin if(!rst_n) baud <= 3'b000; else if(key_in)begin if(baud == 3'b100) baud <= 3'b000; else baud <= baud + 1'b1; end end always@(posedge clk or negedge rst_n)begin if(!rst_n)begin store[0] <= 0; store[1] <= 0; store[2] <= 0; store[3] <= 0; store[4] <= 0; end else begin store[0] <= "w";//8'd119;//w store[1] <= "a";//8'd97;//a store[2] <= "i";//8'd105;//i store[3] <= "t";//8'd116;//t store[4] <= " ";//8'd32;//空格 end end //发送计数器区分发送哪一个字符 always@(posedge clk or negedge rst_n)begin if(!rst_n) str_cnt <= 0; else if(add_str_cnt)begin if(end_str_cnt) str_cnt <= 0; else str_cnt <= str_cnt + 1'b1; end end assign add_str_cnt = tx_finish; assign end_str_cnt = add_str_cnt && str_cnt == N - 1; //接收计数器 always@(posedge clk or negedge rst_n)begin if(!rst_n) rx_cnt <= 0; else if(add_rx_cnt)begin if(end_rx_cnt) rx_cnt <= 0; else rx_cnt <= rx_cnt + 1'b1; end end assign add_rx_cnt = din_vld; assign end_rx_cnt = add_rx_cnt && ((rx_cnt == 10 - 1) || data_in == "#");//接收到的字符串最长为10个 assign din_vld = data_in_vld && wait_flag; //计数器计时等待时间1s always@(posedge clk or negedge rst_n)begin if(!rst_n) wait_cnt <= 0; else if(add_wait_cnt)begin if(end_wait_cnt) wait_cnt <= 0; else wait_cnt <= wait_cnt + 1'b1; end end assign add_wait_cnt = wait_flag; assign end_wait_cnt = add_wait_cnt && wait_cnt == WAIT_TIME - 1; //等待标志位 always@(posedge clk or negedge rst_n)begin if(!rst_n) wait_flag <= 1; else if(end_wait_cnt) wait_flag <= 0; else if(end_str_cnt) wait_flag <= 1; end always@(posedge clk or negedge rst_n)begin if(!rst_n) rx_num <= 0; else if(end_signal) rx_num <= rx_cnt + 1'b1; end assign end_signal = add_rx_cnt && data_in == "#"; //接收缓存 always@(posedge clk or negedge rst_n)begin if(!rst_n) for(i = 0;i < 10;i = i + 1)begin rx_buf[i] <= 0; end else if(din_vld && !end_signal) rx_buf[rx_cnt] <= data_in; else if(end_wait_cnt) rx_buf[rx_num - 1] <= " "; else if(end_str_cnt) for(i = 0;i < 10;i = i + 1)begin rx_buf[i] <= 0; end end //检测有效数据 always@(posedge clk or negedge rst_n)begin if(!rst_n) rec_flag <= 0; else if(end_signal) rec_flag <= 1; else if(end_str_cnt) rec_flag <= 0; end always@(*)begin if(rec_flag) N <= rx_num; else N <= 5; end //发送数据给串口发送模块 always@(*)begin if(rec_flag) data_out <= rx_buf[str_cnt]; else data_out <= store[str_cnt]; end //等待结束后发送使能有效 always@(posedge clk or negedge rst_n)begin if(!rst_n) tx_en <= 0; else if(end_wait_cnt || (add_str_cnt && str_cnt < N - 1 && !wait_flag)) tx_en <= 1; else tx_en <= 0; end endmodule
控制模块设计结束,我们通过仿真验证预期功能是否实现。这里仅测试最重要的控制模块,由于需要用到发送模块的tx_finish信号,在测试文件中同时例化控制模块和串口发送模块。需要注意在仿真前将控制模块设为顶层。测试文件:
`timescale 1ns / 1ps module uart_ctrl_tb; reg clk,rst_n; reg key_in; reg [7:0] data_in; reg data_in_vld; wire tx_finish; wire [2:0] baud; wire [7:0] data_tx; wire tx_en; uart_ctrl uart_ctrl( .clk(clk), .rst_n(rst_n), .key_in(key_in), .data_in(data_in), .data_in_vld(data_in_vld), .tx_finish(tx_finish), .baud(baud), .data_out(data_tx), .tx_en(tx_en) ); uart_tx_module uart_tx_module( .clk(clk), .rst_n(rst_n), .baud_set(baud), .send_en(tx_en), .data_in(data_tx), .data_out(), .tx_done(tx_finish) ); integer i; parameter CYC = 5, RST_TIME = 2; defparam uart_ctrl.WAIT_TIME = 2000_000; initial begin clk = 0; forever #(CYC / 2.0) clk = ~clk; end initial begin rst_n = 1; #1; rst_n = 0; #(CYC * RST_TIME); rst_n = 1; end initial begin #1; key_in = 0; data_in = 0; data_in_vld = 0; #(CYC * RST_TIME); #10_000; #5_000_000; data_in = 8'h80; repeat(4)begin data_in_vld = 1; data_in = data_in + 1; #(CYC * 1); data_in_vld = 0; end data_in_vld = 1; data_in = 8'd32; #(CYC * 1); data_in_vld = 0; #10_000; $stop; end 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开发环境的基本操作熟练度,对串口协议有了深层次的认识。最重要的是时序设计能力有了一定的提升。