-
Notifications
You must be signed in to change notification settings - Fork 8
API tailored for specific microcontrollers. #9
Comments
It is possible to use Cargo features for that but I think it's not a good solution because those features are binary so in theory there's nothing stopping you from enabling several device features at once like +stm32f103c8 +stm32f103vg which doesn't make much sense. I think a better solution would be include cfg attributes like $ cat .cargo/config
[build]
rustflags = [
"--cfg", 'device="stm32f103vg"'
] Of course there's nothing stopping you from passing several $ cat .cargo/config
[build]
rustflags = [
"--cfg", 'device="stm32f103c8"'
"--cfg", 'device="stm32f103vg"',
] With Cargo features the problem is not so obvious because Cargo features are additive. If the stm32f103xx appears more than once in your dependency graph and each instance has different features (devices) enabled then Cargo will compile the stm32f103xx with the union of all the features. But the information about which feature is enabled is scattered across different crates so it's hard to track down the source of the problem. Now the question is how could we modify svd2rust to include these #[cfg] attributes. I don't think SVDs provide a field for this kind of thing so maybe we can use an auxiliary file thas specifies what's available on what device and what's not. Since you have been working with different devices in the stm32f103 family what differences have you found between those devices. Are all the differences just at the peripheral level (e.g. TIM6 is not available in low density devices), or are there also differences at the register level? In any case it probably makes sense to make the file substractive: as in it specifies which peripheral / register is not available for device X. For example it could look like this: # stm32f103xx.toml
[device.stm32f103c8]
# these are not available for this device
peripherals = ["TIM6", "TIM7"] Then the generated code would look like this: #[cfg(not(device = "stm32f103c8")]
pub const TIM6: Peripheral<tim6::RegisterBlock> = ..;
.. Thoughts? |
You are right. I already seen several crates facing the same problem. Cargo really should implement some enum-like functionality for crate settings.
I like the idea to declare type of microcontroller as set of peripherals. It will make possible to include in API only peripherals that you are using (will it reduce a compile time?).
As far as I know differences are only in memory size and availability of some peripherals. At least I am unaware of any example where the same peripheral behaves differently. Registers are the same in whole family but it makes problem even more annoying. Because trying to use unavailable peripheral does not cause exceptions. Registers are there but writing to them does nothing. This is why such mistakes are hard to spot and I decided to modify SVD file. So you could describe differences at peripheral level. But I did not only that but also removed all configuration bits related to peripheral. For example TIM6EN from register APB1ENR. And also I removed all configuration bits related to pins that are not present on given package. So for example when porting from 64 pin part to 48 pin looking at compiler errors it will be easy to tell which pins need to be remapped. I still don't know if it was worth it. Implementing such level of granularity using configuration file and conditional compilation probably will be too cumbersome. |
Maybe. IMO that may lead to proliferation of non composable crates as in crate A
Hmm. Idea: create one Cargo feature per peripheral and add it to the stm32f103 However that sounds like too many Cargo features to manage and to reason about.
It would reduce compile time of the stm32f103xx crate itself but that's a In any case this would require a bunch of #[cfg] attributes in the stm32f103xx
At that point what would be difference from just having one crate per device?
Yeah, that sounds more involved and less automatable. IMO if you are using a HAL The pin remapping problem, though, is interesting. I hope we'll come up with |
This is exactly what I had in mind. And other crates should depend on only required features. For expample USART HAL could depend on USART, RCC, GPIO and optionally DMA peripherals. We still can have features like "stm32f103c8" for convenience. But they will be declared only as a set of peripherals.
This was my original proposal. :)
Yes. For now idea of separating at peripheral level sounds best to me. At least it can be automated and is relatively easy to understand. But if you decide to take this approach then this issue belongs to svd2rust.
I think that things like pin remapping or clock settings are to high level to be handled by device crate. SVD files just don't provide enough information. In the future it would be nice to have tool like CubeMX which generates project and initialization code for peripherals. And let this tool handle such conflicts. |
I don't know. Sounds complicated to manage. Too many features to reason about as // stm32f103xx
#[cfg(peripheral = "usart1")]
pub const USART1: Peripheral<..> = ..;
#[cfg(peripheral = "spi1")]
pub const SPI1: Peripheral<..> = ..; // stm32f103xx-hal
extern crate embedded_hal as hal;
extern crate stm32f103xx;
struct Serial1<'a>(&'a stm32f103xx::USART1);
impl hal::Serial for Serial1<'a> { .. }
struct Spi1<'a>(&'a stm32f103xx::SPI1);
impl hal::Spi for Spi1<'a> { .. } // app
extern crate stm32f103xx_hal;
fn main() {
// only uses Serial1
} Here when building Of course you can make this work if you add #[cfg] attributes to the
Yeah, I think we should not do it that way. Main reason being that this In general, we should try to reduce the number of types that mean the same thing
I agree. I was wondering if that tool / mechanism could also solve the problem Another idea to deal with this: we leave the stm32f103xx crate as it is, // stm32f103c8
extern crate stm32f103xx;
pub use stm32f103xx::{TIM1, TIM2, TIM3, TIM4};
// not available, don't re-export
// pub use stm32f103xx::{TIM6, TIM7};
// .. With this approach we only have to implement the HAL traits for the general |
I didn't even thought about it (still very new to rust). For me separate crates for each device worked so well simply because I used device crates directly in my application. Am I understanding right that TIM2, TIM3 and TIM4 are also different types, even though they are functionally identical? And not only in the same device, they are the same in F101-F107 families (and maybe even in some others). I will look into embedded-hal and blue-pill crates to understand how you are dealing with this problem.
In this case crate stm32f10xxx could make more sense. As far as I know peripherals in all 100-107 families are compatible. Even if there are differences SMT tries their hardest to not cause conflicts. For example look at the description of RCC_APB2ENR register in stm32f100 and stm32f103 reference manuals. Configuration bits for the same peripherals are at the same positions. And if some peripherals are not present in given family at all - corresponding bits are marked as reserved (accessing them will not cause any errors and they will always read as 0). Sadly, looks like I am not experienced enough in rust to choose the best solution. But will help with what I can. |
They are different types but they provide the exact same API because they deref to the same type: fn foo<T>(tim: &T) where T: Deref<tim2::RegisterBlock> {
let tim2: &tim2::RegisterBlock = tim.deref();
tim2.cr1.write(..);
}
// all these work. TIM2 has type &TIM2, etc.
foo(&TIM2);
foo(&TIM3);
foo(&TIM4);
You probably should read about the "newtype pattern" as well.
This is tricky stuff. I'll open an issue in svd2rust to get more opinions / ideas on this matter. |
Thank you for the example.
It is good idea. More people will see the issue there. And looks like it belongs to svd2rust anyway. |
closing in favor of rust-embedded/svd2rust#122 |
I am opening the issue here but it applies to other device crates as well.
SVD file from which this library is generated describes the whole stm32f103 family. But not every peripheral available in all devices. For example basic timers are present only in high-density and XL-density line.
So to catch errors like usage of unavailable peripherals and to make library smaller I made separate SVD file for each type of microcontroller that I using. I even removed configuration bits for GPIO pins that are unavailable on given package. And it works surprisingly well. Now a program that tries to use unavailable peripherals or pins simply will not compile. It makes porting to different device much easier.
I would like to share my work but don't want to create clutter with crates like "stm32f103c8". Maybe better approach will be to somehow combine different variants of API into single crate with ability to select specific microcontroller? Is it possible to use features for that?
The text was updated successfully, but these errors were encountered: