Recently I was trying to write some Rust code for the RP2350 microcontroller using rp-hal, which is amazing. But it doesn’t have a module for setting up PSRAM, (one of the ways to add working memory to the system). Searching led me to this and other similar snippets of code to set it up.
However, it didn’t work on my device. That sent me tilting, because not only do I not know what this code does, I didn’t even know how I would begin to figure it out.
#Literate programming
One approach I’ve found useful in this situation is to do a little literate programming. Essentially, writing a program interleaved with an essay on the program.
Looking through literate programming tools, I wasn’t 100% happy with any of them. I couldn’t find a well-maintained one that agreed with my desired workflow and output, so I decided to make another unmaintained and heavily-encoded-with-the-author’s-opinions tool.
Two or three hours of vibe coding later, I had the tool litsdoc uploaded to GitHub to live its best life with zero stars and about a week of being maintained.
I also recently learned a bit about tree-sitter from configuring it in Neovim, and using that was fun (I was able to dispatch a quick, joking request for Rust support with one prompt).
#Bare metal programming on the RP2350
Which brought me to the main course. The RP2350 has an official SDK and some languages also have Pico SDK-like packages, but what do you do when you need something those don’t supply? How do they work under the hood?
Then it’s time to spend a quality afternoon with the RP2350 datasheet. This is a riveting 1736 page document which answers these questions and more. Though you might want to first read the prequel: the RP2040 datasheet as there are some callbacks to its plot as well.
Armed with that I set out to write a program that doesn’t use libraries and runs on the RP2350. rp2350-minimal was helpful, but the programs aren’t commented. So I made my own blink demo following that and the datasheet. The documented version is here and the code is on GitHub.
Here’s a couple things I found interesting:
#Program layout
The layout of the final program itself matters. Of course this is true on any system, but
it’s normally handled for you by development tools. The Pico SDK and its CMake build
infrastructure also tries to replicate this this — you get to write a normal C program
with int main().
If you don’t have an SDK to use, some amount of linker script and assembly can be used to control the layout of the program, encoding constants or pointers to code at the beginning lets you do that. Then you can do things like:
- Select which architecture your program is targeting (the RP2350 supports both ARM and RISC-V)
- Point your code at important locations like where the stack lives in RAM
- Tell the RP2350 how you want to handle exceptions
#Altering hardware
On microcontrollers, some pretty basic things are left up to you. For example, on this system you can select and alter the clock speed of the processor.
Cooking your device with bad code seems entirely possible, although despite my best attempts I seemingly haven’t damaged any components on the development board I’m using.
#Peripheral access
The RP2350 development boards I’m using have a lot of peripherals. There’s all kinds of general purpose I/O which allows you to do everything from adding RAM to the system to serial console output.
But how do you do those when you don’t have a library? As it turns out, at least on the Arm Cortex M33, you do this by dereferencing magic memory locations and performing basic mathematical operations on them.
For example, here’s the code to set up GPIO pins:
#define REG(x) (*((volatile unsigned int *)(x)))
#define RESETS_BASE 0x40020000
// many many more constant #defines later
REG(RESETS_BASE + REG_ALIAS_CLR_BITS) =
RESET_IO_BANK0_BITS | RESET_PADS_BANK0_BITS;
while ((~REG(RESETS_BASE + RESETS_DONE_OFFSET)) &
(RESET_IO_BANK0_BITS | RESET_PADS_BANK0_BITS))
;
#How to use PSRAM on the RP2350
After that exercise and lots of datasheet consultations, the PSRAM code I was finding started to make some sense. A lot of it seems to derive from rp2_psram.c in MicroPython, which looks quite similar to the Rust example I first found.
QMI as it turns out stands for Quad Serial Peripheral Interface (abbreviated as QSPI in the datasheet) Memory Interface. Section 12.14 explains it, but in short it’s basically a subsystem that lets us set up external memory like flash or PSRAM and then use it normally like any other chunk of memory.
And while the code looks a bit different from the register manipulation above, as it turns
out it’s really not. At the end of the rainbow is this
definition
of qmi_hw, which gives us a struct-looking pointer to another magical memory address.
So effectively the PSRAM initialization snippets are just a more complex variation on the same theme — in this case, instead of just blinking an LED, we enter a configuration mode, set up the PSRAM and query some information about it, and finally disable that and enable the execute-in-place subsystem and use the memory normally. All by writing to and reading from memory addresses.
I still didn’t have a full theory of why the Rust code didn’t work, but after the literate programming exercise I was able to return to the PSRAM snippets and get a C demo working on my specific development board.
A lot of what I learned seemed to come into play; for example, the program layout matters. Since the PSRAM functions are altering the memory subsystem itself they need to live in RAM and shouldn’t call out to other functions that may live in flash. Which also goes towards explaining why some normal debugging techniques may fail.
#Back to literate programming
I learned these things by just trying to literate program the classic LED blinking demo. What I find most helpful is trying to explain everything to an imagined reader at a minute level of detail and understanding I wouldn’t normally reach, even in my head for a topic I’m relatively familiar with.
It’s useful, but tiring — what even is literate programming is a good exposition on the topic that I found while writing my little demo. And continuing down this road, for example, a lot of the code required is just loads and loads of constant definitions. Is it interesting to read those? But regardless, it was a good experience that unblocked me.