STM32F4X SDIO(七) 例程讲解-SD_InitializeCards & SD_GetCardInfo

STM32F4X SDIO(七)例程讲解-SD_InitializeCards & SD_GetCardInfo

本节例程基于 野火电子的STM32F407的SD卡读写例程进行讲解。上一节中讲解了SD卡上电过程,这节将会讲解一下SD卡的初始化过程,包括 获取SD卡的CID、CSD和SD卡RCA地址。

例程讲解-SD_InitializeCards & SD_GetCardInfo

CMD2:ALL_SEND_CID

CMD2的作用是通知所有卡通过CMD线返回CID值,CID值包括SD卡的识别号、制造商ID、OEMID、产品名称、版本号、序列号等信息,相当于是SD卡的身份证。

命令发送程序

CMD2命令无需带参数,所以SDIO控制器可以直接发送CMD2命令

c 复制代码
 SDIO_CmdInitStructure.SDIO_Argument = 0x0; // 不带参数
 SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_ALL_SEND_CID; // 命令所以为CMD2
 SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Long; // 长响应
 SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No; // 不等待
 SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable; // 使能CPSM状态机
 SDIO_SendCommand(&SDIO_CmdInitStructure);

命令响应程序

CMD2的响应是R2,其响应类型为长响应。其中bit[127:1]是其响应的数据。

首先需要先判断SD卡的响应是否正常,如果响应正常,下一步就需要把128位的CID读出来。

c 复制代码
static SD_Error CmdResp2Error(void)
{
  SD_Error errorstatus = SD_OK;
  uint32_t status;

  status = SDIO->STA;

  while (!(status & (SDIO_FLAG_CCRCFAIL | SDIO_FLAG_CTIMEOUT | SDIO_FLAG_CMDREND)))
  {
    status = SDIO->STA;
  }

  if (status & SDIO_FLAG_CTIMEOUT)
  {
    errorstatus = SD_CMD_RSP_TIMEOUT;
    SDIO_ClearFlag(SDIO_FLAG_CTIMEOUT);
    return(errorstatus);
  }
  else if (status & SDIO_FLAG_CCRCFAIL)
  {
    errorstatus = SD_CMD_CRC_FAIL;
    SDIO_ClearFlag(SDIO_FLAG_CCRCFAIL);
    return(errorstatus);
  }

  /*!< Clear all the static flags */
  SDIO_ClearFlag(SDIO_STATIC_FLAGS);

  return(errorstatus);
}
c 复制代码
// 读取128位CID
    CID_Tab[0] = SDIO_GetResponse(SDIO_RESP1);
    CID_Tab[1] = SDIO_GetResponse(SDIO_RESP2);
    CID_Tab[2] = SDIO_GetResponse(SDIO_RESP3);
    CID_Tab[3] = SDIO_GetResponse(SDIO_RESP4);

根据STM32F4X的数据手册可知,如果是长响应,高位的数据保存在SDIO_RESP1寄存器,低位保存在SDIO_RESP4寄存器

CID数据解析

根据波形图可知SD卡的CID响应数据为0x9f54495344333247614af80704017158,CID的数据解析表如下

下面就来对SD卡读取到CID数据进行解析

Name Field Width CID-slice Value
Manufacturer ID MID 8 [127:120] 0x9F
OEM/Application ID OID 16 [119:104] 0x5449
Product name PNM 40 [103:64] 0x5344333247 "SD32G"
Product revision PRV 8 [63:56] 0x61
Product serial number PSN 32 [55:24] 0x4af80704
reserved X 4 [23:20] 0
Manufacturing date MDT 12 [19:8] 0x171
CRC7 checksum CRC 7 [7:1] 0x2C
not used, always 1 x 1 [0:0] 0x1

CMD3:SEND_RELATIVE_ADDR

CMD3的作用是请求SD卡发布一个RCA,RCA的作用相当于是一个地址,这个RCA后续在读写SD卡中用于对SD卡进行寻址。

命令发送程序

c 复制代码
 SDIO_CmdInitStructure.SDIO_Argument = 0x00; // 不带参数
 SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_SET_REL_ADDR; // 命令索引 CMD3
 SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Short; // 短响应
 SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No; // 不等待
 SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable; // 使能 CPSM状态机
 SDIO_SendCommand(&SDIO_CmdInitStructure);

命令响应程序

CMD3的响应是R6,其中SD卡的RCA在响应数据的bit[31:16]

在程序中会先判断SD卡的响应是否正常,如果响应正常就通过SDIO_RESP1寄存器读取R6响应的数据,然后保存高16位的RCA地址。

c 复制代码
static SD_Error CmdResp6Error(uint8_t cmd, uint16_t *prca)
{
  SD_Error errorstatus = SD_OK;
  uint32_t status;
  uint32_t response_r1;

  status = SDIO->STA;

  while (!(status & (SDIO_FLAG_CCRCFAIL | SDIO_FLAG_CTIMEOUT | SDIO_FLAG_CMDREND)))
  {
    status = SDIO->STA;
  }

  if (status & SDIO_FLAG_CTIMEOUT)
  {
    errorstatus = SD_CMD_RSP_TIMEOUT;
    SDIO_ClearFlag(SDIO_FLAG_CTIMEOUT);
    return(errorstatus);
  }
  else if (status & SDIO_FLAG_CCRCFAIL)
  {
    errorstatus = SD_CMD_CRC_FAIL;
    SDIO_ClearFlag(SDIO_FLAG_CCRCFAIL);
    return(errorstatus);
  }

  /*!< Check response received is of desired command */
  if (SDIO_GetCommandResponse() != cmd)
  {
    errorstatus = SD_ILLEGAL_CMD;
    return(errorstatus);
  }

  /*!< Clear all the static flags */
  SDIO_ClearFlag(SDIO_STATIC_FLAGS);

  /*!< We have received response, retrieve it.  */
  response_r1 = SDIO_GetResponse(SDIO_RESP1);

  if (SD_ALLZERO == (response_r1 & (SD_R6_GENERAL_UNKNOWN_ERROR | SD_R6_ILLEGAL_CMD | SD_R6_COM_CRC_FAILED)))
  {
    *prca = (uint16_t) (response_r1 >> 16);
    return(errorstatus);
  }

  if (response_r1 & SD_R6_GENERAL_UNKNOWN_ERROR)
  {
    return(SD_GENERAL_UNKNOWN_ERROR);
  }

  if (response_r1 & SD_R6_ILLEGAL_CMD)
  {
    return(SD_ILLEGAL_CMD);
  }

  if (response_r1 & SD_R6_COM_CRC_FAILED)
  {
    return(SD_COM_CRC_FAILED);
  }

  return(errorstatus);
}

CMD9:SEND_CSD

CMD9的作用是获取SD卡的CSD数据,CSD数据主要包含SD卡一些基本状态信息,包括SD的块大小、SD卡传输速率、SD卡的擦除扇区大小等信息。CMD9命令需要带参数,其参数是SD卡的RCA地址。

命令发送程序

c 复制代码
 SDIO_CmdInitStructure.SDIO_Argument = (uint32_t)(rca << 16); // 参数RCA地址
 SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_SEND_CSD; // 命令索引 CMD9
 SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Long; // 长响应
 SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No; // 不等待
 SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable; // 使能CPSM状态机
 SDIO_SendCommand(&SDIO_CmdInitStructure);

命令响应程序

CMD9的响应是R2,长响应,CSD的返回数据是128位。

c 复制代码
static SD_Error CmdResp2Error(void)
{
  SD_Error errorstatus = SD_OK;
  uint32_t status;

  status = SDIO->STA;

  while (!(status & (SDIO_FLAG_CCRCFAIL | SDIO_FLAG_CTIMEOUT | SDIO_FLAG_CMDREND)))
  {
    status = SDIO->STA;
  }

  if (status & SDIO_FLAG_CTIMEOUT)
  {
    errorstatus = SD_CMD_RSP_TIMEOUT;
    SDIO_ClearFlag(SDIO_FLAG_CTIMEOUT);
    return(errorstatus);
  }
  else if (status & SDIO_FLAG_CCRCFAIL)
  {
    errorstatus = SD_CMD_CRC_FAIL;
    SDIO_ClearFlag(SDIO_FLAG_CCRCFAIL);
    return(errorstatus);
  }

  /*!< Clear all the static flags */
  SDIO_ClearFlag(SDIO_STATIC_FLAGS);

  return(errorstatus);
}
CSD_Tab[0] = SDIO_GetResponse(SDIO_RESP1);
CSD_Tab[1] = SDIO_GetResponse(SDIO_RESP2);
CSD_Tab[2] = SDIO_GetResponse(SDIO_RESP3);
CSD_Tab[3] = SDIO_GetResponse(SDIO_RESP4);

CSD的数据读取跟CID的数据读取是一样,不同的是两个数据的解析方法不一样

CSD数据解析

从波形图可知CSD的数据为0x400e00325b590000e68f7f800a400018,下面就对读取到的CSD数据进行解析。

Name Field Width CID-slice Value
CSD structure CSD_STRUCTURE 2 [127:126] 0x1
reserved X 6 [125:120] 0
data read access-time-1 TAAC 8 [119:112] 0xE "SD32G"
data read access-time-2 in CLK cycles (NSAC*100) NSAC 8 [111:104] 0x0
max. data transfer rate TRAN_SPEED 8 [103:96] 0x32
card command classes CCC 12 [95:84] 0x5b5
max. read data block length READ_BL_LEN 4 [83:80] 0x9
partial blocks for read allowed READ_BL_PARTIAL 1 [79:79] 0x0
write block misalignment WRITE_BLK_MISALIGN 1 [78:78] 0x0
read block misalignment READ_BLK_MISALIGN 1 [77:77] 0x0
DSR implemented DSR_IMP 1 [76:76] 0x0
reserved X 6 [75:70] 0x0
device size C_SIZE 22 [69:48] 0xE68F
reserved X 1 [47:47] 0x0
erase single block enable ERASE_BLK_EN 1 [46:46] 0x1
erase sector size SECTOR_SIZE 7 [45:39] 0x7F
write protect group size WP_GRP_SIZE 7 [39:32] 0x0
write protect group enable WP_GRP_ENABLE 1 [31:31] 0x0
reserved X 2 [30:29] 0x0
write speed factor R2W_FACTOR 2 [28:26] 0x2
max. write data block length WRITE_BL_LEN 4 [25:22] 0x9
partial blocks for write allowed WRITE_BL_PARTIAL 1 [21:21] 0x0
reserved X 5 [20:16] 0x0
File format group FILE_FORMAT_GRP 1 [15:15] 0x0
copy flag COPY 1 [14:14] 0x0
permanent write protection PERM_WRITE_PROTECT 1 [13:13] 0x0
temporary write protection TMP_WRITE_PROTECT 1 [12:12] 0x0
File format FILE_FORMAT 2 [11:10] 0x0
reserved X 2 [9:8] 0x0
CRC CRC 7 [7:1] 0xC
not used, always'1' X 1 [0:0] 0x1
CSD结构版本

CSD_STRUCTURE区域代表的是CSD寄存器结构使用的解析版本号,在SD卡的发展中使用了不同的CSD版本,在上面的例程中CSD_STRUCTURE的值是0x01,则代表使用的CSD V2.0的版本

READ_BL_LEN

最大的数据块读取长度,数值为9则代表是512字节

SD卡容量

根据上面的CSD数据我们可以计算出SD卡的容量大小,其计算公式如下memory capacity = (C_SIZE+1) * 512KByte

memory capacity = (0xE68F + 1) * 512 = 30,220,288(Byte) = 29512(M) = 28G

获取SD卡信息

MCU读取到SD卡的CSD和CID数据后,就可以根据上面的表格进行数据的解析

c 复制代码
SD_Error SD_GetCardInfo(SD_CardInfo *cardinfo)
{
  SD_Error errorstatus = SD_OK;
  uint8_t tmp = 0;

  cardinfo->CardType = (uint8_t)CardType;
  cardinfo->RCA = (uint16_t)RCA;

  /*!< Byte 0 */
  tmp = (uint8_t)((CSD_Tab[0] & 0xFF000000) >> 24);
  cardinfo->SD_csd.CSDStruct = (tmp & 0xC0) >> 6;
  cardinfo->SD_csd.SysSpecVersion = (tmp & 0x3C) >> 2;
  cardinfo->SD_csd.Reserved1 = tmp & 0x03;

  /*!< Byte 1 */
  tmp = (uint8_t)((CSD_Tab[0] & 0x00FF0000) >> 16);
  cardinfo->SD_csd.TAAC = tmp;

  /*!< Byte 2 */
  tmp = (uint8_t)((CSD_Tab[0] & 0x0000FF00) >> 8);
  cardinfo->SD_csd.NSAC = tmp;

  /*!< Byte 3 */
  tmp = (uint8_t)(CSD_Tab[0] & 0x000000FF);
  cardinfo->SD_csd.MaxBusClkFrec = tmp;

  /*!< Byte 4 */
  tmp = (uint8_t)((CSD_Tab[1] & 0xFF000000) >> 24);
  cardinfo->SD_csd.CardComdClasses = tmp << 4;

  /*!< Byte 5 */
  tmp = (uint8_t)((CSD_Tab[1] & 0x00FF0000) >> 16);
  cardinfo->SD_csd.CardComdClasses |= (tmp & 0xF0) >> 4;
  cardinfo->SD_csd.RdBlockLen = tmp & 0x0F;

  /*!< Byte 6 */
  tmp = (uint8_t)((CSD_Tab[1] & 0x0000FF00) >> 8);
  cardinfo->SD_csd.PartBlockRead = (tmp & 0x80) >> 7;
  cardinfo->SD_csd.WrBlockMisalign = (tmp & 0x40) >> 6;
  cardinfo->SD_csd.RdBlockMisalign = (tmp & 0x20) >> 5;
  cardinfo->SD_csd.DSRImpl = (tmp & 0x10) >> 4;
  cardinfo->SD_csd.Reserved2 = 0; /*!< Reserved */

  if ((CardType == SDIO_STD_CAPACITY_SD_CARD_V1_1) || (CardType == SDIO_STD_CAPACITY_SD_CARD_V2_0))
  {
    cardinfo->SD_csd.DeviceSize = (tmp & 0x03) << 10;

    /*!< Byte 7 */
    tmp = (uint8_t)(CSD_Tab[1] & 0x000000FF);
    cardinfo->SD_csd.DeviceSize |= (tmp) << 2;

    /*!< Byte 8 */
    tmp = (uint8_t)((CSD_Tab[2] & 0xFF000000) >> 24);
    cardinfo->SD_csd.DeviceSize |= (tmp & 0xC0) >> 6;

    cardinfo->SD_csd.MaxRdCurrentVDDMin = (tmp & 0x38) >> 3;
    cardinfo->SD_csd.MaxRdCurrentVDDMax = (tmp & 0x07);

    /*!< Byte 9 */
    tmp = (uint8_t)((CSD_Tab[2] & 0x00FF0000) >> 16);
    cardinfo->SD_csd.MaxWrCurrentVDDMin = (tmp & 0xE0) >> 5;
    cardinfo->SD_csd.MaxWrCurrentVDDMax = (tmp & 0x1C) >> 2;
    cardinfo->SD_csd.DeviceSizeMul = (tmp & 0x03) << 1;
    /*!< Byte 10 */
    tmp = (uint8_t)((CSD_Tab[2] & 0x0000FF00) >> 8);
    cardinfo->SD_csd.DeviceSizeMul |= (tmp & 0x80) >> 7;
    
    cardinfo->CardCapacity = (cardinfo->SD_csd.DeviceSize + 1) ;
    cardinfo->CardCapacity *= (1 << (cardinfo->SD_csd.DeviceSizeMul + 2));
    cardinfo->CardBlockSize = 1 << (cardinfo->SD_csd.RdBlockLen);
    cardinfo->CardCapacity *= cardinfo->CardBlockSize;
  }
  else if (CardType == SDIO_HIGH_CAPACITY_SD_CARD)
  {
    /*!< Byte 7 */
    tmp = (uint8_t)(CSD_Tab[1] & 0x000000FF);
    cardinfo->SD_csd.DeviceSize = (tmp & 0x3F) << 16;

    /*!< Byte 8 */
    tmp = (uint8_t)((CSD_Tab[2] & 0xFF000000) >> 24);

    cardinfo->SD_csd.DeviceSize |= (tmp << 8);

    /*!< Byte 9 */
    tmp = (uint8_t)((CSD_Tab[2] & 0x00FF0000) >> 16);

    cardinfo->SD_csd.DeviceSize |= (tmp);

    /*!< Byte 10 */
    tmp = (uint8_t)((CSD_Tab[2] & 0x0000FF00) >> 8);
    
    cardinfo->CardCapacity = ((uint64_t)cardinfo->SD_csd.DeviceSize + 1) * 512 * 1024;
    cardinfo->CardBlockSize = 512;    
  }


  cardinfo->SD_csd.EraseGrSize = (tmp & 0x40) >> 6;
  cardinfo->SD_csd.EraseGrMul = (tmp & 0x3F) << 1;

  /*!< Byte 11 */
  tmp = (uint8_t)(CSD_Tab[2] & 0x000000FF);
  cardinfo->SD_csd.EraseGrMul |= (tmp & 0x80) >> 7;
  cardinfo->SD_csd.WrProtectGrSize = (tmp & 0x7F);

  /*!< Byte 12 */
  tmp = (uint8_t)((CSD_Tab[3] & 0xFF000000) >> 24);
  cardinfo->SD_csd.WrProtectGrEnable = (tmp & 0x80) >> 7;
  cardinfo->SD_csd.ManDeflECC = (tmp & 0x60) >> 5;
  cardinfo->SD_csd.WrSpeedFact = (tmp & 0x1C) >> 2;
  cardinfo->SD_csd.MaxWrBlockLen = (tmp & 0x03) << 2;

  /*!< Byte 13 */
  tmp = (uint8_t)((CSD_Tab[3] & 0x00FF0000) >> 16);
  cardinfo->SD_csd.MaxWrBlockLen |= (tmp & 0xC0) >> 6;
  cardinfo->SD_csd.WriteBlockPaPartial = (tmp & 0x20) >> 5;
  cardinfo->SD_csd.Reserved3 = 0;
  cardinfo->SD_csd.ContentProtectAppli = (tmp & 0x01);

  /*!< Byte 14 */
  tmp = (uint8_t)((CSD_Tab[3] & 0x0000FF00) >> 8);
  cardinfo->SD_csd.FileFormatGrouop = (tmp & 0x80) >> 7;
  cardinfo->SD_csd.CopyFlag = (tmp & 0x40) >> 6;
  cardinfo->SD_csd.PermWrProtect = (tmp & 0x20) >> 5;
  cardinfo->SD_csd.TempWrProtect = (tmp & 0x10) >> 4;
  cardinfo->SD_csd.FileFormat = (tmp & 0x0C) >> 2;
  cardinfo->SD_csd.ECC = (tmp & 0x03);

  /*!< Byte 15 */
  tmp = (uint8_t)(CSD_Tab[3] & 0x000000FF);
  cardinfo->SD_csd.CSD_CRC = (tmp & 0xFE) >> 1;
  cardinfo->SD_csd.Reserved4 = 1;


  /*!< Byte 0 */
  tmp = (uint8_t)((CID_Tab[0] & 0xFF000000) >> 24);
  cardinfo->SD_cid.ManufacturerID = tmp;

  /*!< Byte 1 */
  tmp = (uint8_t)((CID_Tab[0] & 0x00FF0000) >> 16);
  cardinfo->SD_cid.OEM_AppliID = tmp << 8;

  /*!< Byte 2 */
  tmp = (uint8_t)((CID_Tab[0] & 0x000000FF00) >> 8);
  cardinfo->SD_cid.OEM_AppliID |= tmp;

  /*!< Byte 3 */
  tmp = (uint8_t)(CID_Tab[0] & 0x000000FF);
  cardinfo->SD_cid.ProdName1 = tmp << 24;

  /*!< Byte 4 */
  tmp = (uint8_t)((CID_Tab[1] & 0xFF000000) >> 24);
  cardinfo->SD_cid.ProdName1 |= tmp << 16;

  /*!< Byte 5 */
  tmp = (uint8_t)((CID_Tab[1] & 0x00FF0000) >> 16);
  cardinfo->SD_cid.ProdName1 |= tmp << 8;

  /*!< Byte 6 */
  tmp = (uint8_t)((CID_Tab[1] & 0x0000FF00) >> 8);
  cardinfo->SD_cid.ProdName1 |= tmp;

  /*!< Byte 7 */
  tmp = (uint8_t)(CID_Tab[1] & 0x000000FF);
  cardinfo->SD_cid.ProdName2 = tmp;

  /*!< Byte 8 */
  tmp = (uint8_t)((CID_Tab[2] & 0xFF000000) >> 24);
  cardinfo->SD_cid.ProdRev = tmp;

  /*!< Byte 9 */
  tmp = (uint8_t)((CID_Tab[2] & 0x00FF0000) >> 16);
  cardinfo->SD_cid.ProdSN = tmp << 24;

  /*!< Byte 10 */
  tmp = (uint8_t)((CID_Tab[2] & 0x0000FF00) >> 8);
  cardinfo->SD_cid.ProdSN |= tmp << 16;

  /*!< Byte 11 */
  tmp = (uint8_t)(CID_Tab[2] & 0x000000FF);
  cardinfo->SD_cid.ProdSN |= tmp << 8;

  /*!< Byte 12 */
  tmp = (uint8_t)((CID_Tab[3] & 0xFF000000) >> 24);
  cardinfo->SD_cid.ProdSN |= tmp;

  /*!< Byte 13 */
  tmp = (uint8_t)((CID_Tab[3] & 0x00FF0000) >> 16);
  cardinfo->SD_cid.Reserved1 |= (tmp & 0xF0) >> 4;
  cardinfo->SD_cid.ManufactDate = (tmp & 0x0F) << 8;

  /*!< Byte 14 */
  tmp = (uint8_t)((CID_Tab[3] & 0x0000FF00) >> 8);
  cardinfo->SD_cid.ManufactDate |= tmp;

  /*!< Byte 15 */
  tmp = (uint8_t)(CID_Tab[3] & 0x000000FF);
  cardinfo->SD_cid.CID_CRC = (tmp & 0xFE) >> 1;
  cardinfo->SD_cid.Reserved2 = 1;
  
  return(errorstatus);
}

SD卡初始化流程

  1. 发送CMD2获取CID数据
  2. 发送CMD3获取SD卡的RCA地址
  3. 发送CMD9获取SD卡的CSD数据
  4. 根据CID和CSD表格解析数据
相关推荐
hairenjing11236 小时前
使用 Mac 数据恢复从 iPhoto 图库中恢复照片
windows·stm32·嵌入式硬件·macos·word
u01015265810 小时前
STM32F103C8T6学习笔记2--LED流水灯与蜂鸣器
笔记·stm32·学习
北京迅为14 小时前
【北京迅为】《STM32MP157开发板嵌入式开发指南》-第七十八章 Qt控制硬件
linux·stm32·单片机·嵌入式硬件
llhm21 小时前
stm32 踩坑笔记
stm32·单片机·嵌入式硬件
charlie11451419121 小时前
从0开始的STM32之旅8 串口通信(II)
stm32·单片机·嵌入式硬件·c·串口通信
小狗爱吃黄桃罐头1 天前
江协科技STM32学习- P40 硬件SPI读写W25Q64
stm32·江科大
Whappy0012 天前
6. STM32之TIM实验--编码器接口()--(实验5:PWM驱动直流电机)
stm32·单片机·嵌入式硬件
极客小张2 天前
基于STM32的实时监测环境系统设计:Flask、SQLite、Vue.js、HTTP/TCP技术
c语言·stm32·单片机·物联网·毕业设计·课程设计·python后端
Lin2012302 天前
STM32 + CubeMX + 硬件SPI + W5500 +TcpClient
stm32·单片机·嵌入式硬件
极客小张2 天前
基于STM32的智能温室环境监测与控制系统设计(代码示例)
c语言·arm开发·stm32·单片机·嵌入式硬件·物联网·毕业设计