Early versions of Bosch Motronic used the RCA CDP1802 "COSMAC" CPU. (Subsequent generations of the Motronic product line use other CPUs/microcontrollers such as i8051.) I have the following Motronic units from BMWs in my posession:

  • 007 from an '82 528e (ROM download)
  • 008 from an '83 633CSi (ROM download)
  • 021 from an '84 325e
  • 027 from an '85 528e (ROM download)
  • This article shows a photo of an 001 Motronic from a '79 732i (presumably the first one). Interestingly, it has fewer ICs than the 008. Perhaps this one only had 3KB of ROM? It also doesn't seem to have a fuel quality switch.

    This site describes Motronic ECUs 012, 022, 023, and 026 which were used on Volvos. These run similar code to the BMW ones.

    '83-'85 Porsche 944 reportedly used an 015 Motronic which may also be 1802-based.

    ROMs vs. EPROMs

    Some of the units use CDP1833 1KB mask ROMs. These chips use an unusual multiplexed address bus, they are soldered (and glued!) to the PCB, and they are not reprogrammable. Overall, they are a nuisance. Below is a photo of the inside of an 008 ECU, with several ICs removed.

    Other units use a 4KB 2732 EPROM which is much nicer. We can also see an unpopulated footprint where another chip could go. Is it possible there was a Motronic unit with 8KB of ROM? I think the answer is yes. Twenty years ago I downloaded an 8KB file from somewhere which has 1802 code in it. I just don't know what car it is from! Below is a photo of the inside of an 007 ECU, with an empty socket where the 2732 was.

    Hardware overview

    The BMW units have these features:

  • 1802 CPU at 4.19MHz, with 4KB ROM, and 128 bytes of RAM
  • Airflow, air temp, coolant temp, and oxygen sensor inputs
  • Flywheel speed and reference (position) VR sensors
  • coil driver, 2x injector driver, and fuel pump relay outputs
  • optional barometric pressure switch input
  • optional purge valve output
  • The 007,021,027 ECUs are all intended to run the M20B27 inline six cylinder engine. This engine was designed as a high-torque, high-efficiency option and has a relatively low 5000RPM rev limit. Its flywheel has 137 teeth.

    The 008 ECU is intended for the higher-performance M30B32 engine which has a 6480RPM rev limit. Its flywheel has 116 teeth, which results in some different constants and scale factors in the code. For both engines, the crankshaft reference pin passes the sensor at about 106 degrees BTDC.

    Analysis of the 027 ROM code

    Memory map
    $0000 ~ $0FFF - ROM (4096 bytes)
    $2000 ~ $2023 - memory mapped I/O
    $2200 ~ $227F - RAM (128 bytes)

    The 40-pin chip that isn't the CPU is an ASIC which likely handles important stuff such as timing, interrupts, and analog-to-digital conversion. (Are there different revisions of this chip, or could they all be the same?)

    Detailed breakdown of I/O locations
    $2000 (write) - $5B (M20) or $4D (M30) is written here.
            Guess: this is the number of ticks between the firing of each cylinder.
            That is, the number of degrees per flywheel tooth (2.63) divided by two,
            times $5B = ~120 degrees.
    $2001 (read) - Four different ADC values are read from here.
            air temp, coolant temp, engine speed, and (guess) battery voltage
    $2001 (write) - A value is written which seems to be ignition related
    $2002 (read/write) - Seems to be interrupt status/acknowledge flags
    $2003 (read) - Flags. bit 2 set at WOT, bit 3 set at idle, bit 4 set by baro switch?
    $2004 (read) - Guess: timing related
    $2020 (read) - Flags?
    $2020 (write) - Written with $16 at startup
    $2021 (read) - Airflow reading
    $2021 (write) - Written with $21 or the engine speed scale factor from $88B
    $2022 (write) - Written with $22 twice before writing something to $2002 (?)
    $2023 (write) - A variety of values are written here. The only one with a known
            effect is the $23 or $24 which determines the overrun fuel cut RPM.
    
    Detailed breakdown of RAM locations

    The RAM is cleared to zeros at startup. From $2242 to $227F is basically used as stack, though on a few occasions the location $227D is referenced via hardcoded address.

    $2201 - Contains an ADC value (probably air temp. larger number is colder.)
    $2202 - Contains an ADC value (probably coolant temp. larger number is colder.)
    $2203 - Contains an ADC value (engine speed, in units of 40 RPM)
    $2204 - Contains an ADC value (battery voltage??)
    
    $2208 ~ $220A - ignition related?
    
    $220D - Contains the 'final' spark advance value after some mutilation.
            For the M20 engine, it's $98 minus the calculated advance, shifted
            right once, and clamped to the previous value +/- 5
    $220E - The previous value of $220D.
    $220F - AFM reading
    
    $2213 - Contains value from table at $CDE.
    $2214 - Written with the value from the table at $7E6, also counts down.
    $2215 - Set to 1 during rev limit fuel cut.
    
    $2218 - Contains value from table at $CD6.
    $2219 - Contains value from table at $CCE.
    
    $221E~$2220 - These three get filled with values from the $Dxx / $Exx 8x4 maps.
    $2221~$2222 - Temporary storage of fraction bits for interpolated 2D map lookup.
    
    $2226 - Initialized from a table at $D07.
            Counts down, and when it hits 0 all the counters below advance.
    
    Periodic counters:
    $2227 - counts $10,F,E,D,C,B,A,9,8,7,6,5,4,3,2,1,0
    $2228 - always 0
    $2229 - always 0
    $222A - alternates 1,0
    $222B - always 0
    $222C - always 0
    $222D - always 0
    $222E - always 0
    $222F - always 0
    $2230 - always 0
    $2231 - always 0
    $2232 - alternates 1,0
    $2233 - counts 4,3,2,1,0
    
    Down counters: (change when the corresponding counter above hits 0)
    $2234 - counts down by 2
    $2235 - counts down by 2
    $2236 - counts down by 4
    $2237 - counts down by 1
    $2238 - counts down by 1
    $2239 - counts down by 2
    $223A - counts down by 5      unused?
    $223B - counts down by 5      unused?
    $223C - counts down by 5      unused?
    $223D - counts down by 5      unused?
    $223E - counts down by 5      unused?
    $223F - counts down by 1
    $2240 - counts down by 1
    
    CPU Registers

    The 1802 CPU has a set of 16x 16-bit registers. Most of them serve the same purpose consistently throughout the code.

    R(0) is often the PC
    R(1) usually points to interrupt service routine at $0598
    R(2) is usually the stack  (initialized to $227F)
    R(3) is sometimes the PC  (for the $1C4 routine, except
             in interrupt code where it is used for $1A7)
    R(4) is always $009F?  (the CALL / JSR routine)
    R(5) misc
    high byte of R(6) always is $22
    high byte of R(7) always is $22
    high byte of R(8) always is $20
    high byte of R(9) always is $08
    R(A) misc
    R(B) misc
    R(C) is sometimes the PC
    R(D) is sometimes the PC
    R(E) is always $2023
    R(F) contains flags
            RFL bit 7 = set when coolant temp is below '8'
            RFL bit 5 = cleared or set when $2237 counts down to 0
            RFL bit 4 =  / related to EF3 and EF4?
            RFL bit 3 = /
            RFH bit 7 = set during high RPM, high load
            RFH bit 5 = set when $2238 counts down to 0
            RFH bit 4 = set when $2204 is $3C or higher
    
    CPU I/O

    The 1802 has four input pins EF1~EF4, an output pin Q, and three more pins N0~N2 to serve as an address for a byte I/O scheme. EF1 goes to the fuel quality switch. I have yet to trace the other signals!

    ROM constants and tables

    $847 contains the rev limit, in units of 40 RPM. $88B contains an engine speed scale factor, which generally there is no cause to mess with, but some aftermarket chips do. $F0C in the 027 ROM contains $24. Changing it to $23 restores the lower overrun fuel cut that the 007 ECU has.

    There are a whole bunch of one-dimensional tables, and their addresses differ between the different ROM revisions. The routine that performs the table lookups is at $1C4. Table addresses, lengths, and axis scale/offset are located in the $8xx area. Example:

    The $DD causes a jump to the second-to-last in a series of SHR instructions, meaning the value serving as index into the table gets shifted right twice. The $00 is an offset which gets subtracted, after the shift (if any). The $03 is the highest index, in other words the table is going to be 4 bytes long. $7F is the low byte of the address, $0D is the high byte. So there is a 4-byte table at $D7F. It appears to be spark advance for 0 ~ 480 RPM, ie. when cranking the engine.

    How do I know the table is indexed by RPM? The code that calls $1C4 to do this table lookup is at $232. The byte following the SEP instruction is $03. This means the index for the table will come from $2203, which contains engine speed.

    Following the $D7F table is a 6-byte table at $D83, indexed by $2202 (coolant temp). The results of these table lookups get added together. $F8 and $FA are effectively negative numbers in this case, so the cranking spark advance gets reduced at warmer coolant temps.

    Following that at $D8A and $DA3 are two more tables indexed by coolant temp, likely spark advance adjustment tables for idle and non-idle, respectively. At $DBC is a 15-byte table indexed by $2201 which seems to retard ignition at high intake air temps. There are still more tables, with unknown purpose.

    There are also two-dimensional tables which have engine speed on one axis and airflow on the other. The biggest is a 16x16 map for ignition advance. The code allows for more than one ignition map, but these BMW ECUs only use one. There is also a set of three 8x4 maps (on 007 and 008) or two sets (on 027), for unknown purpose (Guess: open-loop fuel ratio). The routine at $14F processes these tables.

    The first time it's called, it compares the current engine speed with the values at $C1C and $C1D. $7C is the last row of the table, 4960RPM. $0C is the first row, 480RPM. For values that are in between, it works backward from the end by adding from the string of digits at $C1E. Each of these numbers corresponds to the RPM difference between rows, yielding these values for the RPM axis of the table:

    480, 640, 800, 960, 1280, 1600, 1760, 2240, 2720, 3040, 3360, 3680, 4160, 4320, 4800, 4960

    That's 16 rows. In the process of calculating the table position, it records a 2-bit fraction for (optionally) interpolating between values. Finally, it jumps into a code block of repeated ADD instructions, effectively multiplying the result by $10 (the row width).

    The next call to the routine handles the other axis, airflow / load, in a similar fashion, using the values beginning at $C2D. This one doesn't get multiplied at the end, instead two bits are tested from memory-mapped I/O port $2003, and the result can be forced to 0 or $0F. Presumably these are the throttle switches. Idle spark advance must come from the first column, and WOT spark advance from the last column.

    It seems likely that the scale of the values in the ignition map is equal to the angular size of a flywheel ringgear tooth divided by 4. 360/137/4=0.657 degrees for the M20, and 360/116/4=0.776 degrees for the M30. It's also stated on this archived site. This scale is used internally for calculating the advance, however the final value is shifted right by one bit, which means the actual precision of the ignition system is only half as good: 1.314 degrees for the M20, and 1.552 degrees for the M30.

    Structure of the code

    At startup/reset, execution begins at 0. Interrupts get disabled, RAM is cleared, registers are initiallized. Then at $05D, we start calling some subroutines:

  • $54A - writes the value from $88B to $2021, among other things.
  • $4AB - reads ADC values into $2201,$2202,$2203,$2204
  • $1ED - calculates spark advance
  • $2BD - some other ignition related stuff?
  • $364 - does many table lookups
  • Then it enables interrupts and goes to what I would call the 'main loop' which follows the jump table at $900. The main loop repeatedly calls those same subroutines (except $54A) and also calls $93F which maintains a bunch of timers/counters.

    The interrupt service routine is at $598. It seems there are three types of interrupts, and if the status flags don't match one of the expected states, it does a software reset. This is where the engine speed is checked against the rev limit. When below the rev limit, a relatively long section of code executes which involves reactivating interrupts before the routine is done (ie. the interrupt service routine is reentrant).

    Miscellaneous info

    Dwell appears to be 4ms at low engine speeds. At 4K RPM it has dropped to about 3.6ms, and to roughly 2.4ms at 5800RPM (tested the same on both 007 and chipped 027)

    Injector pulse at idle:

    Injector pulse at full power: