diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index b0c375eb53d..53ed5bce023 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -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,6 +201,7 @@ 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, @@ -1149,12 +1151,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/. diff --git a/esphome/components/nrf52/boards.py b/esphome/components/nrf52/boards.py index 3f5d9d1b0e3..1af128bcc43 100644 --- a/esphome/components/nrf52/boards.py +++ b/esphome/components/nrf52/boards.py @@ -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 without a + # SoftDevice; Adafruit's own builds for the board carry one + "nrf52840_mdk_usb_dongle": { + KEY_BOOTLOADER: [ + BOOTLOADER_ADAFRUIT_NRF52_NOSD, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, + BOOTLOADER_ADAFRUIT_NRF52_SD132, + ] + }, } # Boards with a GPIO wired to their RESET pin: driving it low is a pin reset, which the factory @@ -72,4 +83,9 @@ BOOTLOADER_CONFIG = { BOOTLOADER_NRF: [ Section("open_bootloader", 0xE0000, 0x20000, "flash_primary"), ], + # Same MBR page and application start as above; the bootloader, its settings and the MBR + # parameters fill the last 48 KB + BOOTLOADER_ADAFRUIT_NRF52_NOSD: [ + Section("Adafruit_nRF52_Bootloader", 0xF4000, 0xC000, "flash_primary"), + ], } diff --git a/esphome/components/nrf52/const.py b/esphome/components/nrf52/const.py index 166e79bacc6..76d0826e4e6 100644 --- a/esphome/components/nrf52/const.py +++ b/esphome/components/nrf52/const.py @@ -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 = [ diff --git a/tests/components/nrf52/test.nrf52-mdk-usb-dongle.yaml b/tests/components/nrf52/test.nrf52-mdk-usb-dongle.yaml new file mode 100644 index 00000000000..40c3da68b0d --- /dev/null +++ b/tests/components/nrf52/test.nrf52-mdk-usb-dongle.yaml @@ -0,0 +1,3 @@ +nrf52: + dcdc: False + dfu: true # the 1200 bps touch reboots into the UF2 bootloader diff --git a/tests/test_build_components/build_components_base.nrf52-mdk-usb-dongle.yaml b/tests/test_build_components/build_components_base.nrf52-mdk-usb-dongle.yaml new file mode 100644 index 00000000000..c146273d581 --- /dev/null +++ b/tests/test_build_components/build_components_base.nrf52-mdk-usb-dongle.yaml @@ -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 diff --git a/tests/unit_tests/components/nrf52/test_run_compile_hex.py b/tests/unit_tests/components/nrf52/test_run_compile_hex.py index 4684f6189d0..98004c0cd54 100644 --- a/tests/unit_tests/components/nrf52/test_run_compile_hex.py +++ b/tests/unit_tests/components/nrf52/test_run_compile_hex.py @@ -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 @@ -138,3 +141,24 @@ def test_sd140_v7_dfu_accepts_all_s140_7_softdevices( 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"] + + +def test_softdevice_free_adafruit_layout_requires_no_softdevice( + build_dir: Path, run_cmd: Mock +) -> None: + """The Makerdiary MDK USB Dongle's bootloader takes an nRF52840 UF2 and a package that + does not ask for a SoftDevice.""" + with patch.object( + nrf52, + "zephyr_data", + return_value={ + KEY_BOARD: "board", + KEY_BOOTLOADER: BOOTLOADER_ADAFRUIT_NRF52_NOSD, + }, + ): + _build(build_dir, cv.Version(3, 4, 0), ["zephyr/zephyr/zephyr.hex"], run_cmd) + uf2 = next(c.args[0] for c in run_cmd.call_args_list if "-f" in c.args[0]) + assert uf2[uf2.index("-f") + 1] == "0xADA52840" + genpkg = next(c.args[0] for c in run_cmd.call_args_list if "genpkg" in c.args[0]) + assert genpkg[genpkg.index("--dev-type") + 1] == "0x0052" + assert genpkg[genpkg.index("--sd-req") + 1] == "0x00" diff --git a/tests/unit_tests/test_nrf52_config.py b/tests/unit_tests/test_nrf52_config.py index eb13a55e488..7c4c815c4a7 100644 --- a/tests/unit_tests/test_nrf52_config.py +++ b/tests/unit_tests/test_nrf52_config.py @@ -11,7 +11,10 @@ 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_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 @@ -37,12 +40,20 @@ def test_dongle_rejects_foreign_bootloader() -> None: _detect_bootloader({"board": "nrf52840dongle", "bootloader": "mcuboot"}) -def _dongle_core_data() -> None: +def test_mdk_usb_dongle_defaults_to_the_softdevice_free_adafruit_layout() -> None: + """Makerdiary's UF2 bootloader is an Adafruit fork that ships without a SoftDevice.""" + config = _detect_bootloader({"board": "nrf52840_mdk_usb_dongle"}) + assert config["bootloader"] == BOOTLOADER_ADAFRUIT_NRF52_NOSD + + +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 +75,16 @@ def test_nrf_bootloader_reserves_factory_region() -> None: assert registered["open_bootloader"].end_address == 0x100000 +def test_adafruit_nosd_reserves_only_the_bootloader() -> None: + """Without a SoftDevice the app follows the MBR page at 0x1000, as with the Open DFU + bootloader; the Adafruit bootloader keeps its usual place at the top of the flash.""" + _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()