Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ ignore_missing_imports = True
[mypy-pylink]
ignore_missing_imports = True

[mypy-pynrfjprog]
ignore_missing_imports = True

[mypy-pyocd.*]
ignore_missing_imports = True

Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ dependencies = [
"colorama",
"httpx",
"pylink-square",
"pynrfjprog",
"pyserial",
"python-dateutil",
"rich",
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ cryptography
colorama
httpx
pylink-square
pynrfjprog
pyserial
pyocd
python-dateutil
Expand Down
147 changes: 100 additions & 47 deletions src/infuse_iot/util/soc/nrf.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# Copyright (c) 2020 Teslabs Engineering S.L.
# Copyright (c) 2025 Embeint Inc
#
# SPDX-License-Identifier: Apache-2.0


from pynrfjprog import LowLevel, Parameters
import json
import subprocess
import sys

from infuse_iot.util.soc.soc import ProvisioningInterface


class NRFFamily:
FICR_ADDRESS: int
DEVICE_ID_OFFSET: int
CUSTOMER_OFFSET: int

Expand All @@ -18,73 +19,116 @@ def soc(device_info):
raise NotImplementedError


class nRF52840(NRFFamily):
class nRF52(NRFFamily):
FICR_ADDRESS = 0x10000000
DEVICE_ID_OFFSET = 0x60
CUSTOMER_OFFSET = 0x80

@staticmethod
def soc(device_info):
assert device_info[1] == Parameters.DeviceName.NRF52840
return "nRF52840"
version: str = device_info["jlink"]["deviceVersion"]
if version.startswith("NRF52840"):
return "nRF52840"
elif version.startswith("NRF52833"):
return "nRF52833"
else:
raise NotImplementedError(f"Unhandled device {version}")


class nRF5340(NRFFamily):
class nRF53(NRFFamily):
FICT_ADDRESS = 0x00FF0000
DEVICE_ID_OFFSET = 0x204
CUSTOMER_OFFSET = 0x100

@staticmethod
def soc(device_info):
assert device_info[1] == Parameters.DeviceName.NRF5340
return "nRF5340"
version: str = device_info["jlink"]["deviceVersion"]
if version.startswith("NRF5340"):
return "nRF5340"
else:
raise NotImplementedError(f"Unhandled device {version}")


class nRF54L(NRFFamily):
FICR_ADDRESS = 0x00FFC000
DEVICE_ID_OFFSET = 0x304
CUSTOMER_OFFSET = 0x500

@staticmethod
def soc(device_info):
version: str = device_info["jlink"]["deviceVersion"]
if version.startswith("NRF54L15"):
return "nRF54L15"
elif version.startswith("NRF54L10"):
return "nRF54L10"
elif version.startswith("NRF54L05"):
return "nRF54L05"
else:
raise NotImplementedError(f"Unhandled device {version}")


class nRF91(NRFFamily):
FICR_ADDRESS = 0x00FF0000
DEVICE_ID_OFFSET = 0x204
CUSTOMER_OFFSET = 0x108

@staticmethod
def soc(device_info):
if device_info[1] == Parameters.DeviceName.NRF9160:
version: str = device_info["jlink"]["deviceVersion"]
if version.startswith("NRF9160"):
return "nRF9160"
elif device_info[1] == Parameters.DeviceName.NRF9120:
# Use version to determine nRF9151 vs nRF9161
if device_info[0] == Parameters.DeviceVersion.NRF9120_xxAA_REV3:
return "nRF9151"
else:
raise NotImplementedError(f"Unknown device: {device_info[0]}")
elif version.startswith("NRF9161"):
return "nRF9161"
elif version.startswith("NRF9151"):
return "nRF9151"
else:
raise NotImplementedError(f"Unknown device {device_info[1]}")
raise NotImplementedError(f"Unhandled device {version}")


DEVICE_FAMILY_MAPPING: dict[str, type[NRFFamily]] = {
"NRF52": nRF52840,
"NRF53": nRF5340,
"NRF91": nRF91,
"NRF52_FAMILY": nRF52,
"NRF53_FAMILY": nRF53,
"NRF54L_FAMILY": nRF54L,
"NRF91_FAMILY": nRF91,
}


class Interface(ProvisioningInterface):
def __init__(self, snr: int | None):
self._api = LowLevel.API()
self._api.open()
if snr is None:
self._api.connect_to_emu_without_snr()
else:
self._api.connect_to_emu_with_snr(snr)
self._family = DEVICE_FAMILY_MAPPING[self._api.read_device_family()]
self._device_info = self._api.read_device_info()
self._soc_name = self._family.soc(self._device_info)
self.snr = snr
devices = self._exec(["device-info"])
if len(devices) == 0:
sys.exit()
devices_info = devices[0]["devices"]

if len(devices_info) > 1:
serials = ",".join([d["serialNumber"] for d in devices_info])
sys.exit(f"Multiple devices found without a SNR provided (Found: {serials})")
self.snr = devices_info[0]["serialNumber"]
self.device_info = devices_info[0]["deviceInfo"]
self.core_info = self._exec(["core-info"])
self.family = DEVICE_FAMILY_MAPPING[self.device_info["jlink"]["deviceFamily"]]
self.uicr_base = self.core_info[0]["devices"][0]["uicrAddress"]
self._soc_name = self.family.soc(self.device_info)

def _exec(self, args: list[str]):
jout_all = []
cmd_base = ["nrfutil", "--json", "--skip-overhead", "device"]
cmd = cmd_base + args
if self.snr is not None:
cmd += ["--serial-number", str(self.snr)]

with subprocess.Popen(cmd, stdout=subprocess.PIPE) as p:
assert p.stdout is not None
for line in iter(p.stdout.readline, b""):
# https://github.com/ndjson/ndjson-spec
jout = json.loads(line.decode(sys.getdefaultencoding()))
jout_all.append(jout)

return jout_all

def close(self):
self._api.pin_reset()
self._api.disconnect_from_emu()
self._api.close()

def _find_region(self, memory_type: Parameters.MemoryType):
for desc in self._api.read_memory_descriptors():
if desc.type == memory_type:
return desc
raise RuntimeError(f"Could not find memory region {memory_type}")
self._exec(["reset"])

@property
def soc_name(self) -> str:
Expand All @@ -95,20 +139,29 @@ def unique_device_id_len(self) -> int:
return 8

def unique_device_id(self) -> int:
ficr = self._find_region(Parameters.MemoryType.FICR)
dev_id_addr = ficr.start + self._family.DEVICE_ID_OFFSET
device_id_addr = self.family.FICR_ADDRESS + self.family.DEVICE_ID_OFFSET

dev_id_bytes = bytes(self._api.read(dev_id_addr, 8))
result = self._exec(["x-read", "--address", hex(device_id_addr), "--bytes", "8", "--direct"])
data_bytes = result[0]["devices"][0]["memoryData"][0]["values"]
dev_id_bytes = bytes(data_bytes)
return int.from_bytes(dev_id_bytes, "big")

def read_provisioned_data(self, num: int) -> bytes:
uicr = self._find_region(Parameters.MemoryType.UICR)
customer_addr = uicr.start + self._family.CUSTOMER_OFFSET
customer_addr = self.uicr_base + self.family.CUSTOMER_OFFSET

result = self._exec(["x-read", "--address", hex(customer_addr), "--bytes", str(num), "--direct"])
data_bytes = result[0]["devices"][0]["memoryData"][0]["values"]

return bytes(self._api.read(customer_addr, num))
return bytes(data_bytes)

def write_provisioning_data(self, data: bytes):
uicr = self._find_region(Parameters.MemoryType.UICR)
customer_addr = uicr.start + self._family.CUSTOMER_OFFSET
customer_addr = self.uicr_base + self.family.CUSTOMER_OFFSET

# x-write only operates on single words
for offset in range(0, len(data), 4):
chunk_bytes = data[offset : offset + 4]
if len(chunk_bytes) != 4:
chunk_bytes += b"\xff" * (4 - len(chunk_bytes))
data_word = int.from_bytes(chunk_bytes, byteorder="little")

self._api.write(customer_addr, data, True)
self._exec(["x-write", "--address", hex(customer_addr + offset), "--value", hex(data_word)])