Instrument Cluster Clock Gets The Show On The Road

While driving around one day, [Esko] noticed that the numbers and dials on a speedometer would be a pretty great medium for a clock build. This was his first project using a microcontroller, and with no time to lose he got his hands on the instrument cluster from a Fiat and used it to make a very unique timepiece.

The instrument cluster he chose was from a diesel Fiat Stilo, which [Esko] chose because the tachometer on the diesel version suited his timekeeping needs almost exactly. The speedometer measures almost all the way to 240 kph which works well for a 24-hour clock too. With the major part sourced, he found an Arduino clone and hit the road (figuratively speaking). A major focus of this project was getting the CAN bus signals sorted out. It helped that the Arduino clone he found had this functionality built-in (and ended up being cheaper than a real Arduino and shield) but he still had quite a bit of difficulty figuring out all of the signals.

In the end he got everything working, using a built-in servo motor in the cluster to make a “ticking” sound for seconds, and using the fuel gauge to keep track of the minutes. [Esko] also donated it to a local car museum when he finished so that others can enjoy this unique timepiece. Be sure to check out the video below to see this clock in action, and if you’re looking for other uses for instrument clusters that you might have lying around, be sure to check out this cluster used for video games.

The mechanics in dashboards are awesome, and produced at scale. That’s why our own [Adam Fabio] is able to get a hold of that type of hardware for his Analog Gauge Stepper kit. He simply adds a 3D printed needle, and a PCB to make interfacing easy.

24 thoughts on “Instrument Cluster Clock Gets The Show On The Road

    1. The numbers don’t, but the scale itself does. There are of course better options in that regard (exactly 6000 rpm and 240 km/h marked, speeds like 150 also marked etc, for example Ford Mondeo, which is also well documented), but the ones I could source locally required analog signals to interface, which didn’t really suit my fancy. Maybe next time I’ll pick one of those and get the perfect scale. But it will also be quite a different challenge altogether. As a side note, I have a BMW cluster lying around (which I bought out of desperation when the Fiat one seemed impossible to get working), that has the correct scale and requires BOTH digital and analog signals.

        1. The numbers are only written for even speeds (20, 40, 60 etc), but there’s a marker for 230 shown on the dial exactly like 210, 190 etc. Since the clock is digitally driven it needs only to point directly at the correct time period, not slowly increase the position of the hour hand between two points as the hour progresses.

          1. There are clusters with “odd hours” marked, but they are much rarer. I considered ponting the hour only at the stops and not in between, but in the end decided to go with a more traditonal look.

        1. Yes, I noticed also that the motors in the BMW cluster made by VDO are virtually entirely quiet, while the ones in the Fiat one by Visteon make a loud whirring noise. But this enables me to have the ticking sound for the clock.

          1. I can’t reply to your comment, so i’m replying to my own. So, if I understand correctly, you’re replacing the original dashboards with this? And also, what else have you turned these into?

  1. GREAT CLOCK! I had to alter his code to get it working. moved “void sendData” to after “void setup”

    CAN BUS library download link [ ]

    // Esko Haas CAN BUS car dash clock




    #define ENGINE_SPEED_FACTOR 32
    #define VEHICLE_SPEED_FACTOR 0.10625

    #define SEND_INTERVAL 1000

    #define HOURS_BUTTON 3 // #2 is taken by CAN-BUS shield
    #define MINUTES_BUTTON 4

    struct {
    struct {
    int hours;
    int minutes;
    int seconds;
    int day;
    int month;
    int year;
    } timeData;
    } dataIn;

    struct {
    int fuelLevel;
    int rpm;
    union {
    short value;
    struct {
    unsigned short low : 8;
    unsigned short high : 5;
    } bytes;
    } speedData;
    int waterTemperature;
    } dataOut;

    unsigned long previousSendTime;
    int previousMinutes;
    int waterTemperatureDelta = WATER_TEMPERATURE_DELTA;

    int hoursButtonState = HIGH;
    int hoursButtonLastState = HIGH;

    int minutesButtonState = HIGH;
    int minutesButtonLastState = HIGH;

    MCP_CAN CAN(10);

    void setup() {


    if (CAN_OK == CAN.begin(CAN_50KBPS)) {
    Serial.println(“CAN BUS Shield init ok!”);
    else {
    Serial.println(“CAN BUS Shield init fail”);
    Serial.println(“Init CAN BUS Shield again”);
    goto START_INIT;

    digitalWrite(HOURS_BUTTON, HIGH); // inverted logic in order to use internal pull-up resistors

    digitalWrite(MINUTES_BUTTON, HIGH);

    void sendData(int id, int len, …) {
    va_list args;
    va_start(args, len);
    unsigned char data[len];
    for (int i = 0; i < len; i++) {
    data[i] = va_arg(args, int);
    CAN.sendMsgBuf(id, 0, len, data);

    int fromPseudoHex(int val) {
    return String(val, HEX).toInt();

    int toPseudoHex(int val) {
    return strtoul(&String(val)[0], 0, 16);

    void loop() {
    unsigned long elapsedTime = millis() – previousSendTime;

    hoursButtonState = digitalRead(HOURS_BUTTON);
    minutesButtonState = digitalRead(MINUTES_BUTTON);

    unsigned char hoursButtonPressed = (LOW == hoursButtonState && hoursButtonLastState != hoursButtonState);
    unsigned char minutesButtonPressed = (LOW == minutesButtonState && minutesButtonLastState != minutesButtonState);

    if (hoursButtonPressed || minutesButtonPressed) {
    if (hoursButtonPressed) {
    if (23 < dataIn.timeData.hours) {
    dataIn.timeData.hours = 0;

    if (minutesButtonPressed) {
    if (59 elapsedTime) {
    // Serial.println(“sending data”);

    waterTemperatureDelta *= -1;

    if (dataIn.timeData.minutes != previousMinutes) {
    dataIn.timeData.seconds = 0;
    else {

    dataOut.fuelLevel = 100 * (60 – dataIn.timeData.seconds) / 60;
    dataOut.rpm = 100 * dataIn.timeData.minutes / ENGINE_SPEED_FACTOR;
    dataOut.speedData.value = 10 * (dataIn.timeData.hours + (float)dataIn.timeData.minutes / 60) / VEHICLE_SPEED_FACTOR;
    dataOut.waterTemperature = MEAN_WATER_TEMPERATURE + waterTemperatureDelta;

    // sendData(0x6e7, 5, 0x92, 0x32, 0x05, 0x50, 0x03); // settings
    // sendData(0x6e7, 5, 0x00, 0x00, 0x00, 0x00, 0x00); // settings

    sendData(0x180, 6,
    00, 00, 00, 00, 00, 00

    sendData(0x281, 8,
    00, 00, 00, dataOut.waterTemperature, 00, 00, dataOut.rpm, 00

    sendData(0x286, 8,
    00, 00, dataOut.speedData.bytes.high, dataOut.speedData.bytes.low, 00, 00, 00, 00

    sendData(0x2a0, 4,
    dataOut.speedData.bytes.high, dataOut.speedData.bytes.low, 00, 00

    sendData(0x380, 8,
    INSTRUMENT_LIGHTING_ON, 00, 00, 00, 00, dataOut.fuelLevel, 00, 00

    previousSendTime = millis();
    previousMinutes = dataIn.timeData.minutes;

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.