Everything else in the sketch is really just life support for a dozen or so lines of code in a function called elm_read that simply listens to the serial connection until it sees an “\r”
Trang 1if(millis() - lastLogWrite > LOG_INTERVAL)
{
The blue LED indicating that a sample is being written is illuminated, and the position counter for the log buffer is reset
digitalWrite(VDIP_WRITE_LED, HIGH);
byte position = 0;
The log entry length and the entry itself are sent to the host for debugging purposes
HOST.print(logEntry.length());
HOST.print(": ");
HOST.println(logEntry);
Now for the interesting bit A WRF (WRite File) command is sent to the VDIP1 with a single
argument that tells it the number of bytes of data to follow in the actual message Because each log entry will have a newline character appended, we have to take the current logEntry length and add 1 to it to get the actual message length
Note that before doing this, the VDIP1 needs to be initialized, and that process is taken care of by a function that we’ll see in just a moment
VDIP.print("WRF ");
VDIP.print(logEntry.length() + 1);
VDIP.print(13, BYTE);
The position counter is used to walk through the log buffer array one character at a time to send it to the VDIP1 However, the RTS (ready to send) pin on the VDIP1 is checked prior to transmission of each character to make sure the VDIP1 input buffer still has free space If RTS is low (inactive) it’s clear to
send the character and increment the position counter Otherwise it shouts loudly to the host to notify you that the VDIP1 buffer was full In production, you probably wouldn’t want the error message being sent to the host, but it can be handy when doing development
while(position < logEntry.length())
{
if(digitalRead(VDIP_RTS_PIN) == LOW)
{
VDIP.print(vdipBuffer[position]);
position++;
} else {
HOST.println("BUFFER FULL");
}
}
After sending a WRF command to the VDIP1, it will keep accepting data until it has received exactly the number of bytes specified in the WRF argument The number passed in was one greater than the
number of bytes in the buffer, so if nothing else was sent, the Vinculum chip on the VDIP1 would sit
patiently waiting for the next character If a mistake is made calculating the number of bytes to be sent, it’s easy to end up in a situation where you send one byte too few and the Vinculum doesn’t finish
reading Then, your program continues on around the loop and comes back to send more data to the
VDIP1 on the next pass through It then starts sending the WRF command, but because the Vinculum
never exited write mode last time around, it sees the “W” character as the final character of the last write, then interprets “RF” is the start of another command RF is meaningless to it so it will then output an
error and you’ll end up with the original entry written to the file with a trailing W and nothing written for the second pass at all
So the moral of the story is to always, always, always check your message length very carefully when preparing data to send to the Vinculum chip If you send fewer characters than it is expecting, it will
remain in write mode waiting for more data; if you send too many characters, it will treat the excess as
separate commands If you’re really unlucky, those excess characters could constitute a command to
perform a dangerous action such as deleting a file!
Trang 2Something that could be done to minimize the risk is to send the characters one at a time and implement a check to look for the prompt response that the Vinculum will send when it finishes writing
to the file If the prompt comes back unexpectedly, it’s better to skip sending the rest of the buffer rather than to keep sending data If the prompt doesn’t come back after all the characters have been sent, the message could be padded by sending spaces until the prompt returns
In this case, though, we’re just carefully counting characters including the trailing newline, so the program then sends the newline character and turns off the LED that indicates a write is in progress It then sets the lastLogWrite variable to the number of milliseconds since startup so next time through the loop it can check whether it’s due to record another log entry
VDIP.print(13, BYTE);
digitalWrite(VDIP_WRITE_LED, LOW);
lastLogWrite = millis();
}
}
Way back in setup(), we looked at pin change interrupts and the way changes to the menu button states cause an ISR to be invoked This is the definition of that ISR, and you can see that it uses an #ifdef check to substitute a different version of the function, depending on whether this is a Mega or non-Mega build
The Mega version is attached to PCINT2, and the first thing it does is check whether it has been more than 20 milliseconds since it was last invoked If not, it’s probably a problem with the physical switch bouncing open and closed rapidly as it settles, so it’s ignored If it is greater than 20 milliseconds, the buttonState global variable is updated with the value of the PINK register, which reads the value of all the pins in port K Analog inputs 7 through 13 on a Mega are all part of port K
#ifdef MEGA
ISR(PCINT2_vect)
{
static unsigned long last_millis = 0;
unsigned long m = millis();
if (m - last_millis > 20)
{
buttonState |= ~PINK;
}
last_millis = m;
}
The non-Mega version does the same thing but with PCINT1, and reads from the port C register using PINC
#else
ISR(PCINT1_vect)
{
static unsigned long last_millis = 0;
unsigned long m = millis();
if (m - last_millis > 20)
{
buttonState |= ~PINC;
}
last_millis = m;
}
Trang 3#endif
Reading from the ELM327 is pretty much the core function of the OBDuinoMega sketch Everything else in the sketch is really just life support for a dozen or so lines of code in a function called elm_read() that simply listens to the serial connection until it sees an “\r” character followed by a prompt,
indicating that the ELM327 has finished sending its message
The function requires two arguments: a pointer to a character array for the response to be stored in, and a byte indicating how many elements it’s allowed to put in that array
It then defines variables to hold response values and the number of characters read so far
byte elm_read(char *str, byte size)
{
int b;
byte i=0;
It loops reading from the serial port until it either sees a prompt character (in which case it knows it got a complete response) or runs out of space in the array It inserts each character into the array and
increments the position counter only if the character is a space character or greater, which is hex value 0x20 in the ASCII table This excludes any control characters that could be sent through
while((b=OBD2.read())!=PROMPT && i<size)
{
if(b>=' ')
str[i++]=b;
}
The two possible outcomes at this point are that the number of characters received is less than the array length and therefore the program got a prompt, or that the number of characters reached the array length and therefore the response was probably meaningless
If the counter “i” is not equal to the array size, everything is probably okay, so the last character
entered into the array pointer (most likely a carriage return) needs to be replaced with a null character to indicate the end of the string The function then returns the prompt character to indicate success
Otherwise, the program assumes the response was meaningless and returns the value 1, signified by the DATA placeholder defined at the start of the sketch, to indicate that there is raw data in the buffer
if(i!=size)
{
str[i]=NUL;
return PROMPT;
}
else
return DATA;
}
The response that comes back from the ELM327 is an ASCII string that represents a hexadecimal
number It may look like hex but don’t be deceived—it’s not!
For example, if the ELM327 sends a response of 1AF8 to mean a decimal value of 6904, what we
actually receive from the serial port is the ASCII values that represent those individual characters: 0x31
to represent 1, 0x41 to represent A, 0x46 to represent F, and 0x38 to represent 8 This is not at all what we wanted, and if you process the bytes literally, you’ll get an incorrect answer
To make sense of the response value, the sketch really needs it as an actual numeric type rather than
a string, so the elm_compact_response() function accepts a raw ELM327 response and turns it into a real hex value stored in a byte array
Because the response from the ELM327 starts with an echo of the mode plus 0x40 and then the PID, the sketch has to skip the first few bytes For example, if the request 010C was sent, the response would
be something like “41 0C 1A F8,“ so the first byte we would actually care about would be the seventh
character The end result that we want is the numeric value 0x1AF8 ready to send back to the calling
function
Trang 4Note that the call to strtoul (string to unsigned long) passes in a third argument of 16, the base required for the response Base 16 is hexadecimal
The return value from the function is simply the number of bytes in the converted value
byte elm_compact_response(byte *buf, char *str)
{
byte i=0;
str+=6;
while(*str!=NUL)
buf[i++]=strtoul(str, &str, 16);
return i;
}
Initializing the serial connection to the ELM327 is quite straightforward First, the serial port itself is opened at the rate configured at the start of the sketch, then the serial buffer is flushed to ensure there’s
no stray data sitting in it
void elm_init()
{
char str[STRLEN];
OBD2.begin(OBD2_BAUD_RATE);
OBD2.flush();
Just in case the ELM327 had already been powered up and had settings changed, it’s then sent a soft-reset command
elm_command(str, PSTR("ATWS\r"));
A message is then displayed on the LCD to show progress If the first character back is an “A,” the program assumes that it’s echoing the command and skips ahead to read the response from the fifth character (position 4) onward Otherwise, it simply displays the message as is
lcd_gotoXY(0,1);
if(str[0]=='A')
lcd_print(str+4);
else
lcd_print(str);
lcd_print_P(PSTR(" Init"));
To get responses back from the ELM327 a little faster it’s a good idea to turn off command echo, otherwise every response will be bloated with several bytes taken up just repeating the command we sent to it The ATE0 command suppresses command echo
elm_command(str, PSTR("ATE0\r"));
The sketch then goes into a do-while loop trying to verify that the ELM327 is alive and
communicating by sending a request for PID 0X0100 (PIDs supported) repeatedly until it gets a
response If you start up the system without putting it into debug mode or connecting it to an ELM327, and it ends up sitting on a screen that reads “Init” forever; this is the loop it’s trapped in
do
{
elm_command(str, PSTR("0100\r"));
delay(1000);
}
while(elm_check_response("0100", str)!=0);
When using the OBD-II interface to communicate with a vehicle’s internal communications bus, there are typically multiple ECUs (electronic control units) sharing that bus The primary ECU that responds with OBD-II values is identified as ECU #1, and the ELM327 can either direct its requests generally to all devices on the bus or it can direct them to a specific ECU
Trang 5By default, the ELM327 shouts its requests to the world, but by modifying the communications
header that it sends to the car, it’s possible to make it specifically ask for the primary ECU
This is done by setting a custom header that the ELM327 uses for messages sent to the car, but the format of the header depends on what communications protocol it’s using Because the ELM327 takes care of all the protocol conversion behind the scenes, the sketch doesn’t generally need to know the
details of what’s going on, but to determine the car’s protocol it can send an ATDPN (ATtention:
Describe Protocol by Number) command to have the ELM327 report which protocol it has
autonegotiated with the car
elm_command(str, PSTR("ATDPN\r"));
The OBDuinoMega sketch can then set a custom header specifying that all requests should go to
ECU #1 using the appropriate format for that particular protocol
if(str[1]=='1') // PWM
elm_command(str, PSTR("ATSHE410F1\r"));
else if(str[1]=='2') // VPW
elm_command(str, PSTR("ATSHA810F1\r"));
else if(str[1]=='3') // ISO 9141
elm_command(str, PSTR("ATSH6810F1\r"));
else if(str[1]=='6') // CAN 11 bits
elm_command(str, PSTR("ATSH7E0\r"));
else if(str[1]=='7') // CAN 29 bits
elm_command(str, PSTR("ATSHDA10F1\r"));
}
All done The ELM327 should now be running in a reasonably well optimized state, with no
command echo and all requests specifically directed to ECU #1
The get_pid() function is called by the display() function to fetch values to display on the LCD, and also in the main loop by the logging code to fetch values to write to the CSV file on the memory stick The majority of the code in this very long function is a massive switch statement that checks which PID is
being requested and then sources the result and processes it appropriately, putting the numeric value in
a long pointer and a version formatted for string output into a buffer The return value of the function
indicates whether retrieval of the PID was successful or not, so a simple call to this function and then a check of the response will give access to just about any information accessible by the Vehicle Telemetry Platform
The start of the function takes the requested PID and sets up some variables
boolean get_pid(byte pid, char *retbuf, long *ret)
{
#ifdef ELM
char cmd_str[6]; // to send to ELM
char str[STRLEN]; // to receive from ELM
#else
byte cmd[2]; // to send the command
#endif
byte i;
byte buf[10]; // to receive the result
byte reslen;
char decs[16];
unsigned long time_now, delta_time;
static byte nbpid=0;
It then checks if the PID is supported by calling out to another function If it is not supported, it puts
an error message in the return buffer and returns a FALSE value
if(!is_pid_supported(pid, 0))
{
Trang 6sprintf_P(retbuf, PSTR("%02X N/A"), pid);
return false;
}
Way back at the start of the sketch, each PID was defined along with the number of bytes to expect
in response to each one The sketch then reads the receive length value out of EEPROM by referencing the memory position for that PID
reslen=pgm_read_byte_near(pid_reslen+pid);
The request is then sent to the vehicle using one of two methods, depending on whether the system was built using an ELM327 as in our prototype, or uses interface hardware specific to the particular car The ELM version formats the request by appending the PID to the mode then adding a carriage return at the end, then sends it to the ELM327, and then waits for the response The response value is checked to make sure there’s no error value If there is, “ERROR” is put in the return buffer and the function bails out with a FALSE return value
Assuming the response was good and the function didn’t bail out, it then proceeds by sending the value off to be converted from an ASCII string to an actual numeric value using the
elm_compact_response() function previously defined
#ifdef ELM
sprintf_P(cmd_str, PSTR("01%02X\r"), pid);
elm_write(cmd_str);
elm_read(str, STRLEN);
if(elm_check_response(cmd_str, str)!=0)
{
sprintf_P(retbuf, PSTR("ERROR"));
return false;
}
elm_compact_response(buf, str);
The non-ELM version follows almost exactly the same process, but rather than use calls to ELM functions, it uses equivalent ISO functions
#else
cmd[0]=0x01; // ISO cmd 1, get PID
cmd[1]=pid;
iso_write_data(cmd, 2);
if (!iso_read_data(buf, reslen))
{
sprintf_P(retbuf, PSTR("ERROR"));
return false;
}
#endif
By this point, the sketch has the raw result as a numeric value, but as explained previously most PIDs require a formula to be applied to convert the raw bytes into meaningful values
Because many PIDs use the formula (A * 256) + B, the sketch then calculates the result of that formula no matter what the PID is The result may be overwritten later if this particular PID is an
exception, but determining a default value first, even if it’s thrown away later, saves 40 bytes over conditionally calculating it based on the PID With the original MPGuino/OBDuino codebases designed
to squeeze into smaller ATMega CPUs, every byte counts
*ret=buf[0]*256U+buf[1];
The rest of the function is a huge switch statement that applies the correct formula for the particular PID being requested We won’t show the whole statement here, but you’ll get the idea by looking at a few examples
The first check is whether the requested PID was the engine RPM In debug mode it returns a hard-coded value of 1726RPM, and otherwise it takes the return value and divides it by 4 The full formula for
Trang 7the engine RPM is ((A * 256) + B) / 4, but because the return value was already calculated, the first part of the formula has already been applied and it just needs the division portion
switch(pid)
{
case ENGINE_RPM:
#ifdef DEBUG
*ret=1726;
#else
*ret=*ret/4U;
#endif
sprintf_P(retbuf, PSTR("%ld RPM"), *ret);
break;
The Mass Air Flow parameter is similar: return a hard-coded value in debug mode, or take the
precalculated value and divide it by 100 as per the required formula
case MAF_AIR_FLOW:
#ifdef DEBUG
*ret=2048;
#endif
long_to_dec_str(*ret, decs, 2);
sprintf_P(retbuf, PSTR("%s g/s"), decs);
break;
Vehicle speed is a trivial parameter, and then it gets to the fuel status parameter Fuel status is a
bitmap value, so each bit in the response value is checked in turn by comparing it to a simple binary
progression (compared in the code using the hex equivalent value) and the matching label is then
returned In the case of this particular parameter, it’s not really the numeric value that is useful, but the label associated with it
case FUEL_STATUS:
#ifdef DEBUG
*ret=0x0200;
#endif
if(buf[0]==0x01)
sprintf_P(retbuf, PSTR("OPENLOWT")); // Open due to insufficient engine temperature
else if(buf[0]==0x02)
sprintf_P(retbuf, PSTR("CLSEOXYS")); // Closed loop, using oxygen sensor feedback to
determine fuel mix Should be almost always this
else if(buf[0]==0x04)
sprintf_P(retbuf, PSTR("OPENLOAD")); // Open loop due to engine load, can trigger
DFCO
else if(buf[0]==0x08)
sprintf_P(retbuf, PSTR("OPENFAIL")); // Open loop due to system failure
else if(buf[0]==0x10)
sprintf_P(retbuf, PSTR("CLSEBADF")); // Closed loop, using at least one oxygen sensor but there is a fault in the feedback system
else
sprintf_P(retbuf, PSTR("%04lX"), *ret);
break;
A number of parameters require an identical formula of (A * 100) / 255, so they’re all applied in a
group
case LOAD_VALUE:
case THROTTLE_POS:
case REL_THR_POS:
Trang 8case EGR:
case EGR_ERROR:
case FUEL_LEVEL:
case ABS_THR_POS_B:
case CMD_THR_ACTU:
#ifdef DEBUG
*ret=17;
#else
*ret=(buf[0]*100U)/255U;
#endif
sprintf_P(retbuf, PSTR("%ld %%"), *ret);
break;
The function continues in a similar way for the rest of the PIDs If you want to see the details of how
a particular PID is processed, it’s best to look in the OBDuinoMega source code
Other functions in the main file then provide features such as calculation of current (instant) fuel consumption and the distance that could be traveled, using the fuel remaining in the tank
Once on every pass through the main loop, a call is placed to the accu_trip() function to accumulate data for the current trip by adding current values to trip values Among other things, it increments the duration of the trip in milliseconds; the distance traveled in centimeters (allowing a trip of up to
42,949km or 26,671mi because the distance is stored in an unsigned long); fuel consumed; and mass air flow
One particularly interesting value it accumulates is “fuel wasted,” which is the amount of fuel that has been consumed while the engine was idling
The display() function takes care of fetching the value associated with a specific PID and displaying
it at a nominated location on the LCD Because the PIDs defined at the start of the sketch can be either real (provided by the engine-management system) or fake (generated by the sketch internally or from some other data source), this function explicitly checks for a number of PIDs that require data to be returned by a specific function
void display(byte location, byte pid)
char str[STRLEN];
if(pid==NO_DISPLAY)
return;
else if(pid==OUTING_COST)
get_cost(str, OUTING);
else if(pid==TRIP_COST)
get_cost(str, TRIP);
else if(pid==TANK_COST)
get_cost(str, TANK);
It goes on in a similar way for dozens of PIDs that it knows about specifically until it falls through to the default behavior, which is to pass the request on to the get_pid() function we just saw
else
get_pid(pid, str, &tempLong);
The function then sets a null string terminator into the result string at the LCD_split position, which was calculated back at the start of the sketch as half the width of the LCD This effectively truncates the result at half the display width so that it can’t overwrite an adjacent value
str[LCD_split] = '\0';
It then does some manipulation of the “location” argument that was passed in to determine which row it goes on given that there are two locations per line, then checks if it’s an even number and should therefore go on the left, and finally calculates the start and end character positions for that location byte row = location / 2; // Two PIDs per line
boolean isLeft = location % 2 == 0; // First PID per line is always left
Trang 9byte textPos = isLeft ? 0 : LCD_width - strlen(str);
byte clearStart = isLeft ? strlen(str) : LCD_split;
byte clearEnd = isLeft ? LCD_split : textPos;
It’s then just a matter of going to that location and printing the string to the LCD
lcd_gotoXY(textPos,row);
lcd_print(str);
The last thing the function needs to do is get rid of any leading or trailing characters that might still
be visible on the LCD after the value was written This can happen if the previously displayed value used more characters than the current value, and because characters are only replaced if they are explicitly
written to, it’s necessary to write spaces into characters we don’t care about
lcd_gotoXY(clearStart,row);
for (byte cleanup = clearStart; cleanup < clearEnd; cleanup++)
{
lcd_dataWrite(' ');
}
}
For maintenance purposes, one of the most important pieces of information available via OBD-II is the response to mode 0x03, “Show diagnostic trouble codes.” It’s also one of the most complex because
of the variations in the type of data that it needs to return
Mode 0x03 doesn’t contain any PIDs, so there’s no need to request anything but the mode itself, and
it always returns four bytes of data A typical response could be as follows:
43 17 71 00 00 00 00
The “43” header is because it’s a response to a mode 0x03 request, and response headers always
start with the mode plus 0x40
The rest of the message is three pairs of bytes, so this example would be read as 1771, 0000, and
0000 The zero value pairs are empty but are always returned anyway so that the response length is
consistent
In this example, the only stored trouble code is 0x1771, so let’s look at how to convert it into
something meaningful and figure out what might have gone wrong with the car
The first byte is 0x17 (or binary 00010111), which consists of two digits, 1 and 7 If we split that
binary value into two halves (nibbles) we end up with 0001 representing the first digit, 1, and 0111
representing the second digit, 7
The first digit represents the DTC prefix that tells us what type of trouble code it is and whether its meaning is standards-defined or manufacturer-defined To complicate things a little more, the first digit
is in turn divided into two sets of bits, so we can’t just take it at face value
In our example, the first digit is 1, or binary 0001 That needs to be split into a pair of two-bit
numbers, so in our case it will be 00 and 01 Each pair can have four possible values, with the first pair
representing the section of the car in which the problem occurred, and the second pair specifying
whether that DTC is defined by the SAE standards body or the manufacturer
The four possible values for the first pair of bits are shown in Table 15-9
Trang 10Table 15-9 DTC location codes
There are also four possible values for the second pair of bits, but unfortunately their meaning can vary depending on the value of the first pair These are given in Table 15-10
Table 15-10 DTC definition source
10 2 SAE in P, manufacturer in C, B, and U
11 3 Jointly defined in P, reserved in C, B, and U
Because the meaning of the second value can vary based on the first value, the easiest way to approach it is so create a big look-up table that maps all 16 possible values of the first four bits (the first character in the response) to its specific meaning These are given in Table 15-11
Table 15-11 DTC location and definitions combined
0011 3 P3 Powertrain, jointly defined