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
112 lines
4.0 KiB
Python
112 lines
4.0 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
flash_package.py - erase the chip and program the merged package via
|
|
NS-LINK (NSpyocd). Streams the vendor tool's output with the vendor
|
|
banner replaced by the CAIIC one (the banner is baked into the
|
|
PyInstaller-packed NSpyocd.exe and cannot be edited).
|
|
|
|
usage: flash_package.bat [image]
|
|
[image] optional, default: tools/out/mothercup_ble_prod.hex
|
|
|
|
Connect NS-LINK to SWD (PA4/PA5) and the reset pin first, and CLOSE
|
|
Keil (it holds the probe).
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
# NSpyocd is a PyInstaller-packed pyocd: its banner is inside the
|
|
# compressed bundle, so we rebrand it on the fly. The console encoding
|
|
# varies (UTF-8 vs GBK), match the byte pattern in both.
|
|
BANNER_PATTERNS = [
|
|
("国民技术NSLINK上位机".encode("utf-8"), b"CAIIC NSLINK UMP"),
|
|
("国民技术NSLINK上位机".encode("gbk"), b"CAIIC NSLINK UMP"),
|
|
]
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
PYOCD = os.path.join(ROOT, "nations-tec", "N32WB03x_SDK_V2.0.0",
|
|
"utilities", "dfu", "NSpyocd", "NSpyocd.exe")
|
|
# The builtin "n32wb031" target only maps 256KB of flash (fails at
|
|
# 0x01040000 on the dual-bank package); the DFP pack target
|
|
# "n32wb031keq6_2" maps the full 512KB of the KEQ6-2 we actually use.
|
|
PACK = os.path.join(ROOT, "nations-tec", "N32WB03x_DFP.1.4.0.pack")
|
|
TARGET = "n32wb031keq6_2"
|
|
|
|
|
|
def run_step(args):
|
|
"""Run one NSpyocd step, streaming output with the banner replaced.
|
|
Reads in small chunks (not line-buffered) so the tool's \\r-based
|
|
progress bar redraws live. Returns the process exit code."""
|
|
proc = subprocess.Popen([PYOCD] + args,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT)
|
|
out = sys.stdout.buffer
|
|
keep = max(len(p) for p, _ in BANNER_PATTERNS) - 1
|
|
tail = b""
|
|
while True:
|
|
chunk = proc.stdout.read(256)
|
|
if not chunk:
|
|
break
|
|
buf = tail + chunk
|
|
# replace on the COMBINED buffer first (a pattern straddling the
|
|
# chunk boundary only exists contiguously here), then hold back
|
|
# the last maxpat-1 bytes so a pattern split by the next chunk
|
|
# is still seen whole
|
|
for old, new in BANNER_PATTERNS:
|
|
buf = buf.replace(old, new)
|
|
emit, tail = buf[:-keep], buf[-keep:]
|
|
out.write(emit)
|
|
out.flush()
|
|
for old, new in BANNER_PATTERNS:
|
|
tail = tail.replace(old, new)
|
|
out.write(tail)
|
|
out.flush()
|
|
return proc.wait()
|
|
|
|
|
|
def main():
|
|
image = sys.argv[1] if len(sys.argv) > 1 else \
|
|
os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
"out", "mothercup_ble_prod.hex")
|
|
if not os.path.isfile(image):
|
|
print("%s not found - run tools\\make_package.bat first" % image)
|
|
return 1
|
|
|
|
# -M under-reset: connect while holding the chip in reset
|
|
# -f 1000000: 1 MHz SWD clock (slow but tolerant of wiring)
|
|
print("Erasing chip...", flush=True)
|
|
rc = run_step(["erase", "--chip", "--pack", PACK, "-M", "under-reset",
|
|
"-f", "1000000", "-t", TARGET])
|
|
if rc != 0:
|
|
return fail()
|
|
|
|
print("Programming %s ..." % image, flush=True)
|
|
# -O smart_flash=false: with the pack target the pre-program diff pass
|
|
# faults reading 0x01000000; programming+verify itself is fine.
|
|
rc = run_step(["load", "--pack", PACK, "-M", "under-reset",
|
|
"-f", "1000000", "-t", TARGET,
|
|
"-O", "smart_flash=false", image])
|
|
if rc != 0:
|
|
return fail()
|
|
|
|
print("Program Finish!")
|
|
return 0
|
|
|
|
|
|
def fail():
|
|
print()
|
|
print("*** FLASH FAILED ***")
|
|
print("Troubleshooting:")
|
|
print(" 1. Close Keil / any tool holding the probe, replug NS-LINK USB")
|
|
print(" 2. List probes: \"%s\" list" % PYOCD)
|
|
print(" 3. Check wiring: SWDIO=PA5 SWCLK=PA4 GND and the RESET pin")
|
|
print(" 4. If several probes are attached, add -u <probe_uid> "
|
|
"to the commands above")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|