Closed diondokter closed 10 months ago
Great initiative, the things you showed in your talk looked super interesting!
/// NOTE Using this can conflict with high-level functionality. Make sure not to break any assumptions that the crate makes.
What does this mean?
What do you think is better? Macro's, generation or maybe something else?
Both have merits. Macros would probably be a bit less expressive (maybe), may increase compile time(?), and often breaks autocompletion in rls/rust-analyser. On the other hand, a file based approach would need to generate a new crate, be run separately from the compiler, probably. It would make it easier to re-use the data for other purposes though...
The API to access registers can be the same as the API used in the PAC crates for the Arm MCU's. (See embedded book)
Quick thought: the PAC interface is neat, but the high amount of zero cost abstraction can be annoying in debug mode. I believe there is a project for writing another PAC-like interface that uses less "expensive" zero cost abstraction, but I can't remember the name right now, peripheral something something.
Might be worth looking into though.
I am thinking of writing a storage embedded hal trait. The current interface in my head is read(usize) -> Word
, write(usize, Word)
, read_slice(usize, [Word])
, write_slice(usize,[Word])
. So I would think the memory space API would want to use something similar.
I think a generic Streaming API is probably in the too hard basket, I think there is more value in dealing with the actual data like ATAT does for AT devices or a GPS format crate.
Some kind of Sleep trait would be nice too. But it is also very device specific.
I don't really like the high level traits here to be honest, for example the power trait.
/// This device can be powered on and off
pub trait PowerManagedDevice : Device {
fn power_off(&mut self) -> Result<(), Self::Error>;
fn power_on(&mut self) -> Result<(), Self::Error>;
}
forces the developer away from using type states for this transition, in case a certain set of functionality is only supported for the chip as long as it is powered down/up. While we can represent that with type states and PhantomData without this trait in order to enforce that this driver is not misused by the consumer when we handle this sort of state transition via this trait that seems impossible to me. Same goes for stuff like the reset() function of
/// General Device trait
pub trait Device {
type Interface;
type Error;
/// Create a new instance of the device with the given interface
fn new(interface: Self::Interface) -> Result<Self, Self::Error>
where Self: Sized;
fn reset(&mut self) -> Result<(), Self::Error>;
}
Maybe some people would instead like to change a type state that indicates the chip is in its reset state instead of
I have to agree with @hargoniX on this - when we force device driver crates to conform to some strictly defined interface, it can make it hard to adopt. For example, often-times, a device driver may want to take in use-once GPIO when instantiating a device (e.g. tied to reset lines or asynchronous edge trigger signals) to bring the device up from a cold boot. With the proposed Device::new()
, that's not really possible. I think in general, we should try to implement this while imposing the minimal amount of requirements on the driver developer.
/// This device can be powered on and off
pub trait PowerManagedDevice : Device {
fn power_off(&mut self) -> Result<(), Self::Error>;
fn power_on(&mut self) -> Result<(), Self::Error>;
}
/// This device can be put to sleep
pub trait SleepManagedDevice : Device {
type SleepMode;
fn sleep(&mut self, mode: Self::SleepMode) -> Result<(), Self::Error>;
fn wakeup(&mut self) -> Result<(), Self::Error>;
}
In regards to power states, devices can often have a multitude of powerstates, such as a radio that is in a low-power polling mode (WOR), but is not completely powered off. I think power state management is a bit complex here. I would also start off simple here and start with just the register API and work up from there.
What do you think is better? Macro's, generation or maybe something else?
I would go for the macro approach personally, due to the following:
I think it is worth mentioning register-rs here.
To summarize what I've heard so far:
/// NOTE Using this can conflict with high-level functionality. Make sure not to break any assumptions that the crate makes.
What does this mean?
The high level code may set a register to a certain value and expect it to stay that way. If you then change it without the high level code knowing about it, then it could start malfunctioning. I do feel you'd still want access, though, because not all high-level layers are complete and you may need other functionality that hasn't been implemented.
Quick thought: the PAC interface is neat, but the high amount of zero cost abstraction can be annoying in debug mode. I believe there is a project for writing another PAC-like interface that uses less "expensive" zero cost abstraction, but I can't remember the name right now, peripheral something something.
You can easily optimize the crates you're using from debug mode by adding this to your Cargo.toml:
[profile.dev.package.my-chip-driver]
opt-level = 3
Memory space API
I am thinking of writing a storage embedded hal trait. The current interface in my head is
read(usize) -> Word
,write(usize, Word)
,read_slice(usize, [Word])
,write_slice(usize,[Word])
. So I would think the memory space API would want to use something similar.
It could be nice for this library to use that trait. Then any library expecting some storage could use devices that use this toolkit.
Streaming data API
I think a generic Streaming API is probably in the too hard basket, I think there is more value in dealing with the actual data like ATAT does for AT devices or a GPS format crate.
You're probably right. I'm hoping someone has a good idea for this. Otherwise, these kind of devices may simply be out of scope to provide low level api generation for.
Sleep
Some kind of Sleep trait would be nice too. But it is also very device specific.
In regards to power states, devices can often have a multitude of powerstates, such as a radio that is in a low-power polling mode (WOR), but is not completely powered off. I think power state management is a bit complex here.
Yes, very specific. But I think it's possible to generalize. If anything more specific is needed, then the user would simply not call it through the trait API, but via the crate's custom high-level layer.
I have to agree with @hargoniX on this - when we force device driver crates to conform to some strictly defined interface, it can make it hard to adopt. For example, often-times, a device driver may want to take in use-once GPIO when instantiating a device (e.g. tied to reset lines or asynchronous edge trigger signals) to bring the device up from a cold boot. With the proposed
Device::new()
, that's not really possible. I think in general, we should try to implement this while imposing the minimal amount of requirements on the driver developer.
I agree as well. The traits proposed here are only to give an idea of the direction I see this going. Defining good ones will be the most challenging part of this crate. For example, there should be a way with extra pins like the reset pin.
Quick thought: the PAC interface is neat, but the high amount of zero cost abstraction can be annoying in debug mode. I believe there is a project for writing another PAC-like interface that uses less "expensive" zero cost abstraction, but I can't remember the name right now, peripheral something something.
I think you're thinking of https://github.com/adamgreig/stm32ral which provides a very lightweight and quick to compile macro-based interface which looks like:
use stm32ral::{modify_reg, gpio};
let gpioa = gpio::GPIOA::take().unwrap();
modify_reg!(gpio, gpioa, MODER, MODER1: Input, MODER2: Output, MODER3: Input);
The crate provides modules for each peripheral and register which contains constants for values you might write to the fields. The macro is expanded into a simple mask-and-shift expression, so it produces small and fast code even without any compiler optimisations turned on, which makes debugging a lot easier too. Personally I prefer the syntax compared to the closure-with-method-chaining of svd2rust, too.
Memory space API
I am thinking of writing a storage embedded hal trait. The current interface in my head is
read(usize) -> Word
,write(usize, Word)
,read_slice(usize, [Word])
,write_slice(usize,[Word])
. So I would think the memory space API would want to use something similar.It could be nice for this library to use that trait. Then any library expecting some storage could use devices that use this toolkit.
I will try to make some progress in the next week or 2 then.
Thanks. My brain immediately saw a pointer+length like in C :)
There is also this https://github.com/jonas-schievink/spi-memory which might be a good playground to try some approaches
A device trait like this would assume that a device can only use one interface:
pub trait Device {
type Interface;
// ...
}
In order to nicely allow for providing multiple interfaces the trait would have to look more like this:
pub trait Device<Interface> {
// ...
}
impl Device<I2c> for Foo42 { … }
impl Device<Spi> for Foo42 { … }
(Your talk's code matches the latter, for what it's worth)
(Your talk's code matches the latter, for what it's worth)
You're quite right! Didn't think of that.
Something like a proc macro creating the interface from a yaml (or similar) description would be nice. 😅
For generating from yaml files I'd like to point at https://github.com/google/cyanobyte, as of now the "standard" they have is just I2C but since the file format is essentially only a YAML defined via a JSON Schema we should be able to fork it and add required changes (maybe even upstream them as cyanobyte was planning to look into SPI etc anyways?). Plus we'd already have a nice set of files in that repo we can easily create test drivers with and try them out against real hardware while developing this crate.
hey this is a super interesting project! i would love to have a better mechanism for describing registers / fields / etc. than the current write-it-all-out-by-hand approach. it's probably a bunch more primitive than what is proposed here but, i have been playing with similar abstractions across the few radio driver crates i have, trying to extract commonality and smooth the driver writing / maintenance situation.
it's all a little cursed due to the outstanding transactional SPI PR but, some links if you're interested: trying to abstract over underlying SPI / UART protocols [1] (though i haven't yet implemented the UART one), as well as common driver functions [2] [3] to simplify driver type bounds, providing blocking [4] and nonblocking [5] accessors over classical / c-style do/poll/done drivers, and hal abstractions to simplify writing utilities to test and interact with drivers [6].
It would also be pretty cool if we could have a separation of registers and protocols used to control said registers, for example OpenBSD has an IC directory containing register definitions https://github.com/openbsd/src/tree/master/sys/dev/ic as well as several bus specific directories such as I2C, SPI, PCI etc. in the directory above the IC one. I think this separation of bus and registers might be a pretty interesting thing to have, of course for code re-usage but also because if we have the register definition strictly separate from the bus specific implementation writing mock tests would be a lot easier for the developer.
As it seems like the acivity on this issue regarding new features we'd like to have has stopped for now. What I get from this is that there is definitely a desire to have 2 parts to this crate
Low level APIs inspired by PAC design that provide a unified way of accessing the information (in whatever form it should be) on the chip the driver is being written for. This lower level API should definitely be auto generated as well, just like a PAC. The biggest questions remaining I can see are:
Higher Level APIs The main point for the traits proposed here was "not generic enough" if I understood everything correctly.
In order to discuss these questions it might be reasonable to split the current thread up into a few more issues so we can discuss everything that's separate separately?
(If something is missing from the questions I collected feel free to bring it up of course)
Yes that's what I got as well from the responses. I've been working on and off to get some basic thing going to get feedback on.
For now it will be a normal macro that will help you create a device with registers. The high level stuff can wait for I think. For defining memory space, I'd like to use this when it eventually lands: https://github.com/rust-embedded/embedded-hal/pull/241
Don't know how soon I can finish it, but I'll have some time off in September which I hope proves to be productive for this.
What about async vs blocking?
I've released a first version! It only does registers for now.
https://crates.io/crates/device-driver
Try it out and look at the example if you want. If you have ideas, then please post them here! For specific problems, please open a new issue. For now I'll keep this open for general discussions.
Also, I don't have a lot of Open Source experience. So I'm open for process/style suggestions as well 🙂
Great news :tada: I'll look into this when I find time :)
Also, I don't have a lot of Open Source experience. So I'm open for process/style suggestions as well slightly_smiling_face
From my f103_hal experience, keeping a structured changelog is really good. For every new feature or fix, add a note to the changelog. Generally, this is much easier to look through than commit messages and makes upgrading particularly breaking changes much simpler.
This is the format we use: https://keepachangelog.com/en/0.3.0/
Hey everyone! I could use some feedback on this, so any is appreciated.
Goal of the crate
I've done a talk at the Oxidize conf about writing the 'best' device driver. There was some interest, so I want to see where this can go.
The goal is to make a toolkit so that writing device drivers will be easier and faster to do.
Some features it should have:
General structure
Every driver has to implement the Device trait. This is the base trait that signifies the high level driver interface. The functionality would also be implemented as traits.
Motivation for using traits like this (especially the power-related ones):
One could have a project where power efficiency is very important. You could then have an iterator over all devices and turn them all off when it's time to go into deep sleep.
It also falls in line with implementing the component abstraction traits.
Other functionality not covered by the existing traits can be implemented by the driver author. Though I expect this should only be high-level functionality like reading out a temperature value or parsing a gps string.
Creating low-level functionality
Writing the low level functionality can be a lot of work depending on the type of device. It could be simple like one memory space for an EEPROM, but it could also be 50 registers with 16 fields each.
So especially for registers, some help would be nice.
Register API
The API to access registers can be the same as the API used in the PAC crates for the Arm MCU's. (See embedded book)
My Oxidize talk was mainly about this. Here's an example of the code that can create such an API: gist. (scroll down to line 178 to see the API in action) However, implementing this by hand, like in the gist, is a lot of boring work. So there needs to be a means of being able to skip all the boilerplate and just focus on the register definitions themselves.
One solution is to create a macro. This is the solution I picked for a device driver I wrote. The syntax was something along the lines of this:
This works pretty well in my experience. This macro could also be expanded with reset/default values and other thing that will be found along the way.
Another option would be to use some kind of file-based definition. This has the advantage that we could maybe join other initiatives. One of which is CyanoByte. This could help even more in creating device drivers for Rust. However, CyanoByte does not seem to be a very active project, only supports I2C devices as of yet and uses YAML (which I personally don't like).
I don't know of any other such project. If you do, please let me know!
If we don't adhere to an existing standard, then we could do our thing and could maybe create a file structure in TOML.
What do you think is better? Macro's, generation or maybe something else?
Memory space API
I don't know what this should look like yet. Maybe you've got ideas?
Usually you've got either single byte/word access and/or (limited) streaming access where the address pointer automatically increments.
Streaming data API
Another common thing you see are devices that just push out data whenever it's got it. Usually this is done over Serial. An example of this is the L80 GPS that spits out some NMEA strings every second. This to me seems like the most difficult API to create because the data being streamed can be very different from device to device.
Conclusion
Do you see something I've not covered? Do you have opinions you'd like to voice?
Any feedback is welcome! Even if it's that the effort would be wasted and better spent somewhere else or that the ambition is too big to get right.
I'd love to get people to use this, but before they do, they need to like it.