New thread accessing a value for non-blocking GPIO?

Hello,

I'm currently working on project to get a PWM signal running in software through the sysfs_gpio library (not using a hardware timer, like in the sysfs_pwm library does). I know it will be a little more jittery than a hardware timer, but I'm curious to see if it will be possible to do something like run a couple of ESCs with it.

I generally have the PWM itself working (via the code below), but have run into the issue putting the PWM signal itself (the loop turning the GPIO high and low) onto a different thread that only has read-access to the PWM struct's duty_cycle and period structures. I think this comes down to lifetimes, since the PWM struct could be dropped while the thread is still running.

I've tried several different methods, like making all of the PWM's internal fields Arc<Mutex<T>> types, using crossbeam's scoped threads, adding a new JoinHandle() field to the struct, and throwing in a bunch of <'a> lifetimes to see if that would help. Unfortunately, I usually end up with an error about unnamed lifetimes <'_> that shows up, and my experience writing multi-threaded code outside of rayon's .iter() -> .par_iter() methods doesn't seem to be sufficient. I was also considering somehow separating an mpsc (tx, rx) channel and putting them in different parts of the struct, but was unsure if that was good practice.

I don't, of course, expect a complete solution, but any suggestions about avenues to explore (hopefully in a way that doesn't require reading/writing raw pointers across threads, although I guess that could work) or links to projects doing something similar would be really appreciated!

// using sysfs_gpio = "0.5" in my Cargo.toml file
use std::error::Error;
use std::thread::sleep;
use std::time::Duration;

#[derive(Debug)]
struct Pwm {
    pin: sysfs_gpio::Pin,
    period: std::time::Duration,
    duty_cycle: f32,
    direction: sysfs_gpio::Direction,
}

impl Pwm {
    fn new(pin: u64, period: u64, duty_cycle: f32) -> Self {
        let pwm = Pwm {
            pin: sysfs_gpio::Pin::new(pin),
            period: std::time::Duration::from_micros(period),
            duty_cycle: duty_cycle,
            direction: sysfs_gpio::Direction::Out,
        };
        pwm
    }

    fn direction(&mut self, direction: sysfs_gpio::Direction) -> Result<(), Box<dyn Error>> {
        self.direction = direction;
        self.pin.set_direction(self.direction)?;
        Ok(())
    }

    fn export(&mut self) -> Result<(), Box<dyn Error>> {
        self.pin.export()?;
        Ok(())
    }

    // I saw a post somewhere about interior/exterior mutability, but I think I need &mut in order
    // to change the Pin state
    fn enable(&mut self) -> Result<(), Box<dyn Error>> {

        // It would be great to put this loop in a new thread that handles writing to the Pin, while 
        // adjusting the loop based on reading values from the pwm.period and pwm.duty_cycle fields
        loop {
            let duty =
                Duration::from_micros((self.period.as_micros() as f32 * self.duty_cycle) as u64);
            self.pin.set_value(1)?;
            sleep(duty);
            self.pin.set_value(0)?;
            sleep(self.period - duty);
        }

        Ok(())
    }

    fn duty_cycle(&mut self, duty_cycle: f32) {
        self.duty_cycle = duty_cycle;
    }

}

fn main() -> Result<(), Box<dyn Error>> {
    let mut pwm = Pwm::new(23, 100_000, 0.75);
    pwm.export()?; // Raspberry Pi BCM Pin 23, GPIO w/o hardware timer
    pwm.enable()?; // The internal loop {} here is currently blocking
    
    // sleep(Duration::from_millis(1000));
    // pwm.set_duty_cycle(0.25)?;  // We never get here

    // Ideally, I'd like to be able to set up the PWM signal on a separate, non-blocking
    // thread, and modify the signal's parameters via a .duty_cycle() or .period() method
    // and have those characteristics change in real time
    
    sleep(Duration::from_millis(1000));

    Ok(())
}

Update: I think I have this figured out, using some of the techniques from this thread about heartbeat messages. If/when the code gets published, I'll update this thread with a link.

I'd be very interested to know what frequency you can achieve with that code. Toggling a GPIO via sysfs must be very slow. It will also suffer from terrible jitters as the Linux kernel reschedules your process with any others you have running.

What do you mean by "publish"?

I'm curious to see myself--it might be possible to use a kernel with the PREMENT_RT patch to help keep some degree of regularity.

"Publish" probably wasn't the right work; I really just meant making the code available in a public repository.

Can I assume you are using a Raspberry Pi for your experiments?

Thing is, a typical PWM driven device works at 20KHz or so. Think servos or even PC fan motors. I don't think this is possible to do reliably using sysfs on Linux.

Meanwhile I have managed to toggle a GPIO pin a 50MHz on a Pi 3 using direct access to the GPIO registers on the SoC via memory mapping.

No PREEMT patches to the kernel required. But using the "isolcpus" option on the kernel boot command and running my program in a isolated core using the "taskset" command.

All of that was in C rather than Rust. But I have been tinkering with doing the same in Rust recently.

I'm using a Pi at the moment, but the main reason why I decided to give it a shot is because I'm working with a group of students at the moment who will (I think) end up building their own custom board. I don't know which processor they're using yet, which might not include enough dedicated PWM channels, so if I could get a workable signal running over a GPIO pin, it could provide a back-up in case they choose something unexpected.

Ultimately, I'm targeting trying to re-produce something like I did here with rppal (which I believe uses the dedicated chip at /sys/class/pwm/pwmchip0), except on any given board with GPIO. Maybe the ESC I'm using (Blue Robotics Basic ESC) just has a particularly slow update rate? Unless I'm misunderstanding how that works, I was under the impression it was only updated at around 50 Hz, which seemed reasonable to do in software alone.

1 Like

That looks like it’s designed to use an R/C servo control signal: a 1-2 ms pulse to communicate power level, with no significant duty cycle requirements.

I suspect that @ZiCog is thinking of PWM that directly controls the power feed to the motor windings, like what’s coming out the output side of that controller.

Interesting.

To be clear I was not using any dedicated PWM hardware on the Pi. Just brute force toggling of GPIO output pins.

Actually I think you are right. Typical ESCs and model servos work at 50Hz.

I was just now trying to control a PC Fan motor, which is said to require a 25KHz PWM signal.

I'm thinking that if your students are building their own custom boards with some unknow processor how can you be sure they even run Linux or have a sysfs driver for GPIO?

Hmm, interesting. Well, hopefully that will give me at least some sort of ceiling to aim for :slight_smile: I wonder if the PC fan driver requires more because it's actually providing the drive voltage/current for the fan, vs. just being a control signal.

To be honest, I'm not 100% sure about either one, although based on our (limited) conversation, it seems really likely that they'll be using Linux. If they are, between the actual sysfs_pwm crate and being able to emulate PWM over GPIO, I'm hoping to be able to cover most of my bases for a decent number of the more common Linux-capable processors out there.

I am beginning to doubt what I have read about PC fans. A PC fan has a motor control chip built in.

For example, when I just hook one up to 12v it runs. Not so fast but in runs.

I presume it's control chip is hoping for a PWM signal on it's speed control input.

I now suspect it would be quite happy with a 50Hz PWM, just like a regular servo controller chip. Why not.

Time to dig out the oscilloscope and see what really happens on a PC fan.

Not exactly Rust-related, but mind if I ask what oscilloscope do you have? Anything in a price range affordable for a home lab? COVID has really forced me to come to terms the with limitations of my reliance on my work gear, and I've been considering purchasing one for more hobbyist-type work (or when I'm too lazy to drive in just to use one piece of equipment for a few minutes).

Why would I mind. Here at home on my hobby budget I have a RIGOL DS1054Z.
4 cannel 50MHz, 1GSa/s.
Like so: https://www.batronix.com/shop/oscilloscopes/Rigol-DS1054Z.html
Only a bit over 300 bucks.

But with the added bonus that one can easily unlock it to achieve the 100MHz bandwidth of the much more expensive version :slight_smile:

I bought that four years ago. I have no idea what the good deals might be today.

1 Like

Great to know, thanks!