ESP32 IDF开发 应用篇⑲ 空中升级OTA

ESP32 IDF开发 应用篇⑲ 空中升级OTA

    • 1、博主写这篇技术文章的目的:
    • 2、概述
    • 3、OTA Flash 空间分区
    • 4、OTA 相关API的介绍
    • 5、软件设计
    • 6、实例
    • 7、调试结果

别迷路-导航栏
快速导航找到你想要的(文章目录)

此篇文章如果对你有用,请点赞收藏,您的支持就是博主坚持的动力。

1、博主写这篇技术文章的目的:

(1)、了解 什么是OTA;
(2)、OTA分区表的分布;
(3)、OTA编程方法及api的使用;

2、概述

在实际产品开发过程中,在线升级可以远程解决产品软件开发引入的问题,更好地满足用户需求。OTA(空中)更新是使用 Wi-Fi 连接而不是串行端口将固件加载到 ESP 模块的过程。(1)、ESP32 的 OTA 升级有三种方式:
·Arduino IDE:主要用于软件开发阶段,实现不接线固件烧写
·Web Browser:通过 Web 浏览器手动提供应用程序更新模块
·HTTP Server:自动使用http服务器 - 针对产品应用
在三种升级情况下,必须通过串行端口完成第一个固件上传。
OTA 进程没有强加的安全性,需要确保开发人员只能从合法/受信任的来源获得更新。更新完成后,模块将重新启动,并执行新的代码。开发人员应确保在模块上运行的应用程序以安全的方式关闭并重新启动。

3、OTA Flash 空间分区

在【ESP32 IDF开发 驱动篇⑩ 存储NVS高级应用和自定义分区表】章节和【ESP32 ID开发 系统篇⑪ 系统启动流程及硬件复位问题分析】已经详细介绍了无OTA分区和双OTA分区的区别以及详细的内容。
双 OTA 分区时,4M SPI Flash 的分区情况:
ESP32 IDF开发 应用篇⑲ 空中升级OTA_第1张图片

Nvs:保存的是用户操作nvs api设置的一些数据
phy_init:区是RF 的数据
factory:应用程序
coredump:存放的是系统运行过程中产生的一些错误快照,当系统崩溃时产出的错处都保存在coredump去,从而可以在随后在 PC 上分析失败的原因。(后续将以实例讲解怎么使用)
otadata:存放启动应用程序的参数
ota_0:升级程序的APP1
ota_1:升级程序的APP2
(1)、ESP32 连接 HTTP 服务器,发送请求 Get 升级固件;每次读取1KB固件数据,写入Flash。
(2)、ESP32 SPI Flash 内有与升级相关的(至少)四个分区:OTA data、Factory App、OTA_0、OTA_1。其中 FactoryApp 内存有出厂时的默认固件。
(3)、首次进行 OTA 升级时,OTA Demo 向 OTA_0 分区烧录目标固件,并在烧录完成后,更新 OTA data 分区数据并重启。
(4)、系统重启时获取 OTA data 分区数据进行计算,决定此后加载 OTA_0 分区的固件执行(而不是默认的 Factory App 分区内的固件),从而实现升级。
(5)、同理,若某次升级后 ESP32 已经在执行 OTA_0 内的固件,此时再升级时 OTA Demo 就会向 OTA_1 分区写入目标固件。再次启动后,执行 OTA_1 分区实现升级。以此类推,升级的目标固件始终在 OTA_0、OTA_1 两个分区之间交互烧录,不会影响到出厂时的 Factory App 固件。

4、OTA 相关API的介绍

这里介绍大部分OTA库的使用更详细的请参考:
更多API 参考esp-idf\components\app_update\include\esp_ota_ops.h
OTA API的分布情况:先来一张思维分析图

ESP32 IDF开发 应用篇⑲ 空中升级OTA_第2张图片

(1)、获得配置和启动程序的分区信息

/**
 * @brief 获取当前配置的启动应用程序的分区信息
 *
 *如果已调用esp_ota_set_boot_partition(),则将返回该函数设置的分区。
 *
 *如果尚未调用esp_ota_set_boot_partition(),则结果通常与esp_ota_get_running_partition()相同。
 *如果配置的启动分区不包含有效的应用程序,则这两个结果不相等(表示正在运行的分区将是引导加载程序通过后备选择的应用)。
 *如果OTA数据分区不存在或无效,则结果是在分区表。
 *
 *请注意,不能保证返回的分区是有效的应用程序。使用esp_image_verify(ESP_IMAGE_VERIFY,...)验证是否返回的分区包含可引导映像。
 * @return指向分区结构信息的指针;如果分区表无效或闪存读取操作失败,则为NULL。任何返回的指针在应用程序的生存期内均有效。
 */
const esp_partition_t* esp_ota_get_boot_partition(void);
/**
  * @brief 获取当前正在运行的应用程序的分区信息
  *此功能与esp_ota_get_boot_partition()的不同之处在于
  *它忽略由于以下原因导致的所选引导分区的任何更改
  * esp_ota_set_boot_partition()。 仅其代码当前为的应用
  *运行中将返回其分区信息。
  *
  *如果配置的启动方式,此函数返回的分区也可能与esp_ota_get_boot_partition()不同
  *分区在某种程度上是无效的,并且引导加载程序在引导时回退到另一个应用程序分区。
  *
  * @return 指向分区结构信息的指针;如果未找到分区或闪存读取操作失败,则为NULL。 返回的指针在应用程序的生存期内有效。
  */
const esp_partition_t* esp_ota_get_running_partition(void);
/**
  * @brief 返回下一个应使用新固件写入的OTA应用程序分区。
  *调用此函数以查找可以传递给esp_ota_begin()的OTA应用程序分区。
  *从当前运行的分区开始查找下一个分区循环。
  * @param start_from如果设置,则将此分区信息视为描述当前正在运行的分区。 可以为NULL,在这种情况下,使用esp_ota_get_running_partition()来查找当前正在运行的分区。 该函数的结果与该参数永远不会相同。
  * @return指向分区信息的指针,该信息接下来应更新。 NULL结果表示无效的OTA数据分区,或者找不到合格的OTA应用程序插槽分区。
  */
const esp_partition_t* esp_ota_get_next_update_partition(const esp_partition_t *start_from);

(2)、更新写入指定的分区

/**
 * @brief 开始OTA更新写入指定的分区。
 *成功时,此函数分配仍在使用的内存
 *直到使用返回的句柄调用esp_ota_end()为止。
 *
 * @param partition指向将接收OTA更新的分区信息的指针。
 * @param image_size新OTA应用程序图像的大小。分区将被删除,以接收此尺寸的图像。如果为0或OTA_SIZE_UNKNOWN,则会擦除整个分区。
 * @param out_handle成功后,返回一个句柄,该句柄应用于后续的esp_ota_write()和esp_ota_end()调用。
 * @return
 *-ESP_OK:OTA操作成功开始。
 *-ESP_ERR_INVALID_ARG:partition或out_handle参数为NULL,或者partition没有指向OTA应用程序分区。
 *-ESP_ERR_NO_MEM:无法为OTA操作分配内存。
 *-ESP_ERR_OTA_PARTITION_CONFLICT:分区包含当前正在运行的固件,无法就地更新。
 *-ESP_ERR_NOT_FOUND:在分区表中找不到分区参数。
 *-ESP_ERR_OTA_SELECT_INFO_INVALID:OTA数据分区包含无效数据。
 *-ESP_ERR_INVALID_SIZE:分区不适合配置的闪存大小。
 *-ESP_ERR_FLASH_OP_TIMEOUT或ESP_ERR_FLASH_OP_FAIL:Flash写入失败。
 *-ESP_ERR_OTA_ROLLBACK_INVALID_STATE:如果正在运行的应用程序尚未确认状态。在执行更新之前,该应用程序必须有效。
 */
esp_err_t esp_ota_begin(const esp_partition_t* partition, size_t image_size, esp_ota_handle_t* out_handle);

(3)、通过http将数据写入到分区中

/**
  * @brief 将OTA更新数据写入分区此功能可以多次调用
  *在OTA操作期间接收到数据。 数据写入
  *
  * @param handle从esp_ota_begin获取的句柄
  * @param data要写入的数据缓冲区
  * @param size数据缓冲区的大小,以字节为单位。
  *
  * @return
  *-ESP_OK:数据已成功写入闪存。
  *-ESP_ERR_INVALID_ARG:句柄无效。
  *-ESP_ERR_OTA_VALIDATE_FAILED:图片的第一个字节包含无效的应用图片魔术字节。
  *-ESP_ERR_FLASH_OP_TIMEOUT或ESP_ERR_FLASH_OP_FAIL:Flash写入失败。
  *-ESP_ERR_OTA_SELECT_INFO_INVALID:OTA数据分区的内容无效
  */
esp_err_t esp_ota_write(esp_ota_handle_t handle, const void* data, size_t size);
/**
  * @brief 完成OTA更新并验证新编写的应用程序映像。
  * @param handle从esp_ota_begin()获得的句柄。
  * @note调用esp_ota_end()之后,该句柄不再有效,并且与之关联的任何内存都被释放(无论结果如何)。
  * @return
  *-ESP_OK:新编写的OTA应用程序图片有效。
  *-ESP_ERR_NOT_FOUND:未找到OTA句柄。
  *-ESP_ERR_INVALID_ARG:从未写入过句柄。
  *-ESP_ERR_OTA_VALIDATE_FAILED:OTA映像无效(不是有效的应用程序映像,或者-如果启用了安全启动,则签名无法验证。)
  *-ESP_ERR_INVALID_STATE:如果启用了闪存加密,则此结果表明内部错误,将最终的加密字节写入闪存。
  */
esp_err_t esp_ota_end(esp_ota_handle_t handle);

(4)、重新设置分区

/**
  * @brief 为新的启动分区配置OTA数据
  * @note如果此函数返回ESP_OK,则调用esp_restart()将引导新配置的应用程序分区。
  * @param partition指向包含要启动的应用程序映像的分区的信息的指针。
  *
  * @return
  *-ESP_OK:OTA数据已更新,下次重启将使用指定的分区。
  *-ESP_ERR_INVALID_ARG:分区参数为NULL或未指向“ app”类型的有效OTA分区。
  *-ESP_ERR_OTA_VALIDATE_FAILED:分区包含无效的应用程序映像。 如果启用了安全启动并且签名验证失败,则也返回。
  *-ESP_ERR_NOT_FOUND:找不到OTA数据分区。
  *-ESP_ERR_FLASH_OP_TIMEOUT或ESP_ERR_FLASH_OP_FAIL:Flash擦除或写入失败。
  */
esp_err_t esp_ota_set_boot_partition(const esp_partition_t* partition);

在这里插入讲解一下http几个函数esp_http_client_init在http章节已经介绍过了

/**
  * @brief 此函数将打开连接,写入所有标头字符串并返回
  *
  * @param [in]客户端esp_http_client句柄
  * @param [in] write_len HTTP内容长度需要写入服务器
  *
  * @return
  *-ESP_OK
  *-ESP_FAIL
  */
esp_err_t esp_http_client_open(esp_http_client_handle_t client, int write_len);
接收http头部
/**
  * @brief 该函数需要在esp_http_client_open之后调用,它将从http流中读取,处理所有接收头
  *
  * @param [in]客户端esp_http_client句柄
  *
  * @return
  *-(0)如果流不包含内容长度标头或分块编码(由`esp_http_client_is_chunked`响应检查)
  *-(-1:ESP_FAIL)如果有任何错误
  */
int esp_http_client_fetch_headers(esp_http_client_handle_t client);

从服务器上接收要下载的数据流文件

/**
  * @brief从http流读取数据
  *
  * @param [in]客户端esp_http_client句柄
  * @param buffer缓冲区
  * @param [in] len长度
  *
  * @返回
  *-(-1)如果有任何错误
  *-读取数据长度
  */
int esp_http_client_read(esp_http_client_handle_t client, char *buffer, int len);

5、软件设计

本例程是基于idf_wifi_tcp程序的基础上添加ota任务
①、在连接好tcp连接之后,通过tcp发出指令UPDATAOTA之后开始启动任务更新
②、设置服务器url并连接服务器,处理接收头部信息

err = esp_http_client_open(client, 0);
esp_http_client_fetch_headers(client);

③、获取当前配置的启动应用程序的分区信息和当前的信息对比

  //获取当前配置的启动应用程序的分区信息
    const esp_partition_t *configured = esp_ota_get_boot_partition();
    //获取当前正在运行的应用程序的分区信息
const esp_partition_t *running = esp_ota_get_running_partition();
typedef struct {
    esp_flash_t* flash_chip;            /*!< SPI flash 分区*/
    esp_partition_type_t type;          /*!< 分区类型(应用程序/数据) */
    esp_partition_subtype_t subtype;    /*!< 分区子类型 */
    uint32_t address;                   /*!< 闪存中分区的起始地址 */
    uint32_t size;                      /*!< 分区大小,以字节为单位 */
    char label[17];                     /*!< 分区标签,零终止的ASCII字符串 */
    bool encrypted;                     /*!< 如果分区已加密,则标志设置为true */
} esp_partition_t;

④、获得运行程序的下一个分区开始写入

update_partition = esp_ota_get_next_update_partition(NULL);
err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);

⑤、从服务端接收数据并开始写入

err = esp_ota_write( update_handle, (const void *)ota_write_data, buff_len);
int buff_len = esp_http_client_read(client, OtaRev, UPDATA_BUFFSIZE);

⑥、写入结束之后,完成更新OTA data区数据,重启时根据OTA data区数据到Flash分区加载执行目标(新)固件

esp_ota_end(update_handle)
err = esp_ota_set_boot_partition(update_partition);

6、实例

在测试之前我们先来配置一下分区
make menuconfig
在Partition Table中
ESP32 IDF开发 应用篇⑲ 空中升级OTA_第3张图片

然后如果编辑出现此类flash大小小的错误
在这里插入图片描述

请在Serial flasher config- Flash size (4 MB) 选择4M大小

在客户端任务中监测UPDATAOTA字符数据,监测到了开始启动更新任务,并且挂起当前任务

注:重点在跟新任务中如果存在其他网络数据交互可能会影响更新中断失败!!!!,所以这里要挂起当前网络通信的任务,更新完成之后再恢复。。如果存在其网络任务一定要中断连接!!!!

 //读取接收数据
 len = recv(client_connect_socket, databuff, sizeof(databuff), 0);//阻塞函数
 if (len > 0)//接收到数据
 {
     if (strncmp((char*)databuff,"UPDATAOTA",9) == 0)//判断接收命令为"UPDATAOTA"
     {
         //先断开重新连接
         close(client_connect_socket);
         xTaskCreate(&vTaskOta, 
                     "vTaskOta",
                     Ota_TASK_STACK_SIZE,
                     NULL,
                     Ota_TASK_PRIO,
                     &xHandleTaskOta);
         vTaskSuspend(NULL);
     }
     else
         send(client_connect_socket, databuff, len, 0);

在一个任务中完成升级过程

/***********************************************************************
* 函数:  
* 描述:   ota空中升级任务
* 参数:
* 返回: 无
* 备注:
************************************************************************/
static void vTaskOta(void *pvParameters)
{
    esp_err_t err;
    esp_ota_handle_t update_handle = 0 ;
    const esp_partition_t *update_partition = NULL;

    ESP_LOGI(TAG, "Starting OTA...");
    esp_http_client_config_t config =
    {
        .url = UPDATA_URL,
        .event_handler = _http_event_handler,
    };
     esp_http_client_handle_t client = esp_http_client_init(&config);
    if (client == NULL)
    {
        ESP_LOGE(TAG, "initialise HTTP connection Failed");
        goto ERR_OTA;
    }
    //这里只是建立了连接,发送数据位0
    err = esp_http_client_open(client, 0);
    if (err != ESP_OK)
    {
        esp_http_client_cleanup(client);
        ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
        goto ERR_OTA;
    }
    //处理所有接收头
    esp_http_client_fetch_headers(client);

    //获取当前配置的启动应用程序的分区信息
    const esp_partition_t *configured = esp_ota_get_boot_partition();
    //获取当前正在运行的应用程序的分区信息
    const esp_partition_t *running = esp_ota_get_running_partition();
    //当前运行的分区与配置的分区相同则,是第一次执行,通过esp_ota_set_boot_partition函数之后就不同
    if (configured != running)
    {
        ESP_LOGI(TAG, "Configured OTA boot partition at offset 0x%08x, but running from offset 0x%08x",
                 configured->address, running->address);
    }
    ESP_LOGI(TAG, "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);
    ESP_LOGI(TAG, "Writing to partition subtype %d at offset 0x%x",
             update_partition->subtype, update_partition->address);
    if (update_partition == NULL)
    {
        ESP_LOGE(TAG, "update partition Err");
        goto ERR_OTA;
    }
    //开始OTA更新写入指定的分区
    err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);
    if (err != ESP_OK) 
    {
        ESP_LOGE(TAG, "esp_ota_begin failed, error=%d", err);
        goto ERR_OTA;
    }

    char *OtaRev = (char *)malloc(UPDATA_BUFFSIZE);
    uint16_t ota_status = WIFI_OTA_WRITE;
    while(ota_status != WIFI_OTA_END)
    {
        int buff_len = esp_http_client_read(client, OtaRev, UPDATA_BUFFSIZE);
        if (buff_len < 0) 
        { //包异常
            ESP_LOGE(TAG, "Error: receive data error! errno=%d", errno);
            goto ERR_OTA;
        }
        switch (ota_status) 
        {
            case WIFI_OTA_WRITE:
                if (buff_len == 0)
                {
                    ota_status = WIFI_OTA_END;
                    break;
                }
                //写flash
                err = esp_ota_write( update_handle, (const void *)OtaRev, buff_len);
                if (err != ESP_OK) 
                {
                    ESP_LOGE(TAG, "Error: esp_ota_write failed! err=0x%x", err);
                    goto ERR_OTA;
                }
                binary_file_length += buff_len;
                ESP_LOGI(TAG, "Have written image length %d", binary_file_length);
            break;
        }
    }
    ESP_LOGI(TAG, "Total Write binary data length : %d", binary_file_length);
    free(OtaRev);
    esp_http_client_close(client);
    esp_http_client_cleanup(client);
    //OTA写结束
    if (esp_ota_end(update_handle) != ESP_OK) 
    {
        ESP_LOGE(TAG, "esp_ota_end failed!");
        goto ERR_OTA;
    }
        //升级完成更新OTA data区数据,重启时根据OTA data区数据到Flash分区加载执行目标(新)固件
    err = esp_ota_set_boot_partition(update_partition);
    if (err != ESP_OK) 
    {
        ESP_LOGE(TAG, "esp_ota_set_boot_partition failed! err=0x%x", err);
        goto ERR_OTA;
    }
    ESP_LOGI(TAG, " restart system!");
    esp_restart();
ERR_OTA:
    esp_http_client_close(client);
    esp_http_client_cleanup(client);
    close(client_connect_socket);
    vTaskDelete(xHandleTaskOta);
    vTaskResume(xHandleTaskTcpClinent);
    //如果升级中断恢复网络接连
    CreateTcpClient(TCP_SERVER_ADRESS,TCP_SERVER_PORT);
}

7、调试结果

调试前前我们先设置好IP和端口号

#define TCP_SERVER_ADRESS “192.168.0.94” //作为client,要连接TCP服务器地址
#define TCP_SERVER_PORT 5200 //服务端端口

这里对应我调试的电脑IP

在发送UPDATAOTA之前我们先要在idf_wifi_ota/build路径启动服务器,在系统中提供了一个服务端
python -m SimpleHTTPServer 8070
有些系统可能需要python2 -m SimpleHTTPServer 8070

然后打开网页http://localhost:8070/,这里可以看到编译输出的文件

ESP32 IDF开发 应用篇⑲ 空中升级OTA_第4张图片
操作如下:
在这里插入图片描述

如果你的防火墙阻止了对端口 8070 的访问,请先关闭防火墙。
程序运行起来之后先打开网络调试助手在 tcp server模式监听状态
ESP32 IDF开发 应用篇⑲ 空中升级OTA_第5张图片
在程序串口可以看到打印已经连接,然后发送“UPDATAOTA”,就可以看到更新的数据

ESP32 IDF开发 应用篇⑲ 空中升级OTA_第6张图片

更新的数据大小

ESP32 IDF开发 应用篇⑲ 空中升级OTA_第7张图片

所有文章源代码:https://download.csdn.net/download/lu330274924/88518092

你可能感兴趣的:(ESP32,IDF小白到大师实战,网络,tcp/ip,服务器,网络协议,嵌入式硬件)