IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 嵌入式 -> 一、对MODEBUS RTU方式通信学习笔记 -> 正文阅读

[嵌入式]一、对MODEBUS RTU方式通信学习笔记

一、首先
1、对于寄存器操作的一些宏进行理解。

#define SET_BIT(REG, BIT)     ((REG) |= (BIT))//设置寄存器的第bit位值为1,SET_BIT(RCC->AHB2ENR,1) 或者 SET_BIT(RCC->AHB2ENR,2) 
#define CLEAR_BIT(REG, BIT)   ((REG) &= ~(BIT))//清除寄存器的第bit位值为1的值,即将第bit位置0,CLEAR_BIT(RCC->CR, 64) 
#define READ_BIT(REG, BIT)    ((REG) & (BIT))//读取寄存器中的值,分两种情况:1.REG为FF,BIT为任意数,结果为BIT对应的数据,2.REG为固定数,BIT为任意数,结果0或者固定数,第三种情况不考虑。
#define CLEAR_REG(REG)        ((REG) = (0x0))//清空寄存器值
#define WRITE_REG(REG, VAL)   ((REG) = (VAL))//对寄存器写入VAL值 
#define READ_REG(REG)         ((REG))	//读取寄存器内容 ok
#define MODIFY_REG(REG, CLEARMASK, SETMASK)  WRITE_REG((REG), (((READ_REG(REG)) & (~(CLEARMASK))) | (SETMASK)))  //对寄存器REG   写入值(清除和设置掩码)

2、对modebus 中RTU模式的应用理解。
modebus为工业领域的一种通信协议。
其占应用层,数据链路层,物理层等3大数据通信层。
同时其遵循一主机对多从机的通信方式。
而却分从机主要从,从机的地址进行区分。
同时存在功能码实现主机和从机的通信功能的控制。
物理层上(485,232等物理上通信芯片为基础)。

1.RTU 通信的帧格式
主机端:

从地址功能码寄存器地址高低寄存器数量CRC
0x010x10x00 0x020x00 0x0080x9c 0x0c

从机端:

从地址功能码数据长度数据CRC
0x010x10x010xb30x9c 0x0c

2.通信时对应的功能码含意
公用功能码:
线圈,8bit(r),
离散输入8bit(rw),
寄存器 16bit?,
保持寄存器 16bit(rw),

功能码含义
01h读线圈
02h读离散量输入
03h读保持(多个)寄存器
04h读输入寄存器
05h写单个线圈
06h写多个线圈
0fh写多个线圈
10h写多个寄存器
14h记录读文件
15h写文件记录
16h屏蔽写寄存器
17h读写多个寄存器

3.通信时的数据中每个字符于每段数据帧间的时间控制 (需要控制并检测超过1.5个字符的时间是否收到数据 ,同时对超过3.5个字符的时间没收到数据,认为是空闲状态)。
即: T1.5 <x && T3.5<x
当超过1.5个字符的时间未收到数据即数据出现异常,该帧数据异常。

在通信时RTU方式传输的每帧数据长度需要在256之内。
4.在主机端:
需要考虑的是对从机的发送指令,(对从机的响应接收通过串口的中断来处理)。

主机对从机而言需要做的就是查询,和写入指令两类操作。
这两类操作通过功能码实现。

第一类:
主机要读取从机的信息。

函数功能: 读输入状态状态(InputStatue)
  * 输入参数: _addr:从站地址,_reg:寄存器地址,_num:待读取的输入数量
  * 返 回 值:* 说    明: 填充数据发送缓存区,然后发送
void MB_ReadInput_02H(uint8_t _addr, uint16_t _reg, uint16_t _num)
{
	uint16_t TxCount = 0;
  uint16_t crc = 0;
	Tx_Buf[TxCount++] = _addr;		    /* 从站地址 */
	Tx_Buf[TxCount++] = 0x02;		      /* 功能码 */	
	Tx_Buf[TxCount++] = _reg >> 8;	  /* 寄存器地址 高字节 */
	Tx_Buf[TxCount++] = _reg;		      /* 寄存器地址 低字节 */
	Tx_Buf[TxCount++] = _num >> 8;	  /* 开关(Input)个数 高字节 */
	Tx_Buf[TxCount++] = _num;		      /* 开关(Input)个数 低字节 */

	crc = MB_CRC16((uint8_t*)&Tx_Buf,TxCount);
  Tx_Buf[TxCount++] = crc;	          /* crc 低字节 */
	Tx_Buf[TxCount++] = crc>>8;		      /* crc 高字节 */
  HAL_UART_Transmit(&husart_debug, (uint8_t *)&Tx_Buf, TxCount, 0xffff);
}
第二类是写指令到从机
* 函数功能: 写N个保持寄存器(HoldingRegister)
  * 输入参数: _addr:从站地址,_reg:寄存器地址,_num:待写入的寄存器数量,_databuf:待写入的寄存器数据
  * 返 回 值:* 说    明: 填充数据发送缓存区,然后发送._databuf的长度需 >= _num*2
void MB_WriteNumHoldingReg_10H(uint8_t _addr, uint16_t _reg, uint16_t _num,uint8_t *_databuf)
{
  uint16_t i;
	uint16_t TxCount = 0;
  uint16_t crc = 0;
	Tx_Buf[TxCount++] = _addr;		    /* 从站地址 */
	Tx_Buf[TxCount++] = 0x10;		      /* 功能码 */	
	Tx_Buf[TxCount++] = _reg >> 8;	  /* 寄存器地址 高字节 */
	Tx_Buf[TxCount++] = _reg;		      /* 寄存器地址 低字节 */
	Tx_Buf[TxCount++] = _num >> 8;	  /* 寄存器(16bits)个数 高字节 */
	Tx_Buf[TxCount++] = _num;		      /*  低字节 */
  Tx_Buf[TxCount++] = _num<<1;		  /* 数据个数 */

  for (i = 0; i < 2 * _num; i++)
  {
		Tx_Buf[TxCount++]  = _databuf[i];		/* 后面的数据长度 */
	}
	crc = MB_CRC16((uint8_t*)&Tx_Buf,TxCount);
  Tx_Buf[TxCount++] = crc;	          /* crc 低字节 */
	Tx_Buf[TxCount++] = crc>>8;		      /* crc 高字节 */
  HAL_UART_Transmit(&husart_debug, (uint8_t *)&Tx_Buf, TxCount, 0xffff);
}

这是主机端对于功能码的函数封装。
对于主机端的处理,就是通过控制条件调用该功能码封装函数。实现对从机的发送(查询控制)。
对于从机端由于要做对主机端的接收判断反馈处理所以较为复杂繁琐写。(同时注意一个道理,提出问题,往往要比解决问题来的简单。)

一、由于从机需要接收来自主机端的功能码数据信息所以需要对地址,以及数据长度,帧内容进行检测判断,并对异常的功能码数据进行处理。
typedef struct {
  __IO uint8_t  Code ;  	        // 功能码
  __IO uint8_t byteNums; 	        // 字节数
  __IO uint16_t Addr ;            // 操作内存的起始地址
  __IO uint16_t Num; 	            // 寄存器或者线圈的数量
  __IO uint16_t _CRC;       	      // CRC校验码
  __IO uint8_t *ValueReg; 	      // 10H功能码的数据
  __IO uint16_t *PtrHoldingbase;  // HoldingReg内存首地址
  __IO uint16_t *PtrHoldingOffset;// HoldingReg内存首地址
}PDUData_TypeDef;

同时在公共功能码的基础上需要定义异常码,
并定义线圈,寄存器
#define FUN_CODE_01H            0x01  // 功能码01H 
#define FUN_CODE_02H            0x02  // 功能码02H
#define FUN_CODE_03H            0x03  // 功能码03H
#define FUN_CODE_04H            0x04  // 功能码04H
#define FUN_CODE_05H            0x05  // 功能码05H
#define FUN_CODE_06H            0x06  // 功能码06H
#define FUN_CODE_10H            0x10  // 功能码10H

#define EX_CODE_NONE           0x00  // 异常码 无异常
#define EX_CODE_01H            0x01  // 异常码
#define EX_CODE_02H            0x02  // 异常码
#define EX_CODE_03H            0x03  // 异常码
#define EX_CODE_04H            0x04  // 异常码

/**
  * 函数功能: 判断地址是否符合协议范围
  * 输入参数: _Addr:起始地址,_RegNum:寄存器数量,_FunCode:功能码
  * 返 回 值: 异常码:02H或NONE
  * 说    明: 地址范围是0x0000~0xFFFF,可操作的空间范围不能超过这个区域
  */
uint8_t MB_JudgeAddr(uint16_t _Addr,uint16_t _RegNum)
{
  uint8_t Excode = EX_CODE_NONE;
  /* 地址+寄存器数量不能超过0xFFFF */
  if( ((uint32_t)_RegNum+(uint32_t)_Addr) > (uint32_t)0xFFFF)
  {
    Excode = EX_CODE_02H;// 异常码 02H
  }
  return Excode;
}
/**
  * 函数功能: 判断操作的数据量是否符合协议范围
  * 输入参数: _RegNum:寄存器数量,_FunCode:功能码,_ByteNum:字节数量
  * 返 回 值: 异常码:03或NONE
  * 说    明: 对可操作连续内存空间的功能码需要验证操作的地址是否符合范围
  */
uint8_t MB_JudgeNum(uint16_t _RegNum,uint8_t _FunCode,uint16_t _ByteNum)
{
  uint8_t Excode = EX_CODE_NONE;
  uint16_t _CoilNum = _RegNum; // 线圈(离散量)的数量
  switch(_FunCode)
  {
    case FUN_CODE_01H: 
    case FUN_CODE_02H:
      if( (_CoilNum<0x0001) || (_CoilNum>0x07D0))
        Excode = EX_CODE_03H;// 异常码03H;
      break;
    case FUN_CODE_03H:
    case FUN_CODE_04H:
      if( (_RegNum<0x0001) || (_RegNum>0x007D))
        Excode = EX_CODE_03H;// 异常码03H;      
      break;
    case FUN_CODE_10H:
      if( (_RegNum<0x0001) || (_RegNum>0x007B))
        Excode = EX_CODE_03H;// 异常码03H
      if( _ByteNum != (_RegNum<<1))
        Excode = EX_CODE_03H;// 异常码03H
      break;
  }
  return Excode;
}
/* 提取数据帧,进行解析数据帧 */
void MB_Parse_Data()
{
  PduData.Code = Rx_Buf[1];                   // 功能码
  PduData.Addr = ((Rx_Buf[2]<<8) | Rx_Buf[3]);// 寄存器起始地址
  PduData.Num  = ((Rx_Buf[4]<<8) | Rx_Buf[5]);// 数量(Coil,Input,Holding Reg,Input Reg)
  PduData._CRC = MB_CRC16((uint8_t*)&Rx_Buf,RxCount-2);             // CRC校验码
  PduData.byteNums = Rx_Buf[6];                                     // 获得字节数
  PduData.ValueReg = (uint8_t*)&Rx_Buf[7];                          // 寄存器值起始地址
  PduData.PtrHoldingOffset = PduData.PtrHoldingbase + PduData.Addr; // 保持寄存器的起始地址
}

/** 
  * 函数功能: 对接收到的数据进行分析并执行
  * 输入参数: 无
  * 返 回 值: 异常码或0x00
  * 说    明: 判断功能码,验证地址是否正确.数值内容是否溢出,数据没错误就发送响应信号
  */
uint8_t MB_Analyze_Execute(void )
{
  uint16_t ExCode = EX_CODE_NONE;
  /* 校验功能码 */
  if( IS_NOT_FUNCODE(PduData.Code) ) // 不支持的功能码
  {
    /* Modbus异常响应 */
    ExCode = EX_CODE_01H;            // 异常码01H
    return ExCode;
  }
  /* 根据功能码分别做判断 */
  switch(PduData.Code)
  {
    /* 这里认为01H功能码和02功能码是一样的,其实也没什么不一样
     * 只是操作地址可能不一样,这一点结合具体来实现,可以在main函数
     * 申请单独的内存使用不同的功能码,在实际应用中必须加以区分使用
     * 不同的内存空间
     */
/* ---- 01H  02H 读取离散量输入(Coil Input)---------------------- */
    case FUN_CODE_01H:
    case FUN_CODE_02H:
      /* 判断线圈数量是否正确 */  
      ExCode = MB_JudgeNum(PduData.Num,PduData.Code,1);
      if(ExCode != EX_CODE_NONE )
        return ExCode;      
      
      /* 判断地址是否正确*/
      ExCode = MB_JudgeAddr( PduData.Addr,PduData.Num);
      if(ExCode != EX_CODE_NONE )
        return ExCode;  
      break;
/* ---- 03H  04H 读取保持/输入寄存器---------------------- */
    case FUN_CODE_03H:
    case FUN_CODE_04H:
      /* 判断寄存器数量是否正确 */
      ExCode = MB_JudgeNum(PduData.Num,PduData.Code,PduData.byteNums);
      if(ExCode != EX_CODE_NONE )
        return ExCode;  
      
      /* 判断地址是否正确*/
      ExCode = MB_JudgeAddr( PduData.Addr,PduData.Num);
      if(ExCode != EX_CODE_NONE )
        return ExCode;  
      break;
/* ---- 05H 写入单个离散量---------------------- */
    case FUN_CODE_05H:
      break;
/* ---- 06H 写单个保持寄存器 ---------------------- */
    case FUN_CODE_06H:     
      break;
/* ---- 10H 写多个保持寄存器 ---------------------- */
    case FUN_CODE_10H:
      /* 判断寄存器数量是否正确 */
      ExCode = MB_JudgeNum(PduData.Num,PduData.Code,PduData.byteNums);
      if(ExCode != EX_CODE_NONE )
        return ExCode;          
      /* 判断地址是否正确*/
      ExCode = MB_JudgeAddr( PduData.Addr,PduData.Num);		      		
      if(ExCode != EX_CODE_NONE )
        return ExCode;  			
      break;
  }
  /* 数据帧没有异常 */
  return ExCode; //   EX_CODE_NONE
}

异常的处理

/**
  * 函数功能: 异常响应
  * 输入参数: _FunCode :发送异常的功能码,_ExCode:异常码
  * 返 回 值: 无
  * 说    明: 当通信数据帧发生异常时,发送异常响应
  */
void MB_Exception_RSP(uint8_t _FunCode,uint8_t _ExCode)
{
  uint16_t TxCount = 0;
  uint16_t crc = 0;
	Tx_Buf[TxCount++] = MB_SLAVEADDR;		    /* 从站地址 */
	Tx_Buf[TxCount++] = _FunCode|0x80;		  /* 功能码 + 0x80*/	
	Tx_Buf[TxCount++] = _ExCode ;	          /* 异常码*/
	
  crc = MB_CRC16((uint8_t*)&Tx_Buf,TxCount);
  Tx_Buf[TxCount++] = crc;	          /* crc 低字节 */
	Tx_Buf[TxCount++] = crc>>8;		      /* crc 高字节 */
  UART_Tx((uint8_t*)Tx_Buf, TxCount);
}




正常的响应处理

/**
  * 函数功能: 正常响应
  * 输入参数: _FunCode :功能码
  * 返 回 值: 无
  * 说    明: 当通信数据帧没有异常时并且成功执行之后,发送响应数据帧
  */
void MB_RSP(uint8_t _FunCode)
{
  uint16_t TxCount = 0;
  uint16_t crc = 0;	Tx_Buf[TxCount++] = MB_SLAVEADDR;		 /* 从站地址 */
	Tx_Buf[TxCount++] = _FunCode;        /* 功能码   */	
  switch(_FunCode)
  {
    case FUN_CODE_01H:
			/* 读取线圈状态 */
			TxCount = MB_RSP_01H(TxCount,PduData.Addr,PduData.Num);
		  break;
    case FUN_CODE_02H:
			/* 读取离散输入 */
      TxCount = MB_RSP_02H(TxCount,PduData.Addr,PduData.Num);
      break;		 
    case FUN_CODE_03H:
			 /* 读取保持寄存器 */
			TxCount = MB_RSP_03H(TxCount,(uint16_t*)PduData.PtrHoldingOffset,PduData.Num);
	  	break;
    case FUN_CODE_04H:
			/* 读取输入寄存器 */
			TxCount =	MB_RSP_04H(TxCount,PduData.Addr,PduData.Num);      
      break;
    case FUN_CODE_05H:
			/* 写单个线圈 */
      TxCount = MB_RSP_05H(TxCount,PduData.Addr,PduData.Num);
      break;
    case FUN_CODE_06H:
			/* 写单个保持寄存器 */
      TxCount = MB_RSP_06H(TxCount,PduData.Addr,PduData.Num, (uint16_t*)PduData.PtrHoldingOffset);
      break;
    case FUN_CODE_10H:
			/* 写多个保持寄存器 */
      TxCount = MB_RSP_10H(TxCount,PduData.Addr,PduData.Num ,(uint16_t*)PduData.PtrHoldingOffset,(uint8_t*)PduData.ValueReg);
      break;
  }
  crc = MB_CRC16((uint8_t*)&Tx_Buf,TxCount);
  Tx_Buf[TxCount++] = crc;	          /* crc 低字节 */
	Tx_Buf[TxCount++] = crc>>8;		      /* crc 高字节 */
  UART_Tx((uint8_t*)Tx_Buf, TxCount);
}

对于功能码的处理

/**
  * 函数功能: 读取离散输入(只读)
  * 输入参数: _TxCount :发送计数器,_AddrOffset地址偏移量,_CoilNum:线圈数量
  * 返 回 值: Tx_Buf的数组元素坐标
  * 说    明: 读取离散输出,并且填充Tx_Buf
  */
static uint16_t MB_RSP_02H(uint16_t _TxCount,uint16_t _AddrOffset ,uint16_t _CoilNum)
{
	/*
		主机发送:
			01 从机地址
			02 功能码
			00 寄存器起始地址高字节
			01 寄存器起始地址低字节
			00 寄存器数量高字节
			08 寄存器数量低字节
			28 CRC校验高字节
			0C CRC校验低字节

		从机应答: 	1代表ON,0代表OFF(使用LED的状态来代替)。若返回的线圈数不为8的倍数,则在最后数据字节未尾使用0代替. BIT0对应第1个
			01 从机地址
			02 功能码
			01 返回字节数
			02 数据1(线圈0002H-线圈0011H)
			D0 CRC校验高字节
			49 CRC校验低字节

		例子:
		发送:	01 01 00 02 00 08   9C 0C	  --- 查询D02开始的8个继电器状态
		返回:	01 01 01 02         D0 49   --- 查询到8个状态为:0000 0010 第二个LED为亮
	*/	
  uint16_t i = 0;
	uint16_t m;	
	uint8_t status[10];	
	
  /* 计算返回字节数(_CoilNum变量是以位为单位) */
  m = (_CoilNum+7)/8;
  /* 返回字节数(数量)*/
	Tx_Buf[_TxCount++] = m; 
	  
	if ((_AddrOffset >= COIL_D01) && (_CoilNum > 0))
  {
		/* 将获取的线圈状态首先清零 */
		for (i = 0; i < m; i++)
		{
			status[i] = 0;
		}		
		/* 获取对应线圈状态,并将其写入status[] */
		for (i = 0; i < _CoilNum; i++)
		{
			/* 读LED的状态,写入状态寄存器的每一位 */
			if (Get_LEDx_State(i + 1 + _AddrOffset - COIL_D01))		
			{  
				status[i / 8] |= (1 << (i % 8));
			}			
		}
	}
	/* 填充发送内容 */
	for (i = 0; i < m; i++)
	{
		/* 继电器状态 */
		Tx_Buf[_TxCount++] = status[i];	
	}	
  return _TxCount;
}

/**
  * 函数功能: 写单个保持寄存器(读/写)
  * 输入参数: _TxCount :发送计数器,_AddrOffset:地址偏移量,_RegNum: 写入数据,_AddrAbs:保持寄存器地址
  * 返 回 值: Tx_Buf的数组元素坐标
  * 说    明: 填充Tx_Buf
  */
static uint8_t MB_RSP_06H(uint16_t _TxCount,uint16_t _AddrOffset ,uint16_t _RegNum ,uint16_t *_AddrAbs)
{
	/*
		写保持寄存器。注意06指令只能操作单个保持寄存器,10H指令可以设置单个或多个保持寄存器
		主机发送:
			01 从机地址
			06 功能码
			00 寄存器地址高字节
			10 寄存器地址低字节
			67 数据1高字节
			4A 数据1低字节
			23 CRC校验高字节
			C8 CRC校验低字节

		从机响应:
			01 从机地址
			06 功能码
			00 寄存器地址高字节
			10 寄存器地址低字节
			67 数据1高字节
			4A 数据1低字节
			23 CRC校验高字节
			C8 CRC校验低字节

		例子:
			发送:	01 06 00 10 67 4A  23 C8    ---- 将0010地址寄存器设置为67 4A
			返回:	01 06 00 10 67 4A  23 C8    ---- 返回同样数据
*/	
  /* 填充地址值 */
  Tx_Buf[_TxCount++] = _AddrOffset>>8;
  Tx_Buf[_TxCount++] = _AddrOffset;	

	/* 将数据写入保持寄存器内 */	
	*_AddrAbs = _RegNum;
	
  /* 填充返回内容 */
	Tx_Buf[_TxCount++] = PduData.Num>>8;
	Tx_Buf[_TxCount++] = PduData.Num;
  
  return _TxCount;	
}

main中使用注意

/**
  * 函数功能: 填充内存
  * 输入参数: buf:内存空间首地址,Code:功能码
  * 返 回 值: 无
  * 说    明: 功能不同的功能码填充内容不一的内存空间,
  */
void FillBuf(uint8_t* buf,uint8_t Code)
{
  uint16_t i = 0;
  uint16_t j = 1;
  switch(Code)
  {
    case FUN_CODE_03H:
    case FUN_CODE_06H:
    case FUN_CODE_10H:
      j = 0x000F;
      for(i= 0;i<0x250;i++)
      buf[i] = j++;
    break;
  }
}


  /* 申请内存空间作为保持寄存器地址,对应功能码03H、06H和10H */  
  PduData.PtrHoldingbase = (uint16_t*)malloc(sizeof(uint16_t)*0x125);
  FillBuf((uint8_t*)PduData.PtrHoldingbase,FUN_CODE_03H);
  

 /* CRC 校验正确 */
      crc_check = ( (Rx_Buf[RxCount-1]<<8) | Rx_Buf[RxCount-2] );
      if(crc_check == PduData._CRC) 
      {
        /* 分析数据帧并执行 */        
        Ex_code = MB_Analyze_Execute();
        /* 出现异常 */
        if(Ex_code !=EX_CODE_NONE)
        {
          MB_Exception_RSP(PduData.Code,Ex_code);
        }
        else
        {
          MB_RSP(PduData.Code);
        }
      }
  嵌入式 最新文章
基于高精度单片机开发红外测温仪方案
89C51单片机与DAC0832
基于51单片机宠物自动投料喂食器控制系统仿
《痞子衡嵌入式半月刊》 第 68 期
多思计组实验实验七 简单模型机实验
CSC7720
启明智显分享| ESP32学习笔记参考--PWM(脉冲
STM32初探
STM32 总结
【STM32】CubeMX例程四---定时器中断(附工
上一篇文章      下一篇文章      查看所有文章
加:2022-04-29 12:18:17  更:2022-04-29 12:19:42 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/30 0:58:11-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码