Loadstone is roughly divided in two layers. An abstract layer under
src/devices/, where you can find behaviour common to all targets, and a
target-specific layer under src/ports/, composed of code that only applies to a
particular microprocessor or family.
The main role of the ports module is to define exactly how a bootloader is
built. This involves many decisions, such as what pins are assigned to what
functionality (serial, flash, etc), which features to enable, and how to
distribute the image banks in the memory map.
Since those decisions must be made at compile time, this would require a lot of
source code to be modified for each particular application, which isn't a clean
way to work. It would likely result in dozens of folders under ports
containing particular permutations of pins and banks used for different
projects.
To combat this, ports can make use of code generation. There's a secondary
crate under loadstone_front, which can be ran natively or as a WASM web app,
and whose sole purpose is to offer an intuitive interface to define all the
port-specific bootloader options. These options are then encoded as a .ron file
(a format similar to Json), which can be read by Loadstone at build time, in
order to generate source code that conforms to these options.
$ LOADSTONE_CONFIG=`cat loadstone_config.ron` cargo b loadstone
The command above will attempt to build Loadstone based on the provided config. If any feature flags are missing, the resulting compiler error will provide a list of them.
Note that it's not mandatory for the ports to make use of this feature. It's possible
to define a manual port that makes no use of code generation at all, in which
case the LOADSTONE_CONFIG environment variable can be assigned an empty
string.
Adding a new feature to a code generation port goes through three phases:
-
Under
loadstone_config/src/lib.rs, theConfigurationstruct is expanded with any number of new fields detailing the new feature. These fields can be at the top level ofConfigurationor inside any of its members. ThisConfigurationstruct is what ultimately gets serialized into the.ronfile, so anything included in it will be available for the code generation engine. -
Under
loadstone_front/src/app/mod.rs, the GUI code is expanded with any necessary widgets, labels, etc. required to configure the new feature. Note how theupdatefunction has access to a variable of typeConfiguration, which is the struct that was expanded in step one. A simple example to follow in order to understand how to offer a widget interface over a feature can be found underloadstone_front/src/app/menus/mod.rs, in theconfigure_boot_metricsfunction. -
Under
loadstone_config/src/codegen/mod.rsor any of its submodules, the code generation functions are expanded with the logic necessary to transform the feature defined in theConfigurationstruct into source code that Loadstone can include. This is done using thequote!macro, which can be used to build arbitrary Rust source code. A good example of this process can be found in thegenerate_top_level_modulefunction, which constructs the source for the top levelautogeneratedmodule.
Adding a new code generation feature requires updating the CI scripts to be
aware of it. .github/workflows/actions.yml contains a few embedded .ron
samples so that CI can verify a variety of feature permutations. If your feature
adds a new field to the Configuration struct, these new samples must be
updated with the new field.