Skip to content
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

RFC: PWM HIL #1139

Open
phil-levis opened this issue Jul 31, 2018 · 11 comments
Open

RFC: PWM HIL #1139

phil-levis opened this issue Jul 31, 2018 · 11 comments
Labels
HIL This affects a Tock HIL interface. rfc Issue designed for discussion and to solicit feedback.

Comments

@phil-levis
Copy link
Contributor

We currently don't have a HIL for PWM. @bradjc has started the conversation in #1003. Let's start with use cases. The major PWM uses I'm aware of are motors, DACs (with supporting circuitry), and situations when you want high frequency square waves (e.g., @bradjc's buzzer).

Motors have the property that you want to control the width of the pulses but don't want to spread them. With DACs, you want to drive at a much higher frequency and spread the pulses to create analog signals. Square waves are pretty simple, just a 50% duty cycle and adjusting the frequency.

When you're driving an analog signal, often you need to clock out changes as sample points of the wave. So, for example, if you want to clock out 32 data points on a 1kHz analog wave, you need to update PWM settings at 32kHz.

So this seems like there are 3 different knobs: the underlying frequency, the pulse width, and the pulse spreading. Furthermore, we probably want separate APIs for high frequency streaming (e.g., DAC) and lower frequency.

@bradjc
Copy link
Contributor

bradjc commented Jul 31, 2018

We have a DAC HIL. Are you describing merging that with the PWM HIL?

A lot of Arudino boards seem to have motors or some other actuator, and I'm impressed with their PWM API and that it supports the development that users want to do:

// pin: the pin to write to. Allowed data types: int.
// value: the duty cycle: between 0 (always off) and 255 (always on). Allowed data types: int.
analogWrite(pin, value)

Doesn't get much simpler than that. Now I don't think we should make that our HIL, but supporting an interface that simple will probably get us pretty far.

@alevy
Copy link
Member

alevy commented Jul 31, 2018

The SAM4L has a DAC controller, but no PWM, the NRF52 has a PWM controller but no DAC. The CC26x2 has a PWM (well a timer that can be in PWM mode, which I don't totally understand), and separately a DAC (well they call it a reference DAC, and I also don't totally understand it).

Are these controllers interchangeable, and thus we should have one HIL for both? Is one a superset of the other (e.g. PWMs can also implement a DAC HIL)?

In the cases @phil-levis mentions (motors, DACs, buzzers):

Do you typically want to generate some wave-form continuously and, if you stop it, stop it out of band? or do you generally want to generate it for a fixed period (of time/iterations/whatever)?

Similarly, what does hardware typically provide? The NRF52 seems to support continuously generating a particular wave, as well

@bradjc bradjc changed the title PWM HIL RFC: PWM HIL Jul 31, 2018
@bradjc bradjc added rfc Issue designed for discussion and to solicit feedback. HIL This affects a Tock HIL interface. labels Jul 31, 2018
@phil-levis
Copy link
Contributor Author

Doesn't get much simpler than that. Now I don't think we should make that our HIL, but supporting an interface that simple will probably get us pretty far.

Yeah, sorta, if that's the level of stuff people want to do. But I don't think we should limit ourselves to Arduino-level applications. The issue is that if the HIL is too simple, then people will have to work around it. This can then play havoc with things like power management. Basically, PWM without a notion of time in it then requires that the higher layers control timing, which can be much easier and more accurate when close to the hardware (e.g., interrupt handlers on samples complete).

E.g., a good 25% of final projects in CS107E use PWM for audio on the Raspberry Pi, and they would not be able to do so with the Arduino API.

There are basically four knobs/parameters you have:

  1. The number of pulses in a sample. E.g., if your PWM has 8-bits of resolution, then this is 256 pulses. A 0.5 setting would have 128 of the 256 pulses be one.
  2. The duration of a sample. If you divide this by the number of pulses in a sample you get the duration of a pulse.
  3. The fraction of pulses that should be on within a sample.
  4. How the on pulses should be spread. E.g., should they be clumped together, or spread.

We can probably cut 4) -- the cases when you don't want to spread them (want to lower the frequency) is pretty rare. But to give you a counterpoint to the Arduino approach, this is the Atmel API to the SAM4 PWM:

http://ww1.microchip.com/downloads/en/AppNotes/Atmel-42294-SAM4-Pulse-Width-Modulation-Controller-PWM_ApplicationNote_AT07906.pdf

It defines 42 functions! I'd much rather be closer to Arduino, but we want to aim higher. Among other things, PWM can have FIFO or DMA, which enable removing jitter (e.g., as on the Raspberry PI BCM2835). The SAM4 seems to have some pretty weird PWM mechanisms, but it would be fine to fit into an API like the one above.

@alevy
Copy link
Member

alevy commented Aug 9, 2018

I have no experience with PWMs, so I can't agree or disagree about 1-4, but they seem like a good balance between control and simplicity. It certainly seems like one could expose the Arduino API in a library call that wraps the 1-4 things @phil-levis suggests.

@phil-levis
Copy link
Contributor Author

Hm -- one trick with cutting 4 is that the SAM4L (and Atmel chips generally) don't really support spreading. E.g., if you want to do a 60% duty cycle, then their hardware is well suited to have the line high for 6 ticks and low for 4, so 1111110000, rather than spreading like so 1010110101.

The place I encountered PWM spreading was on the Raspberry Pi: I'm looking around for MCUs that do it.

I'm therefore going to suggest that we add 4 back in.

@phil-levis
Copy link
Contributor Author

How about something like this. I swapped it such that the time you specify is length of a pulse, since that can be in terms of underlying clock ticks.

trait PwmAdvanced {
    type Frequency: time::Frequency;
    fn start(&self) -> ReturnCode;
    fn stop(&self) -> ReturnCode;
    fn is_running(&self) -> bool;
    fn set_pulses_per_sample(&self, pulses: u16) -> u16;
    fn pulses_per_sample(&self) -> u16;
    fn set_pulse_length(&self, ticks: u32);
    fn pulse_length(&self) -> u32;
    fn set_pulses_high(&self, pulses: u16) -> u16;
    fn pulses_high(&self) -> u16;
    fn set_pulse_spreading(&self, spread: bool) -> bool;
    fn pulse_spreading(&self) -> bool;
}

Another approach might be to couple the three configuration parameters together.

I think we can then layer a simpler interface on top of this, which computes number of pulses, etc.

trait PwmSimple {
    fn start(&self) -> ReturnCode;
    fn stop(&self) -> ReturnCode;
    fn is_running(&self) -> bool;
    fn set_duty_cyle(&self, cycle: double) -> ReturnCode;
    fn duty_cycle(&self) -> double;
}

@alevy
Copy link
Member

alevy commented Aug 15, 2018

Could we combine some of these functions?

E.g. does it make sense to change the duty cycle while the PWM is currently running or start it without having set the duty cycle first? If not, maybe just pass the duty cycle as a parameter to start instead of having a set_duty_cycle function.

Similarly, a lot of state changing functions in the advanced trait seem like they could just be parameters to start.

@phil-levis
Copy link
Contributor Author

phil-levis commented Aug 15, 2018

For the advanced/low-level trait, the challenge then is the caller needs to keep all of those parameters, in order to re-invoke them. For example, suppose you want a buzzer at a particular frequency. What you'd like to be able to do is configure the buzzer to be at that frequency, then turn it on and off. If start() takes all of the parameters, then they need to be stored for when start() is re-invoked. Furthermore, it means re-configuring the PWM each time you want to start it, even if the configuration hasn't changed.

I'm more ambivalent about the simpler trait -- I could see it making sense to put the duty cycle in start(). Although then it shouldn't be start(), because chances you'll want to call it while is_running() is true; on() and off() might make more sense.

@phil-levis
Copy link
Contributor Author

Here's a revised version of the simple trait:

trait PwmSimple {
    fn on(&self, cycle: double) -> ReturnCode;
    fn off(&self) -> ReturnCode;
    fn is_running(&self) -> bool;
    fn duty_cycle(&self) -> double;
}

@lthiery
Copy link
Contributor

lthiery commented Nov 3, 2018

I had to bang out my own HIL and driver for a deliverable this week. Here's where I ended up (with some extra affordances thrown in for 8 bit vs 16 bit timers)

At the HIL level:

use kernel::hil;
use kernel::ReturnCode;

enum TimerBits {
    _8(u8),
    _16(u16)
}

impl<'a> hil::pwm::Signal for Signal<'a> {
    fn enable(&self) -> ReturnCode;
    fn disable(&self) -> ReturnCode;
    fn configure(&self, period: u16, on_period: u16) -> ReturnCode;
    fn bits(&self) -> TimerBits;
}

My userland interface looks like this:

enum Resolution {
  _8_BITS,
  _16_BITS,
}
// gives direct control to period and on_period values
int pwm_configure(uint8_t channel, uint16_t period, uint16_t on_period);

// configures period/on_period values to achieve desired freq with 50% duty cycle
int pwm_set_frequency(uint8_t channel, uint32_t freq_hz);

// duty cycle is achieved with highest accuracy available, frequency is varialbe
int pwm_set_duty_cycle(uint8_t channel, float duty_cycle_percentage);

Resolution get_resolution();

int enable(void);
int disable(void);

What I like about this is it gives you varying degrees of control & ease. Set frequency and set duty cycle assume you don't care about the other. Meanwhile, if you care about controlling both accurately, the pwm_configure is there.

I only did the 16 bit implementation so far because that's what my timer is, but here's what the capsule/pwm.rs looks like (in broad strokes)

pub struct Pwm<'a, S: hil::pwm::Signal> {
    base_freq: usize,
    signals: &'a [S],
}

impl<'a, S: hil::pwm::Signal> Driver for Pwm<'a, S> {
    // allow and subscribe are empty

    fn command(&self, cmd_num: usize, arg1: usize, arg2: usize, _appid: AppId) -> ReturnCode {
    // CMD::CONFIGURE is easy
                CMD::SET_FREQ_HZ => {
                    //argument checking omitted
                    let signal_index = arg1;
                    let freq_hz = arg2;
                    let period: usize = self.base_freq / freq_hz;

                    let period: u16 = period as u16;
                    let on_period = period >> 1;
                    self.signals[signal_index].configure(period, on_period);
                    ReturnCode::SUCCESS
                }
                CMD::SET_DUTY => {
                    //argument checking omitted
                    let signal_index = arg1;
                    let duty_cycle: f32 = arg2 as f32;

                    // we are going to do play games to get accuracy from the 24 bit expansion
                    // exp will accumulate the exponent of the floating point representaiton
                    let mut exp = duty_cycle as u32;
                    for _n in 0..3 {
                        // by multipling the float by 100, we bring convert some bits of the fractional float
                        // to the exponent bits and by casting to u32, we extact the exponent
                        let extract = ((duty_cycle - exp as f32) * 100.0) as u32;
                        // update the exponent
                        exp = exp * 100 + extract;
                    }

                    // since the initial float was max 100.0, we can multiple by 10 again
                    // this allows us to represent the division later with a more accurate int
                    exp *= 10;
                    // convert the percentage into a fraction of the 0xFFFF period
                    // 100*100**3 *10 / float(0xFFFF) ~= 15259
                    let on_period = (exp / 15259) as u16;
                    self.signals[signal_index].configure(0xFFFF, on_period);

                    ReturnCode::SUCCESS
                }

What I'd like to point out in contrast to what it seemed like Phil was converging on is that we can get frequency conversions done at the capsule-driver level so that each HIL needs to provide much less functionality.

Also, I would urge a "lowest level possible" type interface to exist along with "simple ones". That's what "configure" is in my scheme and maps directly to the timers.

I will admit though, that the user might use the interface without checking for the bit-size of the PWM driver. IMO, we could catch those errors and convert them to the best supported resolution at the driver level.

@lthiery
Copy link
Contributor

lthiery commented Nov 3, 2018

@phil-levis BTW, the DAC on the CC26x2 is exclusively as an input to the analog comparator. It's useful because I believe you can setup a low power analog alarm by putting some threshold voltage out on the DAC and then setting up the comparator to give you an interrupt. Much cheaper then running continuous ADC and comparing the values to a threshold in software.

And also, it's worth being explicit about the differences of DAC vs PWM since I don't think this was made clear in this thread.

DAC outputs a constant voltage.

PWM outputs switching signals that can help you simulate a voltage.

Having separate and clear interfaces is important as that's a significantly different property. For example, I would suggest extending the current DAC interface to provide an "easy" voltage control command similar to the way PWM provides an "easy" frequency control command.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
HIL This affects a Tock HIL interface. rfc Issue designed for discussion and to solicit feedback.
Projects
None yet
Development

No branches or pull requests

4 participants