You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This proposal is to add a CLI option to split peripheral modules into separate crates in a project workspace.
Motivation
For large PAC crates, compilation on machines with less-powerful cores is excruciatingly slow. The bottleneck for any projects depending on the PAC crate is obviously the PAC, because one core is pinged to full usage, while all other cores are idle.
By splitting the PAC into separate crates for each peripheral, each core could work on a separate peripheral crate. The separate crates could also improve incremental compilation, since a change in one of the peripheral crates would not necessarily require recompilation of other peripheral crates.
Challenges
Much of the plumbing is already there for creating separate crates for each peripheral.
Each peripheral already generates its own module.
Peripheral versioning would inherit from the top-level Cargo.toml version.
The main challenge will be making minimal changes to enable the code generation, without adding too much to maintainability burden.
groupName should be used for crate names and existing "feature-group" option.
For folder names hash values could be used instead of peripheral names.
With #[path="hash"] mod spi1 attribute. Like in Hashed peripherals djmcgill/form#24. Advantages:
Allows to reuse identical peripherals across different chips.
You can skip writting (or even generating if hash is calculated from SVD directly) peripheral if folder already exists.
So I've tried to implement 1 step but understood that I don't know how to do this. There are 2 issues:
Encapsulation. Some methods of common structures are made private to prevent manual creating. The only solution is to just hide them from docs.
It uses type aliases so you can't add implementation from "outside" (without traits). It can be solved by using "newtypes", which have also other advantages but it creating many new types overloads compiler and it much slower (3 times and bigger). See version 0.14 and previous.
But without this change splitting on different crates makes much less sense as each one will have it's own copy of register access implementation.
I still like the idea as it gives possibility to create and publish independent crates much easier so HALs can reuse them. Like https://github.com/stm32-rs/bxcan
Now to performance issues. I'd want to see on SVDs you are using to generate rust. The best way to improve performance is to reduce of code. You can do it by using register arrays, clusters and by using derivedFrom.
creating many new types overloads compiler and it much slower (3 times and bigger).
Wow, that is definitely the opposite of the goal of this proposal. Thank you for doing the experiment.
I still like the idea as it gives possibility to create and publish independent crates much easier so HALs can reuse them
I had a similar thought when you brought up the module hashing, like the peripheral crates could be reused by other PACs and HALs.
That may also require parameterizing the base address in some way (either on a Peripheral::new(base_addr: usize), Peripheral<const BASE: usize>, etc.).
The best way to improve performance is to reduce of code
This was also brought up in the meeting yesterday, and it certainly seems like one of the first, most straightforward steps to take in reducing compile times (especially when one has full control over the SVD).
However, I'm already heavily utilizing as much code re-use as possible. There is maybe a bit more re-use I could gain with the SVD attribute for saying a non-contiguous peripheral is based on another definition. For example, there are peripherals that are based on the same IP, but are not in contiguous memory.
I'd want to see on SVDs you are using to generate rust.
The main PAC that I maintain is jh71xx-pac, and I use svd-generator to generate the SVD (the latter is where I could probably make some additional optimizations for code re-use).
No registers block. But derivedFrom attribute.
Also I recommend to add groupName for all peripherals which specifies peripheral type.
Besides code reduce this also gives you possibility to write generic (over peripheral) code in your HAL instead of macros.
P.S. You could also try to use patched version of form I mentioned above. But this is useful only to reduce size of code across chips. As it will not give you faster compilation but only less of code.
No registers block. But derivedFrom attribute.
Also I recommend to add groupName for all peripherals which specifies peripheral type.
So, I followed your advice, and applied the derivedFrom attribute where applicable.
It shaved off ~72k LoC, and ~17-20 seconds from CI compile time!
Going to try it on a low-resource, multicore SBC now.
This is encouraging. I think if we can further improve with parallelizing the PAC build as much as possible, it will greatly improve compile times.
Locally, there is still a period where there seems to be a single core pinged with other idle cores.
Edit: attempted the build with reduced code on the low-resource SBC. There was a small time-savings, but the debug build still takes ~3min with the majority spent in the final stage compiling jh71xx-pac itself. Again, it is only using a single core at full use, with other cores remaining idle.
Going to try splitting the crate manually to see how much time saving there is.
Going to take a bit to do that programmatically in svd2rust and form.
form is likely going to need an option to generate new crates, instead of just modules. It may even require bringing the form functionality internal to svd2rust.
It's not clear at the moment, but one potential option would be to define the peripherals with the following pseudo-Rust:
pubexterncrate some_peripheral {// alll the peripheral code that would normally go in the module}
Then, after the form processing, each crate with the above markup would go into its own top-level crate directory. The entry in the main crate would change to:
Have you tried to implement it? That is the question.
Yes, but only had success with a manual implementation of a single small peripheral (Clint).
I tried some additional experiments with programmatic implementation in svd2rust, and that's where the idea to modify form came up.
How do you suggest to avoid code duplication? Of each bits, set_bit, etc.
That is definitely an issue. It may require a complete restructuring of the generic types into traits, and/or using macros.
Through these experiments, I keep wondering about defining PACs per-peripheral instead of at the SoC level. I know this was not the original intention of SVD, but peripherals are shared across a number of different SoCs. The only thing that may change is the base address of the peripheral, and/or some other attributes dynamically defined by register values (which is not expressible in SVD).
If we could dynamically define the base address of the peripheral, the generated PAC could be shared across different SoCs that share the same peripheral.
It would also allow writing a HAL to the peripheral, a driver, etc. The HAL for a SoC would then be composed of HALs for each of its peripherals.
What do you think? Maybe there is a better place for this discussion?
If you are saying about declarative macros that is what stm32ral exactly does.
If you are saying about procedural macros
Also there is chiptool which does not use write proxies. Instead of this it defines all field access variants directly on register writers. Try it, maybe it is better for your use cases.
The only thing that may change is the base address of the peripheral, and/or some other attributes dynamically defined by register values (which is not expressible in SVD).
How much simpler everything would be if this were true.
What do you think? Maybe there is a better place for this discussion?
If you are saying about declarative macros that is what stm32ral exactly does.
If you are saying about procedural macros
I'll keep looking at stm32ral, but I don't really think the same approach would work for the wrapped R + W types.
I am thinking more of a macro that handles the boilerplate for accessing the wrapped inner R + W types. I can work on getting a working example in code to clarify.
Also there is chiptool which does not use write proxies.
Right, I've considered using chiptool, but I think it also generates one large crate with all of the peripherals. So, it wouldn't really help with the overall build-time issues (unless there is some other magic they do to parallelize builds).
How much simpler everything would be if this were true.
Or, are you saying that peripherals on different SoCs are not interchangeable, even if they are based on the same IP (read same compatible string in a DeviceTree)?
If we could dynamically define the base address of the peripheral, the generated PAC could be shared across different SoCs that share the same peripheral.
It would also allow writing a HAL to the peripheral, a driver, etc. The HAL for a SoC would then be composed of HALs for each of its peripherals.
What do you think? Maybe there is a better place for this discussion?
Maybe this would also be a viable new issue, but I think this is super interesting for a project I am currently planning. I am trying to create basic PACs (and maybe HAL if that works?) for the the Xilinx Zynq 7000 SoC. The Xilinx SoCs are hybrid systems with both ARM cores and FPGAs on the same fabric. It is very common on these systems to dynamically create hardware on the FPGA fabric like UART or SPI softcores through IP designs. The IP generally also has registers documented in its respective documentation, and the softcores have a defined register base address and are mapped into the project AXI bus directly. That means that I would also have to generate some kind of dynamic PAC, in addition to the fixed PAC for the ARM cores.. I have not looked at this in more detail, but some thoughts about this:
The softcores have a dynamic base address. svd2rust mostly has to use/process/geenerate code for fixed base addresses of peripheral blocks if I understand correctly.
If a user now wants to use Rust to drive the softcore, they would have to somehow instantiate the peripheral API on a dynamic base address I mentioned earlier because those addresses are configurable via the FPGA design. Maybe I could at least re-use https://github.com/rust-embedded/svd2rust/blob/master/src/generate/generic.rs for first experiments?
Maybe this would also be a viable new issue, but I think this is super interesting for a project I am currently planning.
I was thinking the same thing, dynamic peripheral base addresses are related, but separate from splitting peripherals into their own crates.
If @burrbull thinks it deserves its own issue, I can open a new issue.
If a user now wants to use Rust to drive the softcore, they would have to somehow instantiate the peripheral API on a dynamic base address I mentioned earlier because those addresses are configurable via the FPGA design.
Sounds like another great use-case for a configurable peripheral base address. The main issue I see, it would be a svd2rust-specific extension of the CMSIS-SVD spec. Maybe someone with a bit more clout in the ARM space could request feedback on adding it to the upstream specification?
Otherwise, I'm not sure how much of a blocker having a svd2rust-specific extension would be? I think if we document whatever syntax we use in the SVD to indicate the extension, it should make it easy enough for maintainers, and any other projects that may want to pick it up, e.g. other svd2<some-lang> codegen projects.
Sounds like another great use-case for a configurable peripheral base address. The main issue I see, it would be a svd2rust-specific extension of the CMSIS-SVD spec. Maybe someone with a bit more clout in the ARM space could request feedback on adding it to the upstream specification?
What kind of extension do you mean? How should it look like?
@robamu when you say "dynamically" does it mean the address is not known on compile time or just that it can take any value?
The value would be known compile time when generating / writing / using the dynamic PAC. The user can edit the address in the FPGA design, but usually, one will just take an auto-assigned address and it will generally stay fixed after that.
@burrbull looks like we have another example in a similar context (Rust codegen from SQL), of using separate crates drastically decreasing compile time:
Activity
burrbull commentedon Jan 28, 2025
I like the idea. I think it is last chunk of the puzzle.
But it requires more changes then an option.
form) #430Other notes:
groupNameshould be used for crate names and existing "feature-group" option.With
#[path="hash"] mod spi1attribute. Like in Hashed peripherals djmcgill/form#24. Advantages:burrbull commentedon Jan 29, 2025
So I've tried to implement 1 step but understood that I don't know how to do this. There are 2 issues:
But without this change splitting on different crates makes much less sense as each one will have it's own copy of register access implementation.
I still like the idea as it gives possibility to create and publish independent crates much easier so HALs can reuse them. Like https://github.com/stm32-rs/bxcan
Now to performance issues. I'd want to see on SVDs you are using to generate rust. The best way to improve performance is to reduce of code. You can do it by using register arrays, clusters and by using
derivedFrom.rmsyn commentedon Jan 29, 2025
Wow, that is definitely the opposite of the goal of this proposal. Thank you for doing the experiment.
I had a similar thought when you brought up the module hashing, like the peripheral crates could be reused by other PACs and HALs.
That may also require parameterizing the base address in some way (either on a
Peripheral::new(base_addr: usize),Peripheral<const BASE: usize>, etc.).This was also brought up in the meeting yesterday, and it certainly seems like one of the first, most straightforward steps to take in reducing compile times (especially when one has full control over the SVD).
However, I'm already heavily utilizing as much code re-use as possible. There is maybe a bit more re-use I could gain with the SVD attribute for saying a non-contiguous peripheral is based on another definition. For example, there are peripherals that are based on the same IP, but are not in contiguous memory.
The main PAC that I maintain is jh71xx-pac, and I use svd-generator to generate the SVD (the latter is where I could probably make some additional optimizations for code re-use).
Here is a link to the jh71xx-pac SVD
burrbull commentedon Jan 29, 2025
Yes. This is the first you should fix.
For example UART1 should looks like
No
registersblock. ButderivedFromattribute.Also I recommend to add
groupNamefor all peripherals which specifies peripheral type.Besides code reduce this also gives you possibility to write generic (over peripheral) code in your HAL instead of macros.
P.S. You could also try to use patched version of
formI mentioned above. But this is useful only to reduce size of code across chips. As it will not give you faster compilation but only less of code.rmsyn commentedon Jan 31, 2025
So, I followed your advice, and applied the
derivedFromattribute where applicable.It shaved off ~72k LoC, and ~17-20 seconds from CI compile time!
Going to try it on a low-resource, multicore SBC now.
This is encouraging. I think if we can further improve with parallelizing the PAC build as much as possible, it will greatly improve compile times.
Locally, there is still a period where there seems to be a single core pinged with other idle cores.
Edit: attempted the build with reduced code on the low-resource SBC. There was a small time-savings, but the debug build still takes ~3min with the majority spent in the final stage compiling
jh71xx-pacitself. Again, it is only using a single core at full use, with other cores remaining idle.Going to try splitting the crate manually to see how much time saving there is.
rmsyn commentedon Feb 8, 2025
Reposting a comment I left in the
rust-embeddedMatrix channel for tracking purposes:So, I tried a bit of an experiment manually splitting out the peripheral modules into crates.
From the bit of work done, it requires splitting out the
genericmodule into its own crate, and importing it into the peripheral crate(s).The
RandWtypes can no longer just be aliased (since they now come from an external crate).Instead, the aliases turn into wrapper types:
Going to take a bit to do that programmatically in
svd2rustandform.formis likely going to need an option to generate new crates, instead of just modules. It may even require bringing theformfunctionality internal tosvd2rust.It's not clear at the moment, but one potential option would be to define the peripherals with the following pseudo-Rust:
Then, after the
formprocessing, each crate with the above markup would go into its own top-level crate directory. The entry in the main crate would change to:burrbull commentedon Feb 8, 2025
Have you tried to implement it? That is the question.
How do you suggest to avoid code duplication? Of each
bits,set_bit, etc.rmsyn commentedon Feb 8, 2025
Yes, but only had success with a manual implementation of a single small peripheral (
Clint).I tried some additional experiments with programmatic implementation in
svd2rust, and that's where the idea to modifyformcame up.That is definitely an issue. It may require a complete restructuring of the
generictypes into traits, and/or using macros.Through these experiments, I keep wondering about defining PACs per-peripheral instead of at the SoC level. I know this was not the original intention of SVD, but peripherals are shared across a number of different SoCs. The only thing that may change is the base address of the peripheral, and/or some other attributes dynamically defined by register values (which is not expressible in SVD).
If we could dynamically define the base address of the peripheral, the generated PAC could be shared across different SoCs that share the same peripheral.
It would also allow writing a HAL to the peripheral, a driver, etc. The HAL for a SoC would then be composed of HALs for each of its peripherals.
What do you think? Maybe there is a better place for this discussion?
burrbull commentedon Feb 9, 2025
If you are saying about declarative macros that is what stm32ral exactly does.
If you are saying about procedural macros
Also there is
chiptoolwhich does not use write proxies. Instead of this it defines all field access variants directly on register writers. Try it, maybe it is better for your use cases.How much simpler everything would be if this were true.
I don't mind it.
rmsyn commentedon Feb 11, 2025
I'll keep looking at
stm32ral, but I don't really think the same approach would work for the wrappedR+Wtypes.I am thinking more of a macro that handles the boilerplate for accessing the wrapped inner
R+Wtypes. I can work on getting a working example in code to clarify.Right, I've considered using
chiptool, but I think it also generates one large crate with all of the peripherals. So, it wouldn't really help with the overall build-time issues (unless there is some other magic they do to parallelize builds).Yeah... I opened an issue with the upstream
CMSIS-SVDspec repository about expanding the spec to handle dynamically defining base addresses. Almost a year, no response...Or, are you saying that peripherals on different SoCs are not interchangeable, even if they are based on the same IP (read same
compatiblestring in a DeviceTree)?burrbull commentedon Feb 11, 2025
I can imaging only
Regpart to be made generic without total reorganization. Approximately one third ofgeneric.rsSee https://github.com/burrbull/reg
Other things like field/register readers and writers should be copied anyway.
robamu commentedon Feb 17, 2025
Maybe this would also be a viable new issue, but I think this is super interesting for a project I am currently planning. I am trying to create basic PACs (and maybe HAL if that works?) for the the Xilinx Zynq 7000 SoC. The Xilinx SoCs are hybrid systems with both ARM cores and FPGAs on the same fabric. It is very common on these systems to dynamically create hardware on the FPGA fabric like UART or SPI softcores through IP designs. The IP generally also has registers documented in its respective documentation, and the softcores have a defined register base address and are mapped into the project AXI bus directly. That means that I would also have to generate some kind of dynamic PAC, in addition to the fixed PAC for the ARM cores.. I have not looked at this in more detail, but some thoughts about this:
If a user now wants to use Rust to drive the softcore, they would have to somehow instantiate the peripheral API on a dynamic base address I mentioned earlier because those addresses are configurable via the FPGA design. Maybe I could at least re-use https://github.com/rust-embedded/svd2rust/blob/master/src/generate/generic.rs for first experiments?
rmsyn commentedon Feb 18, 2025
I was thinking the same thing, dynamic peripheral base addresses are related, but separate from splitting peripherals into their own crates.
If @burrbull thinks it deserves its own issue, I can open a new issue.
Sounds like another great use-case for a configurable peripheral base address. The main issue I see, it would be a
svd2rust-specific extension of the CMSIS-SVD spec. Maybe someone with a bit more clout in the ARM space could request feedback on adding it to the upstream specification?Otherwise, I'm not sure how much of a blocker having a
svd2rust-specific extension would be? I think if we document whatever syntax we use in the SVD to indicate the extension, it should make it easy enough for maintainers, and any other projects that may want to pick it up, e.g. othersvd2<some-lang>codegen projects.burrbull commentedon Feb 18, 2025
@robamu when you say "dynamically" does it mean the address is not known on compile time or just that it can take any value?
One of those?
See also #910
What kind of extension do you mean? How should it look like?
robamu commentedon Feb 18, 2025
The value would be known compile time when generating / writing / using the dynamic PAC. The user can edit the address in the FPGA design, but usually, one will just take an auto-assigned address and it will generally stay fixed after that.
rmsyn commentedon Feb 19, 2025
In code, it could look something like:
This is very close to your implementation in #910, so using your generic peripheral:
In the SVD, it could look something like:
I'm not tied to the name of the
baseAddressDynamicattribute, or if we want to provide a default address value there.If we supplied a default value, that would allow for a default instantiation which we could alias to look like the current API.
However, with the dynamic API, users could define their own implementation.
We could also introduce an alignment attribute, to ensure we could enforce that invariant in the generic structure.
With your implementation in #910, there may be no need for a SVD
baseAddressDynamicfield.Then, the user is free to define their own type aliases.
rmsyn commentedon Apr 16, 2025
@burrbull looks like we have another example in a similar context (Rust codegen from SQL), of using separate crates drastically decreasing compile time:
https://www.feldera.com/blog/cutting-down-rust-compile-times-from-30-to-2-minutes-with-one-thousand-crates
Emilgardis commentedon Apr 16, 2025
This would probably be benefitial to this proposal: rust-lang/rust#122349
I also remember some discussion about allowing multiple compilation units with
crate sub_crate_name { ... }but can't seem to find it