uart串口接收模块
创始人
2024-05-29 13:30:41

uart串口接收模块

1、UART(异步串行接口)

  串行通信:指利用一条数据线将资料一位位的顺序传输。
  异步通信:以一个字符为传输单位,通信中两个字符间的时间间隔是不固定的,然而在同一个字符的两个相邻位代码间的时间间隔是固定的。
  通信协议:指通信双方约定的一些规则。在使用串口通信的时候,规定有:空闲位、起始位、数据位、奇偶校验位、停止位。

2、串口通信时序

  这个协议在 FPGA 内部是除 SPI 之外最简单的接口吧,其实就是发送方与接收方相互认定的协议(暗号),这种接口数据一般是单向传输,所以发送方和接收方通信一般需要两根数据线。
在这里插入图片描述

图1 URAT时序图

  数据线在没有数据传输时保持高电平,当需要传输数据时,发送方把数据线拉低一段时间,告诉接收方开始传输数据了。之后把数据从低位到高位或者高位到低位(这个根据通信双方的要求确定)依次发送给对方(数据的位数双方应该事先确认好,通常5~8位数据)。数据发送完,可能会发送一位奇偶校验(这部分在下一节构建完整UART协议时细说)。最后就是将数据线拉高一段时间表示数据传输结束。

  在这之间就会有疑问,每位数据电平持续时间到底是多久?

  这就引出波特率,通常就是说每秒能传输多少位数据,比如波特率为9600bit/s,就是指1秒传输9600位数据(当然这是包含起始位,校验位,停止位在内的,所以有效数据其实并没有这么多)。当使用该波特率时,那每个电平持续时间不就是1/9600秒么。

3、串口接收模块设计

  首先确定模块接口信号,肯定有个串口的输入信号uart_rx吧,然后时钟信号clk和复位信号rst_n也是不可能少的。接收到数据后肯定要输出吧,所以在加一个uart_rx,注意该信号位宽应该是可以改变的(因为串口协议的数据位可以改变)。一般还要有一个信号用于指示接收到的数据什么时候是有效的,便于后续模块使用uart_rx,即uart_tx_vld(为高电平时,表示uart_rx有效)。
表1 端口信号

信号输入输出位宽定义
clkI1系统时钟,50MHZ
rst_nI1系统复位,低电平有效
uart_rxI1UART接口输入信号
rx_dataO8数据输出信号
rx_vldO1数据有效指示信号,高电平有效

  模块总体思路:有了输入输出信号后,模块内部就是根据输入信号生成输出信号而已。通过观察图1时序知道,每位数据传输需要使用 1/波特率 的时间,每次需要传输的 “数据” 包括起始位,数据位,校验位,结束位。那么以上是不是就对应两个计数器?所以使用计数器data_num来计数一位数据传输需要的时间(需要将1/波特率转换为系统时钟个数作为data_num的结束条件),使用计数器cnt来计数目前传输的第几位数据了。整体思路就是如此,大致如下图,接下来就是细节:

在这里插入图片描述

图2 计数器架构

  计数器data_num该从什么时候计数?
  当发送方发送起始位时会把数据线拉低,并且在之后一段时间内发送起始位,数据位等数据,那么data_num在此期间都要计数,直到停止位接收完成为止。由此引入一个标志信号flag,该信号为高电平时,计数器data_num就计数,当计数到时钟频率/波特率(1/波特率对应的时钟个数)时清零。

  故计数器data_num初始值为0,计数条件add_data_num = flag,结束条件为end_data_num = add_data_num && data_num == BSP_NUM - 1。

  flag当然就是检测到数据线下降沿时拉高,当计数器cnt计数结束时拉低,其余时间保持不变了。

  故flag拉高条件:检测到uart_rx下降沿,拉低条件为end_cnt。

  接下来就是计数器cnt了,cnt表示数据线此时传输的是第几位数据了。当计数器data_num计数结束时,表示一位数据传输完成了,此时cnt就应该加一了。当计数器计数到 起始位数+数据位数+校验位数+停止位数 时表示数据传输完成了,此时cnt计数结束并清零,其余时间保持不变。

  故计数器cnt初始值为0,计数条件add_cnt = end_data_num,计数器清零条件end_data_num = add_cnt && cnt == CNT_W - 1。CNT_W = 起始位数+数据位数+校验位数+停止位数 。

  接下来就是接收数据并产生输出信号了,一般会在计数器data_num计数的中部将数据线上的数据取下来进行保存,此时的数据是比较稳定的。由于最终需要输出的只是数据位,本文不考虑校验位,传输第0位是起始位,不需要保存。cnt==1时表示传输第1位数据,需要保存到输出信号上的最低位(这是由于串口调试助手是先发的最低位,实际情况要看发送方先发高位还是低位)。

  flag拉高后,计数器data_num进行计数,当计数完一位数据后清零,并且cnt计数器进行计数,当cnt大于等于1,小于等于8时,表示此时接收的是数据位,将接收到的数据保存到rx_data对应位(最好是在data_num为容量的一半时进行保存),当cnt计数器计数完成,表示一组数据接收完成,此时有效指示信号拉高,并且flag信号拉低,结束一组数据的接收;所以当cnt=1 && data_num == BSP_CNT/2-1时(BSP_CNT表示波特率对应的时钟个数),有rx_data[0] <= uart_rx。

  经过对其它位的详细分析,最终会得到这样的结果:当cnt >=1 && cnt <= DATA_W && data_num == BSP_CNT/2-1 && add_data_num 时(DATA_W表示每次发送的数据位位数),rx_data[cnt - 1] <= uart_rx;这样就产生了输出数据信号。

  之后就是产生输出有效指示信号,该信号当然是接收完数据时产生的,其实可以在计数器cnt计数结束时产生。但数据在接收完数据位后,其实数据就已经接收完成了,此时就可以把输出有效指示信号拉高了,这样后续模块就可以提前使用接收到的数据。所以当cnt == DATA_W && add_data_num && data_num == BSP_NUM/2-1时将rx_data_vld拉高,其余时间拉低。
  如果想要保证输出数据线上数据比较干净,不出现接收过程中的无效数据,那么可以将rx_data和rx_data_vld在rx_data_vld有效时才进行输出,其余时间保持不变。

  最后还要注意,数据线是其他芯片或者设备输入的信号,为了减小亚稳态出现的机率,一般需要将数据线上的信号通过寄存器寄存两个时钟。由于还需要检测数据线的下降沿,所以还要把该信号延迟一个时钟,最终将接收到的信号uart_rx打三拍(前两拍用于同步处理,最后一拍用于检测输入信号的下降沿),然后通过uart_rx_ff1和uart_rx_ff2检测出下降沿,把标志信号flag拉高。
在这里插入图片描述

图3 接收标志信号产生

  上述将模块内部信号讲完了,如果要实现功能完全够了,但是在调用模块时,我们往往不习惯去改模块内部的参数,这就需要通过parameter和localparam添加一些参数,来自动设置计数器位宽,计数器结束条件等等。其实人为需要设置的就是波特率、数据位位数、校验位数、停止位数(起始位是必须的,故不考虑设置参数),由于计算波特率对应是时钟个数时还需要知道系统时钟频率,所以增加一个系统时钟频率参数。

  所以parameter就定义波特率BPS、时钟频率FCLK、数据位数DATA_W、校验位数CHECK_W 、停止位数STOP_W 。而localparam需要通过parameter定义的参数得到波特率对应的 时钟数BPS_CNT=时钟频率FCLK/波特率BPS ,计数器data_num需要计数到BPS_CNT,所以需要通过BPS_CNT计算出计数器data_num的位宽BPS_CNT_W,可以通过以下函数实现。

function integer clogb2(input integer depth);beginif(depth==0)clogb2 = 1;else if(depth!=0)for(clogb2=0;depth>0;clogb2=clogb2+1)depth=depth>>1;end
endfunction

  接下来就是cnt计数器的结束条件了,可以由localparam定义CNT_NUM=DATA_W + CHECK_W + STOP_W。在利用上面函数计算出该计数器的位宽CNT_NUM_W就行了,内部信号根据这些常量变化即可。
  由此设计的模块在例化时,只需要修改parameter的几个常量即可,不要对模块内部代码做任何处理,这部分操作不会占用额外资源,在综合工具对齐进行综合时就会处理,不会消耗FPGA的除法器之类的资源。

  根据以上分析,直接得到以下代码,基本上不需要仿真调试。

4、参考代码

//--###############################################################################################
//--# Designer : 发送一位数据所需系统时钟数计算方式BPS_CNT = 1000_000_000/(Tclk*比特率),
//Tclk是系统时钟周期,单位ns。
//--###############################################################################################
module uart_rx #(parameter       FLCK    =       50_000_000  ,//系统时钟频率,默认50MHZ;parameter       BPS     =       9600        ,//串口波特率;parameter       DATA_W  =       8           ,//接收数据位数以及输出数据位宽;parameter       CHECK_W =       0           ,//校验位,0代表无校验位;parameter       STOP_W  =       1            //1位停止位;
)(input                           clk         ,//系统工作时钟50MHZinput                           rst_n       ,//系统复位信号,低电平有效input                           uart_rx     ,//UART接口输入信号output reg  [DATA_W-1:0]        rx_out      ,//数据输出信号output reg                      rx_out_vld   //数据有效指示信号);localparam      BPS_CNT   =     FLCK/BPS;//波特率为9600bit/s,当波特率为115200bit/s时,DATA_115200==434;localparam      BPS_CNT_W =     clogb2(BPS_CNT-1);//根据BPS_CNT调用函数自动计算计数器data_num位宽;localparam      CNT_NUM   =     DATA_W + CHECK_W + STOP_W;//计数器计数值;localparam      CNT_NUM_W =     clogb2(CNT_NUM);//根据计数器cnt的值,利用函数自动计算此计数器的位宽;reg                             rx_vld          ;//表示接收完一组串口发来的数据了;reg                             uart_rx_ff0     ;reg                             uart_rx_ff1     ;reg                             uart_rx_ff2     ;reg                             flag            ;reg     [BPS_CNT_W-1:0]         data_num        ;reg     [CNT_NUM_W-1:0]         cnt             ;reg     [DATA_W-1:0]            rx_data         ;wire                            add_data_num    ;wire                            end_data_num    ;wire                            add_cnt         ;wire                            end_cnt         ;/******************注释开始******************自动计算信号位宽;******************注释结束******************/function integer   clogb2(input integer depth);beginif(depth==0)clogb2 = 1;else if(depth!=0)for(clogb2=0;depth>0;clogb2=clogb2+1)depth=depth>>1;endendfunction/******************注释开始******************接收一位数据所用时间计数器data_num,初始值为0,当接收到数据时进行计数,当一位数据接收完成时清零;******************注释结束******************/always@(posedge clk or negedge rst_n)beginif(!rst_n)begindata_num <= {{BPS_CNT_W}{1'b0}};endelse if(add_data_num)beginif(end_data_num)data_num <= {{BPS_CNT_W}{1'b0}};elsedata_num <= data_num + {{{BPS_CNT_W-1}{1'b0}},1'b1};endendassign add_data_num = flag;       assign end_data_num = add_data_num && data_num==BPS_CNT-1;//接受一组数据所用时间;always@(posedge clk or negedge rst_n)beginif(!rst_n)begincnt <= {{CNT_NUM_W}{1'b0}};endelse if(add_cnt)beginif(end_cnt)cnt <= {{CNT_NUM_W}{1'b0}};elsecnt <= cnt + {{{CNT_NUM_W-1}{1'b0}},1'b1};endendassign add_cnt = end_data_num;       assign end_cnt = add_cnt && cnt== CNT_NUM-1;/******************注释开始******************PC端相对应于FPGA为异步接口,为预防亚稳态产生,对接收数据进行打两拍处理,由于需要采集信号下降沿,故打三拍处理;******************注释结束******************/always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//三个寄存器组成移位寄存器,初始化为0;{uart_rx_ff2,uart_rx_ff1,uart_rx_ff0} <= 3'd0;endelse begin//时钟上升沿时,将uart_rx信号移入移位寄存器,其余位左移一位;{uart_rx_ff2,uart_rx_ff1,uart_rx_ff0} <= {uart_rx_ff1,uart_rx_ff0,uart_rx};endendalways@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginflag <= 1'b0;endelse if(uart_rx_ff2==1 && uart_rx_ff1==0)begin//取UART_RX信号下降沿flag <= 1'b1;endelse if(end_cnt)begin//一组数据接收完毕;flag <= 1'b0;endend//在中间时刻对输入数据进行采集,并且将数据存入rx_data;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginrx_data <= {{DATA_W}{1'b0}};endelse if(cnt>=1 && cnt<=DATA_W && add_data_num && data_num==BPS_CNT/2-1)beginrx_data[cnt-1] <= uart_rx_ff2;endend//在接收完数据后,指示产生rx_data信号有效;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginrx_vld <= 1'b0;endelse beginrx_vld <= (cnt==CNT_NUM-1 && add_data_num && data_num==BPS_CNT/2-1);endend//当接收完一组数据后,将接收到的数据经过一组触发器暂存后输出;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//rx_out <= 0;endelse if(rx_vld)beginrx_out <= rx_data;endend//在接收完数据后,拉高一个时钟,指示产生rx_out信号有效;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginrx_out_vld <= 1'b0;endelse beginrx_out_vld <= rx_vld;endendendmodule

5、modelism仿真

  仿真部分的代码,通过一个任务task实现串口数据的发送,由于上述设计不支持校验位,所以这个模块设置校验位也是没有用的,将在下篇文章里面加入校验位,以及1.5位停止位之类的数据位。
  发送数据只需要调用tx();任务即可,内部直接输入待发送数据,数据位宽依旧通过DATA_W设置,波特率BPS设置。
参考代码:

`timescale 1 ns/1 ns
module uart_rx_test();parameter	CYCLE		= 20;//The unit is ns. The default value is 10ns;parameter	RST_TIME	= 10;//Reset time: Reset 3 clock widths by default;parameter	STOP_TIME	= 1000;//Time for simulation running after reset (unit: clock cycle). Simulation stops after 1000 clocks are run by default;// uart_rx Parametersparameter   FCLK        = 50_000_000;//系统时钟频率;parameter   BPS         = 9600      ;//串口波特率;parameter   BPS_CNT     = FCLK/BPS  ;parameter   DATA_W      = 8         ;//接收数据位数以及输出数据位宽;parameter   CHECK_W     = 0         ;//校验位,0代表无校验位;parameter   STOP_W      = 1         ;//1位停止位;// uart_rx Inputsreg                     clk         ;reg                     rst_n       ;reg                     uart_tx     ;// uart_rx Outputswire  [DATA_W-1:0]      rx_out      ;wire                    rx_out_vld  ;uart_rx #(.FLCK       (FCLK       ),.BPS        (BPS        ),.DATA_W     (DATA_W     ),.CHECK_W    (CHECK_W    ),.STOP_W     (STOP_W     ))u_uart_rx (.clk        ( clk           ),.rst_n      ( rst_n         ),.uart_rx    ( uart_tx       ),.rx_out     ( rx_out        ),.rx_out_vld ( rx_out_vld    ));//The local clock is generated at 100 MB;initial beginclk = 0; forever #(CYCLE/2) clk=~clk;end//Generate reset signal;initial beginrst_n = 1;#2; rst_n = 0;#(RST_TIME*CYCLE);//复位完成;rst_n = 1;end//Input signal din assignment method;initial begin#1;uart_tx = 1; //初始化时输入高电平;#(100*CYCLE);   //Start assigning values;tx(8'ha5);      //以串口形式发送8'h5a;#(500*CYCLE);   //发送完成后延迟500个时钟;tx(8'h5a);      //之后发送数据8'h59;$stop;          //Stop simulation;end//模拟串口发送函数,1位起始位,1位停止位,无校验位,8位数据,先发低位;integer i;//用于控制循环次数;task tx(input   [DATA_W-1:0]   data//串口待发送数据;);begin@(posedge clk);//延迟一个时钟后发送起始位;#1; uart_tx = 1'b0;repeat(BPS_CNT) @(posedge clk);//延迟BPS_CNT个时钟;for(i=0 ; i<8 ; i=i+1)begin#1; uart_tx = data[i];repeat(BPS_CNT) @(posedge clk);//延迟BPS_CNT个时钟;end@(posedge clk);//延迟一个时钟后发送停止位;#1; uart_tx = 1'b1;repeat(BPS_CNT) @(posedge clk);//延迟BPS_CNT个时钟;endendtaskendmodule

  仿真运行结果(rx_out先接收到8’ha5,后接收到8’h5a):
在这里插入图片描述

图4 仿真结果

  查看细节:开始接收数据(起始位)片段:
在这里插入图片描述

图5 起始位仿真

  接收最低位数据仿真如下:
在这里插入图片描述

图6 接收第一位数据

  接收最后一位数据,并且产生输出有效指示信号,下一个时钟将数据输出,此时串口传输实际上并没有完成,最后一位数据才传输一半(data_num计数器才2603==5208/2-1),但已经接收到完整数据,所以直接输出,节省时间,但flag信号依旧位高电平,表示该模块还处于工作状态。

图7 接收完最后一位数据

  计数器data_num计数到5208-1并且计数器cnt计数器到8,表示一次传输完成,flag信号拉低,并且两个计数器清零,表示完成传输,仿真如下:
在这里插入图片描述

图8 接收完停止位

6、综合测试

  这个工程很久了,之前学的时候使用quartus综合的,综合效果如下所示:
在这里插入图片描述

图9 quartus综合工程

  对应的RTL模块视图(由于时钟频率FCLK和波特率BPS参数设置会影响计数器cnt和data_num的位宽,所以不同数据汇总和出不同的电路,下图为时钟频率50MHZ,波特率9600的RTL视图):
在这里插入图片描述

图10 RTL视图

  对系统时钟频率进行约束后,最大时钟频率为120.86MHZ,远大于实际的50MHZ,满足时序要求;
在这里插入图片描述

图11 系统最大工作时钟频率

sigtap II 测试

  将程序下载到FPGA,打开串口调试助手,设置波特率9600,发送数据0XA5,使用signal tap II抓取数据8’hA5。
在这里插入图片描述

图12 串口助手发送数据

  串口调试助手发送数据0XB3,使用signal tap II抓取数据8’hB3。
在这里插入图片描述

图13 signal tap接收串口助手发送数据

  串口调试助手发送数据0X5a,使用signal tap II抓取数据8’h5A。
在这里插入图片描述

图14 调试

7、总结

  其实最主要的就是能够根据协议找到合适的主架构,然后根据该架构去产生输出信号。本文就利用两个计数器作为主架构,根据计数器的状态生成输出信号,切记我们需要的并不是计数器,而是计数器生成的输出信号,如果使用parameter要考虑模块内部各种会改变的数据与这些参数的关系,最好不要留需要手动修改的数据,这种数据如果忘记修改,会对后续设计造成很大影响,浪费调试时间。

  本模块还需要完善奇偶校验以及停止位,放在下文处理,需要原工程的自取。

  工程链接:https://pan.baidu.com/s/1oMLYvioXl496p9KH3rFy4w

  提取码:98qn

  时序图采用TimeGen所画,如果需要该软件,后台回复即可。

上一篇:JAVA进阶--->并发编程

下一篇:STM32弹U盘

相关内容

热门资讯

脑机接口遇到音乐治疗,AI真能... 志愿者体验“央音一号”。受访者供图 在走进中央音乐学院“央音一号”实验室之前,中青报·中青网记者对脑...
伊朗警告:若遭攻击必将还击 据外媒报道,伊朗议长卡利巴夫11日说,如果美国对伊朗发动打击,伊朗将把以色列以及美国在中东地区的军事...
SpaceX再部署7500颗星... 来源:@央视财经微博 【#SpaceX再部署7500颗星...
商络电子:向不特定对象发行可转... 商络电子公告,公司于2026年1月9日收到深圳证券交易所出具的《关于受理南京商络电子股份有限公司向不...
王毅原定访问索马里计划推迟 中... 新京报讯 据中国驻索马里使馆消息,有媒体报道,中共中央政治局委员、外交部长王毅原定1月9日访问索马里...