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.
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.
I have always used the LM629 ($30+ /axis makes the P2 a giveaway) datasheet. Note how they handle the integral...worked well for me
Craig
Boy, am I out of date
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
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?
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
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:
and corrected it).
The ChatGPT example, above, actually got the DTerm wrong (I since complained to it and it apologised
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
Craig
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
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
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
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.
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 ?
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
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
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 
Once the machine is "homed", the scanning cog can be freed-up for other tasks.
Craig
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
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
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.
>
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