Skip to content

arch/arm/stm32h5: Add OTP support - #20144

Merged
xiaoxiang781216 merged 2 commits into
apache:masterfrom
liam-geotab:stm32h5-otp
Sep 18, 2026
Merged

xiaoxiang781216 merged 2 commits into
apache:masterfrom
liam-geotab:stm32h5-otp

Conversation

@liam-geotab

Copy link
Copy Markdown
Contributor

Summary

Add an API for OTP (one-time programmable) memory on stm32h5. There are OTP APIs for non-STM32 platforms. There is no other API for STM32 so far with these names.

Implement it in stm32h563xx_flash.c since the progmem abstraction also lives there.

int stm32_otp_write(const uint16_t *data, uint16_t len, uint32_t offset);
int stm32_otp_read(uint16_t *data, uint16_t len, uint32_t offset);

The API allows cross-block reads/writes that don't necessarily start/end at block boundaries.
The type of data is uint16_t * to express to the caller that the pointer should be 2-aligned. The natural size of OTP words is 16 bits. len is uint16_t for no strong reason. Preserve author's work.

uint32_t stm32_otp_getlockstatus(void);

Get a mask of blocks that are locked. A block being locked is considered as being one-time programmed. In future stm32 platform support, uint32_t may not be sufficient to represent all blocks. This platform has 32 blocks.

The user can be ignorant of the block sizes but they must be aware of the full size of the OTP area and there is no define for it in a public header. If the user writes half of a block, the unwritten half still gets locked, so the user cannot e.g. write the whole OTP area one word at a time, so they actually do need to be aware of the block size.

If these issues are unacceptable, changes should be requested by reviewers. I am favoring preserving the author's work by default.

Impact

If the new STM32 OTP API is bad, there will be a breaking change later (e.g. when OTP is added for other STM32 platforms) to improve it.

The other impact worth noting is that this permanently sets a device's OTP contents irreversibly, if that wasn't clear.

Testing

nucleo-h563zi:nsh with CONFIG_STM32_PROGMEM enabled.

diff --git a/boards/arm/stm32h5/nucleo-h563zi/src/stm32_bringup.c b/boards/arm/stm32h5/nucleo-h563zi/src/stm32_bringup.c
index 2cfb962226..57100f0975 100644
--- a/boards/arm/stm32h5/nucleo-h563zi/src/stm32_bringup.c
+++ b/boards/arm/stm32h5/nucleo-h563zi/src/stm32_bringup.c
@@ -42,10 +42,53 @@
 #  include "stm32_wdg.h"
 #endif
 
+#include "stm32_flash.h"
+
 /****************************************************************************
  * Pre-processor Definitions
  ****************************************************************************/
 
+#define BLOCK_SIZE   64
+#define BLOCK        5    /* block to write */
+
+static const uint8_t block_data[BLOCK_SIZE] = "\xff\xcd\xef\x33\xab\xcd\xef\x33\xab\xcd\xef\x33\xab\xcd\xef\x33\xab\xcd\xef\x33\xab\xcd\xef\x33\xab\xcd\xef\x33\xab\xcd\xef\x33\xab\xcd\xef\x33\xab\xcd\xef\x33\xab\xcd\xef\x33\xab\xcd\xef\x33\xab\xcd\xef\x33\xab\xcd\xef\x33\xab\xcd\xef\x33\xab\xcd\xef\x33";
+
+static uint16_t otp_block[BLOCK_SIZE / 2];
+
+static int dump_otp(void)
+{
+  int ret;
+  uint8_t *otp_block_u8 = (uint8_t *)otp_block;
+
+  uint32_t lock_bits = stm32_otp_getlockstatus();
+  printf("OTP lock bits: 0x%08"PRIx32"\n", lock_bits);
+
+  for (int i = 0; i < 32; i++)
+    {
+      printf("OTP block %2d (offset %4d):", i, i * BLOCK_SIZE);
+
+      if (((1 << i) & lock_bits) == 0)
+        {
+          printf(" not programmed\n");
+          continue;
+        }
+
+      ret = stm32_otp_read(otp_block, BLOCK_SIZE, i * BLOCK_SIZE);
+      if (ret < 0)
+        {
+          syslog(LOG_ERR, "ERROR: Failed to read OTP: %d\n", ret);
+          return ret;
+        }
+      for (int j = 0; j < BLOCK_SIZE; j++)
+        {
+          printf(" %02"PRIx8, otp_block_u8[j]);
+        }
+      printf("\n");
+    }
+
+  return OK;
+}
+
 /****************************************************************************
  * Public Functions
  ****************************************************************************/
@@ -177,6 +220,35 @@ int stm32_bringup(void)
     }
 #endif
 
+  ret = dump_otp();
+  if (ret < 0)
+    {
+      return ret;
+    }
+
+  if (((1 << BLOCK) & stm32_otp_getlockstatus()) == 0)
+    {
+      printf("writing block %d\n", BLOCK);
+
+      memcpy(otp_block, block_data, BLOCK_SIZE);
+      ret = stm32_otp_write(otp_block, BLOCK_SIZE, BLOCK * BLOCK_SIZE);
+      if (ret < 0)
+        {
+          syslog(LOG_ERR, "ERROR: Failed to write OTP: %d\n", ret);
+          return ret;
+        }
+
+      ret = dump_otp();
+      if (ret < 0)
+        {
+          return ret;
+        }
+    }
+  else
+    {
+      printf("block %d is already written\n", BLOCK);
+    }
+
   UNUSED(ret);
   return OK;
 }
ABCG
OTP lock bits: 0x00000001
OTP block  0 (offset    0): ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33
OTP block  1 (offset   64): not programmed
OTP block  2 (offset  128): not programmed
OTP block  3 (offset  192): not programmed
OTP block  4 (offset  256): not programmed
OTP block  5 (offset  320): not programmed
OTP block  6 (offset  384): not programmed
OTP block  7 (offset  448): not programmed
OTP block  8 (offset  512): not programmed
OTP block  9 (offset  576): not programmed
OTP block 10 (offset  640): not programmed
OTP block 11 (offset  704): not programmed
OTP block 12 (offset  768): not programmed
OTP block 13 (offset  832): not programmed
OTP block 14 (offset  896): not programmed
OTP block 15 (offset  960): not programmed
OTP block 16 (offset 1024): not programmed
OTP block 17 (offset 1088): not programmed
OTP block 18 (offset 1152): not programmed
OTP block 19 (offset 1216): not programmed
OTP block 20 (offset 1280): not programmed
OTP block 21 (offset 1344): not programmed
OTP block 22 (offset 1408): not programmed
OTP block 23 (offset 1472): not programmed
OTP block 24 (offset 1536): not programmed
OTP block 25 (offset 1600): not programmed
OTP block 26 (offset 1664): not programmed
OTP block 27 (offset 1728): not programmed
OTP block 28 (offset 1792): not programmed
OTP block 29 (offset 1856): not programmed
OTP block 30 (offset 1920): not programmed
OTP block 31 (offset 1984): not programmed
writing block 5
OTP lock bits: 0x00000021
OTP block  0 (offset    0): ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33
OTP block  1 (offset   64): not programmed
OTP block  2 (offset  128): not programmed
OTP block  3 (offset  192): not programmed
OTP block  4 (offset  256): not programmed
OTP block  5 (offset  320): ff cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33
OTP block  6 (offset  384): not programmed
OTP block  7 (offset  448): not programmed
OTP block  8 (offset  512): not programmed
OTP block  9 (offset  576): not programmed
OTP block 10 (offset  640): not programmed
OTP block 11 (offset  704): not programmed
OTP block 12 (offset  768): not programmed
OTP block 13 (offset  832): not programmed
OTP block 14 (offset  896): not programmed
OTP block 15 (offset  960): not programmed
OTP block 16 (offset 1024): not programmed
OTP block 17 (offset 1088): not programmed
OTP block 18 (offset 1152): not programmed
OTP block 19 (offset 1216): not programmed
OTP block 20 (offset 1280): not programmed
OTP block 21 (offset 1344): not programmed
OTP block 22 (offset 1408): not programmed
OTP block 23 (offset 1472): not programmed
OTP block 24 (offset 1536): not programmed
OTP block 25 (offset 1600): not programmed
OTP block 26 (offset 1664): not programmed
OTP block 27 (offset 1728): not programmed
OTP block 28 (offset 1792): not programmed
OTP block 29 (offset 1856): not programmed
OTP block 30 (offset 1920): not programmed
OTP block 31 (offset 1984): not programmed

NuttShell (NSH) NuttX-13.0.1-RC1
nsh>

(reset)

ABCG
OTP lock bits: 0x00000021
OTP block  0 (offset    0): ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33
OTP block  1 (offset   64): not programmed
OTP block  2 (offset  128): not programmed
OTP block  3 (offset  192): not programmed
OTP block  4 (offset  256): not programmed
OTP block  5 (offset  320): ff cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33 ab cd ef 33
OTP block  6 (offset  384): not programmed
OTP block  7 (offset  448): not programmed
OTP block  8 (offset  512): not programmed
OTP block  9 (offset  576): not programmed
OTP block 10 (offset  640): not programmed
OTP block 11 (offset  704): not programmed
OTP block 12 (offset  768): not programmed
OTP block 13 (offset  832): not programmed
OTP block 14 (offset  896): not programmed
OTP block 15 (offset  960): not programmed
OTP block 16 (offset 1024): not programmed
OTP block 17 (offset 1088): not programmed
OTP block 18 (offset 1152): not programmed
OTP block 19 (offset 1216): not programmed
OTP block 20 (offset 1280): not programmed
OTP block 21 (offset 1344): not programmed
OTP block 22 (offset 1408): not programmed
OTP block 23 (offset 1472): not programmed
OTP block 24 (offset 1536): not programmed
OTP block 25 (offset 1600): not programmed
OTP block 26 (offset 1664): not programmed
OTP block 27 (offset 1728): not programmed
OTP block 28 (offset 1792): not programmed
OTP block 29 (offset 1856): not programmed
OTP block 30 (offset 1920): not programmed
OTP block 31 (offset 1984): not programmed
block 5 is already written

NuttShell (NSH) NuttX-13.0.1-RC1
nsh>

@github-actions github-actions Bot added Arch: arm Issues related to ARM (32-bit) architecture Size: L The size of the change in this PR is large labels Sep 14, 2026
@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

@darrylring

darrylring commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Beat me to it. I have a similar branch which adds this and support for the efuse driver. It reads/writes word by word, though, not block by block.

b7e7046

Can we make it possible to read/write word-by-word without locking blocks?

I posted in the arch_arm Discord about the issue with flash double ECC errors on reading unwritten OTP memory. In my current board code I have:

// Disable NMI for flash double ECC faults (set ECCNMI_MASK_EN bit)
modifyreg32(STM32_SBS_ECCNMIR, 0, 1);

@liam-geotab

liam-geotab commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Hey @darrylring, The differences in flexibility seem to be in the spirit of preventing faults.

This PR:
pro: detect written blocks by checking the lock status (although this won't help if interrupted between write and lock).
con: write granularity (effective) is block sized
con: I'm told that reading part of a block that wasn't programmed causes a fault

Your branch:
pro: write granularity is word sized
pro: uses existing efuse driver interface
con: writing the same OTP address twice is not prevented and is UB, I'm told. Or crash?

Mykhailo may join the discussion. There may be a way to catch faults and return an error. Too bad developing this feature is literally expensive.

@mykhailosopiha

mykhailosopiha commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

The main idea behind locking the entire block after write was mostly dictated by this restriction in a reference manual: on page 289 (section 7.3.9) is it clearly stated that an attempt to overwrite already written word may lead to data errors. Thus, there is no other way to make sure we are not corrupting our data.

There is a thread in a forum already, proving that the concern is real: https://community.st.com/stm32-mcus-products-25/stm32h503-otp-bug-150915

Lock is an only proper way to prevent double write and possible data corruption.

So the problem effectively boils down to this:

  • how to make sure that driver does not corrupt memory if double write attempt happens?

This is not theoretical question: it has proven to be an issue already - usually OTP burning is only used during provision phase. The provisioning phase is not only "write to OTP" - often it is way more complex process, and in these cases - provisioning happens on factory-ready devices, before shipping those. If the provisioning fails at some phase - a manufacturer must monitor OTP write status explicitly: did that already happen? If yes - we should skip the OTP write phase (it may corrupt the data). That adds complexity on all levels, and instead of "Just relaunch provisioning" - the manufacturer want to make sure they are not trying to burn through already written memory twice by introducing complex error-handling machinery to prevent a fully manufacturead sample from bricking.

I understand you concern here: locking an entire block after writing a single word may be a resource waste, though taking into account the nature of this operation - it is not the highest price for making sure device is not bricked. IT becomes programmer's task to make less commits to the OTP memory, but that is pretty straightforward and native approach.

As an option - we may introduce a separate "blind" write-unsafe function that will not be checking the lock, just a blind "write word by given address", and does not lock. We may want to add a comment "use it if you know what you are doing".

Adding a "locked" check to reading function is not recommended: it may hit back once already provisioned (and non-locked) devices start using this api.

@acassis

acassis commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

@liam-geotab @darrylring @mykhailosopiha I think having it integrated on efuse has another advantage: it makes it possible to use OTP from userspace, that is not possible with this current implementation.

@mykhailosopiha the STM32H5 is ECC granule = 16 bits, you can do a test: try to write only at 0x08FFF000 and later 0x08FFF004 (you can write 0x08FFF000 and 0x08FFF002 too, you just can't write 0x08FFF000 and later 0x08FFF001). And there is another way to make sure we are not corrupting our data: before attempting to write, read the content if it is 0xFFFF you can write that position without damaging the OTP.

@liam-geotab we can merge this PR, but we need Documentation about this feature, otherwise it becomes "Another Hidden Feature of NuttX".

@acassis acassis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@liam-geotab please add some Documentation explaining how to use it, like you added on the Summary testing. It is important to be at STM32H5 documentation, because people will not find this PR easily in the future

@darrylring

Copy link
Copy Markdown
Contributor

The STM32H5 will allow you to re-write the same word in OTP but the second write will corrupt the data and ECC. Data corruption is bad, of course, but the ECC corruption (by default) triggers NMIs.

The ways to prevent this are:

  1. Don't do it.
  2. Don't do it (and use the MPU to ensure that the OTP is read-only)
  3. Lock the entire 64 byte block and prevent future writes completely.

I do like the idea of checking for 0xFFFF. But then you also hit ECC NMIs by default.

The block read/write is definitely the safest approach, but pretty much incompatible with being able to write individual words. It's incompatible with eFuse write, too. Though eFuse read could still be implemented?

We were originally intending to write individual words, and so can't really make use of this driver as is. I'm rethinking that, though.

@acassis

acassis commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

The STM32H5 will allow you to re-write the same word in OTP but the second write will corrupt the data and ECC. Data corruption is bad, of course, but the ECC corruption (by default) triggers NMIs.

The ways to prevent this are:

1. Don't do it.

2. Don't do it (and use the MPU to ensure that the OTP is read-only)

3. Lock the entire 64 byte block and prevent future writes completely.

I do like the idea of checking for 0xFFFF. But then you also hit ECC NMIs by default.

The block read/write is definitely the safest approach, but pretty much incompatible with being able to write individual words. It's incompatible with eFuse write, too. Though eFuse read could still be implemented?

We were originally intending to write individual words, and so can't really make use of this driver as is. I'm rethinking that, though.

@darrylring did you try writing adjacent blocks of 4 bytes (32 bits) ? I think it will not raise ECC errors

@mykhailosopiha

mykhailosopiha commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

@liam-geotab @darrylring @mykhailosopiha I think having it integrated on efuse has another advantage: it makes it possible to use OTP from userspace, that is not possible with this current implementation.

@mykhailosopiha the STM32H5 is ECC granule = 16 bits, you can do a test: try to write only at 0x08FFF000 and later 0x08FFF004 (you can write 0x08FFF000 and 0x08FFF002 too, you just can't write 0x08FFF000 and later 0x08FFF001). And there is another way to make sure we are not corrupting our data: before attempting to write, read the content if it is 0xFFFF you can write that position without damaging the OTP.

@liam-geotab we can merge this PR, but we need Documentation about this feature, otherwise it becomes "Another Hidden Feature of NuttX".

It is possible to write adjacent bytes, I did that when was implementing these changes - absolutely fine (If my memory is not playing jokes with me).

I guess providing access to user space per-word write is a valuable feature, and api like "write a word" is a good way to give user full control over the content of the OTP. Thou - pay the price of managing that access when doing write.
A proper irq orchestration is needed for this: we want to read every writable memory beforehand, distinguishing "normal read of legitimate data", "virgin read" and "read corrupted data" by checking ECCD (I imagine it is still may shoot us in the knee with some unexpected outcome that may report a virgin block even if there is a "lucky" data corruption that would result in ECCD pretending to report a virgin read), though this all is a pure theory right now an not likely to happen).
Also, to write more than 1 word user will need to call "write" function multiple times working with its overheads manually - validate that memory is not fragmented and the entire resulting N bytes are available for writing before actually writing that data. It has it's own implications: multi-read and then multi-write atomicity from user space must be guaranteed, which in non trivial task if we consider a programmed guarantee rather than a contract "please write good code don't write bad code".

TLDR: In my opinion both APIs should be there - they serve different goals. A driver-like API to read/write words should exist with a "how to not brick your memory" guide. I also believe that having a smart API that does "Write this data chunk to this OTP address and make sure data is not corrupted" should also be present - it resolves pretty complex multi-level error handling under the hood.

@darrylring

Copy link
Copy Markdown
Contributor

Thoughts on having separate configuration options for block read/write and word read/write? Both options have obvious caveats which should definitely be documented, but I think both are useful.

Add an API for OTP (one-time programmable) memory on stm32h5.
There are OTP APIs for non-STM32 platforms.
There is no other API for STM32 so far with these names.

Implement it in stm32h563xx_flash.c since the progmem abstraction
also lives there.

int stm32_otp_write(const uint16_t *data, uint16_t len, uint32_t offset);
int stm32_otp_read(uint16_t *data, uint16_t len, uint32_t offset);
The API allows cross-block reads/writes that don't necessarily
start/end at block boundaries.
The type of `data` is uint16_t * to express to the caller that the
pointer should be 2-aligned. The natural size of OTP words is 16 bits.
`len` is uint16_t for no strong reason. Preserve author's work.

uint32_t stm32_otp_getlockstatus(void);
Get a mask of blocks that are locked. A block being locked
is considered as being one-time programmed.

Co-authored-by: Mykhailo Sopiha <mykhailosopiha@geotab.com>
Signed-off-by: Liam Howatt <liamhowatt@geotab.com>
@liam-geotab

Copy link
Copy Markdown
Contributor Author

I have documented this OTP interface matter-of-factly.

@darrylring is there anything in this version that you really want to see changed? Can you foresee anything that forbids wrapping it in the efuse driver in a later PR? I would encourage adding it if you have not been disheartened. I'm on board with your idea for a configuration option that allows multi-write. The current API is opaque enough to support that. The efuse interface could depends on multi-write being enabled.

@liam-geotab
liam-geotab requested a review from acassis September 16, 2026 18:22
Comment thread Documentation/platforms/arm/stm32h5/index.rst Outdated
Describe the OTP API in stm32h5 platform documentation.

Signed-off-by: Liam Howatt <liamhowatt@geotab.com>
@mykhailosopiha

mykhailosopiha commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Thoughts on having separate configuration options for block read/write and word read/write? Both options have obvious caveats which should definitely be documented, but I think both are useful.

I agree - both are needed. I like what @liam-geotab suggested - the eFuse driver may have it's default per-word read/write, and it may include block read/write API if configured.

@darrylring

Copy link
Copy Markdown
Contributor

Agreed. I will rework my branch based on these changes.

@xiaoxiang781216
xiaoxiang781216 merged commit 2c7cf5a into apache:master Sep 18, 2026
35 of 39 checks passed
@darrylring

darrylring commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

There's probably a better place to continue discussing this, but just thinking that for word read/write (without block locking), we can do something like this:

  • read will do the read, check the flash ECC error register, and use a return code to indicate the error
  • write will first do a read, and only attempt a write if the read returns 0xFFFF and an ECC error, and then do a read and check the result

Read can return one of OK, ERR_READ_UNWRITTEN, or ERR_READ_BAD_ECC (or something along those lines).
Write can return one of OK, ERR_WRITE_ALREADY_WRITTEN, or ERR_WRITE_CORRUPTION (or something along those lines).

That should hopefully prevent data corruption in most use cases?

@acassis

acassis commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

There's probably a better place to continue discussing this, but just thinking that for word read/write (without block locking), we can do something like this:

* read will do the read, check the flash ECC error register, and use a return code to indicate the error

* write will first do a read, and only attempt a write if the read returns 0xFFFF _and_ an ECC error

Read can return one of OK, ERR_UNWRITTEN, or ERR_BAD_ECC Write can return one of OK, ERR_ALREADY_WRITTEN

That should hopefully prevent data corruption in most use cases?

Suggestion: please open an Issue as (Feature/Improvement) and mark the involved people.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: arm Issues related to ARM (32-bit) architecture Area: Documentation Improvements or additions to documentation Size: L The size of the change in this PR is large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants