58 ESP32 OTA升级(双OTA分区无factory APP)

1 引言

        产品功能实现后,就要对产品的维护进行考虑。产品出来后,卖了N台出去,如果突然发现自己一行代码写错了,怎么办,肯定不能去现场吧N台设备,免费出差旅游也累啊,所以一般需要有远程升级设备的功能,此篇为对ESP32 OTA实验经验总结。

2 问题

        当前ESP32官方例程,以及网络上的经验例程,都是factory APP + 双OTA_x 方式。我选用的esp32是4M flash的,现在编译的固件大小是1.3M,按照上述的方式,需要用到的空间是:1.3M * 3 =3.9M。

        只有4M大小flash的我,很明显,这看起来马上就不能用,也不够用了。所以,按照网上或官网那种分区的方式,肯定不行。

        然后在继续查找经验过程中,发现有人没有用官方的factory APP+双OTA_X方式,而是直接双OTA_0和OTA_1,没有factory这个东东,省下了1倍固件大小的空间,所以只有4M flash的我,就采用这个方案了。

3 双OTA no factory APP实现

要做OTA升级,需要了解的内容是:

        1.如何对flash划分分区?

        2.升级机制是怎样的?

        3.如何将固件从网络上download下来并写到flash中,以及标记相关参数?

对于问题1:如何对flash进行分区划分?

        对这个问题,我一开始比较迷惑。因为esp32给出的ota升级的demo,是需要三个分区,即factory app+OTA1+OTA2,按照这算法,所需flash的大小是单个app的三倍。

        然后我固件大小是1.3M,整块flash大小4M,这样算下来3*1.3 = 3.9M,显然不够,所以一开始迷惑了。官方怎么不给只有两个的,完全两个就搞定了,然后经过资料搜索,和尝试,最后确认两个OTA分区,不需要factory app也是可以的,所以就敲定了以下的分区划分。

对于问题2:升级机制是怎样的? 

        esp32 OTA的升级机制跟传统的机制是一样的,来回切换,就这样。。。。

对于问题3:如何将固件从网络上download下来并写到flash中,以及标记相关参数?

        下载的话,我采用了httpget方式,将文件从http服务器下载下来。至于下载下来的文件写入flash和如何标记OTA_data相关参数,直接参考官方例程即可。整个搞下来其实不难,要花一点时间调试而已。 

        本实验采用的方式是模块化编程,所以下面给出的代码都是模块独立的,稍微移植就可以用。

#include <stdio.h>
#include <string.h>

#include "esp_http_client.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "freertos/queue.h"
#include "mqtt_client.h"

#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_wpa2.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_ota_ops.h"
#include "ht_led.h"

#include "ht_console.h"
#include "ht_main.h"
#include "HTMngr.h"
#include "ht_ota.h"

/
uint32_t 			ghttpfileSize 	= 0;
uint32_t 			gProtofileSize	= 0;

xQueueHandle 		htOTA_evt_queue = NULL;
TimerHandle_t 		gExitOTATaskTimeID = NULL;

EventGroupHandle_t	ht_OTAEvt_group; 			//全局事件句柄
const int HT_OTAEvt_OTA_UPDATE_BIT	= BIT0;		//ota事件
const int HT_OTAEvt_RUN_HTTP_BIT	= BIT1;		//启动http

extern EventGroupHandle_t	ht_MngrEvt_group; 	//全局事件句柄
extern const int HT_MngrEvt_RebootSys_BIT;		//系统重启

extern GlobalArgs	g_SysParam;
//
esp_err_t htOTA_http_event_handle(esp_http_client_event_t *evt)
{
	HTOTA_queData_t tmpHTOTA_queData;
	
    //判断事件类型
    switch (evt->event_id)
    {
    case HTTP_EVENT_ERROR:
        Console_Debug("HTTP_EVENT_ERROR");
        break;
    case HTTP_EVENT_ON_CONNECTED:
        Console_Debug("HTTP_EVENT_ON_CONNECTED");
        break;
    case HTTP_EVENT_HEADER_SENT:
        Console_Debug("HTTP_EVENT_HEADER_SENT");
        break;
    case HTTP_EVENT_ON_HEADER:
        Console_Debug("HTTP_EVENT_ON_HEADER");
		
		if(strstr((char *)evt->header_key, "Content-Length"))
		{
			ghttpfileSize = atoi(evt->header_value);
			Console_Debug("update file %s:%d\r\n", evt->header_key, ghttpfileSize);
		}


        break;
    case HTTP_EVENT_ON_DATA:
        //接收数据
		memset(&tmpHTOTA_queData , 0, sizeof(tmpHTOTA_queData));
		tmpHTOTA_queData.rcvLength = evt->data_len;
		memcpy(tmpHTOTA_queData.rcvBuffer ,evt->data, tmpHTOTA_queData.rcvLength);

		//发送数据
		if(xQueueSend(htOTA_evt_queue, &tmpHTOTA_queData, (1500 / portTICK_PERIOD_MS) ) != pdPASS) 
		{
			Console_Error("xQueueSend failed\r\n");
		}

        break;
    case HTTP_EVENT_ON_FINISH:		
        Console_Debug("HTTP_EVENT_ON_FINISH");
        break;
    case HTTP_EVENT_DISCONNECTED:
        Console_Debug("HTTP_EVENT_DISCONNECTED");
        break;
    default:
        break;
    }
    return ESP_OK;
}


int htOTA_runHttpGet(void)
{
    esp_http_client_handle_t client;
	
	//参数校验
	if(strlen((char*)g_SysParam.ota_url)<5)
	{
		Console_Error("ota_url %s error!", g_SysParam.ota_url);
		return -1;
	}

    //配置http参数
    esp_http_client_config_t config = {
    	.url = (char*)g_SysParam.ota_url,						//目标文件
        .event_handler = htOTA_http_event_handle, 				//http回调函数
        .buffer_size = (1024),									//指定接收缓存大小
    };
	client = esp_http_client_init(&config);
	
	Console_Debug("Starting htOTA_runHttpGet...");

	//发起http连接
    esp_http_client_perform(client); //发起http连接
    
    esp_http_client_close(client);
    esp_http_client_cleanup(client);

	return 0;
}

void htOTA_setTargetFileSize(uint32_t utargetSize)
{
	gProtofileSize = utargetSize;
}

void htOTA_queueTask(void *pvParameters)
{
	uint32_t uCnt = 0;
	uint32_t uLedCnt = 0;

	uint32_t rcvBinarySize = 0;
	
    esp_err_t err = 0;
    esp_ota_handle_t update_handle = 0 ;
    const esp_partition_t *update_partition = NULL;
	HTOTA_queData_t tmpHTOTA_queData = {0};

	//这里等待1s,等待mqtt回复完再启动
	vTaskDelay((1000) / portTICK_PERIOD_MS);

	Console_Debug("Starting OTA updating...");
	//获取当前boot位置
	const esp_partition_t *configured = esp_ota_get_boot_partition();
	//获取当前系统执行的固件所在的Flash分区
	const esp_partition_t *running = esp_ota_get_running_partition();

	if (configured != running) {
		Console_Debug("Configured OTA boot partition at offset 0x%08x, but running from offset 0x%08x",
			  configured->address, running->address);
		Console_Debug("(This can happen if either the OTA boot data or preferred boot image become corrupted somehow.)");
	}
	Console_Debug("Running partition type %d subtype %d (offset 0x%08x)",
		  running->type, running->subtype, running->address);

	//获取当前系统下一个(紧邻当前使用的OTA_X分区)可用于烧录升级固件的Flash分区
	update_partition = esp_ota_get_next_update_partition(NULL);
	Console_Debug("Writing to partition subtype %d at offset 0x%x",
		   update_partition->subtype, update_partition->address);

	if(update_partition == NULL)
	{
		Console_Error("update_partition == NULL...");
		goto iExit;
	}
		   	
	//启动OTA模块要进行烧写
	err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);
	if (err != ESP_OK) {
		Console_Error("esp_ota_begin failed, error=%d", err);
		goto iExit;
	}
	Console_Debug("esp_ota_begin succeeded");

	//启动启动http
	xEventGroupSetBits(ht_OTAEvt_group, HT_OTAEvt_RUN_HTTP_BIT);

	
	//启动监控定时器
	xTimerStop(gExitOTATaskTimeID, 0);
	xTimerStart(gExitOTATaskTimeID,0);

	//在这里循环等待接收数据
	while(1)
	{
		memset(&tmpHTOTA_queData, 0, sizeof(tmpHTOTA_queData));
		if(xQueueReceive(htOTA_evt_queue, &tmpHTOTA_queData, portMAX_DELAY)) 
		{
			//检测文件大小是否正确
			if(ghttpfileSize != gProtofileSize)
			{
				Console_Error("ghttpfileSize%d,  gProtofileSize:%d! error", ghttpfileSize, gProtofileSize);
				goto iExit;
			}
		
			//先判断是否数据为空
			if(tmpHTOTA_queData.rcvLength >0)
			{
				uLedCnt++;
				htLed_SetRGBLight(RGBLED_MODE_Y, uLedCnt&0x1);
			
				uCnt++;
				if(uCnt%20==0)
				{					
					Console_Debug("file total size: (%d Byte), download...(%d %%)\r\n",ghttpfileSize, (rcvBinarySize*100/ghttpfileSize));
				}
				//写flash
				err = esp_ota_write( update_handle, (const void *)tmpHTOTA_queData.rcvBuffer, tmpHTOTA_queData.rcvLength);
				if (err != ESP_OK) {
					Console_Error("Error: esp_ota_write failed! err=0x%x", err);
					goto iExit;
				}
				rcvBinarySize += tmpHTOTA_queData.rcvLength;

				if(rcvBinarySize >= gProtofileSize)
				{
					Console_Debug("file total size: (%d Byte), download...(100 %%)\r\n",ghttpfileSize);
					Console_Error("Connection closed, all packets received");
					htLed_SetRGBLight(RGBLED_MODE_Y, 0);
					break;
				}
			}
			
			//重置监控定时器
			xTimerStop(gExitOTATaskTimeID, 0);
			xTimerStart(gExitOTATaskTimeID,0);
		}
	}

    Console_Debug("Total Write binary data length : %d", rcvBinarySize);
    //OTA写结束
    if (esp_ota_end(update_handle) != ESP_OK) {
        Console_Error("esp_ota_end failed!");
		goto iExit;
    }
    //升级完成更新OTA data区数据,重启时根据OTA data区数据到Flash分区加载执行目标(新)固件
    err = esp_ota_set_boot_partition(update_partition);
    if (err != ESP_OK) {
        Console_Error("esp_ota_set_boot_partition failed! err=0x%x", err);
		goto iExit;
    }
    Console_Debug("Prepare to restart system!");


iExit:
	//触发系统重启
	xEventGroupSetBits(ht_MngrEvt_group, HT_MngrEvt_RebootSys_BIT);

	vTaskDelete(NULL);

	return;
}


void htOTA_EvtTask(void *pvParameters)
{
	uint8_t isRunOTATaskFlag = 0;
	EventBits_t uxBits;

	while(1)
	{
		/* Wait a maximum of 100ms for either bit 0 or bit 4 to be set within
		the event group.	Clear the bits before exiting. */
		uxBits = xEventGroupWaitBits(
				ht_OTAEvt_group,   /* The event group being tested. */
				HT_OTAEvt_RUN_HTTP_BIT | HT_OTAEvt_OTA_UPDATE_BIT, /* The bits within the event group to wait for. */
				pdTRUE, 	   /* BIT_0 & BIT_4 should be cleared before returning. */
				pdFALSE,	   /* Don't wait for both bits, either bit will do. */
				portMAX_DELAY );/* Wait a maximum of 100ms for either bit to be set. */

		//触发ota升级
		if( ( uxBits & HT_OTAEvt_RUN_HTTP_BIT ) != 0 )
		{
			Console_Debug("HT_OTAEvt_RUN_HTTP_BIT\r\n");
			htOTA_runHttpGet();
		}

		//启动ota升级线程
		if( ( uxBits & HT_OTAEvt_OTA_UPDATE_BIT ) != 0 )
		{
			Console_Debug("HT_OTAEvt_OTA_UPDATE_BIT, isRunOTATaskFlag:%d\r\n", isRunOTATaskFlag);
			
			if(isRunOTATaskFlag == 0)
			{
				isRunOTATaskFlag = 1;
				//OTA接收数据处理函数
				xTaskCreate(htOTA_queueTask, "htOTA_queTsk", (6*1024), NULL, HT_TASK_HTOTA_queue_Priority, NULL);
			}
		}
	}

	//不会走到这里,走到这里需要删除task
	vTaskDelete(NULL);

	return;
}

//监控ota升级任务运行正常,如果服务器卡住,接收数据不完全卡住,则超时重启
static void htOTA_autoExitOTA_callback(void *arg)
{
	//触发系统重启
	xEventGroupSetBits(ht_MngrEvt_group, HT_MngrEvt_RebootSys_BIT);
}


int htOTA_Init(void)
{
	//创建ota等待事件集
    ht_OTAEvt_group = xEventGroupCreate();

	//创建消息队列用以接收http收到的数据
	htOTA_evt_queue = xQueueCreate(2, sizeof(HTOTA_queData_t));
	if(htOTA_evt_queue == NULL)
	{
		Console_Error("xQueueCreate failed\r\n");
		return -1;
	}
	
	//创建业务处理线程
	xTaskCreate(htOTA_EvtTask, "htOTA_EvtTask", (4*1024), NULL, HT_TASK_HTOTA_EVT_Priority, NULL);


    gExitOTATaskTimeID 	= xTimerCreate("exitOTA", (15*1000 / portTICK_PERIOD_MS), pdFALSE, NULL, htOTA_autoExitOTA_callback);


	return 0;
}

本人支持开源精神,以供有需要的人学习,减少重复造轮子带来的浪费,希望各位码主多多支持开源,有好东西多分享,一同进步! 

over! 

猜你喜欢

转载自blog.csdn.net/Chasing_Chasing/article/details/120991794