1、概述

在这篇文章:基于FPGA的任意字节数的串口发送(含源码工程)中实现了基于FPGA的任意字节数的串口发送,那么对应的,这一篇文章将分享给大家如何实现任意字节的FPGA接收方法。

在这篇文章:串口(UART)的FPGA实现(含源码工程),实现了基于FPGA的串口接收驱动。利用接收驱动可以实现 起始位1bit+数据位8bit+停止位1bit 共10bit的单字节接收。

但是在实际应用过程中有时候需要一次性接收多个字节的数据。比如,一次性通过UART接收5个字节的数据,再将其组合成一个位宽为【39:0】的数据。诚然,可以直接更改此文中的串口接收驱动,使其变成起始位1bit+数据位40bit+停止位1bit共42bit的多字节传输。这种方法理论上是可行的,因为UART协议并没有规定你一次要发送、接收多个少bit的数据,既然能接收8个bit,那同样能接收40个bit。

但是很不幸,实际上基本行不通,因为通用的绝大部分上位机软件都不支持一次解析40bit的数据位(如果你自己写上位机就当我没说)。

所以只能想点其他办法,比如:写个逻辑,多次调用8bit即单字节的串口接收驱动,那么40bit的数据就调用5次,也就是5个起始位1bit+数据位8bit+停止位1bit共10bit的单字节 分别接收,再组合成一个40bit的数据就可以了。


2、串口接收驱动

请参考:串口(UART)的FPGA实现(含源码工程),在此文详细介绍了串口的接收驱动。

以下代码可以实现 1bit+数据位8bit+停止位1bit共10bit的单字节接收,无奇偶校验。

// *******************************************************************************************************// ** 作者 : 孤独的单刀                                                   // ** 邮箱 : zachary_wu93@163.com// ** 博客 : https://blog.csdn.net/wuzhikaidetb // ** 日期 : 2022/08/05// ** 功能 : 1、基于FPGA的串口接收驱动模块;//  2、可重新设置波特率BPS、主时钟CLK_FRE;//  3、起始位1bit,数据位8bit,停止位1bit,无奇偶校验。                                                                                                                     // *******************************************************************************************************module uart_rx#(parameterintegerBPS= 9_600,//发送波特率parameter integerCLK_FRE= 50_000_000//输入时钟频率)(//系统接口input sys_clk,//50M系统时钟input sys_rst_n,//系统复位//UART接收线input uart_rxd,//接收数据线//用户接口output reg uart_rx_done,//数据接收完成标志,当其为高电平时,代表接收数据有效output reg [7:0]uart_rx_data//接收到的数据,在uart_rx_done为高电平时有效);//param definelocalparamintegerBPS_CNT = CLK_FRE / BPS;//根据波特率计算传输每个bit需要多个系统时钟//reg definereg uart_rx_d1;//寄存1拍reg uart_rx_d2;//寄存2拍reg uart_rx_d3;//寄存3拍reg [31:0]clk_cnt;//计数器,用于计数发送一个bit数据所需要的时钟数reg [3:0]  bit_cnt;//bit计数器,标志当前发送了多少个bitreg rx_en;//接收标志信号,拉高代表接收过程正在进行reg [7:0]uart_rx_data_reg;//接收数据寄存//wire definewire neg_uart_rxd;//接收数据线的下降沿assignneg_uart_rxd = uart_rx_d3 & (~uart_rx_d2);//捕获数据线的下降沿,用来标志数据传输开始 //将数据线打3拍,作用1:同步不同时钟域信号,防止亚稳态;作用2:捕获下降沿always@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)beginuart_rx_d1 <= 1'b0;uart_rx_d2 <= 1'b0;uart_rx_d3 <= 1'b0;endelse beginuart_rx_d1 <= uart_rxd;uart_rx_d2 <= uart_rx_d1;uart_rx_d3 <= uart_rx_d2;endend//捕获到数据下降沿(起始位0)后,拉高传输开始标志位,并在第9个数据(终止位)的传输过程正中(数据比较稳定)再将传输开始标志位拉低,标志传输结束always@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)rx_en <= 1'b0;else begin if(neg_uart_rxd )rx_en > 1'b1) && (uart_rx_d3 == 1'b1) )rx_en <= 1'b0;else rx_en <= rx_en;endend//当数据传输到终止位时,拉高传输完成标志位,并将数据输出always@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)beginuart_rx_done <= 1'b0;uart_rx_data > 1'd1) && (uart_rx_d3 == 1'b1))beginuart_rx_done <= 1'b1;//仅仅拉高一个时钟周期uart_rx_data <= uart_rx_data_reg;endelse beginuart_rx_done <= 1'b0;//仅仅拉高一个时钟周期uart_rx_data <= uart_rx_data;endend//时钟每计数一个BPS_CNT(传输一位数据所需要的时钟个数),即将数据计数器加1,并清零时钟计数器always@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)beginbit_cnt <= 4'd0;clk_cnt <= 32'd0;endelse if(rx_en)begin            //在接收状态if(clk_cnt < BPS_CNT - 1'b1)begin           //一个bit数据没有接收完clk_cnt <= clk_cnt + 1'b1;              //时钟计数器+1bit_cnt <= bit_cnt;                     //bit计数器不变end                                         else begin                                  //一个bit数据接收完了clk_cnt <= 32'd0;                       //清空时钟计数器,重新开始计时bit_cnt <= bit_cnt + 1'b1;              //bit计数器+1,表示接收完了一个bit的数据end                                         end                                             else begin                                  //不在接收状态bit_cnt <= 4'd0;                        //清零clk_cnt <= 32'd0;                       //清零endend//在每个数据的传输过程正中(数据比较稳定)将数据线上的数据赋值给数据寄存器always@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)uart_rx_data_reg > 1'b1) begin                 //传输过程正中(数据比较稳定)case(bit_cnt)                         //根据位数决定接收的内容是什么4'd1:uart_rx_data_reg[0] <= uart_rx_d3;        //LSB最低位4'd2:uart_rx_data_reg[1] <= uart_rx_d3;        //4'd3:uart_rx_data_reg[2] <= uart_rx_d3;        //4'd4:uart_rx_data_reg[3] <= uart_rx_d3;        //4'd5:uart_rx_data_reg[4] <= uart_rx_d3;        //4'd6:uart_rx_data_reg[5] <= uart_rx_d3;        //4'd7:uart_rx_data_reg[6] <= uart_rx_d3;        //4'd8:uart_rx_data_reg[7] <= uart_rx_d3;        //MSB最高位default:;                                    //1和9分别是起始位和终止位,不需要接收endcase                                          end                                                  else                                                 //数据不一定稳定就不接收uart_rx_data_reg <= uart_rx_data_reg;            elseuart_rx_data_reg <= 8'd0;//不处于接收状态endendmodule 

3、任意字节接收的实现方法

串口接收驱动模块的对外端口如下:

所以,多字节数据的接收逻辑:

  • 1、用户通过UART数据多次发送单字节数据(比如5个)
  • 2、根据uart_rx_done信号判断是否成功接收到一个信号,并用计数器记录此时接收的是第几个字节的数据,同时将其移位寄存起来
  • 3、当所有单字节数据均被接收完毕后,拉高uart_bytes_vld,表示一次多字节接收结束,且此时的数据uart_bytes_data是有效数据

完整代码如下:

// *****************************************************************************************************************************// ** 作者 : 孤独的单刀                                                   // ** 邮箱 : zachary_wu93@163.com// ** 博客 : https://blog.csdn.net/wuzhikaidetb // ** 日期 : 2022/08/05// ** 功能 : 1、基于FPGA的串口多字节接收模块;//  2、可设置一次接收的字节数、波特率BPS、主时钟CLK_FRE;//  3、UART协议设置为起始位1bit,数据位8bit,停止位1bit,无奇偶校验(不可在端口更改,只能更改发送驱动源码);                                                                                                                     //  4、每接收到1次多字节后拉高指示信号一个周期,指示一次多字节接收结束;//  5、数据接收顺序,先接收低字节、再接收高字节。如:第1次接收到8’h34,第2次接收到8’h12,则最终接收到的数据为16'h12_34。                                                                                                                    // *****************************************************************************************************************************module uart_bytes_rx#(parameterintegerBYTES  = 4,//一次接收字节数,单字节8bitparameterintegerBPS = 9600,//发送波特率parameter integerCLK_FRE = 50_000_000//输入时钟频率)(//系统接口input sys_clk,//系统时钟input sys_rst_n,//系统复位,低电平有效//用户接口output[(BYTES * 8 - 1):0] uart_bytes_data,//接收到的多字节数据,在uart_bytes_vld为高电平时有效outputuart_bytes_vld,//成功发送所有字节数据后拉高1个时钟周期,代表此时接收的数据有效//UART接收input uart_rxd//UART发送数据线rx);//reg definereg[(BYTES*8-1):0]uart_bytes_data_reg;//寄存接收到的多字节数据,先接收低字节,后接收高字节reguart_bytes_vld_reg;//高电平表示此时接收到的数据有效reg[9:0]byte_cnt;//发送的字节个数计数(因为懒直接用10bit计数,最大可以表示1024BYTE,大概率不会溢出)//wire definewire[7:0]uart_sing_data;//接收的单个字节数据wireuart_sing_done;//单个字节数据接收完毕信号//对端口赋值assign uart_bytes_data = uart_bytes_data_reg;assign uart_bytes_vld  = uart_bytes_vld_reg;//分别接收各个字节的数据always @(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)uart_bytes_data_reg <= 0;else if(uart_sing_done)begin//接收到一个单字节则将数据右移8bit,实现最先接收的数据在低字节if(BYTES == 1)//单字节就直接接收uart_bytes_data_reg <= uart_sing_data;else//多字节就移位接收uart_bytes_data_reg <= {uart_sing_data,uart_bytes_data_reg[(BYTES*8-1)-:(BYTES-1)*8]};endelseuart_bytes_data_reg <= uart_bytes_data_reg;end//对接收的字节个数进行计数always @(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)byte_cnt <= 0;else if(uart_sing_done && byte_cnt == BYTES - 1)//计数到了最大值则清零byte_cnt <= 0;else if(uart_sing_done)//发送完一个单字节则计数器+1byte_cnt <= byte_cnt + 1'b1;elsebyte_cnt <= byte_cnt;end//所有数据接收完毕,拉高接收多字节数据有效信号always @(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)uart_bytes_vld_reg <= 1'b0;else if(uart_sing_done && byte_cnt == BYTES - 1)//所有单字节数据接收完毕uart_bytes_vld_reg <= 1'b1;else uart_bytes_vld_reg <= 1'b0;end//例化串口接收驱动模块uart_rx #(.BPS(BPS),.CLK_FRE(CLK_FRE))uart_rx_inst(.sys_clk(sys_clk),.sys_rst_n(sys_rst_n),.uart_rx_done(uart_sing_done),.uart_rx_data(uart_sing_data),.uart_rxd(uart_rxd));endmodule 

4、仿真

使用该模块接收数据3次,观测接收结果是否正常。分别使用单字节(8位)、双字节(16位)、5字节(40位)进行测试。


4.1、单字节仿真

模拟上位机按UART协议三次发送随机的单字节数据过来,观察能否正确接收。TB如下:

`timescale 1ns/1ns//定义时间刻度module tb_uart_bytes_rx();localparamintegerBYTES    = 1;//一次接收的字节个数localparamintegerBPS  = 230400;//波特率localparamintegerCLK_FRE  = 50_000_000;//系统频率50MlocalparamintegerCNT      = 1000_000_000 / BPS;//计算出传输每个bit所需要的时间,单位:nsreg sys_clk;//系统时钟reg sys_rst_n;//系统复位,低电平有效reg uart_rxd;//UART接收数据线wire[(BYTES * 8 - 1):0] uart_bytes_data;//接收到的多字节数据,在uart_bytes_vld为高电平时有效wireuart_bytes_vld;//当其为高电平时,代表此时接收到的多字节数据有效initial beginsys_clk <=1'b0;sys_rst_n <=1'b0;uart_rxd <=1'b1;#20 //系统开始工作sys_rst_n <=1'b1;#3000repeat(3) begin//重复生成8位随机数rx_byte({$random} % 256);//end#60$finish();endalways #10 sys_clk=~sys_clk;//设置主时钟,20ns,50M//定义任务,每次发送的数据10 位(起始位1+数据位8+停止位1)task rx_byte(input [7:0] data);integer i; //定义一个常量//用 for 循环产生一帧数据,for 括号中最后执行的内容只能写 i=i+1for(i=0; i<10; i=i+1) begincase(i)0: uart_rxd <= 1'b0;//起始位1: uart_rxd <= data[0];//LSB2: uart_rxd <= data[1];3: uart_rxd <= data[2];4: uart_rxd <= data[3];5: uart_rxd <= data[4];6: uart_rxd <= data[5];7: uart_rxd <= data[6];8: uart_rxd <= data[7];//MSB9: uart_rxd <= 1'b1;//停止位endcase#(CNT+10); //每发送1 位数据延时(加10是为了减小误差)endendtask //任务结束//例化多字节接收模块uart_bytes_rx #(.BYTES(BYTES),.BPS(BPS),.CLK_FRE(CLK_FRE))uart_bytes_rx_inst(.sys_clk(sys_clk),.sys_rst_n(sys_rst_n),.uart_bytes_data(uart_bytes_data),.uart_bytes_vld(uart_bytes_vld),.uart_rxd(uart_rxd));endmodule 

仿真结果如下:

  • 模拟上位机调用了3次,分别发送数据8’h24,8’h81,8’h09
  • 串口接收模块接收到了同样的3个数据8’h24,8’h81,8’h09

4.2、双字节仿真

TB基本不用修改,把BYTES这个参数改成2,并把repeat的次数改成6就行。仿真结果如下:

  • 上位机分别发送了6个数据8’h24,8’h818’h09,8’h638’h0d,8’h8d
  • 接收到了3个数据16’h812416’h630916’h8d0d
  • 根据先接收低字节后高字节的原则,将6个单字节组合成了3个双字节接收

4.3、5字节仿真

TB基本不用修改,把BYTES这个参数改成5,并把repeat的次数改成10就行。仿真结果如下:

您照着上面的逻辑看看就行,我就不啰嗦了。


5、实测

下板实测,可以通过上位机发送数据到FPGA,同时使用在线逻辑仪signaltap II来观察接收的数据是否与发送一致。


5.1、单字节实测

上位机发送数据8’h55:

FPGA同样接收到数据8’h55:

测试结果与预期一致。


5.2、双字节实测

上位机发送数据8’h55、8’haa:

FPGA接收到数据16’haa55(先接收低字节,后接收高字节):

测试结果与预期一致。


5.3、5字节实测

上位机发送数据8’h9a、8’h78、8’h56、8’h34、8’h12:

FPGA接收到数据40’h123456789a(先接收低字节,后接收高字节):

测试结果与预期一致。

源码工程点击这里下载(提取码:oc60)


  • 博客主页:wuzhikai.blog.csdn.net
  • 本文由孤独的单刀原创,首发于CSDN平台
  • 您有任何问题,都可以在评论区和我交流!
  • 创作不易,您的支持是我持续更新的最大动力!如果本文对您有帮助,还请多多点赞、评论和收藏⭐!