Firmware: - bsp_usart: RX moved from RXDNE byte-queue IRQ to DMA CH2 circular ring (2KB) + IDLE-irq wakeup; bytes keep landing during flash-erase interrupt-off windows (the reason lockstep was needed before) - app_ota: stream mode (ble_protocol.md section 6.7) - 16B BEGIN with flags bit0=STREAM (UART channel only), DATA acked on 4KB sector crossings (original chunk length; two ack-gating bugs fixed during field test), throttled BAD_STATE for go-back-N - FreeRTOS heap 20KB->18KB: the 2KB ring pushed the stack top past the 0x2000C000 SRAM cliff (probed via SWD: accesses above fault on this silicon, usable app SRAM is 32KB) which locked the board at boot; merge_image.py now hard-fails the package when the image initial SP leaves (0x20004000, 0x2000C000] Tools: - ble_ota_update.py: UART stream sender (8KB window, stall watchdog rewind, auto-fallback to lockstep on pre-V1.00.24 firmware, --lockstep to force); case-insensitive option parsing - flash_package.py/bat: stream NSpyocd output live (chunked reads keep the \r progress bar), vendor banner rebranded to CAIIC NSLINK UMP - merge_image.py: initial-SP cliff guard Verified: UART stream OTA both directions, 55.8KB in ~5.9s @9.5kB/s 0 rewinds (lockstep was 39s), PASS after reboot; board boot fixed and verified via SWD. Docs: ble_protocol.md section 6.7, dev log section 47 (+ SRAM cliff post-mortem), AGENTS.md RAM rule rewritten (both cliffs) + V1.00.25
742 lines
24 KiB
C
742 lines
24 KiB
C
/**
|
|
* @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[/flags]) -> OTA_DATA* (strictly sequential
|
|
* offsets; lockstep acks per frame, or sector-boundary acks in UART stream
|
|
* mode - ble_protocol.md §6.7) -> 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 <string.h>
|
|
#include <stddef.h>
|
|
|
|
#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;
|
|
|
|
static void ota_uart_sink(const uint8_t* frame, uint16_t len);
|
|
|
|
/* UART streaming mode (ble_protocol.md §6.7): OTA_BEGIN payload extended
|
|
* to 16B {size, crc32, version, flags}; flags bit0 = stream. In stream mode
|
|
* DATA frames are acked only at 4KB sector boundaries (the erase has
|
|
* completed by then) instead of per-frame lockstep; the host pipelines
|
|
* writes with an 8KB window and rewinds on BAD_STATE. BLE stays lockstep. */
|
|
#define OTA_BEGIN_FLAG_STREAM 0x01u
|
|
static uint8_t s_stream; /* current session is streaming */
|
|
static uint8_t s_bad_state_sent; /* throttle repeated BAD_STATE reports */
|
|
|
|
/**
|
|
* @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;
|
|
s_stream = 0;
|
|
s_bad_state_sent = 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}
|
|
* or the extended 16B form {..., flags u32} (ble_protocol.md §6.7).
|
|
*/
|
|
static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
|
|
{
|
|
uint32_t total, crc, version, flags;
|
|
uint32_t self, target;
|
|
|
|
if (len != 12u && len != 16u)
|
|
{
|
|
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);
|
|
flags = (len == 16u) ? ota_rd32(p + 12) : 0;
|
|
|
|
/* 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;
|
|
/* Streaming only on the UART channel: its RSP goes straight out on TX
|
|
* and the DMA ring buffers inbound bytes during flash erases. The BLE
|
|
* channel is read-polled and stays per-frame lockstep. */
|
|
s_stream = ((flags & OTA_BEGIN_FLAG_STREAM) != 0u &&
|
|
s_rsp_sink == ota_uart_sink) ? 1u : 0u;
|
|
s_bad_state_sent = 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). Ack policy depends on the session mode:
|
|
* - lockstep (BLE and legacy hosts): EVERY frame is acked with the
|
|
* new offset, the ack round trip covers a fresh sector's erase;
|
|
* - streaming (UART, BEGIN flags bit0): acks only at 4KB sector
|
|
* boundaries (erase completed) and at the final byte; the host
|
|
* pipelines frames inside an 8KB window and rewinds to the offset
|
|
* reported by an immediate BAD_STATE ack when a frame is lost.
|
|
*/
|
|
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;
|
|
const uint32_t chunk = dlen; /* dlen is consumed down to the sub-word
|
|
* tail by the staging logic below; the
|
|
* stream-ack boundary check needs the
|
|
* original chunk length */
|
|
|
|
if (off != s_offset)
|
|
{
|
|
/* out-of-order: report the expected offset, keep the session.
|
|
* In stream mode the frames after a lost one ALL mismatch until the
|
|
* host rewinds - report once, not per frame */
|
|
if (!s_stream || !s_bad_state_sent)
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_BAD_STATE, s_offset);
|
|
s_bad_state_sent = 1;
|
|
}
|
|
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;
|
|
}
|
|
s_bad_state_sent = 0; /* in-order again: re-arm the error report */
|
|
if (s_stream)
|
|
{
|
|
/* Streaming (UART): ack when a 4KB sector boundary was CROSSED by
|
|
* this chunk (its lazy erase has completed by then) or on the final
|
|
* byte. A chunk rarely lands exactly on a boundary (e.g. 240B
|
|
* frames step 4080->4320), so compare sector indices, not equality.
|
|
* NOTE: use chunk (the original length), not dlen which the staging
|
|
* logic has consumed to the sub-word tail. */
|
|
if (((s_offset - chunk) & ~0xFFFu) != (s_offset & ~0xFFFu) ||
|
|
s_offset == s_total_size)
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_OK, s_offset);
|
|
}
|
|
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 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;
|
|
}
|