FPGA基础协议一:UART

FPGA基础协议一:UART

IDE:QuartusII 18.1(Lite)

仿真软件:Modelsim-Alterl

一、UART协议

1. UART协议

通用异步收发器(Universal Asynchronous Receiver/Transmitter),简称UART。 是一种串行1、异步2、全双工收发协议,应用十分广泛。UART工作原理是将数据的二进制位一位一位的进行传输。在UART通讯协议中信号线上的状态位高电平代表’1’低电平代表’0’。比如使用UART通信协议进行一个字节数据的传输时就是在信号线上产生八个高低电平的组合。当然两个设备使用UART串口通讯时,必须先约定好传输速率(波特率3)和一些数据位。

2. UART通信原理

UART作为异步串口通信协议的一种,工作原理是将数据的字节一位接一位地传输。协议如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RahVbcha-1657375060087)(C:\Users\12711\AppData\Roaming\Typora\typora-user-images\image-20220620161509768.png)]

空闲位:

UART协议规定,当总线处于空闲状态时信号线的状态为‘1’即高电平,表示当前线路上没有数据传输。

起始位:

每开始一次通信时发送方先发出一个逻辑”0”的信号(低电平),表示传输字符的开始。因为总线空闲时为高电平所以开始一次通信时先发送一个明显区别于空闲状态的信号即低电平。

数据位:

起始位之后就是我们所要传输的数据,数据位可以是5、6、7、8,9位等,构成一个字符(一般都是8位)。如ASCII码(7位),扩展BCD码(8位)。先发送最低位,最后发送最高位,使用低电平表示‘0’高电平表示‘1’完成数据位的传输。

奇偶校验位:

数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性。 校验位其实是调整个数,以下是奇偶校验两种方式:

  • 奇校验(odd parity):如果数据位中“1”的数目是偶数,则校验位为“1”,如果“1”的数目是奇数,校验位为“0”。
  • 偶校验(even parity):如果数据为中“1”的数目是偶数,则校验位为“0”,如果为奇数,校验位为“1”。

在FPGA中我们采用一位异或,将每两位相邻数据位异或后得到一位数据,再根据设置的奇校验偶校验来判断校验位的值。

停止位:

它是一个字符数据的结束标志。可以是1位、1.5位、2位的高电平。 由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备之间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟的机会。停止位个数越多,数据传输越稳定,但是数据传输速度也越慢。

波特率:

数据传输速率使用波特率来表示。单位bps(bits per second),常见的波特率9600bps、115200bps等等,其他标准的波特率是1200,2400,4800,19200,38400,57600。举个例子,如果串口波特率设置为9600bps,那么传输一个比特需要的时间是1/9600≈104.2us。

而在FPGA中常用的时钟频率是50MHz,也就是20ns一个时钟周期,如果选择波特率是115200,也就是说1s内传输115200bit,那么每个bit所需要的是50,000,000/115200≈434个时钟周期。

二、需求与设计分析

1. 系统模块划分

  • uart_rx.v:串口接收模块,将上位机接收到的串行数据转换为并行数据。
  • uart_ctrl.v:控制模块,将接收到的并行数据存储到FIFO中,再从FIFO中读取数据一一传输到tx模块中。
  • uart_tx.v:串口发送模块,将从FIFO中读出的并行数据转换为串行数据发送到上位机中去。
  • key_debounce:按键消抖模块

image-20220709214951232

这里为什么需要一个FIFO?

当一次性发送的数据达到一定量时,单单使用串并转换在时间上来讲是不够地,可能会造成数据丢失,所以需要一个fifo来缓存数据。当需要发送的时候再从fifo逐一提取数据,而且根据先进先出的原则,输出后的数据与输入进来的顺序也不会变。

2. 模块解析

2.1 uart_rx 串口接收模块

这个模块主要负责串并转换,将从上位机接收到的数据存储到10个位宽的寄存器中(一个数据帧算上起始位和停止位一共10个位宽)并在rx_vld有效时发送到Ctrl模块中。

并串转换需要两个计数器,一个是根据波特率bps调整的波特baud计数器,一个是bit计算器。因为C4的时钟频率是50Mhz,所以波特率需要以时钟周期为单位重新计算:

    localparam  BAUD_9600   = 13'd5208  , //50M/9600
                BAUD_38400  = 13'd1302  , //50M/38400
                BAUD_57600  = 13'd868   , //50M/57600
                BAUD_115200 = 13'd434   ; //50M/115200

//波特率设置
always @(*) begin 
    case(baud_set)  
        2'd0 :	bps = BAUD_9600  ;  
        2'd1 :	bps = BAUD_38400 ;  
        2'd2 :	bps = BAUD_57600 ;  
        2'd3 :	bps = BAUD_115200;  
        default : bps = BAUD_115200;
    endcase
end

两个计数器:

    reg      [12:00]    cnt_bps ;   //波特计算器
    wire                add_bps ;
    wire                end_bps ;

	reg      [03:00]    cnt_bit ;   //bit计数器
    wire                add_bit ;
    wire                end_bit ;

//bps计数器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        cnt_bps <= 1'b0;
    end 
    else if (add_bps) begin
        if (end_bps) begin
            cnt_bps <= 1'b0;
        end else begin
            cnt_bps <= cnt_bps + 1'b1;
        end
    end
    else begin
        cnt_bps <= cnt_bps;
    end
end
assign add_bps = tx_vld;
assign end_bps = add_bps && cnt_bps == bps;


//bit计数器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        cnt_bit <= 1'b0;
    end 
    else if (add_bit) begin
        if (end_bit) begin
            cnt_bit <= 1'b0;
        end else begin
            cnt_bit <= cnt_bit + 1'b1;
        end
    end
    else begin
        cnt_bit <= cnt_bit;
    end
end
assign add_bit = end_bps;
assign end_bit = add_bit && cnt_bit == 9;

两个计数器开始的一个重要条件是当输入信号有效时才开始,那么如何判断有效呢?

可以利用uart空闲时高点平,发送数据帧的起始位是低电平这一特点来进行判断,当在空闲时检测到低电平则拉高有效信号,bit计数器完成技术后再拉低。

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        tx_vld <= 1'b0;
    end 
    else if (~up_tx) begin
        tx_vld <= 1'b1;
    end
    else if (end_bit) begin
        tx_vld <= 1'b0;
    end
    else begin
        tx_vld <= tx_vld;
    end
end

接下来是串并转换,利用之前的计数器来完成并行数据的赋值

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        rx_data_r <= 10'b00000_00000;
    end 
    else if (add_bps && cnt_bps == (bps >> 1) -1) begin	//取波特计数中间时的值可以避免因时序的错误赋值
        rx_data_r[cnt_bit] <= up_tx;
    end
    else begin
        rx_data_r <= rx_data_r;
    end
end

输出的并行数据完成完整的转换需要时间,如果一直输出就会使ctrl模块接收到错误的值,所以还需要一个发送rx_data的有效模块,rx_vld,同样在end_bit完成后拉高就行了。

assign rx_vld = end_bit;

代码虽然没有贴全,但如果看懂我说的也基本没有影响。

2.2 uart_ctrl 串口接收转存模块

为什么要叫ctrl?不知道,大家都这么叫,可能是我翻译有问题,我只是根据我理解的它的实际用途来命名的。但想一想把按键加入这个模块来控制发送后叫控制模块确实也没有问题。

这个模块负责接收端的数据缓存处理,并响应按键信号来将存入的数据发送到发送端。

这里有个busy信号需要注意一下,它由串口发送端输出,高电平表示串口发送端正在工作,低电平表空闲。需要这样一个信号是为了防止过快地从fifo中读取数据结果覆盖了正在发送的数据,导致数据错误。

assign rdreq = key && ~empty && ~busy;
assign tx_vld = rdreq;

fifo作为缓存模块,所存入的数据位宽与输出数据位宽需要与rx_data对齐,即[09:00],深度可以根据需求随意,这只影响你所能存入的最大数据量。

    wrfifo	wrfifo_inst (
	.aclr       (~rst_n     ),
	.data       (rx_data    ),
	.clock      (clk        ),
	.rdreq      (rdreq      ),
	.wrreq      (wrreq      ),
	.q          (q          ),
	.empty      (empty      ),
	.usedw      (usedw      ),
	.full       (full       )
	);

image-20220709215245753

2.3 uart_tx 串口发送模块

多数内容与rx模块一致,唯二的不同:

  1. 这个模块是做并串转换的
  2. 需要输出忙碌信号

这两点甚至也只需要两行代码:

assign tx = rx_flag?rx_data[cnt_bit]:1'b1;
assign busy = rx_flag;
//rx_flag表接收有效信号,思路与rx模块的类似。

2.4 按键消抖模块

这个模块有多种写法,网上有很多相关文章,这里就略过了。

2.5 top 顶层模块

module uart_top (
    input               clk     ,
    input               rst_n   ,
/*输入信号*/
    input               rx      ,
    input               key     ,
/*输出信号*/
    output              tx      
);
    //剩下的自己写啦~

2.6 RTL总视图

image-20220709215200772

3. 仿真

3.1 仿真文件

根据串口协议的时序图来做一个仿真文件,这里注意起始位停止位和按键消抖即可。

initial begin
    rx = 1'bz;
    key = 1'b1;
    #(CYCLE*4);
    rx = 1'b1;
    #(CYCLE*4);
    repeat(4)begin
        rx = 1'b0;
        #(CYCLE*434);
            repeat(8)begin
                rx = $random;
                #(CYCLE*434);
            end
        rx = 1'b1;
        #(CYCLE*868);
    end

    repeat(4)begin
        #(CYCLE*434*8);
        key = 1'b0;
        #(CYCLE*434*2);
        key = 1'b1;
    end
    #(CYCLE*4);
    $stop;
end

只是简单的看一下效果,所以测试时发送4个数据帧就差不多了

3.2 波形图

概览:

image-20220709212244131

解析验证:

细看后可以发现,第一个完整的数据帧是0011111101(图中因为时序原因是倒过来显示的)

image-20220709212428182

fifo存入的第一个数据帧也是0011111101

image-20220709212927858

同样被输出的第一个数据帧也是0011111101

image-20220709213027086

符合先入先出原则,并且数据没有错误,仿真成功。

三、上板验证

image-20220709213136962

每按下一次key则发送一个fifo中的数据,符合先入先出的原则并且数据没有错误,证明串并转换也没问题,成功!

四、总结

我发现我的输入输出端口命名很混乱,模块内部的输入输出统一成din,dout会在读写代码时思路更清晰一点。

五、参考文章

曾小庆: UARTt通信协议

EPCCcc:【FPGA】串口通信UART


  1. 串行通信是指利用一条传输线将数据一位位地顺序传送 ↩︎

  2. 两个uart设备之间通信的时候不需要时钟线,这时候就需要在两个uart设备上指定相同的传输速率 ↩︎

  3. 每秒钟传送的二进制位数 ↩︎

猜你喜欢

转载自blog.csdn.net/ChenJ_1012/article/details/125699483