Need suggestion for crate organization for embedded project

I am working on an embedded project.
My code is written in Rust, but it needs to link with some C code to create the final executable.

The project is read some serialized data, and operates the hardware registers accordingly.
The project supports two RTOS, and multiple boards, so crates that depends on hardware or RTOS features need to have cargo features to differ RTOS or board.

Let's say all crates in the project prefixed with the name "foo":
Current stucture:

foo_version: Store static data of git version info
foo_support: General code for not OS or hardware related
foo_header: Bindgen of C header of OS, contain feature to different OS
foo_math: API for math
foo_parser: Parser for the serialized data
foo_pac: Peripheral Access Crate, for hardware register definition, contain features for different board.
foo_hal: Hardware abstract Layer, depend on foo_pac
foo_log: Extra api for log crate
foo_rtos: Code that OS dependent, but not depend on hardware
foo_collections: Extra container-like structs, currently depend on foo_rtos, but I am not sure if this good.
foo_log_impl: Register logger, depend on foo_rtos and foo_log
foo_panic: Implement #[panic_handler], depend on foo_rtos,
foo_galloc: Implement #[global_allocator], depend on foo_rtos.
foo_sched: Do the main loop, depend on most other crates
foo_init: Depend on most crates above, to call init functions for global variable
foo_clib: Depend on all crates above, to create a staticlib

Question:

  1. How to organize crates, so that I can run as many unit tests in hosted x86_64 environment as possible?
  2. Should my foo_collections crate depends on any OS related behavior?
  3. Synchronize primitives, such as semphoare,mutex, are OS dependent.
    Some of my container-like struct needs them. Which crate should I put those container.
    Should I create a trait for Semaphore, where the trait does not depend on OS feature.
  4. What features should be inside a hal crate? Should api for cpu, such as invalidate icache, in that crate, or separate crate?
  5. How to transform #[test] code to stuffs that is runable on real board?
  6. I am not sure the scope of foo_support crate. It currently contains code for const_assert, spinlock, Display trait impl for time, macro for convenient, etc. Many stuffs that is not OS or hardware related. Is it better to split the crate?
  7. Any other general suggestions?

generally for cross compiled code, you need multiple cargo workspaces, one for crates that can be compiled and tested on the host, and one for each cross-compiled target.

in case you have not read it yet, here's one great article (of a series) on this topic by ferrous systems, I strongly recommend you to read it:

as for every question, the answer is: it depends. but personally, for embedded applications, I would suggest not to think too much about it. you should first focus on the application aspect, less on the reuse aspect. you can always refactor the code later into reusable components if another application can reuse it and benefit from it.

if the primitives from different RTOSes can be implemented as unified API, it is usually a good idea to create this abstraction. just keep in mind the trade off: if the container types have more generic parameters, the ergonomics for the user might get slight worse.

again, it depends on the application. usually, I see hal as drivers for peripheral devices (but they are typically tightly integrated with the proccessor cores on the SoC, so really, it is rather arbitrary where you draw the line),

take the commonly used cortex-m SoCs as an example, there's the crates cortex-m and cortex-m-rt, which provide APIs to access the CPU (like power-on initialization, wrappers for special assembly instructions, etc) and drivers fo corer peripherals defined by the architectural spec( such as NVIC, SysTick, etc). these are typically not called hal crates, some call them "architectural support" crates, or "architectural runtime" crates.

on the other hand, there are tons of hal crates for different SoC chip models (or families of models), which provide convenient low-level APIs to access the (integrated) peripherals, such as GPIO, UART, or the radio transceivers on the nrf52 chips, or the PIO module on the rp2040 chip, etc.

for higher level functionalities, I would not call them hal either, maybe driver is more appropriate, such as usb stack, or tcpip, etc.

test on board is not trvial, you need special test harness libraries. I have absolute no personal experience on this topic, but I have heard that defmt-test can be used as on board test harness.

also, there's examples for on board tests in the linked ferrous systems blog post.

again, don't over-think reusabilities upfront. I often start by putting everything in the application crate, and if I found some code can be reused, I can easily extract it into a separate library.

2 Likes

Thanks. This is very useful

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.