Linux驱动开发——pinctrl 和 和 gpio 子系统实验
文章目录
系列文章:
Linux驱动开发——字符设备驱动开发
Linux驱动开发——LED驱动开发
Linux驱动开发——新字符设备驱动开发
Linux驱动开发——设备树
Linux驱动开发——基于设备树的LED驱动开发
1 概述
前面的项目中编写了基于设备数的LED驱动,这和裸机开发差不多,都是配置GPIO寄存器的相关参数。但是GPIO这种都是通用的接口,Linux内核中提供了统一的配置方式,可以通过pinctrl和gpio子系统去开发GPIO驱动。
2 pinctrl子系统
2.1 pinctrl子系统简介
Linux驱动讲究驱动分离与分层,pinctrl子系统和gpio子系统就是分层思想的产物。
在之前基于设备树或者裸机开发中,我们首先需要配置引脚的复用功能,然后再配置GPIO的相关参数。
这里就可以将其分为两部分,一部分是引脚的配置,这个就是交给pinctrl子系统来处理的。另一部分就是GPIO相关参数的设置,这个是交给gpio子系统来处理的。
所以pinctrl子系统的主要功能就是配置引脚相关的属性。
2.2 rk3568的pinctrl子系统驱动
2.2.1 PIN配置信息详解
pinctrl: pinctrl {
compatible = "rockchip,rk3568-pinctrl";
rockchip,grf = <&grf>;
rockchip,pmu = <&pmugrf>;
#address-cells = <2>; //起始地址长度为2字节
#size-cells = <2>; //地址长度为2字节
ranges;
gpio0: gpio@fdd60000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfdd60000 0x0 0x100>;
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>;
gpio-controller;
#gpio-cells = <2>;
gpio-ranges = <&pinctrl 0 0 32>;
interrupt-controller;
#interrupt-cells = <2>;
};
gpio1: gpio@fe740000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfe740000 0x0 0x100>;
interrupts = <GIC_SPI 34 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru PCLK_GPIO1>, <&cru DBCLK_GPIO1>;
gpio-controller;
#gpio-cells = <2>;
gpio-ranges = <&pinctrl 0 32 32>;
interrupt-controller;
#interrupt-cells = <2>;
};
gpio2: gpio@fe750000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfe750000 0x0 0x100>;
interrupts = <GIC_SPI 35 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru PCLK_GPIO2>, <&cru DBCLK_GPIO2>;
gpio-controller;
#gpio-cells = <2>;
gpio-ranges = <&pinctrl 0 64 32>;
interrupt-controller;
#interrupt-cells = <2>;
};
gpio3: gpio@fe760000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfe760000 0x0 0x100>;
interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru PCLK_GPIO3>, <&cru DBCLK_GPIO3>;
gpio-controller;
#gpio-cells = <2>;
gpio-ranges = <&pinctrl 0 96 32>;
interrupt-controller;
#interrupt-cells = <2>;
};
gpio4: gpio@fe770000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfe770000 0x0 0x100>;
interrupts = <GIC_SPI 37 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru PCLK_GPIO4>, <&cru DBCLK_GPIO4>;
gpio-controller;
#gpio-cells = <2>;
gpio-ranges = <&pinctrl 0 128 32>;
interrupt-controller;
#interrupt-cells = <2>;
};
};
以上是rk3568.dtsi文件中的pinctrl配置,主要配置了4组gpio端口。但是没有pin引脚的具体配置信息,具体配置信息在rk3568-pinctrl.dtsi文件中:
&pinctrl {
...
can0 {
/omit-if-no-ref/
can0m0_pins: can0m0-pins {
rockchip,pins =
/* can0_rxm0 */
<0 RK_PB4 2 &pcfg_pull_none>,
/* can0_txm0 */
<0 RK_PB3 2 &pcfg_pull_none>;
};
/omit-if-no-ref/
can0m1_pins: can0m1-pins {
rockchip,pins =
/* can0_rxm1 */
<2 RK_PA2 4 &pcfg_pull_none>,
/* can0_txm1 */
<2 RK_PA1 4 &pcfg_pull_none>;
};
};
can1 {
/omit-if-no-ref/
can1m0_pins: can1m0-pins {
rockchip,pins =
/* can1_rxm0 */
<1 RK_PA0 3 &pcfg_pull_none>,
/* can1_txm0 */
<1 RK_PA1 3 &pcfg_pull_none>;
};
/omit-if-no-ref/
can1m1_pins: can1m1-pins {
rockchip,pins =
/* can1_rxm1 */
<4 RK_PC2 3 &pcfg_pull_none>,
/* can1_txm1 */
<4 RK_PC3 3 &pcfg_pull_none>;
};
};
can2 {
/omit-if-no-ref/
can2m0_pins: can2m0-pins {
rockchip,pins =
/* can2_rxm0 */
<4 RK_PB4 3 &pcfg_pull_none>,
/* can2_txm0 */
<4 RK_PB5 3 &pcfg_pull_none>;
};
/omit-if-no-ref/
can2m1_pins: can2m1-pins {
rockchip,pins =
/* can2_rxm1 */
<2 RK_PB1 4 &pcfg_pull_none>,
/* can2_txm1 */
<2 RK_PB2 4 &pcfg_pull_none>;
};
};
...
}
以上配置了三个can接口的信息,每个pinctrl节点必须至少包含一个子节点来存放pinctrl相关信息,也就是pinctrl集,这个集合里面存放当前外设用到哪些引脚、复用配置、上下位、驱动能力等。一般存放这个pinctrl集的子节点名字是“rockchip,pins”
根据rockchip,pinctrl.txt文档里面的介绍,引脚复用设置的格式如下:
rockchip,pins = <PIN_BANK PIN_BANK_IDX MUX &phandle>
一共分为四部分:
- PIN_BANK
PIN_BANK就是PIN所属的组,RK3568一共有5组PIN:GPIO0~GPIO4,分别对应0-4。 - PIN_BANK_IDX
PIN_BANK_IDX是组内编号,以GPIO0为例,一共有A0-A7、B0-B7、C0-C7、D0-D7,这32个PIN,瑞芯微已经给这些PIN编了号,在include/dt-bindings/pinctrl/rockchip.h中:
#define RK_GPIO00
#define RK_GPIO11
#define RK_GPIO22
#define RK_GPIO33
#define RK_GPIO44
#define RK_GPIO66
#define RK_PA00
#define RK_PA11
#define RK_PA22
#define RK_PA33
#define RK_PA44
#define RK_PA55
#define RK_PA66
#define RK_PA77
#define RK_PB08
#define RK_PB19
#define RK_PB210
#define RK_PB311
#define RK_PB412
#define RK_PB513
#define RK_PB614
#define RK_PB715
#define RK_PC016
#define RK_PC117
#define RK_PC218
#define RK_PC319
#define RK_PC420
#define RK_PC521
#define RK_PC622
#define RK_PC723
#define RK_PD024
#define RK_PD125
#define RK_PD226
#define RK_PD327
#define RK_PD428
#define RK_PD529
#define RK_PD630
#define RK_PD731
如果要设置GPIO0_C0,那么PIN_BANK_IDX需要设置为RK_PC0
3. MUX
MUX就是设置PIN的复用功能,一个PIN最多有16种复用功能,在include/dt-bindings/pinctrl/rockchip.h文件中有描述
#define RK_FUNC_GPIO0
#define RK_FUNC_00
#define RK_FUNC_11
#define RK_FUNC_22
#define RK_FUNC_33
#define RK_FUNC_44
#define RK_FUNC_55
#define RK_FUNC_66
#define RK_FUNC_77
#define RK_FUNC_88
#define RK_FUNC_99
#define RK_FUNC_1010
#define RK_FUNC_1111
#define RK_FUNC_1212
#define RK_FUNC_1313
#define RK_FUNC_1414
#define RK_FUNC_1515
- phandle
phandle用来描述一些引脚的通用配置信息
&pinctrl {
/omit-if-no-ref/
pcfg_pull_up: pcfg-pull-up {
bias-pull-up;
};
/omit-if-no-ref/
pcfg_pull_down: pcfg-pull-down {
bias-pull-down;
};
/omit-if-no-ref/
pcfg_pull_none: pcfg-pull-none {
bias-disable;
};
/omit-if-no-ref/
pcfg_pull_none_drv_level_0: pcfg-pull-none-drv-level-0 {
bias-disable;
drive-strength = <0>;
};
...
}
上面就是上下拉的配置
最终,如果要将GPIO0_C0设置为GPIO功能,那么配置为:
&pinctrl {
/omit-if-no-ref/
pcfg_pull_up: pcfg-pull-up {
bias-pull-up;
};
/omit-if-no-ref/
pcfg_pull_down: pcfg-pull-down {
bias-pull-down;
};
/omit-if-no-ref/
pcfg_pull_none: pcfg-pull-none {
bias-disable;
};
/omit-if-no-ref/
pcfg_pull_none_drv_level_0: pcfg-pull-none-drv-level-0 {
bias-disable;
drive-strength = <0>;
};
...
}
上面就是设置上下拉
最终,如果要将GPIO0_C0设置为GPIO,那么配置为:
rockchip,pins =<0 RK_PC0 RK_FUNC_GPIO &pcfg_pull_none>;
2.2.2 设备树中添加pinctrl节点模板
&pinctrl {
uart2 {
/omit-if-no-ref/
uart2m0_xfer: uart2m0-xfer {
rockchip,pins =
/* uart2_tx_m1 */
<0 RK_PD1 1 &pcfg_pull_up>;
};
};
};
在pinctrl节点下添加了一个uart2节点,这里只添加了UART2的TX引脚。
按道理来讲,当我们将一个 PIN 用作 GPIO 功能的时候也需要创建对应的 pinctrl 节点,并且将所用的 PIN 复用为 GPIO 功能,但是!对于 RK3568 而言,如果一个 PIN 用作 GPIO 功能的时候不需要创建对应的 pinctrl 节点!
3 gpio子系统
上一节是pinctrl子系统,用于配置pin引脚,pin引脚配置好之后,如果引脚复用为GPIO,则需要使用gpio子系统来初始化GPIO的一些常用配置。驱动开发者在设备树中添加gpio相关信息,然后可以在驱动程序中使用gpio子系统提供的API函数来操作GPIO,极大的方便了驱动开发者使用GPIO。
3.1 rk3568的gpio子系统驱动
gpio0: gpio@fdd60000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfdd60000 0x0 0x100>;
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>;
gpio-controller;
#gpio-cells = <2>;
gpio-ranges = <&pinctrl 0 0 32>;
interrupt-controller;
#interrupt-cells = <2>;
};
以上是GPIO0引脚的配置信息,属于pinctrl子节点,绑定文档中的Documentation/devicetree/bindings/gpio/gpio.txt 详细描述了gpio控制器节点各个属性信息。compatible配置为rockchip,gpio-bank,可以通过这个找到GPIO驱动源文件,哪个驱动使用了该设备树配置。
reg属性配置了GPIO0控制器的寄存器基地址。
interrupts描述了GPIO0控制器的中断信息。
clocks属性指定GPIO0控制器的时钟。
gpio-controller表示gpio0节点是个GPIO控制器,每个GPIO控制器节点必须包含该属性。
#gpio-cells属性表示有两个cell,第一个为GPIO编号,第二个表示GPIO极性,如果为0表示高电平有效,为1表示低电平有效。
以上是GPIO0控制器节点,具体的某个引脚作为GPIO使用的时候还需要进一步配置。
以CSI1摄像头为例,该摄像头的I2C连接在I2C4上,其使用GPIO3_B6作为RESET引脚
imx415: imx415@1a {
status = "okay";
compatible = "sony,imx415";
reg = <0x1a>;
clocks = <&cru CLK_CIF_OUT>;
clock-names = "xvclk";
power-domains = <&power RK3568_PD_VI>;
pinctrl-names = "rockchip,camera_default";
pinctrl-0 = <&cif_clk>;
reset-gpios = <&gpio3 RK_PB6 GPIO_ACTIVE_LOW>;
power-gpios = <&gpio4 RK_PB4 GPIO_ACTIVE_HIGH>;
rockchip,camera-module-index = <0>;
rockchip,camera-module-facing = "back";
rockchip,camera-module-name = "CMK-OT1522-FG3";
rockchip,camera-module-lens-name = "CS-P1150-IRC-8M-FAU";
port {
imx415_out: endpoint {
remote-endpoint = <&mipi_in_ucam1>;
data-lanes = <1 2 3 4>;
};
};
};
在i2c4节点下面有以上子节点,reset-gpios描述使用GPIO3_B6引脚作为reset引脚,低电平有效。GPIO_ACTIVE_LOW这种有效位定义在include/linux/gpio/machine.h中:
enum gpio_lookup_flags {
GPIO_ACTIVE_HIGH = (0 << 0),
GPIO_ACTIVE_LOW = (1 << 0),
GPIO_OPEN_DRAIN = (1 << 1),
GPIO_OPEN_SOURCE = (1 << 2),
GPIO_PERSISTENT = (0 << 3),
GPIO_TRANSITORY = (1 << 3),
};
3.2 gpio子系统API函数
- gpio_request函数
int gpio_request(unsigned gpio, const char *label)
用于申请一个GPIO管脚,在使用GPIO之前都需要先申请一个GPIO管脚。gpio表示gpio标号,使用of_get_named_gpio从设备树获取指定GPIO属性信息,会返回该标号。label是给gpio设置一个名字。
返回值:0,申请成功;1,申请失败
2. gpio_free函数
void gpio_free(unsigned gpio)
如果某个申请的gpio不再使用了,使用该函数进行释放
3. gpio_direction_input函数
int gpio_direction_input(unsigned gpio)
该函数用于将某个gpio设置为输入
返回值:0,成功;负数,设置失败
4. gpio_direction_output 函数
int gpio_direction_output(unsigned gpio)
该函数用于将某个gpio设置为输出
返回值:0,成功;负数,设置失败
5. gpio_get_value 函数
int gpio_get_value(unsigned int gpio)
用于获取某个GPIO的值
返回值:非负值,GPIO当前值;负数,获取失败
6. gpio_set_value函数
void gpio_set_value(unsigned int gpio, int value)
用于设置某个GPIO的值
3.3 设备树中添加gpio节点模板
gpioled {
compatible = "alientek,led";
led-gpio = <&gpio0 RK_PC0 GPIO_ACTIVE_HIGH>;
status = "okay";
};
在根目录下添加led设备子节点,led-gpio配置了led使用的是哪一个gpio。
3.4 与gpio相关的OF函数
- of_gpio_named_count函数
int of_gpio_named_count(struct device_node *np, const char *propname)
该函数用于获取设备树某个属性定义了几个GPIO信息,如果定义是空的GPIO信息也会被统计。np表示设备节点,propname表示要统计的GPIO属性。
返回值:正值,统计到的GPIO数量;负值,失败。
gpios = <0
&gpio1 1 2
0
&gpio2 3 4>;
以上GPIO,其中有两个是空的,但是该函数还是会返回4。
2. of_gpio_count 函数
int of_gpio_count(struct device_node *np)
此函数统计的是“gpios”属性的GPIO数量,而of_gpio_named_count可以统计任意属性的GPIO信息。
3. of_get_named_gpio 函数
int of_get_named_gpio(struct device_node *np,
const char *propname,
int index)
用于获取GPIO编号,GPIO操作的函数需要使用GPIO编号,此函数会将设备树中类似<&gpio4 RK_PA1 GPIO_ACTIVE_LOW>的属性信息转换成对应的GPIO编号,此函数使用较为频繁。
propname表示要获取GPIO信息的属性名。index表示GPIO索引,一个属性中可能包含多个GPIO,此参数指定获取哪个GPIO编号,如果只有一个GPIO信息的话,此参数可以为0。
4 实验程序编写
4.1 修改设备树
gpioled {
compatible = "alientek,led";
led-gpio = <&gpio0 RK_PC0 GPIO_ACTIVE_HIGH>;
status = "okay";
};
添加设备模块
4.2 驱动程序编写
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define GPIOLED_CNT 1
#define GPIOLED_NAME "gpioled"
#define LEDON 1
#define LEDOFF 0
static void __iomem *PMU_GRF_GPIO0C_IOMUX_L_PI;
static void __iomem *PMU_GRF_GPIO0C_DS_0_PI;
static void __iomem *GPIO0_SWPORT_DR_H_PI;
static void __iomem *GPIO0_SWPORT_DDR_H_PI;
struct gpioled_dev {
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
int major;
int minor;
struct device_node *nd;
int led_gpio;
};
struct gpioled_dev gpioled;
void led_switch(u8 sta) {
u32 val = 0;
if (sta == LEDON) {
val = readl(GPIO0_SWPORT_DR_H_PI);
val &= ~(0x1 << 0);
val |= ((0x1 << 16) | (0x1 << 0));
writel(val, GPIO0_SWPORT_DR_H_PI);
} else if (sta == LEDOFF) {
val = readl(GPIO0_SWPORT_DR_H_PI);
val &= ~(0x1 << 0);
val |= ((0x1 << 16) | (0x0 << 0));
writel(val, GPIO0_SWPORT_DR_H_PI);
}
}
void led_unmap(void) {
iounmap(PMU_GRF_GPIO0C_IOMUX_L_PI);
iounmap(PMU_GRF_GPIO0C_DS_0_PI);
iounmap(GPIO0_SWPORT_DR_H_PI);
iounmap(GPIO0_SWPORT_DDR_H_PI);
}
static int led_open(struct inode *inode, struct file *filp) {
filp->private_data = &gpioled;
return 0;
}
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) {
return 0;
}
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) {
int retValue;
unsigned char dataBuf[1];
unsigned char ledStat;
struct gpioled_dev *dev = filp->private_data;
retValue = copy_from_user(dataBuf, buf, cnt);
if (retValue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledStat = dataBuf[0];
if (ledStat == LEDON) {
gpio_set_value(dev->led_gpio, 1);
} else if (ledStat == LEDOFF) {
gpio_set_value(dev->led_gpio, 0);
}
return 0;
}
static int led_release(struct inode *inode, struct file *filp) {
return 0;
}
static struct file_operations gpioled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
static int __init led_init(void)
{
int ret = 0;
const char *str;
/* 设置LED所使用的GPIO */
/* 1、获取设备节点:gpioled */
gpioled.nd = of_find_node_by_path("/gpioled");
if(gpioled.nd == NULL) {
printk("gpioled node not find!\r\n");
return -EINVAL;
}
/* 2.读取status属性 */
ret = of_property_read_string(gpioled.nd, "status", &str);
if(ret < 0)
return -EINVAL;
if (strcmp(str, "okay"))
return -EINVAL;
/* 3、获取compatible属性值并进行匹配 */
ret = of_property_read_string(gpioled.nd, "compatible", &str);
if(ret < 0) {
printk("gpioled: Failed to get compatible property\n");
return -EINVAL;
}
if (strcmp(str, "alientek,led")) {
printk("gpioled: Compatible match failed\n");
return -EINVAL;
}
/* 4、 获取设备树中的gpio属性,得到LED所使用的LED编号 */
gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);
if(gpioled.led_gpio < 0) {
printk("can't get led-gpio");
return -EINVAL;
}
printk("led-gpio num = %d\r\n", gpioled.led_gpio);
/* 5.向gpio子系统申请使用GPIO */
ret = gpio_request(gpioled.led_gpio, "LED-GPIO");
if (ret) {
printk(KERN_ERR "gpioled: Failed to request led-gpio\n");
return ret;
}
/* 6、设置GPIO为输出,并且输出低电平,默认关闭LED灯 */
ret = gpio_direction_output(gpioled.led_gpio, 0);
if(ret < 0) {
printk("can't set gpio!\r\n");
}
/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (gpioled.major) { /* 定义了设备号 */
gpioled.devid = MKDEV(gpioled.major, 0);
ret = register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
if(ret < 0) {
pr_err("cannot register %s char driver [ret=%d]\n", GPIOLED_NAME, GPIOLED_CNT);
goto free_gpio;
}
} else { /* 没有定义设备号 */
ret = alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME); /* 申请设备号 */
if(ret < 0) {
pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", GPIOLED_NAME, ret);
goto free_gpio;
}
gpioled.major = MAJOR(gpioled.devid); /* 获取分配号的主设备号 */
gpioled.minor = MINOR(gpioled.devid); /* 获取分配号的次设备号 */
}
printk("gpioled major=%d,minor=%d\r\n",gpioled.major, gpioled.minor);
/* 2、初始化cdev */
gpioled.cdev.owner = THIS_MODULE;
cdev_init(&gpioled.cdev, &gpioled_fops);
/* 3、添加一个cdev */
cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);
if(ret < 0)
goto del_unregister;
/* 4、创建类 */
gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
if (IS_ERR(gpioled.class)) {
goto del_cdev;
}
/* 5、创建设备 */
gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);
if (IS_ERR(gpioled.device)) {
goto destroy_class;
}
return 0;
destroy_class:
class_destroy(gpioled.class);
del_cdev:
cdev_del(&gpioled.cdev);
del_unregister:
unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);
free_gpio:
gpio_free(gpioled.led_gpio);
return -EIO;
}
static void __exit led_exit(void)
{
/* 注销字符设备驱动 */
cdev_del(&gpioled.cdev);/* 删除cdev */
unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);
device_destroy(gpioled.class, gpioled.devid);/* 注销设备 */
class_destroy(gpioled.class);/* 注销类 */
gpio_free(gpioled.led_gpio); /* 释放GPIO */
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
4.3 测试驱动程序
使用Linux驱动开发——新字符设备驱动开发中的测试程序进行测试
首先insmod ko文件,会发现报错如下:
是因为GPIO已经被分配给系统原生的心跳灯驱动。
这里需要禁用
禁用掉这个设备节点。整编kernel和刷机包,刷机完成之后,可以看到心跳灯已经被禁用了。
然后测试
./ledApp /dev/gpioled 1
./ledApp /dev/gpioled 0
能够看到led亮灭
原文地址:https://blog.csdn.net/weixin_49274713/article/details/143068173
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!