ERASE_ALL protection disable nrf54

Hello,

I've been working on a feature lately that involves writing the OTP registers in UICR on nrf54L15; after writing the registers I also wrote the protected value (0x50FA50FA) to the ERASEPROTECT registers and this locked the OTP values in place.

Now I want to revert the development kit the previous state to perform some more testing and I saw that the erase protection can be disabled through the handshake mechanism where both the firmware and the debugger write the same key to the respective ERASEPROTECT.DISABLE register in the CTRL-AP.

I added the following piece of code to my application and reflashed it on the board:

NRF_CTRLAP_S->ERASEPROTECT.DISABLE = 0x12345678;
And tried the following sequence of commands on the JLink CLI:
SWDSelect
SWDWriteDP 1 0x50000000 // power up
SWDWriteDP 2 0x02000000 // select AP2 (CTRL-AP)
SWDWriteAP 4 0x12345678 // write erase prot. disable key (register index 4 in AP
I was expecting the ERASE_ALL procedure to be completed automatically as per the data sheet, but nothing happened to the UICR registers. I also tried to manually trigger the ERASE_ALL procedure by writing the debug-side registers through SWD, then reset the board and still no change.
 
There is obviously something I'm missing here, but I couldn't figure out what else needs to be done from the documentation. Could someone please confirm if my understanding of this feature is wrong and how should the code actually look like?
Thank you!
Vlad
Parents
  • Have you written to the ERASEPROTECT.LOCK register? Have you reset it after trying to this handshake? And how are you reflashing the application here if eraseprotect is enabled?

    Regards,

    Elfving

  • I never touched the LOCK register; it reads zero always. I guess the debugger is able to overwrite the binary without a prior successful ERASE_ALL - hence the unchanged UICR values. Do you have some scripts that work with an NRF54? It would be really useful to compare my stuff with a proven configuration. Thanks! 

  • Sorry about the wait, you can have a look at the script provided by my coworker here. Since it deals with the exact same issue there is some other resources there that might be interesting to you, like this that goes a bit further into checking whether eraseprotect is enabled. 

    If this doesn't get you further, though, how are you resetting the device? 

    Regards,

    Elfving

  • Hello,

    I did check the script in the other ticket, it can be used to successfully erase a chip that does not have erase protection enabled. For my scenario, I have a board that has erase protection enabled (UICR.ERASEPROTECT.PROTECT_0/1 both read 0x50FA50FA and CTRL_AP_ERASEPROTECT.STATUS on debugger side is 1, which means erase protection is enabled). At this point, I tried the following sequence:

    - first invoke a shell command to the firmware running on the board, which writes a key value to NRF_CTRLAP_S->ERASEPROTECT.DISABLE (checked the address with a debugger read command afterwards and the key seems to be successfully written)

    - then use the SWD commands I posted in the initial thread to write the same key to the debugger side register

    - at this point I was expecting the erase_all to happen automatically; nevertheless, I also tried to manually invoke the erase all by writing to the debugger registers, also tried to reset, nothing seems to work.

    After reset, all the OTP registers in UICR maintain the same values (so the erase_all does not happen). Am I misinterpreting the functionality of the ERASEPROTECT->DISABLE mechanism?

    Thank you,

    Vlad

  • Hi again, I am again deeply sorry about the wait. Things are moving a bit slowly during the summer.

    It seems that we do not have any go-to examples online for 54L15. Here is an example that is supposed to show how it is done on the 5340, though I understand that this doesn't necessarily help much. 

    I made a quick example here that seems to work on my side for a 54L15DK. I just based it on a hello-world sample.

    /*
     * Copyright (c) 2012-2014 Wind River Systems, Inc.
     *
     * SPDX-License-Identifier: Apache-2.0
     */
    
    #include <stdio.h>
    #include <zephyr/kernel.h>
    #include <nrfx.h>
    
    #define ERASEPROTECT_SECRET_KEY 0xDEADBEEFUL
    
    static void rramc_wait_ready(void)
    {
    	while ((NRF_RRAMC_S->READY & RRAMC_READY_READY_Msk) == 0) {
    		/* Wait until the RRAMC operation completes. */
    	}
    }
    
    /* Protection is active if EITHER redundant UICR word reads Protected. Using OR
     * (instead of AND) keeps the firmware from looping if only one of the two RRAM
     * words happened to land, while still matching the hardware, which enables
     * protection whenever the config is protected.
     */
    static bool eraseprotect_is_enabled(void)
    {
    	return (NRF_UICR_S->ERASEPROTECT[0].PROTECT0 ==
    		UICR_ERASEPROTECT_PROTECT0_PALL_Protected) ||
    	       (NRF_UICR_S->ERASEPROTECT[0].PROTECT1 ==
    		UICR_ERASEPROTECT_PROTECT1_PALL_Protected);
    }
    
    static void eraseprotect_enable_via_uicr(void)
    {
    	/* Enable RRAM writes so the UICR words can be programmed. */
    	NRF_RRAMC_S->CONFIG = (RRAMC_CONFIG_WEN_Enabled << RRAMC_CONFIG_WEN_Pos);
    
    	/* Program and commit each redundant word separately so both are
    	 * guaranteed to persist across the reset.
    	 */
    	NRF_UICR_S->ERASEPROTECT[0].PROTECT0 =
    		UICR_ERASEPROTECT_PROTECT0_PALL_Protected;
    	NRF_RRAMC_S->TASKS_COMMITWRITEBUF = 1;
    	rramc_wait_ready();
    
    	NRF_UICR_S->ERASEPROTECT[0].PROTECT1 =
    		UICR_ERASEPROTECT_PROTECT1_PALL_Protected;
    	NRF_RRAMC_S->TASKS_COMMITWRITEBUF = 1;
    	rramc_wait_ready();
    
    	/* Disable RRAM writes again. */
    	NRF_RRAMC_S->CONFIG = (RRAMC_CONFIG_WEN_Disabled << RRAMC_CONFIG_WEN_Pos);
    }
    
    int main(void)
    {
    	printf("Hello World! %s\n", CONFIG_BOARD_TARGET);
    
    	printf("UICR.ERASEPROTECT PROTECT0: 0x%08X  PROTECT1: 0x%08X\n",
    	       NRF_UICR_S->ERASEPROTECT[0].PROTECT0,
    	       NRF_UICR_S->ERASEPROTECT[0].PROTECT1);
    
    	if (!eraseprotect_is_enabled()) {
    		printf("Erase protection not set. Writing 0x50FA50FA to UICR "
    		       "ERASEPROTECT and resetting to activate it...\n");
    
    		eraseprotect_enable_via_uicr();
    
    		/* Verify the write landed before resetting, otherwise we would
    		 * reset-loop forever. Read back the just-written RRAM values.
    		 */
    		printf("UICR.ERASEPROTECT after write PROTECT0: 0x%08X  "
    		       "PROTECT1: 0x%08X\n",
    		       NRF_UICR_S->ERASEPROTECT[0].PROTECT0,
    		       NRF_UICR_S->ERASEPROTECT[0].PROTECT1);
    
    		if (!eraseprotect_is_enabled()) {
    			printf("ERROR: UICR ERASEPROTECT write did not take. "
    			       "Halting instead of reset-looping.\n");
    			while (1) {
    				k_msleep(1000);
    			}
    		}
    
    		printf("UICR ERASEPROTECT written. Rebooting so the protection "
    		       "takes effect.\n");
    
    		/* Give the UART time to flush before resetting. */
    		k_msleep(100);
    		NVIC_SystemReset();
    
    		/* Not reached. */
    		return 0;
    	}
    
    	printf("Erase protection is active.\n");
    
    	/* Write the CPU side of the handshake. The device will erase itself
    	 * automatically once the debugger writes the same key to its own
    	 * CTRL-AP ERASEPROTECT.DISABLE register.
    	 */
    	printf("Writing secret key 0x%08lX to CTRL-AP ERASEPROTECT.DISABLE "
    	       "(CPU side)...\n", ERASEPROTECT_SECRET_KEY);
    	NRF_CTRLAP_S->ERASEPROTECT.DISABLE = ERASEPROTECT_SECRET_KEY;
    
    	/* Diagnostic heartbeat: prove to the debugger, via the CTRL-AP mailbox,
    	 * that this branch executed past the key write and that CTRL-AP CPU-side
    	 * writes work at all. The debugger reads this back from the debug-side
    	 * MAILBOX.RXDATA register (offset 0x028).
    	 */
    	NRF_CTRLAP_S->MAILBOX.TXDATA = 0xC0DE0002UL;
    
    	printf("CPU-side key written. Waiting for the debugger to write the "
    	       "matching key to trigger ERASEALL.\n");
    
    	while (1) {
    		k_msleep(1000);
    	}
    
    	return 0;
    }

    #!/usr/bin/env python3
    """
    nRF54L15 CTRL-AP ERASEPROTECT.DISABLE handshake (debugger side).
    
    Requires: pip install pylink-square
    Hardware: J-Link probe wired to the nRF54L15 SWD pins.
    
    
    """
    
    import sys
    import time
    import pylink
    
    # --- CTRL-AP is AP index 2 on nRF54L15 ---
    CTRLAP_APSEL = 2
    
    # --- Register offsets within CTRL-AP (VERIFY against PS before trusting) ---
    REG_RESET               = 0x00
    REG_ERASEALL            = 0x04
    REG_ERASEALLSTATUS      = 0x08
    REG_ERASEPROTECTSTATUS  = 0x0C
    REG_ERASEPROTECT_DISABLE = 0x10
    REG_ERASEPROTECT_LOCK   = 0x14
    
    KEY = 0xDEADBEEF # Hanshake key must match firmware-side write to CTRLAP.ERASEPROTECT.DISABLE
    POLL_TIMEOUT_S = 5.0
    POLL_INTERVAL_S = 0.1
    
    
    def select_ctrlap(jlink: pylink.JLink):
        """Select CTRL-AP (AP2) in the DP SELECT register and power up the debug domain."""
        # ensures DP/AP plumbing is initialized by pylink
        jlink.coresight_configure()
        # DP register 1 = CTRL/STAT: request debug + system power-up
        jlink.coresight_write(1, 0x50000000, ap=False)
        # DP register 2 = SELECT: APSEL in bits [31:24], APBANKSEL in [7:4]
        jlink.coresight_write(2, (CTRLAP_APSEL << 24), ap=False)
    
    
    def ap_write(jlink: pylink.JLink, offset: int, value: int):
        reg_index = offset >> 2  # pylink's AP register index is word-addressed
        jlink.coresight_write(reg_index, value, ap=True)
    
    
    def ap_read(jlink: pylink.JLink, offset: int) -> int:
        reg_index = offset >> 2
        # First read after a bank/AP switch is commonly stale on SWD; read twice.
        jlink.coresight_read(reg_index, ap=True)
        return jlink.coresight_read(reg_index, ap=True)
    
    
    def main():
        jlink = pylink.JLink()
        print("Connecting to J-Link...")
        jlink.open()
        jlink.set_tif(pylink.enums.JLinkInterfaces.SWD)
        jlink.connect("Cortex-M33", speed=4000)  # nRF54L15 app core; adjust if targeting a different core
    
        select_ctrlap(jlink)
    
        status_before = ap_read(jlink, REG_ERASEPROTECTSTATUS)
        print(f"ERASEPROTECTSTATUS before: 0x{status_before:08X}")
    
        print(f"Writing key 0x{KEY:08X} to debugger-side ERASEPROTECT.DISABLE...")
        ap_write(jlink, REG_ERASEPROTECT_DISABLE, KEY)
    
        # Sanity check: some silicon/doc revisions report this as read-only from
        # the debugger's perspective even though the datasheet marks it R/W.
        readback = ap_read(jlink, REG_ERASEPROTECT_DISABLE)
        print(f"ERASEPROTECT.DISABLE readback: 0x{readback:08X}")
        if readback != KEY:
            print("WARNING: readback does not match written key. "
                  "This matches a known unresolved community report — "
                  "the handshake may not proceed. Continuing to poll anyway.")
    
        print("Polling ERASEALLSTATUS / ERASEPROTECTSTATUS for completion...")
        deadline = time.time() + POLL_TIMEOUT_S
        while time.time() < deadline:
            erase_status = ap_read(jlink, REG_ERASEALLSTATUS)
            protect_status = ap_read(jlink, REG_ERASEPROTECTSTATUS)
            print(f"  ERASEALLSTATUS=0x{erase_status:08X}  ERASEPROTECTSTATUS=0x{protect_status:08X}")
            if protect_status == 0 and erase_status == 0:
                print("Erase protection disabled and erase-all appears complete.")
                break
            time.sleep(POLL_INTERVAL_S)
        else:
            print("Timed out waiting for erase-all completion. "
                  "Handshake likely did not trigger — see warnings above.")
            jlink.close()
            sys.exit(1)
    
        jlink.close()
    
    
    if __name__ == "__main__":
        main()

    Regards,

    Elfving

Reply
  • Hi again, I am again deeply sorry about the wait. Things are moving a bit slowly during the summer.

    It seems that we do not have any go-to examples online for 54L15. Here is an example that is supposed to show how it is done on the 5340, though I understand that this doesn't necessarily help much. 

    I made a quick example here that seems to work on my side for a 54L15DK. I just based it on a hello-world sample.

    /*
     * Copyright (c) 2012-2014 Wind River Systems, Inc.
     *
     * SPDX-License-Identifier: Apache-2.0
     */
    
    #include <stdio.h>
    #include <zephyr/kernel.h>
    #include <nrfx.h>
    
    #define ERASEPROTECT_SECRET_KEY 0xDEADBEEFUL
    
    static void rramc_wait_ready(void)
    {
    	while ((NRF_RRAMC_S->READY & RRAMC_READY_READY_Msk) == 0) {
    		/* Wait until the RRAMC operation completes. */
    	}
    }
    
    /* Protection is active if EITHER redundant UICR word reads Protected. Using OR
     * (instead of AND) keeps the firmware from looping if only one of the two RRAM
     * words happened to land, while still matching the hardware, which enables
     * protection whenever the config is protected.
     */
    static bool eraseprotect_is_enabled(void)
    {
    	return (NRF_UICR_S->ERASEPROTECT[0].PROTECT0 ==
    		UICR_ERASEPROTECT_PROTECT0_PALL_Protected) ||
    	       (NRF_UICR_S->ERASEPROTECT[0].PROTECT1 ==
    		UICR_ERASEPROTECT_PROTECT1_PALL_Protected);
    }
    
    static void eraseprotect_enable_via_uicr(void)
    {
    	/* Enable RRAM writes so the UICR words can be programmed. */
    	NRF_RRAMC_S->CONFIG = (RRAMC_CONFIG_WEN_Enabled << RRAMC_CONFIG_WEN_Pos);
    
    	/* Program and commit each redundant word separately so both are
    	 * guaranteed to persist across the reset.
    	 */
    	NRF_UICR_S->ERASEPROTECT[0].PROTECT0 =
    		UICR_ERASEPROTECT_PROTECT0_PALL_Protected;
    	NRF_RRAMC_S->TASKS_COMMITWRITEBUF = 1;
    	rramc_wait_ready();
    
    	NRF_UICR_S->ERASEPROTECT[0].PROTECT1 =
    		UICR_ERASEPROTECT_PROTECT1_PALL_Protected;
    	NRF_RRAMC_S->TASKS_COMMITWRITEBUF = 1;
    	rramc_wait_ready();
    
    	/* Disable RRAM writes again. */
    	NRF_RRAMC_S->CONFIG = (RRAMC_CONFIG_WEN_Disabled << RRAMC_CONFIG_WEN_Pos);
    }
    
    int main(void)
    {
    	printf("Hello World! %s\n", CONFIG_BOARD_TARGET);
    
    	printf("UICR.ERASEPROTECT PROTECT0: 0x%08X  PROTECT1: 0x%08X\n",
    	       NRF_UICR_S->ERASEPROTECT[0].PROTECT0,
    	       NRF_UICR_S->ERASEPROTECT[0].PROTECT1);
    
    	if (!eraseprotect_is_enabled()) {
    		printf("Erase protection not set. Writing 0x50FA50FA to UICR "
    		       "ERASEPROTECT and resetting to activate it...\n");
    
    		eraseprotect_enable_via_uicr();
    
    		/* Verify the write landed before resetting, otherwise we would
    		 * reset-loop forever. Read back the just-written RRAM values.
    		 */
    		printf("UICR.ERASEPROTECT after write PROTECT0: 0x%08X  "
    		       "PROTECT1: 0x%08X\n",
    		       NRF_UICR_S->ERASEPROTECT[0].PROTECT0,
    		       NRF_UICR_S->ERASEPROTECT[0].PROTECT1);
    
    		if (!eraseprotect_is_enabled()) {
    			printf("ERROR: UICR ERASEPROTECT write did not take. "
    			       "Halting instead of reset-looping.\n");
    			while (1) {
    				k_msleep(1000);
    			}
    		}
    
    		printf("UICR ERASEPROTECT written. Rebooting so the protection "
    		       "takes effect.\n");
    
    		/* Give the UART time to flush before resetting. */
    		k_msleep(100);
    		NVIC_SystemReset();
    
    		/* Not reached. */
    		return 0;
    	}
    
    	printf("Erase protection is active.\n");
    
    	/* Write the CPU side of the handshake. The device will erase itself
    	 * automatically once the debugger writes the same key to its own
    	 * CTRL-AP ERASEPROTECT.DISABLE register.
    	 */
    	printf("Writing secret key 0x%08lX to CTRL-AP ERASEPROTECT.DISABLE "
    	       "(CPU side)...\n", ERASEPROTECT_SECRET_KEY);
    	NRF_CTRLAP_S->ERASEPROTECT.DISABLE = ERASEPROTECT_SECRET_KEY;
    
    	/* Diagnostic heartbeat: prove to the debugger, via the CTRL-AP mailbox,
    	 * that this branch executed past the key write and that CTRL-AP CPU-side
    	 * writes work at all. The debugger reads this back from the debug-side
    	 * MAILBOX.RXDATA register (offset 0x028).
    	 */
    	NRF_CTRLAP_S->MAILBOX.TXDATA = 0xC0DE0002UL;
    
    	printf("CPU-side key written. Waiting for the debugger to write the "
    	       "matching key to trigger ERASEALL.\n");
    
    	while (1) {
    		k_msleep(1000);
    	}
    
    	return 0;
    }

    #!/usr/bin/env python3
    """
    nRF54L15 CTRL-AP ERASEPROTECT.DISABLE handshake (debugger side).
    
    Requires: pip install pylink-square
    Hardware: J-Link probe wired to the nRF54L15 SWD pins.
    
    
    """
    
    import sys
    import time
    import pylink
    
    # --- CTRL-AP is AP index 2 on nRF54L15 ---
    CTRLAP_APSEL = 2
    
    # --- Register offsets within CTRL-AP (VERIFY against PS before trusting) ---
    REG_RESET               = 0x00
    REG_ERASEALL            = 0x04
    REG_ERASEALLSTATUS      = 0x08
    REG_ERASEPROTECTSTATUS  = 0x0C
    REG_ERASEPROTECT_DISABLE = 0x10
    REG_ERASEPROTECT_LOCK   = 0x14
    
    KEY = 0xDEADBEEF # Hanshake key must match firmware-side write to CTRLAP.ERASEPROTECT.DISABLE
    POLL_TIMEOUT_S = 5.0
    POLL_INTERVAL_S = 0.1
    
    
    def select_ctrlap(jlink: pylink.JLink):
        """Select CTRL-AP (AP2) in the DP SELECT register and power up the debug domain."""
        # ensures DP/AP plumbing is initialized by pylink
        jlink.coresight_configure()
        # DP register 1 = CTRL/STAT: request debug + system power-up
        jlink.coresight_write(1, 0x50000000, ap=False)
        # DP register 2 = SELECT: APSEL in bits [31:24], APBANKSEL in [7:4]
        jlink.coresight_write(2, (CTRLAP_APSEL << 24), ap=False)
    
    
    def ap_write(jlink: pylink.JLink, offset: int, value: int):
        reg_index = offset >> 2  # pylink's AP register index is word-addressed
        jlink.coresight_write(reg_index, value, ap=True)
    
    
    def ap_read(jlink: pylink.JLink, offset: int) -> int:
        reg_index = offset >> 2
        # First read after a bank/AP switch is commonly stale on SWD; read twice.
        jlink.coresight_read(reg_index, ap=True)
        return jlink.coresight_read(reg_index, ap=True)
    
    
    def main():
        jlink = pylink.JLink()
        print("Connecting to J-Link...")
        jlink.open()
        jlink.set_tif(pylink.enums.JLinkInterfaces.SWD)
        jlink.connect("Cortex-M33", speed=4000)  # nRF54L15 app core; adjust if targeting a different core
    
        select_ctrlap(jlink)
    
        status_before = ap_read(jlink, REG_ERASEPROTECTSTATUS)
        print(f"ERASEPROTECTSTATUS before: 0x{status_before:08X}")
    
        print(f"Writing key 0x{KEY:08X} to debugger-side ERASEPROTECT.DISABLE...")
        ap_write(jlink, REG_ERASEPROTECT_DISABLE, KEY)
    
        # Sanity check: some silicon/doc revisions report this as read-only from
        # the debugger's perspective even though the datasheet marks it R/W.
        readback = ap_read(jlink, REG_ERASEPROTECT_DISABLE)
        print(f"ERASEPROTECT.DISABLE readback: 0x{readback:08X}")
        if readback != KEY:
            print("WARNING: readback does not match written key. "
                  "This matches a known unresolved community report — "
                  "the handshake may not proceed. Continuing to poll anyway.")
    
        print("Polling ERASEALLSTATUS / ERASEPROTECTSTATUS for completion...")
        deadline = time.time() + POLL_TIMEOUT_S
        while time.time() < deadline:
            erase_status = ap_read(jlink, REG_ERASEALLSTATUS)
            protect_status = ap_read(jlink, REG_ERASEPROTECTSTATUS)
            print(f"  ERASEALLSTATUS=0x{erase_status:08X}  ERASEPROTECTSTATUS=0x{protect_status:08X}")
            if protect_status == 0 and erase_status == 0:
                print("Erase protection disabled and erase-all appears complete.")
                break
            time.sleep(POLL_INTERVAL_S)
        else:
            print("Timed out waiting for erase-all completion. "
                  "Handshake likely did not trigger — see warnings above.")
            jlink.close()
            sys.exit(1)
    
        jlink.close()
    
    
    if __name__ == "__main__":
        main()

    Regards,

    Elfving

Children
No Data
Related