-
Notifications
You must be signed in to change notification settings - Fork 182
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
Probably you should rethink API structure #114
Comments
Torvalds, is that you? |
Wow, that's some pretty fierce criticism...
https://github.com/rust-embedded/awesome-embedded-rust Lots of people have written libraries for this ecosystem, perhaps you can look there for inspiration. Anyways, while your issue comes off as a bit aggressive, your issues are still valid, so let's discuss them.
This might seem counter intuitive, but it has a large advantage when working with peripherals. It allows us to statically make sure that pins are only used once, and that the pins you intend to use are available. As an example, if you write the following code: // USART1 on Pins A9 and A10
let pin_tx = gpioa.pa9.into_alternate_push_pull(&mut gpioa.crh);
let pin_rx = gpioa.pa11; // PA11 can not be used with USART1
// Create an interface struct for USART1 with 9600 Baud
let serial = Serial::usart1(
p.USART1,
(pin_tx, pin_rx),
&mut afio.mapr,
9_600.bps(),
clocks,
&mut rcc.apb2,
); This will produce an error, something along the lines of In general, the API is designed to never compile unless a peripheral is properly configured, that includes all pins, their modes and all registers. You mention the arduino API, which I think is pretty decent, but I have no idea what this code would do: USART1.begin()
pinMode(PA10, Output)
This is indeed a problem which I've heard discussed quite often. I could be wrong about this as I didn't write the original GPIO code, but I believe this is related to ownership over registers. I think this is something we should look into fixing though. Also, in general, the whole embedded API is designed around generics rather than runtime resolution of things. I.e. if you want to use 3 pins in a function you have two theoretical options: // Prefer this
fn do_stuff<(P1, P2, P3)>(pins: (P1, P2, P3)) -> ...
where P1: OutputPin, P2: OutputPin, P3: OutputPin
{}
// Over this
fn do_stuff(pins: (GenericOutputStruct, GenericOutputStruct, GenericOutputStruct)) {} It's cumbersome to write libraries like this, but the resulting code will be faster, and rusts type inference will hide most of the generic types on the user side, especially if you use some typedefs.
This is also valid, and something I've run into myself a couple of times. I think it too is related to ownership over the CRL struct, but I don't see a fundamental reason that we couldn't safely write to the relevant part of the register if we have a PAx struct. After all, we write to a shared data register.
I would probably argue that this isn't an API issue, apart from some kinks like the one you mentioned. Instead, I think it is a documentation issue, because I know from experience that it's possible to write functioning software with it. Heck, the keyboard i'm writing this reply on is running on top of this API.
Ok, that's a long reply, hopefully it clears things up. I'm going to look into making the GPIO pins a bit easier to use in a generic way. Also, you're always free to submit PRs fixing the issues you find, after all, you can dive deep into code to understand how it works 😉 |
Just curious, what keyboard are you using? |
@TeXitoi Hello, I am a friend of TheZoq2. He built his own keyboard which explains why it is running code using a library he maintains. |
Any link to the code? I myself also write on a keyboard that use this library, so curious to know if that's the same code or if we can share the effort. Mine: https://github.com/TeXitoi/keyberon |
I agree that it would be great to have a downgrade method that is only 1 type, not one by gpio device. I have to do some hacky things to workaround that in a generic way here: https://github.com/TeXitoi/keyberon/blob/master/src/matrix.rs#L7-L99 |
The main repo is here: https://gitlab.com/TheZoq2/kthreeyboard_firmware. Keebrs is a fork of another keyboard project with some minor fixes. https://gitlab.com/TheZoq2/polymer is the board specific code. The workaround for the issues mentioned in this issue are mainly in https://gitlab.com/TheZoq2/polymer/blob/dev/src/matrix.rs Sidenote: with your keyboard project, there are three separate rust keyboard projects that i'm aware of :P |
Ok, that makes it 4. Here is the other one I know of https://github.com/TyberiusPrime/keytokey Edit:
Sharing the effort sounds like a pretty good idea. It would be nice to start something like QMK but rusty. I believe that is the intent of the keebrs project, but I'd be open to something else as well |
No, it just a normal way of conversation in my country 😄
Probably later when my internal masochist would wake up.
Whew. I knew that you are trying to do not sacrifice performance at any cost.
It is really complicated for me to imagine how can I create some kind of application where pin numbers configured via constants in some configuration block. For example as it was done in Marlin firmware.
IMHO arduino lib is not perfect, but probably having generic pin struct is good idea
I assume that you have put excessive "don't" in your notion and you've said that it is bad idea write into shared register, but using Result types is not so good option because you want it to be checked at compile time. But this also restricts you to write using "low level" abstractions.
Documentation could help, but there is a possibility that it can be cumbersome because of overall API structure.
I have no intend to submit PR with global refactoring, but I'll think about something.
Good for you. |
I wouldn't say at any cost, but when we can do something at compile time, we
Do you mean something like this? const int BUTTON_PIN = 5;
const int LED_PIN = 7;
void init() {
pinMode(STEPPER_PIN, Input);
pinMode(LED_PIN, Output);
} If so, you can achieve the same thing using "typedefs" type ButtonPin = PA5<Input<PullUp>>;
type LedPin = PA7<Output<PushPull>>;
fn main () {
// Here you would need to specify the pins again, i.e.
let led_pin = gpioa.pa7.into_push_pull_output(&mut gpioa.CRL);
}
// But everywhere else, you can just use the types
fn light_led(led: &mut LedPin) {
led.set_high();
}
// And if a function doesn't care about which pin is used, make it generic
fn light_any_led(led: impl embedded_hal::OutputPin) {
led.set_high();
} Is this kind of what you had in mind, or did I missinterpret what you said?
No, the don't is meant to be there. I believe we could create a better solution
This seems like a completely different issue from what you've talked about previously, and also something I've run into. Since you have another repo for communicating with DHT22 sensors, I pressume that's what you're talking about. I happen to have code for communicating with such a sensor here However, as I said, this is a separate issue and one that I am aware of, if we manage to Also part of the reason that InputOutput pins aren't implemented here is that |
This tone is absolutely not welcome here.
The problem is that if you do read-modify-write operations on registers you'll need to protect those. The only way to do that without locking is by taking a
Providing proper type erased pins without tons of overhead is something I've been trying to implement for STM32F0 for many days now. I haven't yet found a way to do it using just the type system... |
Oh right, I thought that was what we're doing when setting pin values, but I guess not. https://github.com/stm32-rs/stm32f1xx-hal/blob/master/src/gpio.rs#L163 Also, unless I'm missing something, you wouldn't be able to mix your proposed solutions with the current solution as you would need a lock around |
Well, you can do implicit locking in the implementation but that is frowned upon, especially by the RTFM guys. |
Yes right now I've stuck there. Looks like @TheZoq2 driver is not universal. It is created only over stm32f1xx_hal crate and can not utilize embedded_hal API only. That is why I was trying to create my hal only implementation.
In this case for now having structure that wraps PIN trait inside it is not possible for my case.
I'm trying to reach them and share my ideas.
Don't be so boring or you'll end up writing boring APIs 🌝 |
|
For the one interested in the keyboard crate discussion, I propose to go to TeXitoi/keyberon#14 to continue, as it is off topic here. |
A simple generalization of the downgrade function should be quite trivial, something like: enum Gpio<MODE> {
PAx(PAx<MODE>),
PBx(PBx<MODE>),
PCx(PCx<MODE>),
PDx(PDx<MODE>),
}
impl<MODE> OutputPin for Gpio<Output<MODE>> {
type Error = Void;
fn set_low(&mut self) -> Result<(), Self::Error> {
match self {
PAx(g) => g.set_low(),
PBx(g) => g.set_low(),
// ...
}
fn set_high(&mut self) -> Result<(), Self::Error> {
// ...
}
}
// ... Or I missed something? |
I also had that thought and I don't see a reason it wouldn't work. In fact, I'll go implement it now to see if it works |
Interesting. That sounds rather simple and might work. I take it you'd generate the |
Interesting related crate:https://docs.rs/enum_dispatch/0.1.4/enum_dispatch/ |
Is it possible to use atomc CAS (compare and swap) ARM instructions while working with gpio registers or something similar. Probably it would allow not to use &mut reference to registers structs. |
This is how I see generic solution in general 😕 Having some king of global singleton structure that controls gpio registers (AllGPIO) Each AnyPin contains reference AllGPIO. AnyPin can mutate itself by consuming self and calling appropriate methods from AllGPIO When pin mutates (changes its state) unsafe code with atomic CAS or "synchonized like" blocks used to change pin registers (http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dai0321a/BIHBFEIB.html). This may allow to modify pin in place without necessity to use &mut link to another structure, and having generic pin struct, and generic port controller. |
It would be great to remove this It may require additional feature on sdv2rust to have an atomic modify. |
IIRC, atomic CAS still adds a performance penalty because you essentially have to do this: loop {
let value = read_register();
let new_value = value.modify();
if cas(new_value) == value {break;}
} Some sort of atomic modify might also be nice. It would also probably require additional features in svd2rust, but perhaps that's worth investigating. Also, I think it may be worth implementing even if it has a performance penalty, I doubt a lot of code is doing enough GPIO toggles to really be affected.
I'm not sure I undestand the whole solution you're proposing, but it's certainly something we can investigate. |
Actually, I feel like doing atomic instructions might be a bit overkill. Can't we just disable interrupts around the AFAIK, there are no multi core f1 processors, so disabling interrupts should be safe. Also, the modify calls should probably be quite fast so I don't think we'd be adding that much extra interrupt latency. Also, I can't see a case where you would be switching pin modes very often. |
As a gpio pin "own" a subset of the bits of the CR, in theory, it can be done freely using the bit-band. |
I agree that CAS and critical section have a cost, but that it should not be a problem in practice. Intuitively, I'd prefer the CAS version to the critical section version, but that's just intuition. Bit-banding seems to be the solution, but will need some big modifications on svd2rust. |
User experience suffer update report. Well I was able to manage DHT sensors protocol in not so cool way, but it works. Fortunately it was because it have pull up resistor in module by default and stm32f1xx-hal supports open drain pins. But now I'm trying to deal with tm1637 and it is extremely hard to do somethin. tm1637 module does not have pull up resistor. From C++ you can see that in order to get ACK from chip pin have to be switched into pull up input mode for a while. And this is not possible with stm32f1xx-hal using HAL traits. Only one pin mode support Input+Output trait and it is Output. I think It will be possible to use this module when I add pull up resistor, but it does not look good solution because other API allows me to work without circuit modifications. Second thing I should notice is Delays & Timers AFAIK I can have only one delay or timer object and because HAL traits require &mut access it is not possible to pass references to several places where delay is used. Looks like I have to pass delay in each function and can not wrap it somewhere in struct as reference. This is really bad and sad and whatever ... |
I looked into writing a driver for the TM1637 a long time ago but dropped it because of all the bitbanging necessary. |
With #211 merged, I believe all actionable issues here have been fixed :) |
Hello everyone.
I'm developing software more than 10 years.
I've seen a lot of anti-patters, but your code really impressed me.
I can definitely say that this is worst API approach I've ever seen in my life.
Looks like you've never try to write anything except POC with your API.
I was trying to create some kind of library using embedded-hal and stm32f1xx-hal
and found out that it is not worth it.
You should understand that I'm a really stubborn developer and I can dive deep into code
to understand how it works.
So what is wrong with your code.
Everything in your type system is wrong.
Sometimes it is even looks like some kind of joke to me, the same as
bool x = true; if (x.toString().length() == 5) {}
Lets examine :gpio::gpiob::PB0 it is a separate struct that has same api as :gpio::gpiob::PB1
But PB1 is another struct so they can not be swapped in the code easily, you have to rewrite it.
Are you seriously thinking that defining separate types for any possible abstraction is good idea.
Also you've groupped pins into namespaces (probably is is somehow related to ports) and this is where really fun stuff begins.
Somehow deep inside your soul you are understand that using separate struct for each pin is bad idea.
So you've decided to make an abstraction via downgrade method that returns generic pin struct.
So now I'm able to downgrade PB0 to ::gpio::gpioa::PBx which is still bind to its namespace and it is not the same as ::gpio::gpioa::PAx.
Even generic pins in your API are not the same.
Next not so pleasant thing is pin function that mutates it to another "type" like
fn into_push_pull_output(self, cr: &mut CRL) -> PA1<Output<PushPull>>
.It is not bad to mutate to another abstraction on state change, but only if method signature is like
fn into_push_pull_output(self) -> PA1<Output<PushPull>>
.Expecting reference to second mutable parameter ruins everything.
My point here is that whole approach to API structure is wrong.
It is hard to understand and almost impossible to write flexible code.
But why we are using Rust here? We must remove complexity not adding new one.
Right now kernel GPIO API looks much more convenient than yours https://www.kernel.org/doc/Documentation/gpio/consumer.txt
Should you care about it?
Well I've suggest you too look into Arduino ecosystem. Arduino IDE is terrible and libraries system is half-work solution but it has Wiring with it's so called std library. Some devs clams that wiring is to high level and sometimes to slow comparing to C.
But it is simple, thus may devs were able to start their projects quickly.
stm32f1xx-hal should be more high level abstraction, for low level API we have cortex-m crates.
BTW I'm not only one who thinks that way nrf-rs/nrf-hal#8
The text was updated successfully, but these errors were encountered: