基于STM32F407芯片与IAP技术的OTA项目总结

 概述  

        经过我的专研,终于实现了OTA升级功能。开发的过程中思考良多,于是通过该贴来分享一下,下面即进入正题。首先以我个人的理解对IAP和OTA进行一个通俗易懂的介绍。

1、IAP(In-Application Programming,应用内编程)

        即利用bootloader程序将通过串口/CAN/SPI/TCP等通讯方式接收到的固件烧录至芯片内部Flash并跳转至该区域运行该固件程序。所以IAP应该至少有两部分组成,第一部分是bootloader程序,具备接收、烧录、运行程序的功能,第二部分是APP程序,需要根据具体的应用进行开发。如果开发板搭载芯片的Flash空间够大或者有外部Flash可以考虑冗余设计,即存储两个APP程序,以防其中一个APP程序在运行过程中损坏导致系统不可以正常运行,此时即可启动备用程序进行工作,或者将损坏的程序用备用程序进行替换,以维持设备正常运转。

2、OTA(Over-The-Air,空中下载)

        即通过无线通信技术进行固件的下载与更新,如常见的车机与物联网设备系统升级,就是通过无线网从服务器(如阿里云、巴法云、百度天工等)中下载固件包到本地,再由本地bootloader程序进行安装升级。由于要实现该功能需要程序中搭载网络协议、网络硬件驱动、物联网服务器平台提供的SDK包等代码,会占用较大的存储空间,而bootloader程序要求精简高效、稳定可靠,且一般APP程序中也会有网络通信的需求,所有OTA功能通常集成在APP程序中。

       OTA项目开发的流程:程序由bootloader和APP程序两部分组成,因为bootloader程序需要有APP程序才能进行开发调式,所以应该先开发APP程序,再进行bootloader程序开发。

一、开发板硬件和基本功能要求

        由于APP程序集成了FreeRTOS系统和LWIP协议栈,故其体积较大,所以需要开发板具备充足的内存资源和网络通信硬件,本人使用的开发板上搭载:STM32F407ZGT6芯片(CPU芯片)、LAN8720A芯片(PHY芯片)、XM8A51216芯片(外部SRAM)、W25Q128(外部FLASH)等芯片,然后开发板需要具备基本的串口通信和Debugger调式功能。

二、OTA功能的APP程序开发

        APP程序通过HAL库可以构建工程,缩短项目了开发周期,由于STM32F407芯片资源的限制,故移植了轻量级的lwip协议栈,物联网平台选择阿里云。以下为本人APP程序开发的步骤:

        第一步:基于HAL库移植FreeRTOS系统。

        第二步:移植Lwip协议栈。

        第三步:移植阿里云 IoT(物联网)平台提供的OTA模块C_Link_SDK开发工具包

        第一、二步很简单,我就不进行叙述了,每次移植完后需要运行测试程序,观察移植是否成功,否则等到后面可能会增加了排查程序报错原因的工作量。

        下面着重介绍下阿里云 IoT(物联网)平台OTA模块C_Link_SDK开发工具包的获取与移植。

2.1 OTA模块C_Link_SDK的获取

        阿里云物联网平台网址:阿里云权益中心_助力学生、开发者、企业用云快速上云-阿里云

 操作步骤:

        第一步:在阿里云平台注册账号,进入物联网平台(产品->物联网->物联网平台->管理控制台)并开通公共实例

8d9228f703fc4a819d1126a3596c42e9.png

        第二步:进入公共实例,设备管理->产品->创建产品(每个模块下都是视频教程和相应的帮助文档)

898c59dfa6ac4794a3d99362fd38cbad.png

        第三步:设备管理->设备->添加设备(这个模块下没有视频教程,但是有帮助文档)

c72d098f20aa49a4b30d6240d4896916.png

        第四步:使用MQTT.fx或者MQTTX软件接入阿里云平台,尝试订阅和发布主题(该步建议操作和了解一下,非必须执行,阿里云帮助文档里有教程)

f7fa506023f14ea09dc511ebb56e29e9.png

        第五步:文档与工具->设备接入SDK->SDK定制->SDK功能配置->开始生成

        SDK功能配置:

                1、设备OS->FreeRTOS

                2、设备硬件形态->单板系统

                3、连接物联网平台协议->MQTT3.1.1

                4、数据加密->TLS-CA

                5、设备认证方案->设备秘钥

                6、高级能力->MQTT-OTA

a6b77932211fecaed42505a204bb5e58.png

 通过以上五步即可获取阿里云OTA模块的SDK开发工具包。

2.2 C_Link_SDK开发工具包的介绍

将得到的工具包进行解压,里面有五个文件夹,如下所示:

1388f10633ed31343c6ec987cd34e78f.png

以我个人的理解进行简单的介绍,详细描述的可见阿里云帮助文档。

components文件夹:对应于SDK功能配置中高级能力选项,选择不同能力将会得到不同的api接口用于在自己的主程序中进行调用。比如OTA的就有相应的订阅发布OTA主题、打印相关日志、下载固件等程序。

core文件夹:内核文件,这应该是共用的。

demos文件夹:示例代码,比如本人APP程序开发参考的就是demos/mota_basic_demo.c文件

external文件夹:mbedtls的C语言库,即网络通信加密代码

profiles文件夹:对应于SDK功能配置中设备OS接口,里面涉及内存申请、网络连接的建立和通信等程序

2.3 C_Link_SDK开发工具包的移植

        移植SDK前可将FreeRTOS中的内存管理策略中的heap_4.c或heap_5.c文件中的ucHeap[ configTOTAL_HEAP_SIZE ]即FreeRTOS的内存堆栈使用关键字__attribute__指定到外部SRAM地址中。可解决后续运行内存不足的问题。

        因为该项目属于我的第一个项目,所以经验有限。在移植SDK后编译发现执行空间不足,通过相关资料查阅也尝试了一些办法,如使用加载扩散文件 (Scatter File,文件扩展名为 .sct),利用外部SRAM解决堆栈不足的问题,但是经过排查发现与网络初始化函数有冲突,而该函数中又涉及到一些内存分配与其他函数的调用,就放弃了加载扩散文件这个解决方案,经本人尝试在FreeRTOS系统中(无其他系统、协议栈)使用是加载扩散文件可行的。

        导致内存不足的主要原因是mbedtls程序,也是在我将程序调通后,通过串口打印出发现其占用使用内存高达46~49KB,再加上lwip协议栈和其他程序中占用的运行内存等,芯片内部SRAM不够用,而且IAP设计还得给bootloader分配一部分堆栈内存,所以决定让FreeRTOS使用了外部SRAM。

移植SDK操作步骤:

第一步:将C_Link_SDK文件中的LinkSDK文件夹拷贝至工程中的Middlewares文件夹下。

        在keil组件中添加上除demos文件以外所有的源文件与头文件路径:

1de6b6cb0643448f9ba1a8841f6a8d30.png

1de6b6cb0643448f9ba1a8841f6a8d30.png

 

012b40a8a7293eef44d73dcbce81b0a1.png

第二步 修改freertos_port.c文件,网络头文件和RTOS头文件替换为自己lwip_demo.c与freertos_demo.c中的

/* TODO:网络头文件,用户应该根据实际的接口进行配置 */
//#include <fcntl.h>
//#include <sys/types.h>
//#include <sys/time.h>
//#include <sys/socket.h>
//#include <sys/select.h>
//#include <sys/types.h>
//#include <errno.h>
//#include <netdb.h>
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include "lwip/api.h"
#include "lwip/opt.h"
#include "./lwip/netdb.h"

/* TODO:RTOS头文件,用户应该根据实际的接口进行配置 */
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
 

 第三步 修改_core_sysdep_network_connect函数,将exit函数注释,加上vTaskDelete(NULL)

        将demos文件夹中的mota_basic_demo.c代码复制到lwip_demo.c文件中。

        (1)修改三元组、终端节点和端口号

        (2)参照demos文件夹中的mqtt_basic_demo.c文件,在lwip_demo.c中使用sys_thread_new函数创建保活线程和接收线程

        (3)将wip_demo.c文件里user_download_recv_handler函数中下载固件函数替换为下载到W25Q128中

        lwip_demo.c代码:

/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "queue.h"
#include "task.h"
/* 标准库头文件 */
#include "stdint.h"
#include "string.h"
#include "stdio.h"
/* BSP头文件 */
#include "./BSP/LCD/lcd.h"
#include "./MALLOC/malloc.h"
#include  "./BSP/NORFLASH/norflash.h"
/* LWIP协议头文件 */
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include "lwip/api.h"
#include "lwip/opt.h"
#include "./lwip_app/lwip_demo.h"
#include "./lwip_app/hmac.h"
#include "./lwip/apps/mqtt.h"
#include "./lwip/netdb.h"
/* 阿里云C_Link_SDK头文件 */
#include "aiot_state_api.h"
#include "aiot_sysdep_api.h"
#include "aiot_mqtt_api.h"
#include "aiot_ota_api.h"
#include "aiot_mqtt_download_api.h"

/* TODO: 替换为自己设备的三元组 */
const char *product_key       = "k251uTNQwq2";
const char *device_name       = "STM32F407-MQTT";
const char *device_secret     = "2150cb97a7088b18186e51e64d430805";

/* 开发配置终端节点与端口号 */
const char  *mqtt_host = "iot-06z00ix5umy58bw.mqtt.iothub.aliyuncs.com";
const uint16_t port = 8883;

/* 位于external/ali_ca_cert.c中的服务器证书 */
extern const char *ali_ca_cert;
void *g_ota_handle = NULL;
void *g_dl_handle = NULL;
uint32_t g_firmware_size = 0;

/* 位于portfiles/aiot_port文件夹下的系统适配函数集合 */
extern aiot_sysdep_portfile_t g_aiot_sysdep_portfile;

/*              W25Q128相关参数定义                      */
#define Firmware_START_ADDR  0X100000      //固件写入的起始地址
#define Firmware_Length_ADDR 0X164000      //固件长度的起始地址
#define W25Q128_SECTOR_SIZE 4096          //扇区大小为4KB
uint32_t data_buffer_len = 0;             //固件大小


/*                  保活线程与接收线程                         */
#define MQTT_PROCESS_THREAD_PRIO       ( tskIDLE_PRIORITY + 4 )    			//心跳线程
#define MQTT_RECV_THREAD_PRIO          ( tskIDLE_PRIORITY + 3 )    			//接收线程
static  uint8_t g_mqtt_process_thread_running = 0;
static  uint8_t g_mqtt_recv_thread_running = 0;

/* 执行aiot_mqtt_process的线程, 包含心跳发送和QoS1消息重发 */
void mqtt_process_thread(void *args)
{
    int32_t res = STATE_SUCCESS;

    while (g_mqtt_process_thread_running) {
        res = aiot_mqtt_process(args);
        if (res == STATE_USER_INPUT_EXEC_DISABLED) {
            break;
        }
        vTaskDelay(1);
    }
}

/* 执行aiot_mqtt_recv的线程, 包含网络自动重连和从服务器收取MQTT消息 */
void mqtt_recv_thread(void *args)
{
    int32_t res = STATE_SUCCESS;

    while (g_mqtt_recv_thread_running) {
        res = aiot_mqtt_recv(args);
        if (res < STATE_SUCCESS) {
            if (res == STATE_USER_INPUT_EXEC_DISABLED) {
                break;
            }
            vTaskDelay(1);
        }
    }
}

/* 日志回调函数, SDK的日志会从这里输出 
    打印调式需要
*/
int32_t demo_state_logcb(int32_t code, char *message)
{
    printf("%s", message);
    return 0;
}

/* MQTT事件回调函数, 当网络连接/重连/断开时被触发, 事件定义见core/aiot_mqtt_api.h */
void demo_mqtt_event_handler(void *handle, const aiot_mqtt_event_t *event, void *userdata)
{
    switch (event->type) {
    /* SDK因为用户调用了aiot_mqtt_connect()接口, 与mqtt服务器建立连接已成功 */
    case AIOT_MQTTEVT_CONNECT: {
        printf("AIOT_MQTTEVT_CONNECT\r\n");
        /* TODO: 处理SDK建连成功, 不可以在这里调用耗时较长的阻塞函数 */
    }
    break;

    /* SDK因为网络状况被动断连后, 自动发起重连已成功 */
    case AIOT_MQTTEVT_RECONNECT: {
        printf("AIOT_MQTTEVT_RECONNECT\r\n");
        /* TODO: 处理SDK重连成功, 不可以在这里调用耗时较长的阻塞函数 */
    }
    break;

    /* SDK因为网络的状况而被动断开了连接, network是底层读写失败, heartbeat是没有按预期得到服务端心跳应答 */
    case AIOT_MQTTEVT_DISCONNECT: {
        char *cause = (event->data.disconnect == AIOT_MQTTDISCONNEVT_NETWORK_DISCONNECT) ? ("network disconnect") :
                      ("heartbeat disconnect");
        printf("AIOT_MQTTEVT_DISCONNECT: %s\r\n", cause);
        /* TODO: 处理SDK被动断连, 不可以在这里调用耗时较长的阻塞函数 */
    }
    break;
    default: {
    }
    }
}

/* MQTT默认消息处理回调, 当SDK从服务器收到MQTT消息时, 且无对应用户回调处理时被调用 */
void demo_mqtt_default_recv_handler(void *handle, const aiot_mqtt_recv_t *const packet, void *userdata)
{
    switch (packet->type) {
    case AIOT_MQTTRECV_HEARTBEAT_RESPONSE: {
        printf("heartbeat response\r\n");
        /* TODO: 处理服务器对心跳的回应, 一般不处理 */
    }
    break;
    case AIOT_MQTTRECV_SUB_ACK: {
        printf("suback, res: -0x%04X, packet id: %d, max qos: %d\r\n",
               packet->data.sub_ack.res, packet->data.sub_ack.packet_id, packet->data.sub_ack.max_qos);
        /* TODO: 处理服务器对订阅请求的回应, 一般不处理 */
    }
    break;
    case AIOT_MQTTRECV_PUB: {
        printf("pub, qos: %d, topic: %.*s\r\n", packet->data.pub.qos, packet->data.pub.topic_len, packet->data.pub.topic);
        printf("pub, payload: %.*s\r\n", packet->data.pub.payload_len, packet->data.pub.payload);
        /* TODO: 处理服务器下发的业务报文 */
    }
    break;
    case AIOT_MQTTRECV_PUB_ACK: {
        printf("puback, packet id: %d\r\n", packet->data.pub_ack.packet_id);
        /* TODO: 处理服务器对QoS1上报消息的回应, 一般不处理 */
    }
    break;
    default: {

    }
    }
}

/* 下载收包回调, 用户调用 aiot_download_recv() 后, SDK收到数据会进入这个函数, 把下载到的数据交给用户 */
/* TODO: 一般来说, 设备升级时, 会在这个回调中, 把下载到的数据写到Flash上 */
void user_download_recv_handler(void *handle, const aiot_mqtt_download_recv_t *packet, void *userdata)
{
    uint8_t verify_buffer[128];
    static uint32_t write_addr = Firmware_START_ADDR;  // 写入起始地址
    uint8_t* data = (uint8_t *)packet->data.data_resp.data;  // 数据指针
    uint16_t data_size = packet->data.data_resp.data_size;  // 数据大小

    /* 目前只支持 packet->type 为 AIOT_DLRECV_HTTPBODY 的情况 */
    if (!packet || AIOT_MDRECV_DATA_RESP != packet->type) {
        return;
    }
    /* 调用W25Q128读写函数将固件下载到W25Q128中,将其作为OTA数据缓冲区 */
    //1、擦除目标区域并写入数据
    norflash_write(data,write_addr,data_size);
    //2、显示下载进度
    data_buffer_len += packet->data.data_resp.data_size;
    printf("download %03d%% done, %d bytes\r\n", packet->data.data_resp.percent, data_buffer_len);
    //2、验证写入结果
    for(uint16_t offset = 0; offset < data_size; offset += sizeof(verify_buffer))
    {
      uint16_t read_size = (data_size - offset > sizeof(verify_buffer)) ? sizeof(verify_buffer) : (data_size - offset);
      //从W25Q128中读取
      norflash_read(verify_buffer,write_addr+offset,read_size);
      //比较写入数据和读取数据
      if (memcmp(data + offset, verify_buffer, read_size) != 0)
      {
        printf("固件下载失败!!!\n");
      }
    }
    write_addr += data_size;
    printf("写入地址:0x%x\n",write_addr);
}

/* 保存固件长度至W25Q128的0X164000 */
void save_firmware_length(void)
{
  uint32_t packetlength = 0;
   norflash_write((uint8_t *)&data_buffer_len,Firmware_Length_ADDR,sizeof(uint32_t));   //将32位的data_buffer_len的值分解为4个字节,并以字节为单位逐个写入flash
    norflash_read((uint8_t *)&packetlength,Firmware_Length_ADDR,sizeof(uint32_t));
    printf("从w25q128中读取到的固件长度为:0x%x\n",packetlength);
}

/* 用户通过 aiot_ota_setopt() 注册的OTA消息处理回调, 如果SDK收到了OTA相关的MQTT消息, 会自动识别, 调用这个回调函数 */
void user_ota_recv_handler(void *ota_handle, aiot_ota_recv_t *ota_msg, void *userdata)
{
    uint32_t request_size = 2 * 4 * 1024;           //每次传输8KB固件包,一共122KB
    switch (ota_msg->type) {
    case AIOT_OTARECV_FOTA: {
        if (NULL == ota_msg->task_desc || ota_msg->task_desc->protocol_type != AIOT_OTA_PROTOCOL_MQTT) {
            break;
        }

        if(g_dl_handle != NULL) {
            aiot_mqtt_download_deinit(&g_dl_handle);
        }

        printf("OTA target firmware version: %s, size: %u Bytes\r\n", ota_msg->task_desc->version,
               ota_msg->task_desc->size_total);
        void *md_handler = aiot_mqtt_download_init();
        /* 根据AIOT_MDOPT_TASK_DESC选项,将OTA消息中携带的url, version, digest method, sign等信息复制过来 */
        aiot_mqtt_download_setopt(md_handler, AIOT_MDOPT_TASK_DESC, ota_msg->task_desc);
        /* 根据AIOT_MDOPT_DATA_REQUEST_SIZE选项,设置下载一包的大小,当前为4k */
        aiot_mqtt_download_setopt(md_handler, AIOT_MDOPT_DATA_REQUEST_SIZE, &request_size);
        /* 根据AIOT_MDOPT_RECV_HANDLE选项,调用user_download_recv_handler函数进行固件下载 */
        aiot_mqtt_download_setopt(md_handler, AIOT_MDOPT_RECV_HANDLE, user_download_recv_handler);
        g_dl_handle = md_handler;
    }
    break;
    default:
      break;
    }
}

void lwip_demo(void)
{
    int32_t res = STATE_SUCCESS;
    void *mqtt_handle = NULL;
    aiot_sysdep_network_cred_t cred; /* 安全凭据结构体, 如果要用TLS, 这个结构体中配置CA证书等参数 */
    char *cur_version = NULL;
    void *ota_handle = NULL;

    /* 配置SDK的底层依赖 */
    aiot_sysdep_set_portfile(&g_aiot_sysdep_portfile);

    /* 配置SDK的日志输出 */
    aiot_state_set_logcb(demo_state_logcb);

    /* 创建SDK的安全凭据, 用于建立TLS连接 */
    memset(&cred, 0, sizeof(aiot_sysdep_network_cred_t));
    cred.option = AIOT_SYSDEP_NETWORK_CRED_SVRCERT_CA;  /* 使用RSA证书校验MQTT服务端 */
    cred.max_tls_fragment = 16384; /* 最大的分片长度为16K, 其它可选值还有4K, 2K, 1K, 0.5K */
    cred.sni_enabled = 1;                               /* TLS建连时, 支持Server Name Indicator */
    cred.x509_server_cert = ali_ca_cert;                 /* 用来验证MQTT服务端的RSA根证书 */
    cred.x509_server_cert_len = strlen(ali_ca_cert);     /* 用来验证MQTT服务端的RSA根证书长度 */

    /* 创建1个MQTT客户端实例并内部初始化默认参数 */
    mqtt_handle = aiot_mqtt_init();
    if (mqtt_handle == NULL) {
        printf("创建连接结构体失败\r\n");;
    }

    /* 配置MQTT服务器地址 */
    aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_HOST, (void *)mqtt_host);
    /* 配置MQTT服务器端口 */
    aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_PORT, (void *)&port);
    /* 配置设备productKey */
    aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_PRODUCT_KEY, (void *)product_key);
    /* 配置设备deviceName */
    aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_DEVICE_NAME, (void *)device_name);
    /* 配置设备deviceSecret */
    aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_DEVICE_SECRET, (void *)device_secret);
    /* 配置网络连接的安全凭据, 上面已经创建好了 */
    aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_NETWORK_CRED, (void *)&cred);
    /* 配置MQTT默认消息接收回调函数 */
    aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_RECV_HANDLER, (void *)demo_mqtt_default_recv_handler);
    /* 配置MQTT事件回调函数 */
    aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_EVENT_HANDLER, (void *)demo_mqtt_event_handler);

    /* 创建一个单独的线程, 专用于执行aiot_mqtt_process, 它会自动发送心跳保活, 以及重发QoS1的未应答报文 */
    g_mqtt_process_thread_running = 1;
    sys_thread_new("mqtt_process_thread", mqtt_process_thread, NULL, 512, MQTT_PROCESS_THREAD_PRIO );


    /* 创建一个单独的线程用于执行aiot_mqtt_recv, 它会循环收取服务器下发的MQTT消息, 并在断线时自动重连 */
    g_mqtt_recv_thread_running = 1;
    sys_thread_new("mqtt_recv_thread", mqtt_recv_thread, NULL, 512, MQTT_RECV_THREAD_PRIO );

    /* 与MQTT例程不同的是, 这里需要增加创建OTA会话实例的语句 */
    ota_handle = aiot_ota_init();
    if (NULL == ota_handle) {
        goto exit;
    }

    /* 用以下语句, 把OTA会话和MQTT会话关联起来 */
    aiot_ota_setopt(ota_handle, AIOT_OTAOPT_MQTT_HANDLE, mqtt_handle);
    /* 用以下语句, 设置OTA会话的数据接收回调, SDK收到OTA相关推送时, 会进入这个回调函数 */
    aiot_ota_setopt(ota_handle, AIOT_OTAOPT_RECV_HANDLER, user_ota_recv_handler);
    g_ota_handle = ota_handle;

    /* 与服务器建立MQTT连接 */
    res = aiot_mqtt_connect(mqtt_handle);
    if (res < STATE_SUCCESS) {
        printf("aiot_mqtt_connect failed: -0x%04X\r\n\r\n", -res);
        printf("please check variables like mqtt_host, produt_key, device_name, device_secret in demo\r\n");
        /* 尝试建立连接失败, 销毁MQTT实例, 回收资源 */
        goto exit;
    }

    /*            真实的版本号                */
    cur_version = "1.0";
    
    /* 演示MQTT连接建立起来之后, 就可以上报当前设备的版本号了 */
    res = aiot_ota_report_version(ota_handle, cur_version);
    if (res < STATE_SUCCESS) {
        printf("report version failed, code is -0x%04X\r\n", -res);
    }

    while (1) {
        res = aiot_mqtt_process(mqtt_handle);
        if (res == STATE_USER_INPUT_EXEC_DISABLED) {
            break;
        }
        res = aiot_mqtt_recv(mqtt_handle);
        /* 向互联网平台发送下载请求 */
        if(g_dl_handle != NULL && res == STATE_SUCCESS) 
        {
                /* 向互联网平台发送下载请求 */
            res = aiot_mqtt_download_process(g_dl_handle);

            if(STATE_MQTT_DOWNLOAD_SUCCESS == res) 
            {
                /* 升级成功,这里重启并且上报新的版本号 */
                printf("mqtt download ota success \r\n");
                /* 退出下载器 */
                aiot_mqtt_download_deinit(&g_dl_handle);
                break;
            } else if(STATE_MQTT_DOWNLOAD_FAILED_RECVERROR == res
                      || STATE_MQTT_DOWNLOAD_FAILED_TIMEOUT == res
                      || STATE_MQTT_DOWNLOAD_FAILED_MISMATCH == res) 
            {
                printf("mqtt download ota failed \r\n");
                aiot_mqtt_download_deinit(&g_dl_handle);
                break;
            }
        }
    }

    aiot_ota_deinit(&ota_handle);
    /* 断开MQTT连接, 一般不会运行到这里 */
    g_mqtt_process_thread_running = 0;
    g_mqtt_recv_thread_running = 0;
    vTaskDelay(1);
    
    res = aiot_mqtt_disconnect(mqtt_handle);
    if (res < STATE_SUCCESS) {
        printf("aiot_mqtt_disconnect failed: -0x%04X\r\n", -res);
        goto exit;
    }

exit:
    while (1) {
        /* 销毁MQTT实例, 一般不会运行到这里 */
        res = aiot_mqtt_deinit(&mqtt_handle);

        if (res < STATE_SUCCESS) {
            printf("aiot_mqtt_deinit failed: -0x%04X\r\n", -res);
        } else {
            break;
        }
    }
    /* 销毁OTA实例, 一般不会运行到这里 */
    aiot_ota_deinit(&ota_handle);
}

2.4 APP程序调试

        如果APP程序的OTA功能调试不通,可以先去移植没有高级能力的SDK,先接入阿里云平台,了解大概流程再回来调试OTA功能(其实与基础接入案例功能的区别就在于建立了OTA会话和一些OTA回调函数,这部分SDK案例已经实现了,基础接入案例调式通了,这个自然不难)。

        查看阿里云帮助文档有OTA升级的操作指南:操作指南->监控运维->OTA升级->OTA升级步骤

e285d847eaf4ce9c956bd7990e3c5c4c.png

        乍一看这步骤很多,而且都不熟悉,其实只有图中绿框选中的需要开发者去介入实现,其余步骤都通过SDK包实现了。

APP程序调式步骤:

        第一步:上传固件包到阿里云平台,监控运维->OTA升级->添加升级包(参考帮助文档)->查看->批量升级(参考帮助文档)

        第二步:启动程序,打开串口,正常情况下,串口打印信息如下所示。首先是网络初始化,然后是接入阿里云平台,进而上报版本信息,发布OTA更新主题,阿里云平台发送升级包信息(URL、版本号、大小),阿里云平台下发升级包,下载升级包

(由于本人的开发板是通过网线连接到电脑主机,而主机是通过无线网接上网络,所以一般网络初始化开发板不会像接到路由器那样DHCP成功,需要通过网络适配器选项->WLAN->属性->共享,即ICS( Internet Connection Sharing )互联网共享技术)

d354d34a07986b7b0e5ab959829a7af6.png

第三步:编写一个W25Q128的OTA缓冲区读写程序,通过keil调试功能查看OTA缓冲区固件内容、WinHex软件查看OTA升级包bin文件内容,查看这二者内容是否一致,如果不符,就是APP程序中下载程序出错,需要进一步调试修改

三、BootLoader程序的开发

考虑到bootloader需要具备体积小和高效稳定性能,于是基于标准库进行开发。

第一步:内存资源分配

BootLoader:IROM1(0x8000000*********0x8000) IRAM1(0x20000000************0x5000) 串口接收固件起始地址(0X68001000) W25Q128固件搬至内部Flash的APP1区的静态缓冲区起始地址(0X68066000)

APP:IROM1(0x8008000*********0x7C000) IRAM1(0x20005000************0x20000) APP1(0x8008000*********0x7C000)APP2(0x8088000*********0x7C000)

第二步:驱动程序

        内部Flash初始化读写程序、串口初始化读写中断接收程序、外部SRAM初始化读写程序、W25Q128初始化读写程序、跳转APP程序

第三步程序设计与主函数编写

64a41dd1eccdda7ed73e4aa76a808298.png

第四步:修改APP程序并重新上传固件

修改APP程序的内存地址、中断向量表(SCB->VTOR = FLASH_BASE_ADDR | OFFSET)和固件的版本号,重新生成1.0版本固件和1.1版本固件的bin文件,上传1.1版本固件至阿里云平台

第五步:bootloader程序调式

        通过Keil软件的debug功能,对if或switch条件判断变量打断点并赋值选择程序运行路径,通过memory观察数据缓冲区数值变化,对跳转函数打断点观察程序运行路径

调式步骤:

1、将bootloader程序烧录至开发板并运行开发模式,通过串口烧录1.0版本固件并运行,将会下载1.1版本固件至OTA缓冲区。

ef98baef85b8fa21e502b89a8d9ff31c.png

2、重启开发板运行用户模式,更新固件

        进入用户模式UI,检测到有待更新的固件:

878d5aa6f3e639e8162086a51aa92678.png

 按下KEY1按键,进入到1.1版本的固件程序:

48da635ff269e89260ba5290f8304c7a.png

1.1版本的固件程序打印的串口信息,当前版本号为1.1,并向阿里云平台上传版本信息:

fad253a3918fc630f5a9d03e6ee668f4.png

阿里云平台检测开发板当前程序版本号为1.1,状态更新为升级成功,撒花!!!

f4ec642fbb98a1e5deb0461a07c27a23.png

四、重难点总结

1、网络配置:开发板必须成功DHCP分配到IP地址,才能连接上网络,这步非常的关键,可以通过WireShark抓包工具分析问题所在。

2、内存管理:芯片的Flash的1M内存肯定够用,但是APP程序由于移植了FreeRTOS、LWIP协议栈、SDK开发工具包(mbedtls),如果不对代码进行一个优化的话,128K的SRAM有些窘迫。而且bootloader程序还需要一些固件数据接收缓冲区,所以要对内部SRAM、外部SRAM甚至内部的CCM内存资源进行一个分配。一般不是设置的话默认是使用内部SRAM资源,FreeRTOS的内存大小在FreeRTOSConfig.h中设置configTOTAL_HEAP_SIZE、内存区域在heap_x.c中设置ucHeap,SDK使用的pvPortMalloc申请内存,故使用的是lwip的内存FreeRTOS中ucHeap的内存资源,在lwipopts.h中MEM_SIZE设置大小。

3、MQTT协议的应用:订阅和发布主题,这部分内容可以通过MQTT.fx或MQTTX软件与阿里云平台进行通信演练

4、bootloader跳转应用程序段函数与中断向量表偏移设置

#include <stdint.h>
#include "stm32f4xx.h"  // 根据具体平台包含 CMSIS 头文件

typedef void (*iapfun)(void);  // 定义函数指针类型

iapfun jumptoapp; 

void iap_load_app(uint32_t appxaddr) {
    if (((*(volatile uint32_t *)appxaddr) & 0x2FFE0000) == 0x20000000) { 
        iapfun jumptoapp = (iapfun)*(volatile uint32_t *)(appxaddr + 4);  // 获取复位地址
        __disable_irq();                     // 禁用中断
        SCB->VTOR = appxaddr;                // 重定位向量表
        __set_MSP(*(volatile uint32_t *)appxaddr);  // 初始化堆栈指针
        jumptoapp();                          // 跳转到用户应用程序
    }
}

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值