PotatoPie 4.0 实验教程(14) —— FPGA实现UART串口通信-Anlogic-安路社区-FPGA CPLD-ChipDebug

PotatoPie 4.0 实验教程(14) —— FPGA实现UART串口通信

1.实验说明

在上位机中通过串口调试助手向 PotatoPie 开发板中发送串口数据,开发板将接收到的数据发送给上
位机,完成数据的回环,该例程通信框图如下图所示

20231220165727322-image

通过 UART 可以与主机或其他外部设备进行串口通信。
UART 是一种通用的数据通信协议,也是异步串行通信口的总称, 在进行 UART 通信时可以配置传输速度、
数据格式等参数, 将数据通过串行通讯进行传输。
UART 通信协议如下图所示:

20231220165627800-image

管脚说明

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 数据。

20231220165751399-image

上位机接收到的数据与发送的数据一致, UART 数据回环传输正确。

实验原理

UART介绍

UART(Universal Asynchronous Receiver/Transmitter)是一种异步全双工串行通信协议,由Tx和Rx两根数据线组成,因为没有参考时钟信号,所以通信的双方必须约定串口波特率数据位宽奇偶校验位停止位等配置参数,从而按照相同的速率进行通信。
异步通信以一个字符为传输单位,通信中两个字符间的时间间隔多少是不固定的,然而在同一个字符中的两个相邻位间的时间间隔是固定的。当波特率为9600bps时,传输一个bit的时间间隔大约为104.16us;波特率为115200bps时,传输一个bit的时间间隔大约为8us。

20231220174352833-image

数据传送速率用波特率来表示,即每秒钟传送的二进制位数。例如数据传送速率为120字符/秒,而每一个字符为10位(1个起始位,7个数据位,1个校验位,1个结束位),则其传送的波特率为10×120=1200字符/秒=1200波特。
数据通信时序图:

20231220174835276-image

其中各位的意义如下 : 
起始位:先发出一个逻辑”0”信号,表示传输字符的开始;
数据位:可以是5~8位逻辑”0”或”1”;如ASCII码(7位),扩展BCD码(8位);小端传输,即LSB先发,MSB后发;
校验位:数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验);
停止位:它是一个字符数据的结束标志。可以是1位、1.5位、2位的高电平(用于双方同步,停止位时间间隔越长,容错能力越强);
空闲位:处于逻辑“1”状态,表示当前线路上没有数据传送;
注:异步通信是按字符传输的,接收设备在收到起始信号之后只要在一个字符的传输时间内能和发送设备保持同步就能正确接收。下一个字符起始位的到来又使同步重新校准(依靠检测起始位来实现发送与接收方的时钟自同步的)。

20231220174438431-image

↑图-1 起始位和停止位

20231220174451730-image

↑图-2 数据位

20231220174503645-image

↑传输“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之间的间隔即为帧间隔。 

20231220174649102-image

↑图-3

20231220174709636-image

  ↑图-4

上两图和下两图传送的数据和波特率都是一样的,但是有几个参数是故意设置反了从而形成对比。有助于更深入的理解UART。

20231220174742615-image

实现原理

实现原理就不讲了吧,就是用IO模拟上一切中的串口波形,跟单片机一个思路,只不过FPGA可以更准确的实现UART所需的波形,波特率也能做到更高。

代码说明

代码层次结构如下:

20231220172244309-image

  • 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配置可以参考软件教程。

20231220172951993-image

20231220173001535-image

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各自维护自己的延时计数器,其实没有必要,可以共用。

请登录后发表评论