本篇博文设计思想及代码规范均借鉴明德扬至简设计法,加上些自己的理解和灵活应用,希望对自己和大家都有所帮助。核心要素依然是计数器和状态标志位逻辑相配合的设计方式。在最简单的串口收发一字节数据功能基础上,实现字符串收发。
上一篇博文中详细设计了串口发送模块,串口接收模块设计思想基本相同,只不过将总线的下降沿作为数据接收的开始条件。需要注意有两点:其一,串口接收中读取每一位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开发环境的基本操作熟练度,对串口协议有了深层次的认识。最重要的是时序设计能力有了一定的提升。