'-------------------------------------------------------------------------- ' ' Copyright 1996 Donald Kinzer, All Rights Reserved ' ' This program is intended to monitor and control a diesel motor-generator ' set. A simple feedback loop is used to control the motor speed and, ' therefore, the output frequency of the generator. A linear actuator ' stepper motor controls the engine speed via a throttle linkage. The motor ' speed is calculated from a tach input derived from a magnetic pickup on the ' motor's flywheel. Also, the generator's load current is estimated by ' digitizing the output of a current transformer and then applying a ' transformation function to arrive at the estimated current. The RPM, ' frequency and load current are displayed on an LCD display unit. ' ' The controller is based on the Basic Stamp II microcontroller from ' Parallax. The Stamp II has 16 I/O pins and a set of pins for ' communicating with the chip for the purpose of programming, etc. The ' following discussion will center on the interaction of the I/O pins ' with the remaining circuitry on the controller board. ' ' The controller consists of the Stamp II, a RAM subsystem, an LCD for ' displaying information, a linear stepper motor for controlling the engine ' throttle, a tachometer input, two analog generator load sensor inputs, and ' various switch inputs. Because the number of bits of input and output ' exceeds the number of available pins, the interaction between the Stamp II ' and the remainder of the circuitry is more complicated that it would ' otherwise be. I/O pin 8 is used to read the value of up to eight inputs. ' A multiplexor, controlled by I/O pins 0-2 selects among the signals to ' read. I/O pins 0-2 also control a decoder which generates an active low ' signal on one of eight outputs. I/O pin 3 strobes the decoder to activate ' the selected output. ' ' I/O pin 4 is used as a multi-purpose 'mode' selector. For the RAM ' subsystem it acts as address bit 0 effectively selecting the high byte/low ' byte of a word to be read or written. It is also used as a register ' selector for the LCD unit. I/O pin 5 is used as a read/write selector for ' both the RAM subsystem and the LCD unit. I/O pin 6 is used as a serial ' data output directed to the RAM subsystem and the A/D converter. I/O pin 7 ' is used as a data clock for serial input/output operations. Lastly, I/O ' pins 12-14 are used to control the stepper motor. The remaining I/O pins ' (9, 10, 11 and 15) are unused. '-------------------------------------------------------------------------- ' These constants define the select codes for the system components. The ' value is output on i/o pins 0-2 which are fed to both a multiplexer and ' a decoder. The former is used to select one of eight inputs to read ' while the latter is used to generate one of eight output strobes for ' various devices. ADC_PORT con 0 ' the ADC port (R/W) TACH_PORT con 1 ' the tach input port (R) RAM_ADDR_ENBL con 1 ' enable signal for the address latch (W) DATA_RD con 2 ' the LCD & RAM input (R/W) DISP_ENBL con 3 ' the strobe for the display controller (R/W) MAN_CW con 4 ' manual CW signal (switch) (R) RAM_ENBL con 4 ' strobe signal for activating the RAM (W) MAN_CCW con 5 ' manual CCW signal (switch) (R) LIMIT_CW con 6 ' linear actuator limit switch (R) LIMIT_CCW con 7 ' linear actuator limit switch (R) ' Port constants. PORT_ADDR_MASK con 7 ' low three bits PORT_STROBE con 3 ' port 3 strobes the decoder MODE1 con 4 ' LCD register select, RAM odd/even MODE2 con 5 ' LCD & RAM R/W DATA_OUT con 6 ' data out DCLK con 7 ' data clock DATA_IN con 8 ' data in MOTOR_RUN con 12 ' low to run the stepper MOTOR_STEP con 13 ' high to step once MOTOR_CCW con 14 ' high to run CCW PORT_DISABLE con 8 ' value to OR with port values above ' to disable the port select ' Commands for the LCD LCD_INIT con $38 ' 8 bits, 2 lines, 5x7 cell LCD_CLEAR con $01 ' clear the display memory LCD_MODE_SET con $0c ' display on, no cursor, no blinking LCD_ADDR_INC con $06 ' increment cursor after writing LCD_ADDR_DEC con $04 ' decrement cursor after writing LCD_SET_ADDR con $80 ' add to the desired address LCD_BUSY con $80 ' busy flag ' Display addresses CAP_ADDR con $00 ' where the caption is displayed RPM_ADDR con $40 ' where the current RPM is displayed FREQ_ADDR con $46 ' where the current frequency is displayed AMP1_ADDR con $4a ' where the channel 1 current is displayed AMP_SEP_ADDR con $4c ' where the separator for ch1/ch2 is displayed AMP2_ADDR con $4d ' where the channel 2 current is displayed '-------------------------------------------------------------------------- ' variable declarations i var byte ' gen'l purpose index cnt var word ' gen'l purpose counter result var word ' gen'l purpose result variable ptr var word ' gen'l purpose pointer addr var byte ' desired display or RAM address dataWord var word ' data to send to the LCD or RAM dataLow var dataWord.lowByte dataHigh var dataWord.highByte loopDelay var byte lcdStat var byte ' status of the LCD lcdValue var word ' value to display on the LCD lcdSupprLZ var bit ' flag to suppress leading zeroes sampleIdx var byte lastRPM var word adcChan var bit ' specifies the ADC channel to read stepCCW var bit ' step direction (high = CCW) running var bit atSpeed var bit autoMode var bit adcConfig var nib ' Configuration bits for ADC adcStartB var adcConfig.bit0 ' Start bit for comm with ADC adcSglDif var adcConfig.bit1 ' Single-ended or diff mode adcChanSel var adcConfig.bit2 ' Channel selection adcMsbf var adcConfig.bit3 ' Output 0s after xfer complete '-------------------------------------------------------------------------- ' data declarations lcdInitDataLen data 5 lcdInitData data LCD_INIT,LCD_CLEAR,LCD_MODE_SET,LCD_ADDR_INC,LCD_SET_ADDR captions data " RPM HZ LOAD",0 calStr data "Calibrating...",0 manMode data "Manual Mode", 0 '-------------------------------------------------------------------------- '-------------------------------------------------------------------------- ' ' Execution begins here at startup. ' '-------------------------------------------------------------------------- '-------------------------------------------------------------------------- ' set up the port directions and initialize the outputs outs=$1f7f ' all except data clock and motor run high dirl=%11111111 ' all outputs dirc=%0000 ' all inputs dird=%1111 ' all outputs lcdSupprLZ = 1 ' initialize the LCD subsystem read lcdInitDataLen,cnt for i = 0 to (cnt - 1) read lcdInitData+i,dataLow gosub sendLCDCmd next ' Now we check for a request to enter manual mode. autoMode = 1 ' set for automatic control mode gosub checkManMode ptr = calStr if (autoMode <> 0) then putCaption ptr = manMode putCaption: gosub outputText ' output the selected message restart: running = 0 atSpeed = 0 lastRPM = 0 ' initialize the frequency samples to 0 (stored in RAM) dataWord = 0 for addr = 0 to 9 gosub writeRamWord next sampleIdx = 0 ' Initialize the control system gosub seekMotorHome loopDelay = 0 stepCCW = 0 cnt = 500 gosub stepMotor ' put up the display boilerplate characters, value will be filled in later dataLow = LCD_SET_ADDR | CAP_ADDR gosub sendLCDCmd ptr = captions addr = CAP_ADDR gosub outputText dataLow = LCD_SET_ADDR | AMP_SEP_ADDR gosub sendLCDCmd dataLow = "+" gosub sendLCDData ' test code for development purposes only ' stepCCW = 1 ' cnt = 800 ' gosub stepMotor '-------------------------------------------------------------------------- '-------------------------------------------------------------------------- ' ' Now that the initialization is completed, we begin the control loop. ' '-------------------------------------------------------------------------- '-------------------------------------------------------------------------- controlLoop: gosub readTach ' get the current speed (actually 1/10th of speed) addr = sampleIdx dataWord = result gosub writeRamWord ' write the current reading ' Add the previous 9 readings to the current reading thereby effectively ' computing the average over 10 readings. for i = 9 to 1 addr = (sampleIdx + i) // 10 gosub readRamWord result = result + dataWord if (i <> 9) then continueAvg lastRPM = result * 5 ' average the last two readings continueAvg: next ' display the averaged rpm value (four digits) addr = RPM_ADDR lcdValue = result cnt = 4 gosub outputValue ' calculate the frequency (rpm / 30) and display (two digits) addr = FREQ_ADDR lcdValue = (result + 15) / 30 cnt = 2 gosub outputValue ' see if manual mode is being requested gosub checkManMode ' handle manual speed control if requested if (autoMode <> 0) then checkSpeed outa = MAN_CCW | PORT_DISABLE if (in8 <> 0) then checkManSpeedUp stepCCW = 1 goto setManSpeed checkManSpeedUp: outa = MAN_CW | PORT_DISABLE if (in8 <> 0) then speedDone stepCCW = 0 setManSpeed: cnt = 25 gosub stepMotor goto speedDone checkSpeed: ' analyze the resulting reading and decide whether or not to correct if (loopDelay > 0) then speedDone if (running AND (lastRPM < 1000)) then restart if (lastRPM < 500) then speedDone ' not running? running = 1 if (lastRPM <= 1790) then speedUp atSpeed = 1 if (lastRPM <= 1810) then speedDone speedDown: ' here we back off the throttle but don't delay further correction cnt = lastRPM - 1800 stepCCW = 1 gosub stepMotor goto speedDone speedUp: ' here we increase the throttle and then delay further correction loopDelay = 2 result = 1800 - lastRPM cnt = 25 if (atSpeed) then doSpeedUp cnt = 300 doSpeedUp: cnt = lastRPM MAX cnt stepCCW = 0 gosub stepMotor speedDone: ' read the current transducers and display the result for adcChanSel= 0 to 1 gosub getADCResult gosub convertToAmps lcdValue = result addr = AMP1_ADDR + (adcChanSel * 3) cnt=2 gosub outputValue next ' provide a sync signal for an oscilloscope toggle 15 ' prepare for the next sample cycle sampleIdx = (sampleIdx + 1) // 10 if (loopDelay = 0) then controlLoop loopDelay = loopDelay - 1 goto controlLoop '-------------------------------------------------------------------------- '-------------------------------------------------------------------------- ' ' Subroutines ' '-------------------------------------------------------------------------- '-------------------------------------------------------------------------- '-------------------------------------------------------------------------- ' This function checks to see if the manual cw/ccw switch is being ' thus signifying a request to enter manual mode. checkManMode: outa = MAN_CCW | PORT_DISABLE if (in8 = 0) then setManMode ' see if the CW switch is on outa = MAN_CW | PORT_DISABLE if (in8 <> 0) then checkManModeDone setManMode: autoMode = 0 checkManModeDone: return '-------------------------------------------------------------------------- ' This routine initiates an A/D conversion on the LTC1298 channel ' specified by 'adcChan' and places the result in 'result'. ' getADCResult: outa = ADC_PORT | PORT_DISABLE high DATA_OUT ' Set data pin for first start bit low MODE2 ' write mode adcConfig = adcConfig | %1011 ' Set all bits except channel low PORT_STROBE ' Activate the ADC shiftout DATA_OUT,DCLK,LSBFIRST,[adcConfig\4] ' Send config bits high MODE2 ' read mode shiftin DATA_IN,DCLK,MSBPOST,[result\12] ' Get data bits high PORT_STROBE ' Deactivate the ADC return '-------------------------------------------------------------------------- ' This routine takes the value in 'result' and converts it to the ' equivalent in amps. Because of the non-linear nature of the ' transducer response curve of the current sensors, the calculation is ' done in segments using a conversion formula that was empirically ' determined to produce results that are within 10% or so. convertToAmps: if (result >= 1500) then convertToAmpsCase4 if (result >= 1000) then convertToAmpsCase3 if (result >= 200) then convertToAmpsCase2 if (result >= 115) then convertToAmpsCase1 result=0 return convertToAmpsCase1: ' approximate -1.28 + 0.0243 X with rounding result = ((((result * 200) + 41) / 82) -128 + 50) / 100 return convertToAmpsCase2: ' approximate 0.223 + 0.017 X with rounding result = ((result * 2) + 26 + 59) / 118 return convertToAmpsCase3: ' approximate 5.1 + 0.0108 X with rounding result = (((((result * 10) + 463) / 926) * 10) + 51 + 5) / 10 return convertToAmpsCase4: ' approximate -.0699 + 0.0131 X with rounding result = ((((result * 10) - 53) + 380) / 760) return '-------------------------------------------------------------------------- ' This function moves the stepper motor to its 'home' position by first ' making sure that it is off of the limit switch and then moving back ' to the limit. Stepping is done at a rate of about 250 pulses/sec or ' 1/4 ips. ' seekMotorHome: high PORT_STROBE ' see if the stepper is already at the limit outa = LIMIT_CCW | PORT_DISABLE low MOTOR_CCW seekMotorHomeOffLimitLoop: pulsout MOTOR_STEP,2 pause 3 if (in8 = 0) then seekMotorHomeOffLimitLoop ' Now the stepper is off of the limit switch, move it back high MOTOR_CCW seekMotorHomeLimit: pulsout MOTOR_STEP,2 pause 3 if (in8 <> 0) then seekMotorHomeLimit ' stepperPos = 0 return '-------------------------------------------------------------------------- ' This routine steps the motor the number of times give by 'stepCnt' in ' the direction given by stepCCW. If the limit is reached in the ' direction being stepped toward, stepping is terminated and the 'stepCnt' ' variable reflects the number of steps not taken. ' stepMotor: high PORT_STROBE outa = LIMIT_CCW | PORT_DISABLE high MOTOR_CCW if (stepCCW <> 0) then stepMotorLoop low MOTOR_CCW outa = LIMIT_CW | PORT_DISABLE stepMotorLoop: if (in8 = 0) then stepMotorDone if (cnt = 0) then stepMotorDone pulsout MOTOR_STEP,2 cnt = cnt - 1 pause 3 goto stepMotorLoop stepMotorDone: return '-------------------------------------------------------------------------- ' This routine reads the tach input and calculates rpm and frequency. ' The tach input is derived from a magnetic pickup on the flywheel. The ' flywheel has 120 teeth and therefore the tach input frequency is 120 ' times the number of revolutions per second. A sample interval of 50ms ' (which gives an accuracy of about 1% at 1000 rpm) yields the formula ' below to convert the tach sample count to RPM: ' ' COUNT pulses * 60 sec/minute ' --------------------------------- ' .05 sec * 120 pulses/revolution ' or ' ' COUNT * 10 ' ' The value actually returned by this function is 1/10 of the current ' speed. ' readTach: outa = TACH_PORT | PORT_DISABLE count DATA_IN,50,result return '-------------------------------------------------------------------------- '-------------------------------------------------------------------------- ' ' LCD Subsystem ' ' The LCD subsystem is an 8-bit, two register controller capable of ' handling several read/write commands. The MODE1 I/O pin selects between ' the command mode and the data mode while the MODE2 I/O pin selects ' between reading and writing. ' ' When writing, the data going to the LCD system is shifted out and latched ' and then the LCD unit is strobed to accept the command or data. When ' reading, the LCD is enabled to output its data and the data is shifted ' in. ' ' Note that the LCD unit shares its data bus with the RAM subsystem. ' '-------------------------------------------------------------------------- ' This function reads the status register and loops until the LCD ' is not busy. lcdWaitNotBusy: low MODE1 ' cmd register high MODE2 ' read mode lcdWait: outa = DISP_ENBL | PORT_DISABLE low PORT_STROBE pulsOut DCLK,1 ' latch the data high PORT_STROBE outa = DATA_RD | PORT_DISABLE low PORT_STROBE shiftIn DATA_IN,DCLK,MSBPRE,[lcdStat\8] high PORT_STROBE if ((lcdStat & $80) <> 0) then lcdWait return '-------------------------------------------------------------------------- ' This function sends a command to the LCD controller sendLCDCmd: gosub lcdWaitNotBusy low MODE1 ' cmd register gosub writeLCD return '-------------------------------------------------------------------------- ' This function sends data to the LCD controller sendLCDData: gosub lcdWaitNotBusy high MODE1 ' data register gosub writeLCD return '-------------------------------------------------------------------------- ' This function sends either a command or data to the LCD depending ' on the state of MODE1. writeLCD: shiftOut DATA_OUT,DCLK,MSBFIRST,[dataLow\8] pulsOut DCLK,1 outa = DISP_ENBL | PORT_DISABLE low MODE2 ' write mode pulsOut PORT_STROBE,1 high MODE2 return '-------------------------------------------------------------------------- ' This function outputs a string of text beginning at the current ' display address and continuing until a null byte is encountered. outputText: for i = 0 to 100 read ptr + i, dataLow if (dataLow = 0) then outputTextDone gosub sendLCDData next outputTextDone: return '-------------------------------------------------------------------------- ' This function outputs the current value of 'lcdValue' in decimal format ' beginning at the display address given by 'addr'. The number of ' digits displayed is given by the variable 'cnt'. Leading zeros are ' suppressed if 'lcdSupprLZ' is non-zero outputValue: ' set up the display for outputting from right to left dataLow = LCD_SET_ADDR + ((addr + cnt - 1) & $7f) gosub sendLCDCmd dataLow = LCD_ADDR_DEC gosub sendLCDCmd outputValueLoop: dataLow = (lcdValue // 10) + $30 gosub sendLCDData cnt = cnt - 1 lcdValue = lcdValue / 10 if (lcdSupprLZ <> 0) AND (lcdValue = 0) then outputValueFill if (cnt > 0) then outputValueLoop outputValueFill: ' fill remaining positions with spaces if (cnt <= 0) then outputValueDone dataLow = $20 gosub sendLCDData cnt = cnt - 1 goto outputValueFill outputValueDone: ' put back in incrementing mode dataLow = LCD_ADDR_INC gosub sendLCDCmd return '-------------------------------------------------------------------------- '-------------------------------------------------------------------------- ' ' RAM Subsystem ' ' The RAM subsystem is required because the need for read/write storage ' exceeds the amount available in the Stamp II. Although EEPROM could be ' used for this purpose, the number of times that the EEPROM can be written ' limits its usefulness. ' ' The RAM subsystem is organized as 64 16-bit words and is read from and ' written to one word at a time (actually, it operates on two 8-bit bytes). ' To interact with the RAM subsystem, the following functions first output ' an address (serially) to a latch. Then, if writing, the value to be ' written is output serially to another latch after which the RAM chip ' is strobed to store the data. If reading, the RAM output is read in ' serially. As mentioned earlier, two successive bytes are always read ' and written for each operation. ' ' Note that the RAM subsystem shares its data bus with the LCD subsystem. '-------------------------------------------------------------------------- ' This function writes the data given by 'dataWord' to the address given ' by 'addr' in the RAM chip. Recall that a word is written to RAM storage ' by sending it out serially to a shift register/latch after which the ' value is strobed into the RAM chip. writeRamWord: ' write out the RAM address outa = RAM_ADDR_ENBL | PORT_DISABLE low PORT_STROBE shiftOut DATA_OUT,DCLK,MSBFIRST,[addr\6] high PORT_STROBE ' prepare for writing low MODE2 ' write mode outa = RAM_ENBL | PORT_DISABLE ' write the low byte shiftOut DATA_OUT,DCLK,MSBFIRST,[dataLow\8] pulsOut DCLK,1 low MODE1 pulsOut PORT_STROBE,1 ' strobe the RAM chip ' write the high byte shiftOut DATA_OUT,DCLK,MSBFIRST,[dataHigh\8] pulsOut DCLK,1 high MODE1 pulsOut PORT_STROBE,1 ' strobe the RAM chip high MODE2 return '-------------------------------------------------------------------------- ' This function reads the RAM data from the address given by 'addr' ' and returns it in 'dataWord'. Recall that the word from the RAM storage ' is read in serially. readRamWord: ' write out the RAM address to the address latch outa = RAM_ADDR_ENBL | PORT_DISABLE low PORT_STROBE shiftOut DATA_OUT,DCLK,MSBFIRST,[addr\6] high PORT_STROBE ' prepare to read high MODE2 ' read mode ' read the low byte low MODE1 outa = RAM_ENBL | PORT_DISABLE low PORT_STROBE pulsOut DCLK,1 ' latch the data high PORT_STROBE outa = DATA_RD | PORT_DISABLE low PORT_STROBE shiftIn DATA_IN,DCLK,MSBPRE,[dataLow\8] high PORT_STROBE ' read the high byte high MODE1 outa = RAM_ENBL | PORT_DISABLE low PORT_STROBE pulsOut DCLK,1 ' latch the data high PORT_STROBE outa = DATA_RD | PORT_DISABLE low PORT_STROBE shiftIn DATA_IN,DCLK,MSBPRE,[dataHigh\8] high PORT_STROBE return '-------------------------------------------------------------------------- '-------------------------------------------------------------------------- ' ' Test Functions (for development purposes only) ' '-------------------------------------------------------------------------- '-------------------------------------------------------------------------- ' This test loop drives the stepper from limit to limit using the JOG mode 'motorCycle: ' gosub seekMotorHome ' high PORT_STROBE 'motorLoop2: ' outa = LIMIT_CW | PORT_DISABLE ' low MOTOR_CCW 'stepCWLoop: ' pulsout MOTOR_STEP,2 '' stepperPos = stepperPos + 1 ' pause 3 ' if (in8 <> 0) then stepCWLoop ' high MOTOR_CCW ' outa = LIMIT_CCW | PORT_DISABLE 'stepCCWLoop: ' pulsout MOTOR_STEP,2 '' stepperPos = stepperPos - 1 ' pause 3 ' if (in8 <> 0) then stepCCWLoop ' goto motorLoop2 '-------------------------------------------------------------------------- 'This test loop drives the stepper from limit to limit using the RUN mode 'motorLoop: ' high PORT_STROBE ' high MOTOR_CCW ' low MOTOR_RUN ' outa = LIMIT2 | PORT_DISABLE 'limit2Loop: ' pause 5 ' if (in8 <> 0) then limit2Loop ' high MOTOR_RUN ' low MOTOR_CCW 'back2Loop: ' pulsout MOTOR_STEP,5 ' pause 5 ' if (in8 = 0) then back2Loop ' low MOTOR_RUN ' outa = LIMIT1 | PORT_DISABLE 'limit1Loop: ' pause 5 ' if (in8 <> 0) then limit1Loop ' high MOTOR_RUN ' high MOTOR_CCW 'back1Loop: ' pulsout MOTOR_STEP,5 ' pause 5 ' if (in8 = 0) then back1Loop ' goto motorLoop '-------------------------------------------------------------------------- ' this test loop reads the ADC and displays the converted value ' adcChanSel=0 'adcLoop: ' gosub getADCResult ' cnt = 4 ' addr = RPM_ADDR ' lcdValue = result ' gosub outputValue ' gosub convertToAmps ' lcdValue=result ' addr=AMP1_ADDR ' cnt=2 ' gosub outputValue ' pause 500 ' goto adcLoop '-------------------------------------------------------------------------- ' This test code initializes values, randomly changes them and displays ' dataLow = LCD_SET_ADDR | AMP_SEP_ADDR ' gosub sendLCDCmd ' dataLow = "+" ' gosub sendLCDData ' ' rpm = 1750 ' amp1 = 25 ' amp2 = 45 'dispLoop: ' lcdSupprLZ = 1 ' lcdValue = rpm ' cnt = 4 ' addr = RPM_ADDR ' gosub outputValue ' random rpm ' rpm = rpm // 2000 ' ' lcdSupprLZ = 0 ' lcdValue = (rpm + 29) / 30 ' cnt = 2 ' addr = FREQ_ADDR ' gosub outputValue ' ' lcdValue = amp1 ' cnt = 2 ' addr = AMP1_ADDR ' gosub outputValue ' random amp1 ' amp1 = amp1 // 100 ' ' lcdValue = amp2 ' cnt = 2 ' addr = AMP2_ADDR ' gosub outputValue ' random amp2 ' amp2 = amp2 // 100 ' ' pause 500 ' goto dispLoop ' ptr = text ' gosub outputText 'loop: ' goto loop