当前位置:   article > 正文

RK3399中LED的驱动开发_rk3399 gpio驱动

rk3399 gpio驱动

RK3399中LED的驱动开发

一、硬件环境介绍

1.1 设备介绍

本次Linux驱动开发所使用的设备为LPKT030试验箱,核心处理芯片为瑞芯微的RK3399,下图中运行的是基于RK3399平台设计的Linux Qt系统,具体的其他参数就不打广告了,各位感兴趣可以去公司官网进行查看:产品详情
在这里插入图片描述

1.2 硬件原理图

本次进行演示开发的是对LPKT030试验箱左侧嵌入式主板中左下角的LED灯的驱动开发,下图是本次要进行驱动开发的LED灯硬件电路原理图。

在这里插入图片描述
从原理图可以看到,试验箱左下角一共有4个LED灯,分别连接到 GPIO0_A2,GPIO2_A2,GPIO2_A3,GPIO2_A4 上。从原理图中可以了解到IO口输出高电平时LED灯点亮,低电平LED灯熄灭。

二、驱动开发流程

首先呢,给各位看一下本次驱动开发的整体思维导图,虽然庞大,看起来很难,但是实际一点也不简单!
在这里插入图片描述

接下来还是逐个分析,从1.2小节的原理图来看,LED灯实现亮灭的原理就是控制IO口进行高低电平的输出即可。

既然知道了原理,接下来要解决的就是如何实现对应的IO口输出高低电平。受过单片机毒打的小伙伴都知道一旦涉及到GPIO这四个字母就离不开一个让人又恨又恨(主打没有爱)的东西——寄存器!!!要让对应的IO口输出对应需要的电平,就需要对寄存器进行一系列的配置。

2.1 涉及点亮LED灯相关寄存器

在这里插入图片描述

光是看到这些寄存器的名字就不是很想去打交道,更何况地址还是一串你根本记不住(有大佬记得住当小弟没说)的16进制数。

2.2 GPIO操作流程

接下来,我们就对电路原理图中圈出来的GPIO0_A2作为例子并结合对应的寄存器简单说一下GPIO的操作流程。

  1. 使能GPIO0:PMUCRU_CLKGATE_CON1寄存器的b[19]设置为1,b[3]设置为 0
  2. 设置GPIO0_A2用于GPIO:PMUGRF_GPIO0A_IOMUX寄存器b[21:20]设置为0b11,b[5:4]设置为0b00
  3. 设置GPIO_A2为OUTPUT:GPIO_SWPORTA_DDR寄存器 b[3]设置为 1
  4. 设置GPIO_A2输出高低电平:GPIO_SWPORTA_DR寄存器 b[3]设置为 1输出高电平,设置为0输出低电平

如果你是一位汇编大佬,此时你已经可以直接手lu一端效率贼高、贼简洁的汇编代码直接操作寄存器开始实现LED灯点亮了,只不过现在要在Linux环境下进行驱动开发,因此需要遵循Linux的驱动框架。
至于Linux的驱动框架这里就不多赘述了,并且采用汇编语言进行代码编写,虽然非常高效且简洁,但对于没有汇编基础的人进行阅读会非常不友好,因此在这里就采用Linux设备树以及C语言进行LED驱动的开发,后面是整个驱动开发的详细步骤。

三、修改设备树文件

按照上面的一系列分析,进行驱动开发的本质就是配置寄存器,那第一步就是要确定我们需要配置哪些寄存器,寄存器的物理地址是什么,很显然现在我们已经知道了寄存器要哪些以及对应的寄存器物理地址,接下来就是向Linux内核传递这些寄存器的地址,现在采用的是设备树的方式将寄存器物理地址传递给内核。

相关代码:

xuelitecled { 
		#address-cells = <1>; 
		#size-cells = <1>; 
		compatible = "xuelitec-led"; 
		status = "okay"; 
		reg = < 0xFF750104 0x04 		/* PMUCRU_CLKGATE_CON1_BASE  */ 
				0xFF310000 0x04    		/* PMUGRF_GPIO0A_IOMUX_BASE  */ 
				0xFF720000 0x04   		/* GPIO0_SWPORTA_DR_BASE     */ 
				0xFF720004 0x04 >;  	/* GPIO0_SWPORTA_DDR_BASE    */ 
	}; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • #address-cells 和#size-cells 都为1,表示reg属性中起始地址占用一个字长,同时地址长度也占用一个字节。

  • compatbile属性是设置 xuelitec 节点兼容性为“xuelitec-led”

  • status属性是设置状态可用

  • reg属性中设置的就是相关寄存器的物理地址以及地址长度

修改完成设备树之后,重新对SDK进行编译并使用AndroidTool工具烧录到设备中。

完成烧录后使用终端进入到/proc/device-tree/目录下,查看是否存在xuelitecled这个节点。

在这里插入图片描述

到这,整个Linux设备树LED驱动开发的第一步就已经完成了。

四、编写LED驱动文件

接下来就是对LED驱动的主体文件进行编写,这里是整个驱动开发的核心部分,在这一共分为四个环节,分别是创建dtsled设备结构体、编写LED具体操作函数、LED设备的初始化以及注销LED设备。

4.1 创建dtsled设备结构体

创建dtsled设备结构体是为了方便设备的相关信息,主要是为了存储设备号、cdev、类、设备、主设备号、此设备号以及设备节点这些。

相关的结构体代码如下:

/* dtsled设备结构体 */
struct dtsled_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
	struct device_node	*nd; /* 设备节点 */
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

后续使用只需要创建一个结构体变量即可。

4.2 具体操作函数

4.2.1 LED灯开关功能函数
/*
 * @description		: LED打开/关闭
 * @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return 			: 无
 */
void led_switch(u8 sta)
{
	u32 val = 0;
	if(sta == LEDON) {
		val = readl(GPIO0_SWPORTA_DR);
		val |= (1<<2);	
		writel(val, GPIO0_SWPORTA_DR);
	}else if(sta == LEDOFF) {
		val = readl(GPIO0_SWPORTA_DR);
		val &= ~(1<<2); 	
		writel(val, GPIO0_SWPORTA_DR);
	}	
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

这一段功能函数的主要功能为对GPIO0_A2输出高低电平进行设置,但无法直接通过这个函数进行LED的开关控制,还需要依托下方的向设备写入数据,才能将GPIO0_A2引脚的控制写入设备最终实现效果。

4.2.2 打开设备
/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &dtsled; /* 设置私有数据 */
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

打开设备功能函数主要做的就是三件事!

  1. 传递元信息incode
  2. 将之前创建的dtsled结构体变量设置为私有数据
  3. 默认返回0,表示设备打开成功
4.2.3 从设备中读取数据

因为当前做的是LED灯控制的驱动实验,主要涉及的是对设备的控制,即数据写入,因此对于数据的读取这里只是创建了一个函数保证整个代码功能的完整性,但在函数中并未做任何操作。后续需要从设备中读取数据的时候,在这个函数中写入对应操作即可。

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
4.2.4 向设备写入数据

向设备写入数据是本次驱动开发的核心部分,对于LED的控制主要都是通过这个功能函数实现。

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
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;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}

	ledstat = databuf[0];		/* 获取状态值 */

	if(ledstat == LEDON) {	
		led_switch(LEDON);		/* 打开LED灯 */
	} else if(ledstat == LEDOFF) {
		led_switch(LEDOFF);	/* 关闭LED灯 */
	}
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
4.2.5 释放设备

和从设备中读取数据一样,这里只是为了保证代码功能的完整性放置的一个空函数,后续关闭其他设备需要额外一些操作的时候可以写在这个函数中,在这里就默认返回一个0,表示释放设备成功。

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
4.2.6 设备操作函数结构体

最后,为了方便设备操作函数的使用,创建一个结构体用于存放对应的操作函数。

/* 设备操作函数 */
static struct file_operations dtsled_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = 	led_release,
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

4.3 LED灯初始化

玩过单片机(被单片机玩过也算)的小伙伴都知道,正式使用一个硬件设备之前,第一步要做的就是对硬件设备进行初始化操作。

在使用设备树进行Linux驱动开发中,初始化设备主要分为三个步骤:

  1. 从设备树中获取对应设备的属性数据

    在本次LED驱动开发中,要获取的数据为:设备节点、compatible属性、status属性以及reg属性

  2. 对硬件设备的具体初始化

    在本次LED驱动开发中,具体初始化工作如下:

    • 映射寄存器地址
    • 使能LED所在的GPIO组——GPIO0
    • 对控制LED的GPIO0_A2进行相应配置(设置其为GPIO且是输出引脚)
    • 设置LED灯的默认状态
  3. 注册设备驱动

    注册设备驱动主要工作如下:

    • 创建设备号
    • 初始化cdev
    • 创建类
    • 创建设备

具体代码如下:

static int __init led_init(void)
{
	u32 val = 0;
	int ret;
	u32 regdata[10];
	const char *str;
	struct property *proper;

	/* 获取设备树中的属性数据 */
	/* 1、获取设备节点:xuelitecled */
	dtsled.nd = of_find_node_by_path("/xuelitecled");
	if(dtsled.nd == NULL) {
		printk("xuelitecled node nost find!\r\n");
		return -EINVAL;
	} else {
		printk("xuelitecled node find!\r\n");
	}

	/* 2、获取compatible属性内容 */
	proper = of_find_property(dtsled.nd, "compatible", NULL);
	if(proper == NULL) {
		printk("compatible property find failed\r\n");
	} else {
		printk("compatible = %s\r\n", (char*)proper->value);
	}

	/* 3、获取status属性内容 */
	ret = of_property_read_string(dtsled.nd, "status", &str);
	if(ret < 0){
		printk("status read failed!\r\n");
	} else {
		printk("status = %s\r\n",str);
	}

	/* 4、获取reg属性内容 */
	ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 8);
	if(ret < 0) {
		printk("reg property read failed!\r\n");
	} else {
		u8 i = 0;
		printk("reg data:\r\n");
		for(i = 0; i < 8; i++)
			printk("%#X ", regdata[i]);
		printk("\r\n");
	}	

	/* 初始化LED */
	/* 1、寄存器地址映射 */
	PMUCRU_CLKGATE_CON1 = ioremap(regdata[0], regdata[1]);
	PMUGRF_GPIO0A_IOMUX = ioremap(regdata[2], regdata[3]);
  	GPIO0_SWPORTA_DR = ioremap(regdata[4], regdata[5]);
	GPIO0_SWPORTA_DDR = ioremap(regdata[6], regdata[7]);

	/* 2. 使能GPIO0 
	 * set CRU to enable GPIO0 
	 * PMUCRU_CLKGATE_CON1 0xFF760000 + 0x0104 
	 * (1<<(3+16)) | (0<<3) 
	 */
	val = readl(PMUCRU_CLKGATE_CON1);
	val |= (1<<(3+16)) | (0<<3);
	writel(val, PMUCRU_CLKGATE_CON1);

	/* 3. 设置GPIO0_A2 用于 GPIO 
	 * set PMU/GRF to configure GPIO0_A2 as GPIO 
	 * PMUGRF_GPIO0A_IOMUX  0xFF310000 + 0x00000 
	 * bit[5:4] = 0b00 
	 * (3<<(4+16)) | (0<<4) 
	 */ 
	val = readl(PMUGRF_GPIO0A_IOMUX);
	val |= (3<<(4+16)) | (0<<4);
	writel(val, PMUGRF_GPIO0A_IOMUX);
	
	/* 4. 设置GPIO0_A2 作为 output 引脚 
	 * set GPIO_SWPORTA_DDR to configure GPIO0_A2 as output 
	 * GPIO_SWPORTA_DDR 0xFF720000 + 0x0004 
	 * bit[3] = 0b1 
	 */ 
	val = readl(GPIO0_SWPORTA_DDR);
	val |= (1<<2);
	writel(val, GPIO0_SWPORTA_DDR);

	/* 5、默认关闭LED */
	val = readl(GPIO0_SWPORTA_DR);
	val &= ~(1<<2); 	
	writel(val, GPIO0_SWPORTA_DR);
	
	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (dtsled.major) {		/*  定义了设备号 */
		dtsled.devid = MKDEV(dtsled.major, 0);
		register_chrdev_region(dtsled.devid, DTSLED_CNT, DTSLED_NAME);
	} else {						/* 没有定义设备号 */
		alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DTSLED_NAME);	/* 申请设备号 */
		dtsled.major = MAJOR(dtsled.devid);	/* 获取分配号的主设备号 */
		dtsled.minor = MINOR(dtsled.devid);	/* 获取分配号的次设备号 */
	}
	printk("dtsled major=%d,minor=%d\r\n",dtsled.major, dtsled.minor);	
	
	/* 2、初始化cdev */
	dtsled.cdev.owner = THIS_MODULE;
	cdev_init(&dtsled.cdev, &dtsled_fops);
	
	/* 3、添加一个cdev */
	cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);

	/* 4、创建类 */
	dtsled.class = class_create(THIS_MODULE, DTSLED_NAME);
	if (IS_ERR(dtsled.class)) {
		return PTR_ERR(dtsled.class);
	}

	/* 5、创建设备 */
	dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME);
	if (IS_ERR(dtsled.device)) {
		return PTR_ERR(dtsled.device);
	}
	
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119

4.4 注销LED设备

这一部分呢基本上是初始化LED灯函数的方向操作,初始化的时候需要映射寄存器地址,那先不用了就需要取消映射。在初始化的时候需要注册LED设备驱动,在这里就需要注销LED设备驱动,删除cdev,注销设备号。

static void __exit led_exit(void)
{
	/* 取消映射 */
	iounmap(PMUCRU_CLKGATE_CON1);
	iounmap(PMUGRF_GPIO0A_IOMUX);
	iounmap(GPIO0_SWPORTA_DR);
	iounmap(GPIO0_SWPORTA_DDR);

	/* 注销字符设备驱动 */
	cdev_del(&dtsled.cdev);/*  删除cdev */
	unregister_chrdev_region(dtsled.devid, DTSLED_CNT); /* 注销设备号 */

	device_destroy(dtsled.class, dtsled.devid);
	class_destroy(dtsled.class);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这样,整个LED的驱动文件的主体内容基本上就完成了,但这只是一个驱动功能文件,没有main函数是没法直接运行的,所以还需要写一个测试App程序来使用这个驱动文件。

五、编写测试程序

测试程序就没什么好需要特别交代的了,就是对驱动的使用,具体代码如下:

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

#define LEDOFF 	0
#define LEDON 	1

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	unsigned char databuf[1];
	
	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	/* 打开led驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

	databuf[0] = atoi(argv[2]);	/* 要执行的操作:打开或关闭 */

	/* 向/dev/led文件写入数据 */
	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
		printf("LED Control Failed!\r\n");
		close(fd);
		return -1;
	}

	retvalue = close(fd); /* 关闭文件 */
	if(retvalue < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

六、编译运行测试

在完成所有的驱动程序和测试程序的编写后,接下来就是要编译生成可执行文件烧录到设备中进验证了。

这个环境分为两个部分进行:

  1. 编译生成
  2. 烧录运行

首先是编译生成,将驱动文件和测试文件进行编译,检查是否存在错误(是不能一次成功哒!肯定有错误哒!)并生成最终的驱动模块文件和可执行文件。

首先是对驱动程序进行编译,这里就要用到神奇的Makefile了,具体的Makefile代码如下:

KERNELDIR := /home/xuelitec/linux/src/linuxsdk-xuelitec/kernel
CURRENT_PATH := $(shell pwd)

obj-m := dtsled.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

编写好Makefile文件后,放置在驱动文件同一路径下,在终端通过make命令就可以进行编译然后生成后缀为.ko的驱动模块文件。

而测试程序编译就一个文件,就gcc直接编译了。

aarch64-linux-gnu-gcc ledApp.c -o ledApp
  • 1

最后生成如下两个文件:

在这里插入图片描述

最后就是烧录运行了,首先要通过一些神奇的妙妙工具(有线无线都可以哦)将这两个文件丢到搭载RK3399的设备中,接下来就是最终的三个步骤:

  1. 赋予ledApp可执行权限

    这个so easy就一句话:

    chmod +x ledApp
    
    • 1
  2. 加载驱动

    这个也是一句

    insmod dtsled.ko
    
    • 1

    回车后会有如下输出:
    在这里插入图片描述

    最后可以通过“lsmod”命令查看查看驱动是否成功加载:
    在这里插入图片描述

  3. 控制命令测试

    最后,就是通过命令来控制LED的开关了,对应的命令如下:

    ./ledApp /dev/dtsled 1  //打开 LED 灯
    ./ledApp /dev/dtsled 0  //关闭 LED 灯
    
    • 1
    • 2

    此时就是见证奇迹的时刻,当然了这里没有录视频,奇迹时刻就我自己看看啦,自己动手做出来的奇迹才是最有成就感的,各位可以自己亲自上手试一试!

  4. 卸磨杀驴

    最后,验证玩所有的效果了,那就要卸磨杀驴,将驱动卸载了。

    卸载命令:

    rmmod dtsled.ko
    
    • 1

    回车后再lsmod就看不到dtsled这个驱动了(抱歉,渣了:))

在这里插入图片描述

OK,艾瑞巴蒂,到这里就结束了,再往下也没了,各位,你们可以亲手试一试,相信你们可以完成的!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/870700
推荐阅读
相关标签
  

闽ICP备14008679号