Teensy 4.x GP3 Fuses Always Read 0

Bliss

Member
In adding some security features to my project, I was reading the Code Security page, and read that SW_GP1 and GP3 fuses are recommended to store identity data. I also noticed that the FuseWrite code generated when putting a Lockable Teensy in Secure Mode writes the GP3 fuses with the flash memory chip's unique ID.

However, when I went to write to the fuses myself, they consistently read back 0s afterwards, despite being unlocked. Even reading the fuses from a Teensy 4.1-like board in secure mode would return 0s, showing that the attempt by FuseWrite to set them to the flash ID failed (note this is a custom PCB, and a chip from NXP, not PJRC).

My code is able to write and lock the SW_GP1 fuses fine, however when it comes to GP3, they seemingly cannot be written to. I assume I must be misinterpreting something, and that this is by design, but I'm not entirely sure what I've missed. I've skimmed through the datasheet's pages on the GP3, GP3_LOCK, SW_GP1, and SW_GP1_LOCK fuses, as well as looked through the NXP and PJRC forums, but can't seem to find an answer.

The reason I'm posting this here (and not on NXP's own forums) is that on a stock Teensy 4.1 and 4.0 (both non lockable), I was able to write and lock SW_GP1's fuses, but once again couldn't write GP3's.

Here is the code used for GP3 writing:
C++:
#include <Arduino.h>

const uint32_t to_write[4] = {
  0x12345678,
  0x87654321,
  0xABCDEF12,
  0x12ABCDEF
};

void setup() {
  Serial.begin(1000000);
  while (!Serial && micros() < 2000) {}
 
  if (HW_OCOTP_LOCK & 0x0C000000) {
      Serial.println("GP3 is locked");
  } else {
      if (HW_OCOTP_GP30 == 0 &&
          HW_OCOTP_GP31 == 0 &&
          HW_OCOTP_GP32 == 0 &&
          HW_OCOTP_GP33 == 0) {
            
          Serial.printf("Setting GP3 to: 0x%lX, 0x%lX, 0x%lX, 0x%lX\n", to_write[0], to_write[1], to_write[2], to_write[3]);
          IMXRTfuseWrite(&HW_OCOTP_GP30, to_write[0]);
          IMXRTfuseWrite(&HW_OCOTP_GP31, to_write[1]);
          IMXRTfuseWrite(&HW_OCOTP_GP32, to_write[2]);
          IMXRTfuseWrite(&HW_OCOTP_GP33, to_write[3]);
          Serial.println("Wrote GP3");
      } else {
          Serial.printf("GP3 already had value: 0x%lX, 0x%lX, 0x%lX, 0x%lX\n", HW_OCOTP_GP30, HW_OCOTP_GP31, HW_OCOTP_GP32, HW_OCOTP_GP33);
      }
  }

  IMXRTfuseReload();

  if (HW_OCOTP_GP30 == to_write[0] &&
      HW_OCOTP_GP31 == to_write[1] &&
      HW_OCOTP_GP32 == to_write[2] &&
      HW_OCOTP_GP33 == to_write[3]) {
      Serial.println("GP3 is correctly set");
  } else {
      Serial.printf("GP3 is incorrectly set to: 0x%lX, 0x%lX, 0x%lX, 0x%lX\n", HW_OCOTP_GP30, HW_OCOTP_GP31, HW_OCOTP_GP32, HW_OCOTP_GP33);
  }
}

void loop () {}
And the output from running this code on a Teensy 4.1/4.0 (non-lockable), or the custom board (rt1062 from NXP) in secure mode:
Code:
Setting GP3 to: 0x12345678, 0x87654321, 0xABCDEF12, 0x12ABCDEF
Wrote GP3
GP3 is incorrectly set to: 0x0, 0x0, 0x0, 0x0
 
It doesn't look like the IMXRTfuseWrite function is allowed to touch the GP3 fuses:
Code:
void IMXRTfuseWrite(volatile uint32_t *fuses, uint32_t value)
{
  (...)
  uint32_t addr = ((uint32_t)fuses - (uint32_t)&HW_OCOTP_LOCK) >> 4;
  if (addr > 0x2F) return; // illegal address
  (...)
}
This check only allows the function to write up to HW_OCOTP_MISC_CONF1. Possibly this is because GP3 was an extra addition for the 1062 (judging by the ifdefs in imxrt.h) and Teensy 4 initially started out based on the 1050.
 
It doesn't look like the IMXRTfuseWrite function is allowed to touch the GP3 fuses:
Code:
void IMXRTfuseWrite(volatile uint32_t *fuses, uint32_t value)
{
  (...)
  uint32_t addr = ((uint32_t)fuses - (uint32_t)&HW_OCOTP_LOCK) >> 4;
  if (addr > 0x2F) return; // illegal address
  (...)
}
This check only allows the function to write up to HW_OCOTP_MISC_CONF1. Possibly this is because GP3 was an extra addition for the 1062 (judging by the ifdefs in imxrt.h) and Teensy 4 initially started out based on the 1050.
Oh I guess I should've checked the function definition. Given that the following code is in the FuseWrite file, I assume that it was simply an oversight.
C++:
const uint32_t *id = read_flash_id();
if (id && HW_OCOTP_GP30 == 0 && HW_OCOTP_GP31 == 0
    && HW_OCOTP_GP32 == 0 && HW_OCOTP_GP33 == 0) {
      IMXRTfuseWrite(&HW_OCOTP_GP30, id[0]);
      IMXRTfuseWrite(&HW_OCOTP_GP31, id[1]);
      IMXRTfuseWrite(&HW_OCOTP_GP32, id[0]);
      IMXRTfuseWrite(&HW_OCOTP_GP33, id[1]);
}
 
Yikes, I hope the FuseWrite sketch doesn't lock GP3?
Fortunately it does not.

I wonder if the flash ID should even be written to fuses anyways, given it can be found at any time by simply asking the flash chip, and as such seems like kind of a waste of fuses (for anyone who doesn't realize it's being written when they lock their device). Especially because the Code Security page suggests that GP3 is "an unused portion of the fuse memory."
 
I designed a quick patch to the IMXRTfuseRead/Write functions, and ran it, only to see that GP31's lower 16 bits (when read as a uint32_t) were in use, and so were GP32's lower 8 bits.

These were in use on both the Teensys, and on the chips from NXP. Maybe this is normal, but it seems odd to me that the general purpose fuses come partially pre-burned.
 
I designed a quick patch to the IMXRTfuseRead/Write functions, and ran it, only to see that GP31's lower 16 bits (when read as a uint32_t) were in use, and so were GP32's lower 8 bits.

These were in use on both the Teensys, and on the chips from NXP. Maybe this is normal, but it seems odd to me that the general purpose fuses come partially pre-burned.
Hopefully @Paul can chime in.
 
Are you sure your patch accounted for the discontinuous memory mapping of the shadow registers...
I tested this code on a regular (unlockable) Teensy 4.1:
Code:
#include <imxrt.h>

static uint32_t fuseRead(volatile uint32_t *fuses)
{
  if (((uint32_t)fuses & 0x0F) != 0) return (uint32_t)-1; // illegal address
  uint32_t addr = ((uint32_t)fuses - (uint32_t)&HW_OCOTP_LOCK) >> 4;
  if (addr > 0x2F) {
    if (addr > 0x3F && addr < 0x50)
      addr -= 0x10;
    else
      return (uint32_t)-1; // illegal address
  }
  if (HW_OCOTP_CTRL & HW_OCOTP_CTRL_ERROR) {
    HW_OCOTP_CTRL_CLR = HW_OCOTP_CTRL_ERROR;
  }
  HW_OCOTP_CTRL = HW_OCOTP_CTRL_ADDR(addr);
  HW_OCOTP_READ_CTRL = HW_OCOTP_READ_CTRL_READ_FUSE;
  while (HW_OCOTP_CTRL & HW_OCOTP_CTRL_BUSY) ; // wait
  return HW_OCOTP_READ_FUSE_DATA;
}

void setup() {
  Serial.begin(0);
  while (!Serial);
  Serial.printf("GP30: %08X (Shadow: %08X)\n", fuseRead(&HW_OCOTP_GP30), HW_OCOTP_GP30);
  Serial.printf("GP31: %08X (Shadow: %08X)\n", fuseRead(&HW_OCOTP_GP31), HW_OCOTP_GP31);
  Serial.printf("GP32: %08X (Shadow: %08X)\n", fuseRead(&HW_OCOTP_GP32), HW_OCOTP_GP32);
  Serial.printf("GP33: %08X (Shadow: %08X)\n", fuseRead(&HW_OCOTP_GP33), HW_OCOTP_GP33);
}

void loop() {}
and the output showed the registers to be empty:
Code:
GP30: 00000000 (Shadow: 00000000)
GP31: 00000000 (Shadow: 00000000)
GP32: 00000000 (Shadow: 00000000)
GP33: 00000000 (Shadow: 00000000)
 
Are you sure your patch accounted for the discontinuous memory mapping of the shadow registers...
I tested this code on a regular (unlockable) Teensy 4.1:
Code:
#include <imxrt.h>

static uint32_t fuseRead(volatile uint32_t *fuses)
{
  if (((uint32_t)fuses & 0x0F) != 0) return (uint32_t)-1; // illegal address
  uint32_t addr = ((uint32_t)fuses - (uint32_t)&HW_OCOTP_LOCK) >> 4;
  if (addr > 0x2F) {
    if (addr > 0x3F && addr < 0x50)
      addr -= 0x10;
    else
      return (uint32_t)-1; // illegal address
  }
  if (HW_OCOTP_CTRL & HW_OCOTP_CTRL_ERROR) {
    HW_OCOTP_CTRL_CLR = HW_OCOTP_CTRL_ERROR;
  }
  HW_OCOTP_CTRL = HW_OCOTP_CTRL_ADDR(addr);
  HW_OCOTP_READ_CTRL = HW_OCOTP_READ_CTRL_READ_FUSE;
  while (HW_OCOTP_CTRL & HW_OCOTP_CTRL_BUSY) ; // wait
  return HW_OCOTP_READ_FUSE_DATA;
}

void setup() {
  Serial.begin(0);
  while (!Serial);
  Serial.printf("GP30: %08X (Shadow: %08X)\n", fuseRead(&HW_OCOTP_GP30), HW_OCOTP_GP30);
  Serial.printf("GP31: %08X (Shadow: %08X)\n", fuseRead(&HW_OCOTP_GP31), HW_OCOTP_GP31);
  Serial.printf("GP32: %08X (Shadow: %08X)\n", fuseRead(&HW_OCOTP_GP32), HW_OCOTP_GP32);
  Serial.printf("GP33: %08X (Shadow: %08X)\n", fuseRead(&HW_OCOTP_GP33), HW_OCOTP_GP33);
}

void loop() {}
and the output showed the registers to be empty:
Code:
GP30: 00000000 (Shadow: 00000000)
GP31: 00000000 (Shadow: 00000000)
GP32: 00000000 (Shadow: 00000000)
GP33: 00000000 (Shadow: 00000000)
My mistake, completely glossed over that in the processor manual. Should've realized seeing as how HW_OCOTP_CTRL only supports up to address 0x3F. Thanks for the information.
 
Back
Top