本篇博文設計思想及代碼規范均借鑒明德揚至簡設計法,加上些自己的理解和靈活應用,希望對自己和大家都有所幫助。核心要素依然是計數器和狀態標志位邏輯相配合的設計方式。在最簡單的串口收發一字節數據功能基礎上,實現字符串收發。
上一篇博文中詳細設計了串口發送模塊,串口接收模塊設計思想基本相同,只不過將總線的下降沿作為數據接收的開始條件。需要注意有兩點:其一,串口接收中讀取每一位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開發環境的基本操作熟練度,對串口協議有了深層次的認識。最重要的是時序設計能力有了一定的提升。
