How to create a project that can be compiled for both Linux and a microcontroller?

I want to create a project that has both Linux and microcontroller versions with a common shared core.

I am creating this post, because I think a public discussion about this use case would benefit the community.

I have a Pine64 and a Blue Pill sitting next to me and I want to develop a project that I will be able to deploy and use on both, from the same source code.

Each version needs some extra code specific to it, but the core logic should be common.

The Linux version could have extra functionality using std (like reporting its status over a network socket). The microcontroller version obviously needs to be no-std with its own runtime and setup code, and will have fewer features.

Further, I will probably need an abstraction for global configuration/variables, like what pins to use, which will differ between the boards.

What is the easiest way to structure such a project for minimal development/prototyping hassle? I would prefer to avoid splitting my project into multiple crates, but I am open to discussing the pros/cons of different approaches. Maybe some trickery with cargo features?

EDIT: It will control GPIO and I2C devices. I presume embedded-hal is the way to go for this, which can be backed by sysfs in the linux version and the board support crate on the microcontroller.

This may not be quite what you want, but I've done this before using separate crates in a cargo workspace for my micromouse project (code at Rowan RAS / Micromouse / micromouse ยท GitLab, write-ups at Tim's Website). It is split up into a common logic crate, embedded firmware crate, and simulation crate. In this case, the common logic is fairly independant of the hardware and runs linearly, so it doesn't use embedded-hal for the interface. Instead, it is just a struct with an update method that gets called repeatedly with arguments for sensor readings, and returns motor powers and debug information. You will want to do what works best for the structure of your code, whether it is embedded-hal or something else. The simulation can be either be compiled to wasm for use in a fancy GUI, or a linux binary for use in CI.

Doing it with separate crates makes it easy to clearly separate not only code, but also dependencies and build processes. For example, the micromouse firmware crate needs a build script to include a linker script with the memory layout, while the simulation crate has a bunch of extra stuff for wasm. You can even do things like make cargo run do something different for each one. This also makes it easy to add another hardware target later by just adding another crate. By putting them all in a cargo workspace, cargo will share dependencies between them.

You probably could do it in one crate with a bit of feature flags and #[cfg(..)], but I could see it getting messy pretty fast.

You could also do a hybrid approach, where you split them up but the common logic crate has a feature flag to enable things that don't work in no_std.

2 Likes

OK, thanks for the useful ideas. I have already made some progress on my project and I think I have a project structure that roughly works. I will come back here and post what I came up with when I have everything working.

I am not a fan of splitting the project into small crates, so my approach uses cargo features.

I agree that it seems like a good idea to abstract away the logic from the implementation. I will create a higher-level API for my logic code, rather than calling embedded-hal directly.

Putting this in a trait seems like an easy way to get the compiler to ensure both of my implementations are coherent.

I prefer not to have to list dependencies in multiple places. Having the entire project configuration in a single Cargo.toml seems cleaner and less error-prone.

This can be easily avoided by putting all the stuff in a module. Then you only need one #[cfg(feature = "...")] to include the module.

I will keep working on this and I'll post some code when I am sure my ideas work.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.