概述
经过我的专研,终于实现了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的获取
阿里云物联网平台网址:阿里云权益中心_助力学生、开发者、企业用云快速上云-阿里云
操作步骤:
第一步:在阿里云平台注册账号,进入物联网平台(产品->物联网->物联网平台->管理控制台)并开通公共实例
第二步:进入公共实例,设备管理->产品->创建产品(每个模块下都是视频教程和相应的帮助文档)
第三步:设备管理->设备->添加设备(这个模块下没有视频教程,但是有帮助文档)
第四步:使用MQTT.fx或者MQTTX软件接入阿里云平台,尝试订阅和发布主题(该步建议操作和了解一下,非必须执行,阿里云帮助文档里有教程)
第五步:文档与工具->设备接入SDK->SDK定制->SDK功能配置->开始生成
SDK功能配置:
1、设备OS->FreeRTOS
2、设备硬件形态->单板系统
3、连接物联网平台协议->MQTT3.1.1
4、数据加密->TLS-CA
5、设备认证方案->设备秘钥
6、高级能力->MQTT-OTA
通过以上五步即可获取阿里云OTA模块的SDK开发工具包。
2.2 C_Link_SDK开发工具包的介绍
将得到的工具包进行解压,里面有五个文件夹,如下所示:
以我个人的理解进行简单的介绍,详细描述的可见阿里云帮助文档。
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文件以外所有的源文件与头文件路径:
第二步 修改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升级步骤
乍一看这步骤很多,而且都不熟悉,其实只有图中绿框选中的需要开发者去介入实现,其余步骤都通过SDK包实现了。
APP程序调式步骤:
第一步:上传固件包到阿里云平台,监控运维->OTA升级->添加升级包(参考帮助文档)->查看->批量升级(参考帮助文档)
第二步:启动程序,打开串口,正常情况下,串口打印信息如下所示。首先是网络初始化,然后是接入阿里云平台,进而上报版本信息,发布OTA更新主题,阿里云平台发送升级包信息(URL、版本号、大小),阿里云平台下发升级包,下载升级包
(由于本人的开发板是通过网线连接到电脑主机,而主机是通过无线网接上网络,所以一般网络初始化开发板不会像接到路由器那样DHCP成功,需要通过网络适配器选项->WLAN->属性->共享,即ICS( Internet Connection Sharing )互联网共享技术)
第三步:编写一个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程序
第三步:程序设计与主函数编写
第四步:修改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缓冲区。
2、重启开发板运行用户模式,更新固件
进入用户模式UI,检测到有待更新的固件:
按下KEY1按键,进入到1.1版本的固件程序:
1.1版本的固件程序打印的串口信息,当前版本号为1.1,并向阿里云平台上传版本信息:
阿里云平台检测开发板当前程序版本号为1.1,状态更新为升级成功,撒花!!!
四、重难点总结
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(); // 跳转到用户应用程序
}
}