EE_FPGA基础教程系列 --NO.5-- 串口调试
[复制链接]
Table of Contents
1. 串口通信 ....................................................4 1.1 串口的常识 ..............................................4 1.2 串口通信原理 ..........................................4 2. 代码 ..........................................................5 调试前准备 ...................................................15 4. 上电调试 ....................................................16 5. 总结 ...........................................................16
1. 串口通信 1.1 串口的常识 串口,即UART(Universal Asynchronous Receiver/Transmitter),就是一种通用串行数据总线。这里,我们学习的是基于RS-232的串口,RS-232的电气特性是:逻辑1 = -3V~-15V;逻辑0 = +3~+15V。老式的个人计算机上一般有两组RS-232 接口,分别称为 COM1 和 COM2。现在的电脑,尤其是笔记本电脑,基本是没有串口了的。但,串口作为常用的通信总线,总在很多地方用到,很有必要学习下。“哪里有需求,哪里就有商机!”现在到处都有串口转USB的转换线卖,售价呢也就两三块钱。
回到我们的EE_FPGA上,这块板子,我们不需要转换线了,因为设计的时候已经在板子上加入了串口转USB芯片PL2303HX。调出原理图如下:
其实,人家卖的转换线呢,就是里面就是做了这么一个电路!
这里,PL2303HX的芯片手册,以及更多关于串口和RS-232的知识,请大家自行Google、百度之。重点到我们最关心的串口的代码编写上来。
1.2 串口通信原理
大家观察原理图可以看到,串口通信其实只用到了FPGA的两个I/O口,分别是I/O_39、I/O_37。通常,我们把这两根线命名为tx、rx,其实就是数据接收线和发送线。
我们再看下串口数据的传输协议:
典型的一帧数据格式是1个起始位,8个数据位,1个奇偶校验位,1-2个停止位。其中,数据线上没有数据传输的时候是保持高电平,而第一个低电平的出现就是起始位。当发送数据和接收数据的时候,按以上格式进行就可以了。
不知道大家会不会有这样一个问题,经常听说串口传输的速度是多少多少,看了这个数据帧格式,没发现速度是怎么控制的啊。对的,这儿就引出了波特率的问题。波特率时钟并没有表现在传输线上,它其实是用来指示我们每采样或发送一个数据位的速度的。比如说,我们定义一个波特率时钟为96k,那么我们用这个时钟把数据串行一个一个打出去,接收端只要匹配一个相同的时钟,一个一个数据位接受进来,那数据传输就可以了!
2. 代码
这部分的代码相对于前面的就稍稍有点多了,我们来一点一点分析,由于这次用到了模块调用,这次上代码,我们把每个文件名都写上。首先我们来看下这个工程的树状代码:
通过这个树状图,大家应该就对这个工程有一定了解了,主模块是uart,它包含四个子模块,分别是接收模块rx、接收模块的波特率时钟rx_clk、发送模块tx、发送模块的波特率时钟tx_clk。 1、主模块:uart.v
-
module uart( clk, rst_n, rxd, txd );
input clk; //输入时钟,50M input rst_n; input rxd; //串口输入rx
output txd; //串口输出tx
wire bps_start1,bps_start2; //rx、tx的波特率启动信号 wire bps_clk1,bps_clk2; //rx、tx的波特率时钟 wire rx_done; //数据接收完毕信号,有rx输出到tx,rx接收数据由tx发送回去 wire[7:0] data; //数据寄存器
bps_generate rx_clk( .clk(clk), // 50M .rst_n(rst_n), .bps_start(bps_start1), //拉高,产生波特率时钟 .bps_clk(bps_clk1) );
rx rx ( .clk(clk), .rst_n(rst_n), .rxd(rxd), .bps_clk(bps_clk1), .bps_start(bps_start1), .rx_done(rx_done), .data(data) );
bps_generate tx_clk( .clk(clk), // 50M .rst_n(rst_n), .bps_start(bps_start2), //拉高,产生波特率时钟 .bps_clk(bps_clk2) );
tx tx ( .clk(clk), .rst_n(rst_n), .bps_clk(bps_clk2), .bps_start(bps_start2), .data(data), .rx_done(rx_done), .txd(txd) ); endmodule
复制代码
通常,我们在顶层模块里不会写具体操作的代码,主模块的作用是将各个模块连接在一起。这样有利于代码的维护!
我们看到主模块对外的输入输出引脚只有时钟clk,复位rst_n,以及数据输入线rx和输出线tx。往下看,我们看到一堆wire的定义,这个很重要。由于input和output的端口Quartus软件会自动配置成wire型,所以这些端口不用再wire定义。而其他剩下的端口,如果不“wire”一下,那么下面的各模块例化相互的端口就不会连接在一起。
接下来终于要说到模块例化了,所谓“例化”,是从英文的instantiate翻译过来的。这里我说个不太严谨的说法:就是把另一个模块添加在这个模块当中,更不严谨的,也可以说是调用吧。
- bps_generate rx_clk(
.clk(clk), // 50M .rst_n(rst_n), .bps_start(bps_start1), //拉高,产生波特率时钟 .bps_clk(bps_clk1) );
复制代码
我们引用波特率时钟模块来说明例化的格式吧。首先,bps_generate是原来模块的模块名,后面加一个或几个空格,紧跟着rx_clk是在这个模块下(也就是uart)的模块名。通常,我们会把两个命名一样,或者后面的命名为:i1、i2,意思是模块1,2…这个可以随意。接下来在();里添加这个模块的输入输出引脚,其中.clk()是原来模块的名称,而括号里则是在当前模块下的名称。在当前模块里,两个名称一样,再“wire”一下,那这个信号就连接在一起了。这儿,你可以直接给这个接口赋一个数,例如.data(16’h11),这样,也是可以的。
2、波特率产生模块:bps_generate.v
-
module bps_generate( clk, rst_n, bps_start, bps_clk ); input clk; // 50M input rst_n; input bps_start; //高电平,产生波特率时钟
output bps_clk;
parameter CNT_NUM = 434; //波特率为50M / 434 = 115200 parameter CNT_NUM_2 = 216; //计数值的一半,产生一个高电平
reg[15:0] cnt; always @(posedge clk or negedge rst_n) begin if(!rst_n) cnt <= 16'b0; else if(cnt == CNT_NUM) cnt <= 16'b0; else if(bps_start) cnt <= cnt + 1'b1; else cnt <= 16'b0; end
reg bps_clk_r; always @(posedge clk or negedge rst_n) begin if(!rst_n) bps_clk_r <= 1'b0; else if(cnt == CNT_NUM_2) bps_clk_r <= 1'b1; else bps_clk_r <= 1'b0; end
assign bps_clk = bps_clk_r;
endmodule
复制代码
这里,我们首先用到了parameter,因为我们常通过修改计数值来改波特率时钟,所以,用parameter是很方便的!
接下来就是计数的操作,我想不用介绍太多。最后产生的波特率时钟差不多是这样的,我们利用每个高电平来采集、发送数据。
这里,还要讲到一个模块重复调用的问题。由于接收和发送的波特率时钟是一样的,我们在顶层模块uart调用的时候都是调用了这个代码文件,但分别命名为rx_clk 和tx_clk,Quartus软件在综合的时候就会综合成两个电路。不同于软件程序的调用,大家一定要引起重视!
3、接收模块:rx.v
-
module rx( clk, rst_n, bps_clk, rxd, bps_start, rx_done, data );
input clk; input rst_n; input bps_clk; input rxd;
output bps_start; output rx_done; output[7:0] data;
reg reg_rxd0; always @(posedge clk or negedge rst_n) begin if(!rst_n) reg_rxd0 <= 1'b1; else reg_rxd0 <= rxd; end
reg reg_rxd1,reg_rxd2; always @(posedge clk or negedge rst_n) begin if(!rst_n) begin reg_rxd1 <= 1'b1; reg_rxd2 <= 1'b1; end else begin reg_rxd1 <= reg_rxd0; reg_rxd2 <= reg_rxd1; end end
wire reg_negedge = reg_rxd2 & (~reg_rxd1); //下降沿检测
reg bps_start_r; reg rx_done_r; always @(posedge clk or negedge rst_n) begin if(!rst_n) begin bps_start_r <= 1'b0; rx_done_r <= 1'b0; end else if(reg_negedge) bps_start_r <= 1'b1; //检测到起始位,打开波特率时钟 else if(state == 4'd9) rx_done_r <= 1'b1; //数据接收完成,启动一次数据传输 else if(state == 4'd10) begin bps_start_r <= 1'b0; //一帧数据传输完毕,关闭波特率时钟 rx_done_r <= 1'b0; //标志位关闭,避免重复传输 end end
assign bps_start = bps_start_r; assign rx_done = rx_done_r; //数据传输标志位,拉高,表明rx接收一帧数据完成,tx发送一次该组数据
reg[3:0] state; always @(posedge clk or negedge rst_n) begin if(!rst_n) state <= 4'b0; else if(state == 4'd10) state <= 4'b0; //一帧数据传输完毕,回到初始状态 else if(bps_clk) //波特率每个高电平进行状态转移 begin case(state) 4'd0: state <= 4'd1; 4'd1: state <= 4'd2; 4'd2: state <= 4'd3; 4'd3: state <= 4'd4; 4'd4: state <= 4'd5; 4'd5: state <= 4'd6; 4'd6: state <= 4'd7; 4'd7: state <= 4'd8; 4'd8: state <= 4'd9; 4'd9: state <= 4'd10; 4'd10: state <= 4'b0; default: state <= 4'b0; endcase end end
reg[7:0] data_temp; always @(posedge clk or negedge rst_n) begin if(!rst_n) begin data_temp <= 8'b0; end else if(bps_clk) begin case(state) 4'd1: data_temp[0] <= rxd; 4'd2: data_temp[1] <= rxd; 4'd3: data_temp[2] <= rxd; 4'd4: data_temp[3] <= rxd; 4'd5: data_temp[4] <= rxd; 4'd6: data_temp[5] <= rxd; 4'd7: data_temp[6] <= rxd; 4'd8: data_temp[7] <= rxd; //逐位存入数据 endcase end end
assign data = data_temp;
endmodule
复制代码
在这个代码文件中,蓝色部分是用来下降沿检测的,前面已经介绍过。红色部分,是一个简单的状态机。FPGA内部相当于硬件电路,都是并行执行,但是,有些逻辑却是有一定顺序的,这时候,我们就需要使用状态机来完成顺序执行。大家发现,红色部分第一个always模块的case结构,状态state是随着波特率的高电平else if(bps_clk)一个一个转移的。而在第二个always模块里,利用每一个状态执行一次数据读入的操作!
4、数据发送模块:tx.v
-
module tx( input clk, input rst_n, input bps_clk, input rx_done, input[7:0] data,
output bps_start, output txd );
reg bps_start_r; always @(posedge clk or negedge rst_n) begin if(!rst_n) bps_start_r <= 1'b0; else if(rx_done) bps_start_r <= 1'b1; else if(state == 4'd11) bps_start_r <= 1'b0; end
assign bps_start = bps_start_r;
reg[7:0] tx_data; always @(posedge clk or negedge rst_n) begin if(!rst_n) tx_data <= 8'b0; else if(rx_done) tx_data <= data; end
reg[3:0] state; always @(posedge clk or negedge rst_n) begin if(!rst_n) state <= 4'b0; else if(bps_clk) begin case(state) 4'd0: state <= 4'd1; 4'd1: state <= 4'd2; 4'd2: state <= 4'd3; 4'd3: state <= 4'd4; 4'd4: state <= 4'd5; 4'd5: state <= 4'd6; 4'd6: state <= 4'd7; 4'd7: state <= 4'd8; 4'd8: state <= 4'd9; 4'd9: state <= 4'd11; // 4'd10: state <= 4'd11; 4'd11: state <= 4'b0; default: state <= 4'b0; endcase end end
reg txd_r; always @(posedge clk or negedge rst_n) begin if(!rst_n) txd_r <= 1'b1; else if(bps_clk) begin case(state) 4'd1: txd_r <= 1'b0; 4'd2: txd_r <= tx_data[0]; 4'd3: txd_r <= tx_data[1]; 4'd4: txd_r <= tx_data[2]; 4'd5: txd_r <= tx_data[3]; 4'd6: txd_r <= tx_data[4]; 4'd7: txd_r <= tx_data[5]; 4'd8: txd_r <= tx_data[6]; 4'd9: txd_r <= tx_data[7]; // 4'd10: txd_r <= 1'b1; //crc 4'd11: txd_r <= 1'b1; //stop endcase end end
assign txd = txd_r;
endmodule
复制代码
发送模块和接收模块是非常相似的,这里就不重复介绍了。
3. 调试前准备
对于我们这个串口调试: 1、我们需要准备一个USB数据线,是标准的一边小头,一边大头的那种,我想,很多MP3,手机都是采用这种数据线的,很好找。 2、安装串口转USB的驱动程序。 3、安装一个串口调试助手。 这些,我们都将在帖子里附件打包。 4. 上电调试 分配好管脚,下载代码,打开串口大师如下图:当上面的窗口能显示出下面窗口我们发送的内容,就算是调试成功了O(∩_∩)O~!
5. 总结
这次实验,我们学习了简单的串口RS232的通信时序和逻辑设计。也是我们第一次用到了模块调用,第一次设计了一个相对长一些的代码。
[ 本帖最后由 xieqiang 于 2011-5-12 11:37 编辑 ]
|