This is a basic introduction to assembly language programming for the M68HC908GP32 microcontroller used in the MegaSquirt-I® EFI system (not the MegaSquirt-II™ daughter card, for MS-II code documentation go here). 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.
After reading this tutorial, and some of the above references above, you ought to be able to follow coding discussions on the MegaSquirt® Forums.
The MegaSquirt® EFI controller uses the Motorola 68HC908GP32 processor. This processor has both RAM memory and Flash memory. RAM memory is 'random access memory' that is lost when the power is shut down. Flash memory is retained even if the power is disconnected (this is called 'non-volatile'). MegaSquirt® loads the code into flash, so that it is there when the processor boots up, but uses the RAM memory for many of the tables, etc. for faster and easier access. It does this by copying sections of the Flash memory into RAM on start-up.
The embedded code contains both the program (the instructions to run) and some default parameters. When you tune with MegaTune, you are changing values in RAM. Then when you click the 'Send to ECU' buttons, you are telling MegaSquirt® to 'burn' portions of the RAM memory to the Flash memory, making them non-volatile.
The code is written in assembly language, a fairly low level (i.e. very close to what the machine understands) of programming that can be hard for humans to understand. In this document, we will cover some aspects of how the assembly language programs are written, edited, compiled, and loaded.
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.
The latest 'official' version of the embedded code is version 3.000, which was version 2.98 re-released as version 3.000. The changes to version 2.98/3.000 over 2.00 are:
;-----------------------
; Version 2.98 (aka. V3.000)
;-----------------------
; Modified boot_r12 file to raise the LVI trip point.
; Fill unused RAM locations with $32, an illegal opcode to force a reset. Done
; to prevent flash erasure if code ever gets into a runaway state - for protection only.
; Put in interlocks to prevent VE/Constant section flash erasure unless specifically
; invoked by a flash burn command.
; Put in Odd-fire RPM averaging (Willette) for odd-fire engines.
; Added new calculation structure to code - same calculation as HI-RES code, but results
; are in 0.1 millisecond units. Calculation overflow fixed.
; Flyback damping code installed - jumper port X0 for INJ1 and jumper port X1 for INJ2.
; Fixed display of WARMCOR variable (no flickering).
; Inhibit PWM mode while cranking.
; Made "ADC" into "ADD in ADC interrupt section (Tom D) to properly perform the sum
; without a carry.
; Perform 12 ms time compare for re-enabling of tach IRQ interrupt (fix for random tach problem) -
; original trial time of 20 ms too long (Magnus Bjelk fix - also for high/low byte check).
; Moved after-start enrichment to increment after "N" tach pulses have occurred,
; with N being the number of cylinders. This will lengthen the afterstart enrichment
; time by an amount scaled by the number of cylinders (Tom).
; Fix display of barometer correction (no screen flicker if 100% barometer is selected).
; Fixed IRQ counter to work properly for NCYL above 8 cylinders.
The version 2.98 (aka. V3.000) code can be downloaded from the following links:
megasquirt.asm,
megasquirt.h,
boot_r12.asm,
megasquirt.s19,
megasquirt-I.ini (for MegaTune).
In general, right click on the links as 'Save as', this avoids adding formatting, etc., that could corrupt the files.
You really only need the last two files (megasquirt.s19 and megasquirt-I.ini) if you are not modifying the code.
If your microprocessor already has a bootloader (a special bit of code that allows uploading) on it, you can update code using the serial connection. Get Eric Fahlgren's program download.exe. Unzip the file to a convenient directory. Instructions for using download.exe are in DOWNLOAD.TXT in the zip file. (You can also use hyperterminal or EasyTherm, instruction are here.)
If you are programming a blank chip, such as one you bough direct from Digi-Key, etc., it will not have the bootloader on it, and you will need a programmer to load code for the first time. There is more on that here.
Note that V2.98 (aka. V3.000) is required if you are using the FlyBack Board or the PWM flyback circuit on a V3 main board.
The newer versions of the MegaSquirt® code are fully backwards compatible with the firmware and otherMegaSquirt® specific software programs. All releases of the embedded software and tuning software are compatible in both directions. V1 (version 1.01) embedded works with the latest MegaTune and the old MegaTune releases work with V2 and V3 embedded code (but without access to newer features like priming pulse, flyback board, and Wide Band O2 support).
The hardware for both v1.01, v2.2, and V3 main boards looks the same to the CPU, so either code will work on either board.
Unless you have a specific reason to do otherwise, use V3.000 embedded code (right click and 'Save as') and the latest version of MegaTune, as they are the most robust.
There are a number of other 'unofficial' versions of the code are out there for your MegaSquirt. See Colin Gebhart's guide for compatibility and files.
Here are main versions of code:
There are a number of other specialized versions of embedded code for MegaSquirt. See Colin Gebhart's guide for compatibility and files.
There are a number of files that are used to program your MegaSquirt®.
Unless you want to edit and recompile the source code, you can upgrade to any version of the code by uploading the corresponding .s19 file. If you want to edit the code, you need a number of files:
The .asm file is the human readable source code. Open it up with Notepad and look it over. You may not understand it but it is very well commented.
The .inc files are included by the compiler directed by command in the .asm file. In this case it is used to load tables. This can be read with notepad.
The .h file is called a header file. It contains 'defines' and 'equates' (constants). Open it up with Notepad.
The .s19 is called the 'source record' or S record for short. This is the result of running the .asm through the compiler. This is not the program in binary form but it is a text format that tells the loader where and what to write to the device's memory. You can read this file if you understand Hex and op-codes.
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.
All of the numbers in an S-record file are hexadecimal. Hexadecimal is a base-16 system, and number are frequently written with a $ as a prefix (or sometimes by a small h sufix). Hexadecimal (often called simply 'hex') is a particularly convenient way of representing the binary numbers used in a computer.
(If you want to convince yourself that this makes things easier, try computing 312×45 in Roman numerals using paper and pencil. It would be CCCXII×XLV, the rest is left as an exercise for the reader!)
To a computer, using decimal is like us using Roman numerals.
Decimal | Binary | Hexadecimal |
0 | 0000 0000 | 0 |
1 | 0000 0001 | 1 |
2 | 0000 0010 | 2 |
3 | 0000 0011 | 3 |
4 | 0000 0100 | 4 |
5 | 0000 0101 | 5 |
6 | 0000 0110 | 6 |
7 | 0000 0111 | 7 |
8 | 0000 1000 | 8 |
9 | 0000 1001 | 9 |
10 | 0000 1010 | a |
11 | 0000 1011 | b |
12 | 0000 1100 | c |
13 | 0000 1101 | d |
14 | 0000 1110 | e |
15 | 0000 1111 | f |
16 | 0001 0000 | 10 |
17 | 0001 0001 | 11 |
18 | 0001 0010 | 12 |
etc. | etc. | etc. |
For example:
$ffff = 15×163 + 15×162 + 15×161 + 15×160
= 61440 + 3840 + 240 + 15
= 65535 (= 1111 1111 1111 1111 in binary)
and
$B9 = B×161 + 9×160
= 11×16 + 9×1
= 185 (= 1011 1001 in binary)
In MegaSquirt® assembly code:
So $B9 = 185T = %10111001 in assembly language. We use which ever is most descriptive for the function we are performing. This will become clearer later.
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.
There are a number of assembly language files used to program the MegaSquirt. These include:
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.
And some more on programming: http://savannah.nongnu.org/download/pgubook/
Then you might want to look at the manual at http://www.freescale.comfiles/microcontrollers/doc/ref_manual/CPU08RM.pdf. (Note that this link may change. Search the Motorola site if you get a dead link.) This document covers the architecture, resets and interrupts, addressing modes, and instruction sets for theMegaSquirt processor.
For further reading, an on-line document covering many of the basic microcontroller concepts in great detail is at http://www.freescale.combrdata/PDFDB/docs/M68HC05TB.pdf.
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.
It ranks up there with Horowitz/Hill "Art of Electronics" and "Numerical Recipes". This one is destined to be a classic. It has info on programming Mot HC05/HC08/HC11/12 processors, and all of the hardware that people like us are interested (stepper motor driver circuits, sensor interfaces, etc). 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.
Get this book and you will be able to make MegaSquirt circuit and software changes yourself. It is expensive at $107.00, but worth every dollar.
To create modified code for MegaSquirt® EFI Controller, you need to:
This ZIP file contains:
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 isn't MegaSquirt programmed in C++, or even BASIC? It could be, but only by making the code much larger 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:
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.
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 XXVV: | Write from VETABLE to FLASH
; Erase VE and Constants in FLASH - 128 byte erase
ERASE_N_BURN: DELAYE: ; Now Burn VE (you can only freakin burn 64 bytes at a time)
VE_BURNER
LOOP_TO_BURN_VE: SD1: DELAYVE: |
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.
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:
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:
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.
Examples
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 | ||||
lda | #%00000000 | |||
sta | ddrb | ; Set as inputs (ADC will select which channel later) | ||
lda | #%00110000 | ; Turn off injectors (inverted output) | ||
sta | portd |
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:
______________________________
Example 2
______________________________
; Set up the Real-time clock Timer (TIM2) | ||||
MOV | #Timerstop,t2sc | ; Stop Timer so it can be set up | ||
mov | #$00,T2MODH | |||
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 | ||
sta | scbr | |||
bset | ensci,scc1 | ; Enable SCI | ||
bset | RE,SCC2 | ; Enable receiver | ||
bset | SCRIE,SCC2 | ; Enable Receive interrupt | ||
lda | SCS1 | ; 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
Then
egocount = 0
If ego > 26 (counts, or 0.5 Volts) then (rich)
Then
tmp = egocurr - egodelta
if tmp < egolimit then goto VETABLELOOKUP
egocorr = tmp
goto VETABLELOOKUP
Else (lean)
tmp = egocorr + egodelta
if tmp > egolimit then goto VETABLELOOKUP
egocorr = tmp
goto VETABLELOOKUP
End if
End If
And the subroutine is:
subroutine skipo2:
.... egocorr = 100% ;i.e. no correction
.... goto VETABLELOOKUP
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.
MAE: | ||||
lda | egodelta | LoaD the Accumulator with the value of egodelta | ||
beq | SKIPO2 | beq 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 | ||
lda | rpm | LoaD the Accumulator with the value of rpm | ||
cmp | RPMOXLIMIT | ; 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. | ||
blo | SKIPO2 | blo 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. | ||
brset | TPSAEN,ENGINE,SKIPO2 | brset 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'. | ||
brset | TPSDEN,ENGINE,SKIPO2 | brset 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'. | ||
lda | coolant | LoaD the Accumulator with the value in coolant | ||
cmp | egotemp | cmp 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. | ||
blo | SKIPO2 | blo 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! | ||
lda | tps | Load the accumulator with the value in tps | ||
cmp | #$B2 | cmp 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. | ||
bhi | SKIPO2 | bhi 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. | ||
lda | sech | LoaD the Accumulator with the value at sech. | ||
bne | chk_o2_lag | ; if high seconds set then we can check o2 | ||
lda | secl | LoaD the Accumulator with the value at secl. | ||
cmp | #$1E | ; 30 seconds threshold | ||
blo | SKIPO2 | blo 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 | ||||
CHK_O2_LAG: | ||||
lda | egocount | LoaD the Accumulator with the value at egocount | ||
cmp | egocountcmp | CoMPares the contents of the accumulator (egocount) to the immediate value of egocountcmp and sets condition codes | ||
blo | VETABLELOOKUP | Branch to VETABLELOOKUP if egocount is less than egocountcmp. | ||
; Check if rich/lean | ||||
clr | egocount | CLeaR the value of egocount | ||
lda | config13 | ; Check if Narrow-band (bit=0) or DIY-WB (bit=1) | ||
bit | #$02 | ; Use BIT instead of brset because outside of zero-page | ||
bne | WBO2TYPE | ; Branch if the bit is set | ||
NBO2TYPE: | ||||
lda | ego | LoaD the Accumulator with the value at ego | ||
cmp | VOLTOXTARGET | CoMPare ego with VOLTOXTARGET and set the CCR register. | ||
blo | O2_IS_LEAN | Branch to label O2_IS_LEAN if ego is less than VOLTOXTARGET. | ||
bra | O2_IS_RICH | Branch to O2_IS_RICH if we haven't already branched somewhere else. | ||
WBO2TYPE: | ||||
lda | ego | Load the accumulator with the value at ego. | ||
cmp | VOLTOXTARGET | CoMPare VOLTOXTARGET to ego. | ||
blo | O2_IS_RICH | if ego is less than VOLTOXTARGET branch to O2_IS_RICH | ||
bra | O2_IS_LEAN | Otherwise, branch to O2_IS_LEAN | ||
; rich o2 - lean out egocorr | ||||
O2_IS_RICH: | ||||
lda | #$64 | LoaD Accumulator with the value $64 (=100) | ||
sub | egolimit | ; Generate the lower limit rail point | ||
sta | tmp2 | STores the contents of the Accumulator ($64) in the location tmp2 | ||
lda | egocorr | LoaD Accumulator with the value at egocorr | ||
sub | egodelta | SUBtracts the contents of egodelta from the value in the accumulator (egocorr) and places the result in the accumulator | ||
sta | tmp1 | STores the contents of the Accumulator (= egocorr - egodelta) in the location tmp1 | ||
cmp | tmp2 | CoMPare the value of the accumulator to tmp2 and store the result in the CCR | ||
blo | VETABLELOOKUP | ; railed at egolimit value | ||
lda | tmp1 | LoaD Accumulator with the value at tmp1 | ||
sta | egocorr | STore the contents of the Accumulator in egocorr | ||
bra | VETABLELOOKUP | BRAnch to VETABLELOOKUP | ||
; lean o2 - richen egocorr | ||||
O2_IS_LEAN: | ||||
lda | #$64 | LoaD Accumulator with the value $64 (=100). | ||
add | egolimit | ; 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 | ||
sta | tmp2 | STores the contents of the Accumulator (= egocorr - egodelta) in the location tmp2 | ||
lda | egocorr | LoaD Accumulator with the value at egocorr. | ||
add | egodelta | ADDs the contents of egodelta to the contents of the accumulator (egocorr) and places the result in the accumulator | ||
sta | tmp1 | STores the contents of the Accumulator (= egocorr + egodelta) in tmp1 | ||
cmp | tmp2 | CoMPares the accumulator (= egocorr + egodelta) to tmp2 (= egocorr - egodelta) and places the result in the CCR. | ||
bhi | VETABLELOOKUP | ; railed at egolimit value | ||
lda | tmp1 | LoaD Accumulator with the value at tmp1 | ||
sta | egocorr | STores the contents of the Accumulator (= tmp1) in egocorr | ||
bra | VETABLELOOKUP | Otherwise, BRAnch to VETABLELOOKUP | ||
; reset egocorr to 100% | ||||
SKIPO2: | Routine to skip the O2 Correction code | |||
lda | #$64 | LoaD Acculmulator with the value $64 (=100 decimal). | ||
sta | egocorr | STores the contents of the Accumulator (= $64) in egocorr | ||
bra | VETABLELOOKUP | Otherwise, BRAnch to VETABLELOOKUP |
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.
The serial protocol(s) MegaSquirt and MegaTune use to communicate with each other are listed below:
*************************************************************************** ** ** SCI Communications ** ** Communications is established when the PC communications program sends ** a command character - the particular character sets the mode: ** ** "A" = send all of the realtime variables via txport. ** "V" = send the VE table and constants via txport (128 bytes) ** "W"+{offset}+{newbyte} = receive new VE or constant byte value and ** store in offset location ** "B" = jump to flash burner routine and burn VE/constant values in RAM into flash ** "C" = Test communications - echo back SECL ** "Q" = Send over Embedded Code Revision Number (divide number by 10 - i.e. $21T is rev 2.1) ** *************************************************************************** |
You also need to set the serial port parameters to 9600, No Parity, 8 bit, 1 stopbit (i.e. 9600,N,8,1). This is done automatically by MegaTune.
Look at the MegaSquirt assembly code for complete details on the mode commands.
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.
The cranking speed can be changed in the code to suit your application quite easily. Find a section of the code that has the following lines:
CHK_FOR_WENRCH:
cmp #$03 ; Check if we are cranking
bhi WARM_UP_ENRICH
This is where the cranking rpm check occurs. If rpm exceeds 300 (indicated by #$03), then it branches to the warmup enrichment calculations, and assumes the engine is running. Change the $03 number to suit your cranking speed, and you've got your own custom code version with a higher cranking/running transition point.
Find the section of the code titled "MAE"
remark out the following lines by including the semicolon before it:
;lda tps
;cmp #$B2
;bhi SKIPO2
directly under that, add the following 3 lines:
lda kpa
cmp #90T
bhi SkipO2
Recompile the code, load it onto your processor and go...
In megasquirt.h change the line
Timergo equ %01010010 ;TSC to
Timergo equ %01010001 ;TSC
This changes the prescaler for the rtc from 4 to 2. Then in the 0.1ms interrupt code change
cmp #$64 ; If RPMPH is 100 ..... to
cmp #200T ; If RPMPH is 200 (or RPMPeriod = 2.5 sec)
and
cmp #$0A to
cmp #20T ; 20 1/20ths per ms
Recompile and send toMegaSquirt and you are ready to run. It's not very elegant but it works. Remember, max pulse width becomes 12.70ms and all displayed values in milliseconds in MegaTune will show double the real value. And all settings measured in milliseconds, including Req_Fuel, accel enrich, etc., will have to be doubled as well.
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
T1SCX_NO_PWM = No PWM
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)
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