本篇博文设计思想及代码规范均借鉴明德扬至简设计法,加上些自己的理解和灵活应用,希望对自己和大家都有所帮助。核心要素依然是计数器和状态标志位逻辑相配合的设计方式。在最简单的串口收发一字节数据功能基础上,实现字符串收发。

  上一篇博文中详细设计了串口发送模块,串口接收模块设计思想基本相同,只不过将总线的下降沿作为数据接收的开始条件。需要注意有两点:其一,串口接收中读取每一位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内部的字符串。说到存储字符串,我们需要一个存储结构,它能将多个比特作为一个整体进行索引,这样才能通过计数器找到一整个字符,所以要用到存储器的结构

串口协议 如何发送字符串 java 串口发送字符串的过程_字符串

。上面说要每隔一段时间发送一个字符串,很明显需要等待时间计数器和相应的标志位来区分等待区间和发送区间。至于字符串的接收,其实是一个道理:当然也需要对接收数据计数,这样才能知道接收到字符串的长度。等待区间内若收到结束符#号,则在等待结束后由发送固定字符转而将接收的字符发送出去。其关键也是在于通过接收计数器对接收缓存进行索引。至此,控制模块已设计完毕。你会发现,上述功能仅仅需要几个计数器和一些标志位之间的逻辑即可完成,如此简单的流程不需要使用的状态机。之前的按键检测模块等下也用这种设计思想加以化简。废话不多说,上代码:

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。虽然速度有些慢,不过对简单的设计来说体验区别不明显,而且用起来很方便简单,适合新手。观察行为仿真波形:

串口协议 如何发送字符串 java 串口发送字符串的过程_串口_02

串口协议 如何发送字符串 java 串口发送字符串的过程_串口协议 如何发送字符串 java_03

可以看到波形符合预期功能,成功将串口接收到的129 130 131 132 32五个数据通过串口环回,在没有收到有效字符串时发送“wait”字符串对应的ASCII码十进制数值。如代码有问题修改代码并保存后只需按下仿真界面上方仿真工具栏中重新Relaunch Simulation按钮,开发工具将自动将修改后的代码更新到仿真环境中并重新开始运行仿真:

串口协议 如何发送字符串 java 串口发送字符串的过程_sed_04

  在上述控制模块中,我加入了根据按键按下次数调整常用波特率的功能,因此需要例化按键消抖模块。剩下的工作只需建立顶层文件,把各个模块之间信号连接起来。好像没什么可说的了,相信大家都能看懂,以下是顶层模块

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

看下整体结构图吧,很清晰,也确认信号连接没有犯低级错误

串口协议 如何发送字符串 java 串口发送字符串的过程_串口_05

  确认功能没有问题之后添加约束文件:

串口协议 如何发送字符串 java 串口发送字符串的过程_串口协议 如何发送字符串 java_06

然后步骤同上一篇博文,添加调试IP核,综合、布局布线、生成bit流。打开硬件管理器下载bit流,使用调试界面观察芯片内部波形数据,先来看看接收有没有问题,串口调试助手发送“good#”,观察接收有效指示信号和接收数据:

串口协议 如何发送字符串 java 串口发送字符串的过程_sed_07

成功接收到了good字符串,并且串口调试助手收到了发送的字符,在没有发送字符时每隔3s收到一个“wait”字符串:

串口协议 如何发送字符串 java 串口发送字符串的过程_串口协议 如何发送字符串 java_08

 串口收到数据的工程到这里告一段落,以后可以进一步改进和做些更具应用性的工程。经过三篇博文,提高了VIVADO开发环境的基本操作熟练度,对串口协议有了深层次的认识。最重要的是时序设计能力有了一定的提升。