IMX6ULL_pinctrl/gpio子系统笔记

linux驱动讲究驱动分离和分层,即按照面向对象编程的思想来设计驱动框架,将设备抽象为一个对象结构体(结构体中包含该设备的信息,属性),驱动获取设备树中定义的信息,实现驱动操作

  1. pinctrl子系统
    (1) 没有使用pinctrl+gpio子系统下的驱动框架
    设备树操作:
    定义一个有操作设备的对应的设备节点

加载函数中的操作

  1. 定义一个包含该设备相关信息的设备结构体
  2. 注册设备号
  3. 对cdev对象操作
    //a. 设置该cdev对象作用的模块
    //b. 将fops添加到cdev对象中
    //c. 将设备号添加到cdev对象中
  4. 创建设备类
  5. 创建设备节点
  6. 硬件初始化 (直接对寄存器进行操作)
    a. 获取设备文件
    b. 通过OF函数来获取我们在设备树文件值定义的属性和属性值,原则上是:你需要什么获取什么(在这个demo中最主要的是获取reg属性的值)
    c. 使用of_iomap来将需要的物理地址映射为虚拟地址
    d. 使能时钟
    e. MUX+PAD
    f. 设置输入输出方向为输出
    g. 设置输出的初始值
  7. 实现fops
  8. 错误处理

(2) 使用pinctrl+gpio子系统下的驱动框架与没有使用的区别
首先设备树定义不一样
pinctrl子系统的使用 == 在设备树中通过定义pinctrl节点来实现MUX+PAD功能
操作流程, 在iomuxc节点下定义pinctrl节点,在该驱动设备对应的节点下引用该pinctrl节点
imx6ull.dtsi中的iomuxc节点
#include “imx6ull-pinfunc.h”

iomuxc: iomuxc@020e0000 {
				compatible = "fsl,imx6ul-iomuxc";
				reg = <0x020e0000 0x4000>;
			};

imx6ull-alientek-emmc.dts文件中引用扩展iomuxc节点
#include “imx6ull.dtsi”

&iomuxc {
pinctrl-names = “default”;
pinctrl-0 = <&pinctrl_hog_1>;
imx6ul-evk {
pinctrl_hog_1: hoggrp-1 {
fsl,pins = <
MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059 /* SD1 CD /
MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT 0x17059 /
SD1 VSELECT /
/
MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x17059 SD1 RESET /
MX6UL_PAD_GPIO1_IO00__ANATOP_OTG1_ID 0x13058 /
USB_OTG1_ID */
>;
};

pinctrl_wjc_gpioled: wjcledgrp {
		fsl,pins = <
			MX6UL_PAD_GPIO1_IO03__GPIO1_IO03        0x10B0  /*LED0 MUX+PAD */
		>;
	};

}

==> 将其结合整理后如下
iomuxc: iomuxc@020e0000 {
compatible = “fsl,imx6ul-iomuxc”;
reg = <0x020e0000 0x4000>;

				pinctrl-names = "default";
				pinctrl-0 = <&pinctrl_hog_1>;
				imx6ul-evk {
					pinctrl_hog_1: hoggrp-1 {
						fsl,pins = <
							MX6UL_PAD_UART1_RTS_B__GPIO1_IO19	0x17059 /* SD1 CD */
							MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT	0x17059 /* SD1 VSELECT */
							/* MX6UL_PAD_GPIO1_IO09__GPIO1_IO09        0x17059 SD1 RESET */
							MX6UL_PAD_GPIO1_IO00__ANATOP_OTG1_ID	0x13058	/* USB_OTG1_ID */
						>;
					};
				
				pinctrl_wjc_gpioled: wjcledgrp {
						fsl,pins = <
							MX6UL_PAD_GPIO1_IO03__GPIO1_IO03        0x10B0  /*LED0 MUX+PAD */
						>;
					};
				... ... ...
			};

(3) 前面说pinctrl代替了前面的mux+pad功能,整理分析一下怎么代替
例如: MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059 猜测: MX6UL_PAD_UART1_RTS_B__GPIO1_IO19实现mux功能,0x17059实现电气属性功能

~/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek$ grep -rn “MX6UL_PAD_UART1_RTS_B__GPIO1_IO19” ./
==>
./arch/arm/boot/dts/imx6ul-pinfunc.h:196:#define MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x0090 0x031C 0x0000 0x5 0x0

调用关系 在imx6ull.dtsi中包含了"imx6ull-pinfunc.h"头文件,而"imx6ull-pinfunc.h"头文件又包含了imx6ul-pinfunc.h头文件

在"imx6ull-pinfunc.h"中共定义了UART1_RTS_B引脚的8种复位情况,而在IMX6ULL参考文档中UART1_RTS_B引脚的可选复用IO就是这8种情况
#define MX6UL_PAD_UART1_RTS_B__UART1_DCE_RTS 0x0090 0x031C 0x0620 0x0 0x3
#define MX6UL_PAD_UART1_RTS_B__UART1_DTE_CTS 0x0090 0x031C 0x0000 0x0 0x0
#define MX6UL_PAD_UART1_RTS_B__ENET1_TX_ER 0x0090 0x031C 0x0000 0x1 0x0
#define MX6UL_PAD_UART1_RTS_B__USDHC1_CD_B 0x0090 0x031C 0x0668 0x2 0x1
#define MX6UL_PAD_UART1_RTS_B__CSI_DATA05 0x0090 0x031C 0x04CC 0x3 0x1
#define MX6UL_PAD_UART1_RTS_B__ENET2_1588_EVENT1_OUT 0x0090 0x031C 0x0000 0x4 0x0
#define MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x0090 0x031C 0x0000 0x5 0x0
#define MX6UL_PAD_UART1_RTS_B__USDHC2_CD_B 0x0090 0x031C 0x0674 0x8 0x2

分析宏定义格式
#define MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x0090 0x031C 0x0000 0x5 0x0
0x0090 0x031C 0x0000 0x5 0x0
mux_reg conf_reg input_reg mux_code input_val

mux_reg: mux_reg寄存器偏移地址 reg = <0x020e0000 0x4000>; ==> iomuxc节点的基地址为0x020e0000, 故MX6UL_PAD_UART1_RTS_B的基地址 = 0x020e0000 + 0x0090 (参考书中就是这个值) 这个pin的复用寄存器的地址
conf_reg: conf_reg寄存器偏移地址 MX6UL_PAD_UART1_RTS_B__GPIO1_IO19这个复用之后的基地址 = 0x020e0000 + 0x031C ,conf_reg寄存器 == 表示该io的电气属性
input_reg: input_reg寄存器偏移地址 对于没有input_reg寄存器的设备,就不需要设置
mux_code:这里就相当于设置MX6UL_PAD_UART1_RTS_B这个io复用为MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 非常重要
input_val:input_reg寄存器中的值 没有input_reg寄存器就不需要设置
==>
故该宏定义相当于实现对xxxIO的复用设置
0x17059 == conf_reg寄存器的值,这里设置了该IO的上/下拉,驱动能力等
==>
替代了原本的Pad操作

(4) 分析pinctrl驱动
a. 通过compatible属性来找到匹配的驱动 compatible = “fsl,imx6ul-iomuxc”;
~/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek$ grep -rn ““fsl,imx6ul-iomuxc”” ./
==>
./drivers/pinctrl/freescale/pinctrl-imx6ul.c:327: { .compatible = “fsl,imx6ul-iomuxc”, .data = &imx6ul_pinctrl_info, },

b. 分析驱动文件
	//加载驱动
	static int __init imx6ul_pinctrl_init(void)
	{
		return platform_driver_register(&imx6ul_pinctrl_driver);   // 从这里就知道使用了platform平台
	}
	
	//实现platform_driver对象
	static struct platform_driver imx6ul_pinctrl_driver = {
		.driver = {
			.name = "imx6ul-pinctrl",
			.owner = THIS_MODULE,
			.of_match_table = of_match_ptr(imx6ul_pinctrl_of_match),
		},
		.probe = imx6ul_pinctrl_probe,
		.remove = imx_pinctrl_remove,
	};
	
	//和设备树匹配表
	static struct of_device_id imx6ul_pinctrl_of_match[] = {
		{ .compatible = "fsl,imx6ul-iomuxc", .data = &imx6ul_pinctrl_info, },
		{ .compatible = "fsl,imx6ull-iomuxc-snvs", .data = &imx6ull_snvs_pinctrl_info, },
		{ /* sentinel */ }
	};
	
	//匹配成功,调用probe函数
	static int imx6ul_pinctrl_probe(struct platform_device *pdev)
	{
		... ...
		return imx_pinctrl_probe(pdev, pinctrl_info);
	}
	==>
	int imx_pinctrl_probe(struct platform_device *pdev,struct imx_pinctrl_soc_info *info)
	{
		... ...
		ret = imx_pinctrl_probe_dt(pdev, info);
		
	}
		==>
	static int imx_pinctrl_probe_dt(struct platform_device *pdev,struct imx_pinctrl_soc_info *info)
	{
		... ...
		for_each_child_of_node(np, child)
			imx_pinctrl_parse_functions(child, info, i++);

		return 0;
	}
			==>
	static int imx_pinctrl_parse_functions(struct device_node *np,struct imx_pinctrl_soc_info *info,u32 index)
	{
		... ...
		for_each_child_of_node(np, child) {
			func->groups[i] = child->name;
			grp = &info->groups[info->grp_index++];
			imx_pinctrl_parse_groups(child, grp, info, i++);
		}

		return 0;
	}
				==>
	static int imx_pinctrl_parse_groups(struct device_node *np,struct imx_pin_group *grp,struct imx_pinctrl_soc_info *info,u32 index)
	{
		... ...
		/*
		 * the binding format is fsl,pins = <PIN_FUNC_ID CONFIG ...>,
		 * do sanity check and calculate pins number
		 */
		list = of_get_property(np, "fsl,pins", &size);  //==>这里就决定了我们在pinctrl中的表示io的属性的属性名称必须为"fsl,pins"  ***非常关键  每个soc的这个属性名称不同
		if (!list) {
			dev_err(info->dev, "no fsl,pins property in node %s\n", np->full_name);
			return -EINVAL;
		}

		... ...
		for (i = 0; i < grp->npins; i++) {
			... ...
			//下面的5个赋值,就是配置指定的寄存器   
			pin_reg->mux_reg = mux_reg;				
			pin_reg->conf_reg = conf_reg;
			pin->input_reg = be32_to_cpu(*list++);
			pin->mux_mode = be32_to_cpu(*list++);
			pin->input_val = be32_to_cpu(*list++);

			/* SION bit is in mux register */
			config = be32_to_cpu(*list++);
			if (config & IMX_PAD_SION)
				pin->mux_mode |= IOMUXC_CONFIG_SION;
			pin->config = config & ~IMX_PAD_SION;

			dev_dbg(info->dev, "%s: 0x%x 0x%08lx", info->pins[pin_id].name,
					pin->mux_mode, pin->config);
		}

		return 0;
	}
	综上所述: pinctrl中宏定义配置的好处是:用户只需要提供要操作的io的名称和配置的参数即可,至于将参数配置到指定寄存器的操作函数会帮忙自动完成
			   pinctrl子系统重点是设置PIN的复用和电气属性
  1. gpio子系统
    当pinctrl定义的pin被复用为GPIO时,这时候就是gpio子系统开始作用了
    通常pinctrl+gpio联合使用时,先将要操作的pin定义为pinctrl节点,然后在设备对应的节点中引用该pinctrt节点,然后定义gpio属性,该属性的值就是pinctrl复用的gpio的信息

    (1) 联合使用的demo (imx6ull-alientek-emmc.dts)
    先定义pinctrl节点信息
    pinctrl_wjc_gpioled: wjcledgrp {
    fsl,pins = <
    MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 /*LED0 MUX+PAD */
    >;
    };

    在定义gpio节点信息
    wjcgpioled {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = “wjc-gpioled”;
    pinctrl-names = “default”; /* 设置这个设备要引用的PINCTRL子系统的名称*/
    pinctrl-0 = <&pinctrl_wjc_gpioled>; /* 该设备要引用的PINCTRL子系统的节点*/
    wjcled-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>; /* 我们自定义gpio相关属性,属性值表示该pin的GPIO编号和极性*/
    status = “okay”;
    };

    关键点: 这里必须保证我们使用的pin(pinctrl_wjc_gpioled + wjcled-gpio)是唯一的即只有在这个节点用到,不然会引起冲突

    (2) gpio子系统设备树代码分析
    soc: imx6ull.dtsi
    /:{
    soc {
    aips1{
    gpio1: gpio@0209c000 {
    compatible = “fsl,imx6ul-gpio”, “fsl,imx35-gpio”;
    reg = <0x0209c000 0x4000>;
    interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
    <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
    gpio-controller; //表示gpio1节点是个GPIO控制器
    #gpio-cells = <2>; //表示一共有两个cell,第一个cell为GPIO编号 例如&gpio1 3,第二个cell为GPIO的极性,例如GPIO_ACTIVE_LOW(表示低电平有效)
    interrupt-controller;
    #interrupt-cells = <2>;
    };
    }
    }
    }

    (3) GPIO驱动分析
    a. 根据compatible属性值寻找匹配的驱动
    ~/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek$ grep -rn “fsl,imx35-gpio” ./
    ==>
    ./drivers/gpio/gpio-mxc.c:156: { .compatible = “fsl,imx35-gpio”, .data = &mxc_gpio_devtype[IMX35_GPIO], },
    b. 加载函数
    postcore_initcall(gpio_mxc_init);
    static int __init gpio_mxc_init(void)
    {
    return platform_driver_register(&mxc_gpio_driver); //显而易见是platform平台
    }

    	//实现platform_driver对象
    	static struct platform_driver mxc_gpio_driver = {
    		.driver		= {
    			.name	= "gpio-mxc",
    			.of_match_table = mxc_gpio_dt_ids,
    		},
    		.probe		= mxc_gpio_probe,
    		.id_table	= mxc_gpio_devtype,
    	};
    	
    	//实现设备树匹配表
    	static const struct of_device_id mxc_gpio_dt_ids[] = {
    		{ .compatible = "fsl,imx1-gpio", .data = &mxc_gpio_devtype[IMX1_GPIO], },
    		{ .compatible = "fsl,imx21-gpio", .data = &mxc_gpio_devtype[IMX21_GPIO], },
    		{ .compatible = "fsl,imx31-gpio", .data = &mxc_gpio_devtype[IMX31_GPIO], },
    		{ .compatible = "fsl,imx35-gpio", .data = &mxc_gpio_devtype[IMX35_GPIO], },
    		{ /* sentinel */ }
    	};
    	
    	//匹配成功调用probe函数
    	static int mxc_gpio_probe(struct platform_device *pdev)
    	{
    		struct device_node *np = pdev->dev.of_node;     //设备节点
    		struct mxc_gpio_port *port;					 	//mxc_gpio_port就是LMX6ULL GPIO的抽象
    				|
    		struct mxc_gpio_port {
    			struct list_head node;
    			void __iomem *base;
    			int irq;
    			int irq_high;
    			struct irq_domain *domain;
    			struct bgpio_chip bgc;
    			u32 both_edges;
    		};
    			
    		struct resource *iores;
    		int irq_base;
    		int err;
    
    		mxc_gpio_get_hw(pdev); //获取硬件相关数据 result: mxc_gpio_hwdata == &imx35_gpio_hwdata &&  mxc_gpio_hwtype == hwtype ==IMX35_GPIO
    				|
    		static void mxc_gpio_get_hw(struct platform_device *pdev)
    		{
    			... ... 
    			if (mxc_gpio_hwtype) {
    				/*
    				 * The driver works with a reasonable presupposition,
    				 * that is all gpio ports must be the same type when
    				 * running on one soc.
    				 */
    				BUG_ON(mxc_gpio_hwtype != hwtype);
    				return;
    			}
    
    			if (hwtype == IMX35_GPIO)
    				mxc_gpio_hwdata = &imx35_gpio_hwdata;  //这里IMX6ULL的硬件类型就是IMX35_GPIO
    			else if (hwtype == IMX31_GPIO)
    				mxc_gpio_hwdata = &imx31_gpio_hwdata;
    			else
    				mxc_gpio_hwdata = &imx1_imx21_gpio_hwdata;
    
    			mxc_gpio_hwtype = hwtype;
    		}
    			
    		//申请一个GPIO对象的内存空间
    		port = devm_kzalloc(&pdev->dev, sizeof(*port), GFP_KERNEL);
    		if (!port)
    			return -ENOMEM;
    		
    		//获取地址资源 + 地址映射 == 设备的虚拟地址的基地址 == 可以操作该gpio设备的所有寄存器 reg = <0x0209c000 0x4000>;
    		iores = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    		port->base = devm_ioremap_resource(&pdev->dev, iores);
    		if (IS_ERR(port->base))
    			return PTR_ERR(port->base);
    		
    		//获取中断号  interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,<GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
    		port->irq_high = platform_get_irq(pdev, 1);
    		port->irq = platform_get_irq(pdev, 0);
    		if (port->irq < 0)
    			return port->irq;
    
    		/* disable the interrupt and clear the status */
    		//失能中断 + 清除中断标志(所有中断)
    		writel(0, port->base + GPIO_IMR);
    		writel(~0, port->base + GPIO_ISR);
    
    		if (mxc_gpio_hwtype == IMX21_GPIO) {
    			... ... 
    		} else {
    			/* setup one handler for each entry */\
    			//请求中断,设置中断函数都是mx3_gpio_irq_handler
    			irq_set_chained_handler(port->irq, mx3_gpio_irq_handler);
    			irq_set_handler_data(port->irq, port);
    			if (port->irq_high > 0) {
    				/* setup handler for GPIO 16 to 31 */
    				irq_set_chained_handler(port->irq_high,
    							mx3_gpio_irq_handler);
    				irq_set_handler_data(port->irq_high, port);
    			}
    		}
    		//******* 非常关键 这里就是创造那些GPIO api函数的地方,gpio操作函数 == 对直接操作寄存器过程的封装 == 类似于前面的pinctrl中的宏 
    		err = bgpio_init(&port->bgc, &pdev->dev, 4,
    				 port->base + GPIO_PSR,
    				 port->base + GPIO_DR, NULL,
    				 port->base + GPIO_GDIR, NULL, 0);
    		if (err)
    			goto out_bgio;
    			
    		/*
    			&port->bgc == struct bgpio_chip *bgc
    			struct bgpio_chip {
    				struct gpio_chip gc;
    				... ...
    			}
    			gpio_chip结构体就是抽象出来的GPIO控制器(里面包含了那些gpio操作函数)
    			struct gpio_chip {
    				const char		*label;
    				struct device		*dev;
    				struct module		*owner;
    				struct list_head        list;
    
    				int			(*request)(struct gpio_chip *chip,
    									unsigned offset);
    				void			(*free)(struct gpio_chip *chip,
    									unsigned offset);
    				int			(*get_direction)(struct gpio_chip *chip,
    									unsigned offset);
    				int			(*direction_input)(struct gpio_chip *chip,
    									unsigned offset);
    				int			(*direction_output)(struct gpio_chip *chip,
    									unsigned offset, int value);
    				... ...
    			}
    			port->base + GPIO_PSR 
    			port->base: 前面获取到的gpio节点的基地址 reg = <0x0209c000 0x4000>; 0x0209c000的虚拟地址
    			GPIO_PSR 
    			==>
    				#define GPIO_PSR		(mxc_gpio_hwdata->psr_reg)
    				static struct mxc_gpio_hwdata *mxc_gpio_hwdata;
    				==> 
    					在前面的mxc_gpio_get_hw函数中 mxc_gpio_hwdata = &imx35_gpio_hwdata;
    					==>
    						static struct mxc_gpio_hwdata imx35_gpio_hwdata = {
    							.dr_reg		= 0x00,
    							.gdir_reg	= 0x04,
    							.psr_reg	= 0x08,
    							.icr1_reg	= 0x0c,
    							.icr2_reg	= 0x10,
    							.imr_reg	= 0x14,
    							.isr_reg	= 0x18,
    							.edge_sel_reg	= 0x1c,
    							.low_level	= 0x00,
    							.high_level	= 0x01,
    							.rise_edge	= 0x02,
    							.fall_edge	= 0x03,
    						}
    					综上所述: port->base + GPIO_PSR == 0x0209c000的虚拟地址 + 0x08   ==>PSR寄存器的地址
    			
    		*/	
    			|
    		int bgpio_init(struct bgpio_chip *bgc, struct device *dev,unsigned long sz, void __iomem *dat, void __iomem *set,
    				void __iomem *clr, void __iomem *dirout, void __iomem *dirin,
    	            unsigned long flags)
    		{
    			... ... 
    			//不在跟了,这里就是将这些代表寄存器地址的参数赋值给bgc中对应的参数  ***非常关键  这里你就相当于把这个gpio的DR,GDIR,psr寄存器地址保存在bgc对象中
    			ret = bgpio_setup_io(bgc, dat, set, clr);
    			if (ret)
    				return ret;
    
    			ret = bgpio_setup_accessors(dev, bgc, flags & BGPIOF_BIG_ENDIAN,
    							flags & BGPIOF_BIG_ENDIAN_BYTE_ORDER);
    			if (ret)
    				return ret;
    
    			ret = bgpio_setup_direction(bgc, dirout, dirin);
    			if (ret)
    				return ret;
    
    			return ret;
    		}
    		
    		综上所述: bgpio_chip对象中保存了该gpio的寄存器信息,又有gpio的操作函数(成员gc中定义的),所以只要得到bgc就可以对GPIO进行操作
    			
    
    		port->bgc.gc.to_irq = mxc_gpio_to_irq;
    		port->bgc.gc.base = (pdev->id < 0) ? of_alias_get_id(np, "gpio") * 32 :
    							 pdev->id * 32;
    
    		... ...
    	}
    
    (4) GPIO子系统的API函数
    	关键点: 这里这些api函数都需要知道操作的gpio编号,而gpio编号是在设备树中,驱动开发者自定义的gpio属性中定义的,所以想使用api函数,首先要获取编号
    	demo如下:
    	gpioled.led_gpio = of_get_named_gpio(gpioled.nd,"wjcled-gpio",0);
    	gpioled.led_gpio: 获取到的gpio编号
    	gpioled.nd: 设备节点
    	"wjcled-gpio": 自定义的gpio属性的名称
    	0:下标,如果有多组gpio,这里是获取下标为0的那组gpio信息对应的gpio编号
    	
    	扩展知识:
    	gpio_resquest()和gpio_resquest_one()的区别
    	gpio_resquest(): 申请了该gpio资源之后,该函数不会主动释放,需要我们手动执行gpio_free()来释放申请到的gpio资源,两个参数
    	gpio_resquest_one(): 申请并配置gpio资源一次,他与gpio_resquest函数相比还有个flag参数,来表示你想配置为(输入/输出/其他),函数最后会释放该gpio资源
    	为什么gpio_resquest_one函数怎么设计? 申请 -- 配置 -- 释放
    	原因: 释放了,别人才能使用这个资源,你释放了不是说前面的配置就失效了,那些配置还是存在的,除非别人申请之后重新配置才会改变这个gpio的配置
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值