AXI 总线入门(一)通道握手-AXI-Lite

20 篇文章 9 订阅
19 篇文章 35 订阅

AXI 总线(一)通道握手-AXI-Lite

关于本系列教程

​ 需要注意的是,AXI协议是ARM公司的知识产权,此分享仅为交流学习用途。本人也属于边学边总结,如有纰漏也请大家一同指出交流交流。

​ 其中,由于官方文档实在写得太好,这里原理部分主要参考官方文档(甚至有一些是直接翻译)和赛灵思的官方文档。还有一些我觉得把AXI介绍得很不错的博客文章,可见于参考资料。当然也推荐大家去看官方文档,但是文档稍微有点繁杂,而且我相信大部分人学AXI总线是为了应用,详细点就是把自己写的模块接入到AXI总线中。所以本教程有以下特点:

  • 会着重于总线中的正常读写过程和一些影响读写的关键信号和时序,而不会太注意一些AXI发展中的其他特性(也会介绍,但是更多可能会给出在文档的什么章节)。
  • 注重代码Coding,在介绍协议的时候,会配合讲Xilinx给的示例IP,如本文中讲解了AXI-Lite协议的代码。在具体理解的时候,也会单独做一些实际的小项目和仿真。

​ 废话有点多了,下面直接开始。

什么是AXI协议

本节主要参考自赛灵思官方博客:AXI Basics 1 - Introduction to AXI,可见于参考资料

AXI指的是( Advanced eXtensible Interface) 是ARM提出的AMBA(Advanced Micro_controller Bus Architecture)协议中的一部分。

xilinx家的介绍图

其中又分三项:

  • AXI4 (Full AXI4): 用于高性能内存映射需求。可以理解为满血版AXI
  • AXI4-Lite:用于简单、低吞吐量的内存映射通信(例如,与控制寄存器和状态寄存器之间的通信)。可以理解为青春版AXI。
  • AXI4-Stream:用于高速流数据(实际上,他并没有内存映射能力)而这往往又是摄像头等高速数据流的输入格式,所以后面会单独讲这个协议。

AXI读写通道

AXI协议中,有5条读写通道:

  • 2条有关读的通道
    • read address 读地址
    • read data 读数据(与读响应合并)
  • 3 条有关写的通道
    • write address 写地址
    • write data 写数据
    • write response 写响应

channel

​ 之所以把他们称为“通道”,是因为在每一个通道处都会有一组独立的validready信号,只有在时钟上升沿时,且这两个信号都为高的时候才会进行信息的传输。

​ 那么,现在可以带着**为什么读响应和读数据能合并,写通道不能?这valid和ready信号究竟要怎么握手?**这两个问题进行下面的介绍:

AXI读过程

在AXI中发起一次读需要在2个读通道上进行传输:

image-20210410024432140
  1. 首先,从读地址(AR)通道,由主机告诉从机从什么地址开始读,读多少个数据(burst)
  2. 这个地址的数据在读数据通道上从从机传输到主机

AXI写过程

在AXI中发起一次写需要在3个写通道上进行传输:

image-20210410024726410
  1. 首先,从写地址(AW)通道,由主机告诉从机从什么地址开始写,写多少个数据(burst)
  2. 这个地址的数据在写数据通道上从主机传输到从机
  3. 最后,传输结束时在写响应通道上的发送写响应,从从机发送到主机,以指示传输是否成功

​ 所以在这里我们就可以看到,在AXI读过程中,由于读数据的方向是从机到主机,所以主机一方面可以通过读的数量来看传输是否成功,另一方面,由方向上,读响应也可以随着数据发给主机。但是写通道的方向是主机到从机,所以必须多一条写响应通道。

单一通道的握手

​ 如无特殊说明,valid指发送端,ready指接收端。

​ 在通道中,需要valid和ready同时拉高才能传输信息,总体原则是谁没到就等谁。所以由valid和ready置高的先后顺序,有三种情况:

VALID 信号先到
image-20210410030031971

​ 此时,valid信号已经有效,所以信息线上的数据也是有效的了,但是接收端说我有点别的事要干,你先等会,即T2时刻才刚拉高,所以信息传输就发生在T2-T3,在T3时刻,发送端发现接收端已经把数据读走了便把valid拉低了。

​ 但实际上,这个图只画了burst_length=1的时候,实际上如果burst_length不是1的话,这里两个标志线还会继续维持下去知道数据传输结束,这也是下面AXI-Lite代码中所没能讲到的。

READY信号先到
image-20210410031014815

​ 此时,接收端说我已经准备好了,你赶紧来。等发送端缓过气来,即T2时刻才刚拉高,所以信息传输就发生在T2-T3,发送端感知到接收端已经把数据读走了便把valid拉低了。

​ 这里也需要注意,这个图也只画了burst_length=1的时候。

READY和VALID同时到达
image-20210410031407942

​ 同时到达就很开心了,等到下一个时钟上升沿 T2,传输就完成了。

原则性问题

​ 这里还有一个最后的问题,**到底应该谁来发起一次读写?**毫无疑问,那应该是发送端。

​ 所以在握手的过程中,发送端的valid信号不能依赖接收端的ready信号,不然有可能两边都不拉高,直接死锁

​ 而且,一旦valid被拉高了,直到完成握手只能一直置位,不能左右横跳,即接收端不需要考虑发送方撤销传输的可能。

​ 但是,ready信号的状态可以依赖于valid信号,协议没有规定 READY 信号默认状态,即未进行传输时的电平状态。

除了上面这些比较关键的之外,还有几个比较重要的小细节:

  1. 在写入中,写入响应必须始终紧跟它所属的最后一个写入传输
  2. 读取的数据必须始终跟在数据所关联的地址之后
  3. slave必须等待ARVALID和ARREADY都被断言,然后才拉高RVALID,以表明有效的数据可用
  4. 协议建议 AW/AR READY 信号(这里 AW/AR 指的是读写地址通道的 READY 信号)的默认电平为高电平。

AXI-Lite总线实现解析

​ 下面,我们会解析xilinx官方的AXI-Lite实现,从代码的角度解析每一通道的握手过程。

生成一个AXI(-Lite)外设

​ 在Vivado中,可以通过生成ip的方式来建立AXI协议的外设,具体操作如下:

  1. Vivado中,点Tools-> Create and Package New IP:

    image-20210412233606641
  2. 简介面Next掉,然后选择创建AXI4 外设,Next:

    image-20210412233828401
  3. 进入外设细节面,填好信息,Next:

    image-20210412234112718
  4. 进入到添加接口界面,默认就是AXI-Lite了,可以什么都不改,直接下一步:

    image-20210412234339532
  5. 再最后一步中,直接选择Edit IP,进入修改界面:

    image-20210412234433236
    1. 如果以后再想修改的话,可以通过IP Catalog,找到创建的IP,右键选择Edit IP就可以了.

      image-20210412234614793

整体端口信号解析

​ 进入编辑工程后,找到xxxx_S00_AXI.v找到接口实现模块,就是接下来的重点了。

​ 首先,我们先梳理一下模块中的输入输出信号,为了更好的识别出各个通道,我这里已经帮大家用不同的颜色标注好了:

image-20210410033227435

可见,在每一条通道中基本会有主要信息辅助信息?,和两个握手信号。其中,辅助信息这个概念是我瞎掰的,主要为了后面好讲每一个通道的独特之处。下面,先从写过程开始介绍:

A(ddr)W(rite)通道

端口说明如下:

端口名I/O说明
S_AXI_AWADDRinputWrite address (issued by master, accepted by Slave)
S_AXI_AWPROTinputWrite channel Protection type. This signal indicates the privilege and security level of the transaction, and whether the transaction is a data access or an instruction access.
S_AXI_AWVALIDinputWrite address valid. This signal indicates that the master signaling valid write address and control information.
S_AXI_AWREADYoutputWrite address ready. This signal indicates that the slave is ready to accept an address and associated control signals.
握手S_AXI_AWREADY,S_AXI_AWVALID

握手信号,在这里我们可以参考官方文档的握手流程(取了AXI 3的,对AW通道一致):

image-20210413000530188

先扫一眼代码,注意en和ready的关系:

// I/O Connections assignments
assign S_AXI_AWREADY	= axi_awready;

always @( posedge S_AXI_ACLK )
begin
 if ( S_AXI_ARESETN == 1'b0 )
    begin
      axi_awready <= 1'b0;		//  协议ready
      aw_en <= 1'b1;			//  模块ready
    end 
  else
    begin    
      if (~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en)
        begin
          // slave is ready to accept write address when 
          // there is a valid write address and write data
          // on the write address and data bus. This design 
          // expects no outstanding transactions. 
          axi_awready <= 1'b1;
          aw_en <= 1'b0;
        end
        else if (S_AXI_BREADY && axi_bvalid)	// 写回应结束,模块内恢复ready
            begin
              axi_awready <= 1'b0;
              aw_en <= 1'b1;
            end
      else           
        begin
          axi_awready <= 1'b0;
        end
    end 
end      

可见代码基本符合流程图。可以看到en和ready的关系如下,后面会有大量类似的写法:

  • en=1,axi_awready=0,可以通信/结束通信,等待valid
  • en=0,axi_awready=1,建立通信,拉高ready
地址锁存 AW_addr

注意,这里做的是latch:

而且,注意对比判断逻辑块:

  • ~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en

和上面的ready是同时判断的,所以在ready给出的同时,地址已经锁存好了.

// Implement axi_awaddr latching
// This process is used to latch the address when both 
// S_AXI_AWVALID and S_AXI_WVALID are valid. 

always @( posedge S_AXI_ACLK )
begin
	if ( S_AXI_ARESETN == 1'b0 )
	begin
		axi_awaddr <= 0;
	end 
	else
	begin    
		if (~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en)
		begin
			// Write Address latching 
			axi_awaddr <= S_AXI_AWADDR;
		end
	end 
end  
S_AXI_AWPROT

辅助信息?稍微检索可以得知,在xilinx中并没有实现这一项,翻看ARM官方文档有:

AXI provides access permissions signals that can be used to protect against illegal transactions:

  • ARPROT[2:0] defines the access permissions for read accesses.
  • AWPROT[2:0] defines the access permissions for write accesses.

The term AxPROT refers collectively to the ARPROT and AWPROT signals.

即这个信号是用来做权限管理的,具体定义如下:

image-20210413001400306

由于这是入门篇,这里就先挖坑不讲了,有兴趣的小伙伴可以看看手册的A4.7节。这里还需要注意到,读地址通道的辅助信息也是这个噢。

W(rite)写通道

端口如下(为了篇幅不过长,这里就省了描述了):

  • 主要信号S_AXI_WDATA
  • 辅助信号S_AXI_WSTRB,做掩码
  • 握手信号S_AXI_WVALID,S_AXI_WREADY
S_AXI_WVALID,S_AXI_WREADY握手信号

同样地,我们看官方文档的握手流程(取了AXI 3的,对W通道一致):

image-20210413002815462
// I/O Connections assignments
assign S_AXI_WREADY	= axi_wready;

always @( posedge S_AXI_ACLK )
begin
	if ( S_AXI_ARESETN == 1'b0 )
	begin
		axi_wready <= 1'b0;
	end 
	else
	begin    
		if (~axi_wready && S_AXI_WVALID && S_AXI_AWVALID && aw_en )
		begin
			// slave is ready to accept write data when 
			// there is a valid write address and write data
			// on the write address and data bus. This design 
			// expects no outstanding transactions. 
			axi_wready <= 1'b1;
		end
		else
		begin
			axi_wready <= 1'b0;
		end
	end 
end       

可见,其中也运用了上面AW通道中类似ready和en的写法。

S_AXI_WDATA数据写入

这里可以先看简单版(无S_AXI_WSTRB),比较容易懂,完整代码在后面:

assign slv_reg_wren = axi_wready && S_AXI_WVALID && axi_awready && S_AXI_AWVALID;

always @( posedge S_AXI_ACLK )
begin
	if ( S_AXI_ARESETN == 1'b0 )
	begin
		slv_reg0 <= 0;
		slv_reg1 <= 0;
		slv_reg2 <= 0;
		slv_reg3 <= 0;
	end 
	else begin
	if (slv_reg_wren)
		begin
            case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
                2'h0:
                   slv_reg0 <= S_AXI_WDATA;
                2'h1:
                   slv_reg1 <= S_AXI_WDATA; 
                2'h2:
                   slv_reg2 <= S_AXI_WDATA;
                2'h3:
                   slv_reg3 <= S_AXI_WDATA;
                default : begin
                    slv_reg0 <= slv_reg0;
                    slv_reg1 <= slv_reg1;
                    slv_reg2 <= slv_reg2;
                    slv_reg3 <= slv_reg3;
                end
            endcase
		end
	end
end    
AXI地址解析

可以看到,这里基本就是在通道建立后,做一个选择器,那问题来了,为什么是这两位呢?:

axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB]

首先,在AXI总线中,一个地址指一个字节

回顾代码,关于地址部分:

// Example-specific design signals
// local parameter for addressing 32 bit / 64 bit C_S_AXI_DATA_WIDTH
// ADDR_LSB is used for addressing 32/64 bit registers/memories
// ADDR_LSB = 2 for 32 bits (n downto 2)
// ADDR_LSB = 3 for 64 bits (n downto 3)
localparam integer ADDR_LSB = (C_S_AXI_DATA_WIDTH/32) + 1;
localparam integer OPT_MEM_ADDR_BITS = 1;

所以针对ADDR_LSB,可以看到:

  • ADDR_LSB = 0,每次+1,地址+1,跨1字节8位
  • ADDR_LSB = 1,每次+1,地址+2,跨1字节16位
  • ADDR_LSB = 2,每次+1,地址+4,跨1字节32位
  • ADDR_LSB = 3,每次+1,地址+8,跨1字节64位

而对应OPT_MEM_ADDR_BITS,因为我们只有4个32位的寄存器,只要两条地址线,所以OPT_MEM_ADDR_BITS=1。

而写入过程完整代码如下(不用看,下面有S_AXI_WSTRB解析):

assign slv_reg_wren = axi_wready && S_AXI_WVALID && axi_awready && S_AXI_AWVALID;

always @( posedge S_AXI_ACLK )
begin
	if ( S_AXI_ARESETN == 1'b0 )
	begin
		slv_reg0 <= 0;
		slv_reg1 <= 0;
		slv_reg2 <= 0;
		slv_reg3 <= 0;
	end 
	else begin
	if (slv_reg_wren)
		begin
		case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
			2'h0:
			for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
				if ( S_AXI_WSTRB[byte_index] == 1 ) begin
				// Respective byte enables are asserted as per write strobes 
				// Slave register 0
				slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
				end  
			2'h1:
			for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
				if ( S_AXI_WSTRB[byte_index] == 1 ) begin
				// Respective byte enables are asserted as per write strobes 
				// Slave register 1
				slv_reg1[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
				end  
			2'h2:
			for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
				if ( S_AXI_WSTRB[byte_index] == 1 ) begin
				// Respective byte enables are asserted as per write strobes 
				// Slave register 2
				slv_reg2[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
				end  
			2'h3:
			for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
				if ( S_AXI_WSTRB[byte_index] == 1 ) begin
				// Respective byte enables are asserted as per write strobes 
				// Slave register 3
				slv_reg3[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
				end  
			default : begin
						slv_reg0 <= slv_reg0;
						slv_reg1 <= slv_reg1;
						slv_reg2 <= slv_reg2;
						slv_reg3 <= slv_reg3;
					end
		endcase
		end
	end
end    
S_AXI_WSTRB

我们上面说到,在AXI总线中,一个地址指一个字节,但是如果并不是32位都是有效的怎么办呢?

如果我寄存器只有8位或者16位,但是AXI-Lite只能是32位或者64位,那咋办呢?

所以在写通道中就有一个类似 掩码的功能,用来指示什么数据是有效的,具体解析见下:

实际的代码逻辑其实已经在上面了,以slv_reg0为例:

for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
	if ( S_AXI_WSTRB[byte_index] == 1 ) begin
	// Respective byte enables are asserted as per write strobes 
	// Slave register 0
	slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
	end  

那么我们代入C_S_AXI_DATA_WIDTH = 32,并且拆开for循环:

// slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
// byte_index = 0
if ( S_AXI_WSTRB[0] == 1 ) begin
    // slv_reg0[(0 +: 8] <= S_AXI_WDATA[(0*8) +: 8];
    slv_reg0[7:0] <= S_AXI_WDATA[7:0];
    end

// byte_index = 1
if ( S_AXI_WSTRB[1] == 1 ) begin
    // slv_reg0[(8 +: 8] <= S_AXI_WDATA[(1*8) +: 8];
    slv_reg0[15:8] <= S_AXI_WDATA[15:8];
    end

// byte_index = 2
if ( S_AXI_WSTRB[2] == 1 ) begin
    // slv_reg0[(16 +: 8] <= S_AXI_WDATA[(2*8) +: 8];
    slv_reg0[23:16] <= S_AXI_WDATA[23:16];
    end

// byte_index = 3
if ( S_AXI_WSTRB[3] == 1 ) begin
    // slv_reg0[(24 +: 8] <= S_AXI_WDATA[(3*8) +: 8];
    slv_reg0[31:24] <= S_AXI_WDATA[31:24];
    end

我觉得已经详细到不需要解释了,关注+:和-:的用法,可以查看本人博客一些Verilog的小东西(2)

写响应通道

端口信号:

Port nameDirectionType
S_AXI_BRESPoutputwire [1 : 0]
S_AXI_BVALIDoutputwire
S_AXI_BREADYinputwire
S_AXI_BVALID,S_AXI_BRESP握手信号及其响应

此时需要注意,AXI3和AXI4对于写响应通道的握手流程是不一致的,这里取最新的AXI4:

image-20210413004903016

代码其实还是换汤不换药:

// I/O Connections assignments
assign S_AXI_BRESP	= axi_bresp;
assign S_AXI_BVALID	= axi_bvalid;

always @( posedge S_AXI_ACLK )
begin
	if ( S_AXI_ARESETN == 1'b0 )
	begin
		axi_bvalid  <= 0;
		axi_bresp   <= 2'b0;
	end 
	else
	begin    
		if (axi_awready && S_AXI_AWVALID && ~axi_bvalid && axi_wready && S_AXI_WVALID)
		begin
			// indicates a valid write response is available
			axi_bvalid <= 1'b1;
			axi_bresp  <= 2'b0; // 'OKAY' response 
		end                   // work error responses in future
		else
		begin
			if (S_AXI_BREADY && axi_bvalid) 
			//check if bready is asserted while bvalid is high) 
			//(there is a possibility that bready is always asserted high)   
			begin
				axi_bvalid <= 1'b0; 
			end  
		end
	end
end   

我们可以来看看,响应信号的具体含义:

image-20210409232144661

这里由于我们简单介绍一下含义就算了,具体为什么会有这个机制可以看看手册A3.4.4和A7.2.这篇中我们主要介绍通道的握手通信,这里就不展开了。

那么接下来,就到读过程

A(ddr)R(ead)读地址通道

握手S_AXI_ARREADY,地址写入S_AXI_ARADDR

读地址通道,到这里了,大家应该看到代码基本都是类似的,见于文档:

image-20210413005845518
// I/O Connections assignments
assign S_AXI_ARREADY	= axi_arready;

always @( posedge S_AXI_ACLK )
begin
	if ( S_AXI_ARESETN == 1'b0 )
	begin
		axi_arready <= 1'b0;
		axi_araddr  <= 32'b0;
	end 
	else
	begin    
		if (~axi_arready && S_AXI_ARVALID)
		begin
			// indicates that the slave has acceped the valid read address
			axi_arready <= 1'b1;
			// Read address latching
			axi_araddr  <= S_AXI_ARADDR;
		end
		else
		begin
			axi_arready <= 1'b0;
		end
	end 
end
辅助信息 S_AXI_ARPROT

​ 见于S_AXI_AWPROT,一致的,这里也不需要给。

读R(ead)通道&&读响应通道

这里要注意到,读通道和读响应通道是合并了的,所以读通道的辅助信息理所当然就是响应信号啦。

握手及响应信号S_AXI_RVALID,S_AXI_RRESP

握手过程见官方文档:

image-20210413005959820
// I/O Connections assignments
assign S_AXI_RRESP	= axi_rresp;
assign S_AXI_RVALID	= axi_rvalid;

always @( posedge S_AXI_ACLK )
begin
	if ( S_AXI_ARESETN == 1'b0 )
	begin
		axi_rvalid <= 0;
		axi_rresp  <= 0;
	end 
	else
	begin    
		if (axi_arready && S_AXI_ARVALID && ~axi_rvalid)
		begin
			// Valid read data is available at the read data bus
			axi_rvalid <= 1'b1;
			axi_rresp  <= 2'b0; // 'OKAY' response
		end   
		else if (axi_rvalid && S_AXI_RREADY)
		begin
			// Read data is accepted by the master
			axi_rvalid <= 1'b0;
		end                
	end
end    

基本上都是一致的流程了,注意S_AXI_RRESP响应与写响应通道中的S_AXI_BRESP含义是一致的。

S_AXI_RDATA数据交互

这里也是基本的数据选择器,根据地址给数据就完事了。

// I/O Connections assignments
assign S_AXI_RDATA	= axi_rdata;
// Implement memory mapped register select and read logic generation
// Slave register read enable is asserted when valid address is available
// and the slave is ready to accept the read address.
assign slv_reg_rden = axi_arready & S_AXI_ARVALID & ~axi_rvalid;
always @(*)
begin
    // Address decoding for reading registers
    case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
        2'h0   : reg_data_out <= slv_reg0;
        2'h1   : reg_data_out <= slv_reg1;
        2'h2   : reg_data_out <= slv_reg2;
        2'h3   : reg_data_out <= slv_reg3;
        default : reg_data_out <= 0;
    endcase
end

// Output register or memory read data
always @( posedge S_AXI_ACLK )
begin
	if ( S_AXI_ARESETN == 1'b0 )
	begin
		axi_rdata  <= 0;
	end 
	else
	begin    
		// When there is a valid read address (S_AXI_ARVALID) with 
		// acceptance of read address by the slave (axi_arready), 
		// output the read dada 
		if (slv_reg_rden)
		begin
			axi_rdata <= reg_data_out;     // register read data
		end
	end
end

这里的地址解析和上文中的AXI地址解析也是一致的。

小结

主要介绍了一下几点:

  • AXI大概是啥,通道大致有啥
  • AXI各个通道中的握手过程
  • AXI-Lite的接口代码分析

下一节中,我们会仿真,以及做一些结合ZYNQ-PS端的小应用,看看怎么仿和怎么用。后面介绍完完整AXI后再讲怎么接入。

参考资料

深入 AXI4 总线(O)专栏目录与资料集合–知乎ligibbs

赛灵思家的AXI-Base博客系列

ARM家AXI官方文档

一些Verilog的小东西(2),+:和-:的用法

如果你觉得有一点收获的话

也欢迎关注我的个人公众号,小何的芯像石头:

recommend

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小何的芯像石头

谢谢你嘞,建议用用我的链接

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

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

打赏作者

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

抵扣说明:

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

余额充值