Bound nRF52 UF2 images and reduce repeater BLE DFU memory

This commit is contained in:
mikecarper committed 2026-09-25 08:09:38 -07:00
1 parent fc6efcca03
commit aa4598f739
5 files changed
+130 -5

No files matched your search

+2
View File
@@ -4214,6 +4214,7 @@ collect_nrf52_artifacts() {
local build_output_dir="${PIO_BUILD_DIR_OVERRIDE:-${PLATFORMIO_BUILD_DIR:-.pio/build}}/${pio_env_name}"
python3 bin/uf2conv/uf2conv.py "${build_output_dir}/firmware.hex" -c -o "${build_output_dir}/firmware.uf2" -f 0xADA52840 || return $?
python3 scripts/check_nrf52_uf2.py "${build_output_dir}/firmware.uf2" || return $?
copy_build_output "${build_output_dir}/firmware.uf2" "${OUTPUT_DIR}/${firmware_filename}.uf2" || return $?
if [ -f "${build_output_dir}/firmware.zip" ]; then
copy_build_output "${build_output_dir}/firmware.zip" "${OUTPUT_DIR}/${firmware_filename}.zip" || return $?
@@ -4295,6 +4296,7 @@ PY
;;
NRF52_PLATFORM)
output_artifact_exists "${firmware_filename}.uf2" \
&& python3 scripts/check_nrf52_uf2.py "${OUTPUT_DIR}/${firmware_filename}.uf2" >/dev/null 2>&1 \
&& { [ "${REQUIRE_OTA_UPDATES:-0}" != "1" ] \
|| output_artifact_exists "${firmware_filename}.zip"; }
;;
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Reject nRF52840 UF2 images that overlap the OTAFIX bootloader scratch area."""
from __future__ import annotations
import argparse
from pathlib import Path
import struct
SECTOR_BYTES = 512
PAYLOAD_BYTES = 256
APP_FAMILY = 0xADA52840
SAFE_APP_END = 0xE0000
MAGIC_START = (0x0A324655, 0x9E5D5157)
MAGIC_END = 0x0AB16F30
FAMILY_FLAG = 0x2000
def check_uf2(path: Path, app_end: int = SAFE_APP_END) -> tuple[int, int, int]:
data = path.read_bytes()
if not data or len(data) % SECTOR_BYTES:
raise ValueError(f"{path}: incomplete UF2 sectors")
count = len(data) // SECTOR_BYTES
start = None
for index in range(count):
offset = index * SECTOR_BYTES
magic0, magic1, flags, address, size, block_no, blocks, family = struct.unpack_from(
"<8I", data, offset
)
trailer = struct.unpack_from("<I", data, offset + 508)[0]
if (magic0, magic1) != MAGIC_START or trailer != MAGIC_END:
raise ValueError(f"{path}: invalid UF2 block {index}")
if (flags & FAMILY_FLAG) == 0 or flags & 1 or family != APP_FAMILY:
raise ValueError(f"{path}: wrong nRF52840 application family")
if size != PAYLOAD_BYTES or block_no != index or blocks != count:
raise ValueError(f"{path}: inconsistent UF2 block {index}")
if start is None:
start = address
if address != start + index * PAYLOAD_BYTES:
raise ValueError(f"{path}: noncontiguous UF2 application")
assert start is not None
end = start + count * PAYLOAD_BYTES
if start not in (0x26000, 0x27000):
raise ValueError(f"{path}: unexpected nRF52840 application base 0x{start:X}")
if end > app_end:
raise ValueError(
f"{path}: application 0x{start:X}..0x{end:X} exceeds the "
f"OTAFIX-compatible limit 0x{app_end:X}"
)
return start, end, count
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("uf2", type=Path)
args = parser.parse_args()
try:
start, end, blocks = check_uf2(args.uf2)
except (OSError, ValueError) as error:
parser.exit(1, f"nRF52 UF2 qualification failed: {error}\n")
print(f"nRF52 UF2 safe: 0x{start:X}..0x{end:X}, {blocks} blocks")
if __name__ == "__main__":
main()
+2
View File
@@ -15,6 +15,7 @@ from urllib.parse import quote, urljoin
sys.path.insert(0, str(Path(__file__).resolve().parent))
from firmware_memory_manifest import validate_package
from check_nrf52_uf2 import check_uf2
ROOT = Path(__file__).resolve().parents[1]
@@ -64,6 +65,7 @@ def collect_artifacts(directory, version):
if platform == "NRF52_PLATFORM":
if ".uf2" not in extensions or (manifest.get("ota_update_verified") and ".zip" not in extensions):
raise ValueError(f"{stem}: nRF52 UF2/DFU artifacts incomplete")
check_uf2(directory / (stem + ".uf2"))
if any(item.stat().st_size == 0 for item in files):
raise ValueError(f"{stem}: empty firmware artifact")
memory = validate_package(directory / stem)
+6 -5
View File
@@ -641,11 +641,12 @@ bool NRF52Board::startOTAUpdate(const char *id, char reply[], bool force_ap) {
}
if (!ota_ble_started) {
// Config the peripheral connection with maximum bandwidth
// more SRAM required by SoftDevice
// Note: All config***() function must be called before begin()
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16);
// The application only needs the buttonless DFU control connection; the
// bootloader handles firmware data. Large ATT and queue reservations can
// make SoftDevice startup fail on an otherwise healthy repeater.
// All config***() calls must precede begin().
Bluefruit.configPrphConn(BLE_GATT_ATT_MTU_DEFAULT,
BLE_GAP_EVENT_LENGTH_MIN, 2, 1);
if (!Bluefruit.begin(1, 0)) {
return false;
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Keep manual UF2 images below the OTAFIX scratch bank on nRF52840."""
from __future__ import annotations
from pathlib import Path
import struct
import sys
import tempfile
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
from check_nrf52_uf2 import check_uf2 # noqa: E402
def uf2(start: int, count: int, family: int = 0xADA52840) -> bytes:
image = bytearray(count * 512)
for index in range(count):
offset = index * 512
struct.pack_into("<8I", image, offset, 0x0A324655, 0x9E5D5157,
0x2000, start + index * 256, 256, index, count, family)
struct.pack_into("<I", image, offset + 508, 0x0AB16F30)
return bytes(image)
class Nrf52Uf2SlotTest(unittest.TestCase):
def check(self, data: bytes) -> tuple[int, int, int]:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "firmware.uf2"
path.write_bytes(data)
return check_uf2(path)
def test_11717_rak4631_geometry_fits(self) -> None:
self.assertEqual(self.check(uf2(0x26000, 2117)),
(0x26000, 0x26000 + 2117 * 256, 2117))
def test_11716_rak4631_geometry_is_rejected(self) -> None:
with self.assertRaisesRegex(ValueError, "exceeds the OTAFIX-compatible limit"):
self.check(uf2(0x26000, 3138))
def test_wrong_family_is_rejected(self) -> None:
with self.assertRaisesRegex(ValueError, "wrong nRF52840 application family"):
self.check(uf2(0x26000, 1, 0xE48BFF56))
def test_missing_block_is_rejected(self) -> None:
data = bytearray(uf2(0x26000, 2))
struct.pack_into("<I", data, 512 + 12, 0x26300)
with self.assertRaisesRegex(ValueError, "noncontiguous"):
self.check(bytes(data))
if __name__ == "__main__":
unittest.main()