CNT adding up to trouble
Erlend
Posts: 612
So, since I put together the code I have been running this 'scheduler-based' servo control for some while, assuming the twichting of the servo was due to EM noise that I would fix when doing up good wiring. Not so. Some length of screened wire later I am homing in on the trouble - the algorithm.
Something is not good in the way CNT is used here. I suspect the repeaded adding up overflows the numbers. The symptom is that cyclically - each few tens of iterations - the servo jitters into a couple of other positions before it goes back to the correct one for a while.
My brain is not wired for these kind of flip-around counter math. I need help.
Here's the essential code, causing trouble even when everything else is commented out:
The principle being that the switch on and switch off servo signal points are defined by means of CNT value,
and that each time the next points are set up by adding to the CNT derived value. But it obviously adds up to trouble.
Erlend
Here's the full module code: (with commenting out of most of it)
Something is not good in the way CNT is used here. I suspect the repeaded adding up overflows the numbers. The symptom is that cyclically - each few tens of iterations - the servo jitters into a couple of other positions before it goes back to the correct one for a while.
My brain is not wired for these kind of flip-around counter math. I need help.
Here's the essential code, causing trouble even when everything else is commented out:
'Setting up the first run of 'Milestones' as CNT values '------------------------------------------------------------------------------------------------------------------------------------------------ iServoRate := 20*mSec 'Servo every 20ms iServoPuls := 1200*uSec 'Servo Pos 1000..2000uS nominal pulse length ** will be recalculated by control algorithm iServoPulsStart := CNT + iServoRate 'Define 'milestone' as CNT value iServoPulsEnd := iServoPulsStart + iServoPuls 'Define 'milestone' as CNT value '-------------------------------------------------------------------------------------------------------------------------------------------------- REPEAT UNTIL iPumpStrokes > LiSPvol 'Continously check CNT to detect 'milestones' and perform timed actions - until setpoint volume 'Do the Servo IF (CNT - iServoPulsStart) > 0 'If the iServoPulsStart milestone has been reached OUTA[LPINservo]:= 1 'do what has to be done, iServoPulsStart+= iServoRate 'then set up the next time milestone IF (CNT - iServoPulsEnd) > 0 OUTA[LPINservo]:= 0 iServoPulsEnd:= iServoPulsStart + iServoPuls '---------------------------------------------------------------------------------------------------------------------------------------------------
The principle being that the switch on and switch off servo signal points are defined by means of CNT value,
and that each time the next points are set up by adding to the CNT derived value. But it obviously adds up to trouble.
Erlend
Here's the full module code: (with commenting out of most of it)
{========================================================================================================================================================================================= This code is based on a scheme where CNT is continually checked to see when it is time to read, set, or reset values - in order to maintain two pulse trains and one frequency counter. CNT*mSec: |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| read CNT in a REPEAT loop, and act at each 'milestone' as shown Servo50Hz: : 20mS : : : : : : : : _ _ _ _ _ __ _ _ _ ServoPulse: | |________________| |_________________| |_________________| |_________________| |_________________| |___ variable pulse width 1000..2000us, but pulse every 20mS __________ __________ __________ __________ ___________ VariFreq: | |_______| |________| |_______________| |________________| variable space witdh 200ms..16ms, but every pulse duration 10mS FreqCount: Now_____________________________________________ Now______________________________________________ Now____ read counter 2/Sec to get frequency, store it, and reset counter RunAlgorithm Now________________________________________________________________________________________________Now_ run control algorithm and update control values every sec ======================================================================================================================================================================= } CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 ' use 5MHz crystal clk_freq = (_clkmode >> 6) * _xinfreq ' system freq as a constant mSec = clk_freq / 1_000 ' ticks in 1ms uSec = clk_freq / 1_000_000 ' ticks in 1us CiSecsPerDegC_x10 = 2 ' S_x10 time to heat 1 degC at full blast VAR LONG HWCstack[512] LONG iTempWater LONG iPressWater LONG iFlameRate LONG iLevelWater LONG iOpmode LONG iServoRate LONG iServoPuls LONG iServoPulsStart LONG iServoPulsEnd LONG iPumpRate LONG iPumpPuls LONG iPumpPulsStart LONG iPumpPulsEnd LONG iFreqCountIval LONG iFreqCountNow LONG iAlgorithmIval LONG iAlgoritmNow LONG iPumpStrokes LONG iFreqUV LONG LptrTempWater LONG LptrPressWater LONG LptrFlameRate LONG LptrLevelWater LONG LptrHeatServo LONG LptrOpmode LONG LPINpump LONG LPINignt LONG LPINservo LONG LPINfreqCount LONG LiSPtemp LONG LiSPpress LONG LiSPtime LONG LiSPvol LONG iHeat LONG iHeatNominal LONG iHeatP LONG iHeatI LONG iHeatD LONG iGainP LONG iGainI LONG iGainD LONG iTempWaterOld LONG iFreqPump LONG iHeatFirst LONG iProcessState BYTE cog BYTE iPumpWait OBJ PUB Start(PINpump, PINignt, PINservo, PINfreqCount{NEW}, ptrTempWater, ptrPressWater, ptrFlameRate, ptrLevelWater, ptrHeatServo, ptrOpmode, iSPtemp, iSPpress, iSPtime, iSPvol): Success Stop Success:= (cog:= COGNEW(HotWcontrol(PINpump, PINignt, PINservo, PINfreqCount{NEW}, ptrTempWater, ptrPressWater, ptrFlameRate, ptrLevelWater, ptrHeatServo, ptrOpmode, iSPtemp, iSPpress, iSPtime, iSPvol), @HWCstack) +1) PUB Stop IF Cog COGSTOP(cog~ - 1) PUB HotWcontrol(PINpump, PINignt, PINservo, PINfreqCount{NEW}, ptrTempWater, ptrPressWater, ptrFlameRate, ptrLevelWater, ptrHeatServo, ptrOpmode, iSPtemp, iSPpress, iSPtime, iSPvol) LptrTempWater:= ptrTempWater 'Make life easier by copying into local public variables LptrPressWater:= ptrPressWater LptrFlameRate:= ptrFlameRate LptrLevelWater:= ptrLevelWater LptrHeatServo:= ptrHeatServo LptrOpmode:= ptrOpmode LPINpump:= PINpump LPINignt:= PINignt LPINservo:= PINservo LPINfreqCount:= PINfreqCount LiSPtemp:= iSPtemp LiSPpress:= iSPpress LiSPtime:= iSPtime LiSPvol:= iSPvol ReadGlobals CASE iOpmode 0: Shutdown 1: HWcontrol 2: Time 3: Warm 4: Cold 5: Pon 6: Poff 7: Pilot OTHER: Shutdown PRI Shutdown 'Shutdown pump, close servo, shut off igniter float outputs, COGSTOP ServoPos(2220) OUTA[LPINpump]:= 0 DIRA[LPINpump]~ OUTA[LPINservo]:= 0 DIRA[LPINservo]~ Stop PRI HWcontrol 'Configure the counter and do a first frequency count to measure flame rate, then if not burning ignite it '------------------------------------------------------------------------------------------------------------------------------------------------- CTRA:= %01010 << 26 + LPINfreqCount 'Set up counter to POSEDGE mode (bit26-30, using pin LPINfreqCount (bit 0-5), and FRQA:= 1 'using one count per edge to accumulate into PHSA WAITCNT(clkfreq/2 + cnt) FreqCount IF iFlameRate < 200 'Light the flame if not burning Ignite(1) iFreqUV:= PHSA~ 'reset counter WAITCNT(clkfreq/2 + cnt) FreqCount 'Set up some parameters for the 'milestone' loop '------------------------------------------------------------------------------------------------------------------------------------------------- DIRA[LPINservo]~~ 'Define as outputs DIRA[LPINpump]~~ iPumpStrokes:= 0 'Set volume counter to zero iHeat:= 50 'Set initial values iHeatNominal:= 50 iGainP:= 4 iGainI:= 10 iGainD:= 5 iTempWater:= LONG[LptrTempWater] 'The temperature when the process begins iHeatFirst:= (CiSecsPerDegC_x10 * (LiSPtemp - iTempWater))/10 'Calculate how long to heat initially, to reach pump start temp - in Sec iTempWaterOld:= iTempWater 'Ref for the D-part of control algorithm IF iTempWater > LiSPtemp iPumpWait:= FALSE 'A flag used to avoid pumping when water is not warm enough ELSE iPumpWait:= TRUE 'Setting up the first run of 'Milestones' as CNT values '------------------------------------------------------------------------------------------------------------------------------------------------ iServoRate := 20*mSec 'Servo every 20ms iServoPuls := 1200*uSec 'Servo Pos 1000..2000uS nominal pulse length ** will be recalculated by control algorithm iServoPulsStart := CNT + iServoRate 'Define 'milestone' as CNT value iServoPulsEnd := iServoPulsStart + iServoPuls 'Define 'milestone' as CNT value iPumpRate := 50*mSec {20Hz} 'Pump Freq 5..60Hz = 200mS..16mS periode ** will be recalculated by control algorithm iPumpPuls := 10*mSec 'Pump pulse length 10mS iPumpPulsStart := CNT + iPumpRate 'Define 'milestone' as CNT value iPumpPulsEnd := iPumpPulsStart + iPumpPuls 'Define 'milestone' as CNT value iFreqCountIval:= 500*mSec 'Read, store and reset counter every 500mS iFreqCountNow:= CNT + iFreqCountIval 'Define 'milestone' as CNT value iAlgorithmIval:= 2000*mSec 'Run control algorithm /sec and update values iAlgoritmNow:= CNT + iAlgorithmIval 'Define 'milestone' as CNT value 'Now start 'milestone' based hot water control routines '-------------------------------------------------------------------------------------------------------------------------------------------------- REPEAT UNTIL iPumpStrokes > LiSPvol 'Continously check CNT to detect 'milestones' and perform timed actions - until setpoint volume 'Do the Servo IF (CNT - iServoPulsStart) > 0 'If the iServoPulsStart milestone has been reached OUTA[LPINservo]:= 1 'do what has to be done, iServoPulsStart+= iServoRate 'then set up the next time milestone IF (CNT - iServoPulsEnd) > 0 OUTA[LPINservo]:= 0 iServoPulsEnd:= iServoPulsStart + iServoPuls { 'Do the pump IF (CNT - iPumpPulsStart) > 0 IF NOT iPumpWait 'Only drive pump if not iPumpWait OUTA[LPINpump]:= 1 iPumpStrokes += 1 'Keep tally of total pump strokes (=volume) iPumpPulsStart += iPumpRate IF (CNT - iPumpPulsEnd) > 0 OUTA[LPINpump]:= 0 iPumpPulsEnd:= iPumpPulsStart + iPumpPuls 'Do the frequency counter IF (CNT - iFreqCountNow) > 0 FreqCount iFreqCountNow += iFreqCountIval 'Do the control algorithm IF (CNT - iAlgoritmNow) > 0 'Perform PID control of temperature and simple pump speed control ControlAlgorithm iAlgoritmNow += iAlgorithmIval } LONG[LptrFlameRate]:= -1 'Update of flamerate stops, so set false LONG[LptrOpmode]:= 15 Shutdown '----------------- unfinished procedures------------------- PRI Time 'Ignite (if not burning) heat, and pump setpoint time at max setpoint pressure, min and max setpoint temperature PRI Warm 'Ignite (if not burning) and burn until setpoint temperature, then shut down PRI Cold 'Run pump to setpoint time at max setpoint pressure PRI Pon 'Pump on at nominal speed PRI Poff 'Pump off PRI Pilot 'Ignite (if not burning) heat and run at minimum flame '--------------------- supporting procedures ---------------------- PRI ControlAlgorithm 'Regulating control with rudimentary PID functions - executes 1/sec ReadGlobals 'Update measurements 'Do shutdowns IF iOpmode== 911 'Check for stop command from parent LONG[LptrOpmode]:= 0 Shutdown IF iFlameRate < 100 'Flameout shutdown LONG[LptrOpmode]:= -2 Shutdown IF iTempWater > 150 'Overheat shutdown LONG[LptrOpmode]:= -3 Shutdown IF iPressWater > 1500 'Overpressure shutdown LONG[LptrOpmode]:= -4 Shutdown 'Do pump enable/disable control IF iTempwater > LiSPtemp 'Only allow pump to run when water is warm enough iPumpWait:= FALSE IF iTempwater < LiSPtemp - 5 'Allow some hysteresis iPumpWait:= TRUE 'Do temperature control LONG[LptrOpmode]:= iHeatFirst IF iHeatFirst-- > 0 'While still counting down the HeatFirst duration (1/Sec) iHeat:= 100 'run heating at full blast, unless.. IF (LiSPtemp - iTempWater) < 10 'temperature is approaching setpoint iHeat:= 60 'then run heat at half blast ELSEIF LiSPtemp - iTempWater > 5 'simplistic propotional control iHeat:= 100 ELSEIF iTempWater - LiSPtemp > 5 'simplistic propotional control iHeat:= 60 ELSEIF iTempWaterOld > iTempWater 'if temperature is dropping iHeat:= 100 'run heating at full blast, unless... IF iTempWater - LiSPtemp > 30 'temperature is really too high iHeat:= 60 'run heating at minimum ELSEIF iTempWater > iTempWaterOld 'if temperature is rising iHeat:= 60 'run heating at minimum, unless... IF LiSPtemp - iTempWater > 5 'temperature is really too low iHeat:= 100 'run heating at full blast iTempWaterOld:= iTempWater 'Convert to servo value '100% = 950uSec, 0% = 2200 uSec iServoPuls:= ( 100*950 + ( (100 - iHeat) * (2200 - 950) ) )/100 LONG[LptrHeatServo]:= iServoPuls iServoPuls*= uSec 'Speed control IF NOT iPumpWait IF iPressWater > LiSPpress + 50 iFreqPump:= (iFreqPump - 5) #> 5 'Pump speed not allowed below 5Hz IF iPressWater < LiSPpress iFreqPump:= (iFreqPump + 5) <# 50 'Pump speed not allowed above 50Hz iPumpRate:= (1000/iFreqPump)*mSec PRI Ignite(Sec) 'Method to ignite gas burner DIRA[LPINignt]~~ 'Set I/O pin to output direction OUTA[LPINignt]:= 1 'Energize igniter WAITCNT(clkfreq + cnt) 'Warm up igniter tip ServoPos(950) 'Open gas valve fully WAITCNT(Sec*clkfreq + cnt) 'Let glow for Sec OUTA[LPINignt]:= 0 'Switch off Ignt DIRA[LPINignt]~ 'Set I/O pin to input, ie. float ServoPos(1100) 'Set gas valve low burn PRI ServoPos(position) 'Method to run a servo just long enough, then let it free, assuming the friction of the valves prevents movement DIRA[LPINservo]~~ 'Set I/O pin to output direction repeat 25 '1/2 S to allow servo to move to new pos WAITCNT(clkfreq/50 + cnt) '50 hz update frequency OUTA[LPINservo]:= 1 'Pulse high WAITCNT(position*uSec + cnt) 'Servo pulse duration 0% eq 2220 uS, 100% eq 950mS OUTA[LPINservo]:= 0 'Puse low PRI ReadGlobals 'Method to copy values over from global variables owned by the parent method iTempWater:= LONG[LptrTempWater] iPressWater:= LONG[LptrPressWater] iFlameRate:= LONG[LptrFlameRate] iLevelWater:= LONG[LptrLevelWater] iOpmode:= LONG[LptrOpmode] PRI FreqCount iFreqUV:= PHSA~ 'Capture counter accumulation and then post-zero iFlamerate:= LONG[LptrFlameRate]:= iFreqUV 'Calculate real frequency and store in local and global variable DAT DaaaBbbbb BYTE "string_data",0 {{
Comments
Erlend
EDIT: some testing later: not quite solved, there is still a rytmic twitch - every .8sec or so. So small it is not a problem, but it is still there.
EDIT: this may actually be noise, it is synch with writes to the terminal
-Phil
Cant do that (waitcnt), there are three more tasks to be tended to (see full listing), pulsing the impuls pump, reading the flame counts, and trigging the control algorithm.
Erlend
Using an if count is likely the cause of your jitter.
I could be wrong, though SPIN is not very deterministic.
The problem is not about Spin not making it around - there is plenty of time to spare between each 20mS DIRA operation. I suspect it is something about the way I use CNT.
P1 only has 8 cogs, and I don't have one to spare.
Erlend
An alternative to waitcnt may be to count up and use PHSA as your "waitcnt", but in this case you do not hold up the other code.
IF PHSA > iServoPulsStart
'OUTA[LPINservo]~~
'PHSA~
IF PHSB > iServoPulsEnd
'OUTA[LPINservo]~
'PHSB~
You may be able to slightly improve the timing of your loop.
OUTA[LPINservo]:= 1 OUTA[LPINservo]~~
OUTA[LPINservo]:= 0 OUTA[LPINservo]~
Can you put all four tasks (servo pulsing, pulsing the impuls pump, reading the flame counts, and trigging the control algorithm) in a single loop so you can use a simple waitcnt for timing?
As another test, try your existing code but insert a line with a fixed waitcnt period for servo off instead of CNT.
...
I use .adjust() to back-up the timer two where it should be in case some process took longer than expected; as long as the timer duration was not violated, this should get things back in sync.
BTW, if you only have one and have a servo you can use one of the counters to generate a servo pulse. You'll get much better precision in the pulse and the code will be simpler.
Code has been updated (26 APR 2015)
I think Spin is just not fast enough to handle all your tasks without havy jitter.
The worst case is the Servopulse, because it has the shortest time, so you should replace the pulselength task with a Propeller counter.
In general for such a task scheduler the loop must run as fast as possible to get low jitter. You should not have long routines inside this loop. So your ControlAlgorythm as one of the tasks is problematic, because it will stall the scheduler loop for a long time when the 'iAlgoritmNow' is reached. You should make the regulator as short as possible and split it maybe up in several shorter tasks.
There is a chance that it works anyway when the Servopulse is contolled by a counter and for the other tasks a jitter is not a big problem.
Here is an untested codepart that uses counter B for the servopulse:
Andy
The glitch isn't so regular after all, but it is that frequency range, and with a slightly varying rythm to it (almost jazz). If no one can spot any real errors in my coding, maybe there is none, maybe it is some noise after all.
I could put it all in a waitcnt loop as you say, as long as the lesser frequent operations can be done at multiples of the more frequent ones, but that isn't the case. The servo is fixed frequency, variable pulse length, the pump is fixed pulse length, varable frequency.
Besides, I do think this particular coding scheme is an elegant one (thanks to @Ariba), so I would rather debug it fully, istead of choosing some other scheme.
Erlend
Hey Andy - you put me onto this path, months ago. And I am convinced it is a good one. I have tried to both remove and add algorithms into the loop, but it has no effect, indicating there is lots of margins for the code to make it around before 20mS. But now (as I am writing) you put me onto an idea: make the code such that the slower tasks can not be triggered inside the execution of a servo pulse, i.e. not inside the microseconds duration of the pulse. During the 20ms intervals between pulses there should be plenty of time. I think this is an Eureka!
Ow you one,
Erlend
Your postings ALWAYS help me. This goes into my solutions library for when I meet upon the next challenges.
Erlend
But I would also generate the servopulse time with a counter, this is the most critical part. And as you see in the above example, it's even simpler and faster.
Andy
Erlend
-Phil
There is another answer, provided you can move the timing pieces of the code into a PASM cog. You say you have no cogs left over, but I would bet that you can free one up by moving more code running in other cogs into that same assembler cog. Sort of concentrating all the timing things into a single cog. To effect this, that assembler cog would run a real time scheduler kernel which I can give you. That would allow up to 8 individual timers/threads to operate simultaneously, yet independently of each other. In other words, a change in the timing of one will not (appreciably) affect the others.
The scheduler I have is very SPIN friendly, and interfaces bidirectionally to other cogs through a single long in hubram with single reads or writes. It allows any of the 8 threads in a cog to be suspended, resumed, updated, deleted, or (soon) to be replaced with other code, all while the unaffected threads continue to operate normally.
You probably could expand this concept into other cogs, each running such a scheduler, concentrating more code into each cog, freeing up space to do yet more things in your application. But it would mean you need to convert those codes into PASM, and that might not be your preference.
Let me know if this is of interest..... I am absolutely convinced it will easily solve your problem.
Cheers,
Peter (pjv)
True, I could use a counter in another cog, but I would like each cog process to be as selfsufficient as possibly, I believe that makes for a cleaner design.
Erlend
Erlend
Thanks. I was never a friend with ASM, not even PASM. My whole project is written in Spin.
Erlend
No argument on the having plenty of time.
Though use waitcnt instead of the if statement to reduce jitter.
Andy
Thanks to you the problem is fixed. I experimented with the various proposed solutions and decided on using WAITCNT for the servo pulse.
Erlend
-Phil
I don't quite see - why does it help to delay the pulse start by clkfreq/10000?
Erlend
-Phil
OK, now I get it. Thanks.
Erlend
is only good for delays less that 2^31 ticks (about 26ish seconds); if you need longer periods, my little time object will make things easy. I'm now using it for everything as it hides the ugly math!