Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 15 of 15

Thread: 14+ PPM inputs?

  1. #1
    Senior Member
    Join Date
    Sep 2019
    Posts
    123

    14+ PPM inputs?

    Hello ladies and gentlemen.

    I am currently designing the controller for my radio controlled submarine. I am using a Teensy 3.2 (because I have one)

    I however, need to read the PPM signal from 14 RC servos. 2 of these channels are monitored constantly, as they adjust the planes that keep the submarine level.
    The rest of these channels are not so time critical, as they are basically switches for functions.

    Now, I believe there is a library call PulsePosition, but it's limited to 8 channels on a Teensy 3.2. I could use 2 Teensy's, but that is expensive, wasting a lot of resources and I am REALLY tight for space.

    So, I was wondering if anyone knows of an IC range that is capable of reading PPM and converting it to say I2C? Extensive Googling occuring here.

    I will have 4 servos attached to the Teensy, so I need to avoid interrupt issues as well.

    I did think of switching the inputs in through a multiplexer? Maybe that could work.

    Ideas on a postcard!

  2. #2
    Senior Member
    Join Date
    Sep 2019
    Posts
    123
    Hmm

    https://www.pjrc.com/teensy/td_libs_PulsePosition.html

    Got my receiver hooked up, using pin 20. Common receiver and Teensy ground and it doesn't receive the PPM signal.

    I have checked the receiver specs, and it's definitely PPM. It can support both PPM and PCM, but the receiver has a slide switch and it's set to PPM.

    Zero output on the serial display. What can I adjust to get this to work? Tried other Teensy PulsePosition pins.

    Why does everything have to be a pain in the backside

    Code:
    #include <PulsePosition.h>
    
    // Simple loopback test: create 1 output to transmit
    // test pulses, and 1 input to receive the pulses
    PulsePositionOutput myOut;
    PulsePositionInput myIn;
    
    void setup() {
      myOut.begin(9);  // connect pins 9 and 10 together...
      myIn.begin(20);
      myOut.write(1, 600.03);
      myOut.write(2, 1500);
      myOut.write(3, 759.24);
      // slots 4 and 5 will default to 1500 us
      myOut.write(6, 1234.56);
    
      delay(1000);
      Serial.print("Online");
    }
    
    int count = 0;
    
    void loop() {
      int i, num;
    
      // Every time new data arrives, simply print it
      // to the Arduino Serial Monitor.
      num = myIn.available();
      if (num > 0) {
        count = count + 1;
        Serial.print(count);
        Serial.print(" :  ");
        for (i = 1; i <= num; i++) {
          float val = myIn.read(i);
          Serial.print(val);
          Serial.print("  ");
        }
        Serial.println();
      }
    }
    Last edited by SteveSFX; 01-25-2021 at 09:41 AM.

  3. #3
    Senior Member
    Join Date
    Sep 2019
    Posts
    123
    This works....

    Code:
    int CH2;
    byte Counter;
    
    void setup() {
    
      Serial.begin(9600);                                                                  // Serial Begin
      pinMode(20, INPUT);                                                                  // Pin 20 as input
    }
    
    void loop() {
    
      if (pulseIn(20, HIGH) > 500) {
    
        CH2 = CH2 + pulseIn(20, HIGH);
        Counter++;
    
        if (Counter > 4) {
          Serial.println((CH2 / 5) - 900);
          Counter = 0;
          CH2 = 0;
        }
    
      }
    
    }

  4. #4
    Senior Member
    Join Date
    Sep 2019
    Posts
    123
    Well I didn't realise this was going to be quite so impossible.

    Obviously, PulseIN is a blocking function. I hoped to run the majority of the 'non urgent' channels through an analogue switch and just sample them less often.
    But, the PulseIN just kills the routine.

    I will have to think again.

    I understand I can decode the PPM signal using a 4017 IC, but for that I need to open up my RC receivers, which I do not want to do (to access the direct PPM signal).
    I hoped to read the receivers individual servo outputs (which I would have thought were PWM - not PPM?). But my experimenting has shown I can read a servo output pin (albeit slowly) with PulseIN, but not with any kind of PWM detection.

    So it looks like maybe the Teensy is not capable of decoding that much timing information without getting bogged down.

    Perhaps I need to decode this data outside the Teensy and pass it on to the processor once the timing has been deduced, but that seems like decoding a signal twice.

  5. #5
    Senior Member+ Frank B's Avatar
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    8,348
    Quote Originally Posted by SteveSFX View Post
    Well I didn't realise this was going to be quite so impossible.

    Obviously, PulseIN is a blocking function.
    PulseIN is blocking on all Arduinos.
    So, the pulses all start at the same time?
    That would be doable.

  6. #6
    Senior Member
    Join Date
    Sep 2019
    Posts
    123
    I am trying using interrupts. I thought all digital pins on a 3.2 where interrupt able, but pins 21 and 20 refuse to work? (Other pins do)

  7. #7
    Senior Member+ KurtE's Avatar
    Join Date
    Jan 2014
    Posts
    9,141
    Sorry, I have not had enough coffee yet so I may not be following this yet.

    But I have having a hard time trying to understand your setup (sorry it still is early here)
    Do you have an RC receiver that is receiving PPM for 14 channels on one pin?

    Do you have a link to the receiver to some documents on the receiver you are using?

    If so, I believe PulsePosition is setup to handle by default up to 16 channels per object (#define PULSEPOSITION_MAXCHANNELS 16)
    Note: the 8 is how many of these objects can you have on the different Teensy boards, which for T3.2 is the 8 you mention as these pins I believe connect up to specific types of timers which can do input captures... Or timer controlled outputs for PulsePositionOutput...

    So in theory it should work assuming that your PPM stream is that.

    Now if you are wanting to hook up the 14 signals from the receiver to 14 IO pins that read the output as if you had 14 RC servos on it, then the PulsePosition library would probably not be appropriate.

    Again as you mentioned these Servo outputs are not PPM, but more of a PWM.

    Some different things I have done in the past which may or may not work for you. Sorry it has been a long time since I played at the low level with T3.2 timers...
    But for the time(value) critical settings, you can probably model code after this library which captures the timer time when the signals changes and compute the pulse width. There are probably libraries that already do this.

    But again you may only have 8 of these you could do.

    a) Use PulseIn as you mentioned it is blocking. So what I did way back when (probably back to the Basic Atom Pro days), was to monitor how these signals were received for each of the servos. For example I believe on first HiTec one I tried years ago, I found they were all done one after another, possibly with the next one starting more or less exactly after the previous one completed. So I figured out that order and I wrote the pulseIn code try to complete in two receiver cycles. That is maybe try to read every other one logically on first pass and the others on next pass. Note: it might have taken 3 passes.
    At the time that was sufficient for my needs. Although it would eat up a significant amount of time to do all of this, which was not available to do other things like make the hexapod walk... So at the time I think I might have streamlined it to only read the channels I really needed (4 joystick values) all of the time and maybe every n time read one or more of the others (like switches)...

    b) Use something like Pin Change Interrupts for all or some of these pins, especially ones that are not value critical. That is on those pins do an attachInterrupt either for CHANGE for something like RISING and then change to FALLING. But in ISR figure out which pin and which state happened If logically high remember current micros. If logically low compute the PulseWidth as current micros - saved micros.
    Now again this won't be as accurate as you may differences in time of how long before the ISR is processed... But for things like switches that may be 750 is one state and 2250 is the other, this should be accurate enough. Do you do all of them at same time or turn on a subset at a time... May depend...

    Not sure if any of these other approaches will work for you or not and I can imagine a mix and match type of approach.

    But does sound like fun

  8. #8
    Senior Member
    Join Date
    Sep 2019
    Posts
    123
    OK. This is where we are.

    My Robbe receiver is 8 channel. This uses channel 8 to loop onto a second decoder that gives me a further 8 channels (with lower resolution, but fine for auxiliary functions).

    It is PPM, but from what I understood, the main signal to the receiver is PPM, and then it it broken down internally to give the 8x PWM outputs for the servos.
    If I wanted to decode the original PPM signal, I would need to open the receiver and find that signal before it is decoded into the 8x servo outputs (So I believe anyway).

    PulseIN was troublesome, so I binned that.

    I found this code online:

    Code:
    volatile int pwm_value = 0;
    volatile int prev_time = 0;
     
    void setup() {
      Serial.begin(115200);
      // when pin D2 goes high, call the rising function
      attachInterrupt(0, rising, RISING);
    }
     
    void loop() { }
     
    void rising() {
      attachInterrupt(0, falling, FALLING);
      prev_time = micros();
    }
     
    void falling() {
      attachInterrupt(0, rising, RISING);
      pwm_value = micros()-prev_time;
      Serial.println(pwm_value);
    }
    I have repeated this code (rightly or wrongly) 14 times for my 14x servo inputs. It appears to work very well. I get fast reporting on all 14 channels.

    Pins 20 and 21 refused to work as interrupts for some reason. I just changed to different pins. Must be a reason for that.

    The reason I am reading this data, is because the Teensy is controlling various aspects of the SFX and ship controls.

    The APC (Automatic pitch control).
    The rear submarine planes are on the right hand stick (up/down). This is for 'driven to depth' control while underway. You control the rear submarine planes servo only while the stick is not dead centre.
    When you let the stick return back to dead centre, the Teensy then intervenes and uses a gyro to return the submarine back to level.

    The throttle input is purely for the sound effect generator.

    There is a pump and valve that operate the ballast system. An external pressure sensor detects the depth, and this attempts to keep you statically submerged by adjusting the air in the ballast balloon. The balloon also has a pressure sensor.

    The ADC (Automatic depth control).
    There are also front planes that can be controlled by the Teensy. These are also used for depth control, but the APC has priority. These planes are controlled by the external depth pressure sensor. The Teensy controls the front planes and attempts to maintain a constant depth underway.

    There is a small wireless transmitter on the Teensy that talks to the deck (which is detachable). The receiver is in the conning tower and has a small 18m2 Picaxe processor onboard, Lipo (charged using induction and all watertight) and that controls 3x deck AA guns, the periscope servo, sound effects via an MP3 module and lighting.

    Other 'switch' type inputs open and close the torpedo tubes etc.

    The main Robbe RC system is 40Mhz, which works pretty well through water. I would like to find/make a 40Mhz serial link so that I can report battery levels, depth etc back to me on the shore.

    My questions regarding the interrupts...

    Will they interfere with each other? As in. can one interrupt 'interrupt' another? (Does that make sense?)
    I have left it running and it seems OK.

  9. #9
    Senior Member+ KurtE's Avatar
    Join Date
    Jan 2014
    Posts
    9,141
    Quote Originally Posted by SteveSFX View Post
    My questions regarding the interrupts...

    Will they interfere with each other? As in. can one interrupt 'interrupt' another? (Does that make sense?)
    I have left it running and it seems OK.
    I don't see any reason pins 20 and 21 should not work. However without seeing the program it may be hard to tell. Example if you still have the PulsePosition library configured and it is using those pins or...

    As for interrupts interfere with others. Slightly longer answer.

    Each of the IO pins are contained on IO ports on the Teensy. For example pin 20 is internally D5 (Port D pin 5), likewise 21 is D6...

    When you setup a pin change interrupt, you are actually setting up the interrupt on the PORT, in this case PORTD, the PORT has registers that control which pins to interrupt on and for what

    So for these pins the PORTD would be setup with an ISR, which goes internally to the port_D_isr code (in pins_teensy.c)

    So when a pin changes that hardware is configured to interrupt on. The code will scan the bits of the appriate port register to see which ones were triggered. It will then use the index to find the function that you registered to be called. When your function returns it will continue to scan to see if any other ISR bits were set and again call that one... When done it returns...

  10. #10
    Senior Member
    Join Date
    Sep 2019
    Posts
    123
    No, nothing else configured on pins 20 and 21. Those pins work fine as analog pins. Odd

    Well, it seems OK at the moment. I just added a counter that increments 14 on each loop. Each returned value from the interrupts reduces that variable by 1.
    If that variable reaches a threshold, then I have lost transmitter data.

  11. #11
    Senior Member
    Join Date
    Sep 2019
    Posts
    123
    It's all working.

    My issue is the variance in reported values.
    The PWM value returned from the 14 channels varies each 'loop' from approx 920 to around 2050. I can deduct an offset of say 940 to ensure the starting value is 0, and then divide the remaining 1130 (or whatever it is) into something at or under 180 degrees.

    But the problem is this variance. How do I eliminate the jumping around? Multiple sampling and then division returns a very slow responding servo.
    The start value moves around within something like a 40 millisecond window. As does the ending time sample.

    This variance is enough to make the servo 'twitch'. Measuring in micros() is always going to return differing values, but smoothing these into 180 degrees is proving troublesome

  12. #12
    Senior Member
    Join Date
    Sep 2019
    Posts
    123
    Solved. Constantly sample 8 micros() timing values into an array (shifting them all along each time and binning the oldest) and then divide for the average.
    Write that out to the servo using:

    RearPlanesServo.writeMicroseconds(map(CH2Val, 940, 2035, 1500 - (CH2Range * 10), 1500 + (CH2Range * 10)));

    CH2Range is my radio channel 2 'range' adjustment

  13. #13
    Senior Member+ KurtE's Avatar
    Join Date
    Jan 2014
    Posts
    9,141
    I have done similar in the past as well. Sometimes will also add in some checking to make sure not a bogus value.

    I know from some previous robotics stuff (hexapod) with remote controls and one of the developers who was doing a lot of the work @zenta put in some code for smoothing out the Joystick inputs, which is up in my different projects with the name Phoenix...

    Code:
    #define SmDiv    	 4  //"Smooth division" factor for the smooth control function, a value of 3 to 5 is most suitable
    
    //--------------------------------------------------------------------
    // SmoothControl (From Zenta) -  This function makes the body 
    //            rotation and translation much smoother 
    //--------------------------------------------------------------------
    short SmoothControl (short CtrlMoveInp, short CtrlMoveOut, byte CtrlDivider)
    {
    
      if (CtrlMoveOut < (CtrlMoveInp - 4))
        return CtrlMoveOut + abs((CtrlMoveOut - CtrlMoveInp)/CtrlDivider);
      else if (CtrlMoveOut > (CtrlMoveInp + 4))
        return CtrlMoveOut - abs((CtrlMoveOut - CtrlMoveInp)/CtrlDivider);
    
      return CtrlMoveInp;
    }

    Where we may for example have code like:
    Code:
        if (ControlMode == TRANSLATEMODE) {
          g_InControlState.BodyPos.x =  SmoothControl(((lx)*2/3), g_InControlState.BodyPos.x, SmDiv);
          g_InControlState.BodyPos.z =  SmoothControl(((ly)*2/3), g_InControlState.BodyPos.z, SmDiv);
          g_InControlState.BodyRot1.y = SmoothControl(((command.rightH)*2), g_InControlState.BodyRot1.y, SmDiv);
    Note: the lx, ly, ... are values returned from the joystick code...
    So if I remember correctly instead of just moving from previous position to new position, this one will move like 1/4 of the way for each cycle through...

    Note in the case of code that I am looking at, it was the code I had on the Joystick side that did the averaging of the last 8 samples, before it sent the value (using XBee) to the Robot, who in certain cases would smooth out the values...

  14. #14
    Senior Member
    Join Date
    May 2017
    Posts
    238
    If the servo glitches are from random noise, then an average makes sense. When I have sampled PWM servo signals, I found the issue was due to interrupt latency. My solution was to ignore values that deviated greatly from the normal values.

    Code:
     // return the median value of the last 3 to ignore outliers in the data stream
     // store data for 4 channels
     int median( int8_t ch, int val ){
     static int vals[4][3];
     static int8_t in[4];
     int8_t j,i,k;                                  // low, median, high
    
       vals[ch][in[ch]] = val;                     // save the new value
       ++in[ch];
       if( in[ch] > 2 ) in[ch] = 0;
    
       j = 0, i = 1, k = 2;                             // pretend they are in the correct order
       if( vals[ch][j] > vals[ch][k] ) k = 0, j = 2;    // swap guess high and low
       if( vals[ch][i] < vals[ch][j] ) i = j;           // is lower than the low guess, pick that one instead
       if( vals[ch][i] > vals[ch][k] ) i = k;           // is higher than the high guess
    
       return vals[ch][i];
     }

  15. #15
    Senior Member
    Join Date
    Sep 2019
    Posts
    123
    Thanks

    My sampling of 8 timings into an array, binning the oldest one each loop seems to work well. I have eliminated all the jitter.
    We are up and running well. I have it sampling all 14 channels very accurately, with very smooth servo operation.

    I found it best to attach the servos for the interrupts, wait 30ms, and then un-attach them again every loop. After un-attaching, I run through all my processing tasks etc before the loop starts again.
    I have found this the best way to ensure the interrupts don't totally disrupt all my other functions, yet I get consistent and fast data from the RC receiver.
    The 30ms seems to be enough to grab all the information reliably.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •