Shop OBEX P1 Docs P2 Docs Learn Events
Is There a PID Function in a Library That can be Called on ? — Parallax Forums

Is There a PID Function in a Library That can be Called on ?

Hello I'm a new user with some PicAxe, PLC and Arduino knowledge and would like to move over to the P1/P2 ecosystem. I'm still looking at the example programs and studying the idea of "cogs".

I have many applications that depend on PID control but after doing a cursory search I have not found a function that can be called when needed. Have I missed that in my search of user projects and documents or are the PID controllers always written as needed by everyone?

I've done rung-by-rung PID code for PLCs and it was tedious. So I'm hoping that it might be possible to put in the set point variable, 3 constants for the PID values and a 4th output variable and call this function as many times as needed, roll, pitch, yaw, slant, skid, throttle, etc. (And include features like integral wind-up and saturation prevention.)

If this already exists then that would be infinitely less painful than writing it myself.

Thank you,
Patrick

Comments

  • There is a PID for the BS2 and I've seen one for the Prop P1.

  • evanhevanh Posts: 15,209

    What you're looking for is the old Object Exchange for the Prop1. It was a great contributor resource but was discontinued a number of years back. An archive of the files has been kept on Github of late - https://github.com/parallaxinc/propeller

    I think the Prop2 edition there is mostly converted Prop1 code. Most Prop2 code proper is spread out here on the forums.

  • I can't recommend the PID authored by "cweber"(sp) because he uses a single coefficient for all 3 terms (can't work) :|

    I just write my own, no rocket science. The P2 is perfect for PID :) I know of high-end ($$$$) motion controllers, boasting 16KHz sample rates but having to sacrifice certain features. This is a walk in the park for the P2, even with my 6 axes.

    I have challenged ChatGPT to come-up with a PID, including acceleration and velocity feedforward. It gets pretty close but it's clueless when it comes to writing efficient code. :D

    I have always used the LM629 ($30+ /axis makes the P2 a giveaway) datasheet. Note how they handle the integral...worked well for me :+1:

    Craig

  • Boy, am I out of date :o

    Craig

  • I just asked ChatGPT to write a PID based on the LM629 datasheet. It ignored the part about bit-shifting the integral. I only use FlexBasic and so I requested BASIC code:

    DIM SHARED Kp, Ki, Kd, Ts, Setpoint, Integral, PrevError, PrevMeasurement, ControlSignal
    
    ' PID gains
    Kp = 0.5
    Ki = 0.2
    Kd = 0.1
    
    ' Sampling time
    Ts = 0.001
    
    ' Setpoint
    Setpoint = 100
    
    ' Initial conditions
    Integral = 0
    PrevError = 0
    PrevMeasurement = 0
    ControlSignal = 0
    
    PRINT "PID Control"
    PRINT "-----------"
    PRINT
    
    DO
        ' Example: Measure the actual value
        DIM Measurement
        Measurement = 0 ' Update with actual measurement
    
        ' Calculate error
        DIM Error
        Error = Setpoint - Measurement
    
        ' Calculate proportional term
        DIM PTerm
        PTerm = Kp * Error
    
        ' Calculate integral term
        DIM ITerm
        Integral = Integral + (Ki * Error * Ts)
        ITerm = Integral
    
        ' Calculate derivative term
        DIM DTerm
        DTerm = Kd * (Measurement - PrevMeasurement) / Ts
    
        ' Calculate control signal
        ControlSignal = PTerm + ITerm + DTerm
    
        ' Update previous measurement and error
        PrevMeasurement = Measurement
        PrevError = Error
    
        ' Apply the control signal to the system
        ' Example: Update the system with ControlSignal
    
        PRINT "Setpoint: " + STR$(Setpoint)
        PRINT "Measurement: " + STR$(Measurement)
        PRINT "Control Signal: " + STR$(ControlSignal)
        PRINT
    
        ' Wait for the sampling time
        SLEEP Ts
    LOOP UNTIL INKEY$ <> ""
    

    Craig

  • Rochester electronics has the LM629M, but for $75

  • @DigitalBob said:
    Rochester electronics has the LM629M, but for $75

    Crazy. I last used it in the early 90s. P2 can hugely out perform it and easily handle 12 axes (limited only by pin count). 16bit motor command, much faster quadrature counting, etc., etc. For what, $20? :+1:

    Craig

  • @Mickster said:

    @DigitalBob said:
    Rochester electronics has the LM629M, but for $75

    Crazy. I last used it in the early 90s. P2 can hugely out perform it and easily handle 12 axes (limited only by pin count). 16bit motor command, much faster quadrature counting, etc., etc. For what, $20? :+1:

    Craig

    Thanks Mickster and DigitalBob for the replies.
    This certainly sounds like what I want especially if written wisely per system resources. Now how do I code it ? :/

    At the very least it has to have wind-up prevention, I think I might have to start with PicAxe or Arduino till I'm caught up on the P1/2 thing. If I can figure out this "spin" type coding I'll post a version for all to use freely with an in depth description.

    -Patrick

  • @PatrickABCD said:

    At the very least it has to have wind-up prevention,

    Anti-wind-up is simply:

    If Integral > Integral_Limit Then Integral = Integral_Limit
    

    My preference is to only apply integral at steady-state and so during motion, Ki=0. In the rare case where the axis following-error (during motion) is greater than ideal (usually a hydraulic servo), I close it up with AFF (acceleration feedforward) and VFF (velocity feedforward) as they are open-loop and therefore do not affect loop stability.
    Another cool trick is to memorize a previously establish "safe" level of integral that can be applied instantly as the axis comes to rest and let it increment from there. I always run very tight loops and I only use Ki when Kp runs out of steam (very near to steady-state).

    Also, don't forget:

    If Error > Error_Limit then [do something] 
    

    In my applications, this is serious and I shut-down but others will do something like reducing the command.

    Derivative:
    The ChatGPT example, above, actually got the DTerm wrong (I since complained to it and it apologised :D and corrected it).

    DTerm = Kd * (Error - PrevError) / Ts
    

    This is typically all we need but I maintain a history of the last 4 PrevErrors. I very rarely need this but for very low velocities or low-resolution feedback, one can achieve a smoother response.

    Roll-your-own PID on the Prop is the only way to go. None of the interrupt clap-trap to be concerned with. The P2 is the dream motion-control device. It can out-perform the motion-control big-boys :+1:

    Craig

  • MicksterMickster Posts: 2,611
    edited 2023-07-09 13:06

    @PatrickABCD said:
    Now how do I code it ? :/

    Dedicate a cog to running the PID loop (pseudo code):

    Do
       waitcnt [1ms for example] (sample time)
    
       [read the hub for updated command]
       [Read the feedback]
       [write the feedback and/or other data to the hub for use by other cog(s)]
    
       [run the PID]
    Loop
    

    Craig

  • @Mickster said:

    Roll-your-own PID on the Prop is the only way to go. None of the interrupt clap-trap to be concerned with. The P2 is the dream motion-control device. It can out-perform the motion-control big-boys :+1:

    Craig

    Your making me feel better about this. ;)
    I like the idea of cogs and have been studying them, the P1/2 silicon fabbed features are clearly superior to the STM32 and other uControllers for a lot of what we do. I loath and despise the existence of the Arduino and PicAxe but they are brain dead simple.
    the "waitcnt" is the command I'm looking at now.

    -Patrick

  • JonnyMacJonnyMac Posts: 8,942
    edited 2023-07-09 20:11

    The ability to sync with the system counter in the Px series is fantastic -- no cycle counting required. The feature functions a bit differently in the two devices (achieving the same result).

    I've only ever written one program that changed clock speed on-the-fly (DEF CON convention badge), so I usually create a constant based on the system frequency to set my loop time.

    Spin1:

      t := cnt
      repeat
        ' loop code
        waitcnt(t += LOOP_TIX)
    

    Spin2:

      t := getct()
      repeat
        ' loop code
       waitct(t += LOOP_TIX)
    

    PASM1:

                            mov       tmr, cnt
                            add       tmr, looptix
    loop_code
                            waitcnt   tmr, looptix
                            jmp       #loop_code
    

    Note: PASM 1 does not allow constants larger that 511 (9 bits); you can create a timing value for low-speed loops like this:

    looptix                 long      CLK_FREQ / 10
    

    I always have a constant in my programs called CLK_FREQ that is the system frequency

    PASM2:

                            getct     bittimer
    loop_code
                            addct1    bittimer, ##(CLK_FREQ / 10)
                            waitct1
                            jmp       #loop_code
    

    One of the many nice features of PASM2 is the ability to have large constants with ##.

    Have fun with the P1 and the P2. They're different from what you're used to, but very nice chips and you have lots of choices how to program them Spin, C, BASIC, Forth. I do all my work in Spin with PASM mixed in where needed or advantageous.

    A neat trick in the P2 is the ability to embed assembly into a Spin2 method. This is great for testing PASM2 fragments or bumping the speed of the method when required (assuming you're not running your code through the FlexSpin compiler).

    pub flash_pin(pin) 
    
      org
    
                            getct     pr0
    loop_code               drvnot    pin
                            addct1    pr0, ##(CLK_FREQ / 5)
                            waitct1   
                            jmp       #loop_code
      end
    
  • Just to elaborate on Jon's example; the time period is unaffected by the loop-execution time. If 1ms is selected, the loop will run @ 1ms, 2ms, 3ms, etc.

    Craig

  • JonnyMacJonnyMac Posts: 8,942
    edited 2023-07-09 22:35

    If 1ms is selected, the loop will run @ 1ms, 2ms, 3ms, etc.

    Yep. The only rule is keeping the loop code timing shorter than the loop execution time. In many of my P1 projects I run a background loop at 1ms so that I can have a running milliseconds timer. I have found that I can get a lot of work done in 1ms, even on a P1 running interpreted Spin at 80MHz. The P2 is faster and more efficient than the P1, hence can get a lot more work done in that same millisecond.

    Another great thing about the Propeller is the ability to time-test code.

    Spin1:

      t := cnt
    
      ' code under test
    
      t := cnt-t-368
    

    Spin2:

      t := getct()
    
      ' code under test
    
      t := getct()-t-40
    

    At the end either segment the variable t holds the number of system ticks required to run the test code. I use this frequently to compare different approaches to a given problem.

  • PatrickABCDPatrickABCD Posts: 4
    edited 2023-07-10 01:32

    Whoa! this is looking better and better!
    So let me see if Im getting this. For time critical embedded applications (basically everything everybody wants to do) You can say : 'execute this code block which takes 456 uS, but run it in 1 mS' and without wasting the 544 uS clock ticks it will make the result available at the 1mS moment ? With those 544 unneeded uS being used else where ?
    So this is superior to the "Pause and waste clock cycles", "delay" or "Wait" type barbaric commands. All this is done without traditional hardware and software ISR ?

  • JonnyMacJonnyMac Posts: 8,942
    edited 2023-07-10 02:59

    No ISR required -- though the P2 does support interrupts.

    In the synchronized loops I showed above, the loop timing will be exactly LOOP_TIX in length. If you have 450us of code, you'll sitting on the waticnt instruction for 550us for a 1ms loop. There are times when we want something to happen at a very specific rate; this is how we do it in the Propeller. If you can squeeze other useful work into that space, great, but you have to code that.

    Here's a real-world example of a 1ms background loop (i.e., it runs in its own cog, separate from the main code loop) that I use in P1 projects that run on the EFX-TEX HC-8+ controller. This updates a timer variable (millis), controls a Red-Green LED with flashing and static color modes, and reads 16 digital inputs through a couple of shift-registers. It's looks like a lot of code but everything gets done in well under a millisecond on a P1 running 80MHz.

    con
    
      { ----------------------------- }
      {  B A C K G R O U N D   C O G  }
      {  - global milliseconds        }
      {  - R/G LED                    }
      {  - TTL/DMX inputs scanning    }
      { ----------------------------- }
    
    
    var
    
      long  bcog                                                    ' background cog #
      long  bcstack[32]                                             ' stack for background cog
    
      long  millis                                                  ' global milliseconds register
    
    
    pri background | t                                              ' launch with cognew()
    
      io.low(R_LED)                                                 ' setup R/G LED pins
      io.low(G_LED)
    
      io.high(LD_165)                                               ' setup x165 io pins
      io.low(CLK_165)
      io.input(DO_165)
    
      millis := 0
    
      t := cnt                                                      ' sync loop timer
      repeat
        waitcnt(t += MS_001)                                        ' run loop every millisecond
        ++millis
        refresh_rg_led
        scan_ttl_ins
    
    
    var
    
      long  rgycolor[2]                                             ' led phase colors
      long  rgytime[2]                                              ' led phase timing (ms)
      long  rgphase                                                 ' current phase (red or green chip)
      long  phasetimer                                              ' time in phase
      long  rgtimer                                                 ' timer for rg process
    
    
    pri refresh_rg_led                                              ' call only from background()
    
      if (++phasetimer => rgytime[rgphase])                         ' done with this phase?
        phasetimer := 0                                             ' yes, reset timer
        rgphase := 1 - rgphase                                      '  and invert phase
    
      if (++rgtimer == 16)
        rgtimer := 0
    
      case rgycolor[rgphase]                                        ' set led to color for phase
        OFF:
          outa[R_LED..G_LED] := %00
    
        GRN:
          outa[R_LED..G_LED] := %01
    
        RED:
          outa[R_LED..G_LED] := %10
    
        YEL:
          if (rgtimer < 2)
            outa[R_LED..G_LED] := %10                               ' 1 red
          else
            outa[R_LED..G_LED] := %01                               ' 15
    
    
    var
    
      long  ttlpins
      long  dmxaddr
    
    
    pri scan_ttl_ins : tempin                                       ' call only from background()
    
    '' Scan TTL and DMX address inputs
    
      outa[LD_165] := 0                                             ' blip Shift/Load line
      outa[LD_165] := 1
    
      tempin := ina[DMX_A8]                                         ' bit16
    
      ' unrolled for best speed
    
      tempin := (tempin << 1) | ina[DO_165]                         ' bit15
      outa[CLK_165] := 1                                            ' blip clock
      outa[CLK_165] := 0
      tempin := (tempin << 1) | ina[DO_165]
      outa[CLK_165] := 1
      outa[CLK_165] := 0
      tempin := (tempin << 1) | ina[DO_165]
      outa[CLK_165] := 1
      outa[CLK_165] := 0
      tempin := (tempin << 1) | ina[DO_165]
      outa[CLK_165] := 1
      outa[CLK_165] := 0
      tempin := (tempin << 1) | ina[DO_165]
      outa[CLK_165] := 1
      outa[CLK_165] := 0
      tempin := (tempin << 1) | ina[DO_165]
      outa[CLK_165] := 1
      outa[CLK_165] := 0
      tempin := (tempin << 1) | ina[DO_165]
      outa[CLK_165] := 1
      outa[CLK_165] := 0
      tempin := (tempin << 1) | ina[DO_165]
      outa[CLK_165] := 1
      outa[CLK_165] := 0
    
      tempin := (tempin << 1) | ina[DO_165]                         ' bit7
      outa[CLK_165] := 1
      outa[CLK_165] := 0
      tempin := (tempin << 1) | ina[DO_165]
      outa[CLK_165] := 1
      outa[CLK_165] := 0
      tempin := (tempin << 1) | ina[DO_165]
      outa[CLK_165] := 1
      outa[CLK_165] := 0
      tempin := (tempin << 1) | ina[DO_165]
      outa[CLK_165] := 1
      outa[CLK_165] := 0
      tempin := (tempin << 1) | ina[DO_165]
      outa[CLK_165] := 1
      outa[CLK_165] := 0
      tempin := (tempin << 1) | ina[DO_165]
      outa[CLK_165] := 1
      outa[CLK_165] := 0
      tempin := (tempin << 1) | ina[DO_165]
      outa[CLK_165] := 1
      outa[CLK_165] := 0
      tempin := (tempin << 1) | ina[DO_165]                         ' bit0
    
      ttlpins := tempin.byte[0]                                     ' update global vars
      dmxaddr := tempin >> 8
    

    Here's another example from my servo driver for the P1. This divides the 20ms servo refresh window into 8 slots, refreshing each servo (up to 8) during the slot. The P1 counter (each cog has two) is used as a pulse generator which allows that 2.5ms window time to be used to calculate the position update for the next cycle. Simple code, but lets me control up to 8 servos with a cog, and control movement speed from one position to another.

    pri servo8(count, base) | slottix, t, ch, chmask                ' launch with cognew()
    
    '' Runs up to 8 servos
    '' -- count is number of servos (1 to 8)
    '' -- base is LSB pin of contiguous group
    '' -- runs in own cog; uses ctra
    
      count := 1 #> count <# 8                                      ' keep legal
    
      outa := 0                                                     ' all off
      dira := ($FF >> (8 - count)) << base                          ' set outputs
    
      frqa := 1                                                     ' preset for counter
      phsa := 0
    
      slottix := 2_500 * us001                                      ' ticks in 2.5ms slot
    
      t := cnt                                                      ' sync slot timing
      repeat
        repeat ch from 0 to 7                                       ' run 8 slots (20ms)
          chmask := 1 << ch
          if ((ch < count) and (chmask & enablemask))               ' active and enabled channel?
            ctra := (%00100 << 26) | (base + ch)                    ' PWM/NCO mode on servo pin
            phsa := -pos[ch]                                        ' set pulse timing / start pulse
            if (pos[ch] < target[ch])                               ' update for speed
              pos[ch] := (pos[ch] + delta[ch]) <# target[ch]
            elseif (pos[ch] > target[ch])
              pos[ch] := (pos[ch] - delta[ch]) #> target[ch]
          waitcnt(t += slottix)                                     ' let slot finish
          ctra := 0                                                 ' release ctra from pin
    
  • MicksterMickster Posts: 2,611
    edited 2023-07-10 11:37

    Oh, it's mind-blowing. ;)

    In my case, I have 6 PIDs but dual-loop (12 encoders). In the PID loop, I also added a bunch of do-nothing multiplications (floating point) to allow for future bloat.

    I need to double-check this when I next power-up but I'm pretty sure that I went sub microsecond (just for the heck of it) with no problem.

    Not sure if you plan to use the index pulse for home referencing but this was really irritating with a megabuck big-name controller because it only ever read the encoder during the PID loop. Running at what appears to be the industry norm of 1ms, to be accurate to a single count, the recommended "homing" velocity was 250 cnts/sec. Well in my case this means < 1mm/sec on a linear axis :o Talk about painful. With the Prop, I can have a cog - independent of the PID cog - dedicated to rapidly scanning the encoder count and the index pulse allowing me to "home" at a more reasonable velocity...Nice! In the past, I would receive reports that my machines were "hanging-up" during the homing sequence...nope, they just take forever to find the index pulse :D

    Once the machine is "homed", the scanning cog can be freed-up for other tasks.

    Craig

  • @JonnyMac said:

    In many of my P1 projects I run a background loop at 1ms so that I can have a running milliseconds timer.

    Same here. I have several. It's nice to be able to reset a timer because my stuff rarely gets powered-down.

    Pump_Timer = 0
    .
    .
    .
    .
    If No_Activity and Pump_Timer > 30000 Then ' Machine hasn't been cycled for 30 secs
       ShutdownPump
    Endif
    

    Craig

  • @PatrickABCD said:
    Whoa! this is looking better and better!
    So let me see if Im getting this. For time critical embedded applications (basically everything everybody wants to do) You can say : 'execute this code block which takes 456 uS, but run it in 1 mS' and without wasting the 544 uS clock ticks it will make the result available at the 1mS moment ? With those 544 unneeded uS being used else where ?
    So this is superior to the "Pause and waste clock cycles", "delay" or "Wait" type barbaric commands. All this is done without traditional hardware and software ISR ?

    It's kinda like a timer interrupt but without the latency. The wasted clock-cycle anxiety is really a non-issue because it's one of eight processors. Like Jon, I use the remaining clock-cycles for incrementing timers and toggling an output to an external watchdog.

    Craig

  • evanhevanh Posts: 15,209
    edited 2023-07-10 08:34

    @Mickster said:
    Not sure if you plan to use the index pulse for home referencing but this was really irritating with a megabuck big-name controller because it only ever read the encoder during the PID loop.

    That's quite amusing - The limitations of being tied to the servo loop for bit-bashing. Certainly not a well thought out solution when it comes to such a tiny width as the index pulse. It also sounds like a tail of being forced to use the official libraries as well. With zero documentation on register level access.

    But now you bring it up, CNCs do all seem to go extremely slow moving off the home sensor to the index mark. I hadn't really thought it was a necessity so much as just copy-catting.

  • @evanh said:

    That's quite amusing - The limitations of being tied to the servo loop for bit-bashing. Certainly not a well thought out solution when it comes to such a tiny width as the index pulse.

    >

    This was the #1 priority for me and prior to going all-in with the P2, I was playing with the LS7366R which features a hardware latch.
    T_Chap uses this with the P1 and with it being SPI, he has battery backup to this and also his incremental encoders so he rarely, if ever, needs to re-home. The 7366R also features a power-fail flag so he knows if the battery supply has faded.

    Craig

Sign In or Register to comment.