eldruin / pwm-pca9685-rs

Platform-agnostic Rust driver for the PCA9685 I2C 16-channel, 12-bit PWM/Servo/LED controller

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

"full on" needs to clear the "full off" register

Visic opened this issue · comments

commented

Since the "full off" takes precedence if set and there is no way in this crate to clear it, I think it would be reasonable if the "full on" cleared it.

The full off can be cleared or set to any other non-full value by setting the channel's off as indicated in the documentation:

pwm.set_channel_full_off(Channel::C0).unwrap();
pwm.set_channel_off(Channel::C0, 0).unwrap();

Am I missing something?

commented

I do believe you are correct about the usage, and I have no problem doing that to solve my issue (really, I prefer it so that your driver can stay 1:1 with the 9685 capabilities).

In my particular case I had overlooked the full_on/full_off and their precedence in the documentation, which lead to hours of troubleshooting. It might be worth just adding an example or a caveat in the documentation for the "set_channel_full_on" to help avoid this stumbling block in the future.

I see. I added some notes in the documentation and added an example illustrating the precedence so hopefully this does not happen to other people. Thanks for your input.
I have released this in version 0.1.1.

I just ran into this today also. I think it's a bummer that it takes two i2c transactions to switch a channel into "full on" mode - one to set the "off" register to 0, another to set the "on" register to 4096.

If there were a set_channel_on_off_unchecked or if set_channel_on_off allowed values of 4096 (currently they return an error for anything greater than 4095) then you could switch to any PWM state with a single transaction.

Or you could just change to set_channel_full_on function to zero the "off" register, since if the user is calling that function they probably want to set that channel high, right?

    pub fn set_channel_full_on(&mut self, channel: Channel, value: u16) -> Result<(), Error<E>> {
        if value > 4095 {
            return Err(Error::InvalidInputData);
        }
        let reg = get_register_on(channel);
        let value = value | 0b0001_0000_0000_0000;
        self.write_two_double_registers(reg, value, 0) // this is the changed line
    }

Or am I missing something?

Hmm, are you trying to just switch between full on and full off?
You would only need one call to switch:

// initialization
// full off is activated by default (POR state)
pwm.set_channel_full_on(Channel::C0).unwrap(); // full on is still overridden by full off

// blink
loop {
    pwm.set_channel_off(Channel::C0, 0).unwrap(); // full on: full off is deactivated
    delay.delay_ms(500);
    pwm.set_channel_full_off(Channel::C0).unwrap(); // full off: full on is overriden
    delay.delay_ms(500);
}

The chip is designed as such that it provides a lot of flexibility about how to control the channels.
Like above, it is possible to have a channel on full or partial on in principle but toggle just with the full off setting, for example. In the case of full on and full off, full off overrides full on, rather than indeterminate.

Many other combinations exist so I designed this driver so that the same flexibility is achievable, but with a safe and comfortable interface. That is why full on does not deactivate the full off setting although it may sound easier at first.

Ok after reading the datasheet and double-checking the API, it looks like, if the user is careful, every transition can be covered with a single i2c transaction. But I feel the naming is a little confusing, and it is possible to get into a state that requires extra i2c transactions. Contrived, sure, but still - if it's possible to do in a single i2c transaction, I think the library should expose that. Every i2c transaction is an extra syscall on non-bare-metal systems (like a Raspberry Pi running Linux).

// 50% duty cycle
pwm.set_channel_on_off(Channel::C0, 0, 2047).unwrap();

// Shut off
pwm.set_channel_full_off(Channel::C0).unwrap()

// full on - doesn't work, have to zero the off register
// pwm.set_channel_full_on(Channel::C0, 0).unwrap();

// full on for reals this time
pwm.set_channel_off(0).unwrap();
pwm.set_channel_full_on(Channel::C0, 0).unwrap();

When I implemented a driver for this chip in Python, I had a function like this:

    def set_pin(self, num: int, duty_cycle: float, invert: bool = False):
        # Clamp value between 0 and 4095 inclusive.
        val = int(max(0, min(4095, duty_cycle * 4095)))
        if invert:
            if val == 0:
                # Fully on
                self.set_pwm(num, 4096, 0)
            elif val == 4095:
                # Fully off
                self.set_pwm(num, 0, 4096)
            else:
                self.set_pwm(num, 0, 4095 - val)
        else:
            if val == 4095:
                # Fully on
                self.set_pwm(num, 4096, 0)
            elif val == 0:
                # Fully off
                self.set_pwm(num, 0, 4096)
            else:
                self.set_pwm(num, 0, val)

Where Pca9685.set_pwm above does the same as your Pca9685::set_channel_on_off, except without the input checks (so you can pass it 4096 for the on or off value).

If it is full on or full off (duty cycle = 1.0 or 0.0 respectively), it uses the full on / full off bits. Otherwise it calculates the on and off times based on duty cycle.

I think this function is impossible to implement (where it uses only one i2c transaction) under the current API.

I understand if you feel like this is silly / a micro-optimization. This is just what I feel.

// 50% duty cycle
pwm.set_channel_on_off(Channel::C0, 0, 2047).unwrap();

// Shut off
pwm.set_channel_full_off(Channel::C0).unwrap()

// full on - doesn't work, have to zero the off register
// pwm.set_channel_full_on(Channel::C0, 0).unwrap();

// full on for reals this time
pwm.set_channel_off(0).unwrap();
pwm.set_channel_full_on(Channel::C0, 0).unwrap();

I would ask, why use full off in the code above instead of:

// 50% duty cycle
pwm.set_channel_on_off(Channel::C0, 0, 2047).unwrap();
// Shut off
pwm.set_channel_off(0).unwrap();
// Full on
pwm.set_channel_full_on(Channel::C0, 0).unwrap();

The duty-cycle function is interesting. I see your point about it having to call two times in this case.
However, I think this function is something that belongs inside of the driver (like you did), rather than being implemented on top of it and then I can write it using only one I2C transaction. In fact, I planned to do so to implement embedded_hal::PwmPin on a pin-splitted interface but it also makes sense without that.

What I do not like about allowing 4096 as input value is that it is not a valid duty-cycle value, also as described in the datasheet. It only happens to be that the full on/off is the next bit in the register, and I think this would not be an elegant thing to expose in the public API.
For example, as a user without much knowledge of the chip, it is not clear when full on/full off is being used in this example code (with its overwrite potential in the case of full off), and you may as well think that the values just go from 0 to 4096.

pwm.set_channel_on_off(0, 400).unwrap();
pwm.set_channel_on_off(0, 4096).unwrap();
pwm.set_channel_on(400).unwrap(); // oops, no effect
pwm.set_channel_on_off(4096, 0).unwrap(); // specially this looks quite weird
pwm.set_channel_off(400).unwrap(); // also no effect
pwm.set_channel_on_off(0, 4090).unwrap();

I am not totally opposed to allowing 4096 in an _unchecked method variant, but I ask for a more compelling use case.
I will add a set_channel_duty_cycle function to this driver soon. Thanks for the idea :)