串口完整項目之串口收發字符串


  本篇博文設計思想及代碼規范均借鑒明德揚至簡設計法,加上些自己的理解和靈活應用,希望對自己和大家都有所幫助。核心要素依然是計數器和狀態標志位邏輯相配合的設計方式。在最簡單的串口收發一字節數據功能基礎上,實現字符串收發。

  上一篇博文中詳細設計了串口發送模塊,串口接收模塊設計思想基本相同,只不過將總線的下降沿作為數據接收的開始條件。需要注意有兩點:其一,串口接收中讀取每一位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開發環境的基本操作熟練度,對串口協議有了深層次的認識。最重要的是時序設計能力有了一定的提升。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM