I/O pin managment with embedded bare metal rust

I have recently been working on a GPIO/I2C library for the RPI and have been trying to come up with a good method for keeping track of which GPIO pins are in use.

Specifically for things like serial communication (i.e. I2C, SPI, ect.). I was thinking that I could use an array and put the pins in use into that but I'm worried that that might be bad practice. The important thing to remember is that I can't use the std library because this is for embedded programming and their is no OS. I also checked over on stackoverflow and someone mentioned that embedded-hal uses the singleton pattern but I have been reading through that code and haven't seen anything for singleton in just the main crate.

Ideally this would be accessible between cores, so it would be atomic. I also need to be able to remove pins from the list. If anyone has any ideas or any opinions on using arrays your input would be much appreciated.

I assume you are talking about the rp2040/rp2350 microcontrollers (e.g. the "pico" boards), not the more capable single board computers (e.g. raspberry pi 5). although you can run bare metal software on the single board computers, they are typically running the linux kernel.

can you provide more details about your i2c library? particularly, why you can't use the existing i2c drivers in the hal crates, e.g. rp2040-hal or rp235x-hal on bare metal, or embassy-rp if you prefer RTOS.

btw, the so called "singleton pattern" is typically implemented in PACs (peripheral access crates), not in HAL crates.

PACs are typically mechanically generated from the device svd, and they use a singleton API to model the limited peripheral resources on the given chip. HAL crates are built on top of PACs, there's no need for HAL to use the singleton api.

also note, the term "singleton" refers to the Peripherals::take() API in the PAC, which can only be called once (subsequent calls would always return None). it is a completely different thing than the OOP patterns lingo.

as for the embedded-hal crate, it defines some "portable" APIs for common functionalities on embedded platforms. if you only target specific microcontrollers, you don't need to adhere to embedded-hal's API, but if your implementation is compatible with it, better to support it to make your library more portable.

I am currently writing this on a raspberry pi 4. The plan is eventually to move it to a zero but I don't have one as of now. I am relatively new to both rust and embedded programing but I really don't want to write a program to run on a Linux kernel. My project involves pretty high speed data collection and processing so that's why I need the pi. I probably could use another library but I just need something to easily interface with peripherals. Part of doing that though is making sure I don't use the same pin for something twice.

oh, so my assumption was wrong. it's not the rp2040 microcontrollers, but the ARM64 processors, with which I don't have personal experiences.

technically, every targets can run "bare metal", even for x86_64, we have the x86_64-unknown-none target, but when people talk about bare metal targets, they commonly refer to microcontrollers that are very weak in terms of computing power. for platforms like AArch64, I think terms like "unikernel" are more commonly used than "bare metal" application.

for the rpi 4, I did find an existing PAC on crates.io, and that's it. compared to the cortex-m or riscv based microcontrollers, the ecosystem for bare metal AArch64 targets in rust simply isn't there (yet?).

you can write an i2c driver using the PAC, but you also need all other low level support code to create a full fledged application, such as a custom linker script, the boot routine, the stack and heap initialization, the interrupt handling, etc.

it's possible, e.g. you can utilize existing solutions in C/C++, but the developer experience will not be as smooth as for the microcontrollers which has more mature embedded ecosystem in rust.

yes, that's one of the main benefit of rust's advanced type system compared to tranditional embedded languages like C/C++.

this is the same convention used in the ecosystem for bare metal targets (most microcontrollers). typically, you have a PAC that is generated from svd files provided by the chip vendors, which contains the register definition for the peripherals. and here comes the important bit: each individual configurable hardware resource is represented by a distinct type. these types are typically zero sized and does not have runtime overhead at all, but they do enable the higher level crates to leverage the rust type system to provide a ergonomic yet very safe APIs.

the documentation of svd2rust contains the details how the generated API works.

I am using an aarch64-unkown-none target. This is a link to a working Kernal I wrote for pi 4. It blinks pin 57 and only runs the main code on core 0. This is based off of a ton of other peoples work including rust-raspberrypi-OS-tutorials.

I will read through the PAC crate you linked and see if I could use some of that. The PI4b uses a BCM2711 and Quad-core ARM Cortex-A72. The zero uses its own chip but the peripherals are the same as the BCM2835.

I looked through the PAC and what I might do is use that and then build a HAL on top of it. Does that seem like a good idea?

cool! glad you have the boilerplates and scaffoldings all figured out, then you can focus on solving your problem!

sure, if you can use the existing PAC, better to use it instead of reinvent the wheel. just be careful about virtual memory, as noted in the readme file.

you can refer to some HAL crates of microcontrollers as a starting point, such as the rp2040 chip, which is very flexible in terms of pin configurations: