OTP whitelabel options for RP2350 boards

Continuing the discussion from BusPirate 7 Features:

This topic primarily covers my review of how to use the RP2350 OTP area. It will document my attempts (and failures, and thus learnings) while going through the process. I’ve already got some neat hacks that are applicable, too.

I will keep things relatively short-looking, by lots of use of the details tag, so larger text blocks are collapsed by default.

1 Like
Why bother?

It’s a fair enough question. First, allow me to recall some history of the BusPirate5 saga.

Rev8 vs. Rev10

BP5 has early version (known generally now as “Rev8”), which had some different hardware … including an SD card instead of onboard flash. Later, this was changed in “Rev10” to have onboard flash, and some pin mappings were changed. This raised a problem … how to determine if a firmware was accidentally applied to the wrong board type?

To be fair, Ian found a way to detect this, and the detection occurs early enough in the boot process that hardware should not be damaged. But, it would still be preferable to have a way to identify the specific model.

Manufacturing Glitches

At various times, there were problems in production (or caused during shipping) which only appeared once the boards are in customer hands. Much time was spent tracking down which boards had the problem, with the hope of tracking back to see if there was a common cause (e.g., all made on the same date? same batch?) It would be nice to have human-readable information that can be used in an error report to help correlate these issues.

Recall-like information

Having manufacturing information is also good for the customer. One issue (E9) affected all boards manufactured before a given date. How can a user know when their board was made? Ian and friends did a great job looking things up for folks and answering individual questions … but having it accessible (even without any firmware loaded) would be even better.

More Polished User Experience

The UF2 bootloader is drab. Every device out there using the RP bootloader seems to show the same “RP2350” volume, and the device advertises itself as Raspberry Pi’s product … not a BusPirate product. The INFO_UF2.TXT indicates only that this is a “Raspberry Pi RP2350”, of model “RP2350” … not a BusPirate. Worse, the UF2 bootloader has a HTML site that loads Raspberry Pi’s website. That should go to BusPirate.com, obviously

Can it be better?

The loaded firmware is already full controlling what is exposed over USB. Only the bootrom’s (and thus UF2 partition and behavior) were locked to the above behavior.

Wouldn’t it be even better if the UF2 booloader showed information more appropriate to the actual BusPirate product? YES!

New to the RP2350, there are now settings in the OTP area specifically set aside to provide richer customization of the UF2 bootloader.

I’ll walk through the details on how later, and a full walkthrough of changing things to meet some desired changes as an example.

1 Like

Desired end-goals

For this example, at least at the time this post was authored, I am going to try to modify the bootrom to have the following changes (vs. the stock RP2350 bootloader behavior).

Vendor IDs and Product IDs should refer to the Bus Pirate board. So should the model information. The UF2 volume should have a Bus
Pirate based volume label. The UF2 volume’s text files should point to the buspirate site (for index.htm) and include both model and manufacturing data.


Changes from default bootrom?

  • USB_VID / USB_PID become 0x1209 / 0x7332
    • this is a value that Ian already has assigned to the BusPirate product (for the logic analyzer). Since the LA is now integrated, the PID is free to re-use here. Ian might obtain an additional VID/PID, of course…
    • USB_MANUFACTURER string of “Bus Pirate”
    • USB_PRODUCT string “Bus Pirate 6”
  • SCSI VID/PID become “Bus Pir8”/“Bus Pirate 6”
  • UF2 Volume should have label of “BP6_BOOT”
    • UF2 Volume is FAT16
    • Limit self to 8 uppercase chars
    • ? TODO: Test 11 characters ?
    • ? TODO: Test 4000 characters ? :slight_smile:
  • UF2 INDEX.HTM file should have:
<html>
  <head>
    <meta http-equiv="refresh" content="0;URL='https://buspirate.com'"/>
   </head>
   <body>Redirecting to <a href='https://buspirate.com'>buspirate.com</a></body>
</html>
  • UF2’s INFO_UF2.TXT should have info such as:
UF2 Bootloader v1.0
Model: Bus Pirate 6
Board-ID: Rev0      LineA               20250214T012759Z

That last line deserves an explanation…

  • The first ten characters are reserved to store a revision identifier. This would correlate to BP5’s Rev8 vs. Rev10. The new BP7 might store “Rev0” on the first prototypes.
  • The next twenty characters are manufacturing information. I just put “LineA” as an example. Other values are expected.
  • The last bit is the ISO8601 string for the date of manufacture, as accurate as the factory processes allow. Maybe it’s the time the OTP is programmed.
    Regardless, it’s just an example of three types of useful information that really should be exposed: Board hardware revision, manufacturing trace data, and datetime of manufacture.

Even better:
Program the serial number and a digital signature over the OTP settings & device serial number, and include that signature as an indicator of hardware sourced from DangerousThings / BusPirate.com / DirtyPCBs / Ian. Now the device can self-report with a higher degree of confidence a source of who made it. Missing or no signature just means no validation of the source … all functionality remains.


1 Like
Hoped-for Results from this exercise

When this investigation is complete, I hope to have a working solution for programming the BusPirate 6 with the example data. This solution should even be usable on devices that are already shipped to customers (datetime would obviously be estimates, under customer control). If digital signature applied, however, Ian could provide estimates that match his needs (e.g., even approximate month might be good enough).

The only variable portion is the board revision / manufacturing string, so it should be relatively straightforward to move to production.

Since this is testing the bootloader, a PICO board is sufficient for testing … no need to do early tests on expensive hardware.

Stretch goals:

  • Bus Pirate 7 solution
  • Bus Pirate 5XL solution
  • Automated insertion of current ISO datetime
1 Like
OTP addressing, details and derailments

Mental model:

Simplifying things, one can think of the OTP as being an an array of uint16_t. Reading is done by a row address, which mentally can map to the array index of the uint16_t array.

Addressing Details (if you want them)

OTP addressing is … a bit weird … because of the options it supports. Each row of OTP is 24 bits. Generally, each row stores two bytes of data, plus one byte for ECC and polarity reversal protection.

Raspberry Pi made a choice to allow reading the OTP in multiple modes via alias’d addresses:

  • Read 32 bits of ECC corrected data corresponding to two neighboring rows.
  • Read 32 bits RAW from a single row (most significant 8 bits zero)
  • As either of the above, but using the now infamous “guarded” reads

Since these return different quantities of data for a single row, the API is based on a Row address, rather than byte offsets (which would change depending on mode).

Relevant Layout Details

  • OTP has 8192 bytes total, row addresses 0x00x0FFF.
  • OTP is then logically grouped into 64 pages, each 0x40 Rows (128 ECC corrected bytes).
  • Each page can be locked (various options… ignored here)

Of those 64 pages, six are not usable to store the customization data:

  • Page 0 is locked during manufacturing (0x00x3F)
  • Pages 1…2 are boot config / boot key (0x400xBF)
  • Page 61 (0xF400xF7F) store OTP access keys
  • Pages 62…63 (0xF800xFFF) define OTP page locking

This leaves 48 pages that store any other data, including the customization we’re going to try to get to work:

  • First usable page is 3 (0xC00xFF)
  • Last usable page is 60 (0xF000xF3F)
2 Likes
Predefined OTP fields of interest

OTP Row 0x059 USB_BOOT_FLAGS

Bit 22 indicates the USB_WHITE_LABEL_ADDR field is valid
Each of bits 0…15 indicate a given field in that pointed to structure contains valid data. For example, bit 0 indicates if the USB Device Product ID field is valid.

OTP Row 0x05A USB_BOOT_FLAGS_R1

Redundant copy of 0x059.

OTP Row 0x05B USB_BOOT_FLAGS_R2

Redundant copy of 0x059.

OTP Row 0x05C USB_WHITE_LABEL_ADDR

Row index where the start of the “White Label” structure are stored.

(varies) USB_WHITE_LABEL

Conceptually, this can be thought of as the following C-struct:

typedef struct _STRING_REF {
    // least significant seven bits of first byte
    uint8_t Character_Count : 7;
    // Most significant bit of first byte
    uint8_t Is_Two_Byte_Unicode : 1;
    // Valid range: 0x10..0xFF
    uint8_t Row_Offset_From_USB_White_Label;
} STRING_REF;
_Static_assert(sizeof(STRING_REF) == 2, ""); // 16 bits

typedef struct _USB_WHITE_LABEL {
    uint16_t   USB_Device_VendorID;
    uint16_t   USB_Device_ProductID;
    uint16_t   USB_Device_Standard_Version; // aka BCD
    uint16_t   USB_Device_Language_ID; // 0x0409 is default
    STRING_REF USB_Device_Manufacturer; 
    STRING_REF USB_Device_Product;
    STRING_REF USB_Device_Serial_Number;
    uint16_t   USB_Device_Max_Power_mA;
    STRING_REF Volume_Label;
    STRING_REF SCSI_VendorID;
    STRING_REF SCSI_ProductID;
    STRING_REF SCSI_Version;
    STRING_REF Index_htm_Redirect_URL;
    STRING_REF Index_htm_Redirect_Name;
    STRING_REF Info_uf2_Model;
    STRING_REF Info_uf2_Board_ID;
} USB_WHITE_LABEL;

Major GOTCHA!

The string reference values stored in the USB_WHITE_LABEL structure can only point forward, and can point forward a maximum of 255 rows. Thus, the strings to be used must be stored after, but close to, the USB_WHITE_LABEL itself.

It is not clear yet in which order to store the individual characters in a given OTP row.

Hacks

Because the STRING_REF is a counted (not null-terminated) string, a string that is a sub-string of another string may be directly referenced, so long as the starting point in the string is an even offset (for ASCII). It’s always possible to do this for unicode strings.

e.g., If storing “Bus Pirate ABC” and “Bus Pirate”, both STRING_REF can point to the start of “Bus Pirate ABC”, the second will just indicate fewer characters are used.

I will use this later to reduce the number of rows needed to store the strings.

2 Likes
Starting to put it together...

What is being overridden

For this first example, let’s override the following data:

  • USB_VID = 0x1209
  • USD_PID = 0x7332
  • USB_MANUFACTURER = “Bus Pirate”
  • USB_PRODUCT = “Bus Pirate 6”
  • VOLUME_LABEL = “BP6_BOOT”
  • UF2_REDIRECT_URL = “https://buspirate.com/
  • UF2_REDIRECT_NAME = “buspirate.com
  • SCSI_VENDOR_ID = “Bus Pir8”
  • SCSI_PRODUCT_ID = “Bus Pirate 6”
  • UF2_INFO_MODEL = “Bus Pirate 6”
  • UF2_INFO_BOARD_ID = “Rev 0 LineA 20250214T012759Z”

Just the strings …

There are only a few strings that need to be written to the OTP.

String chars rows RowOffsets Hack?
Bus Pirate 6 0x0C 6 0x10..0x15 Re-use
Bus Pirate 0x0A 5 0x10..0x14
BP6_BOOT 0x08 4 0x16..0x19
https://buspirate.com/ 0x16 11 0x1A..0x24
buspirate.com 0x0D 7 0x1E..0x24 Re-use
Bus Pir8 8 4 0x25..0x28
Rev 0 ... 40 20 0x29..0x3c
Total Rows 0x10..0x3c

The second string re-uses the same OTP rows used for the first string, just fewer characters. For the fifth string, it’s just lucky that the https:// is an even number of characters, allowing to point a few OTP rows into the middle of that string for just the hostname.

Totally needless hacks, but hacks are good!

2 Likes
Potential OTP contents and Programming Procedure

NOTE: None of the data in this section should be presumed valid. It’s entirely untested, just the output after review of some documentation. Reality may differ.

One goal is to ensure everything fits in a single OTP Page (0x40 Row addresses). Keeping stuff aligned with OTP pages avoids contention with the lock bits and similar permissions. This also would mean that single page could be locked, to prevent any additional bits from being inadvertently set. Using the last usable page (0x60 … starting row of 0xF00) might also be preferable.

USB_WHITE_LABEL data

RowOffset uint16_t Type String Description
0x00 0x1209 Value USB_VID
0x01 0x7332 Value USB_PID
0x04 0x0A10 StrRef “Bus Pirate” USB Manufacturer
0x05 0x0C10 StrRef “Bus Pirate 6” USB Product
0x08 0x0816 StrRef “BP6_BOOT” UF2 volume label
0x09 0x0825 StrRef “Bus Pir8” SCSI VendorID
0x0a 0x0C10 StrRef “Bus Pirate 6” SCSI ProductID
0x0c 0x161A StrRef “https://…” URL redirect
0x0d 0x0D1E StrRef buspirate.com URL name
0x0e 0x0C10 StrRef “Bus Pirate 6” UF2 INFO.TXT model
0x0f 0x2829 StrRef “Rev0 …” UF2 INFO.TXT Board ID

Reference Strings

RowOffset uint16_t Type Chars Description
0x10 0x4275 String Bu “BusPirate” / “BusPirate 6”
0x11 0x7320 String s
0x12 0x5069 String Pi
0x13 0x7261 String ra
0x14 0x7465 String te
0x15 0x2036 String 6
0x16 0x4250 String BP “BP6_BOOT”
0x17 0x365F String 6_
0x18 0x424F String BO
0x19 0x4F54 String OT
0x1a 0x6874 String ht https://buspirate.com
0x1b 0x7470 String tp
0x1c 0x733A String s:
0x1d 0x2F2F String //
0x1e 0x6275 String bu buspirate.com
0x1f 0x7370 String sp
0x20 0x6972 String ir
0x21 0x6174 String at
0x22 0x652E String e.
0x23 0x636F String co
0x24 0x6D2F String m/
0x25 0x4275 String Bu “Bus Pir8”
0x26 0x7320 String s
0x27 0x5069 String Pi
0x28 0x7238 String r8
0x29 0x5265 String Re “Rev0 LineA 20250214T012759Z”
0x2a 0x7630 String v0 Up to 10 chars
0x2b 0x2020 String
0x2c 0x2020 String
0x2d 0x2020 String
0x2e 0x4C69 String Li
0x2f 0x6E65 String ne
0x30 0x4120 String A Up to 14 chars
0x31 0x2020 String
0x32 0x2020 String
0x33 0x2020 String
0x34 0x2020 String
0x35 0x3230 String 20 ISO8601 Year H
0x36 0x2335 String 25 ISO8601 Year L
0x37 0x3032 String 02 ISO8601 Month
0x38 0x3134 String 14 ISO8601 Day of Month
0x39 0x2020 String T0 ISO8601 Hour H
0x3a 0x4C69 String 12 ISO8601 Hour L / Min H
0x3b 0x6E65 String 75 ISO8601 Min L / Sec H
0x3c 0x4100 String 9Z ISO8601 Sec L / UTC

Row address offsets 0x3d, 0x3e, 0x3f are reserved for future use. Thus, the total size of the white label data is 0x40 rows (128 bytes).

Program Ordering during testing

  1. Program the USB_WHITE_LABEL data + reference strings @ starting row 0x100 (0x100 … 0x13C, inclusive)

  2. Program the USB_WHITE_LABEL_ADDR to point to 0x100

  3. Program the USB_BOOT_FLAGS to indicate which fields of the USB_WHITE_LABEL data should be considered valid.

    • 0b_0000_0000_0100_0000__1111_0111_0011_0011
      • bit 0: USB VID
      • bit 1: USB PID
      • bit 4: USB Manufacturer string
      • bit 5: USB Product string
      • bit 8: Volume Label string
      • bit 9: SCSI VID string
      • bit 10: SCSI PID string
      • bit 12: Redirect URL string
      • bit 13: Redirect name string
      • bit 14: INFO_UF2.TXT model string
      • bit 15: INFO_UF2.TXT Board ID string
      • bit 22: WHITE_LABEL_ADDR is valid
    • 0x0040F733
  4. Test with this configuration

  5. HACKHACK. If somethings not working, can still get three more tries on this same hardware. Re-write the USB_WHITE_LABEL starting at the following starting row addresses (once per attempt, in order), and then update USB_WHITE_LABEL_ADDR to add the bit needed for the new row address.

    • 0x300
    • 0x700
    • 0xF00
      EDIT: Oops … I forgot about the ECC. It might be possible to get more than one try, but doing so would require finding a first lower encoded address with few ECC bits set, and then a second address with more bits in both address and ECC (and none of the existing bits of the first address or ECC cleared). Glad I ordered a bunch of pico boards…
  6. After those four attempts, if it’s still not working, disable the chip via the RMA procedure / decommisioning procedure (sets a bit in last OTP page…). Try again on a new chip…

1 Like

At present, that’s as far as I’ve gotten.

When I build the BP6 board, I set it to download the PICO SDK (rather than installing it). However, it seems the picotool I end up with doesn’t have many of the OTP-related commands. I also have not found an official release point for just the picotool executable for Win64 (native). Sigh… I hope that doesn’t become the blocking factor. I may need to install a separate WSL2 distribution, just to let the SDK mess it up (but result in my having an updated picotool).

If anyone else wants to brick their devices, nothing here requires a BusPirate … a RP2350 Pico would work just as well. In fact, I have a stack of ten coming … for the explicit purpose of burning while I try some of these ideas. If you try some of this anyways, do share your results!

2 Likes

Great write up! I’m browsing the datasheet now…

Ah… Oh… Hum…

  1. OTP doesn’t mean it is only write-able once, it means it can be made read only by burning bits in the last two pages?
  2. If this is true, " If somethings not working, can still get three more tries on this same hardware" does this still apply?
  3. There is an example of reading and writing OTP (and corrupting it) in the SDK. Shall I make this into a global command so we can poke around at it without PICO tool?

picotool seems like the efficient/proper way to do large manufacturing programming of OTP especially with signed/serialized stuff. However, you probably know by now that I loathe PC crapware for exactly the reasons you’re experiencing.

I can add that example as a global command and we can work it into a otp read/write/protect (maybe) tool. It will work the same way in 15 years as it does now :slight_smile:

I’m going to be a little slow over the next few days (spring festival), but I’ll try to get a basic command pushed shortly.

3 Likes

When I’ve done stuff like this, OTP (when there’s room), I’ve stored an x509 cert.

You can encode your custom stuff in the extensions as bitstrings. The added bonus is it could be signed with Dangerous Prototype’s private key as a “genuine” product. Again, no loss of functionality if not.

There would have to be cert generated for each BP during manufacturing. It could be done as a step in test or initial firmware load.

Just a thought. There’s absolutely nothing wrong with what you’ve laid out and suggested; just an alternative. My most recent experiences with this sort of thing is using certs; but then again I was in a world where IEC 62443-3 and -4 was a requirement.

3 Likes

It was the “birth certificate” :smile: It guaranteed authenticity, had serial number and mfg date, hardware rev information, etc.

3 Likes

There is a new global command called otp that simply runs the PICO SDK demo for now. I have not tried to run it yet.

It is built on the dummy.c example command, and I left in the example boilerplate stuff at the very bottom for cribbing command line parsing and other common tasks.

3 Likes

Big oops!

So it really is one time programmable and I just blew out a couple rows :slight_smile:

Disabled the OTP command and deleted the builds with it until I (or someone) has time to properly safeguard the demo.

2 Likes

Yes, it’s definitely one-time programmable.

No, I would not add OTP programming as a base command; It’s quite complex to get right, set appropriate boundaries, etc. Let it remain a distinct tool. That said, simply dumping the current OTP values is fine. Maybe also soft-lock the OTP early in boot (write to registers that prevent OTP modifications)?

Digital signature – Let’s split discussion of the digital signature options to a new thread. It’s interesting, and there’s many, many questions there; would be a pity to lose that in my wall of text on white labeling.

2 Likes

How about a command that simply looks for and reads the OTP info? That could be part of the base command. Then build on that.

3 Likes

Yes, precisely. First, at boot time, soft-lock the OTP registers.

Then, read-only access to those registers is / should be allowed / safe. I can imagine a command that interprets the pre-defined fields, dumping them to JSON, and/or allowing dumping an arbitrary set of user-defined rows.

The arbitrary reads could even follow the basic BusPirate script format for reading.

1 Like

Can someone help me understand a couple items?

  1. Is bootsel mode different than the UF2 loader?
  2. Presuming it’s the same, has anyone compiled picotool themselves under WSL2, and is then able to use option to get information from a connected device?

Here’s what I try:


Build everything per readme.md

cmake -S . -B build_rp2040 -DPICO_SDK_FETCH_FROM_GIT=TRUE
cmake -S . -B build_rp2350 -DPICO_SDK_FETCH_FROM_GIT=TRUE -DBP_PICO_PLATFORM=rp2350
cmake --build ./build_rp2040 --parallel --target clean
cmake --build ./build_rp2350 --parallel --target clean
cmake --build ./build_rp2040 --parallel --target all
cmake --build ./build_rp2350 --parallel --target all

Then, use the picotool that was pulled / built:

~/bp5$ ./build_rp2350/_deps/picotool/picotool version
picotool v2.1.1-develop (Linux, GNU-13.2.0, Release)
~/bp5$ ./build_rp2350/_deps/picotool/picotool help otp
OTP:
    Commands related to the RP2350 OTP (One-Time-Programmable) Memory

SYNOPSIS:
    picotool otp list [-p] [-n] [-f] [-i <filename>] [<selector>..]

SUB COMMANDS:
    list   List matching known registers/fields

It’s reporting its version as v2.1.1-develop. However, according to the release notes, there were many picotool otp subcommands added in v2.0.0 … so where are they? :rage:

[[Edit: Grrr… found a note that says everything device-related is silently left out of the binary if package libusb-1.0-0-dev is not installed. That’s terrible UI … they should list the additional options, with an error indicating that the build excluded those options, and how to fix it. Trying again… and if it works, will update buspirate docs to indicate folks should also include libusb-1.0-0-dev .]]

[[Edit: Nope … installing libusb-1.0-0-dev did not result in picotool having the device-oriented options. Frustrating…]]

1 Like

I’m sorry, I have compiled picotool on WSL a few times, but I have never tried to connect it directly to a device. A single function (rom_func_otp_access) in the SDK reads/writes to otp and it is pretty easy to use. Might be easier than fighting the picotool, it sounds like otp isn’t enabled by default?

Updated otp command pushed, so at least we can explore the data.

otp -a <address> -r <number of ROWs to read> -e <highlight ecc bytes> -s <search for non-blank rows>

There seems to be a logic error in the number of rows dumped (and addresses) without the -e (raw mode). Don’t trust it. -e appears to be correct though.

The search function loops through every row and reports non-zero rows. I ran the demo so I have some rows used now, they show up.

However, I assumed there would be manufacturing data in the first couple rows and some lock rows set, but I can’t see them. Maybe they are read protected state?

2 Likes

Thanks, Ian! I used this as the basis for a slightly improved OTP dump option. In particular, I noted the following problem with the read-only memory-mapped OTP ranges:

  • The bootrom API appears to allow reading an odd row, and only that odd row.
  • Reads of an OTP row in raw form often showed bit errors.
  • Reads of an OTP row with ECC correction did not have a reliable error indicator (check value is 0xFFFFFFFFu … which is valid!), nor did it have a way to indicate which row of the two rows even had an error.

My learning from this:

When allocating OTP for own use, ensure to allocate OTP rows in pairs (always 32-bits at a time).

Warning:

The docs indicate a neat hack for allowing the OTP to have a single-bit manufacturing flaw per row. If the hardware detects that the OTP row has exactly one bit flipped (impossible for correctly ECC’d row), then when writing to that row, it might store the inverted value (so the already-flipped bit can be read as either zero or one).

Soft-locking the OTP

For safety, I think it have soft-lock’d all the OTP bits early in boot. Makes it much safer to code OTP functions when you don’t have to worry about setting the wrong bit and thus burning rows…

2 Likes