Compare commits

...
8 changed files with 149 additions and 45 deletions
+15 -7
View File
@@ -71,6 +71,7 @@ from esphome.types import ConfigType
from .boards import BOARDS_ZEPHYR, BOOTLOADER_CONFIG, SELF_RESET_PINS
from .const import (
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_NOSD,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
@@ -200,12 +201,19 @@ def set_framework(config: ConfigType) -> ConfigType:
# DFU bootloader with Nordic's nrfutil; mcuboot uses mcumgr instead
SERIAL_DFU_BOOTLOADERS = (
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_NOSD,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
BOOTLOADER_NRF,
)
BOOTLOADERS = [*SERIAL_DFU_BOOTLOADERS, BOOTLOADER_MCUBOOT]
# SoftDevice id and major version in front of the application, for the debug component's check
SOFTDEVICES: dict[str, tuple[int, int | None]] = {
BOOTLOADER_ADAFRUIT_NRF52_SD132: (132, None),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: (140, 6),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: (140, 7),
}
_validate_toolchain = cv.toolchain_enum(_TOOLCHAINS)
@@ -427,13 +435,11 @@ async def to_code(config: ConfigType) -> None:
if config[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT:
cg.add_define("USE_BOOTLOADER_MCUBOOT")
elif "_sd" in config[KEY_BOOTLOADER]:
bootloader = config[KEY_BOOTLOADER].split("_")
sd_id = bootloader[2][2:]
cg.add_define("USE_SOFTDEVICE_ID", int(sd_id))
if (len(bootloader)) > 3:
sd_version = bootloader[3][1:]
cg.add_define("USE_SOFTDEVICE_VERSION", int(sd_version))
elif softdevice := SOFTDEVICES.get(config[KEY_BOOTLOADER]):
sd_id, sd_version = softdevice
cg.add_define("USE_SOFTDEVICE_ID", sd_id)
if sd_version is not None:
cg.add_define("USE_SOFTDEVICE_VERSION", sd_version)
zephyr_setup_preferences()
zephyr_to_code(config)
@@ -1149,12 +1155,14 @@ def run_compile(args, config: ConfigType) -> bool:
BOOTLOADER_ADAFRUIT_NRF52_SD132: ("0x0051", "0x009D"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: ("0x0052", "0x00B6"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA,0x0100,0x0123"),
BOOTLOADER_ADAFRUIT_NRF52_NOSD: ("0x0052", "0x00"), # no SoftDevice to require
}
# UF2 family IDs — nRF52832 vs nRF52840 per SoftDevice variant
_UF2_FAMILY_IDS = {
BOOTLOADER_ADAFRUIT_NRF52_SD132: "0x7EAED30A",
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: "0xADA52840",
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: "0xADA52840",
BOOTLOADER_ADAFRUIT_NRF52_NOSD: "0xADA52840",
}
# SDK < 2.9.2 places artifacts directly in build_dir/zephyr/.
+35 -12
View File
@@ -3,6 +3,7 @@ from esphome.components.zephyr.const import KEY_BOOTLOADER
from .const import (
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_NOSD,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
@@ -36,6 +37,16 @@ BOARDS_ZEPHYR = {
},
# Nordic nRF52840 Dongle (PCA10059): factory Open DFU bootloader at 0xE0000, kept in place
"nrf52840dongle": {KEY_BOOTLOADER: [BOOTLOADER_NRF]},
# Makerdiary nRF52840 MDK USB Dongle: its UF2 bootloader is an Adafruit fork. Dongles made since
# 2023 carry version 0.4.0 or newer without a SoftDevice; older ones carry 0.2.x with S140 6.1.1
"nrf52840_mdk_usb_dongle": {
KEY_BOOTLOADER: [
BOOTLOADER_ADAFRUIT_NRF52_NOSD,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
]
},
}
# Boards with a GPIO wired to their RESET pin: driving it low is a pin reset, which the factory
@@ -54,22 +65,34 @@ MCUMGR_OTA_BOOTLOADERS = (
# https://github.com/ffenix113/zigbee_home/blob/17bb7b9e9d375e756da9e38913f53303937fb66a/types/board/known_boards.go
# https://learn.adafruit.com/introducing-the-adafruit-nrf52840-feather?view=all#hathach-memory-map
# The bootloader with its settings and the MBR parameters, at the top of the flash
_ADAFRUIT_BOOTLOADER = Section(
"Adafruit_nRF52_Bootloader", 0xF4000, 0xC000, "flash_primary"
)
def _softdevice_layout(end: int) -> list[Section]:
"""Nordic's MBR page, the SoftDevice behind it up to ``end``, and the Adafruit bootloader.
The MBR page is named as the partition manager names it, so on a board that declares the
nRF5 bootloader it is not added a second time behind the SoftDevice, which would move the
application away from where the SoftDevice jumps to."""
return [
Section("nrf5_mbr", 0x0, 0x1000, "flash_primary"),
Section("SoftDevice", 0x1000, end - 0x1000, "flash_primary"),
_ADAFRUIT_BOOTLOADER,
]
BOOTLOADER_CONFIG = {
BOOTLOADER_ADAFRUIT_NRF52_SD132: [
Section("SoftDevice", 0x0, 0x26000, "flash_primary"),
Section("Adafruit_nRF52_Bootloader", 0xF4000, 0xC000, "flash_primary"),
],
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: [
Section("SoftDevice", 0x0, 0x26000, "flash_primary"),
Section("Adafruit_nRF52_Bootloader", 0xF4000, 0xC000, "flash_primary"),
],
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: [
Section("SoftDevice", 0x0, 0x27000, "flash_primary"),
Section("Adafruit_nRF52_Bootloader", 0xF4000, 0xC000, "flash_primary"),
],
BOOTLOADER_ADAFRUIT_NRF52_SD132: _softdevice_layout(0x26000),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: _softdevice_layout(0x26000),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: _softdevice_layout(0x27000),
# The board's BOARD_HAS_NRF5_BOOTLOADER already reserves the MBR page at 0x0, so the application
# links at 0x1000, where the bootloader loads it; only the bootloader itself is reserved here
BOOTLOADER_NRF: [
Section("open_bootloader", 0xE0000, 0x20000, "flash_primary"),
],
# Same MBR page and application start as above
BOOTLOADER_ADAFRUIT_NRF52_NOSD: [_ADAFRUIT_BOOTLOADER],
}
+1
View File
@@ -2,6 +2,7 @@ BOOTLOADER_ADAFRUIT = "adafruit"
BOOTLOADER_ADAFRUIT_NRF52_SD132 = "adafruit_nrf52_sd132"
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6 = "adafruit_nrf52_sd140_v6"
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 = "adafruit_nrf52_sd140_v7"
BOOTLOADER_ADAFRUIT_NRF52_NOSD = "adafruit_nrf52_nosd"
BOOTLOADER_NRF = "nrf"
EXTRA_ADC = [
@@ -0,0 +1,3 @@
nrf52:
dcdc: False
dfu: true # the 1200 bps touch reboots into the UF2 bootloader
@@ -0,0 +1,15 @@
esphome:
name: componenttestnrf52
friendly_name: $component_name
nrf52:
board: nrf52840_mdk_usb_dongle
logger:
level: VERY_VERBOSE
packages:
component_under_test: !include
file: $component_test_file
vars:
component_test_file: $component_test_file
@@ -7,7 +7,10 @@ from unittest.mock import Mock, patch
import pytest
from esphome.components import nrf52
from esphome.components.nrf52.const import BOOTLOADER_ADAFRUIT_NRF52_SD140_V7
from esphome.components.nrf52.const import (
BOOTLOADER_ADAFRUIT_NRF52_NOSD,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
)
from esphome.components.zephyr.const import KEY_BOARD, KEY_BOOTLOADER
import esphome.config_validation as cv
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION, Toolchain
@@ -24,7 +27,9 @@ def build_dir(tmp_path: Path) -> Path:
@pytest.fixture
def run_cmd(tmp_path: Path) -> Iterator[Mock]:
def run_cmd(tmp_path: Path, request: pytest.FixtureRequest) -> Iterator[Mock]:
"""The build's command runner; an indirect parameter picks the bootloader."""
bootloader = getattr(request, "param", BOOTLOADER_ADAFRUIT_NRF52_SD140_V7)
with (
patch.object(nrf52, "check_and_install"),
patch.object(nrf52, "_generate_cmake_lists", return_value=False),
@@ -37,10 +42,7 @@ def run_cmd(tmp_path: Path) -> Iterator[Mock]:
patch.object(
nrf52,
"zephyr_data",
return_value={
KEY_BOARD: "board",
KEY_BOOTLOADER: BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
},
return_value={KEY_BOARD: "board", KEY_BOOTLOADER: bootloader},
),
patch.object(nrf52.pch, "pch_enabled", return_value=False),
patch.object(nrf52, "run_command_ok", return_value=True) as run,
@@ -48,13 +50,20 @@ def run_cmd(tmp_path: Path) -> Iterator[Mock]:
yield run
def _uf2_cmd(run_cmd: Mock) -> list[str]:
return next(c.args[0] for c in run_cmd.call_args_list if "-f" in c.args[0])
def _genpkg_cmd(run_cmd: Mock) -> list[str]:
return next(c.args[0] for c in run_cmd.call_args_list if "genpkg" in c.args[0])
def _uf2_hex(run_cmd: Mock) -> str:
cmd = next(c.args[0] for c in run_cmd.call_args_list if "-f" in c.args[0])
return cmd[-1]
return _uf2_cmd(run_cmd)[-1]
def _dfu_hex(run_cmd: Mock) -> str:
cmd = next(c.args[0] for c in run_cmd.call_args_list if "genpkg" in c.args[0])
cmd = _genpkg_cmd(run_cmd)
return cmd[cmd.index("--application") + 1]
@@ -130,11 +139,22 @@ def test_stale_merged_hex_is_removed_before_building(
assert _dfu_hex(run_cmd) == app_hex
def test_sd140_v7_dfu_accepts_all_s140_7_softdevices(
build_dir: Path, run_cmd: Mock
@pytest.mark.parametrize(
("run_cmd", "sd_req"),
[
# Boards such as the Seeed XIAO nRF52840 ship S140 7.3.0, not 7.0.1
(BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, ["0x00CA", "0x0100", "0x0123"]),
# The Makerdiary MDK USB Dongle's bootloader has no SoftDevice to require
(BOOTLOADER_ADAFRUIT_NRF52_NOSD, ["0x00"]),
],
indirect=["run_cmd"],
)
def test_dfu_package_requires_the_bootloaders_softdevices(
build_dir: Path, run_cmd: Mock, sd_req: list[str]
) -> None:
"""Boards such as the Seeed XIAO nRF52840 ship S140 7.3.0, not 7.0.1."""
_build(build_dir, cv.Version(3, 4, 0), ["zephyr/zephyr/zephyr.hex"], run_cmd)
cmd = next(c.args[0] for c in run_cmd.call_args_list if "genpkg" in c.args[0])
sd_req = cmd[cmd.index("--sd-req") + 1].split(",")
assert sd_req == ["0x00CA", "0x0100", "0x0123"]
cmd = _genpkg_cmd(run_cmd)
assert cmd[cmd.index("--dev-type") + 1] == "0x0052"
assert cmd[cmd.index("--sd-req") + 1].split(",") == sd_req
uf2 = _uf2_cmd(run_cmd)
assert uf2[uf2.index("-f") + 1] == "0xADA52840"
@@ -7,21 +7,22 @@ from esphome.components.zephyr import Section
def test_testing_mode_sections_move_only_the_bootloader() -> None:
"""The bootloader pinned to the end of flash follows the faked end; the
SoftDevice at the start stays."""
"""The bootloader pinned to the end of flash follows the faked end; the MBR page
and the SoftDevice at the start stay."""
sections = nrf52._testing_mode_sections(
BOOTLOADER_CONFIG[BOOTLOADER_ADAFRUIT_NRF52_SD140_V7]
)
assert [(s.name, s.address, s.size) for s in sections] == [
("SoftDevice", 0x0, 0x27000),
("nrf5_mbr", 0x0, 0x1000),
("SoftDevice", 0x1000, 0x26000),
(
"Adafruit_nRF52_Bootloader",
nrf52.TESTING_FLASH_SIZE - 0xC000,
0xC000,
),
]
assert sections[1].end_address == nrf52.TESTING_FLASH_SIZE
assert sections[2].end_address == nrf52.TESTING_FLASH_SIZE
def test_testing_mode_sections_leave_unpinned_sections() -> None:
+40 -7
View File
@@ -11,7 +11,11 @@ from esphome.components.nrf52 import (
set_core_data,
)
from esphome.components.nrf52.boards import BOOTLOADER_CONFIG
from esphome.components.nrf52.const import BOOTLOADER_NRF
from esphome.components.nrf52.const import (
BOOTLOADER_ADAFRUIT_NRF52_NOSD,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_NRF,
)
from esphome.components.zephyr.const import KEY_PM_STATIC, KEY_ZEPHYR
from esphome.components.zephyr_mcumgr.ota import _validate_bootloader
from esphome.config import path_context
@@ -26,10 +30,18 @@ def test_detect_bootloader_reports_a_missing_board() -> None:
_detect_bootloader({})
def test_dongle_defaults_to_nrf_bootloader() -> None:
"""The dongle's only bootloader is the factory one, so it is selected without a config line."""
config = _detect_bootloader({"board": "nrf52840dongle"})
assert config["bootloader"] == BOOTLOADER_NRF
@pytest.mark.parametrize(
("board", "bootloader"),
[
# The dongle's only bootloader is the factory one
("nrf52840dongle", BOOTLOADER_NRF),
# Makerdiary's UF2 bootloader is an Adafruit fork that ships without a SoftDevice
("nrf52840_mdk_usb_dongle", BOOTLOADER_ADAFRUIT_NRF52_NOSD),
],
)
def test_board_selects_its_factory_bootloader(board: str, bootloader: str) -> None:
"""The factory bootloader is selected without a config line."""
assert _detect_bootloader({"board": board})["bootloader"] == bootloader
def test_dongle_rejects_foreign_bootloader() -> None:
@@ -37,12 +49,14 @@ def test_dongle_rejects_foreign_bootloader() -> None:
_detect_bootloader({"board": "nrf52840dongle", "bootloader": "mcuboot"})
def _dongle_core_data() -> None:
def _dongle_core_data(
board: str = "nrf52840dongle", bootloader: str = BOOTLOADER_NRF
) -> None:
CORE.data[KEY_CORE] = {}
CORE.testing_mode = (
False # the real flash, not the larger one faked for grouped tests
)
set_core_data({"board": "nrf52840dongle", "bootloader": BOOTLOADER_NRF})
set_core_data({"board": board, "bootloader": bootloader})
@pytest.fixture
@@ -64,6 +78,25 @@ def test_nrf_bootloader_reserves_factory_region() -> None:
assert registered["open_bootloader"].end_address == 0x100000
def test_softdevice_layout_names_the_mbr_page() -> None:
"""On a board with an nRF5 bootloader the partition manager adds an MBR page of its own
unless one is declared, which would push the app behind where the SoftDevice jumps to."""
_dongle_core_data("nrf52840_mdk_usb_dongle", BOOTLOADER_ADAFRUIT_NRF52_SD140_V6)
registered = {s.name: s for s in CORE.data[KEY_ZEPHYR][KEY_PM_STATIC]}
assert registered["nrf5_mbr"].address == 0x0
assert registered["nrf5_mbr"].end_address == registered["SoftDevice"].address
assert registered["SoftDevice"].end_address == 0x26000
def test_adafruit_nosd_reserves_only_the_bootloader() -> None:
"""Without a SoftDevice the app follows the MBR page, as with the Open DFU bootloader."""
_dongle_core_data("nrf52840_mdk_usb_dongle", BOOTLOADER_ADAFRUIT_NRF52_NOSD)
registered = {s.name: s for s in CORE.data[KEY_ZEPHYR][KEY_PM_STATIC]}
assert set(registered) == {"Adafruit_nRF52_Bootloader"}
assert registered["Adafruit_nRF52_Bootloader"].address == 0xF4000
assert registered["Adafruit_nRF52_Bootloader"].end_address == 0x100000
def test_nrf_bootloader_rejects_mcumgr_ota() -> None:
"""OTA with mcumgr places its slot against a SoftDevice, which this layout has none of."""
_dongle_core_data()