基于DDR3的串口传图帧缓存系统设计实现

整体设计框图如图所示
在这里插入图片描述

模块名称模块功能
uart_byte_rx模块负责串口图像数据的接收
bit8_trans_bit16模块将串口接收的每两个 8bit 数据转换成一个 16bit 数据(图像 数据是 16bit 的 RGB565 的数据,电脑是通过串口将一个像素点数据分两次发送到 FPGA, FPGA 需将串口接收数据重组成 16bit 的图像数据),实现过程相对比较简单。
disp_driver模块tft 屏显示驱动控制,对缓存在 DDR3 中的图像数据进行显示。
wr_ddr3_fifo 模块使用的 FIFO IP ,主要用于写入 DDR3 数据的缓存、解决数 据跨时钟域以及数据位宽的转换。
rd_ddr3_fifo 模块使用的 FIFO IP ,主要用于读出 DDR3 数据的缓存、解决数据 跨时钟域以及数据位宽的转换。
fifo2mig_axi 模块主要是用于接口的转换,将 MIG IP 的 AXI 接口换成与 FIFO对接的接口
mig_7series_0 模块主要是用于接口的转换,将 MIG IP 的 AXI 接口换成与 FIFO
pll 模块上述各个模块所需时钟的产生,使用 PLL IP。

1、wr_ddr3_fifo:这个FIFO处于串口接收模块与fifo2mig_axi模块之间,主要是由于两个模块之间时钟和数据传输速率不一致导致需要进行数据缓存。端口数据是 16bit 图像数据,写入时钟采用与串口接收模块相同的工作时钟 50MHz。读端口数据是用于写入到 DDR 存储器,与 DDR 控制器保持一致,数 据位宽使用 128bit,读时钟使用 200MHz 的 ui_clk 时钟。

根据分析,需创建一个独立时钟的 读写数据位宽不一样的 FIFO,具体 FIFO 的相关配置如下图。注意,这里 FIFO 的 Read Mode 需要设置成 First Word Fall Through,写入数据深度暂时设置为 512,根据后面设计情况可进行修改。
在这里插入图片描述

在这里插入图片描述

2、rd_ddr3_fifo

这个 FIFO 的写入数据是来自从 DDR 存储器读出的数据,而读出的数据 是用于图像显示。所以写端口数据位宽为 128bit,写入时钟使用 MIG IP 输出的 200MHz 的 ui_clk 时钟。读端口数据位宽为 16bit,读时钟使用与 TFT 驱动时钟一致的时钟

3、PLL模块

  1. pll 模块是用于产生整个系统中所需各种时钟,整个系统中主要使用到 3 个时钟,分别 为 DDR 控制器所需 200MHz 时钟串口模块工作的 50MHz 时钟5 寸 TFT 屏驱动模块的 33MHz。ACX720 板卡 FPGA 的输入时钟是 50MHz 的输入时钟。根据分析可知,PLL 模块 的输入时钟是来自外部的 50MHz 晶振时钟,输出时钟为 200MHz、50MHz、33MHz。具体 IP 配置界面如下图,其余界面保持默认即可。

4、fifo2mig_axi模块

fifo2mig_axi 模块是系统中相对比较重要的模块,涉及到与 DDR 控制器接口对接。该模块的主要是实现接口的转换,将普通的 FIFO 接口转换成 AXI 接口,用于将 FIFO 里的数据读出然后存储在 DDR 存储器以及将 DDR 存储器读出的数据存放到 FIFO 缓存。

AXI 接口包括 5 个通道,分为写事务和读事务。考虑模块设计实现的简单性(AXI 协议 支持复杂的乱序读写操作等,这里就不做考虑),

将一次完整的写事务流程规定为

  1. 主机向写地址通道写入地址和控制信息。
  2. 写数据通道突发写入数据。
  3. 收到设备的写数据响应。

一次完整的读事务流程规定为

  1. 主机向读地址通道写入地址和控制信息。
  2. 收到设备的读数据响应和读的数据。

写事务的结构图:
在这里插入图片描述

读事务的结构图:

  • 这 5 条独立的通道都包含一个双路的 VALID、READY 握手机制。信息源通过 VALID 信号来指示通道中的数据和控制信息什么时候有效目地源用 READY 信号来表示何时准备好接收数据。传输地址信息和数据都是在 VALID 和 READY 同时为高时有效。
  • 读数据和写数据通道都包括一个 LAST 信号,用来指明一个事物传输的最后一个数据
  • 读/写事务都有自己的地址通道,地址通道携带着传输事务所必须的地址和信息。
  • 读数据通道传送着从设备到主机的读数据和读响应信息。读响应信息指明读事务的完成状态。
  • 写数据通路传送着主机向设备的写数据和写控制信息。写响应通道提供了设备响应写事务的一种方式。在每一次突发式写会产生一个完成信号。

突发读时序图:

在这里插入图片描述

主机发送地址和控制信息到写地址通道中

  1. 当地址通道上 ARVALID 和 ARREADY 同时为高时,地址 A 被有效的传给设备,之后设备输出的数据将出现在读数据通道上。

  2. 当 RREADY 和 RVALID 同时为高的时候表明有效的数据传输,从图中可以看到传输了 4 个数 据。

  3. 为了表明一次突发式读写的完成,设备用 RLAST 信号变高电平来表示最后一个被传输 的数据,D(A3)是本次读突发的最后一个读数据。
    在这里插入图片描述
    写操作的开始时,主机发送地址和控制信息到写地址通道中:

  4. 当地址通道上 AWVALID 和 AWREADY 同时为高时,地址 A 被有效的传给设备。然后主机发送每一个写数据到写数 据通道中,

  5. 当 WREADY 和 WVALID 同时为高的时候表明一个有效的写数据,

  6. 当主机发送 最后一个数据时,WLAST 信号就变为高。

  7. 当设备接收完所有数据之后,设备将一个写响应 发送回主机来表明写事务完成,当 BVALID 和 BREADY 同时为高的时候表明有效的响应。

对于 DDR 控制器 mig_7series_0 模块,需要初始化结束后即 init_calib_complete为高后,才能进行读/写操作。读/写操作不可同时进行,对读/写操作就需要有一个判断仲裁的过程 fifo2mig_axi 模块状态机设计如下图所示。

在这里插入图片描述

上电初始状态为 IDLE 状态,当 DDR 完成初始化和校准(即 init_calib_complete 变为高 电平)后进入读/写仲裁状态 ARB;在该状态根据是否有读/写操作请求跳转到读/写流程的各个状态;完成一次读/写流程后,状态回到 ARB 状态进行下一次的操作。状态机采用三段式,第一二段的代码如下。

always@(posedge ui_clk or posedge ui_clk_sync_rst)begin
	if(ui_clk_sync_rst)
    	curr_state <= S_IDLE;
    else
 		curr_state <= next_state;
end
always@(*)begin
	case(curr_state)
 //具体状态转移见下
    	S_IDLE:begin
            	if(init_calib_complete)
 					next_state = S_ARB;
				else
 					next_state = S_IDLE;
		 end

 	endcase
end
always@(posedge ui_clk or posedge ui_clk_sync_rst)begin
	if(ui_clk_sync_rst)
    	curr_state <= S_IDLE;
    else
 		curr_state <= next_state;
end
always@(*)begin
	case(curr_state)
 //具体状态转移见下
    	S_IDLE:begin
            	if(init_calib_complete)
 					next_state = S_ARB;
				else
 					next_state = S_IDLE;
		 end

 	endcase
end

第三段为

always@(posedge ui_clk or posedge ui_clk_sync_rst)
 begin
 if(ui_clk_sync_rst)
 m_axi_awaddr <= WR_DDR_ADDR_BEGIN;
 else if(wr_addr_clr)
 m_axi_awaddr <= WR_DDR_ADDR_BEGIN;
 else if(m_axi_awaddr >= WR_DDR_ADDR_END)
 m_axi_awaddr <= WR_DDR_ADDR_BEGIN;
 else if((curr_state == S_WR_RESP) && m_axi_bready && m_axi_bvalid && (m_axi_bresp == 2'b00)
&& (m_axi_bid == AXI_ID))
 m_axi_awaddr <= m_axi_awaddr + ((m_axi_awlen + 1'b1)<<4);
 else
 m_axi_awaddr <= m_axi_awaddr;
 end

整体的代码

module fifo2mig_axi
#(
  parameter WR_DDR_ADDR_BEGIN = 0      ,
  parameter WR_DDR_ADDR_END   = 200    ,
  parameter RD_DDR_ADDR_BEGIN = 0      ,
  parameter RD_DDR_ADDR_END   = 200    ,
  
  parameter AXI_ID            = 4'b0000,
  parameter AXI_LEN           = 8'd31    //burst length = 32
)
(
  //FIFO Interface ports
  input               wr_addr_clr         ,//sync ui_clk
  output              wr_fifo_rdreq       ,
  input     [127:0]   wr_fifo_rddata      ,
  input               wr_fifo_empty       ,
  input     [8:0]     wr_fifo_rd_cnt      ,
  input               wr_fifo_rst_busy    ,

  input               rd_addr_clr         ,
  output              rd_fifo_wrreq       ,
  output    [127:0]   rd_fifo_wrdata      ,
  input               rd_fifo_alfull      ,
  input     [8:0]     rd_fifo_wr_cnt      ,
  input               rd_fifo_rst_busy    ,
  // Application interface ports
  input               ui_clk              ,
  input               ui_clk_sync_rst     ,
  input               mmcm_locked         ,
  input               init_calib_complete ,
  // Slave Interface Write Address Ports
  output    [3:0]     m_axi_awid          ,
  output reg[27:0]    m_axi_awaddr        ,
  output    [7:0]     m_axi_awlen         ,
  output    [2:0]     m_axi_awsize        ,
  output    [1:0]     m_axi_awburst       ,
  output    [0:0]     m_axi_awlock        ,
  output    [3:0]     m_axi_awcache       ,
  output    [2:0]     m_axi_awprot        ,
  output    [3:0]     m_axi_awqos         ,
  output reg          m_axi_awvalid       ,
  input               m_axi_awready       ,
  // Slave Interface Write Data Ports
  output    [127:0]   m_axi_wdata         ,
  output    [15:0]    m_axi_wstrb         ,
  output reg          m_axi_wlast         ,
  output reg          m_axi_wvalid        ,
  input               m_axi_wready        ,
  // Slave Interface Write Response Ports
  input     [3:0]     m_axi_bid           ,
  input     [1:0]     m_axi_bresp         ,
  input               m_axi_bvalid        ,
  output              m_axi_bready        ,
  // Slave Interface Read Address Ports
  output    [3:0]     m_axi_arid          ,
  output reg[27:0]    m_axi_araddr        ,
  output    [7:0]     m_axi_arlen         ,
  output    [2:0]     m_axi_arsize        ,
  output    [1:0]     m_axi_arburst       ,
  output    [0:0]     m_axi_arlock        ,
  output    [3:0]     m_axi_arcache       ,
  output    [2:0]     m_axi_arprot        ,
  output    [3:0]     m_axi_arqos         ,
  output reg          m_axi_arvalid       ,
  input               m_axi_arready       ,
  // Slave Interface Read Data Ports
  input     [3:0]     m_axi_rid           ,
  input     [127:0]   m_axi_rdata         ,
  input     [1:0]     m_axi_rresp         ,
  input               m_axi_rlast         ,
  input               m_axi_rvalid        ,
  output              m_axi_rready        
);

  localparam S_IDLE    = 7'b0000001,
             S_ARB     = 7'b0000010,
             S_WR_ADDR = 7'b0000100,
             S_WR_DATA = 7'b0001000,
             S_WR_RESP = 7'b0010000,
             S_RD_ADDR = 7'b0100000,
             S_RD_RESP = 7'b1000000;

  wire[7:0]wr_req_cnt_thresh;
  wire[7:0]rd_req_cnt_thresh;
  wire     wr_ddr3_req ;
  wire     rd_ddr3_req ;
  reg [6:0]curr_state  ;
  reg [6:0]next_state  ;
  reg      wr_rd_poll  ;  //0:allow wr  1:allow rd
  reg [7:0]wr_data_cnt ;

  assign m_axi_awid    = AXI_ID   ; //output [3:0]      m_axi_awid   
  assign m_axi_awsize  = 3'b100   ; //output [2:0]      m_axi_awsize 
  assign m_axi_awburst = 2'b01    ; //output [1:0]      m_axi_awburst
  assign m_axi_awlock  = 1'b0     ; //output [0:0]      m_axi_awlock 
  assign m_axi_awcache = 4'b0000  ; //output [3:0]      m_axi_awcache
  assign m_axi_awprot  = 3'b000   ; //output [2:0]      m_axi_awprot 
  assign m_axi_awqos   = 4'b0000  ; //output [3:0]      m_axi_awqos    
  assign m_axi_awlen   = AXI_LEN  ;
  
  assign m_axi_wstrb   = 16'hffff ; //output [15:0]     m_axi_wstrb 
  assign m_axi_wdata   = wr_fifo_rddata;

  assign m_axi_bready  = 1'b1     ; //output            m_axi_bready

  assign m_axi_arid    = AXI_ID   ; //output [3:0]      m_axi_arid   
  assign m_axi_arsize  = 3'b100   ; //output [2:0]      m_axi_arsize 
  assign m_axi_arburst = 2'b01    ; //output [1:0]      m_axi_arburst
  assign m_axi_arlock  = 1'b0     ; //output [0:0]      m_axi_arlock 
  assign m_axi_arcache = 4'b0000  ; //output [3:0]      m_axi_arcache
  assign m_axi_arprot  = 3'b000   ; //output [2:0]      m_axi_arprot 
  assign m_axi_arqos   = 4'b0000  ; //output [3:0]      m_axi_arqos
  assign m_axi_arlen   = AXI_LEN  ;

  assign m_axi_rready  = ~rd_fifo_alfull; //output            m_axi_rready

  assign wr_fifo_rdreq  = m_axi_wvalid && m_axi_wready;
  assign rd_fifo_wrreq  = m_axi_rvalid && m_axi_rready;
  assign rd_fifo_wrdata = m_axi_rdata;  

  assign wr_req_cnt_thresh = (m_axi_awlen == 1'b0)? 1'b0 : AXI_LEN-1'b1;
  assign rd_req_cnt_thresh = AXI_LEN;
  assign wr_ddr3_req = (wr_fifo_rst_busy == 1'b0) && (wr_fifo_rd_cnt >= wr_req_cnt_thresh) ? 1'b1:1'b0;
  assign rd_ddr3_req = (rd_fifo_rst_busy == 1'b0) && (rd_fifo_wr_cnt <= rd_req_cnt_thresh) ? 1'b1:1'b0;
  
//**********************************
//三段式状态机
//**********************************

//第一段状态机**同步电路 描述状态转移
always@(posedge ui_clk or posedge ui_clk_sync_rst)begin
    if(ui_clk_sync_rst)
        curr_state <= S_IDLE;
    else
        curr_state <= next_state;
end
//第二段状态机**组合逻辑电路 判断状态转移
always@(*)begin
    case(curr_state)
    
        // 状态机在上电复位处于初始状态 IDLE,当 DDR 完成初始化和校准后进入读/写仲裁状态 ARB
        S_IDLE:begin
            if(mmcm_locked && init_calib_complete)
                next_state = S_ARB;
            else
                next_state = S_IDLE;
        end
        
        // 在读/写仲裁状态 ARB,根据当前的读/写请求跳转到读/写操作流程中的地址通道的操作。
        S_ARB:begin
            if((wr_ddr3_req == 1'b1) && (wr_rd_poll == 1'b0))
                next_state = S_WR_ADDR;
            else if((rd_ddr3_req == 1'b1) && (wr_rd_poll == 1'b1))
                next_state = S_RD_ADDR;
            else
                next_state = S_ARB;
        end
        
        //当在 ARB 状态出现写操作请求后,进入到 AXI 写地址通道的操作状态 S_WR_ADDR,
        //在该状态,传输写操作的地址和控制信息,当 awready 和 awvalid 同时为高时表明地址已经
        //传输完成,进入到写操作的写数据通道的操作。
        S_WR_ADDR:begin
            if(m_axi_awready && m_axi_awvalid)
                next_state = S_WR_DATA;
            else
                next_state = S_WR_ADDR;
        end

        //当主机写完最后一个数据后 m_axi_wlast 置高,进入等待写相应的状态。
        S_WR_DATA:begin
            if(m_axi_wready && m_axi_wvalid && m_axi_wlast)
                next_state = S_WR_RESP;
            else
                next_state = S_WR_DATA;
        end

        //在等待写响应状态,当主机接收到设备的写响应后,一次完整的写操作流程完成,状态
        //回到仲裁状态进行下一次的操作,bresp 不同值表示不同的响应结果,bresp 为 2’b00 表示写
        //数据成功,bid 需要与写地址通道传输的 awbid 一致。
        S_WR_RESP:begin
            if(m_axi_bready && m_axi_bvalid && (m_axi_bresp == 2'b00) && (m_axi_bid == AXI_ID))
                next_state = S_ARB;
            else if(m_axi_bready && m_axi_bvalid)
                next_state = S_IDLE;
            else
                next_state = S_WR_RESP;
        end

        S_RD_ADDR:begin
            if(m_axi_arready && m_axi_arvalid)
                next_state = S_RD_RESP;
            else
                next_state = S_RD_ADDR;
        end

        S_RD_RESP:begin
            if(m_axi_rready && m_axi_rvalid && m_axi_rlast && (m_axi_rresp == 2'b00) && (m_axi_rid == AXI_ID))
                next_state = S_ARB;
            else if(m_axi_rready && m_axi_rvalid && m_axi_rlast)
                next_state = S_IDLE;
            else
                next_state = S_RD_RESP;
        end

        default: next_state = S_IDLE;
    endcase
  end

状态机设计完成后,剩下的就是在各个状态中产生各种信号。

写操作的写地址通道比较关键的是产生 awaddrawvalid。其中,awaddr 除了在复位和清除时变为起始地址外,在完成一次写操作流程后,地址就需要增加一次突发写入的数据 量,需要注意的是这里的地址是以字节为单位的,则每次地址增加量应该是突发写数据个数 ×每个数据的字节数。这里每次突发长读为 AWLEN 加 1,每个数据是 16 字节(数据位宽是 128bit),所以每完成一次写操作,地址增加(m_axi_awlen + 1’b1)*16。

这里 AXI 地址对应的数据是以 1 字节进行计算的,不要与 DDR3 的地址和存储数据混淆。板载 DDR3 存储器存储空间 2Gbit(2Gbit = 256MByte = 2^28 Byte,所以 AXI 的地址位宽为 28)。

  always@(posedge ui_clk or posedge ui_clk_sync_rst)
  begin
    if(ui_clk_sync_rst)
      m_axi_araddr <= RD_DDR_ADDR_BEGIN;
    else if(rd_addr_clr)
      m_axi_araddr <= RD_DDR_ADDR_BEGIN;
    else if(m_axi_araddr >= RD_DDR_ADDR_END)
      m_axi_araddr <= RD_DDR_ADDR_BEGIN;
    else if((curr_state == S_RD_RESP) && m_axi_rready && m_axi_rvalid && m_axi_rlast && (m_axi_rresp == 2'b00) && (m_axi_rid == AXI_ID))
      m_axi_araddr <= m_axi_araddr + ((m_axi_awlen + 1'b1)<<4);
    else
      m_axi_araddr <= m_axi_araddr;
  end

对于 awvalid ,在进入 WR_ADDR 状态到就将其输出为高等到 awready 和 awvalid 同时高的时候,就将 awvalid 输出为低,保证 awready 和 awvalid 信号只 有一个时钟周期的同时高。

  always@(posedge ui_clk or posedge ui_clk_sync_rst)
  begin
    if(ui_clk_sync_rst)
      m_axi_arvalid <= 1'b0;
    else if((curr_state == S_RD_ADDR) && m_axi_arready && m_axi_arvalid)
      m_axi_arvalid <= 1'b0;
    else if(curr_state == S_RD_ADDR)
      m_axi_arvalid <= 1'b1;
    else
      m_axi_arvalid <= m_axi_arvalid;
  end

在写操作的写数据通道比较关键的是产生 wvalid 和 wlast 信号。wvalid 在进入到 WR_DATA 状态就变为高电平,在发送完最后一个数据后变为低电平。(期间,在主机给出 的 wvalid 与设备给出的 wready 信号同时为高的情况下,数据写入到设备)

  always@(posedge ui_clk or posedge ui_clk_sync_rst)
  begin
    if(ui_clk_sync_rst)
      m_axi_wvalid <= 1'b0;
    else if((curr_state == S_WR_DATA) && m_axi_wready && m_axi_wvalid && m_axi_wlast)
      m_axi_wvalid <= 1'b0;
    else if(curr_state == S_WR_DATA)
      m_axi_wvalid <= 1'b1;
    else
      m_axi_wvalid <= m_axi_wvalid;
  end

对于wlast信号是主机向设备传输最后一个数据的标识信号,这个信号的产生依赖于一次突发写入数据个数和当前已经传输了几个数据,主机在传输最后一个数据同时将其输出为高,在发送完最后一个数据后立马将其输出为低。

这个过程首先需要对传输数据个数进行计数, 当 wready 和 m_axi_wvalid 同时为高时代表传输一个数据,传输数据个数计数器代码如下。

在产生 wlast 时,分两种情况:
一、是当突发写数据个数为 1,也就是 wlen 等于 0 时,那 么传输的第一个数就是传输的最后一个数据,这种情况下,一进入到 WR_DATA 状态就将 wlast 变为高电平;
二、当突发写数据个数大于 1,也就是 wlen 等于等于 1 时,就在传输完 倒数第二个数(即 wr_data_cnt 为 m_axi_awlen -1’b1)后将 wlast 变为高电平。当最后一个 数据传输完成(m_axi_wready、m_axi_wvalid 和 m_axi_wlast 同时为高电平)后将 wlast 变 为低,(比如突发长度为4,那么判断为3-1=2)具体代码如下。

  //wr_data_cnt
  always@(posedge ui_clk or posedge ui_clk_sync_rst)
  begin
    if(ui_clk_sync_rst)
      wr_data_cnt <= 1'b0;
    else if(curr_state == S_ARB)
      wr_data_cnt <= 1'b0;
    else if(curr_state == S_WR_DATA && m_axi_wready && m_axi_wvalid)
      wr_data_cnt <= wr_data_cnt + 1'b1;
    else
      wr_data_cnt <= wr_data_cnt;
  end

  //m_axi_wlast
  always@(posedge ui_clk or posedge ui_clk_sync_rst)
  begin
    if(ui_clk_sync_rst)
      m_axi_wlast <= 1'b0;
    else if(curr_state == S_WR_DATA && m_axi_wready && m_axi_wvalid && m_axi_wlast)
      m_axi_wlast <= 1'b0;
    else if(curr_state == S_WR_DATA && m_axi_awlen == 8'd0)
      m_axi_wlast <= 1'b1;
    else if(curr_state == S_WR_DATA && m_axi_wready && m_axi_wvalid && (wr_data_cnt == m_axi_awlen -1'b1))
      m_axi_wlast <= 1'b1;
    else
      m_axi_wlast <= m_axi_wlast;
  end

控制状态机条状的读写 DDR 请求信号是根据当前 FIFO 中数据量进行判断产生,当写 FIFO 中的数据量超过一个阈值(这里阈值使用 WLEN,)就产生写 DDR 请求;当读 FIFO 中的数据量低于一个阈值(这里阈值使用 RLEN,也可以设置为其他值)就产生读 DDR 请 求,信号产生需要满足在 DDR 初始化完成之后并且当前 FIFO 不处于复位。

fifo2mip_axi模块仿真

仿真1如下图所示
可以看到当初始化校准完成后(init_calib_complete变高)DDR3向rdfifo写入了一些数据(错误的),读数据之前先对 rd_ddr3_fifo 进行一次复位清空缓存的操作(rd_clr),同时对把 DDR 的地址也复位到起始地址。
在这里插入图片描述
仿真2是DDR3的AXI突发写事务:
往wrfifo里写1024个数据(16bit),AXI总线上发生了4次突发写事务,每次突发写是32个数据(位宽128bit,32位是设置的)。
可以看到一次AXI突发写的流程为:

  1. 在地址通道写入接下来要写入数据的起始地址和写突发长度等信息,在 awready和 awvalid 同时为高时,这些信息被传输给 DDR 控制器;
  2. 在写数据通道写入指定突发长度的数据,当 wready 和 wvalid 同时为高时表示写入数据被传输给 DDR 控制器,在写最后一个数据时,wlast 变为高,写完最后一个 wlast 变为低;
  3. 在写响应通道等待设备的写响应,当 bready 和 bvalid 同时为高时表示响应的到来。

在这里插入图片描述
仿真3:AXI突发读事务的时序

  • 4
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kelinnn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值