Petalinux 下使用 UIO 实现 AXI GPIO & AXI Stream FIFO 驱动

Petalinux 下使用 UIO 实现 AXI GPIO & AXI Stream FIFO 驱动

前言

瑟如电子TDC的很多用户在standalone环境下使用TDC,但近来也有客户在问,能否在linux下使用。在绝大多数的中低速应用下,一般都推荐用户使用axi stream fifo IP 来获取TDC IP产生的时间戳数据。当然也可以使用AXI DMA,虽然有官方的linux驱动,但是AXI DMA占用面积较大,资源较多,中低速环境下太浪费了。对于Linux驱动,我也才是菜鸟级别,照着示例写过一点字符型驱动。不过一套流程走下来,略显繁琐,而且不便于日后维护。一番比较研究之后,发现使用UIO的框架来实现更为简洁,如果不用中断功能的话,几乎不用在kernel里写程序。因此定下方向:使用UIO实现axi stream fifo 的数据读取。

本文主要的两个参考来源如下:

  1. 何晔: 当ZYNQ遇到Linux Userspace I/O

  2. Vacajk: ZYNQ中的UIO驱动和中断程序学习【Xilinx-Petalinux学习】

Vivado 工程

功能定义

  1. 使用UIO驱动通过AXI GPIO控制PL端的LED。

  2. 使用UIO驱动读取AXI Stream FIFO(AXI-Lite)的缓存内容

  3. 此外为了便于调试还需要Zynq具有以下端口:Ethernet, SD,
    UART。其中,Ethernet用于NFS,便于程序的远程运行与调试。

创建Vivado工程

工程基于黑金的AX7020开发板,FPGA型号设置如下图:

在这里插入图片描述

首先添加Zynq PS, 设置DDR、PS/PL MIO,
并使能PL到PS的中断端口。本示例外设的MIO设置如下图:

在这里插入图片描述

第二步,添加AXI GPIO IP,用于控制PL侧的LED。GPIO
IP位宽设为8位,低4位连接LED,高4位连接按键。并勾选Enable
Interrupt,添加中断输出端口。本示例只演示控制GPIO低4位的输出,以控制LED。按键输入及中断功能,如有兴趣可自行测试。

第三步,添加AXI Stream FIFO, 配置如下:

在这里插入图片描述

因为只用到接收功能,所以没有配置数据发送端口。数据接口格式设置为AXI4-Lite。

第四步,添加自定义IP,产生AXI Stream 数据,输出到AXI Stream
FIFO。在Vivado中点击Tools->Create and package New IP,选择创建AXI4外设:

在这里插入图片描述

为自定义IP选取数据端口:AXI Stream
Master。创建完成后,IP中已经包含了示例。该示例的功能为系统初始化后,等待一段时间然后对外发送32字节,也就是8个Word的数据,数据内容为0x01-0x08。可初步满足测试需求。

第五步,连接中断,并自动连接AXI端口。AXI GPIO 以及AXI Stream
FIFO的中断输出通过concate IP 连接到PS的中断接口。

最后,编写XDC文件,将GPIO的端口定义到对应的Led及Key上,并进行综合和实现,最终生成Bitstream。通过File->Export->
Export
Hardware,将硬件定义导出到指定的目录。记住这个目录,之后配置Petalinux的时候要用到。下图是Vivado
工程的Block Diagram。

在这里插入图片描述

Petalinux 配置

在这一节中,我们首先基于之前生成的Vivado工程,配置petalinux,使能UIO platform driver, 修改device tree,将AXI GPIO及AXI Stream FIFO的驱动改为generic-uio,最后生成用于从SD启动的镜像文件。

第一步,创建工程以及配置硬件描述文件:

source /opt/petalinux/settings.sh
petalinux-create -t project –-template zynq -n uio_test
cd uio_test
petalinux-config --get-hw-description ../xxxxxxx/

(…/xxxx是之前导出hardware的目录相对路径)

这一步没什么要修改的,保存,退出就可以。

第二步,配置内核:

petalinux-config -c kernel

Device Drivers —>Userspace I/O drivers —>

在这里插入图片描述

如上图勾选两个选项。

第三步,编译设备树:

petalinux-build -c device-tree

编译完成后,到petalinux 工程目录下.:

/components/plnx_workspace/device-tree-generation/pl.dtsi中可以看到系统自动识别出的PL侧的设备树信息。可以看到识别出来两个设备:分别为axi_fifo_mm_s_0 以及 axi_gpio_0。

/ {

amba_pl: amba_pl {

\#address-cells = \<1\>;

\#size-cells = \<1\>;

compatible = "simple-bus";

ranges ;

axi_fifo_mm_s_0: axi_fifo_mm_s\@43c00000 {

compatible = "xlnx,axi-fifo-mm-s-4.1";

interrupt-parent = \<\&intc\>;

interrupts = \<0 30 4\>;

reg = \<0x43c00000 0x10000\>;

xlnx,axi-str-rxd-protocol = "XIL_AXI_STREAM_ETH_DATA";

xlnx,axi-str-rxd-tdata-width = \<0x20\>;

xlnx,axi-str-txc-protocol = "XIL_AXI_STREAM_ETH_CTRL";

xlnx,axi-str-txc-tdata-width = \<0x20\>;

xlnx,axi-str-txd-protocol = "XIL_AXI_STREAM_ETH_DATA";

xlnx,axi-str-txd-tdata-width = \<0x20\>;

xlnx,axis-tdest-width = \<0x4\>;

xlnx,axis-tid-width = \<0x4\>;

xlnx,axis-tuser-width = \<0x4\>;

xlnx,data-interface-type = \<0x0\>;

xlnx,has-axis-tdest = \<0x0\>;

xlnx,has-axis-tid = \<0x0\>;

xlnx,has-axis-tkeep = \<0x0\>;

xlnx,has-axis-tstrb = \<0x0\>;

xlnx,has-axis-tuser = \<0x0\>;

xlnx,rx-fifo-depth = \<0x200\>;

xlnx,rx-fifo-pe-threshold = \<0x2\>;

xlnx,rx-fifo-pf-threshold = \<0x1fb\>;

xlnx,s-axi-id-width = \<0x4\>;

xlnx,s-axi4-data-width = \<0x20\>;

xlnx,select-xpm = \<0x0\>;

xlnx,tx-fifo-depth = \<0x200\>;

xlnx,tx-fifo-pe-threshold = \<0x2\>;

xlnx,tx-fifo-pf-threshold = \<0x1fb\>;

xlnx,use-rx-cut-through = \<0x0\>;

xlnx,use-rx-data = \<0x1\>;

xlnx,use-tx-ctrl = \<0x0\>;

xlnx,use-tx-cut-through = \<0x0\>;

xlnx,use-tx-data = \<0x0\>;

};

axi_gpio_0: gpio\@41200000 {

\#gpio-cells = \<2\>;

\#interrupt-cells = \<2\>;

compatible = "xlnx,xps-gpio-1.00.a";

gpio-controller ;

interrupt-controller ;

interrupt-parent = \<\&intc\>;

interrupts = \<0 29 4\>;

reg = \<0x41200000 0x10000\>;

xlnx,all-inputs = \<0x0\>;

xlnx,all-inputs-2 = \<0x0\>;

xlnx,all-outputs = \<0x0\>;

xlnx,all-outputs-2 = \<0x0\>;

xlnx,dout-default = \<0x00000000\>;

xlnx,dout-default-2 = \<0x00000000\>;

xlnx,gpio-width = \<0x8\>;

xlnx,gpio2-width = \<0x20\>;

xlnx,interrupt-present = \<0x1\>;

xlnx,is-dual = \<0x0\>;

xlnx,tri-default = \<0xFFFFFFFF\>;

xlnx,tri-default-2 = \<0xFFFFFFFF\>;

};

};

};

打开文件./project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi,
将pl.dtsi中的内容复制进去,并将两个设备的compatible都改为compatible =
“generic-uio”;并在两个设备后添加:

chosen {
	bootargs = "console=ttyPS0,115200 earlyprintk uio_pdrv_genirq.of_id=generic-uio";
	stdout-path = "serial0:115200n8";

};

由于本示例没有用到中断,所以设备树里也没有添加中断的描述,如果需要用到,请参考参考资源(1).

第四步就可以编译petalinux工程了。

petalinux-build

编译完成后,生成镜像文件:

petalinux-package --boot --fsbl=./images/linux/zynq_fsbl.elf --fpga --u-boot --force

之后将petalinux工程目录下/images/linux/中的boot.bin和image.ub拷贝到SD卡上,插入AX7020开发板,设置好从SD卡启动的跳线。

为了实现NFS,将开发板接入与上位主机位于同一网段的局域网,启动板子。

来到登录界面后,输入用户名root, 密码root,进入系统。

UIO GPIO 测试

首先查看UIO设备

ls /dev/uio*:

在这里插入图片描述

根据设备树中的顺序,uio0 为axi stream fifo, uio1 为axi gpio。

编写GPIO测试程序

从vacajk的博文中复制过来的gpio-uio-test.c代码:

/*
 * This application reads/writes GPIO devices with UIO.
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>

#define IN 0
#define OUT 1

#define GPIO_MAP_SIZE 0x10000

#define GPIO_DATA_OFFSET 0x00
#define GPIO_TRI_OFFSET 0x04
#define GPIO2_DATA_OFFSET 0x08
#define GPIO2_TRI_OFFSET 0x0C

#define GIER 0x011C
#define IP_IER 0x0128
#define IP_ISR 0x0120

void usage(void)
{
    printf("*argv[0] -d <UIO_DEV_FILE> -i|-o <VALUE>\n");
    printf("    -d               UIO device file. e.g. /dev/uio0");
    printf("    -i               Input from GPIO\n");
    printf("    -o <VALUE>       Output to GPIO\n");
    return;
}

int main(int argc, char *argv[])
{
    int c;
    int fd;
    int direction=IN;
    char *uiod;
    int value = 0;
    int valued = 0;
    int irq_on = 1;

    void *ptr;

    printf("GPIO UIO test.\n");
    while((c = getopt(argc, argv, "d:io:h")) != -1) {
        switch(c) {
        case 'd':
            uiod=optarg;
            break;
        case 'i':
            direction=IN;
            break;
        case 'o':
            direction=OUT;
            valued=atoi(optarg);
            break;
        case 'h':
            usage();
            return 0;
        default:
            printf("invalid option: %c\n", (char)c);
            usage();
            return -1;
        }

    }

    /* Open the UIO device file */
    fd = open(uiod, O_RDWR);
    if (fd < 1) {
        perror(argv[0]);
        printf("Invalid UIO device file:%s.\n", uiod);
        usage();
        return -1;
    }

    /* mmap the UIO device */
    ptr = mmap(NULL, GPIO_MAP_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

    /* Print Interrupt Registers */
    value = *((unsigned *) (ptr + GIER));
    printf("%s: GIER: %08x\n",argv[0], value);
    value = *((unsigned *) (ptr + IP_IER));
    printf("%s: IP_IER: %08x\n",argv[0], value);
    value = *((unsigned *) (ptr + IP_ISR));
    printf("%s: IP_ISR: %08x\n",argv[0], value);

    /* Enable All Interrupts */
    printf("%s: Enable All Interrupts in Regs\n", argv[0]);
    *((unsigned *)(ptr + GIER)) = 0x80000000;
    *((unsigned *)(ptr + IP_IER)) = 0x3;
    *((unsigned *)(ptr + IP_ISR)) = 0x3;

    /* Enable UIO interrupt */
    write(fd, &irq_on, sizeof(irq_on));

    if (direction == IN) {
    /* Read from GPIO */
        *((unsigned *)(ptr + GPIO_TRI_OFFSET)) = 255;
        value = *((unsigned *) (ptr + GPIO_DATA_OFFSET));
        printf("%s: input: %08x\n",argv[0], value);
    } else {
    /* Write to GPIO */
        *((unsigned *)(ptr + GPIO_TRI_OFFSET)) = 0;
        value = valued;
        *((unsigned *)(ptr + GPIO_DATA_OFFSET)) = value;
    }

    /* Print Interrupt Registers */
    value = *((unsigned *) (ptr + GIER));
    printf("%s: GIER: %08x\n",argv[0], value);
    value = *((unsigned *) (ptr + IP_IER));
    printf("%s: IP_IER: %08x\n",argv[0], value);
    value = *((unsigned *) (ptr + IP_ISR));
    printf("%s: IP_ISR: %08x\n",argv[0], value);

    munmap(ptr, GPIO_MAP_SIZE);

    return 0;
}

在编译之前,我们先简单分析一下源代码:

首先,对输入的参数进行解析:
参数d
定义了UIO设备的文件路径,之后所有对UIO设备的操作,都是以这个文件为接口的。在本示例中,/dev/uio1代表了AXI GPIO, /dev/uio0代表了axi stream fifo。

参数i/o
指定了对GPIO是输入还是输出操作。

之后打开设备文件,通过

ptr = mmap(NULL, GPIO_MAP_SIZE, PROT_READ\|PROT_WRITE, MAP_SHARED, fd,0);

将设备寄存器地址映射到用户空间的虚拟地址上。对该虚拟地址进行读写操作,就能直接对设备寄存器进行读写操作。

忽略中断的内容直接看这一段:

 if (direction == IN) {
    /* Read from GPIO */
        *((unsigned *)(ptr + GPIO_TRI_OFFSET)) = 255;
        value = *((unsigned *) (ptr + GPIO_DATA_OFFSET));
        printf("%s: input: %08x\n",argv[0], value);
    } else {
    /* Write to GPIO */
        *((unsigned *)(ptr + GPIO_TRI_OFFSET)) = 0;
        value = valued;
        *((unsigned *)(ptr + GPIO_DATA_OFFSET)) = value;
    }

可见,当对GPIO执行输出操作时,先将对应的三态寄存器写入0,将GPIO设置为输出状态,然后再写入对应的数据寄存器。

接下来在Linux上位机中进行编译:

source /xxxxx/settings.sh 

(/xxxx是petalinux所在的绝对路径)

arm-linux-gnueabihf-gcc uio_test.c -o uio_test2

生成了针对目标器件在linux下可执行文件uio_test2

下面要把uio_test2复制到Linux主机下的NFS目录,并在AX7020上挂载目录,并运行程序。

挂载NFS文件夹:

mount -t nfs -o nolock 192.168.1.12:/xxxx/Work_NFS /mnt

(/xxxx/Work_NFS 是用户自己设置的NFS绝对路径。NFS的设置不在本文范围之内,需要注意的是,如果像我一样采用虚拟机运行Linux host的,需要将虚拟机的网卡设为桥接模式。)

将可执行程序uio_test2 复制到/xxxx /Work_NFS。在串口终端中进入/mnt,如果NFS没有问题,可以看到刚刚复制的uio_test2。

接着我们就要运行程序,控制LED。之前提到AXI GPIO的低四位接到了PL侧的LED。而查看AX7020的电路图可以发现,LED对应的pin为低时点亮,高时熄灭。

先熄灭第一个LED,点亮其余;(从左侧第3个LED起为PL LED)

在串口终端中运行如下命令

uio_test2 -d /dev/uio1 -o 1

在这里插入图片描述

熄灭第2、3个LED,点亮其余:

uio_test2 -d /dev/uio1 -o 6

在这里插入图片描述

AXI Stream FIFO IP UIO 驱动

有了用UIO 驱动GPIO的经验,写读取AXI Stream FIFO的UIO驱动也就更直观了。在用户空间程序中,我们只要打开axi stream fifo对应的设备文件,映射内存,然后操作对应的寄存器即可。非常有参考价值的两份资料:一是SDK自带的axi stream fifo bare-metal 示例;二是IP的说明文档AXI4-Streaem FIFO IP Product Guide (PG080)。SDK的示例可从bsp的system.mss找到。

在这里插入图片描述

点击Import Example:

在这里插入图片描述

闲话少说,直接上代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>

#define GPIO_MAP_SIZE 0x10000

#define XLLF_ISR_OFFSET  0x00000000  /**< Interrupt Status */
#define XLLF_IER_OFFSET  0x00000004  /**< Interrupt Enable */

#define XLLF_TDFR_OFFSET 0x00000008  /**< Transmit Reset */
#define XLLF_TDFV_OFFSET 0x0000000c  /**< Transmit Vacancy */
#define XLLF_TDFD_OFFSET 0x00000010  /**< Transmit Data */
#define XLLF_AXI4_TDFD_OFFSET   0x00000000  /**< Axi4 Transmit Data */
#define XLLF_TLF_OFFSET  0x00000014  /**< Transmit Length */

#define XLLF_RDFR_OFFSET 0x00000018  /**< Receive Reset */
#define XLLF_RDFO_OFFSET 0x0000001c  /**< Receive Occupancy */
#define XLLF_RDFD_OFFSET 0x00000020  /**< Receive Data */
#define XLLF_AXI4_RDFD_OFFSET 	0x00001000  /**< Axi4 Receive Data */
#define XLLF_RLF_OFFSET  0x00000024  /**< Receive Length */
#define XLLF_LLR_OFFSET  0x00000028  /**< Local Link Reset */
#define XLLF_TDR_OFFSET  0x0000002C  /**< Transmit Destination  */
#define XLLF_RDR_OFFSET  0x00000030  /**< Receive Destination  */

#define XLLF_INT_RPURE_MASK       0x80000000 /**< Receive under-read */
#define XLLF_INT_RPORE_MASK       0x40000000 /**< Receive over-read */
#define XLLF_INT_RPUE_MASK        0x20000000 /**< Receive underrun (empty) */
#define XLLF_INT_TPOE_MASK        0x10000000 /**< Transmit overrun */
#define XLLF_INT_TC_MASK          0x08000000 /**< Transmit complete */
#define XLLF_INT_RC_MASK          0x04000000 /**< Receive complete */
#define XLLF_INT_TSE_MASK         0x02000000 /**< Transmit length mismatch */
#define XLLF_INT_TRC_MASK         0x01000000 /**< Transmit reset complete */
#define XLLF_INT_RRC_MASK         0x00800000 /**< Receive reset complete */
#define XLLF_INT_TFPF_MASK        0x00400000 /**< Tx FIFO Programmable Full,
						* AXI FIFO MM2S Only */
#define XLLF_INT_TFPE_MASK        0x00200000 /**< Tx FIFO Programmable Empty
						* AXI FIFO MM2S Only */
#define XLLF_INT_RFPF_MASK        0x00100000 /**< Rx FIFO Programmable Full
						* AXI FIFO MM2S Only */
#define XLLF_INT_RFPE_MASK        0x00080000 /**< Rx FIFO Programmable Empty
						* AXI FIFO MM2S Only */
#define XLLF_INT_ALL_MASK         0xfff80000 /**< All the ints */
#define XLLF_INT_ERROR_MASK       0xf2000000 /**< Error status ints */
#define XLLF_INT_RXERROR_MASK     0xe0000000 /**< Receive Error status ints */
#define XLLF_INT_TXERROR_MASK     0x12000000 /**< Transmit Error status ints */
/*@}*/

/** @name Reset register values
 *  These bits are associated with the XLLF_TDFR_OFFSET and XLLF_RDFR_OFFSET
 *  reset registers.
 * @{
 */
#define XLLF_RDFR_RESET_MASK        0x000000a5 /**< receive reset value */
#define XLLF_TDFR_RESET_MASK        0x000000a5 /**< Transmit reset value */
#define XLLF_LLR_RESET_MASK         0x000000a5 /**< Local Link reset value */
/*@}*/

void XLlFifo_IntClear(void* baseaddr, unsigned Mask)
{
	*(unsigned*)(baseaddr + XLLF_ISR_OFFSET) = ((Mask) & XLLF_INT_ALL_MASK);
}

int RxReceive (void* baseaddr, unsigned* DestinationAddr)
{
	int i;
	int Status;
	unsigned RxWord;
	static unsigned ReceiveLength;

	printf(" Receiving data ....\n\r");
	/* Read Recieve Length */
	int occp = *(unsigned*)(baseaddr + XLLF_RDFO_OFFSET);
	printf("occp is %d \n\r", occp);
	if( occp)
	{

		ReceiveLength = (*((unsigned*)(baseaddr + XLLF_RLF_OFFSET)) )/4;
		printf("Data Length: is %u \n\r", ReceiveLength);

		/* Start Receiving */
		for ( i=0; i < ReceiveLength; i++){
			RxWord = 0;
			RxWord = *(unsigned*)(baseaddr + XLLF_RDFD_OFFSET);
			*(DestinationAddr+i) = RxWord;
			printf("Data %u is %u \n\r", i, RxWord);
		}
	}

	return 0;
}

void XLlFifo_RxReset(void* baseaddr)
{

	*(unsigned*)(baseaddr + XLLF_RDFR_OFFSET) = XLLF_RDFR_RESET_MASK;
}

static unsigned dest_buffer[200];

int main(int argc, char *argv[])
{

    int c;
	int fd;
	char *uiod;

	int value = 0;
	int valued = 0;




	void *ptr;

	printf("AXI Stream UIO test.\n");
	while((c = getopt(argc, argv, "d:")) != -1) {
		switch(c) {
		case 'd':
			uiod=optarg;
			printf("uiod %s \n\r", uiod);
			break;
		case 'i':
			printf("option i \n\r");
			break;
		default:
			printf("invalid option: %c\n", (char)c);
			return -1;
		}
	}

	/* Open the UIO device file */
	fd = open(uiod, O_RDWR);
	if (fd < 1) {
		perror(argv[0]);
		printf("Invalid UIO device file:%s.\n", uiod);
		return -1;
	}

	/* mmap the UIO device */
	ptr = mmap(NULL, GPIO_MAP_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
	close(fd);

	
	printf("mmaped address is %x \n\r",ptr);

	//XLlFifo_RxReset(ptr);
	//*(unsigned*)(ptr + XLLF_LLR_OFFSET) = XLLF_RDFR_RESET_MASK;
	//usleep(100);

	XLlFifo_IntClear(ptr, 0xffffffff);
	printf("xllfifo is intcleared\n\r");
	RxReceive(ptr,dest_buffer);

	munmap(ptr, GPIO_MAP_SIZE);
	
}

数据读取逻辑在函数 int RxReceive中实现,实现逻辑如下:

  1. 读取RDFO,检查 receive FIFO occupancy,如果非0,继续

  2. 读取RLR(RLF),得到receive length in bytes(待接收的字节数,除以4,得到32bit
    word的个数)

  3. 循环读取RDFD, 每次读取一个32bit word, 直到读完receive length in
    bytes/4个word。

编译源文件:

arm-linux-gnueabihf-gcc axisfifo_test.c -o axisfifo_test

复制到NFS文件夹,在串口终端上运行,以下是串口终端的输出结果:

在这里插入图片描述

关于AXI Stream FIFO读取数据的补充

后续更加深入的测试表明,上述代码只能正常读取没有tlast信号的数据,当axis fifo 使能tlast信号后,每一次读取XLLF_RLF_OFFSET寄存器得到的数值表示一个数据包(用tlast信号区分)的字节数,而不是fifo中所有数据的字节长度。因此,需要将int RxReceive (void* baseaddr, unsigned* DestinationAddr)修改一下,见如下代码:

int RxReceive (void* baseaddr, u32* DestinationAddr)
{
	int i;
	int Status;
	unsigned RxWord;
	static u32 ReceiveLength;
	u32 totalLength = 0;

//	printf(" Receiving data ....\n\r");
	/* Read Recieve Length */
	int occp = *(u32*)(baseaddr + XLLF_RDFO_OFFSET);

	while( occp)
	{

		ReceiveLength = (*((u32*)(baseaddr + XLLF_RLF_OFFSET)) )/4;


		/* Start Receiving */
		for ( i=0; i < ReceiveLength; i++){
			RxWord = 0;
			RxWord = *(u32*)(baseaddr + XLLF_RDFD_OFFSET);
			*(DestinationAddr+totalLength) = RxWord;
			totalLength = totalLength + 1;

		}
		occp = *(u32*)(baseaddr + XLLF_RDFO_OFFSET);
	}

	return totalLength;
}

结论

简单的UIO驱动测试就结束了,接下来我会将TDC IP也放到设计中,实现通过UIO控制TDC IP以及时间戳数据的读取,敬请期待!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值