当前位置:   article > 正文

第八章-PID 速度控制 PID控制 PID调参 PID温度控制 蓝桥杯 单片机 串级PID 模糊PID STM32f103c8t6最小系统板 STM32项目 STM32cubemx正点原子 江科大协_pid中rol速率

pid中rol速率

第八章-PID-速度控制

8.1-速度控制探索

前面我们已经能够通过编码器测量出速度值,下面我们来控制速度

我们先编写一个简单的控制方法

要求:讲转速控制再2.9-3.1转每秒

可以把中断里面不重要的输出注释掉
在这里插入图片描述

	if(Motor1Speed>3.1) Motor1Pwm--;
	if(Motor1Speed<2.9) Motor1Pwm++;
	if(Motor2Speed>3.1) Motor2Pwm--;
	if(Motor2Speed<2.9) Motor2Pwm++;
	Motor_Set(Motor1Pwm,Motor2Pwm);
	printf("Motor1Speed:%.2f Motor1Pwm:%d\r\n",Motor1Speed,Motor1Pwm);
	printf("Motor2Speed:%.2f Motor2Pwm:%d\r\n",Motor2Speed,Motor2Pwm);
	
	HAL_Delay(100);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

开始实验
在这里插入图片描述
现象就开始电机没有到达3转每秒,PWM占空比逐渐增大,电机逐渐达到要求转速、到达要求转速后我们增加阻力,电机变慢,阻力大小不边PWM占空比逐渐更大转速逐渐更大

这样我们就把转速控制到我们想要的范围,但是我们并不满意、能够看出来控制的速度很慢,给电机一些阻力电机至少要2-3秒能够调整过来,这在一些场景是不允许的。

我们理想的控制效果是:在电机转速很慢的是时候能快速调整,在电机一直转的不能达到要求时候能够更快速度调整

8.2-准备工作-匿名上位机曲线显示速度波形方便观察数据

为了方便观察电机速度数据,我们通过上位机曲线显示一下。

这里我们使用的上位机是匿名上位机-大佬写的非常稳定功能也很多

我使用的版本是:匿名上位机V7.2.2.8版本推荐大家和我使用一样
匿名上位机官方下载链接:https://www.anotc.com/wiki/%E5%8C%BF%E5%90%8D%E4%BA%A7%E5%93%81%E8%B5%84%E6%96%99/%E8%B5%84%E6%96%99%E4%B8%8B%E8%BD%BD%E9%93%BE%E6%8E%A5%E6%B1%87%E6%80%BB
在这里插入图片描述
我们要把STM32数据发送到匿名上位机,就要满足匿名上位机的数据协议要求

在匿名上位机资料下载链接,可以下载到协议介绍

  • 匿名上位机V7通信协议,20210528发布:https://pan.baidu.com/s/1nGrIGWj6qr9DWOcGpKR51g 提取码:z8d1

  • CSDN 慕羽★大佬写的协议解析教程博客:https://blog.csdn.net/qq_44339029/article/details/106004997
    1.先补充一下大小端模式
    这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如和将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。

  • 所谓的大端模式(BE big-endian),是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中(低对高,高对低);

  • 所谓的小端模式(LE little-endian),是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中(低对低,高对高)。
    常见的单片机大小端模式:(1)KEIL C51中,变量都是大端模式的,而KEIL MDK中,变量是小端模式的。(2)SDCC-C51是小端寻址,AVRGCC 小端寻址.(3)PC小端,大部分ARM是小端 (4)总起来说51单片机一般是大端模式,32单片机一般是小端模式.
    在这里插入图片描述
    2.看一下上位机要求的协议
    在这里插入图片描述
    灵活格式帧(用户自定义帧)
    在这里插入图片描述
    前面我们好理解

0xAA:一个字节表示开始

0xFF:一个字节表示目标地址

0xF1:一个字节表示发送功能码

1-40:一个字节表示数据长度

数据内容有多个字节如何发送

因为串口每次发送一个字节,但是数据可能是int16_t 16位的数据,或者int32_t 32位数据,每次发送16位数据,先发送数据低八位,还是先发送数据高八位那?

匿名协议通信介绍给出:DATA 数据内容中的数据,采用小端模式传送,低字节在前,高字节在后。

那么就要求,比如我们在发送16位数据0x2314我们要先发送低字节0x14,然后发送高字节0x23

那么如何解析出低字节或者高字节,就需要知道多字节数据在单片机里面是怎么存的,因为STM32是小端存储,所以低字节就在低位地址中,高字节高位地址中。

如果使用32单片机 小端模式,0x23高地址,0x14在低地址,所以我们要先发低地址,再发高地址。

下面就是对16位数据,或者32位数据的拆分

//需要发送16位,32位数据,对数据拆分,之后每次发送单个字节
//拆分过程:对变量dwTemp 去地址然后将其转化成char类型指针,最后再取出指针所指向的内容
#define BYTE0(dwTemp)  (*(char *)(&dwTemp))
#define BYTE1(dwTemp)  (*((char *)(&dwTemp) + 1))
#define BYTE2(dwTemp)  (*((char *)(&dwTemp) + 2))
#define BYTE3(dwTemp)  (*((char *)(&dwTemp) + 3))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

拆分后我们按照协议要求发送数据就可以了
在这里插入图片描述
niming.c

#include "niming.h"
#include "main.h"
#include "usart.h"
uint8_t data_to_send[100];

//通过F1帧发送4个uint16类型的数据
void ANO_DT_Send_F1(uint16_t _a, uint16_t _b, uint16_t _c, uint16_t _d)
{
    uint8_t _cnt = 0;		//计数值
    uint8_t sumcheck = 0;  //和校验
    uint8_t addcheck = 0; //附加和校验
    uint8_t i = 0;
	data_to_send[_cnt++] = 0xAA;//帧头
    data_to_send[_cnt++] = 0xFF;//目标地址
    data_to_send[_cnt++] = 0xF1;//功能码
    data_to_send[_cnt++] = 8; //数据长度
	//单片机为小端模式-低地址存放低位数据,匿名上位机要求先发低位数据,所以先发低地址
	data_to_send[_cnt++] = BYTE0(_a);       
    data_to_send[_cnt++] = BYTE1(_a);
	
    data_to_send[_cnt++] = BYTE0(_b);
    data_to_send[_cnt++] = BYTE1(_b);
	
    data_to_send[_cnt++] = BYTE0(_c);
    data_to_send[_cnt++] = BYTE1(_c);
	
    data_to_send[_cnt++] = BYTE0(_d);
    data_to_send[_cnt++] = BYTE1(_d);
	 for ( i = 0; i < data_to_send[3]+4; i++)
    {
        sumcheck += data_to_send[i];//和校验
        addcheck += sumcheck;//附加校验
    }
    data_to_send[_cnt++] = sumcheck;
    data_to_send[_cnt++] = addcheck;
	HAL_UART_Transmit(&huart1,data_to_send,_cnt,0xFFFF);//这里是串口发送函数
}
//,通过F2帧发送4个int16类型的数据
void ANO_DT_Send_F2(int16_t _a, int16_t _b, int16_t _c, int16_t _d)   //F2帧  4个  int16 参数
{
    uint8_t _cnt = 0;
    uint8_t sumcheck = 0; //和校验
    uint8_t addcheck = 0; //附加和校验
    uint8_t i=0;
   data_to_send[_cnt++] = 0xAA;
    data_to_send[_cnt++] = 0xFF;
    data_to_send[_cnt++] = 0xF2;
    data_to_send[_cnt++] = 8; //数据长度
	//单片机为小端模式-低地址存放低位数据,匿名上位机要求先发低位数据,所以先发低地址
    data_to_send[_cnt++] = BYTE0(_a);
    data_to_send[_cnt++] = BYTE1(_a);
	
    data_to_send[_cnt++] = BYTE0(_b);
    data_to_send[_cnt++] = BYTE1(_b);
	
    data_to_send[_cnt++] = BYTE0(_c);
    data_to_send[_cnt++] = BYTE1(_c);
	
    data_to_send[_cnt++] = BYTE0(_d);
    data_to_send[_cnt++] = BYTE1(_d);
	
	  for ( i = 0; i < data_to_send[3]+4; i++)
    {
        sumcheck += data_to_send[i];
        addcheck += sumcheck;
    }

    data_to_send[_cnt++] = sumcheck;
    data_to_send[_cnt++] = addcheck;
	
	HAL_UART_Transmit(&huart1,data_to_send,_cnt,0xFFFF);//这里是串口发送函数
}
//通过F3帧发送2个int16类型和1个int32类型的数据
void ANO_DT_Send_F3(int16_t _a, int16_t _b, int32_t _c )   //F3帧  2个  int16 参数   1个  int32  参数
{
    uint8_t _cnt = 0;
    uint8_t sumcheck = 0; //和校验
    uint8_t addcheck = 0; //附加和校验
    uint8_t i=0;
    data_to_send[_cnt++] = 0xAA;
    data_to_send[_cnt++] = 0xFF;
    data_to_send[_cnt++] = 0xF3;
    data_to_send[_cnt++] = 8; //数据长度
	//单片机为小端模式-低地址存放低位数据,匿名上位机要求先发低位数据,所以先发低地址
    data_to_send[_cnt++] = BYTE0(_a);
    data_to_send[_cnt++] = BYTE1(_a);
	
    data_to_send[_cnt++] = BYTE0(_b);
    data_to_send[_cnt++] = BYTE1(_b);
	
    data_to_send[_cnt++] = BYTE0(_c);
    data_to_send[_cnt++] = BYTE1(_c);
    data_to_send[_cnt++] = BYTE2(_c);
    data_to_send[_cnt++] = BYTE3(_c);
	
	  for ( i = 0; i < data_to_send[3]+4; i++)
    {
        sumcheck += data_to_send[i];
        addcheck += sumcheck;
    }

    data_to_send[_cnt++] = sumcheck;
    data_to_send[_cnt++] = addcheck;

	HAL_UART_Transmit(&huart1,data_to_send,_cnt,0xFFFF);//这里是串口发送函数
}

  • 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

niming.h

#ifndef  NIMING_H
#define  NIMING_H
#include "main.h"
//需要发送16位,32位数据,对数据拆分,之后每次发送单个字节
//拆分过程:对变量dwTemp 去地址然后将其转化成char类型指针,最后再取出指针所指向的内容
#define BYTE0(dwTemp)  (*(char *)(&dwTemp))
#define BYTE1(dwTemp)  (*((char *)(&dwTemp) + 1))
#define BYTE2(dwTemp)  (*((char *)(&dwTemp) + 2))
#define BYTE3(dwTemp)  (*((char *)(&dwTemp) + 3))


void ANO_DT_Send_F1(uint16_t, uint16_t _b, uint16_t _c, uint16_t _d);
void ANO_DT_Send_F2(int16_t _a, int16_t _b, int16_t _c, int16_t _d);
void ANO_DT_Send_F3(int16_t _a, int16_t _b, int32_t _c );

#endif 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

添加测试代码
在这里插入图片描述

	//电机速度等信息发送到上位机
	//注意上位机不支持浮点数,所以要乘100
	ANO_DT_Send_F2(Motor1Speed*100, 3.0*100,Motor2Speed*100,3.0*100);
  • 1
  • 2
  • 3

下面设置上位机-数据解析
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
这个是控制效果,并不理想,后面我们介绍PID控制
在这里插入图片描述

8.3-P I D 逐个参数理解

在这里插入图片描述
加入的现在 过去 未来概念

p:现在

i:过去

d:未来
在这里插入图片描述
那么我们就开始写PID

PID的结构体类型变量、里面成员都是浮点类型

先在pid.h声明一个结构体类型、声明.c中的函数
在这里插入图片描述

#ifndef __PID_H
#define __PID_H

//声明一个结构体类型
typedef struct 
{
	float target_val;//目标值
	float actual_val;//实际值
	float err;//当前偏差
	float err_last;//上次偏差
	float err_sum;//误差累计值
	float Kp,Ki,Kd;//比例,积分,微分系数
	
} tPid;

//声明函数
float P_realize(tPid * pid,float actual_val);
void PID_init(void);
float PI_realize(tPid * pid,float actual_val);
float PID_realize(tPid * pid,float actual_val);
#endif

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

然后在pid.c中定义结构体类型变量
在这里插入图片描述

#include "pid.h"

//定义一个结构体类型变量
tPid pidMotor1Speed;
//给结构体类型变量赋初值
void PID_init()
{
	pidMotor1Speed.actual_val=0.0;
	pidMotor1Speed.target_val=0.00;
	pidMotor1Speed.err=0.0;
	pidMotor1Speed.err_last=0.0;
	pidMotor1Speed.err_sum=0.0;
	pidMotor1Speed.Kp=0;
	pidMotor1Speed.Ki=0;
	pidMotor1Speed.Kd=0;
}
//比例p调节控制函数
float P_realize(tPid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;//当前误差=目标值-真实值
	//比例控制调节   输出=Kp*当前误差
	pid->actual_val = pid->Kp*pid->err;
	return pid->actual_val;
}
//比例P 积分I 控制函数
float PI_realize(tPid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;//当前误差=目标值-真实值
	pid->err_sum += pid->err;//误差累计值 = 当前误差累计和
	//使用PI控制 输出=Kp*当前误差+Ki*误差累计值
	pid->actual_val = pid->Kp*pid->err + pid->Ki*pid->err_sum;
	
	return pid->actual_val;
}
// PID控制函数
float PID_realize(tPid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;当前误差=目标值-真实值
	pid->err_sum += pid->err;//误差累计值 = 当前误差累计和
	//使用PID控制 输出 = Kp*当前误差  +  Ki*误差累计值 + Kd*(当前误差-上次误差)
	pid->actual_val = pid->Kp*pid->err + pid->Ki*pid->err_sum + pid->Kd*(pid->err - pid->err_last);
	//保存上次误差: 这次误差赋值给上次误差
	pid->err_last = pid->err;
	
	return pid->actual_val;
}

  • 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

然后在main中要调用PID_init();函数
在这里插入图片描述

  PID_init();
  • 1

p调节函数函数只根据当前误差进行控制

//比例p调节控制函数
float P_realize(tPid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;//目标值减去实际值等于误差值
	//比例控制调节
	pid->actual_val = pid->Kp*pid->err;
	return pid->actual_val;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

主函数-可以估算当p=10 就有较好的响应速度

先看根据p比例控制的效果
在这里插入图片描述
p调节 电机稳态后还是存在误差。

下面加入i 调节也就是加入历史误差
pi的控制函数

//比例P 积分I 控制函数
float PI_realize(tPid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;//目标值减去实际值等于误差值
	pid->err_sum += pid->err;//误差累计求和
	//使用PI控制
	pid->actual_val = pid->Kp*pid->err + pid->Ki*pid->err_sum;
	
	return pid->actual_val;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

因为实际值1.6的时候误差为1.4 上次偏差1.4和这次偏差1.4相加2.8 我们乘5 等于10点多就会有较好控制效果
这是pi 调节的控制效果
在这里插入图片描述
下面是PID调节的

// PID控制函数
float PID_realize(tPid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;//目标值减去实际值等于误差值
	pid->err_sum += pid->err;//误差累计求和
	//使用PID控制
	pid->actual_val = pid->Kp*pid->err + pid->Ki*pid->err_sum + pid->Kd*(pid->err - pid->err_last);
	//保存上次误差:最近一次 赋值给上次
	pid->err_last = pid->err;
	
	return pid->actual_val;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

8.4-加入cJSON方便上位机调参

在这里插入图片描述
调大堆栈
在这里插入图片描述
软件开启中断
在这里插入图片描述
开启接收中断
在这里插入图片描述

 __HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE);	//开启串口1接收中断
  • 1

中断回调函数在这里插入图片描述

uint8_t Usart1_ReadBuf[256];	//串口1 缓冲数组
uint8_t Usart1_ReadCount = 0;	//串口1 接收字节计数
  if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_RXNE))//判断huart1 是否读到字节
  {
		if(Usart1_ReadCount >= 255) Usart1_ReadCount = 0;
		HAL_UART_Receive(&huart1,&Usart1_ReadBuf[Usart1_ReadCount++],1,1000);
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

编写函数用于判断串口是否发送完一帧数据
在这里插入图片描述

extern uint8_t Usart1_ReadBuf[255];	//串口1 缓冲数组
extern uint8_t Usart1_ReadCount;	//串口1 接收字节计数

//判断否接收完一帧数据
uint8_t Usart_WaitReasFinish(void)
{
	static uint16_t Usart_LastReadCount = 0;//记录上次的计数值
	if(Usart1_ReadCount == 0)
	{
		Usart_LastReadCount = 0;
		return 1;//表示没有在接收数据
	}
	if(Usart1_ReadCount == Usart_LastReadCount)//如果这次计数值等于上次计数值
	{
		Usart1_ReadCount = 0;
		Usart_LastReadCount = 0;
		return 0;//已经接收完成了
	}
	Usart_LastReadCount = Usart1_ReadCount;
	return 2;//表示正在接受中
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

然后我们把cJSON库放入工程里面

下载cJSON新版

gtihub链接:https://github.com/DaveGamble/cJSON

百度网盘链接:https://pan.baidu.com/s/1AcNHtZuv5bokMQ2f6QoG7Q

提取码:a422

和添加其他文件一样,加入工程,然后指定路径

编写解析指令的函数

在这里插入图片描述

#include "cJSON.h"
#include <string.h>
   cJSON *cJsonData ,*cJsonVlaue;

	if(Usart_WaitReasFinish() == 0)//是否接收完毕
	{
		cJsonData  = cJSON_Parse((const char *)Usart1_ReadBuf);
		if(cJSON_GetObjectItem(cJsonData,"p") !=NULL)
		{
			cJsonVlaue = cJSON_GetObjectItem(cJsonData,"p");	
		    p = cJsonVlaue->valuedouble;
			pidMotor1Speed.Kp = p;
		}
		if(cJSON_GetObjectItem(cJsonData,"i") !=NULL)
		{
			cJsonVlaue = cJSON_GetObjectItem(cJsonData,"i");	
		    i = cJsonVlaue->valuedouble;
			pidMotor1Speed.Ki = i;
		}
		if(cJSON_GetObjectItem(cJsonData,"d") !=NULL)
		{
			cJsonVlaue = cJSON_GetObjectItem(cJsonData,"d");	
		    d = cJsonVlaue->valuedouble;
			pidMotor1Speed.Kd = d;
		}
		if(cJSON_GetObjectItem(cJsonData,"a") !=NULL)
		{
		
			cJsonVlaue = cJSON_GetObjectItem(cJsonData,"a");	
		    a = cJsonVlaue->valuedouble;
			pidMotor1Speed.target_val =a;
		}
		if(cJsonData != NULL){
		  cJSON_Delete(cJsonData);//释放空间、但是不能删除cJsonVlaue不然会 出现异常错误
		}
		memset(Usart1_ReadBuf,0,255);//清空接收buf,注意这里不能使用strlen	
	}
	printf("P:%.3f  I:%.3f  D:%.3f A:%.3f\r\n",p,i,d,a);
  • 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

测试发送cJSON数据就会解析收到数据
在这里插入图片描述
然后我们赋值改变一个电机的PID参数和目标转速
然后我们通过串口发送命令,就会改变PID的参数了
这么我们的第八章就弄好了,下篇我们进行第九章-PID整定

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

闽ICP备14008679号