嵌入式系统调试:基于ISO-TP和CANshell的CAN总线命令行界面实现
开发嵌入式系统时,受限于调试接口,常常面临诸多挑战。当传统的UART接口不可用,但CAN总线却存在时,远程交互的替代方法变得至关重要。本文将介绍一种通过CAN总线实现命令行界面(CLI)的方法,该方法结合了ISO-TP协议和专门的CANshell工具,为微控制器提供了一个全面的调试环境。
在没有UART的情况下进行调试的挑战
经典的嵌入式系统调试传统上依赖于UART等串行接口。然而,在实际项目中,特别是在开发专用电子板时,UART可能不存在或被保留用于其他目的。在这种情况下,当有多个CAN端口可用时,如何利用CAN总线传输调试信息和控制命令就成了一个亟待解决的问题。
主要涉及的复杂性包括:
- 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总线之间的桥梁。
工作原理:
- 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解析器的代码示例如下:
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_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_puts和iso_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
暂无评论。