Skip to content

Commit 21a3bd3

Browse files
hakonfamnordicjm
authored andcommitted
scripts: add IRONside SE update command
Adds a new west command to install an IRONside SE update to an nRF54H20 device. Both the IRONside SE main firmware and recovery firmware can be updated. This command only works on nRF54H20 devices that already has a version of IRONside SE installed. The command is used by invoking: ``` west ncs-ironside-se-update ``` Ref: NCSDK-32168 Signed-off-by: Håkon Amundsen <[email protected]>
1 parent d179cc4 commit 21a3bd3

File tree

4 files changed

+165
-1
lines changed

4 files changed

+165
-1
lines changed

CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,7 @@
708708
/scripts/west_commands/create_board/ @gmarull
709709
/scripts/west_commands/sbom/ @nrfconnect/ncs-co-scripts
710710
/scripts/west_commands/thingy91x_dfu.py @nrfconnect/ncs-cia
711+
/scripts/west_commands/ncs_ironside_se_update.py @nrfconnect/ncs-aurora
711712
/scripts/west_commands/ncs_provision.py @nrfconnect/ncs-pluto
712713
/scripts/bootloader/ @nrfconnect/ncs-pluto
713714
/scripts/reglock.py @nrfconnect/ncs-pluto

doc/nrf/releases_and_maturity/releases/release-notes-changelog.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,8 @@ See the changelog for each library in the :doc:`nrfxlib documentation <nrfxlib:R
516516
Scripts
517517
=======
518518

519-
|no_changes_yet_note|
519+
* Added the :file:`ncs_ironside_se_update.py` script in the :file:`scripts/west_commands` folder.
520+
The script adds the west command ``west ncs-ironside-se-update`` for installing an IRONside SE update.
520521

521522
Integrations
522523
============

scripts/west-commands.yml

+5
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,8 @@ west-commands:
3939
- name: ncs-provision
4040
class: NcsProvision
4141
help: Provision utility
42+
- file: scripts/west_commands/ncs_ironside_se_update.py
43+
commands:
44+
- name: ncs-ironside-se-update
45+
class: NcsIRONsideSEUpdate
46+
help: IRONside SE update utility
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2025 Nordic Semiconductor ASA
3+
#
4+
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
5+
6+
import argparse
7+
import json
8+
import struct
9+
import subprocess
10+
import time
11+
from ctypes import c_char_p
12+
from pathlib import Path, PosixPath
13+
from tempfile import TemporaryDirectory
14+
from zipfile import ZipFile
15+
16+
from west.commands import WestCommand
17+
18+
IRONSIDE_SE_VERSION_ADDR = 0x2F88FD04
19+
IRONSIDE_SE_RECOVERY_VERSION_ADDR = 0x2F88FD14
20+
UPDATE_STATUS_ADDR = 0x2F88FD24
21+
22+
UPDATE_STATUS_MSG = {
23+
0xFFFFFFFF: "None",
24+
0xF0000001: "UnknownOperation",
25+
0xF0000002: "InvalidManifest",
26+
0xF0000003: "StaleFW",
27+
0xF0000005: "VerifyFailure",
28+
0xF0000006: "VerifyOK",
29+
0xF0000007: "UROTUpdateDisabled",
30+
0xF0000008: "UROTActivated",
31+
0xF0000009: "RecoveryActivated",
32+
0xF000000A: "RecoveryUpdateDisabled",
33+
0xF100000A: "AROTRecovery",
34+
}
35+
36+
37+
class NcsIRONsideSEUpdate(WestCommand):
38+
def __init__(self):
39+
super().__init__(
40+
name="ncs-ironside-se-update",
41+
help="NCS IRONside SE update",
42+
description="Update IRONside SE.",
43+
)
44+
45+
def do_add_parser(self, parser_adder):
46+
parser = parser_adder.add_parser(
47+
self.name, help=self.help, description=self.description
48+
)
49+
50+
parser.add_argument(
51+
"--zip",
52+
help="Path to IRONside SE release ZIP",
53+
type=argparse.FileType(mode="r"),
54+
)
55+
56+
parser.add_argument(
57+
"--recovery",
58+
help="Update IRONside SE recovery instead of IRONside SE itself",
59+
action="store_true",
60+
)
61+
62+
parser.add_argument(
63+
"--allow-erase", action="store_true", help="Allow erasing the device"
64+
)
65+
parser.add_argument("--serial", type=str, help="serial number", default=None)
66+
parser.add_argument(
67+
"--wait", help="Wait time (in seconds) between resets", default=2
68+
)
69+
70+
return parser
71+
72+
def _decode_version(self, version_bytes: bytes) -> str:
73+
# First word is semantic versioned 8 bit each
74+
seqnum, patch, minor, major = struct.unpack("bbbb", version_bytes[0:4])
75+
76+
extraversion = c_char_p(version_bytes[4:]).value.decode("utf-8")
77+
78+
return f"{major}.{minor}.{patch}-{extraversion}+{seqnum}"
79+
80+
def _decode_status(self, status_bytes: bytes) -> str:
81+
status = int.from_bytes(status_bytes, "little")
82+
try:
83+
return f"{hex(status)} - {UPDATE_STATUS_MSG[status]}"
84+
except KeyError:
85+
return f"{hex(status)} - (unknown)"
86+
87+
def do_run(self, args, unknown_args):
88+
def nrfutil_device(cmd: str) -> str:
89+
cmd = f"nrfutil device {cmd} --serial-number {args.serial}"
90+
self.dbg(cmd)
91+
result = subprocess.run(cmd, shell=True, text=True, capture_output=True)
92+
self.dbg(result.stdout)
93+
return result.stdout
94+
95+
def nrfutil_read(address: int, num_bytes: int) -> bytes:
96+
json_out = json.loads(
97+
nrfutil_device(
98+
f"x-read --direct --json --skip-overhead --address 0x{address:x} --bytes {num_bytes}"
99+
)
100+
)
101+
return bytes(json_out["devices"][0]["memoryData"][0]["values"])
102+
103+
def get_version() -> str:
104+
address = (
105+
IRONSIDE_SE_RECOVERY_VERSION_ADDR
106+
if args.recovery
107+
else IRONSIDE_SE_VERSION_ADDR
108+
)
109+
return self._decode_version(nrfutil_read(address, 16))
110+
111+
def get_status() -> str:
112+
return self._decode_status(nrfutil_read(UPDATE_STATUS_ADDR, 4))
113+
114+
def program(hex_file: PosixPath) -> None:
115+
nrfutil_device(
116+
f"program --options chip_erase_mode=ERASE_NONE --firmware {hex_file}"
117+
)
118+
119+
if not args.allow_erase:
120+
raise RuntimeError(
121+
"Unable to perform update without erasing the device, set '--allow-erase'"
122+
)
123+
with TemporaryDirectory() as tmpdir:
124+
with ZipFile(args.zip.name, "r") as zip_ref:
125+
zip_ref.extractall(tmpdir)
126+
update_app = Path(tmpdir, "update/update_application.hex")
127+
if args.recovery:
128+
update_hex = "ironside_se_recovery_update.hex"
129+
else:
130+
update_hex = "ironside_se_update.hex"
131+
132+
update_to_install = Path(tmpdir, "update", update_hex)
133+
if not update_to_install.exists():
134+
raise RuntimeError("Unable to locate update hex within zip file")
135+
136+
self.inf(
137+
f"Version before update: {get_version()}, status: {get_status()}"
138+
)
139+
140+
program(update_app)
141+
program(update_to_install)
142+
143+
self.dbg("Reset to execute update service")
144+
nrfutil_device(f"reset --reset-kind RESET_PIN")
145+
time.sleep(args.wait)
146+
147+
self.dbg("Reset to trigger update installation")
148+
nrfutil_device(f"reset --reset-kind RESET_PIN")
149+
self.dbg("Waiting for update to complete")
150+
time.sleep(args.wait)
151+
152+
self.inf(
153+
f"Version after update: {get_version()}, status: {get_status()}"
154+
)
155+
self.inf(
156+
"The update application is still programmed, please program proper image"
157+
)

0 commit comments

Comments
 (0)