Erzielt werden soll eine sichere, transparente Daten-Übertragung zwischen µCs und PCs sowie uCs bzw PCs untereinander. Die geplanten Übertragungsmedien sind im Moment:
- UART (RS232), vor allem zur Anbindung von uCs an PCs
- LAN (IP), vor allem zur Anbindung von PCs untereinander (z.B. Steuerrechner - GUI)
Dabei betrachtet dieses Projekt die IP-Funktionalitäten als "black-box" und alles unterhalb der üblichen Schnittstelle (sockets) wird elegant als Level-0 betrachtet.
- i2c (spätere Ausbaustufe), vor allem zur Anbindung von uCs untereinander
Ziel der Schicht 0 muß es sein für die oberen Schichten Datenpakete mit einer bestimmten Länge zu übertragen. Dabei sollen die oberen Schichten nichts über das Medium (UART, LAN, i2c) wissen müssen. Da die Datenbytes alle Werte von 0-255 einnehmen können, muß die jeweilige Schicht geeignete Kontrollmechanismen definieren.
Inhaltsverzeichnis
Level-0 UART (point-to-point)
Die Implementierung der Schicht 0 hängt stark von den verwendeten Medien ab. Die Arbeitsgruppe ist übereins gekommen, Daten über den UART wie folgt zu übertragen: Die Nachrichten wird mit einem STX-Zeichen begonnen, dann die Daten, eine Kontrollsumme und als Abschluß ein ETX-Zeichen.
START....daten....CHECK.ENDE
Jetzt kann natürlich zwischen den Daten jederzeit ein Zeichen drin sein, das denselben Wert hat wie eine Start- oder Endekennung. Daher wird "Bytestuffing" verwendet
Byte-Stuffing
Es gibt drei besondere Zeichen
- STX als Start-Kennung
- ETX als Ende-Kennung UND
- PFX als Daten-Kennung (durch dieses Zeichen wird das nachfolgende zu einem normalen Datenbyte)
Am besten ein Beispiel:
Oben ist eine Nachricht, die ein Zeichen enthält, das denselben Wert hat wie die Endekennung (ETX) Bis zu dem Zeichen werden die Daten normal übertragen. Dann wird aber das PFX eingefügt und danach der Rest.
Der Empfänger findet das Startzeichen STX und beginnt, die Zeichen nach der Reihe zu übernehmen. Dann entdeckt er PFX. Das speichert er zwar nicht, aber dadurch enterpretiert er das Folgezeichen, das ja eigentlich eine Endekennung wäre, nicht als solches und gibt es zu den normalen Daten dazu. So gehts weiter bis zum Ende ETX, das ja "blank" dasteht, also sozusagen "echt" ist.
Es ist immerhin möglich, daß ein Teilnehmer beim Empfangen irgendwie nicht gut drauf ist, und er erst genau zwischen PFX und Folgezeichen die Bytes wieder richtig mitkriegt. Er würde also denken, daß dieses Folgezeichen ein Kontrollzeichen ist und letzlich Müll empfangen. Für diesen Fall wurde vorgeschlagen, dieses Folgezeichen gleich überhaupt durch eine Addition unkenntlich zu machen. Derjenige, der das PFX mitgekriegt hat, weiß das ja und kann wieder subtrahieren, unser Schläfer weiß das aber eben nicht, kann aber auch kein (falsches) Steuerzeichen erkennen. Beim Wiedereinstieg an einer beliebigen anderen Stelle kann sowieso nicht Schlimmes passieren, denn bis zum nächsten un-prefixten STX wird ja alles ignoriert.
Kontrollsumme BCC
- Beginnend mit NULL, werden alle Datenzeichen (ohne PFX, STX, ETX und andere Kontrollzeichen) mit Exklusiv-Oder "übereinandergelegt". Das Ergebnis hängt der Sender an die Daten an. Es wird zusammen mit den Daten geprefixed.
- Der Empfänger tut das Gleiche, dann aber vergleicht er sein Ergebnis mit dem Prüf-Byte, das der Sender weggeschickt hat.
Stimmt das Prüfbyte nicht, wird die Nachricht verworfen. Das ist zwar auch nicht todsicher, aber immerhin beruhigend. Wenn sich das nicht bewährt, kann immer noch ein CRC-Verfahren festgelegt werden.
Praxis
- Maximale Frame-Size
#define FRAME_C_MAX 127
Das ist die maximale Netto-Länge zwischen STX und ETX, exklusive. Auch das scheint für µC etwas groß, aber es wird davon ausgegangen, daß die real benötigten Messages weit kürzer sein werden, d.h. das regelt sich vermutlich von selbst durch die Vereinbarungen und Restriktionen in den oberen Layern
- Kontrollzeichen
#define CTL_M_MASK 0xF8 #define CTL_M_ADON 0x08 #define CTL_C_BASE 0xA8 #define CTL_C_STX CTL_C_BASE + 1 #define CTL_C_ETX CTL_C_BASE + 2 #define CTL_C_PFX CTL_C_BASE + 3
- Code-Beispiel/Muster für Paket-senden
( transmit() stellt die Funktion für den tatsächlichen physischen Output dar).
static unsigned char bTxBcc; // Checksum für BCC // -------------------------------------------------------------- void TxStartFrame ( void ) { bTxBcc = 0 transmit ( CTL_C_STX ) } // -------------------------------------------------------------- void TxCloseFrame ( void ) { TxSendStuffByte ( bTxBcc ) // auch das BCC mit ev. prefixed werden transmit ( CTL_C_ETX ) } // -------------------------------------------------------------- void TxSendFrameByte ( unsigned char bTxChar) { bTxBcc ^= bTxChar TxSendStuffByte ( bTxChar ) } // -------------------------------------------------------------- void TxSendStuffByte ( unsigned char bTxChar) { if (bTxChar & CTL_M_MASK) == CTL_C_BASE) { transmit ( CTL_C_PFX ) transmit ( bTxChar + CTL_M_ADON ) } else { transmit ( bTxChar ) } }
- Die entsprechende Empfangsroutine ist etwas komplexer und wird wesentlich mehr vom Geschick und den Gewohnheiten des Programmieres bestimmt.
Network Controller/PC RS232 mit Windows
Für unser PC / µC Netzwerk muß auch am PC programmiert werden. Da es doch manchmal Anfragen gibt, stell ich einmal dar, wie man von einem PC-Programm aus an die RS232-Schnittstelle kommen kann. Und zwar im üblichen Modus,
- also 8-Bit,
- ohne Parity,
- ohne Flußkontrolle (3 Draht)
Ich verwende übrigens das MS VisualC++.
Windows-Interface
Open
- Get Handle
char cName[]="\\\\.\\COM1"; HANDLE hFile; hFile= CreateFile(cName,GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0); if(hFile==INVALID_HANDLE_VALUE) // Das war nix, eventuell anderes "COM" versuchen
Das Handle "hFile" wird nun für alle Calls angegeben, wie halt bei Windows üblich.
- Set State
Mit diesem Handle kann (muß) man die Eigenschaften festlegen. Ich sag's ehrlich, mit den Time-Out Argumenten hab' ich nicht weiter 'rumprobiert, mir war nur wichtig, daß es funktioniert. Das letzte Mal hab ich noch "*.SYS" Treiber für das Steinzeit-DOS geschrieben. Also eventuell bitte um Nachsicht.
COMMTIMEOUTS sTo; DCB sDcb; memset(&sDcb,0,sizeof(sDcb)); sDcb.DCBlength = sizeof(sDcb); sDcb.BaudRate = Baud; // 9600 oder eine andere Baudrate sDcb.fParity = FALSE; sDcb.fBinary = TRUE; sDcb.Parity = NOPARITY; sDcb.StopBits = ONESTOPBIT; sDcb.fOutxCtsFlow = FALSE; sDcb.fOutxDsrFlow = FALSE; sDcb.fDtrControl = DTR_CONTROL_ENABLE; sDcb.fRtsControl = RTS_CONTROL_ENABLE; sDcb.fDsrSensitivity = FALSE; sDcb.fAbortOnError = FALSE; sDcb.ByteSize = 8; if(SetCommState(hFile,&sDcb)) { sTo.ReadIntervalTimeout = MAXDWORD; // 0 ms Read-Timeout sTo.ReadTotalTimeoutMultiplier = 0; sTo.ReadTotalTimeoutConstant = 0; sTo.WriteTotalTimeoutMultiplier = 1; // 1*2 ms Write Timeout sTo.WriteTotalTimeoutConstant = 2; if(SetCommTimeouts((HANDLE)hFile,&sTo)) return 1; // O.K. return } CloseHandle(hFile); return 0; // Im Fehlerfall }
Read
- Fragen, ob Daten da sind
COMSTAT sComStat; DWORD dwErrorFlags = 0; ClearCommError(hFile, &dwErrorFlags, &sComStat); sComStat.cbInQue; // Die Anzahl der Bytes im Buffer
- Daten abholen
DWORD dwCount; ReadFile(hFile, Buffer, Max, &dwCount, 0);
- Buffer ein Bereich für die Daten
- Max die Anzahl der gewünschten Bytes
- dwCount die Anzahl der tatsächlich bekommenen Bytes
Write
WriteFile(hFile, Buffer, Count, &dwCount, 0);
- Buffer die zu sendenden Daten
- Max die Anzahl der Bytes
- dwCount die Anzahl der tatsächlich gesendeten Bytes
Close
CloseHandle(hFile);
Anwendung für RN-COMM
Entspricht den Level-0 Spezifikationen.
Allgemeine Definitionen
#define CTL_M_MASK 0xF8 #define CTL_C_BASE 0xA8 #define CTL_M_ADON 0x18 #define CTL_C_STX CTL_C_BASE + 1 #define CTL_C_ETX CTL_C_BASE + 2 #define CTL_C_PFX CTL_C_BASE + 3
Senden
Durch das Protokoll gegeben ist es einfacher, die Zeichen einzeln zu senden und zu lesen. Ich verwende daher eine Zwischenfunktion, die für Einzelzeichen ausgelegt ist. Um mehrbytige Felder zu senden, verwende ich ggf. eine UNION
union { char bByte[8]; short wWord; short iLong; ..usw... } TxUnion; int iX; TxUnion.iLong = ein 32-Bit Wert for (iX = 0; iX < 4; iX++) SendChanData ( TxUnion.bByte[iX]); TxUnion.wWord = ein 16-Bit Wert SendChanData ( TxUnion.bByte[0]); SendChanData ( TxUnion.bByte[1]);
int ComWrite (int Zeichen) { DWORD dwCount; WriteFile(hFile,&Zeichen,1,&dwCount,0); return dwCount; }
- Message-Header
ComWrite (CTL_C_STX); // Schreiben/Senden des Message-Startzeichens bBCC = 0; // Block Check zurücksetzen
- Message-Body
Für jedes Datenzeichen: (+BlockCheck)
void SendChanData ( char bOne ) { bBCC ^= bOne; // Block Check rechnen SendChanChar ( bOne ); // und weiter }
Für jedes Datenzeichen: (+ Kontrolle Sonderzeichen)
void SendChanChar ( char bOne ) { if( (bOne & CTL_M_MASK) == CTL_C_BASE) // Sonderzeichen ? { ComWrite (CTL_C_PFX); // Ja, Präfix senden ComWrite (bOne + CTL_M_ADON); // und sonderzeichen + offset } else ComWrite (bOne); // Zeichen normal 1:1 senden }
- Message-Trailer
SendChanChar (bBCC); // Block Check anfügen ComWrite (CTL_C_ETX); // Message Ende
Empfangen
Man kann das verschieden gestalten. Ich mache es so, daß ich durch einen Timer 10 x in der Sekunde schaue, ob was gekommen ist, und interpretiere dann das, was gerade da ist. Das klingt grausam, hat aber bisher gereicht. Trotzdem wäre wohl eine Interrupt-Routine eleganter.
unsigned char bInChar; if (ComGetReadCount( )) // Wenn was da ist { while (ComRead((void*)&bInChar, 1)) // Zeichen holen { iSts = SniffChan( bInChar); // Zeichen interpretieren. // wenn eine Message komplett ist // kommt eine 1 zurück, sonst eben 0 if (iSts) { // eine Message wird verarbeitet } } }
Die Funktion
int SniffChan( unsigned char bInChar)
hat insgesamt drei Zustände:
- Neutral, noch kein STX-Zeichen erhalten.
- Message, wir haben ein STX-Zeichen erhalten, da gibt's dann zwei Möglichkeiten
- Daten normal, Steuerzeichen werden erkannt und interpretiert
- Daten prefixed, Was auch kommt, es gilt als (modifiziertes) Datenbyte
Neutral
if (bInChar == CTL_C_STX) { // Rücksetzen Daten buffer bBCC = 0; // Rücksetzen Block Check // Ändern Zustand auf "Message" } .. // alles andere ist im Moment uninteressant
Daten Normal
switch (bInChar) { case CTL_C_STX: // Start condition // Ein Start-Zeichen mittendrin. // d.h. die eigentlich laufende Message ist irgendwie verstümmelt gewesen. // Also alles wieder zurücksetzen und eine neue Message anfangen // Rücksetzen Daten buffer // Rücksetzen Block Check // (Zustand bleibt auf "Daten normal") break; case CTL_C_PFX: // Prefix // Zustand ändern auf "Daten Prefixed" break; case CTL_C_ETX: // End Condition // Block Check prüfen. Der Check ist ok, wenn bBCC == 0 // Zustand ändern auf "Neutral" return(1); // Es ist eine komplette Message da break; default: ... // Zeichen speichern bBCC ^= bInChar; // Blockcheck rechnen break; } return (0); // Zeichen verarbeitet, KEINE komplette Message da
Daten Prefixed
.... // ( Zeichen - CTL_M_ADON) speichern bBCC ^= ( Zeichen - CTL_M_ADON) // Blockcheck rechnen return(0);