6 minute read

From the previous post, we know from analysis of UDS service 22 that the Mass airflow sensor voltage reading is stored in variable 0xb0ad0 on IC501 (for firmware image F27SC074). In this post, I’ll look into how the MAF sensor reading is processed in the ECU by lookup table(s).

We know that the MAF ground signal is connected to AN56 on IC501 ( refer to earlier post AJ27 ECU Hardware Schematic ), and copied to variable b0ae2. By checking which functions read the variables b0ad0 and b0ae2 we can quickly zoom in on FUN 1f990 as critical.

maf code snapshot1

This function is called every 4 ms, driven by PWMSM10 interrupt service routine. From the code we have:

  • b0576 = MAF sensor reading
  • b0578 = MAF ground reading
  • b057a = MAF – MAF Gnd (actual maf voltage, accounting for imperfect ground)
  • b057e = lower of previous b057a and current b057a values
  • b057c = greater of previous b057a and b057a values

Review of the new variables b057e and b057c reveal that they are processed by FUN 1f9c2, which is called 8 times over two crank rotations, i.e. once per cylinder for each 4 stroke cycle.

maf code snapshot2

At the start of the function, some manipulation creates two new variables

  • b0584 - highest value of the previous two maf readings
  • b0586 - lowest value of the previous two maf readings

Then these two new variables are processed using some lookup tables.

maf code snapshot3

As would be expected in an ECU from this era, lookup tables are used for many purposes. It’s fairly easy to see that FUN 12354 does the lookup, using the table address stored in Y, the value to be looked up in E, and the result returned in D. One option to understand the table lookup algorithm is to review FUN 12354, and doing this (not shown) reveals that it uses the word value in E to return an interpolated value in D. In order to get a picture of the lookup curve, some pen and paper, and spreadsheet work, can be done to replicate the processing. However, another option, especially if there are quite a few tables to examine (there are many in this ECU), is to emulate the table lookup code for a spread of values, and build a picture of the curves that way.

For this exercise, we will emulate the code from address 0x1fa00 to address 0x1fa44. By setting the memory locations b006a and b006c to various values, we can exercise the entire range of values and extract the lookup tables. A convenient way to do this is to write a script. It will be completely customized to this firmware load, but nevertheless is a quick way to get the data, and can be tweaked to process any table in the code very quickly. Writing generic Ghidra scripts is well covered by others, so I’ll focus on the emulation part. Ghidra has recently added a Debugger/Emulator support tool, but for this exercise a script is more effective, since we want to re-run the emulation of code many times over.

The key Ghidra class for controlling emulation is the EmulatorHelper class. It enables reading and writing of registers and memory addresses, setting breakpoints, and running code starting from a specified address.

The rough outline of the script is:

  • create a class to store lookup table entries
    • store source value (maf voltage), interim lookup value (first table), final lookup value (second table)
    • also compute the final flow rate in gram/sec and store that
    • add method to create string representation of the data for output to a .csv file
  • create an array to store the results
  • initialize the CPU context
  • create Ghidra addresses for the start/entry point, and for the breakpoint/end point
  • create a for-next loop to exercise all possible table entries
    • these can be done two at a time since the function does two lookups
    • run the code for each pair of values, and log the results
  • create a .csv file for the results, and write the array of data to the file

An example class to store data is as below. We know the units of the final MAF reading, because MAF flow rate is a standardized parameter in OBD2 mode 1 with code 0x10. The units are grams/sec, and the value is ( (256 * A) + B ) / 100, where A and B are the two bytes returned by the mode 1 query, which allows values from 0 - 655.36 g/s. When this command is executed in the ECU, the final lookup value N for the MAF flow is processed as follows and returned as value M

M = ( N * 0x9c40) » 16 (Note 0x9c40 = 40 000 decimal)

	class MafTableEntry
	{
		private int mafVoltage;
		private int mafInterimLookup;
		private int mafFinalLookup;
		private int mafFlowRate;
		
		MafTableEntry(int a, int b, int c)
		{
			mafVoltage = a;
			mafInterimLookup = b;
			mafFinalLookup = c;
			long temp = mafFinalLookup;
			temp = temp * 40000;
			temp = temp / 65536;
			mafFlowRate = (int) temp;
		}
		
		@Override
		public String toString()
		{
			String s = "";
			s = s + Integer.toString(mafVoltage)+",";
			s = s + Integer.toString(mafInterimLookup)+",";
			s = s + Integer.toString(mafFinalLookup)+",";
			s = s + Integer.toString(mafFlowRate)+",";
			s = s + " - ,";
			s = s + "0x"+Integer.toHexString(mafVoltage)+",";
			s = s + "0x"+Integer.toHexString(mafInterimLookup)+",";
			s = s + "0x"+Integer.toHexString(mafFinalLookup)+",";
			s = s + "0x"+Integer.toHexString(mafFlowRate)+"\n";
			return s;
		}
	}

To initialize the emulator we need to:

  • create a new EmulatorHelper
  • define some key addresses
  • create an ArrayList to store the data
  • setup the processor context, i.e. initialize key registers
  • set a breakpoint where we wish execution to stop
	private EmulatorHelper emuHelper;
	
	@Override
	protected void run() throws Exception {
		
		try
		{
			emuHelper = new EmulatorHelper(currentProgram);
			Address addr_b0584 = toAddr(0xb0584);
			Address addr_b0586 = toAddr(0xb0586);
			Address addr_b006a = toAddr(0xb006a);
			Address addr_b0588 = toAddr(0xb0588);
			Address start_addr = toAddr(0x1fa00);
			Address end_addr = toAddr(0x1fa48);
			
			ArrayList<MafTableEntry> results = new ArrayList<MafTableEntry>();
			
			Language lang = emuHelper.getLanguage();
			ProcessorContextImpl procContext = new ProcessorContextImpl(lang);
			Register ek = currentProgram.getRegister("EK");
			procContext.setValue(ek, BigInteger.valueOf(0xb0000));
			Register ize = currentProgram.getRegister("IZE");
			procContext.setValue(ize, BigInteger.valueOf(0xb0000));
			
			emuHelper.writeMemoryValue(addr_b006a, 4, 0);
			emuHelper.writeMemoryValue(addr_b0588, 4, 0);

			emuHelper.setBreakpoint(end_addr);

Once this is done, we can execute a loop that exercises all the values of the table that we are interested in:

  • write the values to be looked up into memory
  • run the code with the emuHelper.run() method
  • extract the results from the memory and store in the array
			for (int count=0;count<512;count++)
			{
				//initialize memory locations with net two values to be looked up
				int lookup1 = 2*count;
				int lookup2 = (2*count)+1;
				
				emuHelper.writeMemoryValue(addr_b0584, 2, lookup1);
				emuHelper.writeMemoryValue(addr_b0586, 2, lookup2);
				
				if (monitor.isCancelled())
				{
					println("Emulation cancelled");
					return;
				}
					
				boolean success = emuHelper.run(start_addr, procContext, monitor);
				
				if (!success) {
					String lastError = emuHelper.getLastError();
					printerr("Emulation Error: " + lastError);
					return;
				}

				Address executionAddress = emuHelper.getExecutionAddress();
				
				if (executionAddress.equals(end_addr))
				{
					//save key memory values			
					byte[] interimResults = emuHelper.readMemory(addr_b006a, 4);
					
					int interim1 = Byte.toUnsignedInt(interimResults[0])*256 + Byte.toUnsignedInt(interimResults[1]);
					int interim2 = Byte.toUnsignedInt(interimResults[2])*256 + Byte.toUnsignedInt(interimResults[3]);
					
					byte[] finalResults = emuHelper.readMemory(addr_b0588, 4);			
					
					int final1 = Byte.toUnsignedInt(finalResults[0])*256 + Byte.toUnsignedInt(finalResults[1]);
					int final2 = Byte.toUnsignedInt(finalResults[2])*256 + Byte.toUnsignedInt(finalResults[3]);
					
					results.add(new MafTableEntry(lookup1, interim1, final1));
					results.add(new MafTableEntry(lookup2, interim2, final2));
				}
			}

Finally, we can output the results to a file, and dispose of the emulatorHelper before exiting

			//create the output file
			String outFileName = System.getProperty("user.home") + "\\" +
					currentProgram.getName() + "_MAF_Data.csv";
			
			println("creating file "+outFileName);
			
			String headers = "MAF reading, interim lookup, final lookup, flow (g/s), - ,"
					+ " MAF reading (hex), interim lookup (hex), final lookup (hex), flow (g/s) (hex)\n";
			
			BufferedWriter outFile = new BufferedWriter(new FileWriter(outFileName));
			
			outFile.write(headers);
			
			for (MafTableEntry e : results)
			{
				outFile.write(e.toString());
			}
			
			outFile.close();
			
			println("closed outfile");
			
		}
		catch (IOException e) {
			monitor.setMessage("Issue writing to output file");
			String error = e.getMessage();
			if (error != null)
				monitor.setMessage(error);
			monitor.setMessage("Terminating analysis");
		}
		finally {
			// cleanup resources and release hold on currentProgram
			emuHelper.dispose();
		}

To run the script, open the firmware file in Ghidra, and run the script using the script manager

A plot of the maf curve from the spreadsheet data is shown below.

maf input output curve