-
Notifications
You must be signed in to change notification settings - Fork 949
Proposal: PWM abstraction changes to match hardware #932
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
Comments
Thank you for moving this forward! I've looked a bit into various PWMs and here is what I found:
So that's it for the hardware. The architecture usually appears to be of a single counter that counts in various ways (up, down, up and down for "phase correct" or "center-aligned" PWM) at a configurable frequency with some prescaler. A PWM usually has about 4 channels that can then be configured in various ways. Usually you can set a value in a channel, which roughly corresponds to the duty cycle (but the actual duty cycle depends on the counter top and timer-dependent bit width). If you can, please look into the PWM/timer peripherals of other chip families (stm32, NXP, etc) and include them in this thread to make sure the design caters to as much hardware as possible. Thinking about this, it might make sense to not design a universal "PWM" peripheral but rather a "timer" peripheral that also supports PWM operations, as that is how it is sometimes called in hardware already. And in a way, it is in fact a timer with extra PWM channels. |
Then there is the other end: the users of this API. This is also very important to summarize before doing a PWM design. Dimmable LEDs are fairly trivial: LEDs do not require precise control over frequency. The main tradeoff for LEDs is color depth (how many brightness levels it supports) versus PWM frequency (should be as high as possible to avoid flicker). I've also used the TCC in an atsamd51 for my hardware assisted hub75 driver, to enable the display for a very precise time (basically it needs to be low for a very specific time too short to do with normal GPIO operations). It would be nice to have such support included in this API, but because it deviates significantly from the regular use, I'm not sure whether it cleanly fits. I don't know much about other hardware, #855 (comment) has a great summary of hardware requirements. And as always, simple things should be kept simple while complicated things should remain possible. Here is what a simple usage of an API could look like: // in the board-specific file e.g. itsybitsy-m4.go
var channel machine.PWMChannel
func init() {
pwm := machine.PWM0
pwm.Configure(machine.PWMConfig{}) // default to some sensible frequency for LEDs
channel = pwm.Channel(0)
}
// in the main.go file
func main() {
for i := uint32(0); ; i += 0x1000 {
channel.Set(i)
time.Sleep(time.Second/2)
}
} That's not as simple as I hope it could be. Some remarks:
It would be nice to expose extra options, which gets messy quickly when you pass a slice to the @gwtnz @alankrantas it would be great if you can give some feedback on what specific hardware demands from such a PWM API. |
I believe the nRF52 has both timer and pwm peripherals. Which I guess could both be supported by some common "timer" implementation. But it definitely keeps things confusing! Another option would be a defined
This is the challenge! And it is very hard to do without detailed knowledge of every supported machine - there definitely big gaps in my knowledge.
Good point - it probably makes more sense to have a |
I probably know much less about hardwares than you all do. It just that buzzers and servos are common in education materials of Arduino, micro:bit and ESP8266 alike. micro:bit's PWM is software-simulated (3 max at a time) and probably so is ESP8266 (MicroPython). For me, it's totally acceptable that you can only set one frequency across the board at a time (a separate timer you mentioned, is that it?), software PWM or not. That's how micro:bit and ESP8266 behaves. We simply don't mix buzzers and servos. There's not so many occasions to do that anyway. But it's important that you can change the frequency (in Hz, integer) at any time. I also hope that the API can be as simple as how MicroPython or CircuitPython do it. It's not good for teaching if it gets too complicated. |
The micro:bit (nrf51) doesn't have a dedicated PWM peripheral but it definitely has hardware support. The esp8266 on the other hand doesn't seem to have any hardware support at all so it's hard to get a stable PWM output from it because everything must be done in software. I totally agree the API should be as simple as possible. But I think limiting to just one frequency would make certain use cases impossible. Also: one PWM instance may only support a limited number of pins so making use of all PWM instances will be necessary. If you have any concrete API please write it down. It's best to have many ideas to choose from and pick the best parts. |
Well, it is claimed that micro:bit uses software PWM. Maybe it's a combination of hardware and software (here may have some info). I can't say I really know the answer. But if you tried to output 4th PWM signal in MakeCode the first PWM (on whatever pin) would simply stop. I don't know what concrete API means; If you means what do I hope it would look like, it's probably like this: machine.InitPWM()
pwm := machine.PWM{pwmPin}
pwm.Configure(machine.PWMConfig{duty: 32767, freq: 1046})
pwm.Configure(machine.PWMConfig{duty: 32767}) // set to default frequency
pwm.Configure() // set to default frequency, duty = 0?
pwm.Set(32767) // set duty cycle
pwm.SetFreq(1046) // set frequency in Hz (either this pin only or across whole board)
pwm.Deinit() // stop PWM (set duty and freq to 0?) Also each board may have different available frequency range. |
As @aykevl and @syoder have mentioned, getting the configs right across different boards / chips is going to be tricky. I'm thinking an interface-based approach (as opposed to a struct-based one) might be the way to go, such that we could define the types Something like (very similar to the posts above and Arduino etc libs; I'm still pondering how to pass the more specialized data into the generic function calls):
or maybe:
IMHO the API would be at the simple level (a la the Arduino library equivalent, easy to get started with), and the implementation would allow for the complex level of managing timer channels, interrupts etc. This is definitely going to need a bit of thought; looks like the ESP8266 is pretty much out for raw servo control because of that single (known?) timer; it's performance just doesn't look sufficient; even LED work will have to balance frequency with resolution. But something based on the PCA9685 can provide I2C servo control off-chip (though if you have to add another chip, you could just get another microcontroller 😄 On the other hand the stm32 chips are bristling with timers, channels, config options and pins to output them on, and plenty of clock cycles to give the developer choices for frequencies, so almost any conceivable pulse train setup is possible... It may be that tinygo just plain can't offer features on some of the boards/chips if they can't support it; we probably shouldn't limit the API based on that, but maybe can offer the closest available thing, e.g. no servo control but at least rudimentary PWM on the 8266, similar to the MicroPython lib listed above. |
periph.io has an interesting example here - there's just a duty cycle and frequency; a servo would be a variation of this with a frequency of 40-50 (Hz), and a duty cycle that ranged between ~2.5% and 12.5% (.5ms-2.5ms, 20ms total for 50Hz freq), though if a servo type was included it would be more intuitive to just have a 0-255 or 0-180 value for the setter but could still call down to the PWM method with an appropriate percentage of the duty cycle:
assuming fractions of a percent were available. This would still leave all the quirky timer setup code hidden away nicely. |
I sometimes use a servo function like this in MicroPython: def servoWrite(pin, degree):
actual_degree = int(degree * (122 - 30) / 180 + 30) # convert 0-180 degree to duty cycle (30-122)
servo = machine.PWM(pin, freq=50, duty=actual_degree)
servo = machine.Pin(5)
servoWrite(servo, 60) Of course, usually I'll just add a conversion function and directly control the PWM object itself. This works pretty well for a servo or two, both on ESP8266 and ESP32, and works for several servo variations. Haven't tried more (the power supply is a bigger issue). For more than 3-4 servos it would be indeed sensible to use a PCA9685 board. Actually, quite a few micro:bit accessories have built-in PCA9685 chips/servo pins and a lithium battery. |
For LED and servo control, the users would typically vary the duty cycle, and for piezo / buzzer / speaker square-wave sound output, they'd vary the frequency. This does seem like a pretty flexible simple API to use! More complex waveforms and multi-pin or multi-channel things could be left for a future advanced API setup. |
If I'm reading the ESP8266 MicroPython code correctly, it looks like they're squeezing 1MHz frequency out of the PWM driver / clock, with a 10 bit (1024ish) resolution, and have support for multiple channels, interrupt driven for timing but then software driven for the actual output. |
So, based on this discussion so far, I think:
@aykevl the example that you gave where you said it was not as simple as you had hoped: is that because it required a board specific file in addition to the main.go file? if so, one option would be to have the func main() {
var pwm = machine.PWM{}
PWM.Configure(machine.PWMConfig{Frequency: 1000})
var channel = pwm.Channels(0)
for i := uint32(0); ; i += 0x1000 {
channel.Set(i)
time.Sleep(time.Second/2)
}
} |
No that was intentional. In my own projects I usually put board-specific configuration in a separate file so that I can easily run the same project on multiple boards. What I was referring to was that I didn't like the amount of configuration that was needed. I've been thinking a bit more about this issue the last few days and I honestly think the timer/channel distinction is inherent to the hardware, so much that hiding it will do more harm than good. While this might seem easy to use: machine.LED.SetPWM(frequency, dutyCycle) or this:
It hides the fact that frequency is a property of the timer and not of the pin or the chip as a whole. Even though it introduces a bit more complexity, I think it is better to expose it than to hide it and have a leaky abstraction (or dumbed-down abstraction). I also considered to maybe add some helper functions for this purpose, but I think they will actually make the API more complex simply by making it bigger.
I agree we should have a special driver for servos, but I doubt this driver should be in the machine package. The machine package is intended as a low-level HAL to be used by drivers. For example, most I2C and SPI devices aren't driven directly but through a dedicated driver (in the drivers repo). So what I hope is that we can find a low-level abstraction over the various hardware PWMs that is universal enough for a servo driver. I will investigate the different PWMs further, maybe there is a way to simplify things in a way that does not prevent more advanced use cases. Regarding the ESP8266: that "PWM" should be supported but I don't think it should influence the API too much. The chip doesn't really have any PWM to speak of (just a timer) and my intention of the machine package is to support hardware peripherals, not software peripherals. |
|
I tried implementing a more flexible PWM, see #1121. While doing that, I noticed a few things:
I've also written some preliminary (untested, incomplete, to-be-changed) documentation: tinygo-org/tinygo-site#87. It's worth checking out locally and running it, because I've included some (incomplete) interactive examples to explain how PWM really works. I put a lot of time in it with the idea that an API can only ever be as good as its documentation. Any feedback would be appreciated. |
The simple case of dimming an LED looks like this: import "machine"
func main() {
// Configure the PWM peripheral. The default PWMConfig is fine for many
// purposes.
pwm := machine.PWM0
err := pwm.Configure(machine.PWMConfig{})
handleError(err, "failed to configure PWM0")
// Obtain a PWM channel.
ch, err := pwm.Channel(machine.LED)
handleError(err, "failed to obtain PWM channel for LED")
// Set the channel to a 25% duty cycle.
ch.Set(pwm.Top() / 4)
// Wait before exiting.
time.Sleep(time.Hour)
} It's a bit noisy due to comments and error handling but the basic API isn't all that complex. It looks like this: func main() {
machine.PWM0.Configure(machine.PWMConfig{})
ch, _ := machine.PWM0.Channel(machine.LED)
ch.Set(pwm.Top() / 4)
} Here are some more (untested) examples: The only thing you need to know to use it, is which PWM peripheral you should use for a given pin. This can be added to device documentation. However, if this is still not simple enough, we could add an even simpler API that wraps these: package machine
func PWMForPin(pin Pin) PWM {
// some algorithm to pick an appropriate PWM peripheral
}
// SetPinPWM is a helper function to configure and set a pin with a certain PWM value. It is mainly useful for LEDs. The duty cycle is expressed in a range from 0 to 256, where 0 is entirely off and 256 is entirely on.
func SetPinPWM(pin Pin, dutyCycle int) error {
pwm := PWMForPin(pin)
err := pwm.Configure(PWMConfig{})
if err != nil {
return err
}
ch, err := pwm.Channel(pin)
if err != nil {
return err
}
ch.Set(pwm.Top() * 256 / uint32(dutyCycle))
} But I'm not sure whether hiding this will help new users. It may be better to teach them how a PWM operates so that if anything goes wrong, they can see why (it's not possible to build a completely flexible API that hides all the details, so I think it's better to make the necessary details explicit). This API is close to option B described by @syoder, but some things have been shuffled around. Any feedback would be appreciated. Some more things I learned:
|
I've just read through this quickly, but I like it! A few questions:
|
Not exactly, but I think it covers all use cases. Instead of setting a prescaler, the system figures out the smallest prescaler (and thus the largest resolution) that fits the requested frequency. After configuring, you can change the frequency easily using This is a bit higher level, but it allows abstracting away system specific details. Frequencies and prescalers vary massively between chips, even bit widths vary. But I think this API still provides all the power of the PWM while being portable. I hope I didn't miss something, if so please let me know. |
I wonder if the I may have to find a way to get my Circuit Playground Bluefruit off my desk at work so I can help implement the nrf52840 part of this. Actually I think the PWM peripheral is shared between all the nrf52s, at least. |
Been reading thru this. Really good and thoughtful conversation here. The examples located at https://github.com/tinygo-org/tinygo-site/blob/pwm/content/microcontrollers/machine/pwm.md#using-pwm-in-tinygo are really good at showing the use cases in action. One small question: some PWM interfaces let you reverse the polarity of the output. Is that something that would be handled in the |
I'm working on this again.
I see your point, but I don't think so. The reason is that the period specified there is exactly the period configured in the
I think most if not all allow you to do this, per channel (not per timer peripheral). I think this can be implemented with a separate method on the |
This has been merged a while ago, so closing. |
Currently, the
machine
package implements the methodsConfigure()
andSet()
. We need additional functionality in order to control the frequency and possibly other PWM settings per device. But because most devices have multiple channels per PWM, we really need a slightly different abstraction that would allow changing PWM settings separately from setting PWM channel outputs. See #855Functionality we probably need:
Option A:
PWM
could have the following functions:Configure()
SetFrequency(f int) error
ConfigureChannel(num int, pin machine.Pin)
SetChannel(num int, value uint16)
Stop()
One variation would be to have configure look like this:
Configure(pins []machine.Pin) error
and not have aConfigureChannel()
.Option B:
Have separate
PWM
andPWMChannel
structs.PWM
could have:Configure()
SetFrequency(f int) error
Channel(num int) (PWMChannel, error)
Stop()
and
PWMChannel
could have:Configure(pin machine.Pin)
Set(value uint16)
It would be somewhat painful to rewrite this for all the currently supported machines, but I think we need to do something in order to support servos and other hardware that uses PWMs. I would be happy to work on this if we can reach consensus. Thoughts?
The text was updated successfully, but these errors were encountered: