1.实验说明
在上位机中通过串口调试助手向 PotatoPie 开发板中发送串口数据,开发板将接收到的数据发送给上
位机,完成数据的回环,该例程通信框图如下图所示
通过 UART 可以与主机或其他外部设备进行串口通信。
UART 是一种通用的数据通信协议,也是异步串行通信口的总称, 在进行 UART 通信时可以配置传输速度、
数据格式等参数, 将数据通过串行通讯进行传输。
UART 通信协议如下图所示:
管脚说明
set_pin_assignment { clk_ref } { LOCATION = P11; IOSTANDARD = LVTTL33; PULLTYPE = PULLUP; }
set_pin_assignment { i_rst } { LOCATION = P84; IOSTANDARD = LVTTL33; PULLTYPE = PULLDOWN; }
set_pin_assignment { rx } { LOCATION = P2; IOSTANDARD = LVTTL33; PULLTYPE = PULLUP; }
set_pin_assignment { tx } { LOCATION = P3; IOSTANDARD = LVTTL33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
- rx,UART接收管脚
- clk_ref,参考时钟
- i_rst,复位
- tx,UART发送管脚
实验现象
连接 PotatoPie 开发板,用USB转TTL串口线连接到开发板的UART IO上(PotatoPieV4.0板自带USB转串口,所以只需要数据线连到有”UART”字样标识的TypeC口就行),下载 bit 文件,在上位机中打开串口调试助手,波特率选择 115200,数据位宽 8bit,无校验位, 1bit 停止位,利用串口同时发送 1~9 hex 数据。
上位机接收到的数据与发送的数据一致, UART 数据回环传输正确。
实验原理
UART介绍
UART
(Universal Asynchronous Receiver/Transmitter)是一种异步全双工串行通信
协议,由Tx和Rx两根数据线组成,因为没有参考时钟信号,所以通信的双方必须约定串口波特率
、数据位宽
、奇偶校验位
、停止位
等配置参数,从而按照相同的速率进行通信。起始位
:先发出一个逻辑”0”信号,表示传输字符的开始;数据位
:可以是5~8位逻辑”0”或”1”;如ASCII码(7位),扩展BCD码(8位);小端传输,即LSB先发,MSB后发;校验位
:数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验);停止位
:它是一个字符数据的结束标志。可以是1位、1.5位、2位的高电平(用于双方同步,停止位时间间隔越长,容错能力越强);空闲位
:处于逻辑“1”状态,表示当前线路上没有数据传送;↑图-1 起始位和停止位
↑图-2 数据位
↑传输“A”
上图是uart协议传输一个”A”字符通过示波器的uart解码而得到的波形示意图。根据此图来介绍一下uart的一些基本参数。 波特率
:此参数容易和比特率混淆,其实他们是由区别的。但是我认为uart中的波特率就可以认为是比特率,即每秒传输的位数(bit)。一般选波特率都会有9600,19200,115200等选项。其实意思就是每秒传输这么多个比特位数(bit)。 起始位
:先发出一个逻辑”0”的信号,表示传输数据的开始。 数据位
:可以选择的值有5,6,7,8这四个值,可以传输这么多个值为0或者1的bit位。这个参数最好为8,因为如果此值为其他的值时当你传输的是ASCII值时一般解析肯定会出问题。理由很简单,一个ASCII字符值为8位,如果一帧的数据位为7,那么还有一位就是不确定的值,这样就会出错。 校验位
:数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性。就比如传输“A”(01000001)为例。
1、当为奇数校验:”A”字符的8个bit位中有两个1,那么奇偶校验位为1才能满足1的个数为奇数(奇校验)。图-1的波形就是这种情况。
2、当为偶数校验:”A”字符的8个bit位中有两个1,那么奇偶校验位为0才能满足1的个数为偶数(偶校验)。
此位还可以去除,即不需要奇偶校验位。 停止位
:它是一帧数据的结束标志。可以是1bit、1.5bit、2bit的空闲电平。可能大家会觉得很奇怪,怎么会有1.5位~没错,确实有的。所以我在生产此uart信号时用两个波形点来表示一个bit。这个可以不必深究。。。 空闲位
:没有数据传输时线路上的电平状态。为逻辑1。 传输方向
:即数据是从高位(MSB)开始传输还是从低位(LSB)开始传输。比如传输“A”如果是MSB那么就是01000001(如图-2),如果是LSB那么就是10000010(如下图的图-4)
uart传输数据的顺序就是:刚开始传输一个起始位,接着传输数据位,接着传输校验位(可不需要此位),最后传输停止位。这样一帧的数据就传输完了。接下来接着像这样一直传送。在这里还要说一个参数。 帧间隔
:即传送数据的帧与帧之间的间隔大小,可以以位为计量也可以用时间(知道波特率那么位数和时间可以换算)。比如传送”A”完后,这为一帧数据,再传”B”,那么A与B之间的间隔即为帧间隔。
↑图-3
↑图-4
上两图和下两图传送的数据和波特率都是一样的,但是有几个参数是故意设置反了从而形成对比。有助于更深入的理解UART。
实现原理
实现原理就不讲了吧,就是用IO模拟上一切中的串口波形,跟单片机一个思路,只不过FPGA可以更准确的实现UART所需的波形,波特率也能做到更高。
代码说明
代码层次结构如下:
- sys_pll,用于由板载的10M生成25M时钟。
- uart_rx,UART的接收模块。
- uart_tx,UART的发送模块。
design_top_wrapper
module design_top_wrapper(
input wire clk_ref ,
input i_rst,
input wire rx ,
output wire tx
);
//wire sys_clk;
//wire clko;
//EF2_PHY_OSCDIV inst(
// .rstn(1),
// .stdby(0),
// .div(7'b000_0100),
// .clko(clko));
//EF2_LOGIC_BUFG BUFG_inst(
//.o(sys_clk),
//.i(clko)
//);
//reg [7:0] cnt_rst;
//always @(posedge sys_clk) if (!cnt_rst[7]) cnt_rst <= cnt_rst + 1'b1;
//wire sys_rst_n = cnt_rst[7];
wire sys_clk;
reg [15:0] cnt_rst;
reg pll_rst;
reg [2:0] i_rst_sync;
always @(posedge clk_ref) i_rst_sync <= {i_rst_sync[1:0], i_rst};
always @(posedge clk_ref, posedge i_rst_sync[2]) begin
if (i_rst_sync[2]) begin
cnt_rst <= 16'd1;
pll_rst <= 1;
end
else begin
if (!cnt_rst[15])
cnt_rst <= cnt_rst << 1;
pll_rst <= ~cnt_rst[15];
end
end
wire sys_rst_n;
sys_pll u_sys_pll(.refclk(clk_ref),
.reset(pll_rst),
.stdby(0),
.extlock(sys_rst_n),
.clk0_out(sys_clk));
wire [7:0] rx_data ;
wire rx_flag ;
uart_rx
#(
.UART_BPS ( 115200 ),
.CLK_FREQ ( 58_470_000)
)
uart_rx_inst
(
.sys_clk ( sys_clk ),
.sys_rst_n( sys_rst_n ),
.rx ( rx ),
.out_data ( rx_data ),
.out_flag ( rx_flag )
);
uart_tx
#(
.UART_BPS ( 115200 ),
.CLK_FREQ ( 58_470_000 )
)
uart_tx_inst
(
.sys_clk ( sys_clk ),
.sys_rst_n( sys_rst_n ),
.in_data ( rx_data ),
.in_flag ( rx_flag ),
.tx ( tx )
);
endmodule
可以看到在顶层PLL提供时钟,UART_TX提供发送,UART_RX进行接收,其中有一个rx_flag信号由UART_RX模块输出的,用于告知UART_TX进行发送,以完成环回。
sys_pll
是一个IP,不做代码分析。输入10M,输出25M,没什么可说的,关于IP配置可以参考软件教程。
UART_RX
模块端口
module uart_rx
#(
parameter UART_BPS = 'd9600 ,
parameter CLK_FREQ = 'd50_000_000
)
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire rx ,
output reg [7:0] out_data ,
output reg out_flag
);
- sys_clk,25M时钟
- sys_rst_n,复位
- rx,接收脚
- out_data,输出接收到的数据
- out_flag,输出接收到有效数据的信号
定义波特率计数器的最大计数值
localparam BAUD_CNT_MAX = CLK_FREQ / UART_BPS;
对rx管脚上输入的信号进行打拍同步
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_reg1 <= 1'b1;
else
rx_reg1 <= rx;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_reg2 <= 1'b1;
else
rx_reg2 <= rx_reg1;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_reg3 <= 1'b1;
else
rx_reg3 <= rx_reg2;
捕获UART起始条件,
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
start_flag <= 1'b0;
else if((rx_reg3 == 1'b1)&&(rx_reg2 == 1'b0)&&(work_en == 1'b0))
start_flag <= 1'b1;
else
start_flag <= 1'b0;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
work_en <= 1'b0;
else if(start_flag == 1'b1)
work_en <= 1'b1;
else if((bit_cnt == 4'd8)&&(bit_flag == 1'b1))
work_en <= 1'b0;
else
work_en <= work_en;
因为UART的起始条件是一个低脉冲,所以(rx_reg3 == 1'b1)&&(rx_reg2 == 1'b0)
判定为下降沿时start_flag <= 1'b1;
表示传输启动,同时work_en <= 1'b1;
表示已在进行一次8bit UART传输。
(bit_cnt == 4'd8)&&(bit_flag == 1'b1)
为8bit传输完成判断。
always@(posedge sys_clk or negedge sys_rst_n)
if((sys_rst_n == 1'b0))
baud_cnt <= 16'd0;
else if((baud_cnt == BAUD_CNT_MAX - 1'b1)||(work_en == 1'b0))
baud_cnt <= 16'd0;
else
baud_cnt <= baud_cnt + 1'b1;
上面代码用于产生波特率计时,后面的bit_flag用于标记一个计时满。
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
bit_cnt <= 4'd0;
else if((bit_flag == 1'b1)&&(bit_cnt == 4'd8))
bit_cnt <= 4'd0;
else if(bit_flag == 1'b1)
bit_cnt <= bit_cnt + 1'b1;
上面代码表示进行接收bit的计数。
else if((bit_cnt == 4'd8)&&(bit_flag == 1'b1))
rx_flag <= 1'b1;
rx_flag这个用于判定完成一个8bit接收,跟worker_on一样的逻辑。
always@(posedge sys_clk or negedge sys_rst_n)
if((sys_rst_n == 1'b0))
rx_data <= 8'b0;
else if((bit_cnt >= 4'd1)&&(bit_cnt <= 4'd8)&&(bit_flag == 1'b1))
rx_data <= {rx_reg3,rx_data[7:1]};
进行RX数据的采样和移位组装成8bit.由于UART先传你位,所以是左移。
always@(posedge sys_clk or negedge sys_rst_n)
if((sys_rst_n == 1'b0))
out_data <= 8'b0;
else if(rx_flag == 1'b1)
out_data <= rx_data;
always@(posedge sys_clk or negedge sys_rst_n)
if((sys_rst_n == 1'b0))
out_flag <= 1'b0;
else
out_flag <= rx_flag;
将rx_data, out_flag寄存后输出给UART_TX模块使用。
UART_TX
模块端口
module uart_tx
#(
parameter UART_BPS = 'd9600 ,
parameter CLK_FREQ = 'd50_000_000
)
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire [7:0] in_data ,
input wire in_flag ,
output reg tx
);
in_data, 要发送的数据
in_flag,启动发送的标志位
tx,UART发送管脚
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
work_en <= 1'b0;
else if(in_flag == 1'b1)
work_en <= 1'b1;
else if((bit_cnt == 4'd9)&&(bit_flag == 1'b1))
work_en <= 1'b0;
else
work_en <= work_en;
work_en表示正在进行传输,根据in_flag有效置1表示正在传输,
(bit_cnt == 4'd9)&&(bit_flag == 1'b1)
,表示传输完9位则传输结束,work_en置0。
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
baud_cnt <= 16'd0;
else if((baud_cnt == BAUD_CNT_MAX - 1'b1)||(work_en == 1'b0))
baud_cnt <= 16'd0;
else if(work_en == 1'b1)
baud_cnt <= baud_cnt + 1'b1;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
bit_flag <= 1'b0;
else if(baud_cnt == 16'd1)
bit_flag <= 1'b1;
else
bit_flag <= 1'b0;
第一段是波特率延时计数器,bit_flag表示一个波特率延时已达。
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
bit_cnt <= 4'd0;
else if((bit_cnt == 4'd9)&&(bit_flag == 1'b1))
bit_cnt <= 4'd0;
else if((bit_flag == 1'b1)&&(work_en == 1'b1))
bit_cnt <= bit_cnt + 1'b1;
else
bit_cnt <= bit_cnt;
对bit位进行计数。
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
tx <= 1'b1;
else if(bit_flag == 1'b1)
begin
case(bit_cnt)
0: tx <= 1'b0;
1: tx <= in_data[0];
2: tx <= in_data[1];
3: tx <= in_data[2];
4: tx <= in_data[3];
5: tx <= in_data[4];
6: tx <= in_data[5];
7: tx <= in_data[6];
8: tx <= in_data[7];
9: tx <= 1'b1;
default: tx = 1'b1;
endcase
end
选择输出数据,每次延时到达,bitcnt计数器变化就输出一位,这段代码其实用移位操作更好。
可以看到UART_TX的代码要简单很多。
上面代码中UART_TX UART_RX各自维护自己的延时计数器,其实没有必要,可以共用。