evan.liu f729d16ff2 V1.00.25: UART OTA stream mode + RX DMA ring + SRAM cliff fix
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
2026-09-05 09:48:28 +08:00

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;
}