Click on our menu buttons immediately below to find MegaSquirt® information quickly:

MegaSquirter's Introduction to Assembly Language Programming

This is a basic introduction to assembly language programming for the M68HC908GP32 microcontroller used in the MegaSquirt EFI system. It is not intended to tell you everything you need to know about programming CPU08 microcontrollers (A microcontroller is a complete computer system, including a CPU, memory, a clock oscillator, and I/O (input/output) on a single integrated circuit chip), and is not intended to be a comprehensive look at programming the MegaSquirt.

Instead, this document is intended for those who wish to know enough about how the MegaSquirt is programmed to be able to delve into more technical documents like:

If you are looking for general programming information, the Tech Books for Free site has a scattergun collection of programming resources, for all types of programming and application areas.

Here is some good stuff on hardware design.

After reading this tutorial, and some of the above references above, you ought to be able to follow coding discussions on the MegaSquirt Forums.

If you would like a textbook, an absolutely outstanding book on microcontrollers is Embedded Microprocessor Systems, by Jonathan Valvano, ISBN 0-534-36642-2. It doesn't have a lot of theory, just meat. There is also a lot of information on algorithms like PID loops, how to make a real-time scheduler, etc. - things we are all interested in. The front and rear covers have charts and tables on practical items (like different capacitor types, popular transistor types with relevant info, etc). Get this book and you will be able to make MegaSquirt circuit and software changes yourself. It is expensive at $107, but worth every dollar.

Software to Edit and Compile Code

To create modified code for MegaSquirt® EFI Controller, you need to:

  1. Download the source files. You need a number of files besides the megasquirt.asm to make a modified megasquirt.s19 file. These files are normally included in the ZIP file that contains the .ASM file, such as :

    This ZIP file contains:

  2. Download the compiler. You need the full P&E assembler/linker/simulator/IDE (version 1.50) which can be used to assemble, link, and program the MC68HC908GP32 part with the above files. Download the full P&E assembler/linker/simulator/IDE (version 1.50) which can be used to assemble, link, and program the MC68HC908GP32.

  3. Extract the P&E Assembler tool set to a directory on your hard drive. (The software has a fairly short limit on the path to the executables directory, so don't bury it in a sub-sub-sub-sub... folder! Something like c:\ics08asm\*.* works well.)

  4. Use the WinIDE (Windows Integrated Development Environment), which is included in the above zip file to modify the megasquirt.asm file to include your modifcations. Note that you have to set the Environment [under 'Setup Environment'] to match the directory to which you have installed the ZIP file so that WinIDE can find the compiler, simulator, etc..
    1. 'Open File' for the desired .asm file (megasquirt.asm) and make sure the associated files (*.inc, .h, Gp32.equ, etc.) are in the same directory. You can get these files from the above link.
    2. Edit the file and save it under the same name (you can change the name, this will change the name of the compiled .s19 file that will be produced as well)
    3. Assemble/Compile the code by clicking on the 'Assemble/Compile' button [leftmost], and the megasquirt.s19 file is created (unless you have errors, in which case you'll have to fix them!). The .s19 file has the same name as the .asm file by default.

  5. Download the modified megasquirt.s19 file to the chip using the original bootloader, such as with the downloader or hyperterminal, or the Willette programmer and the prog08sz, which will be the third icon from the left, if you have set the environment to the defaults.

The tutorial covers mostly the coding aspect. Follow the links above to find out more about things like the bootloader, downloading code, etc. It is probably a good idea to get the above downloads now and install them, so you can see other examples, etc. With all that out of the way, lets get into the the discussion!

In this document, we will look in detail at some sections of the code from the megasquirt.asm file. The entire file will not be disected line by line [it's 39 pages, and is left as an exercise for the reader, as textbooks say!]

Why Assembly Language?

Why is MegaSquirt programmed in assembly language? Couldn't it be programmed in C++, or even BASIC? The answer is yes, but only by making the code much longer and slower. Assembly language is the only programming scheme that has a one-to-one correspondence with the machine language operational codes the MegaSquirt's processor recognizes. What you program in assembly language is what the machine runs, nothing more.

Using other higher-level languages would add a great deal of rarely used, but sometime required code for nearly every line of code. Think of it like word processing. A basic ASCII file that contains the message "Thanks to Bruce Bowling and Al Grippo" is much smaller than the Word2000 document that says the same thing. If you want to be able to produce the message easily, in a formatted font, and have it shown in a variety of systems, you would use Word. However, if you want the most efficient package for the message, you would choose an ASCII file. The same reasoning applies to assembly language programming.

In MegaSquirt® EFI Controller, it is important that the code be efficient, since we are asking it to do a lot of operations in a short time. Some of the instructions (also known as 'opcodes') will be almost obvious, like ADD for addition, and SUB for subtraction. Others, like TAX (transfer A to X), or DBNZ (Decrement and Branch if Not Zero) take more getting used to. All of the instructions are listed below, but we should cover a few topics before looking at those.

In order to understand the assembly language code, you need to be familiar with several concepts:

We will start with the first two concepts, and throw in the others as we examine examples from the MegaSquirt code.


The MC68HC908GP has five registers. These are temporary storage for intermediate memory addresses, calculations, and results. Because these registers are directly wired into the CPU, they do not have 'conventional' memory addresses like program or data. Instead they are referred to invoking by the appropriate instruction.

The registers are:

  1. The accumulator (A) is a general-purpose 8-bit register. The central processor unit (CPU) uses the accumulator to hold operands and results of arithmetic and non-arithmetic operations.
  2. The 16 bit index register (H:X), which allows the user to index or address a 64-Kbyte memory space. The concatenated 16-bit register is called H:X. The upper byte of the index register is called H. The lower byte of the index register is called X.
  3. The 16 bit stack pointer (SP), which contains the address of the next location on the stack. The address in the stack pointer decrements as data is pushed onto the stack and increments as data is pulled from the stack. The SP always points to the next available (empty) byte on the stack. The stack pointer (SP) is used as a pointer to the next available location in a last-in-first-out (LIFO) stack. The stack can be thought of as a pile of cards, each holding a single byte of information. At any given time, the CPU can put a card on top of the stack or take a card off the stack. Cards within the stack cannot be picked up unless all the cards piled on top are removed first. This stack is used to hold return addresses while the CPU is executing a subroutine and holds the previous contents of all CPU registers while the CPU is executing an interrupt sequence. By recovering this information from the stack, the CPU can resume where it left off before the subroutine or interrupt was started. The reset stack pointer (RSP) instruction sets the least significant byte to $FF and does not affect the most significant byte.
  4. The 16 bit program counter (PC), that contains the address of the next instruction or operand to be fetched. Normally, the address in the program counter automatically increments to the next sequential memory location every time an instruction or operand is fetched. Jump, branch, and interrupt operations load the program counter with an address other than that of the next sequential location.
  5. The 8 bit condition code register (CCR) contains the interrupt mask and five flags that indicate the results of the instruction just executed. Bits five and six are permanently set to equal 1. For example, the CCR stores the result of CMP (compare) instructions, set bits indicating which of the compared memory locations was larger. This can then be used to perform conditional program branching.

Many instructions are unique to the particular register, but have familiar forms and mnemonics: for example, there is sta $xxxx for SToring the contents of the Accumulator in the memory location $xxxx, and sthx $xxxx for SToring the contents of the index register H:X in the memory location $xxxx. Other examples include lda and ldhx (for LoaDing the contents of a memory address into a particular register), and clra, clrh, and clrx to clear the respective values of the registers. Some instructions operate on more than one register: tax, for example, Transfers the contents of the Accumulator to the X byte of the index register.

Addressing Memory

The MegaSquirt's 68HC908GP32 processor has:

The set of addresses for the entire memory is called a memory map, and you can see the MegaSquirt's memory map here. The MegaSquirt program and any variables that are 'remembered' when MegaSquirt is shut down are stored in FLASH. An example is the Volumetric Efficiency (VE) table data. MegaSquirt handles the whole VE table in the following manner: when you turn on the MegaSquirt box, the embedded code copies the VE table from FLASH to RAM, and the MegaSquirt uses the VE values in RAM. When you are on the tuning page in MegaTune, you are poking numbers into RAM, and the MegaSquirt instantly uses the new value. There is a separate "burn" command which burns the values in RAM into FLASH, such that when you re-power the MegaSquirt® EFI Controller, the new values are used. The VE values are stored in memory in a location defined as VETABLE.

Why not use the values directly from FLASH? There are two reasons: it can be slower because we might have to use longer memory addresses, and it is much more involved to write values back to FLASH. You can only write to FLASH by first erasing it. You can only erase flash in 128-byte chunks. So to change one value we would have to erase 128 bytes, then write 128 bytes, making at least an extra 254 steps (128 erases plus 128 writes minus the 2 we would have done any ways)!. As well, coded delays need to be built in because the FLASH writes are physically slower than writing to RAM. There are various tricks to minimize the number of erase/write cycles with queues and other techniques, but it is way too complicated for our application.


Example 0

Load Flash to RAM

Here are two sections of the megasquirt.asm code that show the transfer of the VE table from the FLASH memory to RAM and back. Don't worry about the details of these two snippets. They are shown here only to demonstrate how much more cumbersome writing to flash is than writing to RAM. As a result, it is much more efficient to do program calculations in RAM whenever possible.

Write from FLASH to VETABLE

sta VE,x
cbeqx #$80,XXXV
bra XXVV

Write from VETABLE to FLASH

; Erase VE and Constants in FLASH - 128 byte erase ERASE_N_BURN:
ldhx #FLCR
lda #%00000010
sta ,x
lda #$0C
lda #%00001010
sta ,x
lda #$F0
lda #%00001000
sta ,x
lda #$06
lda #$00000000
sta ,x
lda #$02


; Now Burn VE (you can only freakin burn 64 bytes at a time) VE_BURNER
lda #%00000001
sta ,x
lda #$0C
lda #%00001001
sta ,x
lda #$06

lda VE,x
lda #$22
cpx #$40
beq SD1

ldhx #FLCR
lda #%00001000
sta ,x
lda #$06
lda #%00000000
sta ,x
lda #$01
bra BURN_C


Notice in the above example how many instructions end in a, h, x, or hx. In every case, the commands perform some operation regarding the contents of the accumulator, the high byte (h) of the index register, the low byte (x) of the index register, or both bytes (h:x) of the index register respectively.

The address range $0000 to $00FF (i.e. 0 to 255) is called the direct page, base page or zero page of RAM, because the first two numbers (the high byte) are zero. On the MegaSquirt® microcontroller, the lower part of the direct page always contains I/O and control registers ($0000 to $003F) and the upper part of the direct page ($0040 - $00FF) always contains RAM (which runs from $0040 - $023F). The direct page is important because most CPU08 instructions have a direct addressing mode variant whereby they can access operands in the direct page in one clock cycle less than in extended addressing mode. Furthermore the direct addressing mode instruction requires one less byte of code, saving memory space. Using the zero page as much as possible makes the code significantly more efficient and faster.

MegaSquirt's Volumetric Efficiency Table resides at $00A6 to $00E5, and we exceed the zero page at REQ_FUEL which lives at address $0100 (because we now require two bytes - $01 and $00 to address it). The last RAM variable, crankrpm lives at $01A5, leaving $0200-$01A6 = $5A = 90 bytes for the stack and FLASH burner code.

A few highly efficient instructions will only work with direct page operands. These are: BSET, BCLR, BRSET and BRCLR. The MOV instruction requires one of the operands to be in the direct page. Click here to see more details of the 68HC908 memory map.

The CPU08 has 16 different addressing modes. When an instruction doesn't need an operand, its addressing mode is referred to as inherent. An example is TAP (Transfer Accumulator to Condition Code Register). Since it is transferring data between the two specified registers, no memory addresses are needed.

A pound sign (#) before a number indicates an immediate operand. The default base is decimal. Hexadecimal numbers are represented by a dollar sign ($) preceding the number. Binary numbers are represented by a percent sign (%) preceding the number. Numbers that end with a T are explicitly in decimal format.

When the operand is immediate, the value is contained in the bytes immediately following the instruction. That is, the instruction is not followed by a memory location, it is followed by a number to be used for the instruction. In this case, the effective address of the instruction is specified by the # sign and implicitly points to the byte following the opcode. The immediate value is limited to either one or two bytes, depending on the size of the register involved in the instruction.

Where operands are not immediate in megasquirt.asm, they are generally assigned labels referring to specific memory locations in the megasquirt.h, boot_R12.asm, or GP32.equ files, and referred to by these labels in the code. For example, the contents of memory in the location labelled 'map' contains the Manifold Absolute Pressure ADC Raw Reading, and the contents of memory in the location labelled 'mat' holds the Manifold Air Temp ADC Raw Reading. Click here for more examples.

The directive EQU is used to associate a binary value with a label in the megasquirt.h file. The value may be either an 8-bit value or a 16-bit address value. This directive does not generate any object code. It is used by the assembler which keeps a cross reference list where it stores the binary equivalent of each label. When a label appears in the source program, the assembler looks in this cross reference table to find the binary equivalent. Each EQU directive generates an entry in this cross reference table.

The RMB (Reserve Memory Byte) directive is used to set aside space in RAM for program variables. The RMB directive does not generate object code but it normally generates an entry in the assembler’s internal cross reference table.

In the indexed addressing mode, the current value of the index register is added to a 0-, 1-, or 2-byte offset in the next 0, 1, or 2 memory locations after the instruction to form a pointer to the address of the operand in memory.

Relative addressing mode is used exclusively for conditional branch instructions. The byte after the opcode is a signed offset value between –128 and +127. If the condition of the branch is true, the offset is added to the program counter value to get the address where the CPU will fetch the next program instruction.

Generally, the number of bytes for the operand must match what the instruction expects. One exception to this is the 'page zero' mode, in which the upper byte (H) of a 16 bit register such as the index register is assumed to be zero, so that we can address the first 256 addresses by specifying just one byte (the lower byte {L}). This allows downwards compatibility with code for older processors and faster processing for those memory locations.

Interrupts and Resets

The CPU08 executes instructions sequentially, i.e. one after the other. With MegaSquirt® EFI Controller, it is often necessary to execute sets of instructions in response to requests from various peripheral devices (such as the laptop, the sensors, etc.). These requests can come any time during the execution of the main program.

Resets and interrupts are both used to force a change to the current flow of the code, and redirect it it to known state (reset) or another bit of code (interrupt). These are both types of CPU08 'exceptions'. Entry to the appropriate service routine is called exception processing.

A reset is used to force the MCU system to a known memory address. Peripheral systems and many control and status bits are also forced to a known state as a result of reset. These internal actions occur as the result of any MCU reset:

  1. All data direction registers cleared to 0 (input)
  2. Stack pointer forced to $00FF
  3. I bit in the CCR set to 1 to inhibit maskable interrupts
  4. External interrupt latch cleared
  5. STOP latch cleared
  6. WAIT latch cleared

As the computer system leaves reset, the program counter (PC) is loaded with the address of the first instruction. This is called fetching the reset vector. At this point, the CPU begins to fetch and execute instructions, beginning at the address that was stored in the reset vector. Any of these conditions can cause the MC68HC908 to reset:

  1. External, active-low input signal on the RESET pin,
  2. Internal power-on reset (POR),
  3. Internal computer operating properly (COP) watchdog timed out,
  4. An attempt to execute an instruction from an illegal address.

The RESET Pin is a particular pin on the 68HC908 - pin #6. An external switch or circuit can be connected to this pin to allow a manual system reset.

A Power-On Reset reset occurs when a positive transition is detected on VDD. The power-on reset is used strictly for initial power-up.

Interrupts provide a way to suspend normal program execution temporarily so that the CPU08 can be freed to attend to these external requests. The CPU08 can process up to 128 separate interrupt sources including a software interrupt (SWI). Interrupts cause the processor registers to be saved on the stack and the interrupt mask (I bit) to be set, to prevent additional interrupts until the present interrupt is finished. The appropriate interrupt vector then points to the starting address of the interrupt service routine

Upon completion of the interrupt service routine, an RTI instruction (normally the last instruction of an interrupt service routine) causes the register contents to be recovered from the stack. Since the program counter is loaded with the value that was previously saved on the stack, processing continues from where it left off before the interrupt. The registers are restored from the stack in the opposite order they were saved.

Interrupts can be inhibited by setting the I bit in the condition code register (CCR) or by clearing individual interrupt enable control bits for each interrupt source.

The SoftWare Interrupt (SWI) is an executable instruction that produces an interrupt. The action of the SWI instruction is similar to the hardware interrupts. An SWI is executed regardless of the state of the interrupt mask (I bit) in the condition code register.

Reset and interrupt operations share the common concept of vector fetching to force a new starting point for further CPU08 operations.


Here are some code snippets from the MegaSquirt version 2.00 code. We'll go through each of these in turn, looking at them line-by-line to see what is going on.


Example 1

; Set up the port data-direction registers
staddrb; Set as inputs (ADC will select which channel later)
lda#%00110000; Turn off injectors (inverted output)

On the first line, the ; character means everything that follows on that line is a comment, and will be ignored when the code is compiled. In this case, the comment is telling us that the purpose of the code that follows is to initialize the data-direction registers. Later in this segment, the ; character is used to make comments after an instruction, but still on the same line. The MegaSquirt code is very well commented, both for the general direction the code is following, and for what individual lines are doing.

On the second line, lda is the 'load the accumulator register from memory' instruction. The mnemonic is LoaD the Accumulator = LDA. It loads the contents of the memory location immediately following the instruction (#%00000000 = zero) into A (the accumulator).

#%00000000 is an immediate memory address mode binary value, i.e. the value of zero.

sta is the 'store the contents of the accumulator register in memory' instruction. It represents for SToreAccumulator. It loads the current contents of the accumulator into memory location 'ddrb'. The contents of A remain unchanged. 'ddrb' is a memory location. It is defined in GP32.equ as the Port B Data Direction Register.

So the first three lines do the following things:

Similarly, the next two lines


Example 2

; Set up the Real-time clock Timer (TIM2)
MOV#Timerstop,t2sc; Stop Timer so it can be set up
mov#$B8,T2MODL; set timer modulus register to 184 decimal
mov#T2SC0_No_PWM,T2SC0; make this normal port output (PWM MODE is #$5E)

Here again we start with a comment (;) telling us what the code is trying to do. Then we have 4 mov instructions. mov A,B moves a byte of data from a source (A) address to a destination (B) address. Obviously, mov stands for MOVe. Data is examined as it is moved, and condition codes are set. Source data is not changed. The accumulator is not affected.


Example 3

; Set up SCI port
lda#$12; This is 9600 baud w/ the osc frequency selected
bsetensci,scc1; Enable SCI
bsetRE,SCC2; Enable receiver
bsetSCRIE,SCC2; Enable Receive interrupt
ldaSCS1; Clear SCI transmitter Empty Bit
clr txcnt
clr txgoal

Here again we start with a comment (;) telling us what the code is trying to do.

Then lda and sta are used to load the accumulator with the immediate value $12 and copy it to scbr (SCI Baud Rate Register).

We then get three bset instructions. bset n,M sets bit n (n = 7, 6, 5, … 2, 1, 0) of the contents of memory location. M can be any RAM or I/O register address in the $0000 to $00FF area of memory because direct addressing mode (a.k.a. 'zero page mode') is used. bset reads the specified 8-bit location, modifies the specified bit, and then writes the modified 8-bit value back to the memory location. bset was named to suggest BitSET.

Next the lda instruction is used to load the accumulator with the contents of SCS1 (SCI Status Register 1).

Finally, two clr instructions are used. clr replaces the contents of the specified memory address (txcnt {SCI transmitter count (incremented)}, txgoal {SCI number of bytes to transmit}) with zeros. clr is meant to imply CLeaR.

Most of the above addresses are defined in GP32.equ.


Example 4

Next we will examine the Exhaust Gas Oxygen Sensor Measurement Section of the megasquirt.asm code to see how EGO sensor correction to the pulse width is implemented. This code employs conditional program flow control (branching) instructions. These instructions generally begin with a 'b', such as blo or bhi.

The logical structure we want to produce in assembly language can be written in pseudo-code as:

Steps are the following:
If egodelta = 0 then goto skipo2
If RPM < RPMOXLIMIT then goto skipo2
If TPSAEN in ENGINE or TPSDEN in ENGINE are set, then goto skipo2
If coolant < egotemp then goto skipo2
If sech = 0 and secl < 30 seconds then got skipo2 (skip first 30 seconds)
If tps > 3.5 volts then goto skipo2

The above lines skip the O2 correction (by invoking the subroutine skipO2) if:

The following lines set the actual correction factor.

If egocount > egocountcmp
egocount = 0
If ego > 26 (counts, or 0.5 Volts) then (rich)
tmp = egocurr - egodelta
if tmp < egolimit then goto VETABLELOOKUP
egocorr = tmp
Else (lean)
tmp = egocorr + egodelta
if tmp > egolimit then goto VETABLELOOKUP
egocorr = tmp
End if
End If

And the subroutine is:

subroutine skipo2:
.... egocorr = 100% ;i.e. no correction

How is this logic implemented in assembly for the M68HC908? This is a much more complicated example than the previous snippets, but the structure is similar, and you will see how the LDA/CMP/BLO series of instructions (and branching variants) are used repeatedly to perform conditional tests of the current state of the engine. A few new instructions will be introduced as we go through the code. Note that every possible path through the code ends in a branch to the VETABLELOOKUP routine.

ldaegodeltaLoaD the Accumulator with the value of egodelta
beqSKIPO2beq is a program flow control instruction that tests the state of the Z bit (the second bit) in the CCR and causes a branch if Z is set. The Z bit of the CCR is the 'Zero Flag'. The CPU sets the zero flag when an arithmetic operation, logical operation, or data manipulation produces a result of $00. The mnemonic is Branch if EQual. So if egodelta=0 then goto skipO2
ldarpmLoaD the Accumulator with the value of rpm
cmpRPMOXLIMIT; Low-end of RPM
cmp CoMPares the contents of the accumulator (rpm) to the contents of RPMLIMIT and sets condition codes in the CCR, which may then be used for arithmetic (signed or unsigned) and logical conditional branching. The contents of both the accumulator (rpm) and RPMLIMIT are unchanged. The first condition bit (C) is set = 1 if the unsigned value of the contents of memory is larger than the unsigned value of the accumulator, (i.e. RPMLIMIT is greater than rpm); cleared otherwise.
bloSKIPO2blo is another branching instruction for program flow control that performs a conditional branch to an address (skipO2 in this case). If the BLO instruction is executed immediately after execution of a CMP instruction, the branch will occur if the unsigned binary number in the A, X, or H:X register was less than the unsigned binary number in memory. In this case, if rpm is less than RPMLIMIT, then branch to skipO2. Note that there is an analogous bhi instruction.
brsetTPSAEN,ENGINE,SKIPO2brset tests bit TPSAEN of location ENGINE and branches to SKIPO2 if the bit is set. brset uses direct addressing mode is used to specify the address of the operand. So instruction means 'if the acceleration bit of the engine status is set, skip the O2 correction'.
brsetTPSDEN,ENGINE,SKIPO2brset tests bit TPSDEN of location ENGINE and branches to SKIPO2 if the bit is set. brset uses direct addressing mode to specify the address of the operand. So this instruction means 'if the deceleration bit of the engine status is set, skip the O2 correction'.
ldacoolantLoaD the Accumulator with the value in coolant
cmpegotempcmp CoMPares the contents of the accumulator (coolant) to the contents of egotemp and sets condition codes, which may then be used for arithmetic (signed or unsigned) and logical conditional branching. The contents of both the accumulator and coolant are unchanged. The first condition bit (C) is set = 1 if the unsigned value of the contents of memory is larger than the unsigned value of the accumulator, (i.e. coolant is greater than egotemp); cleared otherwise.
bloSKIPO2blo performs a conditional branch to an address (skipO2 in this case). Since the BLO instruction is executed immediately after execution of a CMP instruction, the branch will occur if the unsigned binary number in the Accumulator register (coolant) was less than the unsigned binary number in memory. In this case, if coolant is less than egotemp, then branch to skipO2. By now you might be beginning to see that the load/compare/branch structure (lda, cmp, blo (or bhi)) is both useful and frequently used!
ldatpsLoad the accumulator with the value in tps
cmp#$B2cmp CoMPares the contents of the accumulator (tps) to the immediate value of $B2 (=178) and sets condition codes, which will be used by the next instruction for logical conditional branching. The contents of both the accumulator and coolant are unchanged. The first condition bit (C) is set = 1 if the unsigned value of the contents of memory is larger than the unsigned value of the accumulator, (i.e. tps is greater than 178); cleared otherwise.
bhiSKIPO2bhi is another branching instruction for program flow control that performs a conditional branch to an address (skipO2 in this case). When the BHI instruction is executed immediately after execution of a CMP instruction, the branch will occur if the unsigned binary number in the accumulator register was greater than the unsigned binary number in memory. In this case, if tps is greater than $B2 (=178), then branch to skipO2.
ldasechLoaD the Accumulator with the value at sech.
bnechk_o2_lag; if high seconds set then we can check o2
ldaseclLoaD the Accumulator with the value at secl.
cmp#$1E; 30 seconds threshold
bloSKIPO2blo performs a conditional branch to skipO2 if secl is less than $1E (30 seconds), then branch to skipO2.
; Check if exceeded lag time - if so then we can modify egocorr
ldaegocountLoaD the Accumulator with the value at egocount
cmpegocountcmpCoMPares the contents of the accumulator (egocount) to the immediate value of egocountcmp and sets condition codes
bloVETABLELOOKUPBranch to VETABLELOOKUP if egocount is less than egocountcmp.
; Check if rich/lean
clregocountCLeaR the value of egocount
ldaconfig13; Check if Narrow-band (bit=0) or DIY-WB (bit=1)
bit#$02; Use BIT instead of brset because outside of zero-page
bneWBO2TYPE; Branch if the bit is set
ldaegoLoaD the Accumulator with the value at ego
cmpVOLTOXTARGETCoMPare ego with VOLTOXTARGET and set the CCR register.
bloO2_IS_LEANBranch to label O2_IS_LEAN if ego is less than VOLTOXTARGET.
braO2_IS_RICHBranch to O2_IS_RICH if we haven't already branched somewhere else.
ldaegoLoad the accumulator with the value at ego.
bloO2_IS_RICHif ego is less than VOLTOXTARGET branch to O2_IS_RICH
braO2_IS_LEANOtherwise, branch to O2_IS_LEAN
; rich o2 - lean out egocorr
lda#$64LoaD Accumulator with the value $64 (=100)
subegolimit; Generate the lower limit rail point
statmp2STores the contents of the Accumulator ($64) in the location tmp2
ldaegocorrLoaD Accumulator with the value at egocorr
subegodeltaSUBtracts the contents of egodelta from the value in the accumulator (egocorr) and places the result in the accumulator
statmp1STores the contents of the Accumulator (= egocorr - egodelta) in the location tmp1
cmptmp2CoMPare the value of the accumulator to tmp2 and store the result in the CCR
bloVETABLELOOKUP; railed at egolimit value
ldatmp1LoaD Accumulator with the value at tmp1
staegocorrSTore the contents of the Accumulator in egocorr
; lean o2 - richen egocorr
lda#$64LoaD Accumulator with the value $64 (=100).
addegolimit; Generate the upper limit rail point
ADDs the contents of egolimit to the contents of the accumulator ($64) and places the result in the accumulator
statmp2STores the contents of the Accumulator (= egocorr - egodelta) in the location tmp2
ldaegocorrLoaD Accumulator with the value at egocorr.
addegodeltaADDs the contents of egodelta to the contents of the accumulator (egocorr) and places the result in the accumulator
statmp1STores the contents of the Accumulator (= egocorr + egodelta) in tmp1
cmptmp2CoMPares the accumulator (= egocorr + egodelta) to tmp2 (= egocorr - egodelta) and places the result in the CCR.
bhiVETABLELOOKUP; railed at egolimit value
ldatmp1LoaD Accumulator with the value at tmp1
staegocorrSTores the contents of the Accumulator (= tmp1) in egocorr
; reset egocorr to 100%
SKIPO2:Routine to skip the O2 Correction code
lda#$64LoaD Acculmulator with the value $64 (=100 decimal).
staegocorrSTores the contents of the Accumulator (= $64) in egocorr

So our general method for understanding assembly files is to disect them line by line and see what's happening. Having handy access to the instuctions, variables, and reference manual helps. It also becomes much easier with a bit of practice, as patterns begin to emerge and less 'looking-up' is needed.


Ports are used by the processor to perform input and output. This can be anything from taking in the ignition signal, to going into bootloader mode, to sending and receiving data over the serial port.

The 68HC908 used in MegaSquirt® has 5 ports:

Some of these have particular special functions built right in to the processor. For example:

Note that the ports have several pins, generally (but not always) 8, numbered 0 to 7. Also note that the above doesn't tell us if the pins are inputs or outputs. This is because we configure them to be inputs or outputs by using the data direction registers. A typical data direction register command looks like this:

; Set up the port data-direction registers
        lda     #%00000000
        sta     ddrb                   ; Set as inputs (ADC will select which channel later)

This sets all the PTB pins (ddrb) to be inputs (0). If we had set them to #%00001111, then PTA1, PTA2, PTA3, and PTA4 would be outputs (1), and the rest would be inputs.

The 68HC908 has another neat function for some pins configured as inputs - it has a built in 'pull-up' resistor that forces the input to be high or low, not floating.

For example, the pull-up register PTDPUE7–PTDPUE0 is the Port D Input Pullup Enable Bits. These writable bits are software programmable to enable pullup devices on an input port bit.

1 = Corresponding port D pin configured to have internal pullup
0 = Corresponding port D pin has internal pullup disconnected

Note that pull-up resistors are only applicable to inputs, they will NOT power an output pin.

In MegaSquirt® EFI Controller, the CPU pins are configured as follows:

Pin     Name    I/O     MegaSquirt® Use
1       VDDA            VSYN
2       VSSA            GND
3       CGMXFC          CLOCK
4       OSC1            CLOCK
5       OSC2            CLOCK
6       RST             Reset
7       PTC0            Squirt-LED
8       PTC1            Accel-LED
9       PTC2            Warmup-LED
10      PTC3            N/C
11      PTC4            N/C
12      PTE0    out     TxD (Serial transmit)
13      PTE1    in      RxD (Serial receive)
14      IRQ1    in      IGN
15      PTD0            
16      PTD1            N/C
17      PTD2            N/C
18      PTD3            N/C
19      VSS             
20      VDD             
21      PTD4    out     PWM0
22      PTD5    out     PWM1
Analog/Digital Converters                       
23      PTB0    in      MAP
24      PTB1    in      IAT
25      PTB2    in      CLT
26      PTB3    in      TPS
27      PTB4    in      BATT
28      PTB5    in      EGO
29      PTB6            N/C
30      PTB7            N/C
31      VSSAD   in      GND
32      VDDAD           VREF

KeyBoard Interrupts             
33      PTA0    out     Fuel Pump
34      PTA1    out     FIdle
35      PTA2            N/C
36      PTA3            N/C
37      PTA4            N/C
38      PTA5            N/C
39      PTA6            N/C
40      PTA7            N/C

Search the source code for more details on how the data direction registers and pullup enable registers are set in the MegaSquirt® code.

S19 record files

There are a number of assembly language files used to program the MegaSquirt. These inlude:

The computer expects the program to be a series of 8-bit values in memory. But the MegaSquirt code looks as if it were written to be read by fairly smart people, not digital computers. To run the code, the computer needs to load into its memory a file called an object code file. For the MegaSquirt microcontroller, the object code file is the S-record file called megasquirt.s19.

The P&E assembler software compiles the assembly language files in an object code file called megasquirt.s19. This S-record file is an ASCII text file that can be viewed by a text editor or word processor. This is not the program in binary form, but it is a text format that tells the bootloader where and what to write to the MegaSquirt's memory. You can read this file if you understand Hex and op-codes. You should not edit this files because the structure and content of the files are critical to the proper operation of the program.

Each line of an S-record file is a record. Each record begins with a capital letter S followed by a code number from 0 to 9. The only code numbers that are important to us are S0, S1, and S9 because other S-number codes apply only to larger systems. S0 is an optional header record that may contain the name of the file that humans can read. S1 records are the main data records. An S9 record is used to mark the end of the S-record file. For 8-bit microcontrollers, the information in the S9 record is not important, but an S9 record is required at the end of S-record files.

All of the numbers in an S-record file are hexadecimal. The type field is S0, S1, or S9 for the S-record files we will use. The length field is the number of pairs of hexadecimal digits in the record excluding the type and length fields. The address field is the 16-bit address where the first data byte will be stored in memory. Each pair of hexadecimal digits in the machine code data field represents an 8-bit data value to be stored in successive locations in memory. The checksum field is an 8-bit value that represents the ones complement of the sum of all bytes in the S-record except the type and checksum fields. This checksum is used during loading of the S-record file to verify that the data is complete and correct for each record.

Some MegaSquirt® Variables

ACMULT = Acceleration cold multiplication factor (percent/100)
adsel = ADC Selector Variable
aircor = Air density correction is computed from MAT.
asecount = Counter value for after-start enrichment counter - every ignition
AWC = After-start number of cycles
AWEV = After-start Warmup Percent enrichment add-on value
baro = The barometric pressure as measured by MegaSquirt.
barocor = Barometer Lookup Correction - percent, based on the initial MAP sensor reading.
batt = Battery Voltage ADC Raw Reading - counts
BATTFAC = Battery Gamma Factor
clt = Coolant Temperature ADC Raw Reading - counts (0 - 255)
coolant = Coolant temperature in Degrees F plus 40 (allows -40 degress to fit in integer)
CWH = Crank Enrichment at 170 F
CWU = Crank Enrichment at -40 F
ddra = Port A Data Direction Register
ego = Exhaust Gas Oxygen ADC Raw Reading - counts
egocorr = This is the correction factor computed from O2 sensor readings.
egocount = Counter value for EGO step - incremented every ignition pulse
egotemp = Coolant Temperature where EGO is active
egocountcmp = Counter value where EGO step is to occur
egodelta = EGO Percent step size for rich/lean
egolimit = Upper/Lower EGO rail limit (egocorr is inside 100 +/- Limit)
engine = Variable bit-field to hold engine current status
FASTIDLE = Fast Idle Temperature
gammae = Total Gamma Enrichments - percent
InjOpen = Injector Open Time
InjOCFuel = PW-correlated amount of fuel injected during injector open
INJPWM = Injector PWM duty cycle at current limit
INJPWMT = Injector PWM millisec time at which to activate.
kpa = MAP value in units of KPa
KPARANGEVE = VE Table MAP Pressure Bins for 2_D interpolation
last_tps = TPS reading updated every 0.1 seconds
lmap = Manifold Absolute Pressure ADC last Reading
lmat = Manifold Air Temp ADC last Reading
lclt = Coolant Temperature ADC last Reading
ltps = Throttle Position Sensor ADC last Reading
lbatt = Battery Voltage ADC last Reading
lego = Last EGO ADC reading
map = Manifold Absolute Pressure ADC Raw Reading - kPa (0 - 255)
mat = Manifold Air Temp ADC Raw Reading - counts (0 - 255)
mms = 0.0001 second update variable
ms = 0.001 second increment
porta = Port A Data Register
portb = Port B Data Register
portc = Port C Data Register
PRIMEP = Priming pulses (0.1 millisec units)
pulseigncount = Ignition pulse counter
pw = The injector pulse width being used by MS to squirt fuel into your motor.
pwcalc = Computed pulse width - move into variable PW at pulse time
pw = Injector squirt time in 1/10 milliseconds (0 to 25.5 millisec) - applied
pw2= The other PW comparison (injector #2)
pwrun1 = Pulse width timing variable 1 - from 0 to 25.5ms
pwrun2 = Pulse width timing variable 2 - from 0 to 25.5ms
REQ_FUEL = Fuel Constant
RPMOXLIMIT = Minimum RPM where O2 Closed Loop is Active
rpm = Computed engine RPM - rpm/100
rpmch = Counter for high part of RPM
rpmcl = Counter for low part of RPM
rpmpl = Low part of RPM Period
rpmk = Constant for RPM = 12,000/ncyl - downloaded constant
rpmph = High part of RPM Period
rpmphl = last rpmph value (for odd-fire)
rpmpll = last rpmpl value (for odd-fire)
RPMRANGEVE = VE table RPM Bins for 2-D interpolation
rxoffset = offset placeholder when receiving VE/constants vis. SCI
secl = Time in seconds since MegaSquirt last booted. Low seconds - from 0 to 255, then rollover.
sech = High seconds - rollover at 65536 secs (1110.933 minutes, 18.51 hours)
squirt = Event variable bit field for Injector Firing.
tenth = 1/10th second
tmp1,...,tmp19 = Temporary storage.
tps = Throttle Position Sensor ADC Raw Reading - counts, represents 0 - 5 volts
tpsaccel = The acceleration enrichment.
tpsaclk = TPS enrichment timer clock in 0.1 second resolution
TPSAQ = TPS acceleration amount (fn TPSDOT) in 0.1 ms units
tpsacold = Cold acceleration amount (at -40 degrees) in 0.1 ms units
TPSASYNC = ***** TPS Acceleration clock value
TPSDQ = Deacceleration fuel cut
tpsfuelcut = TPS Fuel Cut (percent).
tpsthresh = Accel TPS DOT threshold
txcnt = SCI transmitter count (incremented)
txgoal = SCI number of bytes to transmit
txmode = Transmit mode flag
VE = 64 bytes for VE Table
vecurr = The current computed VE value determined by look up in the VETABLE using RPM and MAP.
VOLTOXTARGET = O2 sensor flip target value
warmcor = The warmup correction factor applied due to startup and coolant temperature status.
WWU = Warmup bins(fn temp)

Instruction set:

ADC = Add with Carry
ADD = Add without Carry
AIS = Add Immediate Value (Signed) to Stack Pointer
AIX = Add Immediate Value (Signed) to Index Register
AND = Logical AND
ASL = Arithmetic Shift Left
ASR = Arithmetic Shift Right
BCC = Branch if Carry Bit Clear
BCLR n = Clear Bit n in Memory
BCS = Branch if Carry Bit Set
BEQ = Branch if Equal
BGE = Branch if Greater Than or Equal To
BGT = Branch if Greater Than
BHCC = Branch if Half Carry Bit Clear
BHCS = Branch if Half Carry Bit Set
BHI = Branch if Higher
BHS = Branch if Higher or Same
BIH = Branch if IRQ Pin High
BIL = Branch if IRQ Pin Low
BIT = Bit Test
BLE = Branch if Less Than or Equal To
BLO = Branch if Lower
BLS = Branch if Lower or Same
BLT = Branch if Less Than
BMC = Branch if Interrupt Mask Clear
BMI = Branch if Minus
BMS = Branch if Interrupt Mask Set
BNE = Branch if Not Equal
BPL = Branch if Plus
BRA = Branch Always
BRA = Branch Always
BRCLR n = Branch if Bit n in Memory Clear
BRN = Branch Never
BRSET n = Branch if Bit n in Memory Set
BSET n = Set Bit n in Memory
BSR = Branch to Subroutine
CBEQ = Compare and Branch if Equal
CLC = Clear Carry Bit
CLI = Clear Interrupt Mask Bit
CLR = Clear
CMP = Compare Accumulator with Memory
COM = Complement (One’s Complement)
CPHX = Compare Index Register with Memory
CPX = Compare X (Index Register Low) with Memory
DAA = Decimal Adjust Accumulator
DAA = Decimal Adjust Accumulator (Continued)
DBNZ = Decrement and Branch if Not Zero
DEC = Decrement
DIV = Divide
EOR = Exclusive-OR Memory with Accumulator
INC = Increment
JMP = Jump
JSR = Jump to Subroutine
LDA = Load Accumulator from Memory
LDHX = Load Index Register from Memory
LDX = Load X (Index Register Low) from Memory
LSL = Logical Shift Left
LSR = Logical Shift Right
MOV = Move
MUL = Unsigned Multiply
NEG = Negate (Two’s Complement)
NOP = No Operation
NSA = Nibble Swap Accumulator
ORA = Inclusive-OR Accumulator and Memory
PSHA = Push Accumulator onto Stack
PSHH = Push H (Index Register High) onto Stack
PSHX = Push X (Index Register Low) onto Stack
PULA = Pull Accumulator from Stack
PULH = Pull H (Index Register High) from Stack
PULX = Pull X (Index Register Low) from Stack
ROL = Rotate Left through Carry
ROR = Rotate Right through Carry
RSP = Reset Stack Pointer
RTI = Return from Interrupt
RTS = Return from Subroutine
SBC = Subtract with Carry
SEC = Set Carry Bit
SEI = Set Interrupt Mask Bit
STA = Store Accumulator in Memory
STHX = Store Index Register
STOP = Enable IRQ Pin, Stop Oscillator
STX = Store X (Index Register Low) in Memory
SUB = Subtract
SWI = Software Interrupt
TAP = Transfer Accumulator to Processor Status Byte
TAX = Transfer Accumulator to X (Index Register Low)
TPA = Transfer Processor Status Byte to Accumulator
TST = Test for Negative or Zero
TSX = Transfer Stack Pointer to Index Register
TXA = Transfer X (Index Register Low) to Accumulator
TXS = Transfer Index Register to Stack Pointer
WAIT = Enable Interrupts; Stop Processor

MegaSquirt 68HC908GP32 Memory Map

$0000 - $003F = I/O Registers: 64 Bytes
$0040 - $023F = RAM 512
$0240 - $7FFF = Unimplemented 32,192 bytes
$8000 - $FDFF = FLASH Memory: 32,256 bytes
$FE00 = SIM Break Status Register (SBSR)
$FE01 = SIM Reset Status Register (SRSR)
$FE02 = Reserved (SUBAR)
$FE03 = SIM Break Flag Control Register (SBFCR)
$FE04 = Interrupt Status Register 1 (INT1)
$FE05 = Interrupt Status Register 2 (INT2)
$FE06 = Interrupt Status Register 3 (INT3)
$FE07 = Reserved (FLTCR)
$FE08 = FLASH Control Register
$FE09 = Break Address Register High (BRKH)
$FE0A = Break Address Register Low (BRKL)
$FE0B = Break Status And Control Register (BRKSCR)
$FE0C = LVI Status Register (LVISR)
$FE0D - $FE0F = Unimplemented: 3 bytes
$FE10 - $FE1F = Unimplemented: 16 bytes Note: Reserved for compatibility with monitor code for A-Family parts
$FE20 - $FF52 = Monitor ROM: 307 bytes
$FF53 - $FF7D = Unimplemented: 43 bytes
$FF7E = Flash Block Protect Register (FLBPR)
$FF7F - $FFDB = Unimplemented: 93 bytes
$FFDC - $FFFF = Flash Vectors: 36 bytes

MegaSquirt® and MicroSquirt® controllers are experimental devices intended for educational purposes.
MegaSquirt® and MicroSquirt® controllers are not for sale or use on pollution controlled vehicles. Check the laws that apply in your locality to determine if using a MegaSquirt® or MicroSquirt® controller is legal for your application.
©2004, 2005 Bruce Bowling and Al Grippo. All rights reserved. MegaSquirt® and MicroSquirt® are registered trademarks. This document is solely for the support of MegaSquirt® boards from Bowling and Grippo.