Combined PPM (fast pulse width measurements)

Showroom for MicroPython related hardware projects.
Target audience: Users wanting to show off their project!
Post Reply
jeffeb3
Posts: 11
Joined: Sun Jan 18, 2015 3:08 pm

Combined PPM (fast pulse width measurements)

Post by jeffeb3 » Sat Nov 21, 2015 2:10 pm

I have several R/C receivers that output a "CPPM" signal, which, as far as I can tell, is a fat sync pulse, followed by each channel as a fast pulse, where the width is the size of the signal for each channel. A decent explanaition is here: http://diydrones.com/profiles/blogs/705 ... Post:38393
with a broken link, which really points to here:
http://wiki.paparazziuav.org/wiki/RC_Re ... _Receivers

It would be nice to be able to have a slick interface like this:

Code: Select all

    from pyb import cppm, Servo
    control = cppm(0)
    servo = Servo(1)
    while not control.ch(6).switch:
        servo.angle(control.ch(0).angle)
The real question is, how far down do I have to go to be able to read these pulses? I'd need to determine the amount of time between sequential rising edges, and the measurements would need to be on the order of microseconds. I'm guessing I could do that with some careful code and the interrupt interface. WDYT?

jeffeb3
Posts: 11
Joined: Sun Jan 18, 2015 3:08 pm

Re: Combined PPM (fast pulse width measurements)

Post by jeffeb3 » Sat Nov 21, 2015 2:55 pm

This seems like a good place for me to start, unless someone else has already done this somewhere:

https://github.com/dhylands/upy-example ... ic_test.py

jeffeb3
Posts: 11
Joined: Sun Jan 18, 2015 3:08 pm

Re: Combined PPM (fast pulse width measurements)

Post by jeffeb3 » Sat Nov 21, 2015 6:33 pm

Holy cow. That didn't take long at all. I've been working on this for less than an hour. This little board is just awesome.

I plan on cleaning this up, and making a nice couple of objects that will let you do things in coordinates other than microseconds. Something akin to the interface I was mentioning above. Here is a quick sample of code, in case I don't have time to get back to this:

Code: Select all

import pyb
import micropython

class Ppm(object):
    """
    Read pulses from a particular pin.

    @note, this uses timer 2.
    """
    timer = None
    def __init__(self, pin, servo = None):
        """
        :pin: ex pyb.Pin.board.X4, the pin the input is connected to.
        :servo: ex pyb.Servo(1) a servo to control directly from the interrupt.
        """
        self.pin = pin
        if Ppm.timer is None:
            Ppm.timer = pyb.Timer(2, prescaler=83, period=0x0fffffff)
        self.interrupt = Ppm.timer.channel(4, pyb.Timer.IC, pin=pin, polarity=pyb.Timer.BOTH)
        self.start = 0
        self.width = 0
        self.servo = servo
        self.interrupt.callback(self.callback)

    def callback(self, tim):
        if self.pin.value():
            self.start = self.interrupt.capture()
        else:
            self.width = self.interrupt.capture() - self.start & 0x0fffffff
            if self.servo:
                self.servo.pulse_width(self.width)

    def demo():
        # This example requires a servo on X1 and a signal (from a radio) on X4.
        micropython.alloc_emergency_exception_buf(100)

        in_ppm = Ppm(pyb.Pin.board.X4, pyb.Servo(1))

        while True:
            # wait forever
            pyb.delay(200)

class Cppm(object):
    """
    Read a combined PPM signal from a R/C radio.

    @note, this uses timer 2.
    """
    timer = None
    def __init__(self, pin, numChannels = 8, servo = None):
        """
        :pin: ex pyb.Pin.board.X4, the pin the input is connected to.
        :servo: ex pyb.Servo(1) a servo to control directly from the interrupt.
        """
        self.pin = pin
        if Ppm.timer is None:
            Ppm.timer = pyb.Timer(2, prescaler=83, period=0x0fffffff)
        self.interrupt = Ppm.timer.channel(4, pyb.Timer.IC, pin=pin, polarity=pyb.Timer.RISING)
        self.start = 0
        self.width = 0
        self.channel = 0
        self.numChannels = numChannels
        self.ch = [0]*numChannels
        self.sync_width = 0
        self.frame_count = 0
        self.servo = servo
        self.interrupt.callback(self.callback)

    def callback(self, tim):
        self.width = (self.interrupt.capture() - self.start) & 0x0fffffff
        self.start = self.interrupt.capture()
        if self.width > 4000:
            self.channel = 0
            self.sync_width = self.width
            self.frame_count += 1
            return
        if self.channel == self.numChannels:
            return
        self.ch[self.channel] = self.width
        self.channel += 1

        if self.servo:
            if self.channel == 1:
                self.servo.pulse_width(self.width)

    def demo():
        # This example requires a servo on X1 and a signal (CPPM from a radio) on X4.
        micropython.alloc_emergency_exception_buf(100)

        in_ppm = Cppm(pyb.Pin.board.X4, 8, pyb.Servo(1))

        while True:
            # wait forever
            pyb.delay(200)
            print(in_ppm.ch, in_ppm.sync_width, in_ppm.frame_count)
That includes a PPM (the normal inverse of a servo signal) and the CPPM, and it's under 100 lines even very spread out!

Here is some example output from Cppm.demo():

Code: Select all

[1512, 1525, 1772, 1441, 1519, 1512, 0, 0] 12726 490
[1525, 1525, 1772, 1441, 1519, 1512, 0, 0] 12729 499
[1205, 1525, 1772, 1441, 1518, 1512, 0, 0] 12977 508
[1025, 1524, 1772, 1441, 1519, 1512, 0, 0] 13210 517
[986, 1525, 1772, 1441, 1518, 1512, 0, 0] 13246 527
[1051, 1525, 1772, 1441, 1518, 1512, 0, 0] 13207 536
[1371, 1524, 1772, 1441, 1518, 1512, 0, 0] 12920 545
[1756, 1524, 1772, 1441, 1519, 1512, 0, 0] 12516 554
[2038, 1525, 1772, 1440, 1519, 1512, 0, 0] 12226 563
[2025, 1525, 1772, 1441, 1518, 1512, 0, 0] 12198 572
critiques, comments, improvments (especially w.r.t. the timer code) is greatly appreciated.

User avatar
dhylands
Posts: 3821
Joined: Mon Jan 06, 2014 6:08 pm
Location: Peachland, BC, Canada
Contact:

Re: Combined PPM (fast pulse width measurements)

Post by dhylands » Sat Nov 21, 2015 9:19 pm

One very minor change I would suggest is to change this:

Code: Select all

    def callback(self, tim):
        self.width = (self.interrupt.capture() - self.start) & 0x0fffffff
        self.start = self.interrupt.capture()
to only call capture once.

That closes the very small possibility that the value from calling capture the second time differs from the first because an edge happens between the 2 calls (this could happen if the initial callback was delayed by other interrupts or something).

So I'd do something like:

Code: Select all

    def callback(self, tim):
        capture = self.interrupt.capture()
        self.width = (capture - self.start) & 0x0fffffff
        self.start = capture
Glad to see you got it to work. This was one of the use cases I had in mind for the input capture stuff. I've hacked R/C receivers before to get at the CPPM signal: http://www.davehylands.com/Robotics/RC-Input/

The VEX receiver http://www.vexrobotics.com/75mhz.html outputs a CPPM signal directly.

jeffeb3
Posts: 11
Joined: Sun Jan 18, 2015 3:08 pm

Re: Combined PPM (fast pulse width measurements)

Post by jeffeb3 » Sun Nov 22, 2015 3:44 pm

I cleaned this all up, and made a git repo for it. Enjoy:

https://github.com/jeffeb3/cppm_micropython

jeffeb3
Posts: 11
Joined: Sun Jan 18, 2015 3:08 pm

Re: Combined PPM (fast pulse width measurements)

Post by jeffeb3 » Sun Nov 22, 2015 3:48 pm

dhylands wrote:One very minor change I would suggest is to change this:

Code: Select all

    def callback(self, tim):
        self.width = (self.interrupt.capture() - self.start) & 0x0fffffff
        self.start = self.interrupt.capture()
to only call capture once.

That closes the very small possibility that the value from calling capture the second time differs from the first because an edge happens between the 2 calls (this could happen if the initial callback was delayed by other interrupts or something).

So I'd do something like:

Code: Select all

    def callback(self, tim):
        capture = self.interrupt.capture()
        self.width = (capture - self.start) & 0x0fffffff
        self.start = capture
Glad to see you got it to work. This was one of the use cases I had in mind for the input capture stuff. I've hacked R/C receivers before to get at the CPPM signal: http://www.davehylands.com/Robotics/RC-Input/

The VEX receiver http://www.vexrobotics.com/75mhz.html outputs a CPPM signal directly.
Thanks. I fixed that capture.

I haven't ever hacked a receiver to get the CPPM, but the frsky and many of the cheap hobby king receivers have it exposed (on the bind pin).

This one is my favorite: http://www.hobbyking.com/hobbyking/stor ... iver_.html

jeffeb3
Posts: 11
Joined: Sun Jan 18, 2015 3:08 pm

Re: Combined PPM (fast pulse width measurements)

Post by jeffeb3 » Mon Nov 30, 2015 3:46 pm

Should I replace the Timer/channel stuff with just registering an interrupt on a rising edge, and measuring using elapsed_microseconds? Seems like it would be fewer shared resources used.

User avatar
dhylands
Posts: 3821
Joined: Mon Jan 06, 2014 6:08 pm
Location: Peachland, BC, Canada
Contact:

Re: Combined PPM (fast pulse width measurements)

Post by dhylands » Mon Nov 30, 2015 4:33 pm

In my mind they're both useful implementations.

The advantage of using the TimerChannel is that the HW grabs the timestamp, so even if your callback is delayed by other interrupts you still get accurate edge times.

The advantage of using ExtInt is that you don't need a timer channel, but your timestamp of edges can be delayed by other interrupts (which would manifest itself as jitter).

Having both implementations available seems worthwhile, especially if they have more or less the same python API exposed.

Post Reply