1. Trang chủ
  2. » Công Nghệ Thông Tin

Practical Arduino Cool Projects for Open Source Hardware- P38 pot

10 402 0
Tài liệu đã được kiểm tra trùng lặp

Đang tải... (xem toàn văn)

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 10
Dung lượng 125,44 KB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

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 1

if(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 2

Something 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 4

Note 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 5

By 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 6

sprintf_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 7

the 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 8

case 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 9

byte 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 10

Table 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

Ngày đăng: 03/07/2014, 20:20

TỪ KHÓA LIÊN QUAN