Hi, I'm all good , I hope you are too.
It seems to be a common fault on the L400. All the secondhand instrument clusters either have the same fault or have far too many kilometers on them.
The LCD in the instrument panel can't be replaced so you have to put the replacement in an enclosure and mount it somewhere on the dash. There are a few mods you have to make to hook it all up. The speed sensor is on the front of the gearbox and has a red/yellow wire running to the dash. The sensor generates 2550 pulses per kilometre.
There is a good instruction of the Delica Club website showing how to remove the cluster and the speedo pulse connection is shown in the photo below.
You will need to make up a circuit board with a 12v to 5v regulator on it. I use an LM7805 which is overkill - it can supply 1A @ 5v. I tried an LM78L05 it can supply 100mA but the ESP32 draws more than that at startup so it caused problems. In addition you will need a Schmidt trigger (I use a Texas Instruments74C14 IC) to convert the pulse sine wave to a square wave. I use that to trigger an ISR on the rising edge to increment the pulse counter. You will also need an FRAM breakout board to store the pulse count - any other type of memory will 'wear out' in about 10,000K.
The TTGO was handy being in a single package but I have gone to an ESP32 D1 Mini and a TZT LCD 1.3" connected via SPI. It's also cheaper.
There is plenty of room on the FRAM to store service data and I intend to display prompts for service in the blue square below the odometer readings on startup.
I have two versions of the sketch:
ESP32 D1 Mini
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include <FRAM.h>
#include <TFT_eSPI.h>
#include <SPI.h>
#include <Wire.h>
TFT_eSPI tft = TFT_eSPI(); // Invoke custom library
char buff[512];
int vref = 1100;
int btnCick = false;
// I2C stuff
const gpio_num_t GPIO21SDA = GPIO_NUM_21;
const gpio_num_t GPIO22SCL = GPIO_NUM_22;
FRAM fram = FRAM();
#define FRAM_ADDR 0x50 // the default address!
uint16_t addr = 2;
unsigned int tusec;
bool bFirst = true; // Used to display info on first loop only
struct SavedData {
unsigned int iPulses;
unsigned int iODO;
unsigned int iTrip;
};
//struct SavedData Odometer = {1908790 * 255, 1908790, 0};
struct SavedData Odometer;
int num = 0;
String sOdo = "";
String sTrip = "";
void IRAM_ATTR GPIO_ISR()
{
Odometer.iPulses++;
}
// Speedo pulse stuff
unsigned int duration;
int PulseCore = 0;
int iNeg = 0;
boolean bUpdateDisp = true;
boolean bResetTrip = false;
static int PulsePin = 35;
static int TripResetPin = 33;
static int TestModePin = 34;
long timeout = 1000000; //One second
int triped; // trip reset duration
bool bTrip = true;
int testMode; // test mode duration
bool bTest = false;
void Task_Pulse( void * pvParameters ) {
static bool bFirst = true;
static unsigned int iOldPulses = 0;
//handle pulses
Serial.print("Pulse task running on Core: ");
Serial.println(xPortGetCoreID());
while (true) {
if (Odometer.iPulses != iOldPulses) {
Serial.print("new pulse count: ");
Serial.println(Odometer.iPulses);
iOldPulses = Odometer.iPulses;
if (!(Odometer.iPulses % 255)) {
Odometer.iODO++;
Odometer.iTrip++;
bUpdateDisp = true;
// Serial.println("Pulse 255");
}
// Write to FRAM via I2C
num = fram.writeObject(addr, Odometer);
} else {
delay(5);
}
}
}
void Trip_Reset( void * pvParameters ) {
//handle trip reset
delay(200);
Serial.print("Trip reset task running on Core: ");
Serial.println(xPortGetCoreID());
while (true) {
triped = digitalRead(TripResetPin);
if (triped == 0) {
if (bTrip) {
Serial.println("Trip reset pressed.");
bTrip = false;
Odometer.iTrip = 0;
bUpdateDisp = true;
}
} else {
bTrip = true;
}
delay(300);
}
}
void Task_Disp( void * pvParameters ) {
//handle Disp[lay
Serial.print("Display task running on Core: ");
Serial.println(xPortGetCoreID());
while (true) {
if (bUpdateDisp) {
bUpdateDisp = false;
tft.setTextColor(TFT_GREEN, TFT_BLACK);
sOdo = format_Num(Odometer.iODO);
// long tus = -micros();
tft.drawString(sOdo, tft.width() / 2,50);
tft.setTextColor(TFT_BLUE, TFT_BLACK);
sTrip = Pad_Trip(Odometer.iTrip);
tft.drawString(sTrip, tft.width() / 2,110);
// tus += micros();
}
delay(70);
}
}
void setup() {
Serial.begin(115200);
while (!Serial) {delay(100);}
Serial.print("setup() running on core ");
Serial.println(xPortGetCoreID());
Serial.println();
Serial.println("ESP32_Odometer");
pinMode(PulsePin, INPUT);
pinMode(TripResetPin, INPUT);
pinMode(TestModePin, INPUT);
pinMode(LED_BUILTIN, OUTPUT);
flash(2, 200);
Serial.println("Attaching interupt: RISING");
attachInterrupt(PulsePin, GPIO_ISR, RISING);
Serial.println("Attached interupt: RISING");
//Initialise screen
Serial.println("Initialising tft");
tft.init();
tft.setRotation(0);
tft.setCursor(0, 0);
tft.setTextDatum(MC_DATUM);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.fillScreen(TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.setTextSize(3);
tft.drawLine(0, 0, 239, 0, TFT_RED);
tft.drawLine(239, 0, 239, 134, TFT_RED);
tft.drawLine(239, 134, 0, 134, TFT_RED);
tft.drawLine(0, 134, 0, 0, TFT_RED);
tft.drawLine(0,141, 239, 141, TFT_BLUE);
tft.drawLine(239, 141, 239, 239, TFT_BLUE);
tft.drawLine(239, 239, 0, 239, TFT_BLUE);
tft.drawLine(0, 239, 0, 141, TFT_BLUE);
Start_FRAM();
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.drawString("Odometer", tft.width() / 2,17);
tft.setTextColor(TFT_BLUE, TFT_BLACK);
tft.drawString("Trip", tft.width() / 2,78);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
Serial.println("Initialised tft");
delay(200);
Serial.println("Display Task Starting.");
xTaskCreatePinnedToCore(
Task_Disp,
"Display",
10000,
NULL,
1,
NULL,
1);
Serial.println("Display created OK");
Serial.println("Trip reset Task Starting.");
xTaskCreatePinnedToCore(
Trip_Reset,
"Trip reset",
1000,
NULL,
1,
NULL,
1);
Serial.println("Trip_Reset created OK");
Serial.println("Pulse Task Starting.");
xTaskCreatePinnedToCore(
Task_Pulse, /* Function to implement the task */
"Task_Pulse", /* Name of the task */
10000, /* Stack size in words */
NULL, /* Task input parameter */
0, /* Priority of the task */
NULL, /* Task handle. */
PulseCore); /* Core where the task should run */
Serial.println("Task_Pulse created OK");
}
void loop() {
if (bFirst) {
Serial.print("loop() running on core ");
Serial.println(xPortGetCoreID());
bFirst = false;
}
testMode = digitalRead(TestModePin);
if (testMode == 0) {
//Toggle between test mode and real mode
Serial.println("Test mode toggled");
bTest = !bTest;
delay(500);
while (digitalRead(TestModePin) == 0) delay(300);
}
if (bTest) {
Odometer.iPulses++;
// tusec = micros();
num = fram.writeObject(addr, Odometer);
// tusec = micros() - tusec;
// Serial.print("FRAM write time: ");
// Serial.print(tusec);
// Serial.println(" µSec.");
if (!(Odometer.iPulses % 255)) {
Odometer.iODO++;
Odometer.iTrip++;
bUpdateDisp = true;
// Serial.println("Pulse 255");
}
}
delay(5);
}
String Pad_Trip ( unsigned int iTrip) {
String s = format_Num(iTrip);
int i = s.length();
while (i++ < 9) {
s = " " + s;
}
return s;
}
void Start_FRAM () {
Wire.begin();
Wire.setClock(1000000);
if (fram.begin(FRAM_ADDR) == 0) {
Serial.println("Found I2C FRAM");
// Write is for initial setup only.
//**Remove after testing
// num = fram.writeObject(0x00, addr);
// tusec = -micros();
// num = fram.writeObject(addr, Odometer);
// tusec += micros();
// Serial.print("Write time = ");
// Serial.print(tusec);
// Serial.println(" µSec.");
//
// Serial.print("Wrote Odometer string with ");
// Serial.print(num);
// Serial.println(" bytes.");
fram.readObject(0x00, addr);
tusec = -micros();
fram.readObject(addr, Odometer);
tusec += micros();
Serial.print("Read time = ");
Serial.print(tusec);
Serial.println(" µSec.");
Serial.print("Read Odometer string value: ");
Serial.print(Odometer.iPulses);
Serial.print("; ");
Serial.print(Odometer.iODO);
Serial.print("; ");
Serial.println(Odometer.iTrip);
} else {
Serial.println("I2C FRAM not found ... check your connections?\r\n");
tft.setTextSize(4);
tft.setTextColor(TFT_RED, TFT_BLACK);
tft.drawString("FRAM ERROR", tft.width() / 2,70);
while (1) delay(10);
}
}
//Comma format of decimal to one decimal place
String format_Num (unsigned int num) {
int rem;
int quotient;
String sNum ="";
if (num < 10) {
return "0." + String(num);
} else {
rem = num % 10;
sNum = "." + String(rem);
quotient = num /10;
while (quotient > 1000) {
rem = quotient % 1000;
sNum = "," + String(rem) + sNum;
quotient = quotient /1000;
}
return quotient + sNum;
}
}
void flash1 (int duration) {
digitalWrite(LED_BUILTIN, HIGH); // sets the LED on
delay(duration); // waits for duration
digitalWrite(LED_BUILTIN, LOW); // sets the LED off
delay(duration); // waits for duration
}
void flash (int flashes, int duration) { // MaX 1 second flash
for (int x = 0; x < flashes; x++) {
flash1(duration);
}
}
and The normal ESP32 DevKit 1
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include <driver/i2c.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <splash.h>
#include <Adafruit_FRAM_I2C.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
Adafruit_FRAM_I2C fram = Adafruit_FRAM_I2C();
bool bFirst = true; // Used to display info on first loop only
struct SavedData {
unsigned int iPulses;
unsigned int iODO;
unsigned int iTrip;
};
struct SavedData Odometer = {1908790 * 255, 1908790, 0};
int num = 0;
String sOdo = "";
String sTrip = "";
// Speedo pulse stuff
int PulseCore = 0;
int iNeg = 0;
boolean bUpdateDisp = true;
boolean bResetTrip = false;
static int PulsePin = 15;
static int TripResetPin = 13;
long timeout = 1000000; //One second
unsigned long duration; // pulse duration
unsigned long triped; // trip reset duration
// I2C stuff
const gpio_num_t GPIO21SDA = GPIO_NUM_21;
const gpio_num_t GPIO22SCL = GPIO_NUM_22;
const int LEDpin = 2; // LED pin
void Task_Pulse( void * pvParameters ) {
//handle ODO pulses
Serial.print("Pulse task running on Core: ");
Serial.println(xPortGetCoreID());
while (true) {
duration = pulseIn(PulsePin, HIGH, timeout);
if (duration > 0) {
Odometer.iPulses++;
if (!(Odometer.iPulses % 255)) {
Odometer.iODO++;
Odometer.iTrip++;
bUpdateDisp = true;
Serial.println("Pulse 255");
}
// Write to EEPROM or FRAM vis I2C
num = fram.writeObject(0x00, Odometer);
Serial.print("Wrote Odometer string with ");
Serial.print(num);
Serial.println(" bytes");
} else {
delay(1);
}
}
}
void Task_Disp( void * pvParameters ) {
//handle Disp[lay
Serial.print("Display task running on Core: ");
Serial.println(xPortGetCoreID());
while (true) {
if (bUpdateDisp) {
bUpdateDisp = false;
//Serial.println("Clearing display");
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 5);
display.println("Odometer and Trip");
display.setTextSize(2);
//iOdo = iPulses / 255;
sOdo = String(Odometer.iODO / 10) + "." + String(Odometer.iODO % 10);
display.setCursor(20, 17);
display.print(sOdo);
sTrip = Pad_Trip(Odometer.iTrip);
display.setCursor(20, 39);
display.print(sTrip);
display.display();
}
delay(100);
}
}
void setup() {
Serial.begin(115200);
while (!Serial) {}
Serial.print("setup() running on core ");
Serial.println(xPortGetCoreID());
Serial.println();
Serial.println("ESP32_Odometer");
pinMode(PulsePin, INPUT);
pinMode(TripResetPin, INPUT);
pinMode(LEDpin, OUTPUT);
flash(2, 200);
if (fram.begin()) {
Serial.println("Found I2C FRAM");
// Write is for initial setup only.
num = fram.writeObject(0x00, Odometer);
Serial.print("Wrote Odometer string with ");
Serial.print(num);
Serial.println(" bytes.");
fram.readObject(0x00, Odometer);
Serial.print("Read back string value: ");
Serial.print(Odometer.iPulses);
Serial.print("; ");
Serial.print(Odometer.iODO);
Serial.print("; ");
Serial.println(Odometer.iTrip);
} else {
Serial.println("I2C FRAM not identified ... check your connections?\r\n");
Serial.println("Will loop in case this processor doesn't support repeated start\r\n");
while (1);
}
Serial.println("Connecting to display");
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
Serial.println("Display connected to I2C");
delay(200); // Let things settle down
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
bUpdateDisp = true;
Serial.println("Display Task Starting.");
xTaskCreatePinnedToCore(
Task_Disp,
"Display",
10000,
NULL,
1,
NULL,
1);
Serial.println("Display created OK");
Serial.println("Pulse Task Starting.");
xTaskCreatePinnedToCore(
Task_Pulse, /* Function to implement the task */
"Task_Pulse", /* Name of the task */
10000, /* Stack size in words */
NULL, /* Task input parameter */
0, /* Priority of the task */
NULL, /* Task handle. */
PulseCore); /* Core where the task should run */
Serial.println("Task_Pulse created OK");
}
void loop() {
if (bFirst) {
Serial.print("loop() running on core ");
Serial.println(xPortGetCoreID());
bFirst = false;
}
// Test for trip reset button pressed
triped = pulseIn(TripResetPin, HIGH, 3000);
if (triped > 0) {
Serial.println("Trip reset pressed.");
Odometer.iTrip = 0;
bUpdateDisp = true;
}
delay(3000);
//Test fiddle
Odometer.iPulses += 255;
bUpdateDisp = true;
}
String Pad_Trip (int iTrip) {
String s = String(iTrip / 10) + "." + String(iTrip % 10);
int i = s.length();
while (i++ < 8) {
s = " " + s;
}
return s;
}
void flash1 (int duration) {
digitalWrite(LEDpin, HIGH); // sets the LED on
delay(duration); // waits for duration
digitalWrite(LEDpin, LOW); // sets the LED off
delay(duration); // waits for duration
}
void flash (int flashes, int duration) { // MaX 1 second flash
for (int x = 0; x < flashes; x++) {
flash1(duration);
}
}
Feel free to use it if you wish and alter it in any way it suits you.
PS. You will need to setup a custom library for the TZT to specify the screen params for whatever LCD you choose. If you use the same LCD as me I will send you the custom library changes.