/* OBDIITach.ino - Uses CAN transceiver and 5" TFT LCD display to read rpm, engine temp, and
inverter temp from OBDII port, and display as dial-gauge tachometer with temp gauges.
dual CAN transceiver from pjrc.com store
RAIO 5" TFT LCD display from buydisplay.com store, configured for 16 bit parallel, reconfigured
for 4-wire SPI. Also configured to use 5v for VDD, while like teensy, signal I/O is 3.3v.
Kurt Funderburg - Jul 2025
* Windows don't do all I thought they did. setActiveWindow only contains text/graphics which start
inside the window. They do not implement their own coord system. Modified to use screen coords
instead of "window coords".
*** THIS IS 29 BIT CAN ID VERSION ***
*/
#include <FlexCAN_T4.h>
//CAN request schedule, timeout
#define CAN_RD_TIMEOUT 100 // ms (10-20mS typical, 100 is good threshold)
FlexCAN_T4<CAN1, RX_SIZE_256, TX_SIZE_16> can1;
uint16_t currentPID = 0x00;
elapsedMillis replyTimer; //timeout
enum RequestState { IDLE, WAITING_FOR_RESPONSE };
RequestState requestState = IDLE;
enum PIDType { EMPTY, RPM, COOLANT_TEMP, INVERTER_TEMP };
PIDType pendingRequest = EMPTY;
CAN_message_t initCanMsg, itempCanMsg;
uint rt; // real-time testing
void setup() {
// Serial.begin(115200);
// while (!Serial);
// if (Serial) Serial.println("Serial started.");
// Initialize CAN connection
can1.begin();
can1.setBaudRate(500000);
delay(1000);
//can1.setMBFilter(REJECT_ALL);
//can1.setMBFilterRange(MB1, 0x18DAF100, 0x18DAF110);
// can1.setMBFilterRange(MB1, 0x7E8, 0x7EF);
// Serial.println("CAN OBD-II Reader Started");
// * Itemp CAN request message NOTE: 1 or 2 other ECTs from Mode 1, PID x67 req.
// * The diagnostic reader initiates a query using CAN ID 7DFh, which acts as a broadcast address,
// and accepts responses from any ID in the range 7E8h to 7EFh. ECUs that can respond to OBD queries
// listen both to the functional broadcast ID of 7DFh and one assigned ID in the range 7E0h to 7E7h.
// Their response has an ID of their assigned ID plus 8 e.g. 7E8h through 7EFh.
// * This establishes a diagnostic session with the ECU and is usually necessary before any other
// commands can be sent. IDH: 07, IDL: E0, Len: 08, Data: 02 10 03 00 00 00 00 00
// * 29-bit ID wake-up message worked on a 2020 Civic (followed by 29-b ID RPM request)
// ID: 0x18DB33F1 OVERRUN: 0 MSG: 0x02 0x10 0x03 0x00 0x00 0x00 0x00 0x00
initCanMsg.id = 0x18DB33F1; // 29b ID equiv. to 7DF.
initCanMsg.len = 8;
initCanMsg.flags.extended = 1;
initCanMsg.flags.remote = 0;
initCanMsg.buf[0] = 2;
initCanMsg.buf[1] = 0x10;
initCanMsg.buf[2] = 0x3; //CAN ECU (gateway?) init
memset(&initCanMsg.buf[3], 0xCC, 5);
can1.write(initCanMsg); //Not sure I need this, since Mike got his 2020 Civic working w/o it.
delay(100);
itempCanMsg.id = 0x18DB33F1; // ChatGPT sez 7E0, or possibly 7E1 targets ECU on mode 22.
itempCanMsg.len = 8;
itempCanMsg.flags.extended = 1;
itempCanMsg.flags.remote = 0;
itempCanMsg.buf[0] = 2;
itempCanMsg.buf[1] = 1;
itempCanMsg.buf[2] = 0x5; //ECT
memset(&itempCanMsg.buf[3], 0xCC, 5);
pinMode(13, OUTPUT); //led init code
digitalWrite(13, 1);
delay(500);
digitalWrite(13, 0);
delay(2000);
} // setup()
void loop() {
// If no request is in progress, check if time to launch. If launching
// request, reset schedule and timeout timers, flag listening
if (requestState == IDLE && pendingRequest == EMPTY) {
can1.write(itempCanMsg);
currentPID = itempCanMsg.buf[2];
replyTimer = 0;
pendingRequest = INVERTER_TEMP;
requestState = WAITING_FOR_RESPONSE;
}
// Read CAN (int)round(RPM data/10), (int)round(Temp data/2)
// Read can msg, if valid and if listening, update corresponding gauge
CAN_message_t inMsg;
if (can1.read(inMsg)) {
digitalWrite(13, 1); //read a msg code on led
delay(250);
digitalWrite(13, 0);
delay(250);
digitalWrite(13, 1);
delay(250);
digitalWrite(13, 0);
delay(250);
// 18DAF110 targets F1 (used as source in request messages), and comes from ECU10 which I suspect is OBD gateway.
if (requestState == WAITING_FOR_RESPONSE && //inMsg.id == 0x18DAF110 &&
inMsg.buf[1] == 0x41 && inMsg.len >= 3) {
uint16_t pid;
pid = (inMsg.buf[2]);
if (pid == currentPID) { //if in msg pid same as last requested
showItemp(inMsg.buf[3] - 40);
requestState = IDLE;
pendingRequest = EMPTY;
}
}
}
// if reply timeout, reset state to resume request schedule
if(replyTimer >= CAN_RD_TIMEOUT && requestState == WAITING_FOR_RESPONSE) {
requestState = IDLE;
pendingRequest = EMPTY;
}
delay(10); // no need to scream through loop at 600MHz. Hopefully this will keep cpu cooler.
} // loop()
void showItemp(uint16_t val) {
for(int i=0; i<10; i++) { //quick-flash 10 times
digitalWrite(13,1);
delay(50);
digitalWrite(13,0);
delay(100);
}
delay(2000);
for(int i=0; i<16; i+=4) { //flash 4 bits at a time
for(int j=0; j<4; j++) {
if(((val << (i+j)) & 0x8000) == 0x8000) {
digitalWrite(13, 1);
delay(750);
digitalWrite(13,0);
delay(500);
} else {
digitalWrite(13, 1);
delay(100);
digitalWrite(13,0);
delay(1150);
}
}
delay(4000);
}
} //showItemp