FOTA远程升级 ============ 作者:陈之敏 时间:2020年09月4日 关键字:csdk、RDA8910、二次开发、远程升级 ## 目录 `点击这里查看所有博文 `__   最近听说有的小伙伴看了我的教程后,有一些问题都跑到官方的gitee上面去问去了。导致官方的人没搞懂问的是啥,小伙伴们也没能知道自己想要的答案。给大家造成了困扰,这里我说声抱歉。   既然出现了这个问题,我这里就声明一下,本系列教程所涉及的内容(demo)不是官方的作品。我个人觉得官方的demo内容太多太全,往往都是把一个模块内所有的东西全部放在一起。这样的话对新手不是很友好,阅读起来也比较费劲。我就把官方的部分demo进行相关的简化,并推出教程这样的话可能会对新手朋友们有一定的帮助。   有时候周末闲暇时间我也会加上一些我觉得好玩的模块在里面,这些可能在官方的demo都没有,比如cJSON、PAHO-MQTT、http-client。   这就是官方的代码仓库。 .. code:: c git clone --recursive https://gitee.com/openLuat/Luat_CSDK_Air724U.git   当然各位小伙伴在看本教程时,我建议还是使用我下面提供的仓库比较好,看完之后在迁移到官方的仓库⇧。 .. code:: c git clone --recursive https://gitee.com/chenxiahuaxu/RDA8910_CSDK.git   再看本教程过程中如果遇到了问题,可以在本人的代码仓库下面评论。也可以在本人的\ `博客 `__\ 下面评论。我要是看到的话,并且这个问题在我的能力范围的话我会尽力解答的(非官方,不要对我要求太多哦,要求太多我可能就不管啦)。 一、前言 --------   也有快一个月没折腾csdk,实在是太忙了没时间。上个星期有个小伙伴告诉我远程升级平时用的挺多的,基本上用模块的都需要用远程升级。问我能不能写一篇csdk的远程升级教程。我想了想,好像说的挺有道理的。 .. figure:: https://img-blog.csdnimg.cn/20200903213521964.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70#pic_center :alt: 在这里插入图片描述 在这里插入图片描述   当时就一口答应下来的,结果让我又拖了1个星期的时间,今天好不容易抽出一点时间。就来写一下吧。 .. figure:: https://img-blog.csdnimg.cn/20200903213835272.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70#pic_center :alt: 在这里插入图片描述 在这里插入图片描述   细心的小伙伴可能就发现了,我们的手机系统平时更新。有时候会发现系统只更新底层文件,有时候会发现其实只更新了几个系统app。有时候在跨大版本升级的时候,比如EMUI的大版本升级,往往都会更新安卓的底层,同时伴随着系统的app也会有大量的改动。我猜市面上其他的手机也会采取这样的升级方案,而不是每次都是选择下载几个g的完整包文件而只是为了升级下系统app,这样的话不仅升级慢还浪费流量。   这样的话我们可以大概理解为,市面上的手机远程升级会分为三种,一种是\ ``全部都升级``\ 。一种是\ ``app升级``\ ,一种是\ ``底层软件的升级``\ 。   那大家有没有想过,我为什么要提上面的这些东西呢。 |在这里插入图片描述|   有这个疑问很正常,至少在嵌入式开发上我很少见到有支持上面三种远程升级方式的芯片。它们往往都是选择全量升级。官方提供直接支持的很少,需要用户自己去解析bin文件写flash,自己去实现,你说这多麻烦啊,行不行还得另外再说。   就这一点我不得不说合宙做得很好,csdk开发包可以\ ``完美支持``\ 三种升级方式,直接就可以编译出来升级文件,放到服务器就可以了升级了。只要不更新底层core文件,只是用户app有改动的话。远程升级也需要就几十kb的流量。在物联网模块上缺的就是流量,要是动不动升级来个几兆,那成本就太高了。 .. figure:: https://img-blog.csdnimg.cn/20200904180136433.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70#pic_center :alt: 在这里插入图片描述 在这里插入图片描述 二、准备工作 ------------   我这里使用合宙的物联网云平台的远程升级工具,使用之前需要获取一些信息。各位小伙伴如果是使用\ ``其他的云平台``\ ,或者是使用\ ``自建的服务器``\ ,那么可以跳过这一小节了。   点我打开\ `物联网云平台 `__\ ,登录自己的账号,没有的话注册一个。 |image1|   点击产品信息,找到\ ``ProductKey``\ 保存下来。 |image2|   确认你所使用的设备imei是不是在设备列表中,设备的imei就印在\ ``芯片屏蔽盖``\ 上。 |image3| 三、编写程序 ------------ 3.1、网络连接处理 -----------------   这里是进行网络的连接,网络连接成功后,我们就可以调用\ ``OTA_Start``\ 开始进行OTA请求了。不管升级失败还是成功,在升级结束后,都会选择打印当前运行的固件版本号。 .. code:: c static void TestTask(void *param) { bool NetLink = FALSE; while (NetLink == FALSE) { T_OPENAT_NETWORK_CONNECT networkparam = {0}; switch (NetWorkCbMessage) { case OPENAT_NETWORK_DISCONNECT: //网络断开 表示GPRS网络不可用澹,无法进行数据连接,有可能可以打电话 iot_debug_print("[17_ota] OPENAT_NETWORK_DISCONNECT"); iot_os_sleep(500); break; case OPENAT_NETWORK_READY: //网络已连接 表示GPRS网络可用,可以进行链路激活 iot_debug_print("[17_ota] OPENAT_NETWORK_READY"); memcpy(networkparam.apn, "CMNET", strlen("CMNET")); //建立网络连接,实际为pdp激活流程 iot_network_connect(&networkparam); iot_os_sleep(500); break; case OPENAT_NETWORK_LINKED: //链路已经激活 PDP已经激活,可以通过socket接口建立数据连接 iot_debug_print("[17_ota] OPENAT_NETWORK_LINKED"); NetLink = TRUE; break; } } if (NetLink == TRUE) { if (OTA_Start() == true) iot_debug_print("[17_ota] OTA_Start OK"); else iot_debug_print("[17_ota] OTA_Start false"); while (1) { iot_debug_print("[17_ota] CSDK_VER :%s", CSDK_VER); iot_os_sleep(2000); } } iot_os_delete_task(TestTask_HANDLE); } 3.2、OTA处理 ------------   首先调用\ ``iot_fota_init``\ 初始化ota功能。OTA使用\ ``http``\ 协议向服务器发送GET请求,然后调用\ ``iot_fota_download``\ 将接收到的每一包数据全部传入进去。http请求结束后,调用\ ``iot_fota_done``\ 告诉底层,数据下载完毕。 .. code:: c bool OTA_Start(void) { if (iot_fota_init() != 0) //fail { iot_debug_print("[17_ota] fota_init fail"); return false; } HTTP_SESSION_HANDLE pHTTP = HTTPClientOpenRequest(0); if (HTTPClientSetVerb(pHTTP, VerbGet) != HTTP_CLIENT_SUCCESS) { iot_debug_print("[17_ota] HTTPClientSetVerb error"); return false; } if (HTTPClientAddRequestHeaders(pHTTP, "Accept", "*/*", TRUE) != HTTP_CLIENT_SUCCESS) return false; if (HTTPClientAddRequestHeaders(pHTTP, "Accept-Language", "cn", TRUE) != HTTP_CLIENT_SUCCESS) return false; if (HTTPClientAddRequestHeaders(pHTTP, "User-Agent", "*Mozilla/4.0", TRUE) != HTTP_CLIENT_SUCCESS) return false; if (HTTPClientAddRequestHeaders(pHTTP, "Connection", "Keep-Alive", TRUE) != HTTP_CLIENT_SUCCESS) return false; { char url[256] = {0}; sprintf(url, FOTA_URL_FOR, PRODUCT_KEY, IMEI, CSDK_PRO, CORE_VER, CSDK_VER); if (HTTPClientSendRequest(pHTTP, url, NULL, 0, TRUE, 0, 0) != HTTP_CLIENT_SUCCESS) { iot_debug_print("[17_ota] HTTPClientSendRequest error"); return false; } } if (HTTPClientRecvResponse(pHTTP, 20000) != HTTP_CLIENT_SUCCESS) { iot_debug_print("[17_ota] HTTPClientRecvResponse error"); return false; } CHAR token[32] = {0}; UINT32 tokenSize = 32; if (HTTPClientFindFirstHeader(pHTTP, "content-length", token, &tokenSize) != HTTP_CLIENT_SUCCESS) { iot_debug_print("[17_ota] HTTPClientFindFirstHeader error"); return false; } else { iot_debug_print("[17_ota] HTTPClientFindFirstHeader %d,%s", tokenSize, token); } int fsz = 0; //固件包总大小 if (strlen(token) > 0) { sscanf(token, "%*s %d", &fsz); iot_debug_print("[17_ota]GetSize fsz: %d", fsz); } else { iot_debug_print("[17_ota]GetSize faild"); return false; } HTTPClientFindCloseHeader(pHTTP); UINT32 nRetCode; uint32 readTotalLen = 0; //总读取到字节数 do { char readBuff[512] = {0}; uint32 readSize = 0; //本次读取到字节数 nRetCode = HTTPClientReadData(pHTTP, readBuff, sizeof(readBuff), 300, &readSize); //升级错误码 if (!strncmp("{\"code\":", readBuff, sizeof("{\"code\":") - 1)) { iot_debug_print("[17_ota] Error readBuff:%s", readBuff); return false; } readTotalLen += readSize; //远程升级 if (iot_fota_download(readBuff, readSize, fsz) != 0) { iot_debug_print("[17_ota] iot_fota_download error"); return false; } } while (nRetCode == HTTP_CLIENT_SUCCESS || nRetCode != HTTP_CLIENT_EOS); if (HTTPClientCloseRequest(&pHTTP) != HTTP_CLIENT_SUCCESS) { iot_debug_print("[17_ota] HTTPIntrnConnectionClose error"); return false; } if (iot_fota_done() < 0) { iot_debug_print("[17_ota]fota error"); return false; } return true; } 3.3、用户需要手动改的内容 -------------------------   PRODUCT_KEY 就是上面我们保存的ProductKey。IMEI 是设备屏蔽盖上印刷的内容。 .. code:: c #define FOTA_URL_FOR "http://iot.openluat.com/api/site/firmware_upgrade?project_key=%s&imei=%s&firmware_name=%s_CSDK_RDA8910&core_version=%s&dfota=1&version=%s" #define PRODUCT_KEY "O5wpMkU7KdwSGIbQ6XrufIjr9GutlMyt" #define IMEI "866714044529146" #define CORE_VER "1031"   CORE_VER的话,这个要麻烦点,要自己看下 ``core\iot_SDK_720U\memd\ats_config.h``\ ,要填箭头所指地方的中间的一串数字。是多少就填多少,我这里填\ ``1031``\ 。 |image4|   是不是觉得这好麻烦,还要自己手动填。实不相瞒,其实这些东西都是可以通过程序获取的。我只是懒的写,全写上的话这个demo就复杂了。有这个需求的话可以参考官方的\ ``demo_fota``\ 里面有具体的实现方法。 |image5| ## 3.4、完整的代码   完整的demo在这里,可以复制直接用。 .. code:: c /* * @Author: your name * @Date: 2020-05-19 14:05:32 * @LastEditTime: 2020-05-31 18:58:02 * @LastEditors: Please set LastEditors * @Description: In User Settings Edit * @FilePath: \RDA8910_CSDK\USER\user_main.c */ #include "string.h" #include "cs_types.h" #include "osi_log.h" #include "osi_api.h" #include "am_openat.h" #include "am_openat_vat.h" #include "am_openat_common.h" #include "iot_debug.h" #include "iot_uart.h" #include "iot_os.h" #include "iot_gpio.h" #include "iot_pmd.h" #include "iot_adc.h" #include "iot_vat.h" #include "iot_network.h" #include "iot_socket.h" #include "httpclient.h" #include "am_openat_httpclient.h" HANDLE TestTask_HANDLE = NULL; uint8 NetWorkCbMessage = 0; #define FOTA_URL_FOR "http://iot.openluat.com/api/site/firmware_upgrade?project_key=%s&imei=%s&firmware_name=%s_CSDK_RDA8910&core_version=%s&dfota=1&version=%s" #define PRODUCT_KEY "O5wpMkU7KdwSGIbQ6XrufIjr9GutlMyt" #define IMEI "866714044529146" #define CORE_VER "1031" bool OTA_Start(void) { if (iot_fota_init() != 0) //fail { iot_debug_print("[17_ota] fota_init fail"); return false; } HTTP_SESSION_HANDLE pHTTP = HTTPClientOpenRequest(0); if (HTTPClientSetVerb(pHTTP, VerbGet) != HTTP_CLIENT_SUCCESS) { iot_debug_print("[17_ota] HTTPClientSetVerb error"); return false; } if (HTTPClientAddRequestHeaders(pHTTP, "Accept", "*/*", TRUE) != HTTP_CLIENT_SUCCESS) return false; if (HTTPClientAddRequestHeaders(pHTTP, "Accept-Language", "cn", TRUE) != HTTP_CLIENT_SUCCESS) return false; if (HTTPClientAddRequestHeaders(pHTTP, "User-Agent", "*Mozilla/4.0", TRUE) != HTTP_CLIENT_SUCCESS) return false; if (HTTPClientAddRequestHeaders(pHTTP, "Connection", "Keep-Alive", TRUE) != HTTP_CLIENT_SUCCESS) return false; { char url[256] = {0}; sprintf(url, FOTA_URL_FOR, PRODUCT_KEY, IMEI, CSDK_PRO, CORE_VER, CSDK_VER); if (HTTPClientSendRequest(pHTTP, url, NULL, 0, TRUE, 0, 0) != HTTP_CLIENT_SUCCESS) { iot_debug_print("[17_ota] HTTPClientSendRequest error"); return false; } } if (HTTPClientRecvResponse(pHTTP, 20000) != HTTP_CLIENT_SUCCESS) { iot_debug_print("[17_ota] HTTPClientRecvResponse error"); return false; } CHAR token[32] = {0}; UINT32 tokenSize = 32; if (HTTPClientFindFirstHeader(pHTTP, "content-length", token, &tokenSize) != HTTP_CLIENT_SUCCESS) { iot_debug_print("[17_ota] HTTPClientFindFirstHeader error"); return false; } else { iot_debug_print("[17_ota] HTTPClientFindFirstHeader %d,%s", tokenSize, token); } int fsz = 0; //固件包总大小 if (strlen(token) > 0) { sscanf(token, "%*s %d", &fsz); iot_debug_print("[17_ota]GetSize fsz: %d", fsz); } else { iot_debug_print("[17_ota]GetSize faild"); return false; } HTTPClientFindCloseHeader(pHTTP); UINT32 nRetCode; uint32 readTotalLen = 0; //总读取到字节数 do { char readBuff[512] = {0}; uint32 readSize = 0; //本次读取到字节数 nRetCode = HTTPClientReadData(pHTTP, readBuff, sizeof(readBuff), 300, &readSize); //升级错误码 if (!strncmp("{\"code\":", readBuff, sizeof("{\"code\":") - 1)) { iot_debug_print("[17_ota] Error readBuff:%s", readBuff); return false; } readTotalLen += readSize; //远程升级 if (iot_fota_download(readBuff, readSize, fsz) != 0) { iot_debug_print("[17_ota] iot_fota_download error"); return false; } } while (nRetCode == HTTP_CLIENT_SUCCESS || nRetCode != HTTP_CLIENT_EOS); if (HTTPClientCloseRequest(&pHTTP) != HTTP_CLIENT_SUCCESS) { iot_debug_print("[17_ota] HTTPIntrnConnectionClose error"); return false; } if (iot_fota_done() < 0) { iot_debug_print("[17_ota]fota error"); return false; } return true; } static void TestTask(void *param) { bool NetLink = FALSE; while (NetLink == FALSE) { T_OPENAT_NETWORK_CONNECT networkparam = {0}; switch (NetWorkCbMessage) { case OPENAT_NETWORK_DISCONNECT: //网络断开 表示GPRS网络不可用澹,无法进行数据连接,有可能可以打电话 iot_debug_print("[17_ota] OPENAT_NETWORK_DISCONNECT"); iot_os_sleep(500); break; case OPENAT_NETWORK_READY: //网络已连接 表示GPRS网络可用,可以进行链路激活 iot_debug_print("[17_ota] OPENAT_NETWORK_READY"); memcpy(networkparam.apn, "CMNET", strlen("CMNET")); //建立网络连接,实际为pdp激活流程 iot_network_connect(&networkparam); iot_os_sleep(500); break; case OPENAT_NETWORK_LINKED: //链路已经激活 PDP已经激活,可以通过socket接口建立数据连接 iot_debug_print("[17_ota] OPENAT_NETWORK_LINKED"); NetLink = TRUE; break; } } if (NetLink == TRUE) { if (OTA_Start() == true) iot_debug_print("[17_ota] OTA_Start OK"); else iot_debug_print("[17_ota] OTA_Start false"); while (1) { iot_debug_print("[17_ota] CSDK_VER :%s", CSDK_VER); iot_os_sleep(2000); } } iot_os_delete_task(TestTask_HANDLE); } static void NetWorkCb(E_OPENAT_NETWORK_STATE state) { NetWorkCbMessage = state; } //main函数 int appimg_enter(void *param) { //注册网络状态回调函数 iot_network_set_cb(NetWorkCb); //关闭看门狗,死机不会重启。默认打开 iot_debug_set_fault_mode(OPENAT_FAULT_HANG); //打开调试信息,默认关闭 iot_vat_send_cmd("AT^TRACECTRL=0,1,3\r\n", sizeof("AT^TRACECTRL=0,1,3\r\n")); //创建一个任务 TestTask_HANDLE = iot_os_create_task(TestTask, NULL, 4096, 10, OPENAT_OS_CREATE_DEFAULT, "TestTask"); return 0; } //退出提示 void appimg_exit(void) { OSI_LOGI(0, "application image exit"); } 四、编译程序 ------------   编译的话要注意下,这里和平时不太一样。先说下编译的命令把,编译的命令有三个。 - ``.\17_OTA.bat fota app`` 只编译远程升级app的.dfota.bin文件 - ``.\17_OTA.bat fota core`` 只编译远程升级底层升级的.dfota.bin文件 - ``.\17_OTA.bat fota all`` 编译远程升级全量升级的.dfota.bin文件   我们等会需要使用\ ``.\17_OTA.bat fota app`` 来编译两个不同的版本。 4.1、上传升级文件 -----------------   合宙的物联网云平台升级会校验软件版本。为了节省时间,测试的话我们需要先编译一个高版本的软件传上去。   打开\ ``project\17_OTA.bat``\ 文件,将CSDK_VER修改为\ ``1.0.1``\ 。然后就可以编译了。 .. code:: c @echo off set CSDK_VER=1.0.1 set CSDK_PRO=17_OTA set FOTA_FLAG=%1 set FOTA_TYPE=%2 set AM_MODEL=iot_SDK_720U call ..\tools\core_launch.bat 17_OTA REM/*+\NEW\chenzhimin\2020.07.22\*/ cd %PROJECT_OUT% cmake ..\.. -G Ninja ^ -D CONFIG_BUILD_APP_HTTP=ON ninja cd ..\..\project REM/*-\NEW\chenzhimin\2020.07.22\*/   编译成功后,回到物联网平台。点击下图新建固件 .. figure:: https://img-blog.csdnimg.cn/20200904183852605.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70#pic_center :alt: 在这里插入图片描述 在这里插入图片描述   选择刚刚编译的\ ``hex\Air720U_CSDK_demo_17_OTA\17_OTA_1.0.1_CSDK_V1031_RDA8910.dfota.bin``\ 文件,点击确定 |image6|   添加需要升级的设备\ ``imei``\ 号。 |image7|   填你自己的,点确定。(在芯片屏蔽盖上) |image8| ## 4.2、编译低版本下载   打开\ ``project\17_OTA.bat``\ 文件,将CSDK_VER修改为\ ``1.0.0``\ 。然后就可以编译下载了。下载程序之前打开\ ``coolwatch``\ 看日志!!!注意了是\ ``下载普通的pac包``\ ,你别把bin文件下载了。 |image9|   升级一共花了十秒钟时间,日志已经提示OTA_Start OK。也能看到当前版本是1.0.0。 |image10|   重启下看看,是不是还能继续升级。日志提示升级失败,错误码:27(意思就是当前运行的是最新版本)。当前当前是1.0.1。说明刚才的升级是成功的。 |image11|   有关上述升级的错误码,\ `点我查看 `__\ 。 不会下载的\ `点击这里 `__\ ,进去查看我的\ ``RDA8910 CSDK二次开发入门教程``\ 专题第一篇博文\ ``1、RDA8910CSDK二次开发:环境搭建``\ 里面讲了怎么下载 这里只是我的学习笔记,拿出来给大家分享,欢迎大家批评指正,本篇教程到此结束 .. |在这里插入图片描述| image:: https://img-blog.csdnimg.cn/2020090417534855.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70#pic_center .. |image1| image:: https://img-blog.csdnimg.cn/20200904180727149.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70#pic_center .. |image2| image:: https://img-blog.csdnimg.cn/20200904180908203.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70#pic_center .. |image3| image:: https://img-blog.csdnimg.cn/20200904181042912.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70#pic_center .. |image4| image:: https://img-blog.csdnimg.cn/20200904182337260.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70#pic_center .. |image5| image:: https://img-blog.csdnimg.cn/20200904182659622.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70#pic_center .. |image6| image:: https://img-blog.csdnimg.cn/20200904184052559.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70#pic_center .. |image7| image:: https://img-blog.csdnimg.cn/20200904184148933.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70#pic_center .. |image8| image:: https://img-blog.csdnimg.cn/20200904184241208.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70#pic_center .. |image9| image:: https://img-blog.csdnimg.cn/20200904184648667.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70#pic_center .. |image10| image:: https://img-blog.csdnimg.cn/20200904184837884.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70#pic_center .. |image11| image:: https://img-blog.csdnimg.cn/20200904185302100.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70#pic_center