Bound nRF52 UF2 images and reduce repeater BLE DFU memory
This commit is contained in:
1 parent
fc6efcca03
commit
aa4598f739
5 files changed
+130
-5
No files matched your search
@@ -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"; }
|
||||
;;
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user