홈으로 돌아가기

CAN을 통한 CLI: ISO-TP와 CANshell을 사용한 임베디드 시스템 디버깅

CAN 버스를 통해 임베디드 시스템용 CLI 구현 심층 탐구: ISO-TP 프로토콜과 CANshell 유틸리티 사용. UART 없이 디버깅하는 효과적인 솔루션.

CAN을 통한 CLI: ISO-TP와 CANshell을 사용한 임베디드 시스템 디버깅
Advertisement 728x90

임베디드 시스템 디버깅: ISO-TP 및 CANshell을 활용한 CAN 버스 CLI 구현

임베디드 시스템 개발은 제한적인 디버깅 인터페이스로 인해 종종 난관에 부딪힙니다. 기존 UART를 사용할 수 없지만 CAN 버스가 존재하는 경우, 원격 상호작용을 위한 대안적인 방법이 필수적입니다. 이 글에서는 ISO-TP 프로토콜과 특수 CANshell 유틸리티를 사용하여 CAN 버스 상에서 CLI(명령줄 인터페이스)를 구현하는 접근 방식을 설명하며, 마이크로컨트롤러를 위한 포괄적인 디버깅 환경을 제공합니다.

UART 없이 디버깅하는 것의 어려움

고전적인 임베디드 시스템 디버깅은 전통적으로 UART와 같은 직렬 인터페이스에 의존합니다. 하지만 실제 프로젝트, 특히 특수 전자 보드를 개발할 때는 UART가 아예 없거나 다른 용도로 예약되어 있을 수 있습니다. 이러한 상황에서 여러 CAN 포트를 사용할 수 있다면, 디버그 정보 및 제어 명령 전송을 위해 CAN 버스를 활용해야 하는 과제가 발생합니다.

주요 복잡성은 다음과 같습니다:

Google AdInline article slot
  • 제한된 CAN 패킷 크기: 표준 CAN 패킷은 8바이트의 데이터만 담을 수 있습니다. 이는 150바이트 이상에 달할 수 있는 긴 CLI 명령을 전송하거나, 방대한 응답 및 로그를 수신하기에는 턱없이 부족합니다. 이 문제를 해결하려면 여러 CAN 패킷에서 데이터를 분할하고 재조립할 수 있는 프로토콜이 필요합니다.
  • 데이터 스트림 분리: 원격 디버깅은 종종 디버거 자체의 CLI와 원격 장치(ECU)의 CLI 간에 동시 상호작용을 요구합니다. 단일 터미널에서 이러한 스트림을 효율적으로 분리하고 표시하는 것이 중요합니다.
  • 로그 리디렉션: 임베디드 펌웨어는 일반적으로 UART 또는 SWO로 로그를 출력합니다. CAN을 통한 CLI를 사용하려면 이러한 로그를 ISO-TP를 통해 선택된 CAN 인터페이스로 리디렉션하는 메커니즘이 필요합니다.

이러한 문제들을 해결하기 위해, 우리는 CAN 버스 상에서 임의 길이의 메시지를 전송할 수 있는 ISO-TP (ISO 15765-2) 프로토콜을 사용하고, PC용 특수 콘솔 유틸리티인 CANshell을 개발하는 방안을 제안합니다.

솔루션 아키텍처: ISO-TP 및 CANshell

제안된 솔루션은 ISO-TP 프로토콜과 CANshell 유틸리티의 조합을 기반으로 합니다. ISO-TP는 CAN을 통한 대용량 데이터의 안정적인 전송을 보장하며, CANshell은 TCP 클라이언트(예: PuTTY 또는 TeraTerm)와 CAN 버스 사이의 브리지 역할을 합니다.

작동 원리:

Google AdInline article slot
  • PC 측: CANshell 유틸리티는 TCP 서버를 시작하고 TCP 소켓을 엽니다. 이 소켓은 한쪽은 ISO-TP 프로토콜에, 다른 한쪽은 TCP 클라이언트에 연결됩니다. 연결 매개변수(ISO-TP 주소, COM 포트, CAN 속도)는 명령줄 인수를 통해 유틸리티에 전달되어 스크립트를 통한 자동화를 가능하게 합니다.
  • 마이크로컨트롤러(ECU) 측: ISO-TP 스택이 구현되어 CAN 버스에서 데이터를 수신하고 CLI 명령 파서로 전달합니다. 로그 및 응답 출력을 위해서는 리디렉션 메커니즘이 사용되어 이 데이터를 ISO-TP를 통해 다시 보낼 수 있습니다.
  • 상호작용: TCP 클라이언트가 명령을 보내면, CANshell은 이를 가로채어 ISO-TP 패킷으로 캡슐화하고 USB-CAN 컨버터를 통해 CAN 버스로 전송합니다. 마이크로컨트롤러에서는 ISO-TP 스택이 패킷을 재조립하고, 명령은 CLI 파서로 전달됩니다. 마이크로컨트롤러의 응답 및 로그는 유사하게 ISO-TP로 캡슐화되어 CAN 버스를 통해 CANshell로, 그리고 다시 TCP 클라이언트로 전송됩니다.

구성 요소 구현 및 코드 예시

실제 구현을 위해, USB2CANFD_V1 USB-CAN 컨버터는 SLCAN 프로토콜을 사용하여 직렬 COM 포트를 통한 상호작용의 용이성과 가용성 때문에 선택되었습니다. 두 개의 CAN 포트를 가진 JZ-F407VET6 개발 보드가 타겟 플랫폼으로 사용되었습니다.

PC 측의 TCP 서버는 WinSock2 함수를 사용하여 순수 C로 구현되었습니다. 이는 마이크로컨트롤러 펌웨어와 PC 애플리케이션 모두에서 ISO-TP 프로토콜 구성 요소 및 FIFO 큐의 재사용을 극대화합니다.

마이크로컨트롤러에서 ISO-TP를 통해 데이터를 수신하고 CLI 파서로 전달하는 과정은 다음과 같습니다:

Google AdInline article slot
void iso_tp1_rx_done(n_indn_t* in_done){
    LOG_DEBUG(ISO_TP,"ISO_TP1,%s",Iso15765_n_indn_ToStr(in_done));
    if(N_OK==in_done->rslt){
#ifdef HAS_CLI
        IsoTpHandle_t* Node=IsoTpGetNode(1);
        if(Node) {
            Node->target_id = in_done->n_ai.n_sa;
            cli_process_data(Node->cli_num, in_done->msg, in_done->msg_sz);
            memset(in_done->msg,0,I15765_MSG_SIZE);
        }
#endif
    }
}

iso_tp1_rx_done 콜백은 완전한 ISO-TP 메시지 수신이 완료될 때 호출됩니다. 이 콜백은 데이터를 추출하여 처리를 위해 cli_process_data 함수로 전달합니다.

마이크로컨트롤러에서 ISO-TP로 로그를 리디렉션하려면 특수 명령 tpw 1이 사용됩니다. 이 명령을 실행하면 모든 로깅 함수(LOG_ERROR, LOG_INFO, LOG_DEBUG)는 첫 번째 ISO-TP 인스턴스와 연결된 TxFIFO 큐에 데이터를 쓰기 시작합니다.

void iso_tp1_puts(void* stream_ptr, const char* str, int32_t len) {
    IsoTpHandle_t* Node = IsoTpGetNode(1);
    if(Node) {
        if(str) {
            if(len) {
                bool res = fifo_push_array(&Node->TxFifo, (uint8_t*)str, (uint32_t)len);
                if(!res) {
                    Node->error_cnt++;
                }
            }
        }
    }
}

void iso_tp1_putc(void* stream_ptr, char ch) {
    IsoTpHandle_t* Node = IsoTpGetNode(1);
    if(Node) {
        bool res = fifo_push(&Node->TxFifo, (uint8_t)ch);
        if(!res) {
            Node->error_cnt++;
        }
    }
}

iso_tp1_putsiso_tp1_putc 함수는 문자 또는 문자열을 TxFifo 버퍼에 배치하며, 이 버퍼에서 ISO-TP를 통해 전송됩니다.

데이터 전송 제어 및 동기화

마이크로컨트롤러의 IsoTpTx 태스크는 프로토콜 상태 머신이 준비되면 TxFifo에서 데이터를 추출하여 ISO-TP를 통해 전송하는 역할을 담당합니다.

static bool iso15765_tx_next(IsoTpHandle_t* const Node) {
    bool res = false;
    res = iso15765_is_idle( &Node->instance);
    if(  res) {
        uint32_t count = fifo_get_count(&Node->TxFifo);
        if(0 < count) {
            uint32_t txLen = 0;
            n_req_t isoTpFrame={0};
            isoTpFrame.fr_fmt=CBUS_FR_FRM_STD;
            res = iso_tp_node_to_address_info( Node, &isoTpFrame.n_ai);
            res = iso_tp_node_to_proto_ctrl_info(  Node, &isoTpFrame.n_pci);
            res = fifo_pull_array(&Node->TxFifo, isoTpFrame.msg, sizeof(isoTpFrame.msg), &txLen);
            if(res) {
                isoTpFrame.msg_sz = txLen;
                n_rslt ret = iso15765_send(&Node->instance, &isoTpFrame);
                res = iso15765_ret_to_res(ret);
            }
        }
    }
    return res;
}

bool iso_tp_tx_proc_one(uint8_t num) {
    bool res = false;
    IsoTpHandle_t* Node = IsoTpGetNode(num);
    if(Node) {
        LOG_PARN(ISO_TP, "IsoTp%u,ProcTx", num);
        res = iso15765_tx_next(Node);
        Node->spin++;
    }
    return res;
}

이 코드 스니펫들은 데이터 전송 로직을 보여줍니다. iso15765_tx_next 함수는 ISO-TP 트랜시버가 유휴 상태인지 확인하고, 그렇다면 TxFifo에서 데이터를 추출하여 전송을 시작합니다.

PC 측 CANshell 유틸리티에서는 ISO-TP를 통해 수신된 데이터도 큐에 배치되며, 이 큐에서 TCP 클라이언트로 전송됩니다.

bool can_shell_iso_tp_rx_data(uint8_t num,
                              uint8_t iso_num,
                              const  uint8_t* const data,
                              uint16_t msg_sz) {
    bool res = false;
    if(data) {
        if(msg_sz) {
            SocketHandle_t * Socket = SocketGetNode(1);
            if(Socket) {
                res = fifo_push_array(&Socket->TxFifo, data, (uint32_t) msg_sz);
                log_debug_res(CAN_SHELL, res, "SocketTxFiFoPush");
            }
        }
    }
    return res;
}

static bool iso_tp_rx_done(uint8_t iso_num, n_indn_t* in_done) {
    bool res = false;
    if(in_done) {
        LOG_INFO(ISO_TP, "ISO_TP_%u,MoveDone:%s", iso_num, Iso15765_n_indn_ToStr(in_done));
        res = iso15765_ret_to_res(in_done->rslt);
        if(res) {
            IsoTpHandle_t *Node = IsoTpGetNode(iso_num);
            if(Node) {
                Node->target_id = in_done->n_ai.n_sa;
                Node->rx_done = true;
#ifdef HAS_CAN_SHELL
                res = can_shell_iso_tp_rx_data(1, iso_num, in_done->msg, in_done->msg_sz);
#endif
            }
        }else{
            LOG_ERROR(ISO_TP, "MoveErr,Ret,%u=%s", in_done->rslt,
                      Iso15765retToStr(in_done->rslt));
        }
    }
    return res;
}

이 함수들은 PC 측에서 ISO-TP 데이터를 수신하고 이를 TCP 소켓을 위해 지정된 버퍼로 전송하는 역할을 합니다.

마지막 단계는 TCP 서버에서 연결된 TCP 클라이언트(PuTTY, TeraTerm)로 데이터를 전송하는 것입니다:

static bool socket_server_connected_tx_proc(SocketHandle_t *const Node) {
    bool res = false;
    uint32_t tx_len = fifo_get_count(&Node->TxFifo );
    if(tx_len) {
        uint8_t TxPart[150] = {0};
        uint32_t tx_size = 0 ;
        res= fifo_pull_array(&Node->TxFifo , TxPart, sizeof(TxPart), &tx_size);
        if(res) {
            int tx_done_cnt = send(Node->socket_remote, (char*) TxPart, (int) tx_size, 0 );
            if(tx_done_cnt==tx_size) {
                res = true;
            } else {
                res = false ;
                int ret = 0;
                ret = WSAGetLastError();
                LOG_ERROR(LG_SOCKET_SERVER, 
                          "SendFailedWithErrorCode,ErrCode:%d=%s", 
                          ret, WSAErrorToStr(ret));
            }
        }
    }
    return res;
}

이 코드는 WinSock2의 send 함수를 사용하여 소켓 버퍼의 데이터가 클라이언트로 어떻게 전송되는지 보여줍니다.

중요한 측면은 ISO-TP 세션의 동기화입니다. CANshell이 데이터 흐름에 압도되어 메시지 손실이 발생하는 상황이 관찰되었습니다. 해결책은 두 프로토콜 인스턴스가 성공적으로 데이터를 재설정하고 처리할 수 있도록 ISO-TP 세션을 시간적으로 분산시키는 것이었습니다.

CANshell 유틸리티 사용하기

CANshell 유틸리티를 실행하려면 일련의 위치 인수를 지정해야 합니다:

  • COM 포트: USB-CAN 컨버터가 연결된 COM 포트 번호 (예: COM3).
  • CAN 비트 전송률: kbit/s 단위의 CAN 버스 속도 (예: 500 kbit/s의 경우 500000).
  • PC의 ISO-TP 주소: 네트워크 상에서 CANshell 유틸리티 자체의 ISO-TP 주소 (예: 0xC).
  • 타겟 장치의 ISO-TP 주소: 통신을 설정할 마이크로컨트롤러(ECU)의 ISO-TP 주소 (예: 0xA).
  • TCP 포트: CANshell이 클라이언트 연결을 위해 열 TCP 포트 번호 (예: 50003).

유틸리티 실행 예시:

CANshell.exe COM3 500000 0xC 0xA 50003

실행 후, CANshell 유틸리티는 두 개의 터미널을 엽니다. 하나는 자체 CLI용이고, 다른 하나는 지정된 TCP 포트를 사용하여 TCP 클라이언트(PuTTY/TeraTerm)를 통해 접근할 수 있는 원격 장치의 CLI용입니다. 이는 디버거에게 원격 시스템에 대한 완전한 접근 권한을 제공하며, 직접적인 UART 연결을 시뮬레이션합니다.

주요 요점

  • UART 부재 문제 해결: 제안된 방법은 보드에 기존 UART가 없더라도 사용 가능한 CAN 버스를 활용하여 임베디드 시스템 디버깅을 위한 완전한 CLI를 가능하게 합니다.
  • 대용량 데이터 전송을 위한 ISO-TP 활용: ISO-TP 프로토콜은 메시지를 분할하고 재조립하여 8바이트 CAN 패킷 제한을 극복하므로, CLI 명령 및 로그 전송에 매우 중요합니다.
  • 브리지로서의 CANshell 유틸리티: PC의 특수 CANshell 유틸리티는 표준 TCP 클라이언트(PuTTY, TeraTerm)와 CAN 버스 사이에서 효율적인 중개자 역할을 하여 투명한 데이터 전송을 보장합니다.
  • 양방향 통신 및 로깅: 이 시스템은 ECU로 명령을 전송하는 것과 비동기 로그 및 응답을 수신하는 것을 모두 지원하며, 로그를 ISO-TP로 직접 리디렉션하는 기능도 갖추고 있습니다.
  • 다용도성 및 확장성: 이 접근 방식은 표준 프로토콜과 널리 사용 가능한 하드웨어를 기반으로 하므로 광범위한 임베디드 프로젝트에 적용할 수 있습니다.

— Editorial Team

Advertisement 728x90

다음 읽기