What is the structure of a real-world embedded workspace?

Hello everyone,

I am having some trouble finding examples of how to organize embedded projects, especially for custom hardware. There are a lot of examples for how to do something on off-the-shelf hardware like Arduinos, but not so much when you are building potentially large workspaces for something that does not have a BSP.

So - the below is my understanding of the logical sections of a large workspace, including features like FPGA interfaces, reusable drivers, and interchangeable hardware. I would appreciate any sort of suggestions for anything that works better in practice.

PAC

Peripheral access controllers are standard and generated from svd files. FPGAs fabric that generates SVD would create one of these separate from the main core's SVD (e.g., zynqs)

HAL

HALs are handwritten and are meant to provide abstraction over PACs. They provide all kinds of interfaces, including ways to construct the embedded-hal types. In general, these seem to get quite complicated with traits & types (e.g. samd-hal)

There might also be something custom that provides traits for things not in embedded-hal.

Drivers

Drivers are generic and depend only on embedded-hal - should be generic over embedded-hal traits. These provide functionality to use ICs that somehow interface with the main IC (e.g. tcal9539 i2c i/o expander, mt25q qspi flash, tmp107 uart temp sensor, tja1043 can controller, ethernet phys)

BSPs

BSPs depend on HAL crates, and expose correctly named (and sometimes typed) interfaces. This should be set during PCB design, and can actually help check for pinout errors if the HAL has very strong typing. If pinout changes, this should be the only component to change (I think). BSPs for similar things should aim to export identical names, for easy reuse in application code. I don't think any reliance on drivers belongs here.

I have also seen helper functions for configuring peripherals (uart, i2c, etc), such as the samd-hal crate. These always seem a bit tricky to write, but potentially nice to have.

Application Code

This is the actual entrypoint, imports the desired bsp as bsp and performs the desired tasks. Which bsp it imports may be feature gated.

Different app code crates may depend on something like rtic, embassy, etc as needed.


So, all that in mind, I'd wind up with a directory structure like this:

# Cargo.toml in each leaf directory
ffi/                     # Bindings to existing C code to be called
drivers/                 # Drivers for external interfaces 
    pca9535/
    tmp107/
pac/                     # PACs for nonstandard peripherals
    fpga-config-1/
    fpga-config-2/
hal/                     # Nonstandard HALs, feature gated for
    fpga-hal/            # custom PACs
bsp/                     # Define interfaces to the main chip here
    pcb-1/
    pcb-2-chip-1/
    pcb-2-chip-2/
app/                     # Run the actual code
    application-1/
    application-2/
Cargo.toml               # cargo workspace config

Please let me know what parts of the above structure work well, and what needs some tweaking to be more accurate to what works well.

That seems like a pretty reasonable solution for a large project which needs to support a wide range of hardware.

That said, I'd probably prefer to keep everything under a single crates/ folder and just use a naming convention.

  • crates/
    • pca9535-driver/
    • tmp107-driver/
    • fpga-pac-config-1/
    • fpga-pac-config-2/
    • fpga-hal/
    • pcb-1-bsp/
    • pcb-2-chip-1-bsp/
    • pcb-2-chip-2-bsp/
    • application-1/
    • application-2/
  • Cargo.toml

I normally look to rust-analyzer for suggestions for dealing with larger Rust projects, and Matklad has actually written something up on the subject:

In particular,

It is true that nested structure scales better than a flat one. But the constant matters — until you hit a million lines of code, the number of crates in the project will probably fit on one screen.

Finally, the last problem with hierarchical layout is that there are no perfect hierarchies. With a flat structure, adding or splitting the crates is trivial. With a tree, you need to figure out where to put the new crate, and, if there isn’t a perfect match for it already, you’ll have to either:

  • add a stupid mostly empty folder near the top
  • add a catch-all utils folder
  • place the code in a known suboptimal directory.

This is a significant issue for long-lived multi-person projects — tree structure tends to deteriorate over time, while flat structure doesn’t need maintenance.

These sort of projects have a tendency to grow small sub-crates as you find common abstractions and solve similar problems. While the driver/pac/hal/bsp/app layering gives you some pretty good folders, I doubt all your crates will fit into that hierarchy.

Awesome, thank you for the insight! Flat structure definitely makes sense for smaller projects, I suppose I was just moreso trying to plan the logical sections and their relationships