Read/write mutex and firmware protection on Raspberry Pi Pico

  • Using read/write mutexes to synchronize concurrent accesses on Raspberry Pi Pico, avoiding race conditions.
  • Design of structures and flow control inspired by concurrency patterns and modern data types as in Go.
  • Firmware protection using OTP memory keys, UF2 file encryption, and read restrictions with picotool.
  • Control of updates and blocking of unauthorized UF2 to ensure that only verified code runs on the hardware.

Read/write mutex on Raspberry Pi Pico

If you are starting to work with Raspberry Pi Pico or Pico 2 and you want to maximize concurrencySooner or later, you'll encounter read and write mutexes. And, incidentally, you'll probably also be concerned about how to protect your firmware, prevent it from being extracted with tools like picotool, and control which binaries are running on your hardware. All of this might seem confusing at first, but with a solid foundation, it will become much clearer.

In this article we are going to unite two worlds that, in practice, go hand in hand: on the one hand, Concurrent synchronization using mutex and RWMutex (reading/writing) inspired by the typical concepts of languages ​​like Go, and on the other hand, the more "craft" part: How to secure your code on Raspberry Pi PicoThis involves encrypting UF2, using OTP in-memory keys, and disabling unauthorized access with picotool. The goal is for you to end up with a comprehensive and practical overview, without so much loose theory.

Basic concepts of concurrency and mutexes in the context of Raspberry Pi Pico

Concurrency and mutexes in microcontrollers

When we talk about concurrence in embedded systems like Raspberry Pi PicoWe're actually referring to several execution flows that share resources: global variables, peripherals, communication buffers, etc. Although the Pico doesn't have "threads" in the style of a PC, it does have multiple cores (in the RP2040) and mechanisms like interrupts and cooperative loops that can overlap if not properly managed.

Un mutex (mutual exclusion) It's a synchronization structure designed to ensure that only one task (or core, or routine) accesses a critical resource at a time. If two pieces of code attempt to write to the same section of memory without coordination, the dreaded race conditions will appear: unpredictable results, corrupted data, and intermittent failures that are difficult to debug.

In many modern languages, such as Go, the standard library includes sync.Mutex and sync.RWMutex Precisely to deal with this. Although you usually program in C/C++ or MicroPython on the Raspberry Pi Pico, the concepts are the same: you lock the resource before you touch it, you release it when you're finished, and you make sure that no one else enters while you're inside the critical section.

The RP2040, the heart of the Raspberry Pi Pico, offers hardware primitives that allow for the design of read/write type locksThis pattern allows multiple readers to access a resource simultaneously, but only one writer can modify it, and always exclusively. It is particularly useful when most accesses are read-only and there are very few write operations.

Read/write mutex: general idea and parallels with Go

RWMutex read/write mutex

In high-level environments like Go, a RWMutex (Read-Write Mutex) It's an evolution of the classic mutex. Instead of allowing only exclusive locking, it distinguishes between read and write access. The advantage is that multiple readers can log in simultaneously, as long as no writer is active, and the writer can only log in when no one is reading or writing.

The usual interface of an RWMutex is built with methods of type RLock, RUnlock, Lock and UnlockRLock locks in read mode; Lock, in write mode. On the Raspberry Pi Pico, although you don't literally have those function names, you can implement the same logic with reader count variables, an active writer flag, and, in C/C++, atomic primitives or critical exclusions around those counters.

This pattern is very useful in typical microcontroller scenarios: for example, a configuration table that is rarely modified but is constantly consulted from multiple interrupts or tasks. Using only one exclusive mutex would force reads to wait longer than necessary. With a well-designed read/write mutex, concurrent reads do not block each other.

It is worth keeping in mind the philosophy taught when working with parallelism in Go: It only syncs when needed.Avoid overly long critical sections and design your data structures with concurrent access in mind from the start, not as a later addition. This approach is directly applicable to development on the Raspberry Pi Pico.

Practical implementation of a read/write mutex on Raspberry Pi Pico

Implementing RWMutex on Raspberry Pi Pico

To implement a read/write mutex on Raspberry Pi PicoYou need to combine concepts of data structures, pointers, and concurrency. A typical structure might contain a reader counter, a writer indicator, and optionally, a queue or priority mechanism to prevent writers from becoming "starved" while there are continuous readers.

In terms of design, it's normal to have a structure (struct) with fields for the reader count and an internal mutex that protects those fields. Similar to the chapters on structured types and methods in Go, you can define functions that operate on that structure as "methods": for example, a function that receives a pointer to your homemade RWMutex and performs locking or unlocking as appropriate.

In C/C++ for Pico, you work in a very similar way to managing pointers and pass-by-reference As explained in general programming texts, you pass the address of the mutex structure to the locking and unlocking functions so they can modify its internal fields (reader counter, flags, etc.). Understanding the difference between a value and a reference is essential to avoid unintentionally duplicating the mutex's state.

You should also take into account the flow control and error handlingIf your blocking function can fail (for example, due to timeout or detection of an inconsistent state), you need to return an error code and handle it appropriately, very much in line with the error handling philosophy based on return values ​​and error types used in Go.

Finally, don't forget to test it thoroughly. With a similar approach to the automated tests In high-level languages, you can set up small tests that simulate multiple readers and writers, measure access times, and detect potential deadlocks or race conditions before deploying the code in your final product.

Advanced synchronization: race conditions, atomic and channels

When you get fully immersed in the crowd, the race and fine-tuning problems They appear immediately. On Raspberry Pi Pico, as in any concurrent environment, two uncoordinated accesses to shared memory can produce inconsistent results, especially if the access is not atomic or involves several operations (read, modify, write).

In the world of Go, there is a strong emphasis on detecting these situations with career analysis tools and using combinations of Mutex, RWMutex and atomic operations When what you want to protect is a simple counter or a flag, the RP2040 allows you to use atomic instructions and point-to-point interrupt disabling to ensure that a variable is updated safely.

Another interesting lesson from the goroutines and channels It's preferable, when it makes sense, to use message passing over memory sharing. Although Pico doesn't have channels exactly like Go's, you can mimic the concept with queues, circular buffers, and lightweight protocols between interrupts and the main loop, reducing the need for complex mutexes.

The key is to make the right decision: When to use a read/write mutex, when to use a simple mutex, and when to use atomic operations? For resource-intensive and complex data structures, an RWMutex is a good option. For simple variables that change infrequently, atomic synchronization is usually sufficient. And if you can isolate the logic into queues and messages, you can often save yourself a significant amount of explicit synchronization.

All of this fits with the general philosophy of the chapters dedicated to parallelism and concurrency: design with scalability and underlying hardware in mindespecially if you work with multi-core processors or microcontrollers with multiple execution units like the RP2040.

Firmware protection on Raspberry Pi Pico with picotool and OTP

Besides the "theoretical" aspect of concurrency, many real-world projects on Raspberry Pi Pico require protect the firmware and internal data against copying or reverse engineering. In particular, you may need to ensure several things: that your encrypted UF2 file only works on authorized devices, that tools like picotool cannot extract the binary or read sensitive information, and that the device only runs validated firmware.

The starting point is usually the use of OTP (One-Time Programmable) memory of the RP2040 and of RISC-V safe elementsIn this area, you can store cryptographic keys and configuration flags that, once written, cannot be modified (or only partially, depending on the design). Using picotool, you can write these OTP keys, which will later be used to validate and decrypt your firmware.

The typical workflow would go something like this: first, You configure the security keys and flags in OTP using picotoolThen, you generate your UF2 files already encrypted with those keys; finally, you drag and drop the UF2 into bootable USB mode as usual or automate the process with scripts that call picotool in batch.

One of the usual requirements is to prevent picotool or other utilities can read or extract the firmware Once the device is configured, certain bits are set in the OTP to disable flash memory reads or limit access to debugging information. This reduces the attack surface and makes it much harder for someone to copy your code.

You can also consider a two-stage flow: first load a “configuration” UF2 file that Write the final values ​​in OTP (keys, flags, etc.) and then burn the main, now protected, UF2 file. In any case, it's advisable to document and version these steps very well, because once certain OTP options are burned, there's no going back.

Prevent the execution of unauthorized UF2 and control updates

Another common requirement when protecting code on Raspberry Pi Pico is prevent unauthorized UF2 execution on the same hardware. You not only want to prevent your firmware from being "stolen," you also want to prevent anyone from uploading modified or malicious binaries that exploit your board.

The solution involves combining encrypted with AES and integrity verification. Your firmware is distributed as UF2 encrypted and signed with your keys. At startup, the bootloader (whether the original modified one or a custom one) checks the signature and decrypts the content only if the keys in the OTP match. If the verification fails, the code does not execute.

At the same time, you need to keep going being able to update the firmware without the user having to perform any technical maneuvers. The usual practice is to maintain the ability to enter bootable USB mode via software to flash new versions, provided they are also encrypted and signed. From the end user's perspective, it remains as simple as drag and drop, but internally the device rejects any file that does not meet the requirements.

In this scenario, automating the generation and uploading process is highly recommended. You can create scripts or batch files They should call picotool with the exact sequence of commands: writing keys to OTP (for "virgin" devices), setting security flags, and flashing the encrypted UF2. If you're working with batches of boards, you can even connect several at once and process them in sequence, always taking care to correctly identify each device.

The combination of read blocking, signature verification, and encryption makes charging a flat, unprotected UF2 becomes impossible In practice, this is because the bootloader will either reject it or simply won't be able to decrypt it. This covers both the protection of your intellectual property and basic system security.

Relationship between code organization, data types, and security

This whole framework of concurrency, read/write mutexes, and firmware security fits best when your code organization and your data types They are well thought out from the beginning. In practice, it's about applying good practices similar to those taught in Go books or other modern languages, but adapted to the embedded environment.

Separate the code into modules or packagesWith clear interfaces for the synchronization, data access, and encryption/validation layers, maintenance is greatly simplified. For example, you can encapsulate all the logic related to your custom RWMutex in a module, so the rest of the program simply calls functions like "read_config" or "update_config" without worrying about the locking details.

The use of structured types (struct), defined types, and pseudo-enumerations It helps you express in the code itself who can do what and what state the system is in. Security flags, bootloader states, reader/writer roles… all of that can be modeled with clear types, avoiding the use of magic integers or loose strings that are impossible to understand later.

Regarding mistakes, it's a good idea to adopt an explicit style: each function that performs a sensitive operation (Locking a resource, updating a key, writing to an OTP, flashing firmware) should always return a status or error code that is checked. Don't ignore returns "because it almost never fails." In embedded systems, that "almost never" is what ends up happening in production.

Finally, it's advisable to have a battery of automated and, if possible, performance testingAlthough the testing environment in a microcontroller is more limited, it is possible to create cases that verify that the read/write mutex does not get stuck under load, that access times are reasonable, and that encryption and verification operations do not cause excessive CPU or memory consumption.

With this whole set of components—well-designed concurrency, read/write mutexes for shared resources, UF2 encryption, OTP keys, and read blocking with picotool—the Raspberry Pi Pico and Pico 2 become a a much more robust platform in terms of both performance and securityThis combination allows the construction of commercial products where the firmware is protected, the hardware only executes what it should, and at the same time, the system takes full advantage of the ability to process tasks in parallel without falling into race conditions or unwanted lockups.

embedded systems
Related article:
Embedded systems: what they are, how they work, and examples