返回首页

通过 CAN 的 CLI:使用 ISO-TP 和 CANshell 调试嵌入式系统

深入探讨通过 CAN 总线使用 ISO-TP 协议和 CANshell 实用工具为嵌入式系统实现 CLI。没有 UART 的有效调试解决方案。

通过 CAN 的 CLI:使用 ISO-TP 和 CANshell 调试嵌入式系统
Advertisement 728x90

嵌入式系统调试:基于ISO-TP和CANshell的CAN总线命令行界面实现

开发嵌入式系统时,受限于调试接口,常常面临诸多挑战。当传统的UART接口不可用,但CAN总线却存在时,远程交互的替代方法变得至关重要。本文将介绍一种通过CAN总线实现命令行界面(CLI)的方法,该方法结合了ISO-TP协议和专门的CANshell工具,为微控制器提供了一个全面的调试环境。

在没有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接口。

为了应对这些挑战,我们建议使用ISO-TP(ISO 15765-2)协议,该协议允许在CAN总线上传输任意长度的消息,并开发一个专门用于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转换器,因为它易于获取且可以通过串行COM端口使用SLCAN协议进行交互。JZ-F407VET6开发板作为目标平台,它具有两个CAN端口。

PC端的TCP服务器使用WinSock2函数纯C语言实现。这使得ISO-TP协议组件和FIFO队列在微控制器固件和PC应用程序中都能最大程度地重用。

微控制器通过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-TP消息接收完成后,会调用此iso_tp1_rx_done回调函数。它提取数据并将其传递给cli_process_data函数进行处理。

为了将微控制器的日志重定向到ISO-TP,使用了一个特殊命令tpw 1。执行此命令后,所有日志函数(LOG_ERRORLOG_INFOLOG_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比特率: CAN总线速度,单位为kbit/s(例如,500000表示500 kbit/s)。
  • 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,另一个用于远程设备的CLI,后者可以通过TCP客户端(PuTTY/TeraTerm)使用指定的TCP端口访问。这为调试器提供了对远程系统的完全访问权限,模拟了直接的UART连接。

关键要点

  • 解决UART缺失问题: 即使板上没有传统的UART接口,通过利用可用的CAN总线,本文提出的方法也能为嵌入式系统调试提供一个功能完善的CLI。
  • 利用ISO-TP进行大数据传输: ISO-TP协议对于传输CLI命令和日志至关重要,因为它通过分段和重组消息克服了8字节CAN数据包的限制。
  • CANshell工具作为桥梁: PC上专门的CANshell工具充当标准TCP客户端(PuTTY、TeraTerm)与CAN总线之间的有效中介,确保透明的数据传输。
  • 双向通信和日志记录: 该系统支持向ECU发送命令以及接收异步日志和响应,并能够将日志直接重定向到ISO-TP。
  • 多功能性和可扩展性: 该方法基于标准协议和广泛可用的硬件,使其适用于各种嵌入式项目。

— Editorial Team

Advertisement 728x90

继续阅读