A Digital Distributorless Ignition System - Part II

By Tim Drury

Preface

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.

Signal Conditioning

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.

Ignitor

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.

On Board Software

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)

Example

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.

Table 1. RPM and Advance lookup tables.
 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.

Assembly Language Program Listing

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

Conclusion

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!


Last Modified: 10:37pm , January 17, 1996