Ever since I was a kid, I have always loved electronics, audio and anything mechanical (mostly cars). Throughout the years I owned a few cars, and in the summer of 2016 I purchased a brand new VW Golf GTI.
My car was in stock form for a while, but slowly I started to modify it - OEM LED tail lights, replica HID headlights, 19" VMR wheels, VWR lowering springs, Carbon mirror covers and, eventually, Full Bolt On APR stage 3 engine mods+tune.
Here is what It looks like today:
With such drastic engine mods (from a stock 220hp to roughly 380hp), I needed a means of monitoring critical engine data while driving.
Options are:
- BT OBD dongle with an additional Android phone to display data - requires another phone that needs to be mounted/demounted each time I drive, and perhaps manually connect to the BT dongle
- 52mm CAN gauge - these require a mounting pod (which I hate), have tiny 1" screens and cost a fortune (350 UDS +)
- Log with a PC every now and then - not an option for spirited daily driving.
I wasn't digging any of these options above, so I decided I will try build something myself.
The first stepWith ZERO knowledge of how car OBD ports work, or even how Arduino works, I purchased an Arduino Uno R3 and a CAN bus shield. Very quickly, using an available OBDII library, I was able to pull data from the GTI's OBDII port and see it changing on the Arduino's serial monitor. Data rate was slow (1s per request) and I now needed to hook up a display and see the data on the display. Once again, a standard library and a few examples got me there using a small 1.2" I2C OLED display
But, that was only one parameter, refreshing really slowly. So, back to Google - I stumble on the Teensy 3.2 - WOW!! a 96Mhz processor with built in CAN bus and a Teensy footprint ;) - Exactly what I needed. I also purchased an ILI9341 2.8" display and a CAN transceiver, hooked it all up and started to write some basic code.
After about three months of learning how to pull more data from the OBD port via CAN and reading/posting on the PJRC forum, I was able to come up with the following:
The more I got into researching OBD and CAN bus, the more complicated it got for me.
Most of my research & testing was done in the car - I had no means of emulating the data, playing with the poll rate of data etc on a test bench. I would play around with code in the car for 30 mins after I got home from work 2-3 times a weeks, for months.
At some point I reached out to a friend for some guidance. Up until this point I was pulling standard OBDII PID's, which are mostly universal, but, I wanted more data which is not standard and not documented anywhere.
This is where UDS came into the picture.
UDS is the underlying application layer of the CAN communication in cars. Each manufacture dictates their own assignment of message IDs, Service ID's, Data ID's and response structures. There is much more data available on the UDS layer than on the OBDII layer, but as I mentioned it's not documented anywhere - you have to be creative to find out how to request data and how it's sent back.
My friend was able to guide me on how to move away from OBD requests and into UDS requests, allowing more to fetch more data and at a faster rate.
I'll provide an example below of an OBDII request vs a UDS request of a simple parameter such as engine RPM
OBDII:
Request: ID: 0x7E0 0x02 0x01 0x0C 0x00 0x00 0x00 0x00 0x00
ID 0x7E0 is the engine's ECU, 0x02 is the number of bytes in the request, 0x01 is the mode of the request (OBDII) and 0x0C is the engine RPM PID.
Response: ID: 0x7E8 0x04 0x41 0x0C 0x21 0x05 0x00 0x00 0x00
ID 0x7E8 is the device (Teensy) requesting the data, 0x05 is the number of bytes in the response, 0x41 is a positive response to the mode 1 request, 0x0C is the RPM PID, and 0x21 + 0x05 is the RPM data that goes into the following equation:
A * 256 + B / 4 == 0x21 * 256 + 0x05 / 4 == 33 * 256 + 5 / 4 == 2113.25
Note: The numbers are not accurate in my example, but this is the concept.
Now, lets see how it looks in UDS format:
UDS:
Request: ID: 0x7E0 0x03 0x22 0xF4 0x0C 0x00 0x00 0x00 0x00
ID 0x7E0 is the engine's ECU, 0x03is the number of bytes in the request, 0x22 is the mode of the request (UDS enhanced data) and 0xF40C is the engine RPM PID.
Response: ID: 0x7E8 0x06 0x62 0xF4 0x0C 0x21 0x05 0x00 0x00
ID 0x7E8 is the device (Teensy) requesting the data, 0x06 is the number of bytes in the response, 0x62 is a positive response to the mode 22 request, 0xF40C is the RPM PID and 0x21 + 0x05 is the RPM data that goes into the following equation:
So what in the difference between UDS and OBD??
OBD PID's are requested from mode 0x01 only. The PID range is limited to 1 byte (0x00 - 0xFF) and are mostly common between vehicles. They are also processed via the CAN gateway so there is a middle-man between the tester device and the engine ECU.
UDS PIDs (DID's) are requested from mode 0x22 (enhanced data service). The PID range is 2 bytes (0x00 to 0xFFFF and are sent directly to the ECU that data is being requested from, so the CAN gateways doesn't introduce further delay in converting the OBD request into a UDS request.
I used Tony's FlexCAN_t4 library to handle the CAN communications. See a sample of a request and a global callback for catching incoming data:
#include <FlexCAN_T4.h>
//FlexCAN_T4<CAN0, RX_SIZE_256, TX_SIZE_16> Can0; // Teensy 3.2
FlexCAN_T4<CAN1, RX_SIZE_256, TX_SIZE_16> Can0; // Teensy 4.0
int RPM = 0;
void setup(){
while (!Serial && millis() < 5000);
Serial.begin(115200);
Can0.begin();
Can0.setBaudRate(500000);
Can0.setMBFilter(REJECT_ALL);
Can0.setMBFilter(MB1, 0x7E8);
Can0.enableMBInterrupt(MB1);
Can0.onReceive(canSniff);
}
const long loopDelay1 = 100;
unsigned long timeNow1 = 0;
void loop(){
Can0.events();
if (millis() > timeNow1 + loopDelay1) // Send the request every 100ms
{
timeNow1 = millis();
can_TX();
}
Serial.println("RPM: "); Serial.print(RPM);
}
void canSniff (const CAN_message_t &msg){ // Global callback - prints out the received message in HEX + saves RPM into a global variable
Serial.println(" ");
Serial.print("MB: ");
Serial.print(msg.mb);
Serial.print(" ID: 0x");
Serial.print(msg.id, HEX);
Serial.print(" EXT: ");
Serial.print(msg.flags.extended);
Serial.print(" LEN: ");
Serial.print(msg.len);
Serial.print(" DATA: ");
for (uint8_t i = 0; i < 8; i++)
{
Serial.print(msg.buf[i], HEX);
Serial.print(" ");
}
if (msg.buf[1] == 0x62 && msg.buf[2] == 0xf4 && msg.buf[3] == 0x0c){
RPM = ((msg.buf[4] * 256 + msg.buf[5]) / 4); // RPM
}
}
void can_TX(){ // Send a request to get RPM from ECU
CAN_message_t msgTx, msgRx;
msgTx.buf[0] = 0x03;
msgTx.buf[1] = 0x22; // UDS Enhanced Data
msgTx.buf[2] = 0xf4; // RPM
msgTx.buf[3] = 0x0c; // RPM
msgTx.buf[4] = 0x00;
msgTx.buf[5] = 0x00;
msgTx.buf[6] = 0x00;
msgTx.buf[7] = 0x00;
msgTx.len = 8; // number of bytes in request
msgTx.flags.extended = 0; // 11 bit header, not 29 bit
msgTx.flags.remote = 0;
msgTx.id = 0x7E0; // request header for OBD
Can0.write(msgTx);
}
It works, but I want it to look way better!So once I had my head wrapped around how CAN bus and UDS work, I was able to get a really high refresh rate (requesting data as fast as 3ms at a time). But I wanted something visual on the display that looks like an analog gauge for at least one parameter - Intake manifold pressure
So thanks to many members of the PJRC forum (KurtE!!!) I was able to go from displaying just a number, to displaying a gauge with a moving needle, and very fast!
I used a test sketch provided by KurtE that sweeps the needle up and down in the gauge - see attached file ILI9341_t3n_gauge_test.zip
You can download KurtE's enhanced library for the ILI9341 here.
I came to realise that I need a bigger display, so I upgraded to an Adafruite HX8357 3.5" SPI display as well as a faster Teensy 4.0 running at 450Mhz. I also fabricated some PCB's (OSHPark purples!) to mount the Teensy 4.0, CAN transceiver and a small step down buck converter behind the Adafruite display
Here is what it looks like:
And some video's in action:
The display currently queries and presents the following data:
- Intake Manifold Pressure (from -30inHg vacuum to 30 PSI boost) with peak value
- Air fuel ratio from 10:1 to 22:1 (limited the range via code)
- o2 sensor Temperature (similare to EGT)
- Oil Temperature
- Intake Air Temperature
- Relative Throttle Position
- Ignition timing
- Ignition timing corrections (Cly 1, 2, 3, 4) with peak values
- Charge voltage
I have also added resistive touch screen capabilities to reset peak values.
Next step?! Next steps!!So this isn't enough for me, I want something better - a cheaper BOM and a slimmer design.
The Teensy 4.1 was released not long ago, which is basically a Teensy 4.0 but with a lot of extra capabilities. One of those extras's in more PSRAM - 8mb in this case. It will allow me to create a bigger frame buffer for slightly better performance on the SPI bus and also buffer data for data-logging onto an SD card via the the 4.1's SD card slot.
I have also migrated to a Capacitive touch ILI9488 3.5" SPI displays as they are much cheaper and I can purchase them as bare displays (no breakout board on them) as well as moving to an automotive grade buck convertor (TPS560430)
I have started to design new PCB's for the above, so here is a screenshot of the design - everything is surface mounted, even the Teensy, while the display is attached to the opposite side of the PCB.
This will allow access to the Teensy's USB port from the bottom side, the SD card slot from the top side and a slightly more compact design/casing than I currently have.
I do apologize for not posting full code - its based on many libraries and blocks of sample code - it's always evolving and some of it is secret sauce ;) happy to guide anyone in the right direction though.
Thank you for reading! Feel free to comment with questions, suggestions and feedback!
Comments