Repository: zephyrproject-rtos/zephyr
Pull request #88551: scripts: runners: nrf: Extend the erase options
Author: login: carlescufi
Labels: name: area: Samples
description: Samples
name: area: West
description: West utility
name: Release Notes
description: To be mentioned in the release notes
Created: 2025-04-12T16:42:08Z | Merged: 2025-04-16T06:07:44Z

See commit messages.


Diff:
diff --git a/doc/releases/migration-guide-4.2.rst b/doc/releases/migration-guide-4.2.rst
index f347d41544ffc..c156eac13b548 100644
--- a/doc/releases/migration-guide-4.2.rst
+++ b/doc/releases/migration-guide-4.2.rst
@@ -41,8 +41,8 @@ Boards
 * All boards based on a Nordic IC of the nRF54L series now default to not
   erasing any part of the internal storage when flashing. If you'd like to
   revert to the previous default of erasing the pages that will be written to by
-  the firmware to be flashed you can use the new ``--erase-pages`` command-line
-  switch when invoking ``west flash``.
+  the firmware to be flashed you can set the new ``--erase-mode`` command-line
+  switch when invoking ``west flash`` to ``ranges``.
   Note that RRAM on nRF54L devices is not physically paged, and paging is
   only artificially provided, with a page size of 4096 bytes, for an easier
   transition of nRF52 software to nRF54L devices.
diff --git a/scripts/west_commands/runners/nrf_common.py b/scripts/west_commands/runners/nrf_common.py
index ff21790692698..d18029c6b2e68 100644
--- a/scripts/west_commands/runners/nrf_common.py
+++ b/scripts/west_commands/runners/nrf_common.py
@@ -79,8 +79,8 @@ class NrfBinaryRunner(ZephyrBinaryRunner):
     '''Runner front-end base class for nrf tools.'''
 
     def __init__(self, cfg, family, softreset, pinreset, dev_id, erase=False,
-                 erase_pages=False, reset=True, tool_opt=None, force=False,
-                 recover=False):
+                 erase_mode=None, ext_erase_mode=None, reset=True,
+                 tool_opt=None, force=False, recover=False):
         super().__init__(cfg)
         self.hex_ = cfg.hex_file
         # The old --nrf-family options takes upper-case family names
@@ -89,7 +89,8 @@ def __init__(self, cfg, family, softreset, pinreset, dev_id, erase=False,
         self.pinreset = pinreset
         self.dev_id = dev_id
         self.erase = bool(erase)
-        self.erase_pages = bool(erase_pages)
+        self.erase_mode = erase_mode
+        self.ext_erase_mode = ext_erase_mode
         self.reset = bool(reset)
         self.force = force
         self.recover = bool(recover)
@@ -139,9 +140,14 @@ def do_add_parser(cls, parser):
                             help='''erase all user available non-volatile
                             memory and disable read back protection before
                             flashing (erases flash for both cores on nRF53)''')
-        parser.add_argument('--erase-pages', required=False,
-                            action='store_true', dest='erase_pages',
-                            help='erase pages to be used by the firmware')
+        parser.add_argument('--erase-mode', required=False,
+                            choices=['none', 'ranges', 'all'],
+                            help='Select the type of erase operation for the '
+                                 'internal non-volatile memory')
+        parser.add_argument('--ext-erase-mode', required=False,
+                            choices=['none', 'ranges', 'all'],
+                            help='Select the type of erase operation for the '
+                                 'external non-volatile memory')
 
         parser.set_defaults(reset=True)
 
@@ -341,6 +347,18 @@ def _get_core(self):
 
         return None
 
+    def _get_erase_mode(self, mode):
+        if not mode:
+            return None
+        elif mode == "none":
+            return "ERASE_NONE"
+        elif mode == "ranges":
+            return "ERASE_RANGES_TOUCHED_BY_FIRMWARE"
+        elif mode == "all":
+            return "ERASE_ALL"
+        else:
+            raise RuntimeError(f"Invalid erase mode: {mode}")
+
     def program_hex(self):
         # Get the command use to actually program self.hex_.
         self.logger.info(f'Flashing file: {self.hex_}')
@@ -421,13 +439,11 @@ def program_hex(self):
         else:
             if self.erase:
                 erase_arg = 'ERASE_ALL'
-            elif self.family == 'nrf54l' and not self.erase_pages:
-                erase_arg = 'ERASE_NONE'
+            elif self.family == 'nrf54l':
+                erase_arg = self._get_erase_mode(self.erase_mode) or 'ERASE_NONE'
             else:
                 erase_arg = 'ERASE_RANGES_TOUCHED_BY_FIRMWARE'
 
-        self.logger.debug(f'Erase type: {erase_arg}')
-
         xip_ranges = {
             'nrf52': (0x12000000, 0x19FFFFFF),
             'nrf53': (0x10000000, 0x1FFFFFFF),
@@ -437,8 +453,16 @@ def program_hex(self):
             xip_start, xip_end = xip_ranges[self.family]
             if self.hex_refers_region(xip_start, xip_end):
                 # Default to pages for the external memory
-                ext_mem_erase_opt = erase_arg if erase_arg == 'ERASE_ALL' else \
-                                    'ERASE_RANGES_TOUCHED_BY_FIRMWARE'
+                ext_mem_erase_opt = self._get_erase_mode(self.ext_erase_mode) or \
+                        (erase_arg if erase_arg == 'ERASE_ALL' else \
+                                    'ERASE_RANGES_TOUCHED_BY_FIRMWARE')
+
+        if not ext_mem_erase_opt and self.ext_erase_mode:
+            self.logger.warning('Option --ext-erase-mode ignored, no parts of the '
+                                'image refer to external memory')
+
+        self.logger.debug(f'Erase modes: chip:{erase_arg} ext_mem:'
+                          f'{ext_mem_erase_opt}')
 
         self.op_program(self.hex_, erase_arg, ext_mem_erase_opt, defer=True, core=core)
         self.flush(force=False)
@@ -531,14 +555,18 @@ def do_run(self, command, **kwargs):
             raise RuntimeError('Options --softreset and --pinreset are mutually '
                                'exclusive.')
 
-        if self.erase and self.erase_pages:
-            raise RuntimeError('Options --erase and --erase-pages are mutually '
+        if self.erase and self.erase_mode:
+            raise RuntimeError('Options --erase and --erase-mode are mutually '
+                               'exclusive.')
+
+        if self.erase and self.ext_erase_mode:
+            raise RuntimeError('Options --erase and --ext-erase-mode are mutually '
                                'exclusive.')
 
         self.ensure_family()
 
-        if self.family != 'nrf54l' and self.erase_pages:
-            raise RuntimeError('Option --erase-pages can only be used with the '
+        if self.family != 'nrf54l' and self.erase_mode:
+            raise RuntimeError('Option --erase-mode can only be used with the '
                                'nRF54L family.')
 
         self.ensure_output('hex')
diff --git a/scripts/west_commands/runners/nrfjprog.py b/scripts/west_commands/runners/nrfjprog.py
index 506ce24e3b6c6..fd0a47e5dc40b 100644
--- a/scripts/west_commands/runners/nrfjprog.py
+++ b/scripts/west_commands/runners/nrfjprog.py
@@ -18,11 +18,12 @@ class NrfJprogBinaryRunner(NrfBinaryRunner):
     '''Runner front-end for nrfjprog.'''
 
     def __init__(self, cfg, family, softreset, pinreset, dev_id, erase=False,
-                 erase_pages=False, reset=True, tool_opt=None, force=False,
-                 recover=False, qspi_ini=None):
+                 erase_mode=None, ext_erase_mode=None, reset=True, tool_opt=None,
+                 force=False, recover=False, qspi_ini=None):
 
         super().__init__(cfg, family, softreset, pinreset, dev_id, erase,
-                         erase_pages, reset, tool_opt, force, recover)
+                         erase_mode, ext_erase_mode, reset, tool_opt, force,
+                         recover)
 
         self.qspi_ini = qspi_ini
 
@@ -46,9 +47,11 @@ def tool_opt_help(cls) -> str:
     def do_create(cls, cfg, args):
         return NrfJprogBinaryRunner(cfg, args.nrf_family, args.softreset,
                                     args.pinreset, args.dev_id, erase=args.erase,
-                                    erase_pages=args.erase_pages, reset=args.reset,
-                                    tool_opt=args.tool_opt, force=args.force,
-                                    recover=args.recover, qspi_ini=args.qspi_ini)
+                                    erase_mode=args.erase_mode,
+                                    ext_erase_mode=args.ext_erase_mode,
+                                    reset=args.reset, tool_opt=args.tool_opt,
+                                    force=args.force, recover=args.recover,
+                                    qspi_ini=args.qspi_ini)
     @classmethod
     def do_add_parser(cls, parser):
         super().do_add_parser(parser)
@@ -93,7 +96,7 @@ def do_exec_op(self, op, force=False):
                     cmd.append('--sectoranduicrerase')
                 else:
                     cmd.append('--sectorerase')
-            elif erase == 'NO_ERASE':
+            elif erase == 'ERASE_NONE':
                 pass
             else:
                 raise RuntimeError(f'Invalid erase mode: {erase}')
diff --git a/scripts/west_commands/runners/nrfutil.py b/scripts/west_commands/runners/nrfutil.py
index 583b58bc15ce2..e7efda9ac5aa3 100644
--- a/scripts/west_commands/runners/nrfutil.py
+++ b/scripts/west_commands/runners/nrfutil.py
@@ -17,11 +17,13 @@ class NrfUtilBinaryRunner(NrfBinaryRunner):
     '''Runner front-end for nrfutil.'''
 
     def __init__(self, cfg, family, softreset, pinreset, dev_id, erase=False,
-                 erase_pages=False, reset=True, tool_opt=None, force=False,
-                 recover=False, suit_starter=False, ext_mem_config_file=None):
+                 erase_mode=None, ext_erase_mode=None, reset=True, tool_opt=None,
+                 force=False, recover=False, suit_starter=False,
+                 ext_mem_config_file=None):
 
         super().__init__(cfg, family, softreset, pinreset, dev_id, erase,
-                         erase_pages, reset, tool_opt, force, recover)
+                         erase_mode, ext_erase_mode, reset, tool_opt, force,
+                         recover)
 
         self.suit_starter = suit_starter
         self.ext_mem_config_file = ext_mem_config_file
@@ -50,9 +52,10 @@ def tool_opt_help(cls) -> str:
     def do_create(cls, cfg, args):
         return NrfUtilBinaryRunner(cfg, args.nrf_family, args.softreset,
                                    args.pinreset, args.dev_id, erase=args.erase,
-                                   erase_pages=args.erase_pages, reset=args.reset,
-                                   tool_opt=args.tool_opt, force=args.force,
-                                   recover=args.recover,
+                                   erase_mode=args.erase_mode,
+                                   ext_erase_mode=args.ext_erase_mode,
+                                   reset=args.reset, tool_opt=args.tool_opt,
+                                   force=args.force, recover=args.recover,
                                    suit_starter=args.suit_manifest_starter,
                                    ext_mem_config_file=args.ext_mem_config_file)
 


Reviews:
user: login: nordicjm
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T07:24:43Z
user: login: anangl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T07:38:25Z
user: login: carlescufi
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T09:46:16Z
user: login: anangl
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-14T09:58:03Z
user: login: fabiobaltieri
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-14T13:04:26Z
user: login: nordicjm
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T13:10:22Z

Review comments:
diff_hunk: @@ -41,8 +41,8 @@ Boards
 * All boards based on a Nordic IC of the nRF54L series now default to not
   erasing any part of the internal storage when flashing. If you'd like to
   revert to the previous default of erasing the pages that will be written to by
-  the firmware to be flashed you can use the new ``--erase-pages`` command-line
-  switch when invoking ``west flash``.
+  the firmware to be flashed you can set the new ``--erase-mode`` command-line
+  switch when invoking ``west flash`` to ``pages``.
path: doc/releases/migration-guide-4.2.rst
user: login: anangl
body: The erase mode should be `ranges`, not `pages`. Also the commit message mentions `pages` as the possible mode, which is incorrect.
created_at: 2025-04-14T07:37:44Z
author_association: MEMBER
diff_hunk: @@ -41,8 +41,8 @@ Boards
 * All boards based on a Nordic IC of the nRF54L series now default to not
   erasing any part of the internal storage when flashing. If you'd like to
   revert to the previous default of erasing the pages that will be written to by
-  the firmware to be flashed you can use the new ``--erase-pages`` command-line
-  switch when invoking ``west flash``.
+  the firmware to be flashed you can set the new ``--erase-mode`` command-line
+  switch when invoking ``west flash`` to ``pages``.
path: doc/releases/migration-guide-4.2.rst
user: login: carlescufi
body: Well spotted @anangl! Fixed.
created_at: 2025-04-14T09:46:15Z
author_association: MEMBER

PR comments:
user: login: pdgendt
created_at: 2025-04-13T10:34:14Z
body: The first commit touches a sample, it this intended? Also the commit has no body text.
author_association: CONTRIBUTOR
user: login: carlescufi
created_at: 2025-04-13T18:16:55Z
body: > The first commit touches a sample, it this intended? Also the commit has no body text.

Indeed, a rebase mistake. Thanks for noticing, fixed!
author_association: MEMBER