Trouble with multiple cog methods reading same variables
Erlend
Posts: 612
Just when I believed I had a breakthrough-
I am making a gas burner hot water pumping process as part of an espresso machine. The code is organized such that the top level object defines a list of contigous varables that will hold all measured and calculated process varables. A pointer to this block of varables is passed to a ADC scanner object running in a separate object. Same manner for other I/O systems. These all do the job of feeding the variables with values.
In the same manner the pointer is passed to an object which displays the variables (for debugging) through PST. For the HotWaterControl, also running in a dedicated cog, pointers are passed as parameters to give the object access to such values as measured water temperature.
Trouble is, some times it works, sometimes not, so I got suspicious that the various objects were competing for access to the variables. Tried putting in some delays in the various repeat loops, but that did not fix it. As an example:
I have already tried setting all Stack sizes very high.
Something I do not know that I should?
Erlend
I am making a gas burner hot water pumping process as part of an espresso machine. The code is organized such that the top level object defines a list of contigous varables that will hold all measured and calculated process varables. A pointer to this block of varables is passed to a ADC scanner object running in a separate object. Same manner for other I/O systems. These all do the job of feeding the variables with values.
In the same manner the pointer is passed to an object which displays the variables (for debugging) through PST. For the HotWaterControl, also running in a dedicated cog, pointers are passed as parameters to give the object access to such values as measured water temperature.
Trouble is, some times it works, sometimes not, so I got suspicious that the various objects were competing for access to the variables. Tried putting in some delays in the various repeat loops, but that did not fix it. As an example:
'Wait with starting pump until initial heating of water is finished
'-------------------------------------------------------------------------------------------------------------------------------------------------
REPEAT UNTIL LONG[LptrTempWater] > 90 'LiSPtemp 'Wait until water is hot enough for brewing
WAITCNT(clkfreq/2 + cnt)
does not (always) work, but sometimes triggers only when the temperature (as displayed through PST) reaches much higher, say 115 deg - but this varies.I have already tried setting all Stack sizes very high.
Something I do not know that I should?
Erlend

Comments
The technique you're using is very common and cogs can only access variables one (cog) at a time. A cog will never see a halfway written variable.
The problem is likely something else. If you're willing to post an archive of your code and describe what you expect it to do and what it does do, some of us would likely be willing to take a look at it.
-the hotwatercontrol
{{ HeatWaterControl, Controls impulse pump and gas burner to feed hot water at pressure, ver.1, ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- Runs in separate cog, is called (start) to run automatic hot water control, continously reading control parameters from global varables (temp, pressure, volume, duration, opmode) to ignite, control, and monitor a gas burner, and run and modulate an impulse pump, based on setpoints passed in the call. Also measures for flame-out, and initiates a shutdown in case of loss of flame or failure to ignite ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- }} { Aknowledgements: I got the idea of CNT-multitaksing from 'Ariba' at the parallax forum Naming convention: Variables starts with: s: String, i: Integer, f: Floating point, c: Character, d: Consecutive bytes containing data sets Constant starts with: Cs: String, Ci: Integer, Cf: Floating point, Cc: Character, Cd: Consecutive bytes containing data sets DAT starts with: Ds: String, Di: Integer, Df: Floating point, Dc: Composite, Dc: Character, Dd: Consecutive bytes containing data sets PIN constant numbers are: 'PINfunction' wher function is an abbreviation of the signal/function Pointers start with ptr followed by the variable core name, e.g. ptrVolume Reserved words are always in capitals Included objects are called by three or four letter lower case names Methods (PUB and PRI) have first letter capital then lower case descriptive. Local variables are free text beginning in lower case, or if they are copies of globals begin with capitol L ========================================================================================================================================================================================= 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 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 BYTE cog 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 '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:= 1000*mSec 'Run control algorithm /sec and update values iAlgoritmNow:= CNT + iAlgorithmIval 'Define 'milestone' as CNT value '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 < 2 '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:= 2 iGainI:= 4 iGainD:= 1 iTempWaterOld:= iTempWater 'Wait with starting pump until initial heating of water is finished '------------------------------------------------------------------------------------------------------------------------------------------------- REPEAT UNTIL LONG[LptrTempWater] > 90 'LiSPtemp 'Wait until water is hot enough for brewing WAITCNT(clkfreq/2 + cnt) '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 OUTA[LPINpump]:= 1 iPumpPulsStart += iPumpRate iPumpStrokes += 1 'Keep tally of total pump strokes (=volume) 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 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 ReadGlobals 'Update measurements 'Do shutdowns IF iOpmode== 0 'Check for stop command from parent Shutdown IF iFlameRate < 2 'Flameout shutdown Shutdown IF iTempWater > 150 'Overheat shutdown Shutdown IF iPressWater > 1900 'Overpressure shutdown Shutdown 'Propotional control IF (LiSPtemp - iTempWater) > 2 'Deadband 2degC iHeatP:= (iHeatNominal * iGainP * (LiSPtemp - iTempWater)/10) IF (iTempWater - LiSPtemp) > 2 iHeatP:= (iHeatNominal / (iGainP * (1+ ||(LiSPtemp - iTempWater)/10))) iHeatP:= 0 #> iHeatP <# 100 'Integrational control IF (LiSPtemp - iTempWater) > 1 'Deadband 1 degC iHeatI+= iGainI 'Increment detemines ramp-up/down rate IF (LiSPtemp - iTempWater) < -1 iHeatI-= iGainI iHeatI:= -30 #> iHeatI <# 30 'Derivative control IF iHeatD > 1 'If there is a D-part, ramp it down at each iteration iHeat-= 1 IF iHeatD < -1 iHeatD += 1 IF ||(iTempWaterOld - iTempWater) > 3 'Threshold temperature change for Derivative to kick in iHeatD:= iGainD * (iTempWaterOld - iTempWater) iTempWaterOld:= iTempWater iHeatD:= -30 #> iHeatD <# 30 'Sum up and limit iHeat:= 80 #> (iHeatP + iHeatI + iHeatD) <# 100 '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 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 {{ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. }}the adc
{{ ADCreaderConv, reads MCP3208 A/D converter over a SPI protocol, and calls "ScaleUnits*" to convert raw adc into engineering units value, does not do averaging or filtering Erlend Fjosna 2014 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- Reads MCP3208 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- Acknowledgments: Virtually all the code is originally by Jon "JonnyMac" McPhalen, it is only slightly modified by me. ======================================================================================================================================================================= Hardware & wiring: 3v3-5v0 MCP3208  ┌─────────────────┐ │ ch0 ────┤1 CH0 VDD 16├──┫ ch1 ────┤2 CH1 VREF 15├──┘ ch2 ────┤3 CH2 AGND 14├──┐ ch3 ────┤4 CH3 CLK 13├──│──────── clk ch4 ────┤5 CH4 DOUT 12├──│──┐ 3k3 ch5 ────┤6 CH5 DIN 11├──│────┻── dio ch6 ────┤7 CH6 /CS 10├──│──────── cs ch7 ────┤8 CH7 DGND 9├──┫ └─────────────────┘ │  ======================================================================================================================================================================= }} CON _clkmode = xtal1 + pll16x 'Standard clock mode * crystal frequency = 80 MHz _xinfreq = 5_000_000 OBJ conv: "ScaleUnitsGS1" var long cog ' cog running reader long cs ' pins used long clk long dio long channels long p_adcvals ' pointer to adc readings long adc[8] ' adc readings long stack[128] ' memory for reader cog pub start(cspin, clkpin, diopin) '' Start free-running MCP3204/8 reader '' -- store readings in object var space '' -- assumes 8-channel MCP3208 return startx(cspin, clkpin, diopin, 8, -1) pub startx(cspin, clkpin, diopin, nchans, p_analog) '' Start free-running MCP3204/8 reader '' -- store readings at user-specified location (parent var space) '' -- nchans is number of channels, 1..8 '' -- p_analog is a pointer to a block of eight longs '' * use -1 for local storage stop ' kill if already running longmove(@cs, @cspin, 3) ' copy pins channels := 1 #> nchans <# 8 ' keep channel count legal if (p_analog < 0) p_adcvals := @adc ' use internal array else p_adcvals := p_analog ' use specified array cog := cognew(mcp3208, @stack) + 1 ' start reader cog return cog pub stop '' Unload MCP3204/8 reader cog if (cog) ' if running cogstop(cog - 1) ' stop cog := 0 longfill(p_adcvals, 0, channels) pub read(ch) '' Get last read channel value if ((ch => 0) and (ch < channels)) ' valid? return long[p_adcvals][ch] ' return channel value else return 0 pub address '' Returns address of adc results storage return p_adcvals pri mcp3208 | ch, mux, level ' call with cognew() outa[cs] := 1 ' output high dira[cs] := 1 outa[clk] := 0 ' output low dira[clk] := 1 repeat repeat ch from 0 to (channels-1) ' loop through channels outa[cs] := 0 ' activate adc ' output mux bits, MSBFIRST dira[dio] := 1 ' dio is output mux := (%11000 + ch) << (32-5) ' single-ended mode repeat 5 ' send mux bits outa[dio] := (mux <-= 1) & 1 ' output a bit outa[clk] := 1 ' clock the bit outa[clk] := 0 ' input data bits, MSBPOST dira[dio] := 0 ' dio is input level := 0 ' clear work var repeat 13 ' null + 12 data bits outa[clk] := 1 ' clock a bit outa[clk] := 0 level := (level << 1) | ina[dio] ' input data bit outa[cs] := 1 ' de-activate adc 'long[p_adcvals][ch] := level & $FFF ' update results array long[p_adcvals][ch] := conv.ScaleUnits((level & $FFF),ch ) WAITCNT(clkfreq/9 + cnt) 'avoid jamming the global variables dat {{ Terms of Use: MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. }}the value monitor
{ ValueMonitor, sits in the background and monitors global variables, printing the values to a serial terminal at 19200 baud. Ver.1 TO BE: support for n number of varables, and support for display texts defined in DAT Erlend Fjosna 2013 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- A pointer to a block of LONG is passed as parameter, together with the number of LONGs to read from that block. The method continously reads these varaiables and displays them on a serial terminal. ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- Acknowledements Chris Gadd kindly shared the bare bone code for this monitor. ======================================================================================================================================================================= ======================================================================================================================================================================= } CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 VAR long monitorStack[128] PUB start(intLong0{ptr}, intLongN) cognew(monitor(intLong0{ptr}, intLongN), @monitorStack) {need a stop + pluss a stop call} PUB monitor(intLong0{ptr}, intLongN) | pm dira[30]~~ repeat waitcnt(cnt + clkfreq / 3) Tx($00) str(string("Water level [mm]: ")) pm:= LONG[intLong0] [0] Dec(pm) str(string($0D,"Water temp [C]: ")) pm:= LONG[intLong0] [1] Dec(pm) str(string($0D,"Water pressure [cBar]: ")) pm:= LONG[intLong0] [2] Dec(pm) str(string($0D,"Light int.[%]: ")) pm:= LONG[intLong0] [3] Dec(pm) str(string($0D,"Weight [g]: ")) pm:= LONG[intLong0] [4] Dec(pm) str(string($0D,"Voltage [cV]: ")) pm:= LONG[intLong0] [5] Dec(pm) str(string($0D,"adc: ")) pm:= LONG[intLong0] [6] Dec(pm) str(string($0D,"adc ")) pm:= LONG[intLong0] [7] Dec(pm) str(string($0D,"HWC ")) pm:= LONG[intLong0] [8] Dec(pm) str(string($0D,"QUAD ")) pm:= LONG[intLong0] [9] Dec(pm) str(string($0D,"RFID ")) pm:= LONG[intLong0] [10] Dec(pm) str(string($0D,"OutA ")) 'NEW pm:= LONG[intLong0] [11] Dec(pm) str(string($0D,"OutB ")) pm:= LONG[intLong0] [12] Dec(pm) str(string($0D,"InA ")) pm:= LONG[intLong0] [13] Bin(pm, 8) str(string($0D,"InB ")) pm:= LONG[intLong0] [14] Bin(pm, 8) str(string($0D,"Tray ")) pm:= LONG[intLong0] [15] Bin(pm, 8) str(string($0D,"temp threshold ")) pm:= LONG[intLong0] [16] Dec(pm) str(string($0D,"Flame ")) pm:= LONG[intLong0] [17] Dec(pm) str(string($0D,"Heat ")) pm:= LONG[intLong0] [18] Dec(pm) PRI Str(stringPtr) repeat strsize(stringPtr) Tx(byte[stringPtr++]) 'Transmit each byte in the string PRI Dec(value) | i, x x := value == NEGX 'Check for max negative if value < 0 value := ||(value+x) 'If negative, make positive; adjust for max negative Tx("-") 'and output sign i := 1_000_000_000 'Initialize divisor repeat 10 'Loop for 10 digits if value => i Tx(value / i + "0" + x*(i == 1)) 'If non-zero digit, output digit; adjust for max negative value //= i 'and digit from value result~~ 'flag non-zero found elseif result or i == 1 Tx("0") 'If zero digit (or only digit) output it i /= 10 'Update divisor PRI Hex(value, digits) value <<= (8 - digits) << 2 Tx("$") repeat digits 'do it for the number of hex digits being transmitted Tx(lookupz((value <-= 4) & $F : "0".."9", "A".."F"))' Transmit the ASCII value of the hex characters PRI Bin(value, digits) value <<= 32 - digits Tx("%") repeat digits Tx((value <-= 1) & 1 + "0") PRI Tx(txByte) | t, bit_delay bit_delay := clkfreq / 19200 txByte <<= 2 t := cnt repeat 10 outa[30] := (txByte >>= 1) & 1 waitcnt (t += bit_delay) outa[30]~~ DAT DaaaBbbbb BYTE "string_data",0 {{ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. }}the Main - very much a debug version
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 'pin assignments: PinADCdata = 7 PinADCclk = 9 PinADCcs = 8 PinRIOslc = 10 PinRIOsda = 11 PinQuadA = 20 PinQuadB = 21 PinRFIDser = 22 PinRFIDen = 23 PINfreqCount = 24 PINservo = 25 PINpumpImp = 26 PINignt = 27 'remote digital io assignments MCP32017addr = %010 CintDinATrayIs = 0 'Digital inputs on bank A CintDinAHolder = 1 CintDinATBN01 = 2 CintDinATBN02 = 3 CintDinAPIR01 = 4 CintDinAMic = 5 CintDinALaser = 6 CintDinATBN04 = 7 intDoutIrish = 0 'Digital outputs on bank B 'parameters VAR 'Variables to hold I/O values '============================ 'ADC analogue input values - obeying the requirement for declaring 8 consecutive LONGs to make it easy for the ADC scanner object LONG gAnLevelWater LONG gAnTempWater LONG gAnPressWater LONG gAnLigth LONG gAnWeightCup LONG gAnVoltageSupply LONG gAnCh6 LONG gAnCh7 '---------------------------------------------------------------------------------------------------------------------------------- LONG gHWCmode LONG gQuadPos LONG intMatch LONG intOutA 'NEW LONG intOutB LONG intInA LONG intInB LONG intTrayIs LONG intIrish LONG gAnUVflame LONG gAnHeatServo BYTE strnRFID[11] OBJ adc : "ADCreaderConv" val : "ValueMonitor" hwc : "HeatWaterControl" quad : "QuadDecoder" rfid : "RfidReader" vm2 : "TestVM2" oled : "TestOLED" drw : "DioReadWriter" PUB Main 'start ad converter reader adc.startx(PinADCcs, PinADCclk, pinADCdata, 8, @gAnLevelWater) 'gAnLevelWater is the first in the block of 8 LONGs WAITCNT(clkfreq + CNT) 'configure and start digital io readwriter drw.Init(PinRIOslc, PinRIOsda, 2, %1111_1111, %0000_0000, %0000_0000, %0000_0000) drw.Start(PinRIOslc, PinRIOsda, 2, @intOutA, @intOutB, @intInA, @intInB) WAITCNT(clkfreq + CNT) 'Wait for the Dio to rev up 'start monitor val.start(@gAnLevelWater, 10) intTrayIs:= intInA & |< CintDinATrayIs 'Reads one bit value from Long at given bit position intOutB:= Wr_bit(intOutB, intDoutIrish, intIrish) 'Writes one bit value into Long at given bit postion 'start rotary Gray decoder quad.start(PinQuadA, @gQuadPos) 'start RFID reader rfid.start(PinRFIDser, PinRFIDen, @strnCodes, 7, 10, 0, @intMatch, @strnRFID) ' IF intMatch > 0 ' vm2.demo repeat until gQuadPos <> 0 'start hotwatercontrol gHWCmode:= 1 ' PINpump, PINignt, PINservo, PINfreqCount{NEW}, ptrTempWater, ptrPressWater, ptrFlameRate, ptrLevelWater, ptrHeatServo, ptrOpmode, iSPtemp, iSPpress, iSPtime, iSPvol hwc.Start(PINpumpImp, PINignt, PINservo, PINfreqCount, @gAnTempWater, @gAnPressWater, @gAnUVflame, @gAnLevelWater, @gAnHeatServo, @gQuadPos{@gHWCmode}, 93, 1500, 25, 3000) oled.demo 'watchdog DIRA[5]~~ repeat until gQuadPos == 3 !OUTA[5] WAITCNT(clkfreq/5 + cnt) PRI Wr_bit(target, pos, value) 'Writes value.0 to target.pos 'NEW if (value & 1) return target | (1 << pos) else return target & !(1 << pos) DAT strnCodes BYTE "0100A648BD", 0 {Erlend} BYTE "0100E3E9CB", 0 {Lisa} BYTE "8400338B3C", 0 {Mathias} BYTE "8400336BB8", 0 {Irene} BYTE "8400338FC0", 0 {Keith} BYTE "8400338778", 0 {Guest1} BYTE "8400338778", 0 {Guest2} intCodes LONG 7This is still not all, but maybe enough?
Erlend
surely it can be done. But it is another source of bugs if the value of the pointer is wrong.
You have to check if you use the "@"-operator or not to access the right location in memory.
As long as you keep all methods within ONE *.SPIN-file you can use all variables even ACROSS cogs
If you have methods in different *.SPIN-files you can use "SetVar" und "GetVar" methods.
As a general advice:
Code in small steps. Code one method for one thing to do.
Do extensive tests to each piece of your code. Testing all situations: value zero, value is max, value is min, value is out of allowed range
After all these tests. Code next piece of code. In this manner you can be 99% sure that a bug is NOT inside previous written code
but most of the time in the new method you are coding right now.
All this testing takes time. But in the long run it will save a lot of time because you are not searching for bugs across hundreds of lines of code.
As I'm coding now for more than 25 years me experience is: if it (should) go fast it will turn out to be real slow.
here is a small demo-code that shows how variables can be accessed across cogs directly by their name.
http://forums.parallax.com/showthread.php/113075-Variables-across-objects-again
best regards
Stefan
If two COGS are simply writing some values to the same HUB location the result in that location will be whatever the last COG wrote. It will make no difference if you use locks or not.
But if they are doing a read/update/write operation, for example incrementing a counter, then locks will be required around the read and write other wise things will go wrong.
Admittedly the first case above is not very useful as far as I can tell. Although if the writes and reads are some how ordered in time nicely, COG A writes, COG C reads, COG B writes, COG C reads, no locks would be required.
I would much rather download an archive. Trying the read through the code in the browser does not sound like fun.
The archive should have all the code needed to compile.
I do work according to the principle of testing piece by piece, module by module, but I know from lifelong experience (from other technologies) that the real hard part is when bringing the pieces togheter into a system. I have tested basic stuff such as that the pointers point to where they should, and that the values arrive. When tested as small pieces. So, refering to my first post; the repeat loop does work as intended, when tested in isolation, but as part of the complete system it only occasionally works.
I feel brave posting all this more or less half-cooked and untidy code. Thankfully the forum is very kind at heart.
Erlend
What does it do when it works and what does it do (or not do) when it doesn't?
I do see a problem with your P control. I think you're getting a rounding error when the temperature is too hot.
Line #286 of "HeatWaterControl".
This probably doesn't do what you want. The value of iHeatP will only change with every ten degrees of temperature difference.
I think the equation below would give a little more resolution:
With small values, you want to wait to divide until the end of the calculation. There's likely a better formula than the one above for the intended purpose since the number being divided by ten is still pretty small.
I'm not sure if this at all related to the issue you're having since I don't know what "not working" means.
You're right that, strictly speaking, multiple cogs can write to the same variable while other cogs are reading that variable as long as no cog makes a private copy of the variable value while it changes the value (read / modify / write). The private copy might be implicit (like in a register or stack). It's difficult to design multiple cog routines that keep in synchrony in a particular order of execution as you illustrated, but it can be done (usually with WAITCNT) ... not an exercise for anyone but a very experienced programmer used to working with multiprocessing (multiple processors with shared resources).
The value monitor looks good.
Erlend
Yes accuracy and control algorithms are basic still, I know. But they work.
What's the indicator the main process control has started?
I'm not sure maybe you are doing at already this way.
What I meant with testing was
method A testing for itself
adding method A to the whole thing
testing it there
writing method B testing it for itself
adding it to the whole thing testing it in the whole thing.
etc.
Spin supports global variables and parameters
Changing global variables sometimes causes strange behaviour if you loose the oversight over your code
which parts of the code change the value of which variable at which time.
And as the code in each cog runs independendly you can't predict which loop is at which point.
Whenever a method just needs to read a value why not coding pass parameter by value?
OK if the value is updated from one cog and another cog needs to read the value regularly
instead of accessing a variable via a pointer accessing the variable via its variablename?
If I understand right your method "HotWcontrol" runs through one time and thats all. There is no loop.
hwc.start is called once and that's all
Duane posted while I was still writing
So how about monitoring inbetween-results of the caclulation
I mean monitoring
LiSPtemp - iTempWater
(LiSPtemp - iTempWater) / 10
||(LiSPtemp - iTempWater)/10)
(1+ ||(LiSPtemp - iTempWater)/10))
(iGainP * (1+ ||(LiSPtemp - iTempWater)/10))
etc.
best regards
Stefan
I can't tell you how many times something like this has helped be catch integer math errors (which in hindsight are always obvious).
My guess right now is there's an integer math issue causing the problem.
The top level objects starts the hotwater control in a separate cog, that method starts by inititializing variables, sets up the 'scheduler', and then does the following: 1) ignites the burner - 2) waits for the water to heat up - then 3) starts the PID control of the heater and speed control of the pump, which runs until a cup of espresso coffee is finished brewing. Step 3 is what I refer to as the 'main process'. My present trouble is that step 2 is governed by a repeat loop waiting for the temperature to be reached (around line 210-), simply by doing a comparison to the measured temperature which is held in a global variable - there is no complexity, it just LONG reads the value and does a > comparison. Problem is, it behaves unpredictable.
@Stefan
I do not want to go into a discussion of how I structure my code as I do, because then I need to describe all the features of the finished system as well as many other coding-philosophical things. I do not think it is a bad way to split my code into many independant modules, centered around a common set of global (measured and derived value) varaibles. It is a well proven concept. The mechanism of passing pointers to variables is also straight forward when the rules a so simple as in my code (one can write, many can read, assume asynch updating).
The calculations you are pointing to are still to be polished - but they are not executed until later - until after the problem I described. Yes, I know the pains of integer math traps.
Erlend
What's your indicator the PID control is active?
I think it would help if you added some sort of indicator to you know when the "main process" starts.
'Wait with starting pump until initial heating of water is finished '------------------------------------------------------------------------------------------------------------------------------------------------- REPEAT UNTIL LONG[LptrTempWater] > 90 'LiSPtemp 'Wait until water is hot enough for brewing WAITCNT(clkfreq/2 + cnt) [b] ' Light a LED or set some flag variable indicating the temperature has been reached.[/b] 'Now start 'milestone' based hot water control routines '-------------------------------------------------------------------------------------------------------------------------------------------------- REPEAT UNTIL iPumpStrokes > LiSPvol 'Continously check CNT to detect 'milestones' and perform timed actions - until setpoint volumeThanks,
Erlend
as the propeller-chip has no interrupts the behaviour of code is completely deterministic.
old programmers wisdom:
a programm does ALWAYS what the programmer has coded.
If the program does some unexpected things it STILL does exactly what the programmer has coded
only thing is the programmer coded something he does not fully understand and that's the reason why unexpected
things happen.
The only thing that really helps is to monitor every dammed detail of the code. Beginning at that place you find suspicious.
Your monitor-method uses
the "wait-for-temperature high enough"-loop uses
REPEAT UNTIL LONG[LptrTempWater] > 90 'LiSPtemp 'Wait until water is hot enough for brewing WAITCNT(clkfreq/2 + cnt)I guess you have thought lot's of times "LONG[LptrTempWater]" has exactly the same value as "LONG[intLong0] [1]"
but did you really monitor it?
You are using multiple cogs.
This opens the possability that parts of the code execute from loop to loop in different amounts of time.
For several test-runs
Did you monitor all the code involved how far it is executed?
You could do this with extra longs with via constants hardcoded adresses that are in memory far away from the end of your code
Something like $7F80. Through using names of constants you make sure that the adress-value is REALLY constant.
Each part of the code that runs in his own cog has this such a long. Then insert in your code assigning increasing values
to these longs
example
PRI HWcontrol long[AbsAdressOf_hwc] := 1 '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 ... ... 'Wait with starting pump until initial heating of water is finished '------------------------------------------------------------------------------------------------------------------------------------------------- long[AbsAdressOf_hwc] := 2 REPEAT UNTIL LONG[LptrTempWater] > 90 'LiSPtemp 'Wait until water is hot enough for brewing WAITCNT(clkfreq/2 + cnt) long[AbsAdressOf_hwc] := 3or alternativly switching on/off different LEDs
to get feedback which part of the code is executing where
You are right: it's not nescessary to start a discussion about coding philisophies.
But copying values to new variables opens the possability that values can be different
indeed it is really strange that your loop
REPEAT UNTIL LONG[LptrTempWater] > 90 'LiSPtemp 'Wait until water is hot enough for brewing WAITCNT(clkfreq/2 + cnt)leaves the loop sometimes at a temperature of 115 degrees
to me this would mean:
- the code starts looping at 115 degrees
- the value of LONG[LptrTempWater] is different from the monitored one LONG[intLong0] [1]
best regards
Stefan
Erlend
'start hotwatercontrol gHWCmode:= 1 ' PINpump, PINignt, PINservo, PINfreqCount{NEW}, ptrTempWater, ptrPressWater, ptrFlameRate, ptrLevelWater, ptrHeatServo, ptrOpmode, iSPtemp, iSPpress, iSPtime, iSPvol ' PUB Start(PINpump, PINignt, PINservo, PINfreqCount{NEW}, ptrTempWater, ptrPressWater, ptrFlameRate, ptrLevelWater, ptrHeatServo, [B]ptrOpmode[/B], iSPtemp, iSPpress, iSPtime, iSPvol): Success hwc.Start(PINpumpImp, PINignt, PINservo, PINfreqCount, @gAnTempWater, @gAnPressWater, @gAnUVflame, @gAnLevelWater, @gAnHeatServo, [B]@gQuadPos{@gHWCmode}[/B], 93, 1500, 25, 3000)When I compare the call to hwc.Start(...) with PUB Start(...) I see a difference in the parameters.
In PUB start there is ptrOpmode the opposite side has
@gQuadPos{@gHWCmode}.
As I see you are using @gQuadPos while {@gHWCmode} this is a comment.
I guess that it should be @gHWCmode, but I could be wrong.
You've spotted right, but I am afraid it is only the result of me being half-way to setting up a varable handle to use for hotwatercontrol to write back it's status - for debugging purpose. The gQuadPos is an easy one to use for this purpose, as it is already being displayed by ValueMonitor. Sorry, but I am sharing half-baked code here.
Erlend
Thanks all, for the help to trace the problem, and for helping me to take the approach of tracing the problem instead of drilling down into what I thought was the problem.
It turns out that the scheme of sharing-reading global variables was fine, and also that the REPEAT UNTIL Temp_measured > Temp_setpoint worked fine.
I inserted 'telltales' into the code to see at what stage it got stuck - it was just was after the temperature comparison code! So why didn't the pump start running? The 'milestone scheduler' code is all governed by the CNT values as it executes, and in my original code the 'milestones' values got referenced to CNT -- before the wait for the water to get hot REPEAT UNTIL loop. So when finally the water was hot enough that CNT value was minutes old. I assume therefore that some 32bit 'counting-around effect' caused the problem. I cannot explain exactly how. Maybe someone can? When I moved the code which sets up the milestones referenced to CNT to just before the 'scheduler' code, it all works perfect.
Again, thanks for the help. Now I can go on to improve and tune the PID control algorithm.
Erlend
Present code - with telltales:
PRI HWcontrol LONG[LptrOpmode]:= 10 '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 < 2 'Light the flame if not burning Ignite(1) iFreqUV:= PHSA~ 'reset counter WAITCNT(clkfreq/2 + cnt) FreqCount LONG[LptrOpmode]:= 11 'Wait with starting pump until initial heating of water is finished '------------------------------------------------------------------------------------------------------------------------------------------------- REPEAT UNTIL LONG[LptrTempWater] > LiSPtemp 'Wait until water is hot enough for brewing WAITCNT(clkfreq/2 + cnt) LONG[LptrOpmode]:= 12 '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:= 4 iGainD:= 1 iTempWaterOld:= iTempWater LONG[LptrOpmode]:= 13 '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:= 1000*mSec 'Run control algorithm /sec and update values iAlgoritmNow:= CNT + iAlgorithmIval 'Define 'milestone' as CNT value LONG[LptrOpmode]:= 14 '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 OUTA[LPINpump]:= 1 iPumpPulsStart += iPumpRate iPumpStrokes += 1 'Keep tally of total pump strokes (=volume) 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 ShutdownI'm glad you found it.
I'm sure you're not the first or last to look for a problem in the wrong place.
I often use a multiple cog and multiple object serial driver with locks to find these sorts of bugs.
You can only test for delays less than (about) 26 seconds (at 80MHz). If the delay is longer than 26s, the interval will be negative and fail the ">" comparison used to see if a set time has passed.
congrats that you found it!
the WaitCnt-command works as follows:
the parameter of WaitCnt (usually something like (ClkFreq */ somefactor + cnt)
gives a value which will be reached after some time in the future.
The systemvariable cnt delivers the value of an always free-running 32bit counter which rolls over to zero in just one clocktick when the counter reaches his
maximum value (which is 2^32)
The WaitCnt command stops the cog completely until the value given as paremeter in the WaitCnt-command matches the value of the systemcounter
with smaller and easier to understand numbers let's say maximum-value is 1000
let's say cnt has a value of 600
if you execute a WaitCnt with a value 500
the freerunning counter has already a higher value.
this means the next match will occur after counting up to 1000 then roll over to zero and start counting up again until 500 is reached.
With the real system running at 80 MHz counting up will take 2^32 / 80.000.000 = 53,7 seconds
So whenever code reacts after aprox one minute you have a "cnt has passed matching value already problem"
and if code does not react soon wait for at least a bit more than a minute to see if the code reacts then.
In Spin there is a minimumtime of 385 clockticks which it takes to interprete a WaitCnt-command.
So if you want to create a pulse shorter than 1/80.000.000 * 385 = 4,8 microseconds it will not work in spin
by using WaitCnt. You would have to do this in assember or by using the counter-modules.
If you still can spend a cog how about using a software RTC like JonMcPhalens softrtc http://obex.parallax.com/object/322
with milliseconds and "summarised seconds of day" as a timereference?
best regards
Stefan
Stefan,
I don't think the issue was the waitcnt statement. I think it was the way time periods were compared using a start time more than 26 seconds from the current time.
With waitcnt you can pause up to about 53 seconds but then comparing intervals the longest interval one can safely compare is about 26 seconds. Intervals longer than 26 seconds will be calculated as a negative number.
you are right. In SPIN MSB is the sign. So it cuts down to half the time.
best regards
Stefan