FPGA学习项目——RAM读写与UART串口数据发送

目录

一、工程简介

二、功能介绍

三、具体实现

         1.RAM IP核的配置

2.读写模块

1)RAM 写模块设计

2)RAM 读模块设计

3.串口发送模块

4.读写时钟模块

5.顶层模块设计

四、仿真与板级验证


一、工程简介

     使用正点原子达芬奇A7板子,在学习8位以及多字节数据的UART串口的接受和发送,以及ROM,RAM存储器IP核的建立和使用等课程后,尝试完成了一道自命题项目:向RAM中循环写入2048个16位数据,通过UART串口将数据发送至上位机,且在发送数据的同时还在向RAM中继续存数据。

二、功能介绍

     1.数据是16位的,使用串口发送2个字节的数据到电脑上。

     2.写入的数据这里暂定与写地址相同,因为位宽不等(2048个数据地址是12位的,这里写入的数据是16位的,进行高位补零)。

     3.在发送数据的同时还在向RAM中继续存数据,因此简单的单口RAM不能实现数据的同时读写,所以这里使用伪双端RAM。

     4.要求RAM数据的读写速度和串口数据发送速度相匹配,这里暂时不用FIFO,所以需要根据数据发送波特率调整RAM读写的时钟频率。

根据设计功能,可将本项目大体可划分为五个模块:

  • RAM核调用
  • 读写时钟模块
  • RAM写模块
  • RAM读模块
  • 串口发送模块

三、具体实现

1.RAM IP核的配置

      双端口 RAM 是指拥有两个读写端口的 RAM ,有伪双端口 RAM (一个端口只能读,另一个端口只能写)和真双端口 RAM (两个端口都可以进行读写操作)之分。一般当我们需要同时对存储器进行读写操作时会使用到双端口 RAM ,例如有一个 FIFO 存储器,我们需要同时对其进行数据的写入和读出,这时候就需要一个写端口和一个读端口了。 
     伪双端口 RAM 输入有两路时钟信号 CLKA/CLKB ;独立的两组地址信号ADDRA/ADDRB; Port A 仅提供 DINA 写数据总线,作为数据的写入口; Port B 仅提供数据读的功能,读出的数据为 DOUTB
                                
                                              简单(伪)双端口 RAM 框图
需要注意的是在伪双端口模式下我们需要避免读写冲突,即同时刻读写同一地址所出现的冲突。
                  
这里写入数据是2048个16位的数据,所以存储器位宽要设置成16位,深度要大于2048。 完成 RAM  IP 核的配置,如下图所示:
                 

2.读写模块

1)RAM 写模块设计

      在 RAM 写模块中,我们的输入信号主要有系统时钟信号和系统复位信号;输出有控制写 RAM 所需的 ram_wr_en (写端口使能)、 ram_wr_we (写使能)、 ram_wr_addr (写 地址)和 ram_wr_data (写数据)这四个信号,以及控制读模块启动 rd_flag (读启动)信号。由上述分析
绘制出如下图所示的模块框图:

                             

       因为我们需要一直向 RAM 中写入数据,所以当复位结束后,我们就将 ram_wr_en (写端口使能)和 ram_wr_we( ram 写使能)一直置为高。当写使能拉高后,写地址一直在 0~2048 之间循环计数,并向对应的RAM地址中写入数据,当写地址第一次计数到 1024  时,将 rd_flag 信号拉高并保持,以启动读模块进行读操作。这里引入了一个读启动信号(rd_flag),该信号的作用是错开两个端口的启动时间,以此达到错开读写地址,防止读写冲突的目的。哪怕我们只错开一个地址,也是可以避免读写冲突的,这里我们在写完一半的数据时拉高 rd_flag信号,只是为了方便我们进行观察而多错开了一些地址而已。
编写代码
  module ram_wr(
        input Clk , //时钟信号
        input Reset_n , //复位信号,低电平有效4 
        //RAM 写端口操作 
        output ram_wr_we , //ram 写使能
        output reg ram_wr_en , //端口使能
        output reg rd_flag , //读启动信号
        output reg [11:0] ram_wr_addr , //ram 写地址 
        output [15:0] ram_wr_data //ram 写数据
 );

 //*****************************************************
 //**  main code
 //*****************************************************

 //ram_wr_we 为高电平表示写数据
  assign ram_wr_we = ram_wr_en ;

 //写数据与写地址相同,因位宽不等,所以高位补 0
  assign ram_wr_data = {2'b0,ram_wr_addr} ;
  
 //控制 RAM 使能信号
 always @(posedge Clk or negedge Reset_n) begin
     if(!Reset_n)
        ram_wr_en <= 1'b0;
     else
        ram_wr_en <= 1'b1;
     end

 //写地址信号 范围:0~2048
 always @(posedge Clk or negedge Reset_n) begin
     if(!Reset_n) 
        ram_wr_addr <= 12'd0;
     else if(ram_wr_addr < 12'd2048 && ram_wr_we)
        ram_wr_addr <= ram_wr_addr + 1'b1;
     else
        ram_wr_addr <= 12'd0;
     end 

 //当写入 2048 个数据(0~1024)后,拉高读启动信号
 always @(posedge Clk or negedge Reset_n) begin
    if(!Reset_n)
       rd_flag <= 1'b0;
    else if(ram_wr_addr == 11'd1024) 
       rd_flag <= 1'b1;
    else
       rd_flag <= rd_flag;
    end 

 endmodule

2)RAM 读模块设计

RAM 读模块中,我们的输入信号主要有系统时钟信号、系统复位信号、从 RAM 中读出的数据( ram_rd_data )以及我们自己定义的读启动标志信号( rd_flag );输出有控制读 RAM 所需的 ram_rd_en (读端口使能)和 ram_rd_addr (读地址)这两个信号。由上述分析绘制出如下图所示的模块框图:
                            
编写代码
module ram_rd(
    input Clk , //时钟信号
    input  Reset_n , //复位信号,低电平有效4 
    //RAM 读端口操作 
    input rd_flag , //读启动标志
    input [15:0] ram_rd_data, //ram 读数据
    output ram_rd_en , //端口使能
    output reg [11:0] ram_rd_addr //ram 读地址 
 );
 //*****************************************************
 //** main code
 //*****************************************************

 //控制 RAM 使能信号
 assign ram_rd_en = rd_flag; 

 //读地址信号 范围:0~2048 
 always @(posedge Clk or negedge Reset_n) begin
 if(!Reset_n) 
    ram_rd_addr <= 12'd0;
 else if(ram_rd_addr < 12'd2048 && ram_rd_en)
    ram_rd_addr <= ram_rd_addr + 1'b1;
 else
    ram_rd_addr <= 12'd0;
 end

 endmodule
注意:
1 、伪双端 RAM 时,因为端口 A 只能写不能读,所以 WEA 可以理解为端口 A 的写使能信号(高有效)
2 、端口 B 只能读,其没有像 WEA 一样的读写使能信号,所以当端口使能后,便一直处于读状态。

3.串口发送模块

1)串口通信模块设计的目的是用来发送数据的,因此需要有一个数据输入端口。

2)波特率是 UART 通信中需要设置的参数之一。在波特率时钟生成模 块中,计数器需要的计数值与波特率之间的关系如下表所示,其中系统时钟周期为 Clk,这里为 20ns。如果接入到该模块的时钟频率为其他值,需要根据具体的频率值修改该参数。

波特率计算
baud_set
波特率
波特率周期
波特率分频计数值
50M 系统时钟计数值
09600
104167ns
104167/Clk
5208-1
119200
52083ns
52083/Clk
2604-1
238400
26041ns
26041/Clk
1302-1
357600
17361ns
17361/Clk
868-1
4115200
8680ns
8680/Clk
434-1
      本模块的设计是为了保证模块的复用性。当需要不同的波特率时,只需设置不同的波
特率时钟计数器的计数值。使用查找表即可实现,下面的设计代码中只包含了针对 5 个波
特率的设置,如需要其他波特率可根据实际使用情况具体修改。
reg [17:0]bps_DR;
   always@(*)
      case(Baud_set)
      0:bps_DR= 1000000000/9600/20;
      1:bps_DR= 1000000000/19200/20;
      2:bps_DR= 1000000000/38400/20;
      3:bps_DR= 1000000000/57600/20;
      4:bps_DR= 1000000000/115200/20;
      default: bps_DR= 1000000000/9600/20;
      endcase

3)串口通信的本质就是将8位的并行数据通过一根信号线在不同的时刻传输并行数据的不同位,通过多个时刻,最终将8位并行数据全部传出。

4)串口通信以1位的低电平标志串行传输的开始,待8位数据传输完成之后,再以1位高电平标志传输的结束。

5)控制信号,控制并转串模块,什么时候开始工作,什么时候一个8位数据发送完成?须有一个发送开始信号以及一个发送完成信号。

                            

编写代码

module uart_8(
    Clk,
    Reset_n,
    Data,
    Baud_set,
    uart_tx,
    Send_Go,
    Tx_done
    );
   input Clk;
   input Reset_n;
   input Send_Go;
   input [7:0]Data;
   input [2:0]Baud_set;
   output reg Tx_done;
   output reg uart_tx;
   
   //Baud_set=0,波特率=9600;
   //Baud_set=1,波特率=19200;
   //Baud_set=2,波特率=38400;
   //Baud_set=3,波特率=57600;
   //Baud_set=4,波特率=115200;
   reg [17:0]bps_DR;
   always@(*)
      case(Baud_set)
      0:bps_DR= 1000000000/9600/20;
      1:bps_DR= 1000000000/19200/20;
      2:bps_DR= 1000000000/38400/20;
      3:bps_DR= 1000000000/57600/20;
      4:bps_DR= 1000000000/115200/20;
      default: bps_DR= 1000000000/9600/20;
      endcase
  
reg Send_en;           //send_en变成内部信号,由顶层send_go信号输入到这里进行控制
 always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
   Send_en<=0;
else if(Send_Go)
   Send_en<=1;
else if(Tx_done)                   
   Send_en<=0;
   
 reg [7:0]r_Data;    
 always@(posedge Clk )
 if(Send_Go)
    r_Data<=Data;
 else
    r_Data<=r_Data;
    
 
   
 //bps_clk
 wire bps_clk;
 assign bps_clk=(counter==1);
 
   //自定义数据位传输时间,小状态计数器 Time 
reg [17:0]counter; 
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
   counter<=0;
else if(Send_en)begin
    if(counter==bps_DR-1)
         counter<=0;
     else
          counter<=counter+1'b1;
   end
else
   counter<=0;
   
  reg [3:0]counter2;
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
   counter2<=0;   
else if(Send_en)  begin 
    if(bps_clk)begin         
       if(counter2==11)                 
           counter2<=0;
       else 
            counter2<=counter2+1'b1;
      end
    end
else 
   counter2<=0;  

always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
  uart_tx<=1;   //要求数据在不传送状态时保持高电平。为在开始传输数据时可以辨别起始位
else begin
    case(counter2)
       1:uart_tx<=0;
       2:uart_tx<=r_Data[0];
       3:uart_tx<=r_Data[1];
       4:uart_tx<=r_Data[2];
       5:uart_tx<=r_Data[3];
       6:uart_tx<=r_Data[4];
       7:uart_tx<=r_Data[5];
       8:uart_tx<=r_Data[6];
       9:uart_tx<=r_Data[7];
       10:uart_tx<=1;
       11:uart_tx<=1;                               
         default:uart_tx<=uart_tx;
    endcase
    end

always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
    Tx_done<=0;
else if((bps_clk==1)&&(counter2==10))                         //这是为了让Tx_done只保持一个时钟周期时间  所作出的条件改变
    Tx_done<=1;
else
    Tx_done<=0;
    
endmodule

因为UART协议规定了,发送的数据位只能有6,7,8位。
在第一种情况下,等待传输请求(Trans_Go)的到来,Data40[7:0]给到uart_byte_tx的Data,并同时产生Send_Go信号,启动第一个字节的发送。
接着等待Tx_Done信号的到来。
16位数据是否发完了?
发完了,回到第一步继续等Trans_Go。
没发完,启动下一个8位数据的发送

module uart_rx_16bit(
   Clk,
   Reset_n,
   Data16,
   Trans_Go,
   uart_tx,
   Trans_done
);
   input Clk;
   input Reset_n;
   input [15:0]Data16;
   input Trans_Go;
   output  uart_tx;
   output reg Trans_done;
    
   reg [7:0]Data;           //Data成为一个内部信号
   reg  Send_Go;            //Send_Go作为一个内部信号使能uart_0中的send_en
   wire Tx_done;
   
     uart_8  uart_8(
    .Clk(Clk),
    .Reset_n(Reset_n),
    .Data(Data),
    .Baud_set(3'd4),
    .uart_tx(uart_tx),
    .Send_Go(Send_Go),
    .Tx_done(Tx_done)
 );
 
reg [2:0]state;
 always@(posedge Clk or negedge Reset_n)
    if(!Reset_n)begin
      state<=0;
      Data<=0;
      Send_Go<=0;
      Trans_done<=0;
    end
    else case(state)
    0:begin                                   //情况0:等待传输请求。
        Trans_done<=0;
        if(Trans_Go)begin                                       //请求到来,使能Send_Go,把第1字节数据赋给数据传输模块,情况从0转到1.
           Data<=Data16[7:0];
//           Data<=Data16[15:8];
           Send_Go<=1;
           state<=1;
           end
        else begin                                               //请求没有到来,继续保持不变;
           Data<=Data;
           Send_Go<=0;
           state<=0;
           end
        end
        
    1:begin                                  //情况1:传输数据
         if(Tx_done)begin                                     //第1节数据传输完成Tx_done信号拉高,把第2字节数据赋给数据传输模块,情况从1转到2.
            Data<=Data16[15:8];
//            Data<=Data16[7:0];
            Send_Go<=1;
            state<=2;
         end        
         else begin                                               //第1节数据传输没有完成,继续传输;
           Data<=Data;
           Send_Go<=0;
           state<=1;
         end
     end
     
    2:begin                                  
         if(Tx_done)begin                                     
            Send_Go<=0;
            state<=0;
            Trans_done<=1;
         end        
         else begin                                              
           Data<=Data;
           Send_Go<=0;
           state<=2;
         end
     end
     
     default:begin                                              
           Data<=Data;
           Send_Go<=0;
           state<=0;
         end  
     endcase
         
          
endmodule

这里写的uart_rx16bit模块,其中调用uart_8,波特率在例化时直接写的4即选择的115200。

4.读写时钟模块

数据发送的波特率是115200,即1s发送115200个数据位,由于每8位数据位有1个起始位和1个停止位,发送两个字节数据共20位。即频率f=1/(115200/20)=5760Hz。计数器计数MNCA=1000000000/5760/20/2=4340

编写代码

module clk_divider(
    input Reset_n,
    input Clk,
    output reg clk_5760Hz
);
  //ram读写时钟,要符合115200波特率的数据发送速度
 parameter MNCA=4340;
reg [22:0] counter = 0;
always @(posedge Clk or negedge Reset_n)
    if(!Reset_n)
       clk_5760Hz<=0; 
    else if (counter ==MNCA) begin
        counter <= 0;
        clk_5760Hz <= ~clk_5760Hz;
      end 
    else 
        counter <= counter + 1;

endmodule

5.顶层模块设计

顶层模块中实例化各个功能模块并调用,根据上述几个部分编写功能模块。

module uart_ram_test(
  input Clk,
  input Reset_n,
  output uart_tx
    );
  wire clk_5760Hz;
  wire rd_flag;
  wire [15:0]rd_data;
  wire rd_en;
  wire [11:0]rd_addr;
  wire wr_we;
  wire wr_en;
  wire [11:0]wr_addr;
  wire [15:0]wr_data;
  wire Trans_done;
  reg Trans_Go;
  
clk_divider clk_ram(
    .Reset_n(Reset_n),
    .Clk(Clk),
    .clk_5760Hz(clk_5760Hz)
);
     
  RAM  RAM(
  .clka(clk_5760Hz),
  .ena(wr_en),
  .wea(wr_we),
  .addra(wr_addr),
  .dina(wr_data),
  .clkb(clk_5760Hz),
  .enb(rd_en),
  .addrb(rd_addr),
  .doutb(rd_data)
);
ram_wr ram_wr(
  .Clk(clk_5760Hz), 
  .Reset_n( Reset_n), 
  .ram_wr_we(wr_we),
  .ram_wr_en(wr_en),
  .rd_flag(rd_flag),
  .ram_wr_addr(wr_addr), 
  .ram_wr_data(wr_data)
 );
ram_rd ram_rd(
   .Clk (clk_5760Hz),
   .Reset_n( Reset_n) ,
   .rd_flag (rd_flag), 
   .ram_rd_data(rd_data), 
   .ram_rd_en (rd_en), 
   .ram_rd_addr(rd_addr) 
 );
 
 uart_rx_16bit uart_rx_16bit(
   .Clk(Clk),
   .Reset_n(Reset_n),
   .Data16(rd_data),
   .Trans_Go(1'b1),
   .uart_tx(uart_tx),
   .Trans_done(Trans_done)
);
endmodule

四、仿真与板级验证

进行综合后的原理图如下:

写个tb文件,进行行为级仿真,结果如下:

添加约束后上板验证,结果如下:

总结:这仅仅作为自己的学习笔记进行整理,其中还有很多不完善的地方,还请大家多多指教。

  • 47
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值