当前位置:   article > 正文

100ask七天物联网训练营学习笔记 - 裸机程序框架设计

100ask

1. 前言

报名参加了100ask组织的七天物联网智能家居训练营,每天早上2个小时讲基础,下午2个小时讲进阶。通过两天的学习确实感到比较充实。下面几天我会陆续把每节课程的重点、难点以及自己的理解记录下来。

2. 理论

2.1 抛出问题

这节课韦老师以程序框架设计作为切入点开始了课程,以嵌入式裸机开发为例,很多初学者通常会在业务层代码甚至是main函数中,直接调用用于描述或硬件的接口(如HAL库):

void main(void)
{
    GPIO_PinState key;
    while (1)
    {
        key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
        if (key == GPIO_PIN_RESET)
            HAL_GPIO_WritePin(GPIOF, GPIO_PIN_5, GPIO_PIN_RESET);
        else
            HAL_GPIO_WritePin(GPIOF, GPIO_PIN_5, GPIO_PIN_SET);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这会造成业务层代码与板级代码严重耦合,对后续的软件功能扩展、硬件升级及代码复用都会产生不便,同时也会对不懂硬件的业务层开发人员造成障碍。

为了解决这一问题,我们将程序结构进行分层,将业务逻辑与硬件驱动代码分离:

// main.c
void main(void)
{
    int key;
    while (1)
    {
        key = read_key();
        if (key == UP)
            led_on();
        else
            led_off();
    }
}

// key.c
int read_key(void)
{
    GPIO_PinState key;
    key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
    if (key == GPIO_PIN_RESET)
        return 0;
    else
        return 1;
}

// led.c
void led_on(void)
{
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_5, GPIO_PIN_RESET);
}

void led_off(void)
{
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_5, GPIO_PIN_SET);
}
  • 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

这样解决了业务逻辑代码与硬件驱动代码的耦合问题,但还有两个问题没有解决:

一、因硬件版本迭代导致的软件兼容性问题

二、功能扩展性问题

2.2 引入函数指针

这里我们要解决第一个问题,通常有3种方法:

  1. 宏开关

    #define HARDWARE_VER  1
    
    // key.c
    // 返回值: 0表示被按下, 1表示被松开
    int read_key(void)
    {
        GPIO_PinState key;
    #if (HARDWARE_VER == 1)
        key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
    #else
        key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);
    #endif
        if (key == GPIO_PIN_RESET)
            return 0;
        else
            return 1;            
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    宏开关如果一多,维护起来将是灾难。

  2. EEPROM中保存硬件版本号,根据版本号调用硬件版本差异接口

    // key.c
    // 返回值: 0表示被按下, 1表示被松开
    int read_key(void)
    {
        GPIO_PinState key;
        int ver = read_hardware_ver();
        
    	if (ver == 1)
    	    key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
    	else (ver == 2)
    	    key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);
    
        if (key == GPIO_PIN_RESET)
            return 0;
        else
            return 1;            
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    与宏开关类似,数量一多,维护起来很困难。

  3. 函数指针

    // key.c
    int (*read_key)(void);
    
    // 返回值: 0表示被按下, 1表示被松开
    int read_key_ver1(void)
    {
        GPIO_PinState key;
        key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
        if (key == GPIO_PIN_RESET)
            return 0;
        else
            return 1;            
    }
    
    // 返回值: 0表示被按下, 1表示被松开
    int read_key_ver2(void)
    {
        GPIO_PinState key;
        key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
        if (key == GPIO_PIN_RESET)
            return 0;
        else
            return 1;            
    }
    
    void key_init()
    {
        int ver = read_hardware_ver();
        if (ver == 1)
            read_key = read_key_ver1;
        else
            read_key = read_key_ver2;
    }
    
    // main.c
    void main(void)
    {
        int key;
        
        key_init();
        
        while (1)
        {
            key = read_key();
            if (key == UP)
                led_on();
            else
                led_off();
        }
    }
    
    • 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

我觉得这种方法应该属于第2种方法的升级版,同样需要将硬件版本号写入EEPROM中,在软件中进行判断,但是区别在于引入函数指针后,只需要在上电初始化过程中根据版本号判断一次,将版本对应接口赋值给指针,不需要在后续的代码中进行大量的判断调用。

这样第一个问题就得到解决,我们来看第二个问题,如何解决软件扩展性问题?

设计模式中有一个设计原则:OCP,开闭原则,大致意思是好的设计需要对扩展开放,对修改关闭。用人话说就是做功能扩展时只新增代码,不对已有代码做修改。

回到问题,如果我们需要增加按键的数量,按我们以前的思路应该是:

// key.c
// 返回值: 0表示被按下, 1表示被松开
int read_key1(void)
{
    GPIO_PinState key;
    key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
    if (key == GPIO_PIN_RESET)
        return 0;
    else
        return 1;
}

// 返回值: 0表示被按下, 1表示被松开
int read_key2(void)
{
    GPIO_PinState key;
    key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);
    if (key == GPIO_PIN_RESET)
        return 1;
    else
        return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

// key.c
// 返回值: 0表示被按下, 1表示被松开
int read_key(int which)
{
    GPIO_PinState key;
    switch (which)
    {
        case 0:
			key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
            if (key == GPIO_PIN_RESET)
                return 0;
            else
                return 1;
            break;
            
        case 1:
			key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);
            if (key == GPIO_PIN_RESET)
                return 1;
            else
                return 0;
            break;
            
    }
}
  • 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

这两种方式都能一定程度上缓解问题,但是治标不治本。前一种方法会随着按键数量的增多,在调用处越来越混乱,难以维护。后一种方法,只要有按键新增就会修改我们的read_key函数,违反OCP原则。

2.3 引入结构体

为了用新的思路解决问题,这里就引入了新的知识点:结构体。

请添加图片描述

我们先将程序分层,main函数属于应用层或业务逻辑层,key_manager属于中间层,最下面属于硬件驱动层,通过中间层来实现对按键的管理,同时将业务逻辑层与驱动层解耦。

2.3.1 key_system
// key_manager.h
typedef struct key {
	char *name;
    void (*init)(struct key *k);
    int (*read)(void);
}key, *p_key;

// 所有按键的初始化
void key_init(void);

// 根据按键name获取按键
key *get_key(char *name);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
// key_manager.c
int key_cnt = 0;
key *keys[32];

void register_key(key *k)
{
	keys[key_cnt] = k;
	key_cnt++;
}

void key_init(void)
{
	k1_init();
	k2_init();
}

key *get_key(char *name)
{
	int i = 0; 
	for (i = 0; i < key_cnt; i++)
		if (strcmp(name, keys[i]->name) == 0)
			return keys[i];

	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
// key1.c
// 返回值: 0表示被按下, 1表示被松开
static int read_key1(void)
{
    GPIO_PinState key_status;
    key_status = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
    if (key_status == GPIO_PIN_RESET)
        return 0;
    else
        return 1;
}

static key k1 = {"k1", 0, read_key1};

void k1_init(void)
{
	register_key(&k1);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
// key2.c
// 返回值: 0表示被按下, 1表示被松开
static int read_key2(void)
{
    GPIO_PinState key_status;
    key_status = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);
    if (key_status == GPIO_PIN_RESET)
        return 1;
    else
        return 0;
}

static key k2 = {"k2", NULL, read_key2};

void k2_init(void)
{
	register_key(&k2);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
// main.c
void main(void)
{
	key *k;
	
	key_init();	

	/* 使用某个按键 */
	k = get_key("k1");
	if (k == NULL)
		return;

	while (1)
	{
		if (k->read(k) == 0)
			led_on();
		else
			led_off();
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
2.3.2 key_system_read_multi_key

目前的代码还有一些问题,没有将业务层与驱动层解耦,在main函数中还有具体按键read函数的调用及状态判断。同时,作为业务层期望中间层可以同时读取所有按键的状态。

再对中间层实现进行优化:

// key_manager.h
typedef struct key {
	char *name;
    unsigned char id;
    void (*init)(struct key *k);
    int (*read)(void);
}key, *p_key;

#define KEY_UP		0xA
#define KEY_DOWN 	0xB

// 所有按键的初始化
void key_init(void);

// 读取所有按键的状态
int read_key(void)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
// key_manager.c
int key_cnt = 0;
key *keys[32];

void register_key(key *k)
{
	keys[key_cnt] = k;
	key_cnt++;
}

void key_init(void)
{
	k1_init();
	k2_init();
}

int read_key(void)
{
    int val;
	for (int i = 0; i < key_cnt; i++)
    {
        val = keys[i]->read();
        if (val == -1)
            continue;
        else
            return val;
    }
    return -1;
}
  • 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
// key1.c
// 返回值: 0表示被按下, 1表示被松开
#define KEY1_ID		1
static int read_key1(void)
{
    static GPIO_PinState pre_key_status;
    GPIO_PinState key_status;
    key_status = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
    
    if (key_status == pre_key_status)
        return -1;
    pre_key_status = key_status;
    if (key_status == GPIO_PIN_RESET)
        return KEY_DOWN | (KEY1_ID << 8);
    else
        return KEY_UP | (KEY1_ID << 8);
}

static key k1 = {"k1", KEY1_ID, NULL, read_key1};

void k1_init(void)
{
	register_key(&k1);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
// key2.c
// 返回值: 0表示被按下, 1表示被松开
#define KEY2_ID		2
static int read_key1(void)
{
    static GPIO_PinState pre_key_status;
    GPIO_PinState key_status;
    key_status = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);
    
    if (key_status == pre_key_status)
        return -1;
    pre_key_status = key_status;
    if (key_status == GPIO_PIN_RESET)
        return KEY_UP | (KEY2_ID << 8);
    else
        return KEY_DOWN | (KEY2_ID << 8);
}

static key k2 = {"k2", KEY2_ID, NULL, read_key2};

void k2_init(void)
{
	register_key(&k2);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
// main.c
void main(void)
{
	int val;
	
	key_init();	

	while (1)
	{
		val = read_key();

		if (val == -1)
		{
			/* 没有按键 */
		}
		else
		{
            key_status = val & 0xFF;
            key_id = (val>>8) & 0xFF:
			switch (key_status)
			{
				case KEY_UP: 
                    /* key_id 松开 */
					break;
				case KEY_DOWN: 
                    /* key_id 按下 */
					break;
                default:
                    break;
            }
		}
	}
}
  • 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
2.3.3 key_system_read_usr_irq

但是,这个代码还是有一些问题,比如现在按键的检测属于轮询,若可以将按键检测修改为中断方式,在使用RTOS时就不需要定时进行轮询了,完全可以等待中断来触发。

此时,可以引入一个fifo,中断事件作为数据的生产者,应用层作为数据的消费者,进一步解耦。

这里fifo实现是我从github上随便找的。

// key_manager.h
typedef struct key {
	char *name;
    unsigned char id;
    void (*init)(struct key *k);
    int (*read)(void);
}key, *p_key;

#define KEY_UP		0xA
#define KEY_DOWN 	0xB

// 所有按键的初始化
void key_init(void);

// 读取按键状态
int read_key(void)

// 向fifo写入一个按键状态
void put_buffer(int val);

// 从fifo读出一个按键状态
int read_buffer(void);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
// key_manager.c
int key_cnt = 0;
key *keys[32];

// 定义一个Fifo缓冲器
static RingBufferPointer fifo;

void put_buffer(int val)
{
    ringBufferAdd(fifo, val);
}

int read_buffer()
{
    int val = -1;
    if (isRingBufferNotEmpty(fifo))
        val = ringBufferGet(fifo);
    return val;
}

void register_key(key *k)
{
	keys[key_cnt] = k;
	key_cnt++;
}

void key_init(void)
{
    fifo = getRingBufferInstance(100);
	k1_init();
	k2_init();
}

int read_key(void)
{
	return read_buffer();
}
  • 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
// key0.c
#define KEY0_ID		0

static key k0 = {"k0", KEY0_ID, NULL, NULL};

void k0_init(void)
{
	register_key(&k0);
}

void key0_irq(void)
{
    int val;
    GPIO_PinState key_status;
    key_status = HAL_GPIO_ReadPin(KEY0_GPIO_Port, KEY0_Pin);
    
    if (key_status == GPIO_PIN_RESET)
        val = KEY_DOWN | (KEY0_ID << 8);
    else
        val = KEY_UP | (KEY0_ID << 8);
    put_buffer(val);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
// key1.c
#define KEY1_ID		1

static key k1 = {"k1", KEY1_ID, NULL, read_key1};

void k1_init(void)
{
	register_key(&k1);
}

void key1_irq(void)
{
    int val;
    GPIO_PinState key_status;
    key_status = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
    
    if (key_status == GPIO_PIN_RESET)
        val = KEY_DOWN | (KEY1_ID << 8);
    else
        val = KEY_UP | (KEY1_ID << 8);
    put_buffer(val);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
// key2.c
#define KEY2_ID		2

static key k2 = {"k2", KEY2_ID, NULL, read_key2};

void k2_init(void)
{
	register_key(&k2);
}

void key2_irq(void)
{
    int val;
    GPIO_PinState key_status;
    key_status = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);
    
    if (key_status == GPIO_PIN_RESET)
        val = KEY_DOWN | (KEY2_ID << 8);
    else
        val = KEY_UP | (KEY2_ID << 8);
    put_buffer(val);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
// stm32f7xx_it.c
void EXTI2_IRQHandler(void)
{
  HAL_GPIO_EXTI_IRQHandler(KEY1_Pin);
}

void EXTI3_IRQHandler(void)
{
  HAL_GPIO_EXTI_IRQHandler(KEY0_Pin);
}

void EXTI15_10_IRQHandler(void)
{
  HAL_GPIO_EXTI_IRQHandler(KEY2_Pin);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
// main.c
void main(void)
{
    int val;
    char key_status;
    char key_id;
	
	key_init();	

    // 如果是rtos就可以不适用轮询方式
	while (1)
	{
        val = read_key();
        if (val != -1)
        {
            key_status = val & 0xFF;
            key_id = (val >> 8) & 0xFF;
            switch (key_status)
            {
                case KEY_DOWN:
                    if (key_id == 0)
                    {
                        HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
                    }
                    else if (key_id == 1)
                    {
                        HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
                    }
                    else if (key_id == 2)
                    {                
                        HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET);
                        HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);
                    }
                    break;
                case KEY_UP:
                    break;
            }
        }
	}
}

void HAL_GPIO_EXTI_Callback(uint16_t pin)
{
    HAL_Delay(50);
    switch (pin)
    {
        case KEY0_Pin:
            key0_irq();
            break;
        case KEY1_Pin:
            key1_irq();
            break;
        case KEY2_Pin:
            key2_irq();
            break;
        default:
            break;
    }
}
  • 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

3. 实验过程

根据上面的思路,在开发板上实践一下。

3.1 创建cubemx工程

请添加图片描述

经过查看该开发板原理图得知,资源分布如下:

  • 开发板共有用户按键3个,分别是KEY0(PH3)、KEY1(PH2)、KEY2(PC13)
  • 开发板共有LED2个,分别是LED0(PB1)、LED1(PB0)

请添加图片描述

对这5个GPIO进行配置,然后生成代码。

3.2 项目结构

请添加图片描述

3.3 实验效果

按下KEY0,LED0亮起,按下KEY1,LED1亮起,按下KEY2,LED0、LED1同时熄灭。实验完成。

4. 总结

实际按照韦老师的课程中,将led也建立了子系统,通过中间层来管理led,以降低代码的耦合,增强代码的扩展性、复用性。由于思路和key子系统基本一样,就再没有进一步实现。

通过这节课学习了软件结构分层设计的思路,从设计之初就考虑未来的功能扩展、代码的健壮。通过牺牲一点点运行效率,来提高整个工程的可维护性。

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

闽ICP备14008679号