From f3f1f36525f68199d363ddb3f6cd62ac3ec7ac5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 30 May 2022 22:23:40 +0200 Subject: [PATCH] [core] Add uf2ota library source --- SUMMARY.md | 4 +- .../libraries/Update/uf2ota/uf2binpatch.c | 32 ++++ .../libraries/Update/uf2ota/uf2binpatch.h | 26 ++++ .../libraries/Update/uf2ota/uf2ota.c | 100 ++++++++++++ .../libraries/Update/uf2ota/uf2ota.h | 68 ++++++++ .../libraries/Update/uf2ota/uf2priv.c | 146 ++++++++++++++++++ .../libraries/Update/uf2ota/uf2priv.h | 61 ++++++++ .../libraries/Update/uf2ota/uf2types.h | 103 ++++++++++++ .../port/flashdb/fal_flash_ambz_port.c | 2 +- docs/ota/README.md | 6 + docs/ota/library.md | 58 +++++++ 11 files changed, 604 insertions(+), 2 deletions(-) create mode 100644 arduino/libretuya/libraries/Update/uf2ota/uf2binpatch.c create mode 100644 arduino/libretuya/libraries/Update/uf2ota/uf2binpatch.h create mode 100644 arduino/libretuya/libraries/Update/uf2ota/uf2ota.c create mode 100644 arduino/libretuya/libraries/Update/uf2ota/uf2ota.h create mode 100644 arduino/libretuya/libraries/Update/uf2ota/uf2priv.c create mode 100644 arduino/libretuya/libraries/Update/uf2ota/uf2priv.h create mode 100644 arduino/libretuya/libraries/Update/uf2ota/uf2types.h create mode 100644 docs/ota/library.md diff --git a/SUMMARY.md b/SUMMARY.md index 21f58e7..9a09d6f 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -6,7 +6,7 @@ * [Static functions](ltapi/_libre_tuya_a_p_i_8cpp.md) * [Logger](ltapi/lt__logger_8h.md) * [Chip types & UF2 families](ltapi/_chip_type_8h.md) - * [POSIX utilities](lt__posix__api_8h.md) + * [POSIX utilities](ltapi/lt__posix__api_8h.md) * Common API * [Flash](ltapi/class_i_flash_class.md) * [FS](ltapi/classfs_1_1_f_s.md) @@ -36,6 +36,8 @@ * [File list](ltapi/files.md) * [OTA format](docs/ota/README.md) * [uf2ota.py tool](docs/ota/uf2ota.md) + * [uf2ota.h library](docs/ota/library.md) + * [uf2ota.h reference](ltapi/uf2ota_8h.md) * Platforms * Realtek AmebaZ Series * Boards diff --git a/arduino/libretuya/libraries/Update/uf2ota/uf2binpatch.c b/arduino/libretuya/libraries/Update/uf2ota/uf2binpatch.c new file mode 100644 index 0000000..9c510f0 --- /dev/null +++ b/arduino/libretuya/libraries/Update/uf2ota/uf2binpatch.c @@ -0,0 +1,32 @@ +/* Copyright (c) Kuba Szczodrzyński 2022-05-29. */ + +#include "uf2priv.h" + +uf2_err_t uf2_binpatch(uint8_t *data, const uint8_t *binpatch, uint8_t binpatch_len) { + const uint8_t *binpatch_end = binpatch + binpatch_len; + // +2 to make sure opcode and length is present + while ((binpatch + 2) < binpatch_end) { + uf2_opcode_t opcode = binpatch[0]; + uint8_t len = binpatch[1]; + switch (opcode) { + case UF2_OPC_DIFF32: + uf2_binpatch_diff32(data, binpatch + 1); + break; + } + // advance by opcode + length + data + binpatch += len + 2; + } + return UF2_ERR_OK; +} + +void uf2_binpatch_diff32(uint8_t *data, const uint8_t *patch) { + uint8_t num_offs = patch[0] - 4; // read offset count + uint32_t diff = *((uint32_t *)(patch + 1)); // read diff value + patch += 5; // skip num_offs and diff value + for (uint8_t i = 0; i < num_offs; i++) { + // patch the data + uint8_t offs = patch[i]; + uint32_t *value = (uint32_t *)(data + offs); + *(value) += diff; + } +} diff --git a/arduino/libretuya/libraries/Update/uf2ota/uf2binpatch.h b/arduino/libretuya/libraries/Update/uf2ota/uf2binpatch.h new file mode 100644 index 0000000..33068ce --- /dev/null +++ b/arduino/libretuya/libraries/Update/uf2ota/uf2binpatch.h @@ -0,0 +1,26 @@ +/* Copyright (c) Kuba Szczodrzyński 2022-05-29. */ + +#pragma once + +#include "uf2types.h" + +/** + * @brief Apply binary patch to data. + * + * @param data input data + * @param data_len input data length + * @param binpatch binary patch data + * @param binpatch_len binary patch data length + * @return uf2_err_t error code + */ +uf2_err_t uf2_binpatch(uint8_t *data, const uint8_t *binpatch, uint8_t binpatch_len); + +/** + * Apply DIFF32 binary patch. + * + * @param data input data + * @param len input data length + * @param patch patch data, incl. length byte + * @return uf2_err_t error code + */ +void uf2_binpatch_diff32(uint8_t *data, const uint8_t *patch); diff --git a/arduino/libretuya/libraries/Update/uf2ota/uf2ota.c b/arduino/libretuya/libraries/Update/uf2ota/uf2ota.c new file mode 100644 index 0000000..1d2d4de --- /dev/null +++ b/arduino/libretuya/libraries/Update/uf2ota/uf2ota.c @@ -0,0 +1,100 @@ +/* Copyright (c) Kuba Szczodrzyński 2022-05-29. */ + +#include "uf2priv.h" + +uf2_ota_t *uf2_ctx_init(uint8_t ota_idx, uint32_t family_id) { + uf2_ota_t *ctx = (uf2_ota_t *)zalloc(sizeof(uf2_ota_t)); + ctx->ota_idx = ota_idx; + ctx->family_id = family_id; + return ctx; +} + +uf2_info_t *uf2_info_init() { + uf2_info_t *info = (uf2_info_t *)zalloc(sizeof(uf2_info_t)); + return info; +} + +void uf2_info_free(uf2_info_t *info) { + if (!info) + return; + free(info->fw_name); + free(info->fw_version); + free(info->lt_version); + free(info->board); + free(info); +} + +uf2_err_t uf2_check_block(uf2_ota_t *ctx, uf2_block_t *block) { + if (block->magic1 != UF2_MAGIC_1) + return UF2_ERR_MAGIC; + if (block->magic2 != UF2_MAGIC_2) + return UF2_ERR_MAGIC; + if (block->magic3 != UF2_MAGIC_3) + return UF2_ERR_MAGIC; + if (block->file_container) + // ignore file containers, for now + return UF2_ERR_IGNORE; + if (!block->has_family_id || block->file_size != ctx->family_id) + // require family_id + return UF2_ERR_FAMILY; + return UF2_ERR_OK; +} + +uf2_err_t uf2_parse_header(uf2_ota_t *ctx, uf2_block_t *block, uf2_info_t *info) { + if (!block->has_tags || block->file_container || block->len) + // header must have tags and no data + return UF2_ERR_NOT_HEADER; + + uf2_err_t err = uf2_parse_block(ctx, block, info); + if (err) + return err; + + if ((ctx->ota_idx == 1 && !ctx->has_ota1) || !ctx->has_ota2) + return UF2_ERR_OTA_WRONG; + return UF2_ERR_OK; +} + +uf2_err_t uf2_write(uf2_ota_t *ctx, uf2_block_t *block) { + if (ctx->seq == 0) + return uf2_parse_header(ctx, block, NULL); + if (block->not_main_flash || !block->len) + // ignore blocks not meant for flashing + return UF2_ERR_IGNORE; + + uf2_err_t err = uf2_parse_block(ctx, block, NULL); + if (err) + return err; + + if (!ctx->part1 && !ctx->part2) + // no partitions set at all + return UF2_ERR_PART_UNSET; + + fal_partition_t part = uf2_get_target_part(ctx); + if (!part) + // image is not for current OTA scheme + return UF2_ERR_IGNORE; + + if (ctx->ota_idx == 2 && ctx->binpatch_len) { + // apply binpatch + err = uf2_binpatch(block->data, ctx->binpatch, ctx->binpatch_len); + if (err) + return err; + } + + int ret; + // erase sectors if needed + if (!uf2_is_erased(ctx, block->addr, block->len)) { + ret = fal_partition_erase(part, block->addr, block->len); + if (ret < 0) + return UF2_ERR_ERASE_FAILED; + ctx->erased_offset = block->addr; + ctx->erased_length = ret; + } + // write data to flash + ret = fal_partition_write(part, block->addr, block->data, block->len); + if (ret < 0) + return UF2_ERR_WRITE_FAILED; + if (ret != block->len) + return UF2_ERR_WRITE_LENGTH; + return UF2_ERR_OK; +} diff --git a/arduino/libretuya/libraries/Update/uf2ota/uf2ota.h b/arduino/libretuya/libraries/Update/uf2ota/uf2ota.h new file mode 100644 index 0000000..aadfbbb --- /dev/null +++ b/arduino/libretuya/libraries/Update/uf2ota/uf2ota.h @@ -0,0 +1,68 @@ +/* Copyright (c) Kuba Szczodrzyński 2022-05-28. */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#include "uf2types.h" + +/** + * @brief Create an UF2 OTA context. + * + * @param ota_idx target OTA index + * @param family_id expected family ID + * @return uf2_ota_t* heap-allocated structure + */ +uf2_ota_t *uf2_ctx_init(uint8_t ota_idx, uint32_t family_id); + +/** + * @brief Create an UF2 Info structure. + * + * @return uf2_info_t* heap-allocated structure + */ +uf2_info_t *uf2_info_init(); + +/** + * @brief Free values in the info structure AND the structure itself. + * + * @param info structure to free; may be NULL + */ +void uf2_info_free(uf2_info_t *info); + +/** + * @brief Check if block is valid. + * + * @param ctx context + * @param block block to check + * @return uf2_err_t error code; UF2_ERR_OK and UF2_ERR_IGNORE denote valid blocks + */ +uf2_err_t uf2_check_block(uf2_ota_t *ctx, uf2_block_t *block); + +/** + * @brief Parse header block (LibreTuya UF2 first block). + * + * Note: caller should call uf2_check_block() first. + * + * @param ctx context + * @param block block to parse + * @param info structure to write firmware info, NULL if not used + * @return uf2_err_t error code + */ +uf2_err_t uf2_parse_header(uf2_ota_t *ctx, uf2_block_t *block, uf2_info_t *info); + +/** + * @brief Write the block to flash memory. + * + * Note: caller should call uf2_check_block() first. + * + * @param ctx context + * @param block block to write + * @return uf2_err_t error code + */ +uf2_err_t uf2_write(uf2_ota_t *ctx, uf2_block_t *block); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/arduino/libretuya/libraries/Update/uf2ota/uf2priv.c b/arduino/libretuya/libraries/Update/uf2ota/uf2priv.c new file mode 100644 index 0000000..35cc1ca --- /dev/null +++ b/arduino/libretuya/libraries/Update/uf2ota/uf2priv.c @@ -0,0 +1,146 @@ +/* Copyright (c) Kuba Szczodrzyński 2022-05-29. */ + +#include "uf2priv.h" + +uf2_err_t uf2_parse_block(uf2_ota_t *ctx, uf2_block_t *block, uf2_info_t *info) { + if (block->block_seq != ctx->seq) + // sequence number must match + return UF2_ERR_SEQ_MISMATCH; + ctx->seq++; // increment sequence number after checking it + + if (!block->has_tags) + // no tags in this block, no further processing needed + return UF2_ERR_OK; + + if (block->len > (476 - 4 - 4)) + // at least one tag + last tag must fit + return UF2_ERR_DATA_TOO_LONG; + + uint8_t *tags_start = block->data + block->len; + uint8_t tags_len = 476 - block->len; + uint8_t tags_pos = 0; + if (block->has_md5) + tags_len -= 24; + + ctx->binpatch_len = 0; // binpatch applies to one block only + char *part1 = NULL; + char *part2 = NULL; + + uf2_tag_type_t type; + while (tags_pos < tags_len) { + uint8_t len = uf2_read_tag(tags_start + tags_pos, &type); + if (!len) + break; + tags_pos += 4; // skip tag header + uint8_t *tag = tags_start + tags_pos; + + char **str_dest = NULL; // char* to copy the tag into + + switch (type) { + case UF2_TAG_OTA_VERSION: + if (tag[0] != 1) + return UF2_ERR_OTA_VER; + break; + case UF2_TAG_FIRMWARE: + if (info) + str_dest = &(info->fw_name); + break; + case UF2_TAG_VERSION: + if (info) + str_dest = &(info->fw_version); + break; + case UF2_TAG_LT_VERSION: + if (info) + str_dest = &(info->lt_version); + break; + case UF2_TAG_BOARD: + if (info) + str_dest = &(info->board); + break; + case UF2_TAG_HAS_OTA1: + ctx->has_ota1 = tag[0]; + break; + case UF2_TAG_HAS_OTA2: + ctx->has_ota2 = tag[0]; + break; + case UF2_TAG_PART_1: + str_dest = &(part1); + break; + case UF2_TAG_PART_2: + str_dest = &(part2); + break; + case UF2_TAG_BINPATCH: + ctx->binpatch = tag; + ctx->binpatch_len = len; + break; + default: + break; + } + + if (str_dest) { + *str_dest = (char *)zalloc(len + 1); + memcpy(*str_dest, tag, len); + } + // align position to 4 bytes + tags_pos += (((len - 1) / 4) + 1) * 4; + } + + if (part1 && part2) { + // update current target partition + uf2_err_t err = uf2_update_parts(ctx, part1, part2); + if (err) + return err; + } else if (part1 || part2) { + // only none or both partitions can be specified + return UF2_ERR_PART_ONE; + } + + return UF2_ERR_OK; +} + +uint8_t uf2_read_tag(const uint8_t *data, uf2_tag_type_t *type) { + uint8_t len = data[0]; + if (!len) + return 0; + uint32_t tag_type = *((uint32_t *)data); + if (!tag_type) + return 0; + *type = tag_type >> 8; // remove tag length byte + return len - 4; +} + +uf2_err_t uf2_update_parts(uf2_ota_t *ctx, char *part1, char *part2) { + // reset both target partitions + ctx->part1 = NULL; + ctx->part2 = NULL; + // reset offsets as they probably don't apply to this partition + ctx->erased_offset = 0; + ctx->erased_length = 0; + + if (part1[0]) { + ctx->part1 = fal_partition_find(part1); + if (!ctx->part1) + return UF2_ERR_PART_404; + } + if (part2[0]) { + ctx->part2 = fal_partition_find(part2); + if (!ctx->part2) + return UF2_ERR_PART_404; + } + + return UF2_ERR_OK; +} + +fal_partition_t uf2_get_target_part(uf2_ota_t *ctx) { + if (ctx->ota_idx == 1) + return ctx->part1; + if (ctx->ota_idx == 2) + return ctx->part2; + return NULL; +} + +bool uf2_is_erased(uf2_ota_t *ctx, uint32_t offset, uint32_t length) { + uint32_t erased_end = ctx->erased_offset + ctx->erased_length; + uint32_t end = offset + length; + return (offset >= ctx->erased_offset) && (end <= erased_end); +} diff --git a/arduino/libretuya/libraries/Update/uf2ota/uf2priv.h b/arduino/libretuya/libraries/Update/uf2ota/uf2priv.h new file mode 100644 index 0000000..abbe1e4 --- /dev/null +++ b/arduino/libretuya/libraries/Update/uf2ota/uf2priv.h @@ -0,0 +1,61 @@ +/* Copyright (c) Kuba Szczodrzyński 2022-05-28. */ + +#pragma once + +// include platform stdlib APIs +#include + +#include "uf2binpatch.h" +#include "uf2types.h" + +/** + * @brief Parse a block and extract information from tags. + * + * @param ctx context + * @param block block to parse + * @param info structure to write firmware info, NULL if not used + * @return uf2_err_t error code + */ +uf2_err_t uf2_parse_block(uf2_ota_t *ctx, uf2_block_t *block, uf2_info_t *info); + +/** + * @brief Parse a tag. + * + * @param data pointer to tag header beginning + * @param type [out] parsed tag type + * @return uint8_t parsed tag data length (excl. header); 0 if invalid/last tag + */ +uint8_t uf2_read_tag(const uint8_t *data, uf2_tag_type_t *type); + +/** + * @brief Update destination partitions in context. + * + * Partition names cannot be NULL. + * + * Returns UF2_ERR_IGNORE if specified partitions don't match the + * current OTA index. + * + * @param ctx context + * @param part1 partition 1 name or empty string + * @param part2 partition 2 name or empty string + * @return uf2_err_t error code + */ +uf2_err_t uf2_update_parts(uf2_ota_t *ctx, char *part1, char *part2); + +/** + * @brief Get target flashing partition, depending on OTA index. + * + * @param ctx context + * @return fal_partition_t target partition or NULL if not set + */ +fal_partition_t uf2_get_target_part(uf2_ota_t *ctx); + +/** + * Check if specified flash memory region was already erased during update. + * + * @param ctx context + * @param offset offset to check + * @param length length to check + * @return bool true/false + */ +bool uf2_is_erased(uf2_ota_t *ctx, uint32_t offset, uint32_t length); diff --git a/arduino/libretuya/libraries/Update/uf2ota/uf2types.h b/arduino/libretuya/libraries/Update/uf2ota/uf2types.h new file mode 100644 index 0000000..c8d09e4 --- /dev/null +++ b/arduino/libretuya/libraries/Update/uf2ota/uf2types.h @@ -0,0 +1,103 @@ +/* Copyright (c) Kuba Szczodrzyński 2022-05-28. */ + +#pragma once + +#include +#include + +#include + +#define UF2_MAGIC_1 0x0A324655 +#define UF2_MAGIC_2 0x9E5D5157 +#define UF2_MAGIC_3 0x0AB16F30 + +#define UF2_BLOCK_SIZE sizeof(uf2_block_t) + +typedef struct __attribute__((packed)) { + // 32 byte header + uint32_t magic1; + uint32_t magic2; + + // flags split as bitfields + bool not_main_flash : 1; + uint16_t dummy1 : 11; + bool file_container : 1; + bool has_family_id : 1; + bool has_md5 : 1; + bool has_tags : 1; + uint16_t dummy2 : 16; + + uint32_t addr; + uint32_t len; + uint32_t block_seq; + uint32_t block_count; + uint32_t file_size; // or familyID; + uint8_t data[476]; + uint32_t magic3; +} uf2_block_t; + +typedef struct { + uint32_t seq; // current block sequence number + + uint8_t *binpatch; // current block's binpatch (if any) -> pointer inside block->data + uint8_t binpatch_len; // binpatch length + + bool has_ota1; // image has any data for OTA1 + bool has_ota2; // image has any data for OTA2 + + uint8_t ota_idx; // target OTA index + uint32_t family_id; // expected family ID + + uint32_t erased_offset; // offset of region erased during update + uint32_t erased_length; // length of erased region + + fal_partition_t part1; // OTA1 target partition + fal_partition_t part2; // OTA2 target partition +} uf2_ota_t; + +typedef struct { + char *fw_name; + char *fw_version; + char *lt_version; + char *board; +} uf2_info_t; + +typedef enum { + UF2_TAG_VERSION = 0x9FC7BC, // version of firmware file - UTF8 semver string + UF2_TAG_PAGE_SIZE = 0x0BE9F7, // page size of target device (32 bit unsigned number) + UF2_TAG_SHA2 = 0xB46DB0, // SHA-2 checksum of firmware (can be of various size) + UF2_TAG_DEVICE = 0x650D9D, // description of device (UTF8) + UF2_TAG_DEVICE_ID = 0xC8A729, // device type identifier + // LibreTuya custom, tags + UF2_TAG_OTA_VERSION = 0x5D57D0, // format version + UF2_TAG_BOARD = 0xCA25C8, // board name (lowercase code) + UF2_TAG_FIRMWARE = 0x00DE43, // firmware description / name + UF2_TAG_LT_VERSION = 0x59563D, // LT version (semver) + UF2_TAG_PART_1 = 0x805946, // OTA1 partition name + UF2_TAG_PART_2 = 0xA1E4D7, // OTA2 partition name + UF2_TAG_HAS_OTA1 = 0xBBD965, // image has any data for OTA1 + UF2_TAG_HAS_OTA2 = 0x92280E, // image has any data for OTA2 + UF2_TAG_BINPATCH = 0xB948DE, // binary patch to convert OTA1->OTA2 +} uf2_tag_type_t; + +typedef enum { + UF2_OPC_DIFF32 = 0xFE, +} uf2_opcode_t; + +typedef enum { + UF2_ERR_OK = 0, + UF2_ERR_IGNORE, // block should be ignored + UF2_ERR_MAGIC, // wrong magic numbers + UF2_ERR_FAMILY, // family ID mismatched + UF2_ERR_NOT_HEADER, // block is not a header + UF2_ERR_OTA_VER, // unknown/invalid OTA format version + UF2_ERR_OTA_WRONG, // no data for current OTA index + UF2_ERR_PART_404, // no partition with that name + UF2_ERR_PART_ONE, // only one partition tag in a block + UF2_ERR_PART_UNSET, // image broken - attempted to write without target partition + UF2_ERR_DATA_TOO_LONG, // data too long - tags won't fit + UF2_ERR_SEQ_MISMATCH, // sequence number mismatched + UF2_ERR_ERASE_FAILED, // erasing flash failed + UF2_ERR_WRITE_FAILED, // writing to flash failed + UF2_ERR_WRITE_LENGTH, // wrote fewer data than requested +} uf2_err_t; diff --git a/arduino/realtek-ambz/port/flashdb/fal_flash_ambz_port.c b/arduino/realtek-ambz/port/flashdb/fal_flash_ambz_port.c index 9435090..1e8f98b 100644 --- a/arduino/realtek-ambz/port/flashdb/fal_flash_ambz_port.c +++ b/arduino/realtek-ambz/port/flashdb/fal_flash_ambz_port.c @@ -18,7 +18,7 @@ static int erase(long offset, size_t size) { for (uint16_t i = 0; i < size; i++) { flash_erase_sector(NULL, offset + i * FLASH_ERASE_MIN_SIZE); } - return size; + return size * FLASH_ERASE_MIN_SIZE; } const struct fal_flash_dev flash0 = { diff --git a/docs/ota/README.md b/docs/ota/README.md index b382f5e..9da088c 100644 --- a/docs/ota/README.md +++ b/docs/ota/README.md @@ -11,6 +11,7 @@ UF2 files may contain multiple firmware images that are to be flashed, i.e. main Some CPUs support dual-OTA schemes: firmware runs from one image, while the other one is reserved for updated firmware. After applying the update, a reboot causes to run the other image instead. Each firmware image may be either applicable: + 1. only when flashing OTA1 (`part:file::`) 2. only when flashing OTA2 (`::part:file`) 3. for both schemes to a single partition (`part:file`) @@ -88,11 +89,16 @@ OTA2 images are not stored directly, as that would needlessly double the UF2 fil There can be at most one binpatch tag in a UF2 block. It has the following format: - opcode (1 byte) - operation type: + - `DIFF32` (0xFE) - difference between 32-bit values + - length (1 byte) - data length - data (`length` bytes) + - for `DIFF32`: + - difference value (signed int 32-bit) + - offset table (`length-4` bytes) The presented structure can be repeated in a single binpatch tag. diff --git a/docs/ota/library.md b/docs/ota/library.md new file mode 100644 index 0000000..5900d7b --- /dev/null +++ b/docs/ota/library.md @@ -0,0 +1,58 @@ +# uf2ota library + +uf2ota library allows to write a LibreTuya UF2 file to the flash, while parsing all the necessary tags. It manages the target partitions, compatibility checks, and works on top of the FAL provided by FlashDB. + +## Usage example + +```c +uint8_t target = 1; // target OTA scheme - 1 or 2 +uint32_t family = RTL8710B; // chip's UF2 family ID +uf2_ota_t *ctx = uf2_ctx_init(target, family); +uf2_info_t *info = uf2_info_init(); // optional, for getting firmware info +uf2_block_t *block = (uf2_block_t *)malloc(UF2_BLOCK_SIZE); +uf2_err_t err; + +// ... // read the first header block (512 bytes) into *block + +// check the block for validity +err = uf2_check_block(ctx, block); +if (err > UF2_ERR_IGNORE) + // handle the error + return; + +// parse the header block +// note: if you don't need info, you can skip this step and call uf2_write() directly +err = uf2_parse_header(ctx, block, info); +if (err) + // handle the error + return; + +while (/* have input data */) { + + // ... // read the next block into *block + + // check the block for validity + err = uf2_check_block(ctx, block); + if (err == UF2_ERR_IGNORE) + // skip this block + continue; + if (err) + // handle the error + return; + + // write the block to flash + err = uf2_write(ctx, block); + if (err > UF2_ERR_IGNORE) + // handle the error + return; +} + +// finish the update process + +// ... // activate your new OTA partition + +// cleanup +free(ctx); +free(block); +uf2_info_free(info); +```