[开发资料] CW32L052 实现串口IAP升级

[复制链接]
 楼主| lulugl 发表于 2023-8-2 21:25 | 显示全部楼层 |阅读模式
本帖最后由 lulugl 于 2023-8-2 21:30 编辑

#申请原创# #有奖活动#[url=home.php?mod=space&uid=760190]@21小跑堂 [/url]
IAP(In applicating Programing)
  • IAP就是通过软件实现在线电擦除和编程的方法。IAP技术是从结构上将Flash存储器映射为两个存储体,当运行一个存储体上的用户程序时,可对另一个存储体重新编程,之后将程序从一个存储体转向另一个。
  • IAP是用户自己的程序在运行过程中对User Flash的部分区域进行烧写。简单来说,就是开发者代码出bug了或者添加新功能了,能够利用预留的通讯接口,对代码进行升级
  • UART、SPI、IIC、USB等等,当然还有wifi、4G、蓝牙等无线通讯手段,都可以作为IAP升级的方式,今天主要介绍如何使用串口对CW32L052固件进行升级。
  • 要想设计IAP,首先需要对MCU的代码启动过程有个了解,先来看看CW32L的代码启动过程是怎样的吧。
  • 首先,我们的CW32L系列的内核为ARMCortex-M0+,在数据手册《CW32L052_DataSheet_CN_V1.1.pdf》表明:
c4ad1952c7e6216c6a5ebcb25047924a
在《ARM Cortex-M0 Cortex-M0+权威指南》(第2版)第7章存储系统一章中描述了Cortex-M0/Mo+处理器架构定义的存储器映射:
78cd2e326c245063b7a07f306894e165
从上图中看到,Cortex-M0+的代码是从地址0x00000000开始,到0x1FFFFFFF的512M存储空间。在CW32L052的数据手册中,他的FLASH有64K的空间,也就是从0x0000-0xFFFF。如下图所示。
6228cbe2e4126892554b3aafa9619295
在《ARM Cortex-M0 Cortex-M0+权威指南》(第2版)第7章第4节中描述程序存储器、Bootloader和存储器重映射中,描述,当Cortex-M0+处理器从复位中启动时,会首先访问0地址的向量表,从而读取MSP的初始值和复位向量,然后从复位和向量开始执行程序。
CW32L052的代码启动过程
1、上电复位后,从 0x0000 0000 地址取出栈顶地址赋给MSP寄存器(主堆栈寄存器),即MSP = __initial_sp。这一步是由硬件自动完成的
2、从0x0000 0004 地址取出复位程序的地址给PC寄存器(程序计数器),即PC = Reset_Handler。这一步也是由硬件自动完成调用SystemInit函数初始化系统时钟
3、跳到C库的__main函数初始化堆栈(初始化时是根据前面的分配的堆空间和栈空间来初始化的)和代码重定位(初始RW 和ZI段),然后跳到main函数执行应用程序
IAP设计思路
大体分为两部分设计,bootloader、APP代码设计,bootloader用于检查APP区代码是否需要更新,以及跳转到APP区执行APP程序
我设计的如下图所示升级流程:
d7d126df812dd227fa9b89836b5d6b1f
Flash分区
CW32L052的flash分区,我们编写好bootloader后,代码的长度为8k,为此我们分把前面的12k分配给bootloader,后面的52k留给APP。分区如下图所示:
d42be8e855e16313c39248d89672a01b
Bootloader区为0x000-0x2FFF,在工程里设置ROM如下图:
a7240de474c85ece15d8c759cf7ca61b
APP大小为52K,ROM起始地址为0x3000,长度为0xD000。MDK设置如下:
bbed43b6e35e868dcbffa7bf8bf01237

Bootloader,我们这次采用的是用串口升级,使用Ymodem协议进行数据传输。在武汉芯源的官方有应用文档( HYPERLINK "https://www.whxy.com/files/doc/CW32F030_IAP_Fuction_CN_V1.0.pdf" )

Ymodem协议
Ymodem协议用于计算机间传输文件,同样适用于嵌入式领域,如MCU升级固件时,可以使用Ymodem协议传输固件文件,传输总线不限于USB、UART、CAN等。
Ymodem 帧格式
Ymodem 有两种帧格式,主要区别是信息块长度不一样。
14862ea73731483957390a6df01bd159
帧头
帧头表示两种数据帧长度,主要是信息块长度不同。
33583339eb00ec5c5af17e1f7c5094af
包序号
数据包序号只有1字节,因此计算范围是0~255;对于数据包大于255的,序号归零重复计算。
帧长度
【1】以SOH(0x01)开始的数据包,信息块是128字节,该类型帧总长度为133字节。
【2】以STX(0x02)开始的数据包,信息块是1024字节,该类型帧总长度为1029字节。
校验
Ymodem采用的是CRC16校验算法,校验值为2字节,传输时CRC高八位在前,低八位在后;CRC计算数据为信息块数据,不包含帧头、包号、包号反码。
Ymodem握手信号
握手信号由接收方发起,在发送方开始传输文件前,接收方需发送YMODEM_C (字符C,ASII码为0x43)命令,发送方收到后,开始传输起始帧。
Ymodem起始帧
Ymodem起始帧并不直接传输文件内容,而是先将文件名和文件大小置于数据帧中传输;起始帧是以SOH 133字节长度帧传输,格式如下。
0345cbaba0aec4ba2a55fde40255b60e
其中包号为固定为0;Filename为文件名称,文件名称后必须加0x00作为结束;Filesize为文件大小值,文件大小值后必须加0x00作为结束;余下未满128字节数据区域,则以0x00填充。
Ymodem数据帧
Ymodem数据帧传输,在信息块填充有效数据。
传输有效数据时主要考虑的是最后一包数据的是处理,SOH帧和STR帧有不同的处理。
【1】对于SOH帧,若余下数据小于128字节,则以0x1A填充,该帧长度仍为133字节。
【2】对于STX帧需考虑几种情况:
●余下数据等于1024字节,以1029长度帧发送;
●余下数据小于1024字节,但大于128字节,以1029字节帧长度发送,无效数据以0x1A填充。
●余下数据等于128字节,以133字节帧长度发送。
●余下数据小于128字节,以133字节帧长度发送,无效数据以0x1A填充。
Ymodem结束帧
Ymodem的结束帧采用SOH 133字节长度帧传输,该帧不携带数据(空包),即数据区、校验都以0x00填充。
26a56fbf4226cafccc36c651998d664b
Ymodem命令
d399601dae8f2ae2f6e57bc503456e36
代码实现:
Ymodem开源的资料非常多。主要实现有4个文件一个是common.c/common.h,ymodem.c/ymodem.h。
Common.c中主要的功能是实现串口接收字符、发送字符的通用接口
  1. /**
  2.   * [url=home.php?mod=space&uid=247401]@brief[/url]  Test to see if a key has been pressed on the HyperTerminal
  3.   * @param  key: The key pressed
  4.   * @retval 1: Correct
  5.   *         0: Error
  6.   */
  7. uint32_t SerialKeyPressed(uint8_t *key)
  8. {
  9.     if (UART_GetFlagStatus(IAP_UARTx, UART_FLAG_RC) != RESET)
  10.     {
  11.         *key = (uint8_t)IAP_UARTx->RDR;
  12.         UART_ClearFlag(IAP_UARTx, UART_FLAG_RC);      
  13.         return 1;
  14.     }
  15.     else
  16.     {
  17.         return 0;
  18.     }
  19. }
  20. /**
  21.   * @brief  Print a character on the HyperTerminal
  22.   * @param  c: The character to be printed
  23.   * @retval None
  24.   */
  25. void SerialPutChar(uint8_t c)
  26. {
  27.     UART_SendData_8bit(IAP_UARTx, c);
  28.     while (UART_GetFlagStatus(IAP_UARTx, UART_FLAG_TXE) == RESET);   
  29. }

串口下载函数SerialDownload:
  1. void SerialDownload(void)
  2. {
  3.     uint8_t Number[10] = " ";
  4.     int32_t Size = 0;
  5.     SerialPutString("\n\n\rWaiting for the file to be sent ... (press 'a' to abort)\n\r");
  6.     Size = Ymodem_Receive(&tab_1024[0]);
  7.     if (Size > 0)
  8.     {
  9.         SerialPutString("\n\n\r Programming Completed Successfully!\n\r--------------------------------\r\n Name: ");
  10.         SerialPutString(file_name);
  11.         Int2Str(Number, Size);
  12.         SerialPutString("\n\r Size: ");
  13.         SerialPutString(Number);
  14.         SerialPutString(" Bytes\r\n");
  15.         SerialPutString("-------------------\n");
  16.         SerialPutString("\n\n\r MCU is going to reset...\n");
  17.         NVIC_SystemReset();
  18.     }
  19.     else if (Size == -1)
  20.     {
  21.         SerialPutString("\n\n\rThe image size is higher than the allowed space memory!\n\r");
  22.     }
  23.     else if (Size == -2)
  24.     {
  25.         SerialPutString("\n\n\rVerification failed!\n\r");
  26.     }
  27.     else if (Size == -3)
  28.     {
  29.         SerialPutString("\r\n\nAborted by user.\n\r");
  30.     }
  31.     else
  32.     {
  33.         SerialPutString("\n\rFailed to receive the file!\n\r");
  34.     }
  35. }

具的代码见附件
Ymodem.c主要是实现数据包的接收与解析、FLASH的刷写,主要的函数有如下两个,一个是接收一个包并做解析的Receive_Packet:
  1. /**
  2.   * @brief  Receive a packet from sender
  3.   * @param  data
  4.   * @param  length
  5.   * @param  timeout
  6.   *     0: end of transmission
  7.   *    -1: abort by sender
  8.   *    >0: packet length
  9.   * @retval 0: normally return
  10.   *        -1: timeout or packet error
  11.   *         1: abort by user
  12.   */
  13. static int32_t Receive_Packet (uint8_t *data, int32_t *length, uint32_t timeout)
  14. {
  15.     uint16_t i, packet_size;
  16.     uint8_t c;
  17.     *length = 0;
  18.     if (Receive_Byte(&c, timeout) != 0)
  19.     {
  20.         return -1;
  21.     }
  22.     switch (c)
  23.     {
  24.         case SOH:
  25.             packet_size = PACKET_SIZE;
  26.             break;
  27.         case STX:
  28.             packet_size = PACKET_1K_SIZE;
  29.             break;
  30.         case EOT:            
  31.             return 0;
  32.         case CA:
  33.             if ((Receive_Byte(&c, timeout) == 0) && (c == CA))
  34.             {
  35.                 *length = -1;
  36.                 return 0;
  37.             }
  38.             else
  39.             {
  40.                 return -1;
  41.             }
  42.         case ABORT1:
  43.         case ABORT2:
  44.             return 1;
  45.         default:
  46.             return -1;
  47.     }
  48.     *data = c;
  49.     for (i = 1; i < (packet_size + PACKET_OVERHEAD); i ++)
  50.     {
  51.         if (Receive_Byte(data + i, timeout) != 0)
  52.         {
  53.             return -1;
  54.         }
  55.     }
  56.     if (data[PACKET_SEQNO_INDEX] != ((data[PACKET_SEQNO_COMP_INDEX] ^ 0xff) & 0xff))
  57.     {
  58.         return -1;
  59.     }
  60.     *length = packet_size;
  61.     return 0;
  62. }

另一个是接收一个文件并刷写到flash里面:
  1. /**
  2.   * @brief  Receive a file using the ymodem protocol
  3.   * @param  buf: Address of the first byte
  4.   * @retval The size of the file
  5.   */
  6. int32_t Ymodem_Receive (uint8_t *buf)
  7. {
  8.     uint8_t packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD], file_size[FILE_SIZE_LENGTH], *file_ptr, *buf_ptr;
  9.     int32_t i, j, packet_length, session_done, file_done, packets_received, errors, session_begin;
  10.     static int32_t size = 0;
  11. int32_t state_receive_p;
  12.     /* Initialize FlashDestination variable */
  13.     FlashDestination = ApplicationAddress;
  14.     for (session_done = 0, errors = 0, session_begin = 0; ;)
  15.     {
  16.         for (packets_received = 0, file_done = 0, buf_ptr = buf; ;)
  17.         {
  18. state_receive_p = Receive_Packet(packet_data, &packet_length, NAK_TIMEOUT);
  19.             switch (state_receive_p)
  20.             {
  21.                 case 0:
  22.                     errors = 0;
  23.                     switch (packet_length)
  24.                     {
  25.                         /* Abort by sender */
  26.                         case - 1:
  27.                             Send_Byte(ACK);
  28.                             return 0;
  29.                         /* End of transmission */
  30.                         case 0:                             
  31.                             if (file_done == 0)
  32.                             {                                
  33.                                 Send_Byte(NAK);
  34.                                 file_done = 1;
  35.                             }
  36.                             else if (file_done == 1)
  37.                             {
  38.                                 Send_Byte(ACK);
  39.                                 Send_Byte(CRC16);
  40.                                 file_done = 2;
  41.                             }                           
  42.                             break;
  43.                         /* Normal packet */
  44.                         default:
  45.                             if ((packet_data[PACKET_SEQNO_INDEX] & 0xff) != (packets_received & 0xff))
  46.                             {
  47.                                 if (file_done == 0)
  48.                                 {
  49.                                     Send_Byte(NAK);
  50.                                 }
  51.                                 else
  52.                                 {
  53.                                     Send_Byte(ACK);
  54.                                     file_done = 3;
  55.                                 }
  56.                             }
  57.                             else
  58.                             {
  59.                                 if (packets_received == 0)
  60.                                 {
  61.                                     /* Filename packet */
  62.                                     if (packet_data[PACKET_HEADER] != 0)
  63.                                     {
  64.                                         /* Filename packet has valid data */
  65.                                         for (i = 0, file_ptr = packet_data + PACKET_HEADER; (*file_ptr != 0) && (i < FILE_NAME_LENGTH);)
  66.                                         {
  67.                                             file_name[i++] = *file_ptr++;
  68.                                         }
  69.                                         file_name[i++] = '\0';
  70.                                         for (i = 0, file_ptr ++; (*file_ptr != ' ') && (i < FILE_SIZE_LENGTH);)
  71.                                         {
  72.                                             file_size[i++] = *file_ptr++;
  73.                                         }
  74.                                         file_size[i++] = '\0';
  75.                                         Str2Int(file_size, &size);
  76.                                         /* Test the size of the image to be sent */
  77.                                         /* Image size is greater than Flash size */
  78.                                         if (size > (FLASH_SIZE - 1))
  79.                                         {
  80.                                             /* End session */
  81.                                             Send_Byte(CA);
  82.                                             Send_Byte(CA);
  83.                                             return -1;
  84.                                         }
  85.                                         /* Erase the needed pages where the user application will be loaded */
  86.                                         /* Define the number of page to be erased */
  87.                                         NbrOfPage = FLASH_PagesMask(size);
  88.                                         /* Erase the FLASH pages */
  89.                                         FLASH_UnlockPages(FlashDestination, FlashDestination+ (PageSize * NbrOfPage));
  90.                                         FLASH_ErasePages(FlashDestination, FlashDestination+ (PageSize * NbrOfPage));
  91.                                         Send_Byte(ACK);
  92.                                         Send_Byte(CRC16);
  93.                                     }
  94.                                     /* Filename packet is empty, end session */
  95.                                     else
  96.                                     {
  97.                                         Send_Byte(ACK);
  98.                                         file_done = 1;
  99.                                         session_done = 1;
  100.                                         break;
  101.                                     }
  102.                                 }
  103.                                 /* Data packet */
  104.                                 else
  105.                                 {
  106.                                     memcpy(buf_ptr, packet_data + PACKET_HEADER, packet_length);                                    
  107.                                     RamSource = (uint32_t)buf;
  108.                                     for (j = 0; (j < packet_length) && (FlashDestination <  ApplicationAddress + size); j += 4)
  109.                                     {
  110.                                         /* 把接收到的数据编写到Flash中 */
  111.                                         FLASH_WriteWords(FlashDestination, (uint32_t*)RamSource, 1);
  112.                                         if (*(uint32_t*)FlashDestination != *(uint32_t*)RamSource)
  113.                                         {
  114.                                             /* End session */
  115.                                             Send_Byte(CA);
  116.                                             Send_Byte(CA);
  117.                                             return -2;
  118.                                         }
  119.                                         FlashDestination += 4;
  120.                                         RamSource += 4;
  121.                                     }
  122.                                     Send_Byte(ACK);
  123.                                 }
  124.                                 packets_received ++;
  125.                                 session_begin = 1;
  126.                             }
  127.                     }
  128.                     break;
  129.                 case 1:
  130.                     Send_Byte(CA);
  131.                     Send_Byte(CA);
  132.                     return -3;
  133.                 default:
  134.                     if (session_begin > 0)
  135.                     {
  136.                         errors ++;
  137.                     }
  138.                     if (errors > MAX_ERRORS)
  139.                     {
  140.                         Send_Byte(CA);
  141.                         Send_Byte(CA);
  142.                         return 0;
  143.                     }
  144.                     Send_Byte(CRC16);
  145.                     break;
  146.             }
  147.             if (file_done == 3)
  148.             {
  149.                 session_done = 1;
  150.                 break;
  151.             }
  152.         }
  153.         if (session_done != 0)
  154.         {
  155.             break;
  156.         }
  157.     }   
  158.     return (int32_t)size;
  159. }

【工程实现步骤】
打开一个初始化串口工程示例,在工程里面添加commom.c/h,以及ymodem.c/h。
e1f5e3f1f25ac104f7c62fd5a97fd987
初始化板载的KEY来做升级检测标志,在复位的1秒之内按下按键,来当做升级标志。如果在1秒之内没有检测到标志,则直接跳转到APP。如果检测到标志,则进入SerialDownload函数,等待上位机发送固件。
  1. int32_t main(void)
  2. {
  3. volatile uint32_t u32Ticks, u32ElapsedTicks;
  4. RCC_Configuration();
  5. InitTick(SystemCoreClock);      // 配置SYSTICK频率为1ms
  6. GPIO_Configuration();
  7. UART_Configuration();
  8. UART_SendString(IAP_UARTx, "start...\r\n");
  9. u32Ticks = GetTick();
  10. do
  11. {
  12. u32ElapsedTicks = GetTick() - u32Ticks;
  13. if (!KEY_GETVALUE())    // 检测按键
  14. {
  15. // 按下按键
  16. break;
  17. }
  18. }
  19.   while(u32ElapsedTicks < 1000);      // 等待1s
  20. if (u32ElapsedTicks < 1000)
  21. {
  22. // 1s内有按键按下,进入串口升级流程
  23.     // 配置串口,波特率115200
  24.   UART_SendString(IAP_UARTx, "serialDownload...\r\n");
  25. SerialDownload();         // 通过YMODEM协议下载升级程序
  26. }
  27. else
  28. {
  29. // 超时,从boolloader程序向用户APP程序跳转
  30. UART_SendString(IAP_UARTx, "goto app...\r\n");
  31. Boot2APP();
  32. }
  33. while (1);
  34. }
  35. 跳转到APP的函数为:
  36. void Boot2APP(void)
  37. {
  38.     __disable_irq();    // 关中断
  39.     if (((*(__IO uint32_t*)ApplicationAddress) & 0x2FFE0000 ) == 0x20000000)  //判断跳转的地址是否有合法程序存在
  40.     {
  41.         // 向用户的APP程序进行跳转
  42.         JumpAddress = *(__IO uint32_t*) (ApplicationAddress + 4);    // ResetHandle函数的地址
  43.         Jump_To_Application = (func_ptr_t) JumpAddress;    // 将地址强制转换为函数指针
  44.         __set_MSP(*(__IO uint32_t*) ApplicationAddress);   // 设置用户APP程序的栈地址
  45.         Jump_To_Application();    // 跳入用户APP程序的ResetHandle处
  46.     }
  47. }

APP程序:
我们在ROM中指定固件的起始地址为0x3000
6a38fc594e0e02b303b6e51a9a8fa056
代码如下,初始IO与串口,进入APP时LED1闪烁,并在串口中打印出字符串。
  1. int32_t main(void)
  2. {
  3.     GPIO_InitTypeDef GPIO_InitStruct = {0};
  4. LogInit();
  5.     RCC_HSI_Enable(RCC_HSIOSC_DIV6);
  6.     __RCC_GPIOC_CLK_ENABLE();
  7.     GPIO_InitStruct.IT = GPIO_IT_NONE;
  8.     GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  9.     GPIO_InitStruct.Pins = LED_GPIO_PINS;
  10.     GPIO_Init(LED_GPIO_PORT, &GPIO_InitStruct);
  11.     while (1)
  12.     {
  13.         GPIO_TogglePin(LED_GPIO_PORT, LED_GPIO_PINS);
  14.         Delay(0xFFFF);
  15. printf("hello cw32l052!\r\n");
  16. printf("这是一个IAP升级的DEMO\r\n");
  17.     }
  18. }

同时为了生成.bin文件,我在user下面添加生成.bin的命令:$K\ARM\ARMCC\bin\fromelf.exe --bin --output=..\@L.bin !L
fb8e01367babd10ad9779acb4f978a64
升级的操作示例我们先把IAP的固件用wch_link烧到开发板,然后打印超级终端,我这里使用SerureCRT的Ymodem发送工具。
cf97d3cdced570059ba02e329c1eccb1
426379ecf213b6543311b509f1d9893b
1c9279f4ebc5d654867d9f5426a801d6
然后我们设置ymodem发送为1024字节:
90da25665a1cb7cc961acc898f96628f
连接终端,开机后按下key1键,就会出现如下提示:
280f7c6c1b963e5ada4019d4d9702859
我们选择需要发送的bin文件:
9559bff17f7d27ee2a2756c84750acab
8318ee2ff7cb3678ea830c7fafa8cb3e
出现如下提示,显示传完成,并且成功的完成APP的跳转。
feeb723d5130cffd78e2a68a05265144
【总结】
经过一个星期的学习,终于掌握了IAP的串口升级活动。主要的难点是如何匹配串口接收完一个数据包,写入flash的原理。
期间遇到非常多的问题,武汉芯源的技术支持耐心的帮助我排查问题,在此特别感谢孙工、张工、吴工。
附件:
Template.zip (2.71 MB, 下载次数: 52)
caigang13 发表于 2023-8-3 08:02 来自手机 | 显示全部楼层
要做好FLASH代码的备份。
 楼主| lulugl 发表于 2023-8-3 08:48 | 显示全部楼层
caigang13 发表于 2023-8-3 08:02
要做好FLASH代码的备份。

如果flash空间够的话,可以考虑双空间。这里是实验性的,只是纯实现。
libotongxun 发表于 2024-6-6 23:13 | 显示全部楼层
中断不能跳转,如何处理
 楼主| lulugl 发表于 2024-6-7 07:55 | 显示全部楼层
libotongxun 发表于 2024-6-6 23:13
中断不能跳转,如何处理

是指什么时候,是升级期间,还是运行APP不能跳转?
libotongxun 发表于 2024-6-7 16:34 | 显示全部楼层
lulugl 发表于 2024-6-7 07:55
是指什么时候,是升级期间,还是运行APP不能跳转?

串口升级正常,跳转也正常,跳到APP运行后,也可以运行,但是中断没有反应,APP中 第一时间开了enable_irq(),中断向量 SCB->VTOR=0x4000u, 在systemint中和main中都试设过。就是中断没反应。

评论

@lulugl :各种方式都试了,就是不行。  发表于 2024-6-8 10:00
这个我也没有详细的去测试,你看一下,是不是中断向量没有映射过去。  发表于 2024-6-8 09:49
 楼主| lulugl 发表于 2024-6-9 08:29 | 显示全部楼层
libotongxun 发表于 2024-6-7 16:34
串口升级正常,跳转也正常,跳到APP运行后,也可以运行,但是中断没有反应,APP中 第一时间开了enable_ir ...

你先不跑APP,先基地址跑一下,看中断是不是正常的。
libotongxun 发表于 2024-6-10 15:03 | 显示全部楼层
基地址是正常的。
gouguoccc 发表于 2024-6-10 22:23 来自手机 | 显示全部楼层
IAP可以通过串口,CAN等方式实现。
AdaMaYun 发表于 2024-6-14 08:40 | 显示全部楼层
IAP升级非常实用简单
小夏天的大西瓜 发表于 2024-6-17 22:54 | 显示全部楼层
升级要做好FLASH代码的备份,以防万一
OKAKAKO 发表于 2024-6-21 21:10 | 显示全部楼层
IAP就是通过软件实现在线电擦除和编程的方法。
中国龙芯CDX 发表于 2024-6-26 16:40 | 显示全部楼层
IAP就是通过软件实现在线电擦除和编程的方法。
jf101 发表于 2024-6-27 16:52 | 显示全部楼层
UART、SPI、IIC、USB等等,当然还有wifi、4G、蓝牙等无线通讯手段,都可以作为IAP升级的方式
您需要登录后才可以回帖 登录 | 注册

本版积分规则

180

主题

830

帖子

12

粉丝
快速回复 在线客服 返回列表 返回顶部