본문 바로가기
아두이노

아두이노와 MCP2515를 통한 OBD2 차량정보 읽어 오기

by 구루가 되고픈 2022. 6. 3.

차량은 상당히 복잡한 기계장치로 작동에 필요한 각종 정보를 ECU에서 처리합니다.

차량의 상태를 파악하기 위해 상당히 많은 센서들이 실시간으로 차량의 상태를 모니터하고 있기도 합니다.

 

차량에서는 이러한 상태값들을 can-bus라는 통신 프로토콜을 통해 통신하고 있어서 이 프로토콜을 통해 자동차의 여러 센서값이나 상태를 확인할 수가 있습니다.

 

자동차 정비시 활용하는 차량용 스캐너 장비가 대표적인 예라고 할 수 있습니다.

 

또는 일부 운전자들이 저렴하게 구매해서 사용하는 obd2용 블루투스 장치도 이 can-bus신호를 분석하는 사례입니다.

 

혹시 차량에 시중에 판매하고 있는 hud장치를 구매해서 장착하셨다면 이것도 can-bus신호를 분석해서 표시해주는 것입니다.

 

차량용 HUD, 스캐너 장비

 

저는 오래전부터 차량용 아두이노로 차량용 대시보드를 제작해 왔고 최근에도 계속 재작업을 하고 있는 상황이었습니다.

 

제가 차량에서 OBD2정보를 가져오기 위해서 freematics사에서 판매하는 obd2 아답터를 구매하여 사용한 것이었습니다.

 

freematics사에서 판매하는 obd2 uart 아답터

 

이 아답터를 사용하는 것의 장점은 라이브러리만 설치하면 아주 단 몇줄의 코드만으로 차량정보를 가져올 수 있다는 것이었습니다.

 

그렇기 때문에 가져온 신호를 표시하거나 처리하는것만 구현하면 되기 때문에 코드도 간결해지고 무엇보다 obd2 신호를 분석해서 원하는 정보를 가져오는데 필요한 노력과 수고를 들이지 않아도 된다는 것이었습니다.

 

아래 코드를 보면 주석을 제외하면 단 몇줄의 코드만으로 RPM값을 가져옵니다.

 

간결한 소스코드

 

최근에 차량용 대시보드를 다시 만들면서 이 아답터가 고장이 났고, 이 아답터를 다시 주문하기에는 시간과 돈을 다시 투자해야 해서 직접 신호를 처리해서 읽어오는 작업을 하였습니다.

 

먼저 차량 OBD2 커넥터의 can-bus신호를 분석하기 위해서는 신호처리를 위한 모듈이나 쉴드가 있어야 합니다.

 

일반적으로 많이 사용하는 것이 MCP2515 칩셋을 사용하는 드라이버 모듈입니다.

 

MCP2515

이 모듈의 강점은 무엇보다 가격이 3천원 정도에 불과하다는 것입니다.

 

신호처리는 8MHZ와 16MHZ 2종류가 있으니 그건 오실레이터(위 사진의 은색 타원형 부품)를 보시고 구분하시면 되겠습니다.

 

연결은 SPI로 하기 때문에 모든 아두이노 보드에서 사용이 가능합니다.

 

저는 테스트를 위해 micro pro 보드에 연결하였습니다.

 

MCP2515를 micro pro 보드에 연결

 

아두이노 보드마다 SPI 핀번호가 다르기 때문에 연결시 잘 찾아서 주의해서 연결합니다.

 

그리고 INT 단자는 아두이노 보드마다 interrupt핀이 지정되어 있으니 역시나 핀맵을 보시고 찾아서 연결해주고, CS는 남는 디지털핀에 연결해 줍니다.

 

MCP2515보드를 통해서 신호를 처리하기 위해서는 우선 라이브러리를 설치해주어야 하는데 아래 URL의 라이브러리를 설치합니다.

 

https://github.com/coryjfowler/MCP_CAN_lib

 

GitHub - coryjfowler/MCP_CAN_lib: MCP_CAN Library

MCP_CAN Library. Contribute to coryjfowler/MCP_CAN_lib development by creating an account on GitHub.

github.com

 

주의할 점은 MCP2515 라이브러리가 상당히 구글링을 해보면 상당히 여러종류가 검색됩니다.

 

다른 라이브러리로도 충분히 처리가 가능하겠지만 아래 코드를 실행하기 위해서는 꼭 위의 라이브러리로 설치하시기 바랍니다.

MCP_CAN_lib-master.zip
0.05MB

 

링크가 안될 경우는 위 파일을 다운로드 하시면 됩니다.

 

차량정보를 가져오기 위한 기본코드는 아래와 같습니다.

 

// Service 01 PIDs (more detail: https://en.wikipedia.org/wiki/OBD-II_PIDs)
#define PID_ENGINE_LOAD 0x04
#define PID_COOLANT_TEMP 0x05
#define PID_SHORT_TERM_FUEL_TRIM_1 0x06
#define PID_LONG_TERM_FUEL_TRIM_1 0x07
#define PID_SHORT_TERM_FUEL_TRIM_2 0x08
#define PID_LONG_TERM_FUEL_TRIM_2 0x09
#define PID_FUEL_PRESSURE 0x0A
#define PID_INTAKE_MAP 0x0B
#define PID_ENGINE_RPM  0x0C
#define PID_VEHICLE_SPEED 0x0D
#define PID_TIMING_ADVANCE 0x0E
#define PID_INTAKE_TEMP 0x0F
#define PID_MAF_FLOW 0x10
#define PID_THROTTLE 0x11
#define PID_AUX_INPUT 0x1E
#define PID_RUNTIME 0x1F
#define PID_DISTANCE_WITH_MIL 0x21
#define PID_COMMANDED_EGR 0x2C
#define PID_EGR_ERROR 0x2D
#define PID_COMMANDED_EVAPORATIVE_PURGE 0x2E
#define PID_FUEL_LEVEL 0x2F
#define PID_WARMS_UPS 0x30
#define PID_DISTANCE 0x31
#define PID_EVAP_SYS_VAPOR_PRESSURE 0x32
#define PID_BAROMETRIC 0x33
#define PID_CATALYST_TEMP_B1S1 0x3C
#define PID_CATALYST_TEMP_B2S1 0x3D
#define PID_CATALYST_TEMP_B1S2 0x3E
#define PID_CATALYST_TEMP_B2S2 0x3F
#define PID_CONTROL_MODULE_VOLTAGE 0x42
#define PID_ABSOLUTE_ENGINE_LOAD 0x43
#define PID_AIR_FUEL_EQUIV_RATIO 0x44
#define PID_RELATIVE_THROTTLE_POS 0x45
#define PID_AMBIENT_TEMP 0x46
#define PID_ABSOLUTE_THROTTLE_POS_B 0x47
#define PID_ABSOLUTE_THROTTLE_POS_C 0x48
#define PID_ACC_PEDAL_POS_D 0x49
#define PID_ACC_PEDAL_POS_E 0x4A
#define PID_ACC_PEDAL_POS_F 0x4B
#define PID_COMMANDED_THROTTLE_ACTUATOR 0x4C
#define PID_TIME_WITH_MIL 0x4D
#define PID_TIME_SINCE_CODES_CLEARED 0x4E
#define PID_ETHANOL_FUEL 0x52
#define PID_FUEL_RAIL_PRESSURE 0x59
#define PID_HYBRID_BATTERY_PERCENTAGE 0x5B
#define PID_ENGINE_OIL_TEMP 0x5C
#define PID_FUEL_INJECTION_TIMING 0x5D
#define PID_ENGINE_FUEL_RATE 0x5E
#define PID_ENGINE_TORQUE_DEMANDED 0x61
#define PID_ENGINE_TORQUE_PERCENTAGE 0x62
#define PID_ENGINE_REF_TORQUE 0x63
//----------------------------------------------

#define CAN_ID_PID 0x7DF //OBD-II CAN frame ID

#include <mcp_can.h>
#include <SPI.h>

#define CAN0_INT 2                              // Set INT to pin 2  <--------- CHANGE if using different pin number
MCP_CAN CAN0(10);                               // Set CS to pin 10 <--------- CHANGE if using different pin number

long unsigned int rxId;
unsigned char len = 0;
unsigned char rxBuf[8];
char msgString[128];                        // Array to store serial string

void sendPID(unsigned char __pid)
{
  unsigned char tmp[8] = {0x02, 0x01, __pid, 0, 0, 0, 0, 0};

  byte sndStat = CAN0.sendMsgBuf(CAN_ID_PID, 0, 8, tmp);

  if (sndStat == CAN_OK) {
    Serial.print("PID sent: 0x");
    Serial.println(__pid, HEX);
  }
  else {
    Serial.println("Error Sending Message...");
  }
}

void receivePID(unsigned char __pid)
{
    if (!digitalRead(CAN0_INT)) {                      // If CAN0_INT pin is low, read receive buffer
    CAN0.readMsgBuf(&rxId, &len, rxBuf);      // Read data: len = data length, buf = data byte(s)

    sprintf(msgString, "Standard ID: 0x%.3lX, DLC: %1d, Data: ", rxId, len);
    Serial.print(msgString);

    for (byte i = 0; i < len; i++) {
      sprintf(msgString, " 0x%.2X", rxBuf[i]);
      Serial.print(msgString);
    }
    Serial.println("");


    switch (__pid) {
      case PID_COOLANT_TEMP:
        if(rxBuf[2] == PID_COOLANT_TEMP){
          uint8_t temp;
          temp = rxBuf[3] - 40;
          Serial.print("Engine Coolant Temp (degC): ");
          Serial.println(temp, DEC);
        }
      break;

      case PID_ENGINE_RPM:
        if(rxBuf[2] == PID_ENGINE_RPM){
          uint16_t rpm;
          rpm = ((256 * rxBuf[3]) + rxBuf[4]) / 4;
          Serial.print("Engine Speed (rpm): ");
          Serial.println(rpm, DEC);
        }
      break;
    }
  }
}

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

  // Initialize MCP2515 running at 16MHz with a baudrate of 500kb/s and the masks and filters disabled.
  if (CAN0.begin(MCP_STDEXT, CAN_500KBPS, MCP_16MHZ) == CAN_OK) { //< -------- - CHANGE if using different board
    Serial.println("MCP2515 Initialized Successfully!");
  }
  else {
    Serial.println("Error Initializing MCP2515...");
    while (1);
  }

  //initialise mask and filter to allow only receipt of 0x7xx CAN IDs
  CAN0.init_Mask(0, 0, 0x07000000);              // Init first mask...
  CAN0.init_Mask(1, 0, 0x07000000);              // Init second mask...

  
  for (uint8_t i = 0; i < 6; ++i) {
    CAN0.init_Filt(i, 0, 0x07000000);           //Init filters
  }
  
  CAN0.setMode(MCP_NORMAL);                     // Set operation mode to normal so the MCP2515 sends acks to received data.

  pinMode(CAN0_INT, INPUT);                    // Configuring pin for /INT input

  Serial.println("Sending and Receiving OBD-II_PIDs Example...");
}

void loop()
{
  //request coolant temp
  sendPID(PID_COOLANT_TEMP);

  delay(40); //to allow time for ECU to reply

  receivePID(PID_COOLANT_TEMP);

  //request engine speed
  sendPID (PID_ENGINE_RPM);

  delay(40); //to allow time for ECU to reply

  receivePID(PID_ENGINE_RPM);

  //abitrary loop delay
  delay(40);

}

 

코드 구조는 sendPID()함수로 pid를 넘기고 잠시후 receivePID로 값을 받아서 해당하는 pid값으로 처리하는 구조입니다.

 

각 함수나 세부코드는 저도 분석을 할수 없고 해외 개발자가 구현한 코드를 그대로 사용하고 있습니다.

 

한가지 주의할 것이 소스코드에 주석으로도 넣었듯이 MCP2515보드가 8MHZ, 16MHZ두가지 버전이 있습니다.

 

그에 맞게 코드를 수정해 주어야 합니다.

 

https://forum.arduino.cc/t/reading-ecu-data-via-can-bus-and-mcp2515/597914

 

Reading ECU data via CAN-BUS and MCP2515

I have been trying to build an Arduino dashboard for an F-SAE project that displays ECU data from the CAN-BUS, but can't find any good code to base it off of to get me started. My setup is an UNO, connected to an MCP2515 module: These have an 8Mhz crystal,

forum.arduino.cc

 

제가 참고한 코드의 출처입니다.

 

이 코드에서 추가로 원하는 정보를 받으려면 receivePID() 함수에 case문을 하나 더 추가해 주고, loop()함수에서 sendPID(), receivePID()를 각각 추가해 주면 추가정보를 가져올 수 있습니다.

 

다만 코드 상단에 defind된 pid리스트가 많습니다.

상당히 많은 정보를 가져올 수 있는데, 이게 전부는 아니고 전체 pid리스트는 아래 위키피디아에서 확인할 수 있습니다.

 

단 이 모든 pid가 전부 지원하지는 않습니다.

 

차량에 따라 지원하지 않는 pid가 있습니다. 이것은 차에 설치된 센서 종류와 개수가 다르기 때문에 어떤 pid가 값이 리턴되는지는 실차에서 테스트해보셔야 합니다.

 

https://en.wikipedia.org/wiki/OBD-II_PIDs

 

OBD-II PIDs - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search OBD-II PIDs (On-board diagnostics Parameter IDs) are codes used to request data from a vehicle, used as a diagnostic tool. SAE standard J1979 defines many OBD-II PIDs. All on-road vehi

en.wikipedia.org

 

마지막으로 샘플코드를 보면 냉각수 온도와 rpm을 구하는 계산식이 다른것을 알 수 있습니다.

 

이것은 위 위키피디아의 pid리스트를 보면 값을 구하는 계산식이 있는데, 이 계산식대로 계산을 해 주어야 정확하게 계산이 됩니다.

 

RPM을 구하려면 ((256*A)+B)/4 로 계산합니다
냉각수 온도는 A-40 계산식으로 구합니다

 

샘플 코드를 보면 rxBuf[3] 값이 A이고 rxBuf[4]값이 B라는 것을 알수 있습니다.

 

이렇듯 다른 정보를 가져오게 되면 꼭 pid리스트를 보고 계산해 주어야 정확한 값이 나옵니다.

 

코드가 준비되었으면 테스트 실차에서 해야 합니다.

 

하드웨어 연결은 아래처럼 합니다.

 

아두이노 보드와 MCP2515, OBD2커넥터 연결

 

이 그림의 출처는 외국 개발자의 블로그 입니다.

https://leafdriveblog.wordpress.com/2019/08/25/arduino-can-module/

 

Arduino CAN module

I decided to make a small module to spit out charger ID and data for charging. It will allow me to practice my programming skills and get me some ideas. First i used Arduino Nano or its clone b eca…

leafdriveblog.wordpress.com

 

MCP2515보드에는 두가닥의 OBD2커넥터에서 2가닥의 선이 연결되어야 합니다.

 

위 그림처럼 6번 CAN High선을 2515의 H, 14번 CAN Low선을 2515 L에 연결합니다.

 

OBD2 커넥터 핀맵

 

두가닥 선으로 OBD2 커텍터의 6, 14번에 연결

 

참고로 2가닥 선은 일반 점퍼선이고 핀은 아두이노 헤더핀으로 아래처럼 제작하여 꽂았습니다.

딱 맞아떨어집니다.

 

헤더핀을 커넥터에 꽂아서 고정

 

모든 하드웨어 연결을 끝내고 코드를 실행시켜 시리얼 모니터를 열어보면 냉각수 온도와 RPM이 매우 빠르게 올라오는 것을 확인할 수 있습니다.

 

(화면 캡춰를 못했네요)

 

이것을 잘 응용하면 재밌는 것을 많이 만들수 있습니다.

 

기어 변속시점을 알려주는 기어 쉬프터(LED)를 만들수도 있고, 저처럼 대시보드를 만들 수도 있습니다.