-
Notifications
You must be signed in to change notification settings - Fork 215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rework USART API to reduce macro usage #116
Conversation
For a following large-scale refactor, create a copy of the usart module to be referenced while working on the new implementation. Later, this copy will be removed again. This allows bisection of the history, even over the following changes.
It is showing more and more that generating the entire API for the HAL crates from macros is a bad idea. Reasons include: - It obscures the code. People have trouble finding where a certain type is defined, especially if they are not too well aquainted with rust macro_rules!(). - It breaks auto-completion in IDEs because they'd need to introspect the macros. - It leaves many types with the same methods instead of a single one. This also makes code that is generic over the exact peripheral impossible to write. - It makes it much harder to cleanly support different peripherals under a common interface. To mitigate these problems, shift to a different design: A single `Usart` type provides the public API and is generic over the USART peripherals which all implement a common interface called `UsartOps`. Only this trait implementation will be macro generated and thus there is no user-facing macro-generated API left. This design also makes it much easier to implement support for multiple different hardware peripherals: As long as the `UsartOps` are expressive enough to fit all of them, we can simply provide multiple macros for generating the implementation for each one.
Switch to the refactored USART implementation macro and define local type-aliases instead in all HAL crates. This does (for the most part) not lead to a breaking change.
Now that the refactor is over, the copy is no longer needed.
Reimplement the interrupt mechanism (former `interrupt_rxc()` and `interrupt_udre()` methods) ontop of the new API. While doing this, switch to a design that more closely resembles what is present in other rust-embedded HALs.
Add back the `.split()` method for separating the USART peripheral into a reader and a writer half.
833ccef
to
05afea5
Compare
This part is still a big question mark for me. Right now, user-code could trivially do stupid things like use avr_hal_generic::usart::{self, UsartOps};
let mut usart = dp.USART0;
usart.deinit();
usart.interrupt(usart::Event::RxComplete, true);
usart.flush(); This obviously invalidates all safety mechanisms and needs to be prevented. My original idea was to just make the functions in Ideally, we can find a way to make those functions "private" in some sense, but I am not quite sure how. The trait must be public because downstream code might want to be generic over it: fn takes_any_usart<USART: UsartOps<RX, TX>, RX, TX, CLOCK>(any_usart: Usart<USART, RX, TX, CLOCK>) {
// ...
} |
@Rahix Could a possible solution be to create a type within the macro that implements UsartOps. Something like I've hashed together here: https://github.com/chris-ricketts/avr-hal/blob/adff98e9d38d2224d3a0e4e6915bc0be4c397734/avr-hal-generic/src/usart.rs#L423 This would expand to: // Excuse the underscore - I didn't realise the macro created a `Uart1<..>` type alias
pub struct _Usart1 {
peripheral: crate::pac::USART1,
}
impl core::ops::Deref for _Usart1 {
type Target = crate::pac::USART1;
fn deref(&self) -> &Self::Target {
&self.peripheral
}
}
impl core::ops::DerefMut for _Usart1 {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.peripheral
}
}
pub fn usart1<CLOCK>(
peripheral: crate::pac::USART1,
rx: portd::PD2<
::avr_hal_generic::port::mode::Input<::avr_hal_generic::port::mode::Floating>,
>,
tx: portd::PD3<::avr_hal_generic::port::mode::Output>,
baudrate: ::avr_hal_generic::usart::Baudrate<CLOCK>,
) -> ::avr_hal_generic::usart::Usart<
_Usart1,
portd::PD2<::avr_hal_generic::port::mode::Input<::avr_hal_generic::port::mode::Floating>>,
portd::PD3<::avr_hal_generic::port::mode::Output>,
::avr_hal_generic::usart::Baudrate<CLOCK>,
> {
let usart = _Usart1 { peripheral };
Usart::new(usart, rx, tx, baudrate)
} This would meant that a downstream user doesn't have ready access to a type that implements UsartOps - they'd have to explicitly create It would mean creating the uart like: let serial = hal::usart::usart1(
dp.USART1,
pins.d0,
pins.d1.into_output(&mut pins.ddr),
57600.into_baudrate(),
);
// instead of
let serial = hal::usart::Usart1::new(
dp.USART1,
pins.d0,
pins.d1.into_output(&mut pins.ddr),
57600.into_baudrate(),
); EDIT:
But there could be a higher level Usart Trait that users could be generic over whilst also eliding the pub trait Serial { ... }
fn takes_any_usart(any_usart: impl Serial) {
...
} |
I think this idea does look promising, with a few further tweaks this might work quite well. I'll have to play around with it a bit more ... |
OTOH: We're not gaining much by hiding this: Downstream code can already do lots of stupid things safely because the API generated by |
Maybe we can define FSM? https://docs.rs/rust-fsm/0.4.0/rust_fsm/ |
@Rahix is it possible to help you to push forward this? |
Hey @explicite, sorry for the inactivity here ... I hope I can allocate some time next weekend to get this resolved. I did some work in other HAL layers (for other MCUs) in the meantime and I think I now got some new ideas which could be applied here as well. |
The methods of `UsartOps` aren't meant to be used directly. Reflect this by prefixing their names with `raw_` and adding a warning in the documentation. In the future we might want to reconsider this design to actually hide these methods from the user entirely.
Okay, I've decided to just use a simple solution for now: The methods are now named I'll merge this state and we'll do any future changes ontop of it later. |
https://rust-embedded.github.io/book/static-guarantees/index.html pinned here for further reading |
It is showing more and more that generating the entire API for the HAL crates from macros is a bad idea. Reasons include:
To mitigate these problems, shift to a different design: A single
Usart
type provides the public API and is generic over the USART peripherals which all implement a common interface calledUsartOps
. Only this trait implementation will be macro generated and thus there is no user-facing macro-generated API left.This design also makes it much easier to implement support for multiple different hardware peripherals: As long as the
UsartOps
are expressive enough to fit all of them, we can simply provide multiple macros for generating the implementation for each one.As the diff in this PR is quite unreadable, please look at the new API here: https://github.com/Rahix/avr-hal/blob/refactor-usart/avr-hal-generic/src/usart.rs
Remaining work:
.split()
API).UsartOps
from downstream code or make it unsafe to access.UsartOps
for ATmega4809 (Arduino Nano Every support #97, ATmega4809 support #101).