-
Notifications
You must be signed in to change notification settings - Fork 232
Refactor PWM for simpler generic use #256
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
Conversation
r? @eldruin (rust_highfive has picked a reviewer for you, use r? to override) |
@@ -1,5 +1,7 @@ | |||
//! Pulse Width Modulation | |||
|
|||
use core::time::Duration; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have not used this so far. How good does it play with embedded-time
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no idea, but, it avoids an external dependency / seems like it should be viable to have embedded-time play well with this?
/// Type for the `duty` methods | ||
/// | ||
/// The implementer is free to choose a float / percentage representation | ||
/// (e.g. `0.0 .. 1.0`) or an integer representation (e.g. `0 .. 65535`) | ||
type Duty; | ||
|
||
/// Disables a PWM `channel` | ||
fn try_disable(&mut self, channel: Self::Channel) -> Result<(), Self::Error>; | ||
fn try_disable(&mut self, channel: usize) -> Result<(), Self::Error>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this is simpler, it does away with having names for channels, which may be more descriptive when reading the code.
Also, this introduces the necessity for a new runtime error: WrongChannel
(or similar), which is unnecessary when the channel is an enum
or so. It would be nicer if this could be enforced at compile time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right, the idea of this is that an abstract Something<T: Pwm>
can drive N channels using an arbitrary PWM driver, which is somewhere between difficult and impossible using the current system because the abstract thing does not (and should not) know about the associated Channel
type. with this approach a Pwm
consumer can check the number of channels and address them as required, independent of implementation.
Thank you @ryankurte for working on this! I appreciate that you're making a concrete proposal here. That said, I don't like it :-) I do have the following problems:
(Side note: The conflict between Aside from these big-ticket criticisms, I'm wondering if embedded-time would be better than Regarding the replacement of
I think this is pretty obvious, but I wanted to spell it out. All those type parameters and associated types exist for a reason. Yes, they are a pain to build abstractions on top of, and we pay a price in complexity, but the alternative might be to make embedded-hal less useful as a general abstraction. (And as a side note, personally I wouldn't mind if all number types were I'd like to provide a counter-proposal that I think is more flexible and more robust. It would completely replace the current traits. This is also only a draft, and a quick one at that. I haven't thought all aspects through (we might want to support type state for enabled/disabled channels, for example, which this proposal does not). And I haven't had a chance to try this in real life yet. trait EnableChannel {
type Error;
fn try_enable(&mut self) -> Result<(), Self::Error>;
fn try_disable(&mut self) -> Result<(), Self::Error>; First, a separate trait for enabling/disabling channels. A HAL that wants to support one type per channel, can implement this directly on the channel type ( trait Period {
type Period; // or embedded-time, or `core::Duration`, I don't know
fn try_set_period(&mut self, period: impl Into<Self::Period>) -> Result<(), Self::Error>;
fn try_get_period(&self) -> Result<Self::Time, Self::Error>;
} On the hardware I've seen, the period can be set once per timer and affects all channels of that timer (which is represented in the current traits). So most likely, HALs would implement this on timers directly ( trait Duty {
type Error;
type Duty;
fn try_set_duty(&mut self, duty: impl Into<Self::Duty>) -> Result<(), Self::Error>;
fn try_get_duty(&self) -> Result<Self::Duty, Self::Error>;
fn try_get_max_duty(&self) -> Result<Self::Duty, Self::Error>;
} Like the So much for the traits themselves. Now my thoughts on how to abstract over them. I'll use drivers for motor controllers as an example, as that's what I've been thinking about recently. Example 1: DC Motor Driver I think for a DC motor, you just need to control the duty cycle to control speed. So the driver would just take ownership of a
If, for some reason, you can't separate timers and channels (my Example 2: Stepper Motor Driver To a stepper motor driver, the duty cycle doesn't matter much. It needs control over the period. One option is for the driver to require an let motor1 = StepperDriver::new(timer1, channel1_1);
let motor2 = StepperDriver::new(timer2, channel2_1); Another option is for the driver to require one single type that implements both let timer_and_channel = TimerAndChannel::new(timer, channel1); // API is up for debate
let timer_and_channel = channel1.upgrade(timer); // maybe this instead; doesn't matter to embedded-hal anyway
let motor = StepperDriver::new(timer_and_channel); As I said, this is just a quick draft. I'm going to have need for solid PWM traits in the near-ish future (timeline is a bit unclear; it's a side project and I got lots to do), including traits that abstract over DMA-powered PWM. So unless someone preempts me with another solution, I will likely try to implement my proposal on both the driver and HAL sides. Elsewhere I've floated the idea of doing this in an embedded-pwm crate, which would take away some burden from embedded-hal (allowing for a 1.0 release that much sooner) and merging the traits back into embedded-hal, once matured. |
swap to concrete types for Duty and Period to support boxing / dyn dispatch of different types
thanks for looking at it, all fair criticisms. the key point to me is to address the use of generic traits by consumers, though i am sure there are many ways to go about this and happy with any that do adequately provide abstraction over
yeah, i didn't intend to address this initially. splitting the traits as you suggest seems reasonable, though the
keeping the
once embedded-time has stabilised and been adopted here this may be a good option, however, for now this means we avoid an additional (unstable) dependency and it'll just work (tm) with
i have wondered about this too, we could remove all the unfinished / blocked traits from the
absolutely, the problem is that every associated type that isn't usefully generic breaks the abstraction, and any additional complexity either in or due to our As it stands, it is currently impossible to write a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, I've taken a look at the new commits and left some comments.
Overall, I think this new version concerns itself too much with matters that have always been out of scope of embedded-hal, namely initialization and HAL architecture. Other modules just provide I/O traits. How to provide those traits is up to the HAL, and I think that's for the better. I believe my own proposal is very much in the spirit of that.
Providing all these traits that have redundant methods, and telling HAL authors when to implement each, is just muddying the waters, in my opinion.
/// Disables a PWM `channel` | ||
fn try_disable(&mut self, channel: usize) -> Result<(), Self::Error>; | ||
|
||
/// Enables a PWM `channel` | ||
fn try_enable(&mut self, channel: usize) -> Result<(), Self::Error>; | ||
|
||
// Note: should you be able to _set_ the period once the channels have been split? | ||
// my feeling is, probably not |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this question is misplaced here. The answer is "whatever makes sense for the target hardware", meaning it is for HAL authors to answer. embedded-hal should provide the building blocks for HAL authors, but not stand in their way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i can see this on either side, agreed the key focus is usability but both for hal authors and consumers. imo it'd be pretty unexpected if you split your PWM
to use in two separate places and one of the altered the operation of other, and should you for some reason have two pins from two PWM
s passed into a driver the operation of set_period
is going to be fairly non-obvious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that there are potential problems, and I haven't thought this through yet. I don't know what the right solution would be, and whether there are valid use cases for changing the period of split channels.
I'm saying that embedded-hal should be careful about pre-deciding such questions. Defining these interfaces is already hard enough (because there always seems to be some use case or hardware capability/constraint that wasn't considered). I can totally see adding such rules later, as experience with implementing and using these traits grows (which would be a breaking change, strictly speaking; another reason why I'm concerned about the current push for 1.0).
/// ``` | ||
// unproven reason: pre-singletons API. The `PwmPin` trait seems more useful because it models independent | ||
// PWM channels. Here a certain number of channels are multiplexed in a single implementer. | ||
pub trait Pwm { | ||
pub trait Pwm<Duty> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering about this change. In my experience, type parameters are much more annoying to abstract over than associated types. It makes sense, if we expect HALs to implement this trait multiple times per timer, for u8
, u16
, f32
, etc., but I don't know if that makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is consistent with the manner we have implemented spi. Duty
could be a primitive type reflecting the word-size of the underlying timer, implemented once for each HAL (addressing #226). abstract consumers would require more complex bounds to support arbitrary duty types, but, this is somewhat inevitable for any abstraction and somewhat simplified by the use of a concrete type parameter vs. associated type w/ bounds ime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is consistent with the manner we have implemented spi.
Duty
could be a primitive type reflecting the word-size of the underlying timer, implemented once for each HAL (addressing #226).
If it's implemented once per HAL, then it should be an associated type. SPI (and serial) traits have this design, because it's reasonable to implement them multiple times per peripheral, as peripherals generally support multiple word sizes (I've done this in several HALs).
If there's one implementation-defined value of Duty
, it should be an associated type. Associated types are less cumbersome to abstract over, in my experience, as they don't have to show up as a type parameter on the methods or impl blocks of the driver.
/// Number of available channels | ||
const CHANNELS: usize; | ||
|
||
/// Maximum duty cycle value | ||
const MAX_DUTY: Duty; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering how much good that will do. Any driver built on top of this still won't know what Duty
actually is, so what can it do with this constant? Maybe we should have a duty Duty
trait that is implemented for any type that provides certain operations (division, multiplication)? Then it would be pretty straight-forward to do something like set_duty(MAX_DUTY / 2)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a driver built on this has to know what duty is (or have a bound supporting conversion to duty) or it can't call any of the methods?
it would be possible for drivers to impose num_traits
bounds on Duty
if required, or to directly implement over <u16>
and <u32>
if those are all the duty types we see in practice. is there a case in which it's useful to a HAL to implement multiple Duty
instances or could this be simplified by assuming Duty is a timer-defined primitive?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a driver built on this has to know what duty is (or have a bound supporting conversion to duty) or it can't call any of the methods?
I can imagine cases where all duty values are passed into the driver by the consumer, but even if that is a real-word use case, it would be limited.
it would be possible for drivers to impose
num_traits
bounds onDuty
if required,
I agree that this is totally reasonable. This is another point where I'm concerned about stabilization though. If it turned out that most drivers require similar bounds, and HALs can provide them (in the end, duty is just a number after all, or isn't it?), we could later add those bounds to embedded-hal, to improve interoperability.
With 1.0 out, we could lock ourselves into a non-optimal situation, where most drivers require some bounds, but not always the same ones, and not all HALs provide them (if the duty type were a newtype, and not u16
/u32
directly, for example).
or to directly implement over
<u16>
and<u32>
if those are all the duty types we see in practice.
I think this would be an unfortunate situation. It's fine for SPI/Serial, as there's a standard type that every HAL provides in practice (u8
), but I don't see such a standard type here (as timers have different resolutions). Not sure what the best solution here is.
is there a case in which it's useful to a HAL to implement multiple
Duty
instances or could this be simplified by assuming Duty is a timer-defined primitive?
I don't know. If stabilization weren't a concern (and it wouldn't be, if we had embedded-pwm), I'd suggest to assume that Duty
is timer-defined, make it an associated type, and wait for the complaints to roll in.
P: Into<Duration>; | ||
|
||
// Note: technically there could be a `channel` or `split` method here but this is | ||
// rather extremely difficult prior to const-generics landing (and CHANNELS would need to be a const generic) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we shouldn't concern ourselves with split
here. Initialization and configuration have always been out of scope for embedded-hal, and I think that's for the better. Just provide I/O interfaces that can be provided by HALs and consumed by drivers. Setting those up is the user's responsibility, and probably too device-specific to be abstracted over in a zero-cost/unopinionated way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah this is fair, we do have to communicate the expectations around split'ing to hal implementers tho, so if there's any opportunity for us to smooth that experience it seemed worth a look? can't do till const-generics and mostly agreed about not being opinionated about this just, exploring options.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can see a future where the design space is generally much better understood, and where adding such constraints makes sense. I don't think we're there yet.
+1 to exploring options though!
I don't think a
embedded-time can work with You said "for now", and I totally agree that
The fact is, there are interfaces in embedded-hal which are not ready (otherwise we wouldn't be able to have this discussion), and the goal of the working group (as far as I know) is to release embedded-hal 1.0. These two points are in conflict, and there is some price we have to pay to resolve this conflict:
It's not my call to make, but out of these I think option 3 is the only unacceptable solution. My fear is that the desire to avoid option 2 will lead to option 3, which is why I favor option 1. |
hey thanks for all the reviewing! again i don't really intend to land this just, think it useful to explore the problem space. probably be good to do some impls / drivers to prove any approach but, doesn't seem worth the time without more direction / consensus.
ohhhh, i always forget there's a
relates to #211, it seems to me this would also block |
Thank you again for the proposal and the discussion! Totally understood that this isn't intended to be the final solution. I hope I don't come across as too stand-offish. Like you, I'm interested in exploring the problem space and pointing to potential problems where I see them.
Yes, it would. No idea how pratical that would be in the near-term. Personally, I'd be fine chugging away with alpha release for the next few years, if that's what it takes. Given the overall push for 1.0 though, I'd imagine that would be a problem for some people. Hence my idea of taking problematic areas off the table, via an embedded-pwm and maybe other crates along those lines. |
After this whole discussion and some thinking, I looked at the current traits with fresh eyes again, and I think I've been wrong about most things I said about them:
With these new insights, I think that the current traits capture two different use cases and need to be kept. At least I can't think of a way to substantially improve upon them (modulo the details, like From my perspective, this leaves one thing wrong with the current traits: That abstracting over them requires a Based on my new understanding, I'm thinking that the changes made in this pull request are a step in the wrong direction:
I'd like to expand on that second point a bit:
|
replying to lots of comments here because, we're a bit everywhere 😂
yep, gotta be careful of all this eh. with i2c we've sealed the traits so
the resolution thing is interesting but, as a fundamental a timer has some count register width and as a low level abstraction it seems reasonable to me to bound duty to the appropriate width (like
oh, the other reason to do this is i believe (though may have missed something) on cannot have a
yeah passing a |
I think I said somewhere before, as far as I'm concerned, we could make it I'm going off of the stated design goals here. I think it's totally reasonable to want a simple abstraction layer that works well enough on common embedded platforms and is not necessarily zero-cost on all of them. In that case,
Interesting, I didn't know about that limitation. Short note: You say duty, but I think you're talking about the period. The maximum duty will depend on the timer period, which is runtime adjustable, while the maximum period is a constant value that depends on the timer resolution. |
yeah, i have conceptually combined period and duty on the basis that they're usually the same width (with the same maximums), which is the justification for a type parameter over this (which can be u32 or u16 or u8 as appropriate). perhaps this would be better termed |
Ah, understood, makes sense. |
closing now PWM has been removed, see #358 for reintroduction |
this is just a draft / proof of concept for discussion
related to complaints about existing PWM trait usability, this attempts to improve the
Pwm
trait for use as a generic in higher level applications / logic. the key consideration here is that associated types that vary by implementation should be avoided as inputs to minimize integration complexity, and this applies to roughly allChannel
based traits.Time
associated type, replaced withcore::time::Duration
Channel
associated type, replaced with associated constCHANNELS
andusize
indicesTryFrom<usize>
bound, but, we're getting more complicated againQuestions:
Duty
associated type?From
bounds for useful intermediates such asf32
so consumers could leaveDuty
unbound but consistently pass inf32
or other useful arguments.&dyn
case, which is useful but maybe not critical