-
디지털/ 아날로그 신호를 CAN 신호와 함께 측정하기 - ADI2CANhardware 2025. 6. 7. 21:14
시작하기 전에
CAN 신호를 측정하다보면 CAN과 신호와 함께 디지털 신호(on/ off. 스위치 상태)나 아날로그 신호(센서 출력)를 측정하면 유용한 경우가 있다. 예를 들면,
- 이그니션 신호(on/ off만 있으므로 디지털 신호이다.)를 CAN 신호와 함께 저장하면 시험 시작과 종료를 정확히 알 수 있다. 시험 자동화를 한 수준 높일 수 있을 것이다.
- 외부 로드셀 신호(하중 값은 일정 범위에서 변하므로 아날로그 신호이다.)를 CAN 신호와 함께 저장하여 하중과 액추에이터 소비 전류의 관계를 검증할 수도 있다.
시중에 CAN 하드웨어 제품들 중에 디지털 입력이나 아날로그 입력을 받을 수 있는 것들이 있다. 이런 하드웨어들은 CAN만 측정하는 하드웨어에 비하여 (당연히) 비싸다. 나는 단지 디지털 입력 채널 1개만 필요한데, 디지털 채널이 여러 개인 제품은 있어도 한 개인 제품이 없어 그 제품을 비싸게 사야하는 경우가 있다. 이런 경우에 비용 효율적인 해결책을 생각해봤다.
아두이노(Arduino) 보드로 ADI2CAN (Analog/Digital Input to CAN) 장치를 만들어 본다.
개요
- ADI2CAN 구상: 기능, 장치 구상
- ADI2CAN 하드웨어 구성
- ADI2CAN 코드 작성
- ADI2CAN 동작
ADI2CAN 구상
기능
- 복수의 디지털 입력을 받을 수 있다.
- 디지털 입력 신호들을 한 CAN 메시지에 담는다. 이 메시지를 digitalMsg라고 부르겠다.
- 디지털 입력 신호는 0b00: off, 0b01: on, 0b10: reserved, 0b11: failure들 중 한가지 값을 갖을 수 있다. 이렇게 하면 8 바이트 CAN 메시지에 32개의 디지털 입력 신호들을 넣을 수 있다.
- digitalMsg의 구조는 아래 그림과 같이 된다.

- CAN 통신으로 digitalMsg의 아이디를 변경할 수 있다.
- CAN 통신으로 digitalMsg의 전송 주기를 변경할 수 있다.
- 복수의 아날로그 입력을 받을 수 있다.
- 아날로그 신호들을 한 CAN 메시지에 담는다. 이 메시지를 analogMsg라고 부르겠다.
- 아날로그 신호의 크기는 하드웨어의 ADC(Analog to Digital Converter) 해상도 이상으로 한다. (12 bit ADC가 일반적인 것 같다.)
- analogMsg의 구조는 아래 그림과 같이 된다. (코딩에 편리하도록 아날로그 신호를 16 bit에 담았다.)

- CAN 통신으로 analogMsg의 아이디를 변경할 수 있다.
- CAN 통신으로 analogMsg의 전송 주기를 변경할 수 있다.
장치 구상
- 아래 그림과 같은 구상이다.

ADI2CAN 장치 내부 구성 
측정 시스템 구성 - MCU(Microcontroller)는 디지털 신호와 아날로그 신호를 입력으로 받는다.
- MCU는 디지털 신호의 상태와 아날로그 신호의 값을 CAN 메시지 구조에 따라 SPI 통신으로 CAN Controller에게 전달한다.
- CAN Controller는 전달 받은 메시지를 CAN Tranceiver에 전달한다.
- CAN Transceiver는 CAN 버스 선(CAN High, CAN Low 전선)에 CAN 메시지에 따른 전기 신호를 전송한다.
- CAN Transceiver는 CAN 버스 선에서 메시지를 수신한다.
- CAN Transceiver는 CAN Controller에 수신한 메시지를 전달한다.
- CAN Controller는 SPI 통신으로 수신한 메시지를 MCU에 전달한다.
- MCU는 메시지에 따라 전송하는 CAN 메시지의 아이디나 주기를 변경한다.
- TSMaster로 ECU와 ADI2CAN 장치에서 전송되는 CAN 메시지들을 동시에 모니터링/ 저장한다.
- ADI2CAN가 전송하는 CAN 메시지들의 아이디나 전송 주기를 변경할 필요가 있는 경우, TSMaster의 CAN 메시지 Transmit 기능으로 설정용 CAN 메시지를 ADI2CAN에 전송한다.
ADI2CAN 하드웨어 구성
- 하드웨어 선정의 기준은 1) 적절한 성능, 2) 낮은 자재비, 3) 낮은 개발비이다.
- 간단한 기능이라 큰 컴퓨팅 파워가 필요없다. Arduino UNO 보드를 이용하기로 한다.
- 아두이노 우노에는 (설정에 따라 다르지만) 10개 내외의 디지털 입력 핀들과 최대 6개의 아날로그 입력 핀들이 있다.
- 알리익스프레스에서 Arduino UNO R4 Minima 보드를 샀다. 3주 전(2025년 5월 중순)에 내가 살 때 가격은 아래 그림에 있는 가격의 2배 정도였다.

Arduino UNO R4 Minia. 마이크로콘트롤러에 CAN Controller가 내장되어 있다. - 아두이노는 메인 보드에 없는 기능을 확장 보드를 메인 보드에 끼우는 방식으로 추가할 수 있다. 이런 확장 보드를 쉴드라고 부른다.
- 위 보드에 적용된 르네사스 칩에는 CAN Controller가 있다. 나는 AI에게 CAN Transceiver가 필요한지 문의하였다. 필요없다는 AI의 답에 따라 이 보드만 샀다가 물건을 받은 후에 보드에 CAN 트랜시버가 없다는 것을 알게되었다. 데이터시트를 잘 살펴보고, 사전에 인터넷에서 검색을 좀 했었더라면 아두이노 CAN-BUS 쉴드를 함께 주문해서 시간을 절약했을 텐데 ... UNO R4가 아닌 UNO R3를 샀다면 돈을 조금 더 아꼈을 텐데 ...
- CAN 통신을 위한 CAN Controller 칩과 CAN Transceiver 칩이 있는 확장 보드를 CAN-BUS 쉴드라고 한다. 알리익스프레스에서 Arduino UNO R3 CAN-BUS 쉴드를 샀다. 10일 전에 내가 살 때 가격은 아래 그림에 있는 가격의 5.5배 정도였다.

Arduino UNO R3의 CAN-BUS 쉴드. 이 확장 보드를 메인 보드에 끼우면 CAN 통신을 할 수 있다. - Arduino UNO R4의 CAN Controller를 그대로 사용할 수 있는 CAN-BUS 쉴드를 찾아봤으나 못 찾았다. 열심히 찾지 않은 이유는 Arduino UNO R3 CAN-BUS 쉴드가 Arduino UNO R4와 호환이 되고 가격이 충분히 저렴해서다.
- 아두이노를 선택한 것은 낮은 개발비가 목표들 중에 하니이기 때문이다. 코딩에 시간이 많이 들이면 개발비가 올라간다. 나는 처음부터 코딩은 AI에게 시킬 생각이었다. 왠지 AI는 아두이노 코딩을 잘 할 것 같았다. 아두이노는 오픈 소스 개발이 활발하고, 덕택에 예제 코드가 인터넷에 많다. 즉, AI가 학습할 수 있는 자료가 많다. 내 예상은 적중했다. AI가 아두이노 코딩을 잘해서 내가 코딩에 사용한 시간은 길지 않다.
- 참고로 나는 아두이노 코드를 읽고 이해하는 정도는 되지만 작성할 줄은 모른다. 읽고 척척 이해하는 수준도 아니다.
ADI2CAN 코드 작성
- 위 'ADI2CAN 구상'에 나열한 모든 기능들을 먼저 생각해낸 후에 코딩을 하였으면 좋았겠지만 그렇지 않았다. ^^
- AI에게 코딩을 시키고, AI가 작성한 코드를 내가 컴파일을 하고, 컴파일이 안 되면 코드 개선을 요청하고, 컴파일이 되면 아두이노에 프로그래밍하여 원하는대로 기능을 하는지 확인하기를 반복하였다. 그러다가 새로운 기능에 관한 아이디어들이 떠올랐다. 그러면 그 기능들을 추가한 코드를 요청하였다. 예를 들어, 개발을 시작할 때는, digitalMsg, analogMsg의 아이디나 전송 주기 변경이 필요할 때마다 코드를 직접 변경하는 것으로 생각했었다. 코드를 바꾸면, 컴파일을 해야하고 컴파일된 실행 코드를 마이크로콘트롤러에 프로그래밍 하는 작업을 해야 한다. 이런 작업의 난이도는 낮지만 귀찮다. 제3자가 한다면 작업 환경을 갖추도록 하고, 그 환경을 내 환경과 맞추는 것도 귀찮은 일이다. 그래서 메시지 아이디와 전송 주기를 EEPROM에 저장하고, 부팅이 되면 EEPROM의 메시지 아이디와 전송 주기를 읽어서 digitalMsg와 analogMsg가 전송하도록 했다. 메시지 아이디나 주기 변경이 필요한 경우, CAN 메시지로 장치에 요청하여 변경하고, 변경된 메시지 아이디와 주기로 EEPROM의 기존 데이터를 덮어쓰도록 했다.

CAN 메시지를 ADI2CAN으로 전송하여 메시지 아이디와 전송 주기를 변경할 수 있도록 한다. - 기본적으로 digitalMsg, analogMsg를 Extended Frame으로 만들었다. 일반적으로 Extended Frame을 잘 사용하지 않기에 이런 용도에는 Extended 프레임이 더 적합하다고 생각했다. Standard Frame으로 할 수도 있도록 하였다.
#include <SPI.h> #include <EEPROM.h> #include "mcp2515_can.h" // CAN 쉴드 SPI CS 핀 설정 (Keystudio CAN-BUS 쉴드는 보통 D10 사용) #define CAN0_CS 10 mcp2515_can CAN0(CAN0_CS); // Set CS pin // EEPROM 주소 정의 #define EEPROM_MAGIC_ADDR 0 // 매직 넘버 저장 주소 (2바이트) #define EEPROM_DIGITAL_ID_ADDR 2 // 디지털 메시지 ID 저장 주소 (2바이트) #define EEPROM_ANALOG_ID_ADDR 6 // 아날로그 메시지 ID 저장 주소 (2바이트) #define EEPROM_DIGITAL_PERIOD_ADDR 10 // 디지털 전송 주기 저장 주소 (2바이트) #define EEPROM_ANALOG_PERIOD_ADDR 12 // 아날로그 전송 주기 저장 주소 (2바이트) #define EEPROM_MAGIC_NUMBER 0xB6D5 // EEPROM 초기화 확인용 매직 넘버 // 기본값 정의 #define DEFAULT_DIGITAL_MSG_ID 0x12345100 #define DEFAULT_ANALOG_MSG_ID 0x12345200 #define DEFAULT_DIGITAL_PERIOD 200 // 200ms #define DEFAULT_ANALOG_PERIOD 200 // 200ms #define CAN_CONFIGURATION_MODE MODE_CONFIG #define CAN_NORMAL_MODE MODE_NORMAL #define EXTENDED_FRAME 0x01 #define STANDARD_FRAME 0x00 // 설정 메시지 ID 정의 (Extended Frame) const uint32_t DEBUG_OUTPUT_ID = 0x12345678; // Tx 메시지 프린트 설정용 ID const uint32_t CONFIG_DIGITAL_MSG_ID = 0x1234567D; // 디지털 설정용 메시지 ID const uint32_t CONFIG_ANALOG_MSG_ID = 0x1234567A; // 아날로그 설정용 메시지 ID const uint32_t CONFIG_FILTER_MSG_ID = 0x1234567F; // 필터 설정용 메시지 ID // 설정 메시지 ID 정의 (Standard Frame - 11비트) const uint32_t DEBUG_OUTPUT_STD_ID = 0x678; // 디버그 출력 제어 (Standard) const uint32_t CONFIG_DIGITAL_STD_ID = 0x67D; // 디지털 설정용 (Standard) const uint32_t CONFIG_ANALOG_STD_ID = 0x67A; // 아날로그 설정용 (Standard) const uint32_t CONFIG_FILTER_STD_ID = 0x67F; // 필터 설정용 (Standard) // 동적 메시지 ID 및 전송 주기 변수 uint32_t digitalMsgId = DEFAULT_DIGITAL_MSG_ID; uint32_t analogMsgId = DEFAULT_ANALOG_MSG_ID; uint16_t digitalPeriod = DEFAULT_DIGITAL_PERIOD; uint16_t analogPeriod = DEFAULT_ANALOG_PERIOD; byte digitalMsgFrmType = STANDARD_FRAME; byte analogMsgFrmType = STANDARD_FRAME; // 디지털 상태 정의 (2비트 사용) const byte DIGITAL_OFF = 0; // 00: OFF 상태 const byte DIGITAL_ON = 1; // 01: ON 상태 const byte DIGITAL_RESERVED = 2; // 10: 예약됨 const byte DIGITAL_FAILURE = 3; // 11: 고장 상태 // 타이머 관련 변수 unsigned long previousDigitalMillis = 0; unsigned long previousAnalogMillis = 0; // 필터 관련 변수 // 처리할 CAN 메시지가 많을 경우 성능 저하를 우려하여, // 필수가 아닌 메시지들은 CAN 콘트롤러의 하드웨어 마스크와 필터를 이용해서 거르기 위한 목적이다. bool filterEnabled = false; bool extendedFrameOnly = false; // 출력 on/off // 전송 주기가 빠른 경우, Arduino IDE의 Serial Monitor에 출력이 너무 많다. // digitalMsg나 analogMsg의 출력을 on/off 하기 위한 목적이다. byte digitalMsgPrintEnabled = 0; byte analogMsgPrintEnabled = 0; // 디지털 입력 핀 정의 (SPI 핀 제외) const int DIGITAL_PIN_COUNT = 11; const int digitalPins[DIGITAL_PIN_COUNT] = {3, 4, 5, 6, 7, 8, 9, A0, A1, A2, A3}; // 아날로그 입력 핀 정의 // 실험을 해보니 A0, A1, A2, A3 입력은 주변 핀들의 입력에 영향을 많이 받는다. // 이 핀들을 디지털 입력으로 전환하였다. const int ANALOG_PIN_COUNT = 2; const int analogPins[ANALOG_PIN_COUNT] = {A4, A5}; // EEPROM 관련 함수들 void saveConfigToEEPROM() { EEPROM.write(EEPROM_MAGIC_ADDR, EEPROM_MAGIC_NUMBER & 0xFF); EEPROM.write(EEPROM_MAGIC_ADDR + 1, (EEPROM_MAGIC_NUMBER >> 8) & 0xFF); EEPROM.write(EEPROM_DIGITAL_ID_ADDR, digitalMsgId & 0xFF); EEPROM.write(EEPROM_DIGITAL_ID_ADDR + 1, (digitalMsgId >> 8) & 0xFF); EEPROM.write(EEPROM_DIGITAL_ID_ADDR + 2, (digitalMsgId >> 16) & 0xFF); EEPROM.write(EEPROM_DIGITAL_ID_ADDR + 3, (digitalMsgId >> 24) & 0xFF); EEPROM.write(EEPROM_ANALOG_ID_ADDR, analogMsgId & 0xFF); EEPROM.write(EEPROM_ANALOG_ID_ADDR + 1, (analogMsgId >> 8) & 0xFF); EEPROM.write(EEPROM_ANALOG_ID_ADDR + 2, (analogMsgId >> 16) & 0xFF); EEPROM.write(EEPROM_ANALOG_ID_ADDR + 3, (analogMsgId >> 24) & 0xFF); EEPROM.write(EEPROM_DIGITAL_PERIOD_ADDR, digitalPeriod & 0xFF); EEPROM.write(EEPROM_DIGITAL_PERIOD_ADDR + 1, (digitalPeriod >> 8) & 0xFF); EEPROM.write(EEPROM_ANALOG_PERIOD_ADDR, analogPeriod & 0xFF); EEPROM.write(EEPROM_ANALOG_PERIOD_ADDR + 1, (analogPeriod >> 8) & 0xFF); Serial.println("Configuration saved to EEPROM"); } void loadConfigFromEEPROM() { uint16_t magicNumber = EEPROM.read(EEPROM_MAGIC_ADDR) | (EEPROM.read(EEPROM_MAGIC_ADDR + 1) << 8); if (magicNumber == EEPROM_MAGIC_NUMBER) { digitalMsgId = EEPROM.read(EEPROM_DIGITAL_ID_ADDR) | (EEPROM.read(EEPROM_DIGITAL_ID_ADDR + 1) << 8) | (EEPROM.read(EEPROM_DIGITAL_ID_ADDR + 2) << 16) | (EEPROM.read(EEPROM_DIGITAL_ID_ADDR + 3) << 24); analogMsgId = EEPROM.read(EEPROM_ANALOG_ID_ADDR) | (EEPROM.read(EEPROM_ANALOG_ID_ADDR + 1) << 8) | (EEPROM.read(EEPROM_ANALOG_ID_ADDR + 2) << 16) | (EEPROM.read(EEPROM_ANALOG_ID_ADDR + 3) << 24); digitalPeriod = EEPROM.read(EEPROM_DIGITAL_PERIOD_ADDR) | (EEPROM.read(EEPROM_DIGITAL_PERIOD_ADDR + 1) << 8); analogPeriod = EEPROM.read(EEPROM_ANALOG_PERIOD_ADDR) | (EEPROM.read(EEPROM_ANALOG_PERIOD_ADDR + 1) << 8); digitalMsgFrmType = (digitalMsgId > 0x7FF); analogMsgFrmType = (analogMsgId > 0x7FF); Serial.println("Configuration loaded from EEPROM"); } else { // EEPROM이 초기화되지 않았거나 손상됨 - 기본값 사용 및 저장 digitalMsgId = DEFAULT_DIGITAL_MSG_ID; analogMsgId = DEFAULT_ANALOG_MSG_ID; digitalPeriod = DEFAULT_DIGITAL_PERIOD; analogPeriod = DEFAULT_ANALOG_PERIOD; saveConfigToEEPROM(); Serial.println("EEPROM initialized with default values"); } } void printCurrentConfig() { Serial.println("=== Current Configuration ==="); Serial.print("Digital Message ID: 0x"); Serial.println(digitalMsgId, HEX); Serial.print("Analog Message ID: 0x"); Serial.println(analogMsgId, HEX); Serial.print("Digital Period: "); Serial.print(digitalPeriod); Serial.println(" ms"); Serial.print("Analog Period: "); Serial.print(analogPeriod); Serial.println(" ms"); Serial.print("Filter Enabled: "); Serial.println(filterEnabled ? "YES" : "NO"); Serial.print("Extended Frame Only: "); Serial.println(extendedFrameOnly ? "YES" : "NO"); Serial.println("============================="); } bool isIdConflict(uint16_t newDigitalId, uint16_t newAnalogId) { return (newDigitalId == newAnalogId); } // MCP2515 필터 설정 함수들 void setupExtendedFrameFilter() { // Extended frame만 수신하도록 필터 설정 // Mask를 0x1FFFFFFF로 설정하여 모든 Extended ID를 허용 // 하지만 Extended 프레임만 받도록 설정 // Configuration mode로 전환 CAN0.setMode(CAN_CONFIGURATION_MODE); // 모든 필터를 Extended 프레임용으로 설정 // 필터 0: 설정 메시지들을 위한 필터 CAN0.init_Mask(0, 1, 0x1FFFFFFF); // Mask 0 (Extended frame) CAN0.init_Mask(1, 1, 0x1FFFFFFF); // Mask 1 (Extended frame) // 필터 설정 - 우선 설정 메시지들을 받을 수 있도록 CAN0.init_Filt(0, 1, DEBUG_OUTPUT_ID); // 디버그 출력 제어 CAN0.init_Filt(1, 1, CONFIG_DIGITAL_MSG_ID); // 디지털 설정 CAN0.init_Filt(2, 1, CONFIG_ANALOG_MSG_ID); // 아날로그 설정 CAN0.init_Filt(3, 1, CONFIG_FILTER_MSG_ID); // 필터 설정 CAN0.init_Filt(4, 1, digitalMsgId); // 현재 디지털 ID CAN0.init_Filt(5, 1, analogMsgId); // 현재 아날로그 ID // Normal mode로 복귀 CAN0.setMode(CAN_NORMAL_MODE); Serial.println("Extended frame filter enabled"); } void setupStandardFrameFilter() { // Standard frame만 수신하도록 필터 설정 // Configuration mode로 전환 CAN0.setMode(CAN_CONFIGURATION_MODE); // 모든 필터를 Standard 프레임용으로 설정 CAN0.init_Mask(0, 0, 0x7FF); // Mask 0 (Standard frame) - 모든 11비트 비교 CAN0.init_Mask(1, 0, 0x7FF); // Mask 1 (Standard frame) - 모든 11비트 비교 // 필터 설정 - Standard 프레임용 ID들 사용 CAN0.init_Filt(0, 0, digitalMsgId & 0x7FF); // 현재 디지털 메시지 ID CAN0.init_Filt(1, 0, analogMsgId & 0x7FF); // 현재 아날로그 메시지 ID CAN0.init_Filt(2, 0, DEBUG_OUTPUT_STD_ID); // 디버그 출력 제어 CAN0.init_Filt(3, 0, CONFIG_DIGITAL_STD_ID); // 디지털 설정 CAN0.init_Filt(4, 0, CONFIG_ANALOG_STD_ID); // 아날로그 설정 CAN0.init_Filt(5, 0, CONFIG_FILTER_STD_ID); // 필터 설정 // Normal mode로 복귀 CAN0.setMode(CAN_NORMAL_MODE); Serial.println("Standard frame filter enabled"); Serial.println("Use Standard frame IDs for configuration:"); Serial.print(" Debug control: 0x"); Serial.println(DEBUG_OUTPUT_STD_ID, HEX); Serial.print(" Digital config: 0x"); Serial.println(CONFIG_DIGITAL_STD_ID, HEX); Serial.print(" Analog config: 0x"); Serial.println(CONFIG_ANALOG_STD_ID, HEX); Serial.print(" Filter config: 0x"); Serial.println(CONFIG_FILTER_STD_ID, HEX); } void disableFilter() { // Configuration mode로 전환 CAN0.setMode(CAN_CONFIGURATION_MODE); // 모든 메시지를 받도록 마스크를 0으로 설정 CAN0.init_Mask(0, 0, 0x00000000); CAN0.init_Mask(1, 0, 0x00000000); CAN0.init_Mask(0, 1, 0x00000000); CAN0.init_Mask(1, 1, 0x00000000); // Normal mode로 복귀 CAN0.setMode(CAN_NORMAL_MODE); Serial.println("Hardware filter disabled - receiving all frames"); } void setup() { Serial.begin(115200); // 시리얼 포트 초기화 대기 while (!Serial) { ; // 시리얼 포트가 연결될 때까지 대기 } Serial.println("Arduino UNO R4 + CAN Shield Start Communication"); // EEPROM에서 설정 로드 loadConfigFromEEPROM(); printCurrentConfig(); // 디지털 핀 설정 for (int i = 0; i < DIGITAL_PIN_COUNT; i++) { // pinMode(digitalPins[i], INPUT_PULLUP); // 내부 풀업 저항 사용 pinMode(digitalPins[i], INPUT); // 내부 풀업 저항 사용하지 않음 } // CAN 초기화 if (CAN0.begin(CAN_500KBPS) == CAN_OK) { Serial.println("CAN initialization successful!"); } else { Serial.println("CAN initialization failure"); } // 인터럽트 핀 설정 (선택사항 - D2 핀 사용) pinMode(2, INPUT); Serial.println("System ready"); Serial.println("Digital Pin: 3, 4, 5, 6, 7, 8, 9, A2"); Serial.println("Analog Pin : A0, A1, A3, A4"); Serial.println("SPI Pin : D10(CS), D11(MOSI), D12(MISO), D13(SCK)"); Serial.print("digitalMsgPrintEnabled: "); Serial.println(digitalMsgPrintEnabled); Serial.print("analogMsgPrintEnabled: "); Serial.println(analogMsgPrintEnabled); Serial.println("Use 0x12345678 message to turn on/off msg println"); Serial.println("Use 0x1234567D message to configure digital settings"); Serial.println("Use 0x1234567A message to configure analog settings"); Serial.println("Use 0x1234567F message to configure hardware filter"); Serial.println(" Byte 0: Filter enable (0=disable, 1=enable)"); Serial.println(" Byte 1: Frame type (0=standard, 1=extended)"); Serial.println(""); Serial.println("Standard Frame equivalents (when Standard filter active):"); Serial.println(" Debug control: 0x678"); Serial.println(" Digital config: 0x67D"); Serial.println(" Analog config: 0x67A"); Serial.println(" Filter config: 0x67F"); Serial.println(""); } void loop() { unsigned long currentMillis = millis(); // 디지털 메시지 전송 (개별 타이머) if (currentMillis - previousDigitalMillis >= digitalPeriod) { previousDigitalMillis = currentMillis; sendDigitalInputs(); } // 아날로그 메시지 전송 (개별 타이머) if (currentMillis - previousAnalogMillis >= analogPeriod) { previousAnalogMillis = currentMillis; sendAnalogInputs(); } // CAN 메시지 수신 처리 receiveCanMessages(); } void sendDigitalInputs() { unsigned char digitalData[8] = {0}; // CAN 데이터는 최대 8바이트 // 디지털 핀 상태 읽기 및 2비트 상태 인코딩 for (int i = 0; i < DIGITAL_PIN_COUNT; i++) { byte pinState; int pinValue = digitalRead(digitalPins[i]); // 디지털 입력 값 기반으로 상태 결정 if (pinValue == LOW) { // 풀업 저항 사용 시 LOW는 ON을 의미 pinState = DIGITAL_ON; } else { pinState = DIGITAL_OFF; } // 각 디지털 입력은 2비트 사용 digitalData[i/4] |= (pinState << ((i % 4) * 2)); } // CAN 메시지 전송 (동적 ID 사용) byte sndStat = CAN0.sendMsgBuf(digitalMsgId, digitalMsgFrmType, 8, digitalData); if (sndStat == CAN_OK) { if (digitalMsgPrintEnabled == 1) { Serial.print("d_msg: 0x"); Serial.print(digitalMsgId, HEX); Serial.print(": "); for (int i = 0; i < 8; i++) { if (digitalData[i] < 0x10) Serial.print("0"); Serial.print(digitalData[i], HEX); Serial.print(" "); } Serial.print(" | state: "); for (int i = 0; i < DIGITAL_PIN_COUNT; i++) { Serial.print("D"); Serial.print(digitalPins[i]); Serial.print(":"); Serial.print(digitalRead(digitalPins[i]) == LOW ? "ON" : "OFF"); Serial.print(" "); } Serial.println(); } } else { Serial.print("digitalMsg Tx failure: "); Serial.println(sndStat); } } void sendAnalogInputs() { unsigned char analogData1[8] = {0}; // 첫 번째 메시지 (아날로그 입력 0-3) // 첫 번째 CAN 메시지에 4개 아날로그 값 저장 for (int i = 0; i < ANALOG_PIN_COUNT; i++) { int analogValue = analogRead(analogPins[i]); // 각 아날로그 값을 2바이트(16비트)로 저장 (리틀 엔디안 방식) analogData1[i*2] = analogValue & 0xFF; // 하위 8비트 analogData1[i*2+1] = (analogValue >> 8) & 0xFF; // 상위 8비트 } // 아날로그 데이터 전송 (동적 ID 사용) byte sndStat = CAN0.sendMsgBuf(analogMsgId, analogMsgFrmType, 8, analogData1); if (sndStat == CAN_OK) { if (analogMsgPrintEnabled == 1) { Serial.print("a_msg: 0x"); Serial.print(analogMsgId, HEX); Serial.print(": "); for (int i = 0; i < 8; i++) { if (analogData1[i] < 0x10) Serial.print("0"); Serial.print(analogData1[i], HEX); Serial.print(" "); } Serial.print(" | state: "); for (int i = 0; i < ANALOG_PIN_COUNT; i++) { int value = analogData1[i*2] | (analogData1[i*2+1] << 8); Serial.print("A"); Serial.print(analogPins[i] - A0); Serial.print(":"); Serial.print(value); Serial.print(" "); } Serial.println(); } } else { Serial.print("analogMsg Tx failure: "); Serial.println(sndStat); } } void receiveCanMessages() { byte status = 0; unsigned long canId = 0x000; byte ext = 0; byte rtr = 0; unsigned char len = 0; unsigned char buf[8]; if (CAN0.checkReceive() == CAN_MSGAVAIL) { status = CAN0.readMsgBufID(CAN0.readRxTxStatus(), &canId, &ext, &rtr, &len, buf); Serial.print("msg Rx: 0x"); Serial.print(canId, HEX); Serial.print(" status: "); Serial.print(status); Serial.print(" DLC:"); Serial.print(len); Serial.print(" : "); // 데이터 출력 for (int i = 0; i < len; i++) { if (buf[i] < 0x10) Serial.print("0"); Serial.print(buf[i], HEX); Serial.print(" "); } Serial.println(); // 기존 디버그 출력 제어 메시지 if (canId == DEBUG_OUTPUT_ID) { if (buf[0] == 0x00) { digitalMsgPrintEnabled = 0; } else { digitalMsgPrintEnabled = 1; } if (buf[1] == 0x00) { analogMsgPrintEnabled = 0; } else { analogMsgPrintEnabled = 1; } } // 디지털 설정 메시지 처리 (0x1234567D) else if (canId == CONFIG_DIGITAL_MSG_ID && len >= 7) { uint32_t newDigitalId = buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24); uint16_t newDigitalPeriod = buf[4] | (buf[5] << 8); // ID 충돌 확인 if (!isIdConflict(newDigitalId, analogMsgId)) { digitalMsgId = newDigitalId; digitalMsgFrmType = (digitalMsgId > 0x7FF); digitalPeriod = newDigitalPeriod; saveConfigToEEPROM(); Serial.print("Digital config updated - ID: 0x"); Serial.print(digitalMsgId, HEX); Serial.print(", Period: "); Serial.print(digitalPeriod); Serial.println(" ms"); } else { Serial.println("Error: Digital ID conflicts with Analog ID!"); } } // 아날로그 설정 메시지 처리 (0x1234567A) else if (canId == CONFIG_ANALOG_MSG_ID && len >= 4) { uint32_t newAnalogId = buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24); uint16_t newAnalogPeriod = buf[4] | (buf[5] << 8); // ID 충돌 확인 if (!isIdConflict(digitalMsgId, newAnalogId)) { analogMsgId = newAnalogId; analogMsgFrmType = (analogMsgId > 0x7FF); analogPeriod = newAnalogPeriod; saveConfigToEEPROM(); Serial.print("Analog config updated - ID: 0x"); Serial.print(analogMsgId, HEX); Serial.print(", Period: "); Serial.print(analogPeriod); Serial.println(" ms"); } else { Serial.println("Error: Analog ID conflicts with Digital ID!"); } } // 필터 설정 메시지 처리 (0x1234567F) else if (canId == CONFIG_FILTER_MSG_ID && len >= 2) { bool newFilterEnabled = (buf[0] != 0); bool newExtendedFrameOnly = (buf[1] != 0); filterEnabled = newFilterEnabled; extendedFrameOnly = newExtendedFrameOnly; if (filterEnabled) { if (extendedFrameOnly) { setupExtendedFrameFilter(); } else { setupStandardFrameFilter(); } } else { disableFilter(); } Serial.print("Filter config updated - Enabled: "); Serial.print(filterEnabled ? "YES" : "NO"); Serial.print(", Extended only: "); Serial.println(extendedFrameOnly ? "YES" : "NO"); } } }adi2can_uno_r4_eeprom_ext.ino0.02MBADI2CAN 동작
- ADI2CAN을 PC와 연결하여 사용하는 모습은 아래 사진과 같다.

아두이노로 만든 ADI2CAN 장치를 TC1013을 통해서 TSMaster로 연결하였다. 아날로그 입력, 디지털 입력이 TSMaster에 잘 표시된다. 
ADI2CAN과 TC1013을 연결하였다. - ADI2CAN가 전송한 digitalMsg와 analogMsg를 TSMaster에서 보면 아래 그림과 같다.

- 점프 와어이의 한쪽 끝은 GND에 다른쪽 끝은 아날로그 입력 포트에 하나씩 연결하면서 신호 변화를 보았다. A1를 GND에 연결했는데, A0와 A1도 함께 값이 변하는 것을 볼 수 있다. 나는 PC의 USB 전원으로 아두이노 보드에 전원을 공급하였다. 전력이 충분하지 않아서 발생한 일일 수도 있다. 어쨌든 이렇게 점검을 해보니 A0, A1, A2, A3는 모두 주변 핀의 신호에 영향을 받았다. A4와 A5는 영향이 미미했다. A0, A1, A2, A3를 모두 디지털 입력으로 설정하였다.
- 점프 와이어의 한쪽 끝은 GND에 다른쪽 끝은 디지털 입력 포트에 하나씩 연결하면서 신호 변화를 보았다. 연결할 때마다 off에서 on으로 상태가 잘 변하는 것을 볼 수 있다.
DBC
- 위와 같이 신호를 보기 위해 첨부 dbc 파일을 이용했다. 구조가 간단해서 dbc를 만드는 것도 간단하다.
adi2can.dbc0.00MB- 메시지 아이디를 변경하면 dbc의 메시지 아이디를 변경해야 하는 번거로움이 있다. 이런 일은 드문 일이다. 가성비를 생각하면 문제가 될 정도로 귀찮고 어려운 일은 아니라고 생각한다.
digitalMsg와 analogMsg의 아이디와 전송 주기 변경
- digitalMsg의 아이디와 전송 주기 변경에는 0x1234567D 메시지를 이용한다. 메시지 아이디 마지막의 D는 digital에서 가져왔다. 기억하기 쉽도록.
- analogMsg의 아이디와 전송 주기 변경에는 0x1234567A 메시지를 이용한다. 메시지 아이디 마지막의 A는 analog에서 가져왔다. 기억하기 쉽도록.
- 0x1234567D와 0x1234567A 메시지의 구조는 아래 그림에 테두리친 부분들과 같다.

TSMaster에서 0x1234567D, 0x1234567A 메시지를 이용하여 digitalMsg와 analogMsg의 아이디나 전송 주기를 변경할 수 있다. - 0x1234567D, 0x1234567A 메시지의
- 첫 4 바이트가 각각 digitalMsg와 analogMsg의 새 아이디이다. 위 그림의 예는 아래와 같다.
- 0x1234567D 메시지: 00 51 34 12 --> 0x12345100
- 0x1234567D 메시지: 00 52 34 12 --> 0x12345200
- 그 다음 2 바이트는 각각 digitalMsg와 analogMsg의 새 전송 주기이다.
- F4 01 --> 0x01F4 --> 500(십진수) --> 전송 주기: 500msec
- 첫 4 바이트가 각각 digitalMsg와 analogMsg의 새 아이디이다. 위 그림의 예는 아래와 같다.
- TSMaster의 CAN Transmit 기능을 이용하여 0x1234567D, 0x1234567A 메시지를 전송하여 아이디와 전송 주기를 변경할 수 있다.
추가 개발
- ADI2CAN를 현재 상태 그대로 사용하려면 사용할 수도 있다. 하지만 아래의 개발을 더 하려고 한다.
- 케이스가 필요하다.
- 적당한 크기의 케이스를 사거나 3D 프린터로 제작한다.
- 나는 3D 프린터를 사용해본 적이 없다. 간단한 케이스를 위해서 3D 설계를 한다면 너무 시간이 많이 들 것이다.
- 12V 입력을 받을 수 있도록 전압 배분 회로를 추가한다. 아두이노 우노 디지털 입력의 전압 범위는 0V에서 5V이다. 12V 신호를 입력하기 위해서 전압 배분 회로가 필요하다.
- 디지털 입력 핀들을 INPUT, INPUT_PULLUP, OUTPUT_LOW, OUTPUT_HIGH로 설정할 수 있다. CAN 통신으로 이 설정을 할 수 있도록 한다.
결론
- 기술의 가능성은 커졌고, 커진 만큼 기술의 가치도 높아졌는데, 기술 구현의 비용은 정말 낮아졌다.
- ADI2CAN 장치를 하나 만드는데 현재까지 재료비는 3만원/개 이하이다. 한 1,000개를 만든다고 하면 1.5만원/개 이하로 할 수 있지 않을까 짐작한다.
- 장치 구상은 오가는 버스나 지하철 안에서 했다. 아두이노 검색과 주문도 대부분 버스나 지하철에서 했다. 실제 집중하여 작업한 것은 그저께 밤, 어제 저녁과 밤, 오늘 오전, 저녁, 밤이다. AI와 코딩하고, dbc를 만들어 실험하고 지금(9:04pm) 블로그를 작성하고 있다. 중간에 유튜브 보며 놀지 않았다면 몇 시간 전에 마쳤을 것이다. ^^
- 장치를 직접 만들다 보니,
- 여러 가지 다양한 아이디어들이 떠올랐다. 나중에 만들어 볼까 한다.
- 장치 개발의 어려움을 알게 되었다. 특히 아날로그 신호들이 옆 신호에 영향을 받는 것은 직접 해보지 않고 책에서 읽고 비디오로 보고 생각만 했다면 그 영향의 크기를 지금처럼 실감하며 깨우치지는 못했을 것이다. 역시 엔지니어링은 직접 만지작 거리며 해볼 필요가 있다.
ADI2CAN 개선 :: hsl's tsmaster 사용기
'hardware' 카테고리의 다른 글
adi2can 사용자 설명서 (3) 2025.07.21 ADI2CAN 개선 - Open Source 공개 (1) 2025.07.19 CAN과 CAN-FD를 혼합 사용하면 ...? (3) 2025.06.13 timestamp, ACK와 ACK error (0) 2025.06.11 Q&A: 옵션에 없는 CAN(-FD) baud rate 설정하는 방법 (0) 2024.12.06
