Debugging eingebetteter Systeme: CLI über CAN mit ISO-TP und CANshell
Die Entwicklung eingebetteter Systeme stellt aufgrund begrenzter Debugging-Schnittstellen oft eine Herausforderung dar. Wenn die traditionelle UART-Schnittstelle nicht verfügbar ist, aber CAN-Busse vorhanden sind, werden alternative Methoden für die Ferninteraktion unerlässlich. Dieser Artikel beschreibt einen Ansatz zur Implementierung einer Befehlszeilenschnittstelle (CLI) über einen CAN-Bus unter Verwendung des ISO-TP-Protokolls und eines spezialisierten CANshell-Dienstprogramms, das eine umfassende Debugging-Umgebung für Mikrocontroller bietet.
Die Herausforderung des Debuggings ohne UART
Das klassische Debugging eingebetteter Systeme stützt sich traditionell auf serielle Schnittstellen wie UART. In realen Projekten, insbesondere bei der Entwicklung spezialisierter Elektronikplatinen, kann UART jedoch fehlen oder für andere Zwecke reserviert sein. In solchen Szenarien, in denen mehrere CAN-Ports verfügbar sind, stellt sich die Aufgabe, den CAN-Bus für die Übertragung von Debug-Informationen und Steuerbefehlen zu nutzen.
Die primären Komplexitäten sind:
- Begrenzte CAN-Paketgröße: Ein Standard-CAN-Paket enthält nur 8 Byte Daten. Dies ist bei weitem nicht ausreichend für die Übertragung langer CLI-Befehle, die 150 Byte oder mehr erreichen können, sowie für den Empfang umfangreicher Antworten und Protokolle. Die Lösung dieses Problems erfordert ein Protokoll, das Daten aus mehreren CAN-Paketen fragmentieren und wieder zusammensetzen kann.
- Trennung von Datenströmen: Remote-Debugging erfordert oft eine gleichzeitige Interaktion mit der eigenen CLI des Debuggers und der CLI des entfernten Geräts (Steuergerät). Es ist entscheidend, diese Ströme effizient zu trennen und auf einem einzigen Terminal anzuzeigen.
- Log-Umleitung: Eingebettete Firmware gibt Protokolle typischerweise an UART oder SWO aus. Um CLI über CAN zu verwenden, ist ein Mechanismus erforderlich, um diese Protokolle über ISO-TP an die gewählte CAN-Schnittstelle umzuleiten.
Um diese Herausforderungen zu bewältigen, schlagen wir die Verwendung des ISO-TP-Protokolls (ISO 15765-2) vor, das die Übertragung von Nachrichten beliebiger Länge über einen CAN-Bus ermöglicht, sowie die Entwicklung eines spezialisierten Konsolen-Dienstprogramms, CANshell, für den PC.
Lösungsarchitektur: ISO-TP und CANshell
Die vorgeschlagene Lösung basiert auf der Kombination des ISO-TP-Protokolls und des CANshell-Dienstprogramms. ISO-TP gewährleistet die zuverlässige Übertragung großer Datenmengen über CAN, während CANshell als Brücke zwischen einem TCP-Client (z. B. PuTTY oder TeraTerm) und dem CAN-Bus fungiert.
Funktionsprinzip:
- Auf der PC-Seite: Das CANshell-Dienstprogramm startet einen TCP-Server und öffnet einen TCP-Socket. Dieser Socket ist auf der einen Seite mit dem ISO-TP-Protokoll und auf der anderen Seite mit dem TCP-Client verbunden. Verbindungsparameter (ISO-TP-Adressen, COM-Port, CAN-Geschwindigkeit) werden dem Dienstprogramm über Kommandozeilenargumente übergeben, was eine Automatisierung durch Skripte ermöglicht.
- Auf der Mikrocontroller- (Steuergerät-) Seite: Ein ISO-TP-Stack wird implementiert, der Daten vom CAN-Bus empfängt und an den CLI-Befehls-Parser weiterleitet. Für die Ausgabe von Protokollen und Antworten wird ein Umleitungsmechanismus verwendet, der es ermöglicht, diese Daten über ISO-TP zurückzusenden.
- Interaktion: Wenn der TCP-Client einen Befehl sendet, fängt CANshell diesen ab, kapselt ihn in ISO-TP-Pakete und sendet ihn über einen USB-CAN-Konverter an den CAN-Bus. Auf dem Mikrocontroller setzt der ISO-TP-Stack die Pakete wieder zusammen, und der Befehl wird dem CLI-Parser zugeführt. Antworten und Protokolle vom Mikrocontroller werden ähnlich in ISO-TP gekapselt, über den CAN-Bus an CANshell und dann an den TCP-Client übertragen.
Komponentenimplementierung und Codebeispiele
Für die praktische Implementierung wurde der USB-CAN-Konverter USB2CANFD_V1 aufgrund seiner Verfügbarkeit und einfachen Interaktion über einen seriellen COM-Port mittels des SLCAN-Protokolls gewählt. Das Entwicklungsboard JZ-F407VET6 mit seinen zwei CAN-Ports diente als Zielplattform.
Der TCP-Server auf der PC-Seite ist in reinem C unter Verwendung von WinSock2-Funktionen implementiert. Dies ermöglicht eine maximale Wiederverwendung von ISO-TP-Protokollkomponenten und FIFO-Warteschlangen sowohl in der Mikrocontroller-Firmware als auch in der PC-Anwendung.
Der Empfang von Daten auf dem Mikrocontroller über ISO-TP und deren Weitergabe an den CLI-Parser sieht wie folgt aus:
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
}
}
Dieser iso_tp1_rx_done-Callback wird nach dem vollständigen Empfang einer ISO-TP-Nachricht aufgerufen. Er extrahiert die Daten und übergibt sie zur Verarbeitung an die Funktion cli_process_data.
Um Protokolle vom Mikrocontroller an ISO-TP umzuleiten, wird ein spezieller Befehl tpw 1 verwendet. Nach dessen Ausführung beginnen alle Logging-Funktionen (LOG_ERROR, LOG_INFO, LOG_DEBUG) Daten in die TxFIFO-Warteschlange zu schreiben, die der ersten ISO-TP-Instanz zugeordnet ist.
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++;
}
}
}
Die Funktionen iso_tp1_puts und iso_tp1_putc legen Zeichen oder Zeichenketten in den TxFifo-Puffer ab, von wo aus sie über ISO-TP gesendet werden.
Datenübertragungssteuerung und Synchronisation
Die IsoTpTx-Aufgabe auf dem Mikrocontroller ist dafür verantwortlich, Daten aus TxFifo zu extrahieren und über ISO-TP zu senden, sobald die Protokoll-Zustandsmaschine bereit ist.
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;
}
Diese Code-Snippets veranschaulichen die Datenübertragungslogik. Die Funktion iso15765_tx_next prüft, ob der ISO-TP-Transceiver im Leerlauf ist, und falls ja, extrahiert sie Daten aus TxFifo und initiiert die Übertragung.
Auf der PC-Seite, im CANshell-Dienstprogramm, werden über ISO-TP empfangene Daten ebenfalls in eine Warteschlange gelegt, von wo aus sie dann an den TCP-Client gesendet werden.
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;
}
Diese Funktionen sind für den Empfang von ISO-TP-Daten auf der PC-Seite und deren Übertragung in den für den TCP-Socket vorgesehenen Puffer verantwortlich.
Die letzte Phase umfasst die Übertragung von Daten vom TCP-Server an den verbundenen TCP-Client (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;
}
Dieser Code zeigt, wie Daten aus dem Socket-Puffer mit der send-Funktion von WinSock2 an den Client gesendet werden.
Ein wichtiger Aspekt ist die Synchronisation von ISO-TP-Sitzungen. Es wurden Situationen beobachtet, in denen CANshell durch den Datenfluss überfordert wurde, was zu Nachrichtenverlust führte. Die Lösung bestand darin, ISO-TP-Sitzungen zeitlich zu staffeln, um beiden Protokollinstanzen zu ermöglichen, sich zurückzusetzen und Daten erfolgreich zu verarbeiten.
Verwendung des CANshell-Dienstprogramms
Um das CANshell-Dienstprogramm zu starten, müssen eine Reihe von Positionsargumenten angegeben werden:
- COM-Port: Die Nummer des COM-Ports, an den der USB-CAN-Konverter angeschlossen ist (z. B.
COM3). - CAN-Bitrate: Die CAN-Bus-Geschwindigkeit in kbit/s (z. B.
500000für 500 kbit/s). - ISO-TP-Adresse des PCs: Die eigene ISO-TP-Adresse des CANshell-Dienstprogramms im Netzwerk (z. B.
0xC). - ISO-TP-Adresse des Zielgeräts: Die ISO-TP-Adresse des Mikrocontrollers (Steuergerät), mit dem die Kommunikation aufgebaut werden soll (z. B.
0xA). - TCP-Port: Die TCP-Portnummer, die CANshell für Client-Verbindungen öffnen wird (z. B.
50003).
Beispiel für den Start des Dienstprogramms:
CANshell.exe COM3 500000 0xC 0xA 50003
Nach dem Start öffnet das CANshell-Dienstprogramm zwei Terminals: eines für die eigene CLI und ein weiteres für die CLI des entfernten Geräts, auf das über einen TCP-Client (PuTTY/TeraTerm) unter Verwendung des angegebenen TCP-Ports zugegriffen werden kann. Dies verschafft dem Debugger vollen Zugriff auf das entfernte System und simuliert eine direkte UART-Verbindung.
Wichtige Erkenntnisse
- Behebung des UART-Mangels: Die vorgeschlagene Methode ermöglicht eine vollwertige CLI für das Debugging eingebetteter Systeme, selbst wenn die traditionelle UART-Schnittstelle auf der Platine fehlt, indem vorhandene CAN-Busse genutzt werden.
- Nutzung von ISO-TP für große Datenübertragungen: Das ISO-TP-Protokoll ist entscheidend für die Übertragung von CLI-Befehlen und Protokollen, da es die 8-Byte-CAN-Paketbeschränkung durch Fragmentierung und Wiederzusammensetzung von Nachrichten überwindet.
- CANshell-Dienstprogramm als Brücke: Das spezialisierte CANshell-Dienstprogramm auf dem PC fungiert als effizienter Vermittler zwischen Standard-TCP-Clients (PuTTY, TeraTerm) und dem CAN-Bus und gewährleistet eine transparente Datenübertragung.
- Bidirektionale Kommunikation und Protokollierung: Das System unterstützt sowohl das Senden von Befehlen an das Steuergerät als auch den Empfang asynchroner Protokolle und Antworten, mit der Möglichkeit, Protokolle direkt an ISO-TP umzuleiten.
- Vielseitigkeit und Skalierbarkeit: Der Ansatz basiert auf Standardprotokollen und weit verbreiteter Hardware, wodurch er für eine breite Palette eingebetteter Projekte anwendbar ist.
— Editorial Team
Noch keine Kommentare.