/** * @file app_ota.c * @brief OTA receiver implementation (channel-agnostic: BLE OTA * characteristic, UART binary mode, legacy notify frame path). * * Flow: OTA_BEGIN (size/crc/version) -> OTA_DATA* (strictly sequential * offsets, EVERY frame acked with the new offset - lockstep flow control) * -> OTA_END (flush the sub-word tail, verify whole-image CRC32 against * flash, verify the vector table targets the right bank, update * bootsetting, reset). OTA_ABORT aborts the session. * * Direct-write design (no 4KB staging buffer): * - Sectors of the target bank are erased lazily: the first received byte * falling into a not-yet-erased sector triggers that sector's erase. * - Received payload bytes are programmed to flash immediately; only a * 4-byte word staging buffer is kept because Qflash_Write() requires a * 4-byte aligned length (tail bytes are padded with 0xFF at OTA_END). * * Notes: * - Runs in the BLE schedule task context; vTaskDelay() is available. * - Qflash erase/write disable interrupts for tens of ms per sector (SDK * driver behavior, the flash algorithm executes from RAM). The BLE link * survives this via the 5 s connection supervision timeout. */ #include "app_ota.h" #include "app_ble_proto.h" #include "bsp_usart.h" #include "dfu_layout.h" #include "boot_crc32.h" #include "n32wb03x.h" #include "n32wb03x_qflash.h" #include #include #include "FreeRTOS.h" #include "task.h" /* armlink execution-region base symbol: address of this running image */ extern uint32_t Image$$ER_IROM1$$Base; #define OTA_MIN_IMAGE_SIZE 4u /* Deferred reset after a successful OTA_END: the END response must reach * the host before the chip resets, otherwise the ack is lost (BLE: the RSP * sits in the read-back buffer until the host's ATT read fetches it). * Reset once the response was consumed AND a short grace elapsed (lets the * controller actually emit the ATT read response), or unconditionally after * the timeout so the device never hangs. */ #define OTA_RESET_GRACE_MS 300u #define OTA_RESET_TIMEOUT_MS 2000u /* OTA session state */ static uint8_t s_active; /* session in progress */ static uint32_t s_target_base; /* opposite bank base */ static uint32_t s_total_size; static uint32_t s_image_crc32; static uint32_t s_version; static uint32_t s_offset; /* bytes received so far */ static uint32_t s_prog; /* bytes programmed to flash */ static uint32_t s_erased; /* bytes erased (sector granularity) */ static uint8_t s_word_buf[4]; /* sub-word alignment staging */ static uint32_t s_word_fill; static uint8_t s_qflash_ready; /* Deferred post-OTA_END reset (see OTA_RESET_GRACE_MS above) */ static volatile uint8_t s_rsp_consumed; /* END RSP taken by the host */ static volatile uint8_t s_reset_pending; /* reset scheduled, polled by the BLE task */ static TickType_t s_reset_stamp; /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ static uint32_t ota_rd32(const uint8_t* p) { return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); } /* ------------------------------------------------------------------ */ /* Channel-agnostic response routing */ /* */ /* The OTA engine speaks 0xCA-framed OTA_RSP messages. Each transport */ /* channel (legacy notify, BLE OTA characteristic, UART binary mode) */ /* registers a sink before feeding frames in; the sink receives one */ /* complete framed OTA_RSP. NULL sink = legacy notify path via */ /* app_ble_proto_send_frame(). */ /* ------------------------------------------------------------------ */ static app_ota_rsp_sink_fn s_rsp_sink; /** * @brief CRC16-CCITT (poly 0x1021, init 0xFFFF) - same as the frame * protocol's proto_crc16 (app_ble_proto.c). */ static uint16_t ota_crc16(const uint8_t* d, uint16_t len) { uint16_t crc = 0xFFFFu; while (len--) { uint8_t bit; crc ^= (uint16_t)*d++ << 8; for (bit = 0; bit < 8u; bit++) { crc = (crc & 0x8000u) ? (uint16_t)((crc << 1) ^ 0x1021u) : (uint16_t)(crc << 1); } } return crc; } /** * @brief Send an OTA_RSP frame: {cmd_echo, status, offset(LE)}. */ static void ota_respond(uint8_t seq, uint8_t cmd_echo, uint8_t status, uint32_t offset) { uint8_t p[6]; p[0] = cmd_echo; p[1] = status; p[2] = (uint8_t)(offset & 0xFFu); p[3] = (uint8_t)((offset >> 8) & 0xFFu); p[4] = (uint8_t)((offset >> 16) & 0xFFu); p[5] = (uint8_t)((offset >> 24) & 0xFFu); if (s_rsp_sink != NULL) { /* Wrap into a 0xCA frame and hand to the channel */ uint8_t fr[5 + sizeof(p) + 2]; uint16_t crc; fr[0] = BLE_FRAME_SOF; fr[1] = BLE_FRAME_OTA_RSP; fr[2] = seq; fr[3] = sizeof(p) & 0xFFu; fr[4] = 0; memcpy(fr + 5, p, sizeof(p)); crc = ota_crc16(fr + 1, (uint16_t)(4u + sizeof(p))); /* TYPE..PAYLOAD */ fr[5 + sizeof(p)] = (uint8_t)(crc & 0xFFu); fr[5 + sizeof(p) + 1] = (uint8_t)(crc >> 8); s_rsp_sink(fr, (uint16_t)sizeof(fr)); return; } (void)app_ble_proto_send_frame(BLE_FRAME_OTA_RSP, seq, p, sizeof(p)); } uint32_t app_ota_current_bank_base(void) { return (uint32_t)&Image$$ER_IROM1$$Base; } void app_ota_abort(void) { s_active = 0; s_offset = 0; s_prog = 0; s_erased = 0; s_word_fill = 0; } /** * @brief Erase every sector of the target bank that [s_erased, end) touches. * @return 0 on success, BLE_OTA_ST_FLASH_FAIL on error. */ static uint8_t ota_erase_up_to(uint32_t end) { while (s_erased < end) { if (Qflash_Erase_Sector(s_target_base + s_erased) != 0) { return BLE_OTA_ST_FLASH_FAIL; } s_erased += CAIIC_FLASH_SECTOR_SIZE; } return 0; } /** * @brief Program one chunk to flash at the current program pointer. * @return 0 on success, BLE_OTA_ST_FLASH_FAIL on error. */ static uint8_t ota_program(const uint8_t* data, uint32_t len) { if (Qflash_Write(s_target_base + s_prog, (uint8_t*)data, len) != 0) { return BLE_OTA_ST_FLASH_FAIL; } s_prog += len; return 0; } /* ------------------------------------------------------------------ */ /* Frame handlers */ /* ------------------------------------------------------------------ */ /** * @brief OTA_BEGIN: {total_size u32, image_crc32 u32, version u32}. */ static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len) { uint32_t total, crc, version; uint32_t self, target; if (len != 12u) { ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_BAD_FRAME, 0); return; } total = ota_rd32(p); crc = ota_rd32(p + 4); version = ota_rd32(p + 8); /* One session at a time across all channels: a second BEGIN (e.g. BLE * while a UART session runs) is refused instead of corrupting state */ if (s_active) { ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_BAD_STATE, s_offset); return; } /* Pick the opposite bank as the update target */ self = app_ota_current_bank_base(); if (self == CAIIC_APP1_BASE) { target = CAIIC_APP2_BASE; } else if (self == CAIIC_APP2_BASE) { target = CAIIC_APP1_BASE; } else { ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_BAD_STATE, 0); return; } if (total < OTA_MIN_IMAGE_SIZE || total > CAIIC_APP_BANK_SIZE) { ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_SIZE_TOO_BIG, 0); return; } if (!s_qflash_ready) { Qflash_Init(); /* copy the flash algorithm to RAM (once) */ s_qflash_ready = 1; } s_active = 1; s_target_base = target; s_total_size = total; s_image_crc32 = crc; s_version = version; s_offset = 0; s_prog = 0; s_erased = 0; s_word_fill = 0; ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_OK, 0); } /** * @brief OTA_DATA: {offset u32 + data}. Data must arrive strictly in order. * Payload is programmed to flash immediately (sectors are erased * lazily); EVERY frame is acked with the new offset (lockstep flow * control on the BLE/UART OTA channels; the erase time of a fresh * sector is covered by the ack round trip). * Errors are acked immediately and the state is * kept so the peer can resend. */ static void ota_on_data(uint8_t seq, const uint8_t* p, uint16_t len) { uint32_t off; const uint8_t* data; uint32_t dlen; uint8_t err; if (!s_active) { ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_BAD_STATE, 0); return; } if (len < 4u) { ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_BAD_FRAME, s_offset); return; } off = ota_rd32(p); data = p + 4; dlen = (uint32_t)len - 4u; if (off != s_offset) { /* out-of-order: report the expected offset, keep the session */ ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_BAD_STATE, s_offset); return; } if (s_offset + dlen > s_total_size) { ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_SIZE_TOO_BIG, s_offset); return; } s_offset += dlen; /* Lazily erase every sector this chunk touches */ err = ota_erase_up_to(s_offset); if (err == 0 && s_word_fill != 0) { /* Complete the staged partial word first */ while (s_word_fill < 4u && dlen != 0) { s_word_buf[s_word_fill++] = *data++; dlen--; } if (s_word_fill == 4u) { err = ota_program(s_word_buf, 4u); s_word_fill = 0; } } if (err == 0 && dlen >= 4u) { /* Bulk aligned part goes straight from the frame payload to flash */ uint32_t n4 = dlen & ~3u; err = ota_program(data, n4); data += n4; dlen -= n4; } if (err == 0 && dlen != 0) { /* Sub-word tail: stage until the next chunk or OTA_END */ memcpy(s_word_buf, data, dlen); s_word_fill = dlen; } if (err != 0) { ota_respond(seq, BLE_FRAME_OTA_DATA, err, s_offset); app_ota_abort(); return; } /* Lockstep ack: every DATA frame is answered with the new offset */ ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_OK, s_offset); } /** * @brief Program the bootsetting record and verify by read-back. * @return 0 on success, BLE_OTA_ST_FLASH_FAIL on error. */ static uint8_t ota_update_bootsetting(void) { caiic_bootsetting_t bs; caiic_bank_t* bank; memcpy(&bs, (const void*)CAIIC_BOOTSETTING_ADDR, sizeof(bs)); /* Validate the existing record; rebuild from scratch when invalid */ if (bs.magic != CAIIC_BOOT_MAGIC || bs.crc32 != caiic_crc32((const uint8_t*)CAIIC_BOOTSETTING_ADDR, (uint32_t)offsetof(caiic_bootsetting_t, crc32))) { memset(&bs, 0, sizeof(bs)); } bs.magic = CAIIC_BOOT_MAGIC; bs.active_bank = (s_target_base == CAIIC_APP1_BASE) ? CAIIC_ACTIVE_BANK1 : CAIIC_ACTIVE_BANK2; bank = (s_target_base == CAIIC_APP1_BASE) ? &bs.bank1 : &bs.bank2; bank->start_address = s_target_base; bank->size = s_total_size; bank->crc32 = s_image_crc32; bank->version = s_version; bs.crc32 = caiic_crc32((const uint8_t*)&bs, (uint32_t)offsetof(caiic_bootsetting_t, crc32)); if (Qflash_Erase_Sector(CAIIC_BOOTSETTING_ADDR) != 0) { return BLE_OTA_ST_FLASH_FAIL; } /* sizeof(caiic_bootsetting_t) = 44, already 4-byte aligned */ if (Qflash_Write(CAIIC_BOOTSETTING_ADDR, (uint8_t*)&bs, sizeof(bs)) != 0) { return BLE_OTA_ST_FLASH_FAIL; } if (memcmp(&bs, (const void*)CAIIC_BOOTSETTING_ADDR, sizeof(bs)) != 0) { return BLE_OTA_ST_FLASH_FAIL; } return 0; } /** * @brief OTA_END: {crc32 u32}. Flush the staged tail, verify, switch bank, * reset. */ static void ota_on_end(uint8_t seq, const uint8_t* p, uint16_t len) { uint32_t crc_end, crc_calc; if (!s_active) { ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_BAD_STATE, 0); return; } if (len != 4u) { ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_BAD_FRAME, s_offset); return; } crc_end = ota_rd32(p); if (s_offset != s_total_size) { ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_BAD_STATE, s_offset); return; } /* Flush the staged sub-word tail, padded with 0xFF */ if (s_word_fill != 0) { while (s_word_fill < 4u) { s_word_buf[s_word_fill++] = 0xFFu; } if (ota_program(s_word_buf, 4u) != 0) { ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_FLASH_FAIL, s_offset); app_ota_abort(); return; } s_word_fill = 0; } /* Whole-image CRC32 read back from flash */ crc_calc = caiic_crc32((const uint8_t*)s_target_base, s_total_size); if (crc_calc != crc_end || crc_calc != s_image_crc32) { ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_CRC_FAIL, s_offset); app_ota_abort(); return; } /* Link-address sanity: the image vector table's reset handler must land * inside the target bank, otherwise the payload was linked for the wrong * bank (code is position-dependent on Cortex-M0) - refuse to switch. */ { uint32_t reset_pc = ota_rd32((const uint8_t*)s_target_base + 4u); if (reset_pc < s_target_base + 8u || reset_pc >= s_target_base + s_total_size) { ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_BANK_MISMATCH, s_offset); app_ota_abort(); return; } } if (ota_update_bootsetting() != 0) { ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_FLASH_FAIL, s_offset); app_ota_abort(); return; } s_rsp_consumed = 0; ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_OK, s_total_size); /* Defer the reset until the host has fetched the END response (polled * from the BLE schedule task, which keeps running meanwhile so the ATT * read / notify flush can actually happen) */ s_reset_stamp = xTaskGetTickCount(); s_reset_pending = 1; } /** * @brief Poll the deferred post-OTA_END reset. Must be called from the BLE * schedule task loop (it keeps scheduling BLE events while the host * fetches the END response). */ void app_ota_reset_poll(void) { if (s_reset_pending) { TickType_t elapsed = xTaskGetTickCount() - s_reset_stamp; if ((s_rsp_consumed && elapsed >= pdMS_TO_TICKS(OTA_RESET_GRACE_MS)) || elapsed >= pdMS_TO_TICKS(OTA_RESET_TIMEOUT_MS)) { NVIC_SystemReset(); } } } static void ota_uart_sink(const uint8_t* frame, uint16_t len); static uint8_t s_uart_exit_req; /* OTA_ABORT on UART: drop back to CLI now */ void app_ota_handle_frame(uint8_t type, uint8_t seq, const uint8_t* payload, uint16_t len) { switch (type) { case BLE_FRAME_OTA_BEGIN: ota_on_begin(seq, payload, len); break; case BLE_FRAME_OTA_DATA: ota_on_data(seq, payload, len); break; case BLE_FRAME_OTA_END: ota_on_end(seq, payload, len); break; case BLE_FRAME_OTA_ABORT: app_ota_abort(); ota_respond(seq, BLE_FRAME_OTA_ABORT, BLE_OTA_ST_OK, s_offset); /* Explicit exit from UART binary mode (ack already on the wire): * the CLI frontend returns to text mode immediately instead of * waiting out the 3 s idle timeout */ if (s_rsp_sink == ota_uart_sink) { s_uart_exit_req = 1; } break; default: break; } } /* ------------------------------------------------------------------ */ /* Transport channels (docs/ble_protocol.md section 6.6) */ /* */ /* The OTA engine is channel-agnostic. Each channel feeds raw bytes */ /* through the reassembler below and gets OTA_RSP frames via its sink: */ /* - BLE OTA characteristic (...e0005): write-with-response carries */ /* one frame; the RSP is fetched by an ATT read (lockstep, no */ /* notify needed). */ /* - UART binary mode (CLI "ota"): frames on the raw byte stream, */ /* RSP frames go straight out on TX. */ /* ------------------------------------------------------------------ */ #define OTA_CHAN_MAX_PAYLOAD 480u /* same limit as the frame protocol */ enum { CHAN_RX_WAIT_SOF = 0, CHAN_RX_HEADER, CHAN_RX_BODY, }; static uint8_t s_chanBuf[BLE_FRAME_HDR_LEN + OTA_CHAN_MAX_PAYLOAD + 2u]; static uint16_t s_chanHave; static uint16_t s_chanNeed; static uint8_t s_chanState; /** * @brief Feed channel bytes into the OTA frame reassembler. * @param sink response sink for this channel (NULL = legacy notify path) */ void app_ota_chan_rx(const uint8_t* data, uint16_t len, app_ota_rsp_sink_fn sink) { uint16_t i; s_rsp_sink = sink; for (i = 0; i < len; i++) { uint8_t ch = data[i]; switch (s_chanState) { case CHAN_RX_WAIT_SOF: if (ch == BLE_FRAME_SOF) { s_chanBuf[0] = ch; s_chanHave = 1; s_chanNeed = BLE_FRAME_HDR_LEN; s_chanState = CHAN_RX_HEADER; } break; case CHAN_RX_HEADER: s_chanBuf[s_chanHave++] = ch; if (s_chanHave == BLE_FRAME_HDR_LEN) { uint16_t payLen = (uint16_t)s_chanBuf[3] | ((uint16_t)s_chanBuf[4] << 8); if (payLen > OTA_CHAN_MAX_PAYLOAD) { s_chanState = CHAN_RX_WAIT_SOF; s_chanHave = 0; } else { s_chanNeed = (uint16_t)(BLE_FRAME_HDR_LEN + payLen + 2u); s_chanState = (s_chanNeed == s_chanHave) ? CHAN_RX_WAIT_SOF : CHAN_RX_BODY; } } break; case CHAN_RX_BODY: s_chanBuf[s_chanHave++] = ch; if (s_chanHave == s_chanNeed) { uint16_t payLen = (uint16_t)s_chanBuf[3] | ((uint16_t)s_chanBuf[4] << 8); uint16_t crc = ota_crc16(&s_chanBuf[1], (uint16_t)(4u + payLen)); uint16_t rxCrc = (uint16_t)s_chanBuf[5 + payLen] | ((uint16_t)s_chanBuf[5 + payLen + 1] << 8); if (crc == rxCrc) { app_ota_handle_frame(s_chanBuf[1], s_chanBuf[2], &s_chanBuf[5], payLen); } /* CRC mismatch: silently drop; the host's lockstep * resend recovers (its resync relies on our offset * report in the next ack) */ s_chanState = CHAN_RX_WAIT_SOF; s_chanHave = 0; } break; default: s_chanState = CHAN_RX_WAIT_SOF; s_chanHave = 0; break; } } } /* --- BLE OTA characteristic (...e0005): write frames in, read RSP out --- */ #define OTA_BLE_RSP_BUF_SIZE 16u /* largest RSP frame = 7B hdr + 6B */ static uint8_t s_bleRsp[OTA_BLE_RSP_BUF_SIZE]; static uint16_t s_bleRspLen; static void ota_ble_sink(const uint8_t* frame, uint16_t len) { if (len <= OTA_BLE_RSP_BUF_SIZE) { memcpy(s_bleRsp, frame, len); s_bleRspLen = len; } } void app_ota_chan_rx_ble(const uint8_t* data, uint16_t len) { app_ota_chan_rx(data, len, ota_ble_sink); } const uint8_t* app_ota_chan_rsp_ble(uint16_t* out_len) { *out_len = s_bleRspLen; if (s_bleRspLen != 0u) { s_rsp_consumed = 1; /* host has fetched the END response */ } s_bleRspLen = 0; /* consumed by the read */ return s_bleRsp; } /* --- UART binary mode (CLI "ota"): RSP frames straight out on TX --- */ static void ota_uart_sink(const uint8_t* frame, uint16_t len) { bsp_usart_tx_lock(); bsp_usart_write_dma(frame, len); bsp_usart_tx_unlock(); /* Blocking DMA TX: the frame is physically out when we get here */ s_rsp_consumed = 1; } uint8_t app_ota_chan_rx_uart(uint8_t ch) { uint8_t exit_req; app_ota_chan_rx(&ch, 1, ota_uart_sink); exit_req = s_uart_exit_req; s_uart_exit_req = 0; return exit_req; } /** * @brief Channel idle timeout helper: dropping the channel state also * aborts a half-finished session (called when UART binary mode or * the BLE link goes away mid-session). */ void app_ota_chan_idle(void) { s_chanState = CHAN_RX_WAIT_SOF; s_chanHave = 0; }