Mazda RX8 instrument cluster - reverse engineering

透過 CAN Bus 控制 Mazda RX8 儀表


reverse engineering


Mazda RX-8 儀表總成



上個月從 eBay 弄了一顆 Mazda RX-8 的儀表板(instrument cluster)回來:



Teensy

剛開始很天真的想說 - Teensy 有支援 CAN bus 的功能, 那麼 cluster 到貨後就可以直接拿來玩了! 結果哪, 事情不是憨人想的那麼簡單... 還需要買顆 CAN Bus transceiver 來配合才行 -_-

在露天買了幾顆便宜的 NXP TJA1050T/CM(廣告時間 - 該賣家很熱心, 東西也不貴 :-))(datasheet) 回來, 再把相關測試電路設好後才可以開始測試:


pinout

硬體都就緒了, 剩下的就是軟體這部份; 不過, 因為每家汽車公司對於 CAN Bus 的定義差異非常大的關係(就算是同廠牌, 但是不同產品線也是有不同定義的情況), 所以就得靠 reverse engineering 才行...

還好, 現在網路非常便利、發達, 且大部分的人都樂意分享他們的成果, 藉由 Google 找到的資料東拼西湊就把大部分的的功能解出來了 ;-)

接線方式. 來源

詳細的接線方式

PIDs

目前除了油量表 & 動力方向盤、安全氣囊的狀態燈尚不知道如何透過  CAN Bus 控制外, 其餘像是轉速、車速、油壓(假的)、水溫、行駛距離、警示燈號這些基本的功能, 已經可以利用 CAN Bus 控制.

在 eBay 二手的 RX-8 儀表板實在便宜到爆! 雖然價格很便宜, 但視覺效果卻是非常棒! 新版的儀表板看起來更漂亮; 但因為當初想要舊版的油壓表做其他運用, 所以買了舊版的手排形式儀表板; 誰知道油壓表竟然只有 on/off 的區分(但這顆油壓表確實由 stepper motor 驅動, 只是 cluster 的軟體僅接受 0 & 1 的參數值), 根本不能設定壓力值! @_@

Mazda 當時可能要預留未來再把這個功能補回, 但是... 現在新版的儀表板已經將油壓表取消掉了!

source code

因為 git repo. 尚未設立的關係, 若有需要 RX8 的 CAN bus 功能測試程式, 請留言給我. ;-)
CAN Bus 的功能測試程式請於 https://github.com/jimkoeh/rx8 這裡取得; 以下是目前的程式碼:

#include <FlexCAN.h>

#define CANbaund    500000
#define LED        13

FlexCAN CANbus(CANbaund);

static CAN_message_t msg_tx;

int16_t ary_count[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
uint8_t ary_RPM[8] = { 0, 0, 0xFF, 0xFF, 0, 0, 0, 0 };
uint8_t ary_PCM[8] = { 0x04, 0x00, 0x28, 0x00, 0x02, 0x37, 0x06, 0x81 };
uint8_t ary_MIL[8] = { 0x98, 0, 0, 0, 1, 0, 0, 0 };
uint8_t ary_DSC[8] = { 0xFE, 0xFE, 0xFE, 0x34, 0, 0x40, 0, 0 };
uint8_t ary_RAN[8] = { 0x02, 0x2D, 0x02, 0x2D, 0x02, 0x2A, 0x06, 0x81 };
uint8_t ary_ECU[8] = { 0x0F, 0x00, 0xFF, 0xFF, 0x02, 0x2D, 0x06, 0x81 };
uint8_t ary_TCP[8] = { 0x00, 0x00, 0xCF, 0x87, 0x7F, 0x83, 0x00, 0x00 };
uint8_t ary_EPS[8] = { 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x32, 0x06, 0x81 };
uint8_t ary_CY[8] = { 0x89, 0x89, 0x89, 0x19, 0x34, 0x1F, 0xC8, 0xFF };
uint8_t ary_FL[8] = { 0x0A, 0x95, 0, 0, 0, 0xcc, 0, 0 };

/*
 * value    RPM
 * 980        1,000
 * 1,450    1,500
 * 1,920    2,000
 * 2,420    2,500
 * 2,890    3,000
 * 3,370    3,500
 * 3,840    4,000
 * 4,330    4,500
 * 4,800    5,000
 * 5,280    5,500
 * 5,770    6,000
 * 6,240    6,500
 * 6,720    7,000
 * 7,200    7,500
 * 7,670    8,000
 * 8,125    8,500
 * 8,580    9,000
 * 9,070    9,500
 * 9,520    10,000
 */
const uint16_t ary_map_RPM[38] = {
    1000,  980, 1500, 1450, 2000, 1920, 2500, 2420, 3000, 2890,
    3500, 3370, 4000, 3840, 4500, 4330, 5000, 4800, 5500, 5280,
    6000, 5770, 6500, 6240, 7000, 6720, 7500, 7200, 8000, 7670,
    8500, 8125, 9000, 8580, 9500, 9070, 10000, 9520
};

#ifdef DEBUG
static CAN_message_t msg_rx;
static uint8_t hex[17] = "0123456789abcdef";
#endif

/** **************************************************************************************************************
 * setup()
 */
void setup()
{
    Serial.begin(115200);

    delay(1000);

    Serial.println("Init...");

    pinMode(LED, OUTPUT);

    CANbus.begin();

    msg_tx.len = 8;

    Serial.println("OK!");
}

/** **************************************************************************************************************
 * loop()
 */
void loop()
{
    /*
     * ary_count[0] -> 較慢迴圈的計數器
     * ary_count[1] -> 預計增加的 trip 數(單位 KM)
     * ary_count[2] -> 目標 RPM
     * ary_count[3] -> 目標 RPM 的比對值
     * ary_count[4] -> 目標 Speed(KPH)
     * ary_count[5] -> 目標 Speed 的比對值
     * ary_count[6] -> 配合 RPM & Speed 使用的 "延遲" counter
     */

    if (ary_count[0] == 0)                            // *** 需要送出 "可較慢/少" 的 CAN messages?
    {
        if (Serial.available())
        {
            char ary_str[8];
            uint8_t idx = 0;

            ary_str[idx] = Serial.read();

            if (ary_str[idx] == 'T')                // *** temperture?
            {
                while (Serial.available())
                {
                    ary_str[idx++] = Serial.read();
                    ary_str[idx] = '\0';
                }

                ary_MIL[0] = atoi(ary_str);

                Serial.print("temperture: ");
                Serial.println(ary_MIL[0]);
            }
            else if (toupper(ary_str[idx]) == 'I')            // *** Trip distance?
            {
                while (Serial.available())
                {
                    ary_str[idx++] = Serial.read();
                    ary_str[idx] = '\0';
                }

                ary_count[1] = atoi(ary_str);

                Serial.print("Trip distance: ");
                Serial.print(ary_count[1]);

                ary_count[1] = ary_count[1] * 2560 + 1;

                Serial.print("KM, count: ");
                Serial.println(ary_count[1]);
            }
            else if (toupper(ary_str[idx]) == 'N')            // *** Check engine warning?
            {
                ary_str[0] = Serial.read();

                switch (ary_str[0])
                {
                    default :
                    case '0' :
                        ary_MIL[5] = 0;
                        break;

                    case '1' :
                        ary_MIL[5] = 0b01000000;
                        break;

                    case '2' :
                        ary_MIL[5] = 0b10000000;
                        break;
                }

                Serial.print("Check engine warning: ");
                Serial.println(ary_MIL[5], BIN);
            }
            else if (toupper(ary_str[idx]) == 'O')            // *** Oil Pressure?
            {
                if (ary_str[idx] == 'o')
                {
                    ary_MIL[4] = 1;
                    ary_MIL[6] &= 0b01111111;
                }
                else
                {
                    ary_MIL[4] = 0;
                    ary_MIL[6] |= 0b10000000;
                }

                Serial.print("Oil Pressure: ");
                Serial.print(ary_MIL[4], HEX);
                Serial.print(", ");
                Serial.println(ary_MIL[6], HEX);
            }
            else if (toupper(ary_str[idx]) == 'R')            // *** Bat charge warning?
            {
                if (ary_str[idx] == 'r')
                {
                    ary_MIL[6] &= 0b10111111;
                }
                else
                {
                    ary_MIL[6] |= 0b01000000;
                }

                Serial.print("Bat charge warning: ");
                Serial.println(ary_MIL[6], BIN);
            }
            else if (toupper(ary_str[idx]) == 'W')            // *** Low water warning?
            {
                if (ary_str[idx] == 'w')
                {
                    ary_MIL[6] &= 0b11111101;
                }
                else
                {
                    ary_MIL[6] |= 0b00000010;
                }

                Serial.print("Low water warning: ");
                Serial.println(ary_MIL[6], BIN);
            }
            else if (toupper(ary_str[idx]) == 'E')            // *** ETC status?
            {
                ary_DSC[6] = ary_str[idx] == 'e' ? 0b00000100 : 0b00001000;

                Serial.print("ETC status: ");
                Serial.println(ary_DSC[6], BIN);
            }
            else if (toupper(ary_str[idx]) == 'B')            // *** Brake warning?
            {
                if (ary_str[idx] == 'b')
                {
                    ary_DSC[4] &= 0b10111111;
                }
                else
                {
                    ary_DSC[4] |= 0b01000000;
                }

                Serial.print("Brake warning: ");
                Serial.println(ary_DSC[4], BIN);
            }
            else if (toupper(ary_str[idx]) == 'A')            // *** ABS warning?
            {
                if (ary_str[idx] == 'a')
                {
                    ary_DSC[4] &= 0b11110111;
                }
                else
                {
                    ary_DSC[4] |= 0b00001000;
                }

                Serial.print("ABS warning: ");
                Serial.println(ary_DSC[4], BIN);
            }
            else if (toupper(ary_str[idx]) == 'D')            // *** DSC & TCS warning?
            {
                if (ary_str[idx] == 'D')
                {
                    ary_DSC[3] = 0;
                    ary_DSC[5] = 0b00000000;
                }
                else
                {
                    ary_DSC[3] = 0x34;
                    ary_DSC[5] = 0x40;
                }

                Serial.print("DSC & TCS warning: ");
                Serial.print(ary_DSC[3], HEX);
                Serial.print(", ");
                Serial.println(ary_DSC[5], HEX);
            }
            else if (toupper(ary_str[idx]) == 'P')            // *** RPM?
            {
                while (Serial.available())
                {
                    ary_str[idx++] = Serial.read();
                    ary_str[idx] = '\0';
                }

                ary_count[2] = atoi(ary_str);
                ary_count[6] = 300;

                Serial.print("Target RPM: ");
                Serial.println(ary_count[2]);
            }
            else if (toupper(ary_str[idx]) == 'S')            // *** Speed?
            {
                while (Serial.available())
                {
                    ary_str[idx++] = Serial.read();
                    ary_str[idx] = '\0';
                }

                ary_count[4] = atoi(ary_str);
                ary_count[6] = 80;

                Serial.print("Target Speed: ");
                Serial.println(ary_count[4]);
            }
        }

        msg_tx.id = 0x200;
        memcpy(msg_tx.buf, &ary_EPS, 8);
        CANbus.write(msg_tx);

        msg_tx.id = 0x202;
        memcpy(msg_tx.buf, &ary_CY, 8);
        CANbus.write(msg_tx);

        msg_tx.id = 0x212;
        memcpy(msg_tx.buf, &ary_DSC, 8);
        CANbus.write(msg_tx);

        msg_tx.id = 0x215;
        memcpy(msg_tx.buf, &ary_RAN, 8);
        CANbus.write(msg_tx);

        msg_tx.id = 0x231;
        memcpy(msg_tx.buf, &ary_ECU, 8);
        CANbus.write(msg_tx);

        msg_tx.id = 0x240;
        memcpy(msg_tx.buf, &ary_PCM, 8);
        CANbus.write(msg_tx);

        msg_tx.id = 0x250;
        memcpy(msg_tx.buf, &ary_TCP, 8);
        CANbus.write(msg_tx);

        msg_tx.id = 0x420;
        memcpy(msg_tx.buf, &ary_MIL, 8);
        CANbus.write(msg_tx);

        msg_tx.id = 0x430;
        memcpy(msg_tx.buf, &ary_FL, 8);
        CANbus.write(msg_tx);

        if ((ary_count[1] =  ary_count[1] - 1) > 0)            // *** 有設定預計增加的 trip 數(單位 KM)?
        {
            /*
             * 根據實驗:
             * -每送出 43 次 10 即增加 100M 的距離
             * -每送出 22 次 20 即增加 100M 的距離
             * ->每單位 0.23M
             *
             * 似乎 ary_MIL[1] overflow 的時候就算 100M, 並非 1 單位是 0.23M 的樣子...
             * 依照這個方式計算, 每單位應為 0.390625M
             */
            ary_MIL[1] = ary_MIL[1] + 1;

            Serial.print("Trip distance count1: ");
            Serial.print(ary_count[1]);
            Serial.print(", count2: ");
            Serial.println(ary_MIL[1]);
        }

        digitalWrite(LED, !digitalRead(LED));
    }

    if ((ary_count[0] = ary_count[0] + 1) > 10)
    {
        ary_count[0] = 0;        
    }

    if (ary_count[2] > 0)                            // *** 指定 RPM?
    {
        uint16_t rpm = map_rpm(ary_count[3]);

        ary_RPM[0] = (rpm * 4) / 256;
        ary_RPM[1] = (rpm * 4) % 256;

        ary_count[3] = ary_count[3] + 20;

        if (ary_count[3] > ary_count[2])                // *** 已經超過指定轉速值?
        {
            ary_count[2] = 0;
            ary_count[3] = 1;
        }
    }
    else if (ary_count[3] == 1 && ary_count[6] > 0)
    {
        ary_count[6] = ary_count[6] - 1;

        if (ary_count[6] <= 0)
        {
            ary_count[3] = 0;
            ary_count[6] = 0;
            ary_RPM[0] = 0;
            ary_RPM[1] = 0;
        }
    }

    if (ary_count[4] > 0)                            // *** 指定 Speed?
    {
        /*
         * 根據實際的測試, RX8 的時速會比實際的多出 1 ~ 2 KMPH
         */

        ary_RPM[4] = (ary_count[5] * 100 + 10000) / 256;
        ary_RPM[5] = (ary_count[5] * 100 + 10000) % 256;

        ary_count[5] = ary_count[5] + 1;

        if (ary_count[5] > ary_count[4])                // *** 已經超過指定 Speed?
        {
            ary_count[4] = 0;
            ary_count[5] = 1;
        }
    }
    else if (ary_count[5] == 1 && ary_count[6] > 0)
    {
        ary_count[6] = ary_count[6] - 1;

        if (ary_count[6] <= 0)
        {
            ary_count[5] = 0;
            ary_count[6] = 0;
            ary_RPM[4] = 0;
            ary_RPM[5] = 0;
        }
    }

    msg_tx.id = 0x201;
    memcpy(msg_tx.buf, &ary_RPM, 8);
    CANbus.write(msg_tx);

#ifdef DEBUG
    if (CANbus.read(msg_rx))
    {
        hexDump(sizeof(msg_rx), (uint8_t *)&msg_rx);
    }
#endif

    delay(10);
}

/** **************************************************************************************************************
 * 回傳查表後的 RPM 值
 */
uint16_t map_rpm(uint16_t rpm)
{
    int idx = 38 - 2;

    if (rpm < ary_map_RPM[0] || rpm > ary_map_RPM[36])            // *** RPM too low/high?
        return rpm;

    for (int x = 0; x < idx; x += 2)
    {
        if (rpm >= ary_map_RPM[x]                    // *** Interpolate the lookup
            && rpm <= ary_map_RPM[x + 2])
        {
            return (rpm - ary_map_RPM[x]) * (ary_map_RPM[x + 3] - ary_map_RPM[x + 1]) / (ary_map_RPM[x + 2] - ary_map_RPM[x]) + ary_map_RPM[x + 1];
        }
    }
    
    return 0;
}

#ifdef DEBUG
/** **************************************************************************************************************
 * https://forum.pjrc.com/threads/24720-Teensy-3-1-and-CAN-Bus/page11
 */
static void hexDump(uint8_t dumpLen, uint8_t *bytePtr)
{
    uint8_t working;
    
    while (dumpLen--)
    {
        working = *bytePtr++;
        Serial.write(hex[working >> 4]);
        Serial.write(hex[working & 15]);
    }
    
    Serial.write('\r');
    Serial.write('\n');
}
#endif

/* ************************************************************************************************************** */


其他

!求助! 如果有人知道如何透過 CAN Bus 控制油量表的, 麻煩留言給我. 非常感謝!
根據找到的影片、資料看來, 似乎是直接連接 sensor, 不可由 CAN Bus 控制的樣子...
經過實際驗證, 確實是經由量測 fuel level sensor 的電阻值判斷油面高度:



參考資料

Reverse engineering the RX-8’s instrument cluster, part one
https://www.cantanko.com/rx-8/reverse-engineering-the-rx-8s-instrument-cluster-part-one/

DIY Removing Armor All from Instrument Cluster lenses :)
http://www.rx8club.com.au/forum/viewtopic.php?f=9&t=9207

CANbus Library for Teensy 3.1
https://github.com/teachop/FlexCAN_Library

Oh no, Odometer: Reading and Righting… er, Writing.
http://www.canbushack.com/blog/index.php?title=oh-no-odometer-reading-and-righting-er-writing&more=1&c=1&tb=1&pb=1

Message 00000400 as per the table above shows the trip computer information such as fuel consumption etc etc!
http://www.madox.net/blog/projects/mazda-can-bus/comment-page-1/

Mazda CAN ID
http://opengarages.org/index.php/Mazda_CAN_ID



(本 BKSPtw Blog 內任何一篇文章皆可自由轉載, 但是煩請註明出處並附上文章連結. 感謝!)

若各位朋友有任何保養/維修的需求, 歡迎來電或者留言詢問. Thanks!
較新的 較舊