Last time I described the digital, distributorless ignition system design and its advantages over conventional vacuum-controlled, distributor ignition systems. I also talked about microcontroller design in general and Motorola's 68HC11 microcontroller specifically. This month I'll explain how engine speed is determined by converting the analog inductive pickup signal into a digital TTL-compatible signal for the microcontroller to read. I'll also explain how the microcontroller will trigger the ignitor circuit to fire the spark plugs. Finally, there will be an in-depth analysis of the embedded assembly language source code. The actual source code will be mixed with the text.
Possibly the most crucial circuit in data acquisition systems is input signal conditioning. Sensor output signals must be modified in some manner to resemble a signal that a microprocessor can read. Before a circuit can be designed, you need to decide what information of the sensor output is important. It may be the output signal strength, its frequency of oscillation, or, in the case of the ignition circuit, its instant of occurrence.
In most vehicles, including the Honda CBR 600 my ignition system is targeted, an inductive pickup senses the rotational speed and position of the crankshaft. An ambitious (or sadistic) reader can review Maxwell's equations regarding magnetic fields or, just "trust me". When a ferrous material moves within a magnetic field of a conductor-wrapped magnet, current flows through the conductor. Figure 1 shows a typical arrangement for crank speed and angle sensing. The inductive pickup is positioned about 3 mm from a steel-toothed wheel. The seven, 5 mm wide teeth of the steel wheel are arranged as eight equally spaced teeth with one missing. As a tooth approaches the sensor current flows in one direction, and as the tooth moves away from the sensor the current is reversed. Between teeth the sensor generates no current. The signal that the sensor produces resembles the topmost waveform in Figure 1.
The only information needed from this signal is the occurrence of a tooth passing the sensor. Engine speed can be found by multiplying the time between adjacent teeth by eight. Engine angle is found by looking for the first pulse after the missing tooth. This pulse occurs at a known angle before top dead center (BTDC) of Piston 1; for this example let's assume it occurs 45 degrees BTDC.
The signal conditioner shown in Figure 2 takes the output from the inductive pickup and converts it into a series of pulses. First, the input diode removes any negative voltages from the signal. That signal is then compared to a voltage set by the potentiometer. This voltage needs to be set high enough so noise won't trigger the amplifier, but low enough so that the real signal is captured. I set mine around 1.5 volts. The LF357 amplifier was chosen because it is common and cheap. It is being used as a voltage comparator instead of an amplifier. A voltage comparator yields a high output (5 volts, in this case) when the positive input (pin 3) is greater than the negative input (pin 2). It yields a low output (0 volts) when the negative input is greater than the positive input.
Now that the signal is a clean digital waveform we can connect it to a microprocessor. The 68HC11 has three inputs specifically designed to read pulsed waveforms. These input capture ports were described in the last article. The 68HC11 can be programmed to be interrupted every time a falling edge occurs on the input capture port. As you will see, I have written an interrupt service routine that looks for the sync pulse, determines the engine speed, and signals the ignitor to fire the spark plugs.
The ignitor circuit is shown at the top of Figure 2. Actually two of these circuits exist in the ignitor unit. One ignitor charges the coil that fires spark plugs 1 and 4; the other ignitor fires plugs 2 and 3. This circuit was placed on a printed circuit board since it switches plenty of current and because its very simple to lay out and etch.
The ignition coil in your car stores electrical energy as current flows through the primary winding. If the current is abruptly halted a large voltage is induced in the secondary coil winding, arcing across the spark plug electrodes. This process is known as "flyback". Our ignitor uses a Motorola MJ10012 Darlington transistor as a current switch to charge and discharge the coil. It can switch five amperes of current at 400 volts though the primary winding of the ignition coil. A 450 volt Zener diode protects the transistor from excess flyback voltage. A potentiometer is used to limit the transistor's current output. My ignition coil can handle five amperes so I have turned the potentiometer down to 35 ohms, allowing the transistor to run at peak current flow.
Motorola's MC3334 (High Energy Ignition Circuit) controls the Darlington's switching time. This chip intelligently controls the dwell time of the ignitor. "Dwell" refers to the amount of time the coil is fully charged. In Figure 3 I have shown the ignition coil current over time. At a specified time the MC3334 switches the Darlington transistor on and current flows into the coil ramping up until the coil is fully charged. The MC3334 does not want to charge the coil too long because that wastes energy; conversely, it does not want to under-charge the coil since that reduces spark energy. The MC3334 senses how well the coil was charged during the last engine revolution. If it was under-charged, additional time is allotted this revolution to fully charge the coil. If the coil was fully charged too long last revolution, the MC3334 will not charge the coil as quickly this revolution.
The 68HC11 tells the MC3334 when it is allowed to charge the coil and when it must fire the spark plugs. Direct your attention back to Figure 1. The two signals labeled "Cylinders 1 and 4" and "Cylinders 2 and 3" are inputs into the two channels of the ignitor (see IN+ in Figure 2; IN- is grounded back to the microcontroller box). While this signal is high (5 volts) the MC3334 may dwell. I stress "may" since the MC3334 decides whether or not to start charging the coil. When the 68HC11 drops the signal low (0 volts) the MC3334 is forced to turn the Darlington transistor off, causing flyback and ignition.
The program that the 68HC11 microcontroller runs resides on a two kilobyte EEPROM. The program is written in Motorola's 68HC11 assembly language. The program is then compiled using Motorola's freeware compiler. The compiler is available from Motorola's freeware BBS and numerous Internet FTP sites. The compiler creates a S19 file which many (E)EPROM programmers accept. The S19 file can also be converted into a binary file which all (E)EPROM programmers accept.
Since this article is not a discourse on assembly language, I will keep descriptions of the code focused only on the pertinent sections. The operation of the program relies on two lookup tables. These lookup tables have static values programmed into the EEPROM (see rpmstatic and advstatic in the code listing) and are copied into RAM when the ignition system is turned on. This is done so you can re-program the amount of advance while the system is running.
The lookup tables operate in the following way. When the third or seventh pulse occurs the engine speed and advance is calculated. The engine speed is determined by reading the 68HC11's free running clock and subtracting the last pulse time. The value remaining is the number of 2 MHz clock tics between inductive pickup pulses and is related to engine speed using Equation 1.
(Eqn. 1)
If this value is below the rev limiter constant, spark advance is determined. First, the clock value obtained above is located in the rpm lookup table using a binary search. The binary search needs eight iterations to find the value. When the value (or the closest to it) is found the offset into the table is used as the offset into the advance lookup table. The number found in the advance table is the number of 2 MHz clock tics that the ignition system will delay into the spark window (Figure 1) before signaling the ignitor to fire the spark plugs. This table is calculated using Equation 2.
(Eqn. 2)
The engine is spinning at 7125 revolutions per minute. Reversing Equation 1, the 68HC11 counts 2105 clock tics between pulses six and seven. Using Table 1 as a guide, the binary search runs through a similar table in RAM and, after eight iterations, finds entry 151 at 7150 RPM (2097 clock tics) to be the closest solution. The nature of the binary search doesn't actually consider the fact that entry 150 is closer to the actual RPM, but since the difference is only 0.12 degrees of advance it is close enough.
Entry Memory RPM RPM Advance tics tics after 45 Offset (in tics) (deg) BTDC deg BTDC --------------------------------------------------------------- 1 0 250 60000 3.00 4000 56000 2 2 296 50675 3.37 3798 46876 3 4 342 43859 3.75 3651 40207 . . . . . . . . . . . . . . 149 296 7058 2125 30.12 1422 702 150 298 7104 2111 30.24 1418 692 151 300 7150 2097 30.36 1414 682 . . . . . . . . . . . . . . 254 506 11888 1261 35.00 980 280 255 508 11934 1256 35.00 976 279 256 512 11980 1252 35.00 973 278
Entry 151 corresponds to an offset of 300 bytes into the rpm lookup table. The 68HC11 retrieves the spark advance value from offset 300 in the advance lookup table; it finds 682 clock tics; the ignition system wants to fire 30.36 degrees before top-dead-center. Using Equation 2 above, 30.36 degrees BTDC at 7150 RPM (2097 clock tics) equals 1414 clock tics before top-dead-center. Since the processor cannot count back from an event yet to occur, 1414 clock tics is subtracted from 2105 clock tics between pulses. It is found that the 68HC11 should trigger the ignitor 681 clock tics after the first pulse. This would be approximately 30.36 degrees before top-dead-center of cylinders one and four.
These first six equates define the memory map of the ignition system. Our two kilobyte EEPROM is physically mapped to address $F800 ($ denotes a hexidecimal value), variables will reside in the eight kilobyte SRAM and are mapped to address $2000, and the stack is mapped to the high end of the SRAM and uses memory in reverse fashion. In other words, if we placed large amounts of data on the stack it would start overwriting our variables in low RAM. The remaining three equates are pointers to the interrupt service routines that will be placed in the interrupt vector table.
ROM EQU $F800 start of program RAM EQU $2000 start of variables STACK EQU $3FFF bottom of stack SCI_IRQV EQU $FFD6 serial communications interrupt vector TIC1_IRQ EQU $FFEE timer input capture 1 interrupt vector RESETV EQU $FFFE reset interrupt vector
The following code shows how the variables are arranged in RAM. The variables were arranged so that multi-byte variables are located on an even address boundary. If not located on an even address ($2002, $200A, etc.), the 68HC11 requires an additional clock cycle to grab the odd byte. Sometimes a dummy byte has to be used to keep the variables aligned.
ORG RAM blinkcnt RMB 2 led blinker counter rev_limit RMB 2 rev limit count value rev_limit_flag RMB 1 set if rev limiter activated sci_complete RMB 1 serial message completely received flag sci_continuous RMB 1 send serial data continuously flag sci_type RMB 1 serial message type sci_chksum RMB 1 serial message checksum sci_index RMB 1 index into serial receive message buffer sci_buffer RMB 256 serial message buffer sci_c_start RMB 2 serial data send start address sci_c_index RMB 1 index into serial send message buffer sci_c_size RMB 1 serial send data size in bytes sync_count RMB 1 sync pulse count pulse RMB 1 inductive pickup pulse counter new_tic1 RMB 2 time captured by timer input capture port 1 old_tic1 RMB 2 last time captured by timer input capture port 1 new_time RMB 2 time between pulses old_time RMB 2 time between last two pulses rpmcount RMB 2 RPM count for PC to read sparkadv RMB 2 spark advance calculated from binary search bin_offset RMB 2 binary search offset bin_count RMB 1 binary search iteration count dummy RMB 1 not used - realign variables to even address tempword RMB 2 temporary 2-byte variable tempbyte RMB 1 two temporary 1-byte variables tempbyte2 RMB 1 rpm RMB 512 TIC1 count -> RPM lookup table advance RMB 512 RPM -> spark advance lookup table
The test string below was added to test the serial communications. From the PC I would request 32 bytes to be sent starting at address $F800. The ignition controller would send "Ignition Controller is running!" and confirm that the serial communications is indeed functioning. Following the test string are the two static lookup tables. Both sets of 512 bytes are copied into RAM at the memory locations reserved by the rpm and advance variables above.
ORG ROM teststring FCC \Ignition Controller is running! \ rpmstatic FDB $EA60,$C5F3,$AB53,$9703,$8702,$7A12,$6F65,$666F FDB $5ECF,$583E,$5286,$4D81,$490F,$4518,$418A,$3E55 FDB $3B6C,$38C6,$365A,$3421,$3214,$302F,$2E6D,$2CCB FDB $2B46,$29DA,$2885,$2745,$2618,$24FD,$23F2,$22F5 FDB $2206,$2124,$204D,$1F80,$1EBD,$1E04,$1D53,$1CAA FDB $1C09,$1B6E,$1ADA,$1A4C,$19C4,$1941,$18C3,$184A FDB $17D6,$1766,$16FA,$1692,$162D,$15CC,$156E,$1513 FDB $14BB,$1466,$1414,$13C4,$1377,$132C,$12E3,$129C FDB $1258,$1215,$11D4,$1195,$1158,$111C,$10E2,$10AA FDB $1073,$103D,$1009,$FD6, $FA4, $F73, $F44, $F15 FDB $EE8, $EBC, $E91, $E67, $E3E, $E15, $DEE, $DC7 FDB $DA1, $D7D, $D58, $D35, $D12, $CF0, $CCF, $CAE FDB $C8E, $C6F, $C50, $C32, $C14, $BF7, $BDB, $BBF FDB $BA3, $B88, $B6E, $B54, $B3A, $B21, $B08, $AF0 FDB $AD8, $AC1, $AAA, $A93, $A7D, $A67, $A51, $A3C FDB $A27, $A13, $9FE, $9EA, $9D7, $9C4, $9B0, $99E FDB $98B, $979, $967, $956, $944, $933, $922, $911 FDB $901, $8F1, $8E1, $8D1, $8C2, $8B2, $8A3, $894 FDB $886, $877, $869, $85B, $84D, $83F, $831, $824 FDB $817, $80A, $7FD, $7F0, $7E3, $7D7, $7CB, $7BF FDB $7B3, $7A7, $79B, $78F, $784, $779, $76E, $763 FDB $758, $74D, $742, $738, $72D, $723, $719, $70F FDB $705, $6FB, $6F1, $6E8, $6DE, $6D5, $6CB, $6C2 FDB $6B9, $6B0, $6A7, $69E, $695, $68D, $684, $67C FDB $673, $66B, $663, $65A, $652, $64A, $642, $63B FDB $633, $62B, $623, $61C, $614, $60D, $606, $5FE FDB $5F7, $5F0, $5E9, $5E2, $5DB, $5D4, $5CE, $5C7 FDB $5C0, $5B9, $5B3, $5AC, $5A6, $5A0, $599, $593 FDB $58D, $587, $580, $57A, $574, $56E, $569, $563 FDB $55D, $557, $551, $54C, $546, $541, $53B, $536 FDB $530, $52B, $525, $520, $51B, $516, $510, $50B FDB $506, $501, $4FC, $4F7, $4F2, $4ED, $4E8, $4E4 advstatic FDB $DAC0,$B71C,$9D0F,$8930,$7987,$6CDF,$626D,$59A9 FDB $5233,$4BC6,$462E,$4145,$3CEB,$390A,$3590,$326D FDB $2F94,$2CFD,$2A9E,$2871,$2670,$2495,$22DD,$2144 FDB $1FC7,$1E63,$1D15,$1BDC,$1AB6,$19A1,$189C,$17A4 FDB $16BB,$15DD,$150B,$1442,$1384,$12CF,$1221,$117C FDB $10DE,$1047,$FB6, $F2B, $EA6, $E26, $DAB, $D35 FDB $CC3, $C55, $BEC, $B86, $B23, $AC4, $A68, $A0F FDB $9B9, $966, $916, $8C8, $8A6, $878, $84B, $820 FDB $7F6, $7CD, $7A5, $77F, $759, $735, $711, $6EF FDB $6CD, $6AC, $68C, $66D, $64E, $630, $613, $5F6 FDB $5DB, $5C0, $5A5, $58C, $572, $559, $541, $52A FDB $512, $4FC, $4E6, $4D0, $4BB, $4A6, $491, $47D FDB $46A, $457, $444, $431, $41F, $40D, $3FC, $3EB FDB $3E1, $3D8, $3CF, $3C6, $3BE, $3B5, $3AD, $3A5 FDB $39D, $395, $38E, $386, $37F, $377, $370, $369 FDB $362, $35B, $354, $34E, $347, $341, $33A, $334 FDB $32E, $328, $322, $31C, $316, $311, $30B, $305 FDB $300, $2FB, $2F5, $2F0, $2EB, $2E6, $2E1, $2DC FDB $2D7, $2D2, $2CD, $2C9, $2BE, $2B4, $2AA, $2A0 FDB $296, $28D, $283, $27A, $270, $267, $25E, $256 FDB $24D, $244, $23B, $233, $22A, $222, $21A, $212 FDB $20A, $202, $1FA, $1F2, $1EA, $1E3, $1DB, $1D4 FDB $1CD, $1C5, $1BE, $1B7, $1B0, $1A9, $1A2, $19B FDB $195, $18E, $188, $181, $17A, $174, $172, $170 FDB $16E, $16D, $16B, $169, $167, $165, $164, $162 FDB $160, $15E, $15D, $15B, $159, $158, $156, $154 FDB $153, $151, $150, $14E, $14D, $14B, $14A, $148 FDB $147, $145, $144, $142, $141, $140, $13E, $13D FDB $13B, $13A, $138, $137, $136, $134, $133, $132 FDB $131, $12F, $12E, $12D, $12C, $12A, $129, $128 FDB $127, $126, $124, $123, $122, $121, $120, $11E FDB $11D, $11C, $11B, $11A, $119, $118, $117, $116
Here is the main entry point of the software. When the reset vector is fetched by the 68HC11 the address for this code is loaded into the program counter and the microcontroller starts executing this code. These first lines re-map the 68HC11's register set into low memory starting at $0000 so they can be accessed a little faster than if they were at their default $1000. The 68HC11's internal RAM is re-mapped to $1000 but is not used by the ignition controller.
main: lds #STACK load stack pointer ldaa #$10 remap ram=10xx and regs=00xx staa $103D by writing to CONFIG register jsr initial do other initialization
The main loop of code below performs two primary functions. It first checks if a serial command message has been received and, if so, performs the requested command. It also toggles the state of an LED on the system board. Casual inspection of this LED will tell you immediately if the system is functioning properly; it should blink at a continuous, steady rate.
loop: ldaa sci_complete tsta entire serial message received? bne decod_mes yes, decode the message jmp chk_continuous no, jump to continuous mode check decod_mes: ldaa sci_type get message type cmpa #0 type = 0? (send bytes) bne type1 no, check for type 1 * send size, tag, N data bytes, and checksum back to PC via RS 232. ldx sci_buffer+1 get address of bytes to send ldab sci_buffer+3 get number of bytes to send type0size: * loop until serial xmitter ready to send brclr SCSR #%1000000 type0size stab SCDR send the size stab tempbyte checksum = size ldaa sci_buffer get the data tag type0tag: * loop until serial xmitter ready to send brclr SCSR #%10000000 type0tag staa SCDR send tag adda tempbyte checksum = checksum + tag staa tempbyte store checksum type0loop: tstb done sending all the bytes? beq end_type0 yes, skip sending code ldaa ,X get byte to send type0wait: * loop until serial xmitter ready to send brclr SCSR #%10000000 type0wait staa SCDR send the byte adda tempbyte checksum = checksum + data byte staa tempbyte store checksum inx point to next byte to send decb decrement number of bytes to send bra type0loop loop back end_type0: * loop until serial xmitter ready to send brclr SCSR #%10000000 end_type0 ldaa tempbyte get the checksum staa SCDR send the checksum clra staa sci_complete clear flag, ready for another message ldaa PORTD read Port D anda #%11110111 clear CTS (ready for another serial message) staa PORTD write the byte bra chk_continuous jump down type1: cmpa #1 type = 1? (write bytes) bne type2 no, check for type 2 ldx sci_buffer get address of bytes to write over ldy #sci_buffer+3 get address of source of data ldab sci_buffer+2 get number of bytes to write type1loop: tstb all bytes written? beq end_type1 yes, skip write ldaa ,Y get data byte to write staa ,X write data byte to destination inx increment destination pointer iny increment source pointer decb decrement byte counter bra type1loop loop back end_type1: clra staa sci_complete clear flag, ready for another message ldaa PORTD read Port D anda #%11110111 clear CTS (ready for another serial message) staa PORTD write the byte bra chk_continuous jump down type2: cmpa #2 type = 2? (read bytes continuously) bne type3 no, check for type 3 ldx sci_buffer get address of bytes to read stx sci_c_start save address in continuous mode start address ldaa sci_buffer+2 get number of bytes staa sci_c_size save in continuous mode size variable ldaa #1 staa sci_continuous set continuous mode flag deca staa sci_c_index set continuous mode index to start of buffer staa sci_complete clear flag, ready for another message ldaa PORTD read Port D anda #%11110111 clear CTS (ready for another serial message) staa PORTD write the byte bra chk_continuous jump down type3: cmpa #3 type = 3? (jump to new code) bne type4 no, check for type 4 ldx sci_buffer get address to jump to clr sci_complete clear flag, ready for another message ldaa PORTD read Port D anda #%11110111 clear CTS (ready for another serial message) staa PORTD write the byte jmp ,X jump to new code type4: clra error, no more types staa sci_complete clear flag, ready for another message ldaa PORTD read Port D anda #%11110111 clear CTS (ready for another serial message) staa PORTD write the byte chk_continuous: ldaa sci_continuous tsta serial continuous mode set? beq cont_done no, skip next block of code brclr SCSR #%10000000 cont_done if serial xmitter busy, skip send ldx sci_c_start get beginning of send buffer ldab sci_c_index get send buffer index abx add buffer start address to index ldaa ,X get byte from send buffer + index staa SCDR transmit byte to PC incb increment send buffer index cmpb sci_c_size past end of buffer? bne no_reset no, dont reset index clrb yes, reset index to zero no_reset: stab sci_c_index cont_done: ldd blinkcnt get blink count xgdx dex decrement blink count xgdx std blinkcnt store blink count beq blinkled if zero, skip down jmp loop else, jump to main loop blinkled: ldaa PORTA read port A eora #%01000000 toggle LED bit staa PORTA write to port A ldd #BLINKCOUNT load new blink count std blinkcnt jmp loop loop back
The initialization code below is executed when the system is first turned on. The blinking LED countdown variable is initialized, and the static RPM and spark advance tables are copied into RAM. One of the 68HC11's input capture ports (TIC1) is configured to capture the inductive pickup on falling edges. It also generates an interrupt on the same falling edge; the interrupt is cleared and turned on.
The 68HC11's serial port (PORT D) is programmed for handshake input and output. The baud rate is set for 9600 bits per second and some serial communication variables (sci_xxx) are initialized.
initial: ldd #BLINKCOUNT std blinkcnt led blink count down value ldaa #1 staa tempbyte2 going to perform 2 copies clr tempbyte each copy is 256 (=0) words ldx #rpmstatic source ldy #rpm destination copy: ldd ,X get word from source std ,Y store in destination inx inx iny iny increment indexes to next word inc tempbyte increment word count bne copy has count rolled over? * no, keep copying dec tempbyte2 yes, decrement number of copies beq copy if only 1 copy done, copy 256 more ldaa #%00100000 staa TCTL2 capture on falling edges of TIC 1 ldaa #%00000100 staa TMSK1 enable interrupt on falling edge of TIC 1 staa TFLG1 clear TIC 1 flag by writing 1 to it ldaa #2 staa sync_count initialize sync count to 2 deca initialize rev limit flag to 1 so no staa rev_limit_flag firing occurs until safe rpm ldaa #%00001110 staa DDRD configure Port D data direction register * bit 5 input handshake bit 1 (PC=RQS) * bit 4 input handshake bit 0 (PC=DTR) * bit 3 output handshake bit 1 (PC=CTS) * bit 2 output handshake bit 0 (PC=DSR) * bit 1 serial data out (to PC RxD) * bit 0 serial data in (from PC TxD) ldaa #%00110000 staa BAUD serial interface runs at 9600 baud ldaa #%00101100 staa SCCR2 enable serial output & input clra staa sci_complete clear serial message complete flag staa sci_continuous clear continuous send mode flag staa sci_chksum clear serial checksum staa sci_index reset receive buffer pointer to zero staa rev_limit_flag clear rev limiter flag staa pulse clear inductive pulse count ldd #REV_LIMIT std rev_limit set rev limit ldaa PORTD read Port D anda #%11110011 clear CTS and DSR staa PORTD write the byte cli enable interrupts rts return
The next section of code is the serial communications interrupt service routine. If the interrupt was not called because of a communications error, the serial byte and handshake lines are read. Depending on the handshake bits, the byte is interpreted as a type, data, or checksum byte. If a type byte, it is stored and the interrupt ends. If a data byte it is stored in the serial message buffer pointed to by sci_buffer and the interrupt returns. If the byte the checksum, it is compared with the running checksum. It they don't match the received message is thrown away.
* entry point for serial IRQ sci_irq: ldaa SCSR get serial status anda #%00001110 clear all but error bits tsta any error bits set? beq sci_receive no, go to rest of IRQ ldaa SCDR yes, read SCDR to clear rti return sci_receive: ldaa PORTD read Port D oraa #%00001000 set CTS (serial I/O busy) staa PORTD write the byte ldab SCDR get byte from serial port ldaa PORTD get handshake bytes anda #%00110000 clear all but input handshake bytes cmpa #%00000000 handshake = 00? (type byte) bne sci_chk_data no, check if data byte stab sci_type yes, save type byte stab sci_chksum store checksum clra staa sci_continuous clear continuous flag rti return sci_chk_data: cmpa #%00100000 handshake = 10? (data byte) bne sci_chk_chksum no, check if checksum byte ldx #sci_buffer yes, get offset to input buffer tba move serial byte to register A ldab sci_index get index into input buffer abx add index to buffer staa ,X put data byte in input buffer incb increment index into buffer stab sci_index and store back in memory adda sci_chksum add data byte to checksum staa sci_chksum and store checksum rti return sci_chk_chksum: cmpb sci_chksum does input byte match sci_chksum? bne sci_bad_message no, dont set message complete flag ldaa #1 yes, staa sci_complete set incoming message complete flag sci_bad_message: clra staa sci_chksum clear checksum byte staa sci_index reset index into input buffer to zero staa sci_continuous clear continuous mode flag ldaa PORTD read Port D anda #%11110111 clear CTS (serial I/O ready) staa PORTD write the byte rti return
The following code is the timer input capture (TIC1) interrupt service routine. The ISR is executed whenever the 68HC11 detects a falling edge at Input Capture Port 1. It first checks if at least three inductive pulses have occurred; the routine needs two engine speed samples before it can detect the sync pulse. As soon as the first sync pulse is detected, the program starts counting inductive pulses.
Engine speed and spark advance are calculated on the third and seventh pulses. On pulses one and five an output compare port (TOC5 and TOC4, respectively) is programmed to count down and then drop to 0 volts. This forces the MC3334 to turn the Darlington transistor off and causes the coils to flyback, sparking the appropriate pair of spark plugs. If the rev-limiter flag is set because the engine speed is too high, TOC5 is not programmed and those pair of plugs will not fire. On pulses three and seven the timer output compare ports are returned to their high state, allowing the MC3334 to dwell again.
tic1_irq: ldaa #%00000100 clear TIC 1 interrupt flag so next staa TFLG1 interrupt will be caught ldx TIC1 get time from free running clock stx new_tic1 store it ldaa sync_count get the sync count cmpa #2 1st pulse counted? bne chk_if_2nd no, not 1st pulse, jump dec sync_count yes, decrement sync count jmp endTICirq and exit chk_if_2nd: cmpa #1 2nd pulse? bne chk_if_3rd no, jump xgdx put TIC 1 count in D register subd old_tic1 get time difference std new_time store it dec sync_count decrement sync counter jmp endTICirq jump to exit chk_if_3rd: xgdx put TIC 1 count in D register subd old_tic1 get time difference std new_time store it lsrd D = time / 2 pshb psha push D onto stack pulx X = D = time / 2 lsrd D = time / 4 stx tempword addd tempword done! D = 3/4 * new_time cpd old_time 3/4*new_time > old_time ? bhi synch_pulse yes, synch pulse just occured...jump tst pulse no, not sync pulse. already synched? bne synched yes, jump jmp endTICirq no, exit synch_pulse: * *** 1st pulse *** ldaa #1 staa pulse set pulse count = 1 tst rev_limit_flag rev limiter on? bne nofire yes, skip firing sequence ldd sparkadv no, get spark advance count subd #DELAY1 subtract delay (contant) addd TCNT add to current time std TOC5 put into Output Capture 5 bclr TCTL1 #%00000001 bset TCTL1 #%00000010 enable TOC 5 to go low nofire: jmp endTICirq exit synched: ldaa #1 A = 1 cmpa pulse is this pulse 2? bne chk_pulse3 no, check if 3rd pulse * *** 2nd pulse *** inc pulse increment pulse counter jmp endTICirq exit chk_pulse3: inca A = 2 cmpa pulse is this pulse 3? bne chk_pulse4 no, check if 4th pulse * *** 3rd pulse *** inc pulse increment pulse counter bclr TCTL1 #%00000011 disable OC 5 bset PORTA #%00001000 set OC 5 high to start dwelling ldd new_time get time std rpmcount put in memory for PC to read cpd rev_limit over rev limit? bhi below_rev_limit no, jump down ldab #1 stab rev_limit_flag yes, set flag jmp endTICirq exit below_rev_limit: clr rev_limit_flag clear rev limiter flag xgdx move rpm count to X ldab #8 stab bin_count binary search has 8 iterations ldd #256 std bin_offset index = 128 xgdx move rpm count back to D ldx #rpm get offset into rpm lookup table bin_loop_top: pshx save X xgdx addd bin_offset get offset xgdx cpd ,X new rpm < rpm in rpm+offset bhi belowRPM yes, jump down puly no, throw away X value from stack lsr bin_offset+0 ror bin_offset+1 offset = offset / 2 bra bin_loop_bottom jump down belowRPM: lsr bin_offset+0 ror bin_offset+1 offset = offset / 2 pulx restore X nop nop add delay to equalize branches bin_loop_bottom: dec bin_count counter = counter - 1 bne bin_loop_top loop until 8 iterations are done xgdx put offset into rpm lt in D subd #rpm get just the offset addd #advance offset into advance lookup table xgdx put offset in X ldd ,X get spark advance count std sparkadv save spark advance count jmp endTICirq exit chk_pulse4: inca A = 3 cmpa pulse is this pulse 4? bne chk_pulse5 no, check if 5th pulse * *** 4th pulse *** inc pulse increment pulse counter jmp endTICirq exit chk_pulse5: inca A = 4 cmpa pulse is this pulse 5? bne chk_pulse6 no, check if 6th pulse * *** 5th pulse *** inc pulse increment pulse counter ldd sparkadv no, get spark advance count subd #DELAY2 subtract delay (contant) addd TCNT add to current time std TOC4 put into Output Capture 4 bclr TCTL1 #%00000100 bset TCTL1 #%00001000 enable TOC 4 jmp endTICirq exit chk_pulse6: inca A = 5 cmpa pulse is this pulse 6? bne chk_pulse7 no, check if 7th pulse * *** 6th pulse *** inc pulse increment pulse counter jmp endTICirq exit chk_pulse7: * since there are no more pulses... * *** 7th pulse *** bclr TCTL1 #%00001100 disable OC 4 bset PORTA #%00010000 set OC 4 high ldd new_time get time std rpmcount put in memory for PC to read cpd rev_limit over rev limit? bhi below_rev_limit2 no, jump down ldab #1 stab rev_limit_flag yes, set flag jmp endTICirq exit below_rev_limit2: clr rev_limit_flag clear rev limiter flag xgdx move rpm count to X ldab #8 stab bin_count binary search has 8 iterations ldd #256 std bin_offset index = 128 xgdx move rpm count back to X ldx #rpm get offset into rpm lookup table bin_loop_top2: pshx save X xgdx addd bin_offset get offset xgdx cpd ,X new rpm < rpm in rpm+offset bhi belowRPM2 yes, jump down puly no, throw away X value from stack lsr bin_offset+0 ror bin_offset+1 offset = offset / 2 bra bin_loop_bottom2 jump down belowRPM2: lsr bin_offset+0 ror bin_offset+1 offset = offset / 2 pulx restore X nop nop add delay to equalize branches bin_loop_bottom2: dec bin_count counter = counter - 1 bne bin_loop_top2 loop until 8 iterations are done xgdx yes, put offset into rpm lt in D subd #rpm get just the offset addd #advance offset into advance lookup table xgdx put offset in X ldd ,X get spark advance count std sparkadv save spark advance count * dont need to reset pulse counter since the IRQ checks for synching * each time it is called. when the synch pulse occurs the pulse counter * will be reset to zero. endTICirq: ldd new_tic1 get TIC 1 count from this interrupt std old_tic1 store as old TIC 1 count for next IRQ ldd new_time get time calculated from this IRQ std old_time store as old time for next IRQ rti return
The next statements map the interrupt service routines in the 68HC11's interrupt vector table. The interrupt vector table is located in upper memory and, not coincidental, the upper area of the EEPROM. When the 68HC11 is interrupted by a power reset, for example, it fetches the two-byte address located in the reset vector (address $FFFE-$FFFF). In my code, a power-on reset causes the 68HC11 to load the address of the main code into the program counter and start executing from that address.
ORG SCI_IRQV vector to serial interrupt FDB sci_irq replace with new vector ORG TIC1_IRQ vector to timer input capture interrupt FDB tic1_irq replace with new vector ORG RESETV vector to reset interrupt FDB main replace with new vector
This system can be easily modified to fit virtually any ignition setup. Depending on the number of teeth and position of the timing wheel with respect to the crankshaft the timer input capture interrupt service routine will need to be adjusted. Just cut and paste the code into the proper pulse sections. If you have more teeth you may have a lot of empty pulse sections and if you have many more teeth you need to be careful that you complete your computations in the interrupt service routine before another tooth is detected - it may be necessary to half the number of pulses coming to the 68HC11 by adding a flip-flop or using a counter chip if even more division of pulses is necessary.
Alas I haven't even mentioned the software that runs on the PC and talks to the ignition system. I have written a graphical DOS program that you can download or you can write your own. All you have to know is the serial communications protocol that the ignition controller understands; this was described in the first article. My program lets the user modify a bar graph that represents the spark advance over the engine's RPM range. When the user elects to send the new curve to the ignition controller, the new advance table is calculated from the bars and uploaded into the RAM of the ignition system.
There is one piece of code that hasn't been completed. Ideally, when the user is finished changing the spark advance curve with the DOS program, it would be nice to have the advance table stored in RAM copied back into the EEPROM that stores the static advance table. This way when the system is turned off the new spark advance curve is not lost and, when powered back up, the new spark advance curve is loaded into RAM.
Lastly a word of caution. Advancing the spark too much can be dangerous to the engine. Start with a conservative curve and gradually advance the spark curve. If your engine has a knock sensor then monitor it. If it doesn't then be gentle - advancing the spark until you hear knock is not a good idea since any knocking is potentially dangerous. Have you ever seen a hole burned clear through a piston? Not a pretty sight. Oh yeah, connecting the spark plug wires to the wrong plugs isn't a good idea either so label them. Being 180 degrees out of phase blasted our intake manifold off the engine and scared the bejeezus out of me. I think it took about five years off my life span. Besides that, have fun!