透過 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
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
/* ************************************************************************************************************** */
其他
經過實際驗證, 確實是經由量測 fuel level sensor 的電阻值判斷油面高度:
參考資料
Reverse engineering the RX-8’s instrument cluster, part onehttps://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! |