Die Firmware des TWI-Master teilt sich in folgende wesentlichen Funktionsblöcke auf:
Zur Unterstüzung dieser Funktionen sind folgende Einheiten vorhanden:
Auf der Paltine wurde kein IPS-Adapter vorgesehen. Ein möglicher Firmware-Update kann über einen Bootloader geschehen.
Verbesserungsideen für eine spätere Version gibt es ebenfalls.
Download:
.hex-File
Quellen als AVR Studio Projekt
Hinweis: Das Projekt wurde so erweitert, dass der TWI-Master auch mit einem ATmega48 funktioniert (hatte gerade keinen ATmega8 zur Hand). Allerdings klappt es dann mit dem Bootloader nicht. Der Controller muss ausgebaut und extern programmiert werden.
Nach einem Reset des Systems übertägt der TWI-Master dem TWI-Host den Grund für den Reset. Dieser kann dann darauf angemessen reagieren. Damit die Firmware die Reset-Ursache möglichst treffend ermitteln zu kann, wird das MCU Control and Status Register gleich zu Beginn der Restart-Sequenz ausgelesen bevor es noch durch andere Programmteile überschreiben werden kann (s. auch Reset-Einheit und Reset-Gründe).
// ***********************************************************************************************
// Reset-Quelle auswerten
// ***********************************************************************************************
// Die Variable RstTrigger wird gefüllt, bevor ein Watchdog-Reset absichtlicht herbei geführt wird
// und muss auswertet werden, wenn ein Watchdog-Reset erkannt wird.
uint8_t RstTrigger __attribute__((section (".noinit")));
// RstSource enthält den codierten Reset-Grund. Er wird noch vor dem eigentlichen
// Programmstart ermittelt.
uint8_t RstSource __attribute__((section (".noinit")));
static void __attribute__((section(".init3"), naked, used))
init3 (void)
{ uint8_t mcusr = MCUSR;
MCUSR = 0;
switch(mcusr)
{ case _BV(PORF):
RstSource = 0x00;
break;
case _BV(EXTRF):
RstSource = 0x01;
break;
case _BV(WDRF):
if(RstTrigger)
RstSource = RstTrigger;
else
RstSource = 0x02;
RstTrigger = 0;
break;
}
}
Der Code zur Ermittlung der Ursache des Reset wird in der Section .init3 abgelegt. Diese Section wird vom Linker ganz am Anfang, gleich nach der Belegung des Stackpointers in die Initialisierungssequenz des C-Programms eingefügt, andere Programmteile können die benötigten Daten nicht verfälschen(siehe auch Anatomie: C bis auf's Byte geschaut). Der Speicher für die Reset-Ursache liegt in der Section .noinit, wird also von der Variablen-Initialisierung verschont.
Nach der Initalialisierung der C-Laufzeit-Umgebung werden in main() wie üblich zunächst sämtliche Komponenten initialisiert:
int main(void)
{ // ***********************************************************************************************
// Initialisierung
// ***********************************************************************************************
DDR(PowerPort) |= PowerPinMask; // Pins für anschaltbare Stromversorgung auf Output.
DDR(SignalLedPort) |= SignalLedPinMask; // Pin für Signal-LED auf Output.
ActionLedInit(); // Ports und Timer für Action-LED initialisieren.
TwiInitialise(); // I2C-Library initialisieren.
InitTime(); // Timer für Systemzeit.
InitTrigger(); // Trigger-Einheit initialisieren.
ActionLedOn(); // Action- und Singal-LED zeigen Programmstart an.
SignalLedPort |= SignalLedPinMask;
_delay_ms(100);
// Prüfen, ob (noch) durch den TWI-Host eine Break-Condition erzeugt wird.
bool toggle = false;
while ( (PIN(UsartPort) & UsartRxMask) == 0) // warten, bis Break beendet.
{ toggle = ! toggle; // Beide LEDs blicken im Wechselteakt.
if (toggle)
{ ActionLedOn();
SignalLedPort &= ~SignalLedPinMask;
}
else
{ ActionLedOff();
SignalLedPort |= SignalLedPinMask;
}
_delay_ms(50);
}
ActionLedOff(); // Beide LEDs wieder aus.
SignalLedPort &= ~SignalLedPinMask;
UsartInit(); // USART initialisieren. Sendet zuerst Break.
wdt_enable(WDTO_250MS ); // Watchdog einschalten.
USARTPutc(APP_START + RstSource); // Melden, dass das System (neu) gestartet wurde.
sei(); // Interrupts freigeben.
Dies sind:
Das benutzte Makro DDR() ist in GeneralDefinitions.h und die Port-Konstanten in Ressources.h hinterlegt.
Die Kommandos werden in eine Endlos-Schleife eingelesen, auswertet, ausgeführt und das Ergebnis zurückgemeldet.
while(1)
{ uint8_t command;
uint8_t rtc;
// ***********************************************************************************************
// Kommando empfangen und auswerten
// ***********************************************************************************************
command= UsartGetCommand(); // erstes Befehls-Byte empfangen, dabei Watchdog zurücksetzen.
ActionLedStart(); // Action-LED einschalten.
if ((command & 0x80) && (command != (Trigger + 0x80))) // Bei Kommando Trigger wird das Signal als Kommando erzeugt.
TriggerPulse(); // Trigger-Signal erzeugen.
command &= 0x7F; // Trigger-Bit abtrennen.
// Kommando-Typ ermitteln und Ausführung ansteuern.
// Die Kommandos decken den vollen Zahlen-Umfang von 0..0x7F ab.
if (command <= LastSingelByteCmd)
rtc = ExecSingleByteCommand(command);
else if(command <= LastPinCmd)
rtc = ExecPinCommand(command);
else if (command <= LastDoubleByteCmd)
rtc = ExecDoubleByteCommand(command);
else if (command <= LastWriteTwiPacketCmd)
rtc = ExecWriteTwiPacketCommand(command);
else if (command <= LastWriteTwiRegisterCmd)
rtc = ExecWriteTwiRegisterCommand(command);
else
rtc = ExecSetBitRateCommand(command);
// ***********************************************************************************************
// Ergebnis zurückmelden
// ***********************************************************************************************
... (s.u.)
UsartGetCommand() wartet auf das nächste Kommando. Dabei wird dafür gesorgt, dass der Watchdog zurück gesetzt wird ( s. auch Reset-Einheit).
Wenn ein Kommando empfangen wurde, wird zunächst das Trigger-Bit (Bit 7) abgetrennt. Danach wird ggf. ein angeforderter Trigger-Impuls ausgelöst und die Action-LED eingeschaltet (s. auch Trigger-Einheit).
Der Kommando-Code wird analysiert und an die entsprechende Ausführungsroutine weiter geleitet. In den Ausführungsroutinen sind jeweils die Kommandos zusammengefasst, die eine ähnliche Kommando-Datenstruktur haben, also z.B. mit einem Byte als Code auskommen (ExecSingleByteCommand) oder bei denen ein Pin in gleicher Art im Code verschlüsselt ist (ExecPinCommand), usw.
Die einzelnen Kommando-Codes sind in GlobalConstants.h hinterlegt. Die Zahlencodes der Kommandos sind passend zu den Ausführungs-Routinen gruppiert. Der jeweils letzte Code einer Gruppe ist separat als Last...Cmd (z.B. LastSingelByteCmd) hinterlegt. Bei der Abfrage zur gruppenzugehörigkeit wird immer dieser Last...Cmd-Code abgefragt. Bei Korrekturen an der Struktur muss deshalb die oben aufgeführte Abfrage-Logik nicht jedes Mal angepasst werden. Es reicht die Konstanten-Definitionen anzupassen.
Im Anschluss an die Kommando-Ausführung wird das Ergebnis zurück geliefert.
(s.o.)...
// ***********************************************************************************************
// Ergebnis zurückmelden
// ***********************************************************************************************
UsartPutc(rtc);
if (rtc == SUCCESS_1B) // Ergebnis befindet sich in ioBuffer[0]
UsartPutc(ioBuffer[0]);
else if (rtc == SUCCESS_2B) // Ergebnis befindet sich in ioBuffer[0] und ioBuffer[1]
{ UsartPutc(ioBuffer[0]);
UsartPutc(ioBuffer[1]);
}
else if (rtc == SUCCESS_NB)
{ UsartPutc(ioBufferLength); // Ergebnis befindet sich in ioBuffer, Anzahl Bytes in ioBufferLength
for (uint8_t i=0; i < ioBufferLength; i++)
UsartPutc(ioBuffer[i]);
}
} // while
} // main
Jedes ausgeführte Kommando liefert einen Returncode, der zunächst übertragen wird. Die Codes geben auch Auskunft darüber, wie viele weitere Daten-Bytes zurückgemeldet werden müssen. Diese werden direkt anschließend übertragen.
Damit ist die Kommando-Sequenz beendet und der TWI-Master ist für das nächste Kommando bereit.
Wie in "Kommandos empfangen und auswerten" beschrieben, werden die Kommandos je nach Anzahl der zu erwartenden Daten-Bytes an die entsprechenden Service-Funktionen weitergeleitet. Diese empfangen zunächst die weiteren benötigten Daten und verteilen dann die Ausführung in einer großen switch-Anweisung. Jede einzelne Kommando-Ausführung liefert eine individuelle Rückmeldung, d.h. jeder case-Block wird mit einem return abgeschlossen. Hier ein Code-Auszug, der das Prinzip verdeutlicht:
uint8_t ExecDoubleByteCommand(TwiCommand Command)
{ uint8_t Data = UsartGetcWait(); // Datenbyte empfangen. Ist bei TWI-Kommandos
// die Slave-Adresse.
switch (Command)
{
// --- ResetTime ---------------------------------------------------------------------------------
case SetResetTime:
if (Data > 100)
return INVALID;
ResetTime = Data;
return SUCCESS;
...
Vor dem Rücksprung werden evtl. zurück zu meldende Daten in der Variablen ioBuffer abgelegt. Bei einigen Befehlsrückmeldungen wird auch der Return Code im niederwertigen Nibble um zurückzugebende Informationen angereichert.
Der Code für die meisten Kommandos ist unspektakulär (s.o. SetResetTime()). Neben den TWI-Methoden sind das Schalten der TWI-Leitungen und das der Pull-Up-Widerstände ein wenig aufwändiger. Diese drei Vorgänge werden im Folgenden näher erläutert.
Hierdurch wird die TWI-Einheit abgeschaltet und die manuelle Kontrolle über die Signalleitungen übernommen. Solch eine Funktionalität ist z.B. notwendig, um meinen I²C-Bootloader zu betreiben.
Hier gilt es zu verhindern, dass keine ungewollten Zwischenzustände entstehen und somit z.B. versehentlich eine Start-Bedingung erzeugt wird.
// Bei SetTwixx ist es wichtig, dass nicht unabsichtlich eine Start-Bedingung erzeugt wird!
// a) TWI-Unit ist aktiviert. Dann sind die Pins auf "Open Collector".
// b) Die Pins stehen bereits auf Output.
case SetTwi00: // Auftrag: SDA=0, SCL=0
TwiPort &= ~(SDAMask | SCLMask); // Pins auf 0. Evtl. Pull-Up-Widerstände sind aus.
TWCR = 0; // Twi-Unit ausschalten.
DDR(TwiPort) |= SDAMask | SCLMask; // Pins auf Output. Output jetzt 0.
return SUCCESS;
case SetTwi01: // Auftrag: SDA=0, SCL=1
TwiPort = (TwiPort & ~SDAMask) | SCLMask; // Pins auf passend.
TWCR = 0; // Twi-Unit ausschalten.
DDR(TwiPort) |= SDAMask | SCLMask; // Pins auf Output.
return SUCCESS;
case SetTwi10: // Auftrag: SDA=1, SCL=0
TwiPort = (TwiPort & ~SCLMask) | SDAMask; // Pins auf passend.
TWCR = 0; // Twi-Unit ausschalten.
DDR(TwiPort) |= SDAMask | SCLMask; // Pins auf Output.
return SUCCESS;
case SetTwi11: // Auftrag: SDA=1, SCL=1
TwiPort |= SDAMask | SCLMask; // Pins auf passend.
TWCR = 0; // Twi-Unit ausschalten.
DDR(TwiPort) |= SDAMask | SCLMask; // Pins auf Output.
return SUCCESS;
case EnableTwi:
DDR(TwiPort) &= ~(SDAMask | SCLMask); // Pins auf Input.
TwiPort &= ~(SDAMask | SCLMask); // Pins auf Low, interne Pull-Up aus.
TwiInitialise();
return SUCCESS;
Das Prinzip soll an SetTwi10 erläutert werden. Der Auftrag ist: Die TWI-Einheit abschalten SDA auf Output High und SCL auf Low zu schalten. Hierzu sind zwei Fälle zu unterscheiden: erster Fall, die TWI-Einheit ist aktiv und besitzt die Kontrolle über die Signalleitungen und zweiter Fall, sie besitzt die Kontrolle nicht mehr, d.h. es wurde bereits ein SetTwiXX-Kommando ausgeführt.
Gehen wir zunächst davon aus, dass die TWI-Einheit aktiviert ist. In diesem Fall werden die Leitungen im Open-Collector-Modus betrieben. Sie werden mit Hilfe von Pull-Up-Widerständen auf High gehalten. Die zugehörigen Data Direction Register (DDR) sind 0.
TwiPort = (TwiPort & ~SCLMask) | SDAMask;
bewirkt nun, dass der interne Pull-Up-Widerstand für SDA dazu geschaltet wird. Damit ändert sich der Zustand der Leitung nicht. Lediglich der resultierende Pull-Up-Widerstand wird durch die Parallelschaltung der internen und externen Widerstände verringert.
TWCR = 0;
schaltet die TWI-Einheit aus. Die Pins gehen über in den Standard-Input-Modus (DDR ist 0, s.o.). Die Pull-Up-Widerstände halten die Leitungen weiterhin auf High-Level.
DDR(TwiPort) |= SDAMask | SCLMask;
Die Pins werden auf Output geschaltet und nehmen den gewünschten Ausgangzustand an.
Der andere Fall liegt vor, wenn die TWI-Einheit bereits abgeschaltet ist, also bereits ein SetTwiXX-Kommando ausgeführt wurde.
TwiPort = (TwiPort & ~SCLMask) | SDAMask;
bewirkt nun direkt, dass der gewünschte Ausgangszustand angenommen wird.
TWCR = 0;
hat keine Wirkung, die TWI-Einheit ist bereits angeschaltet.
DDR(TwiPort) |= SDAMask | SCLMask;
hat ebenfalls keine Wirkung. Die Pins befinden sich bereits im Output-Modus.
Sind die Pull-Up-Widerstände abgeschaltet, müssen diese zunächst wieder angeschaltet werden.
DDR(TwiPort) &= ~(SDAMask | SCLMask);
schaltet die Pins in den Input-Modus. Die Pull-Up-Widerstände (ggf. Kombination aus internen und externen) legen die Signalleitungen auf High-Level.
TwiPort &= ~(SDAMask | SCLMask);
schaltet die internen Pull-Up-Widerstände ab.
TwiInitialise();
die TWI-Einheit wird wieder eingeschaltet.
Auch hier gilt es zu verhindern, dass keine ungewollten Zwischenzustände entstehen und z.B. versehentlich eine Start-Bedingung erzeugt wird.
Wenn die Pull-Up-Widerstände abgeschaltet werden sollen, ist die Situation noch recht einfach. Die zugehörigen Pins werden hochohmig geschaltet und evtl. interne Pull-Up-Widerstände abgeschaltet.
...
case SetTwiPullUp_None: // Auftrag: Pull-Up-Widerstände abschalten.
DDR(PullUpPort) &= ~PullUpAllPinMask; // Pins hochohmig. Jetzt liegt noch ggf.
// ein interner Pull-Up vor.
PullUpPort &= ~PullUpAllPinMask; // Alle Ports auf Low und damit Abschaltung
// der internen Pull-Up-Widerstände.
PullUpState = PullUpNone;
return SUCCESS;
...
Wenn die Pull-Up-Widerstände angeschaltet werden sollen, muss auf den aktuellen Zustand der Widerstände geachtet werden.
...
case SetTwiPullUp_5K: // Auftrag 5K-Widerstände einschalten.
switch (PullUpState)
{ ...
case PullUpNone:
PullUpPort |= PullUp5kPinMask; // Erst den internen Pull-Up einschalten,
DDR(PullUpPort) |= PullUp5kPinMask; // damit beim Ändern auf Output die Leitung
// nicht versehentlich auf Null gezogen wird.
PullUpState = PullUp5k;
return SUCCESS;
...
In dieser Situation sind alle Pins des PullUpPort hochohmig (DDR=0) und die internen Pull-Up-Widerstände sind ausgeschaltet (PORT=0). Weder die externen Pull-Up-Widerstände noch der Steuer-Ports nehmen Einfluss auf SCL oder SDA.
PullUpPort |= PullUp5kPinMask;
schaltet die Ports so, dass nach dem Umschalten in den Output-Modus bereits die richtigen Ausgangszustände vorliegen. Solange die Ports jedoch im Input-Modus sind, bewirkt die Anweisung das Einschalten der internen Pull-Up-Widerstände. SCL und SCA werden also über die internen Pull-Up-Widerstände der 5K-Pins auf High-Level gezogen.
DDR(PullUpPort) |= PullUp5kPinMask;
schaltet die Pull-Up-Ports auf Output. Jetzt liegt das korrekte Signal vor. SCL und SCA werden über 5K-Pull-Up-Widerstände auf High-Level gezogen.
...
case SetTwiPullUp_5K: // Auftrag 5K-Widerstände einschalten.
switch (PullUpState)
{ ...
case PullUp50k:
PullUpPort |= PullUpAllPinMask; // Jetzt sind die 50K-Widerstände und
// die internen Pull-Ups der 5k-Pins parallel.
DDR(PullUpPort) = (DDR(PullUpPort) & ~PullUp50kPinMask) | PullUp5kPinMask;
// Jetzt sind die 5K-Widerstände und die
// internen Pull-Ups der 50k-Pins parallel.
PullUpPort &= ~PullUp50kPinMask; // 5k jetzt allein.
PullUpState = PullUp5k;
return SUCCESS;
...
Die Situation ist wie folgt: Die 50K-Pins befinden sich im Output-Modus (DDR=1) auf High-Level (PORT=1). Die 5K-Pins befinden sich im Input-Modus (DDR=0) auf Low-Level (PORT=0). Die zugehörigen internen Pull-Up-Widerstände sind abgeschaltet. SDA und SCL werden über die angeschalteten externen 50K-Widerstände auf High-Level gehalten.
PullUpPort |= PullUpAllPinMask;
schaltet alle Pins auf High-Level. Dies ist bei den 50K-Pins bereits der Fall. Bei den 5K-Pins werden jetzt die internen Pull-Up-Widerstände hinzugeschaltet. SCL und SDA werden nun über die parallel geschalteten 50K- und internen Pull-Up-Widerstände auf High-Level gehalten.
DDR(PullUpPort) = (DDR(PullUpPort) & ~PullUp50kPinMask) | PullUp5kPinMask;
schaltet die 50K- und 5K-Widerstände wechselseitig um. SCL und SDA werden nun über die parallel geschalteten 5K- und internen Pull-Up-Widerstände auf High-Level gehalten.
PullUpPort &= ~PullUp50kPinMask;
schaltet die internen Pull-Up-Widerstände ab.
Der Umfang der Trigger-Einheit ist recht überschaubar. Sie besteht lediglich aus zwei Inline-Methoden. Eine, die Initialisierungs-Methode, bereitet den Port vor. Die andere gibt einen 10 µs langen High-Puls auf dem Trigger-Pin aus. Beide Methoden sind in Trigger.h hinterlegt.
// ***********************************************************************************************
// Trigger-Einheit initialisieren
// ***********************************************************************************************
static inline
void InitTrigger(void)
{ DDR(TriggerPort) |= TriggerPinMask; // Pin für Trigger auf Output
}
// ***********************************************************************************************
// Triggerpuls erzeugen
// ***********************************************************************************************
static inline
void TriggerPulse(void)
{ TriggerPort |= TriggerPinMask; // Trigger-Pin kurz einschalten.
_delay_us(10); // Tiggerpuls ist konstant 10 µs lang.
TriggerPort &= ~TriggerPinMask;
}
Das Bit 7 des Kommando-Codes gibt an, ob direkt vor der Kommando-Ausführung ein Trigger-Impuls ausgegeben werden soll (Bit 7=1).
Weiterhin gibt es ein separates Kommando, zur Auslösung eines Trigger-Impulses. Ist bei diesem Kommando das Trigger-Bit gesetzt, wird nur ein Trigger-Impuls generiert. Der andere wird unterdrückt.
Die Action-LED dient zur Anzeige einer Kommando-Ausführung. Sie wird zu Beginn der Kommando-Ausführung eingeschaltet und von Timer0 nach ca. 160 ms wieder ausgeschaltet.
Trifft innerhalb dieser Zeit ein weiterer Befehl ein, leuchtet die LED durchgehend, bis eine Befehlspause von min. 160 ms vorliegt. Die zugehörigen Funktionen sind als Inline-Methoden in ActionLed.h hinterlegt. ActionLed.c enthält die ISR des Timers.
Irgendein Wert zwischen 100 und 200 ms Leuchtdauer ist ein guter Kompromiss zwischen Sichtbarkeit und der Option, einzelne Kommandos unterscheiden zu können. 160 ms ergeben sich durch eine unkomplizierte Auzählung von Timer-Overflow-Interrupts.
volatile uint8_t ActionLedCount; // Zähler für die eingtreteten Overflow-Interrupts des Timers
#define ActionLedCountMax 5 // Leucht-Dauer = Anzahl Overflow-Interrupts
// (= 5 * 32 ms = ca. 160 ms)
// ***********************************************************************************************
// Action-LED-Einheit initialisieren
// ***********************************************************************************************
static inline
void ActionLedInit(void)
{ DDR(ActionLedPort) |= ActionLedPinMask; // Pin für Action-LED auf Output
TIMSK |= (1<<TOIE0); // Overflow Interrupt erlauben
}
// ***********************************************************************************************
// Action-LED anschalten
// ***********************************************************************************************
static inline
void ActionLedStart(void)
{ ActionLedPort |= ActionLedPinMask;
ActionLedCount = 0;
// Timer0: Prescaler konfigurieren und damit Timer starten
TCCR0 = _BV(CS02) | _BV(CS00); // Prescaler 1024 => Interrupt nach ca. 32 ms bei 8 MHz
}
// ***********************************************************************************************
// Action-LED ausschalten
// ***********************************************************************************************
ISR(TIMER0_OVF_vect, ISR_NOBLOCK) // Genaues Timing ist unwichtig.
{ if (ActionLedCount++ >= ActionLedCountMax) // Zeit abgelaufen?
{ ActionLedPort &= ~ActionLedPinMask; // Action-LED ausschalten
TCCR0 = 0; // Timer ausschalten
}
}
Die ISR hat das Attribut ISR_NOBLOCK, kann also von anderen Interrupts, die zeitkritischer sind, unterbrochen werden.
Zusätzlich zu dieser Funktion wird die Action-LED zusammen mit der Signal-LED nach einem Reset kurz eingeschaltet. Ist die Übertragungsleitung nicht frei (Break-Zustand), blinken diese LEDs, um den Problemzustand zu signalisieren. ActionLedOn() und ActionLedOff() erlauben es, die Action-LED ohne Einfluss durch den Timer zu steuern.
// ***********************************************************************************************
// Action-LED anschalten
// ***********************************************************************************************
static inline
void ActionLedOn(void)
{ ActionLedPort |= ActionLedPinMask;
}
// ***********************************************************************************************
// Action-LED ausschalten
// ***********************************************************************************************
static inline
void ActionLedOff(void)
{ ActionLedPort &= ~ActionLedPinMask;
}
Diese Einheit soll
Es gibt mehrere Möglichkeiten einen Reset auszulösen und einen durchgeführten Reset zu melden. Einfach einen Code per RS232 zu versenden, ist leider nicht sicher. Die Empfangsstation könnte den Code als Datenbyte interpretieren und nicht als Befehlscode oder als Meldung. Gerade dieses wird aber bei einer verloren gegangenen Synchronisation der Fall sein.
Man könnte separate Leitungen zur Auslösung und zur Meldung eines Resets benutzen. Die Steuerleitungen RS232-Schnittstelle würden sich anbieten. Leider muss man für diese Option –neben der Reset-Leitung– mindestens einen weiteren Pin zur Rückmeldung an den PC bereitstellen. Diesen wollte ich aber nicht unbedingt für diesen Zweck opfern. Durch entsprechende Schaltungstechnik und Programmierung muss man außerdem sicherstellen, dass sich diese Leitungen immer in einem wohldefinierten Zustand befinden. Und zuletzt: der eingesetzte USB-RS232-Konverter besitzt solche Leitungen nicht.
Es gibt die Möglichkeit, den Break-Zustand der RS232 für diesen Zweck zu nutzen. Dies ist eine wenig aufwendige und zudem sichere Möglichkeit. Der PC erzeugt eine Break-Bedingungen, wenn ein Reset ausgelöst werden soll. Der TWI-Master erzeugt eine Break-Bedingung, wenn ein Reset durchgeführt wurde. Auf dem PC kann durch die Break-Bedingung ein Ereignis (Event) ausgelöst werden. Auf dem ATmega kann ein Break durch Abfrage der Register erkannt werden.
Um eine aus der Synchronisation gelaufene Firmware wieder einzufangen oder andere unvorhergesehene Fehlersituationen zu beenden, wird der Watchdog benutzt. Der Watchdog ist auf 250 ms eingestellt, ein Zeitdauer, die länger als die absehbare Ausführungsdauer eines jeden Befehls ist. Wird der Watchdog nicht innerhalb dieser Zeit zurückgesetzt, löst er einen Reset aus.
Es gibt nur zwei Situationen, in der die Firmware davon ausgehen kann, das alles in Ordnung ist. Dies ist der Fall, wenn die Ausführung eines Kommandos abgeschlossen und ist und auf die Übertragung des nächsten gewartet wird oder wenn gerade ein Datenbyte übertragen wurde. Nur in diesen Fällen wird der Watchdog zurückgesetzt. Während der Befehlsausführung selbst kontrolliert der Watchdog dann die Abarbeitung.
Sollte hier trotzdem die Synchronisation verloren gegangen sein, würde dies der Host bemerken, weil er z.B. auf eine nicht eintreffende Rückmeldung wartet.
Der TWI-Host kann von sich aus ein Reset der Firmware veranlassen, z.B. wenn er meint, die Synchronisation verloren zu haben. Hierzu erzeugt er eine Break-Bedingung. Eine Break-Bedingung liegt dann vor, wenn die RX-Leitung der RS232 für länger als die Dauer, die zur Übertragung eines einzelnen Zeichens benötigt wird, auf "space"-Niveau liegt (bei der TTL-Variante ist dies Low). In diesem Fall hat das erwartete Stopp-Bit den falschen Wert. Die USART des ATmega zeigt einen Frame-Error an. Dieses Bit wird von der Firmware beim nächsten lesenden Zugriff auf USART ausgewertet. Die Firmware geht daraufhin in eine Endlosschleife und wartet bis der Watchdog einen Reset auslöst.
Eigentlich müsste geprüft werden, ob auch wirklich die ganze Zeit ein Low-Level am RX-Eingang vorgelegen hat, d.h. das Datenbyte im UDR müsste 0x00 sein. Andererseits müsste sowieso auf den Frame-Error reagiert werden. Offensichtlich stimmt auch in diesem Fall etwas mit der Übertragung nicht. Ein Reset auszulösen, ist auch in diesem Fall eine gute Idee. Also reicht es, auf einen Frame-Error zu testen.
Der Fall, dass der TWI-Master auf Grund einer Fehlersituation gar nicht mehr zum Zugriff auf die USART kommt, ist unproblematisch, da in diesem Fall der Watchdog ebenfalls einen Reset auslösen würde. Es würde lediglich ein anderer Grund für den Reset zurück gemeldet.
Der zugehörige Code:
uint8_t UsartGetCommand (void)
{ uint8_t status;
uint8_t udr;
// Warten, bis etwas empfangen wird.
while ( (status = USART_STATUS) == 0)
wdt_reset();
udr = UDR;
// Wenn UDR nicht 0x00 ist, liegt ein 'normaler' Frame-Error vor.
// Auch dann ist es sinnvoll ein Reset durchzuführen. Der TWI-Host kann sich dann wieder synchronisieren.
// Eine extra Fehlerbehandlung scheint zu aufwendig, da wahrscheinlich selten.
if (status & _BV(FE)) // Break erkannt
{ if (udr) // Frame-Error
RstTrigger = FRAME_ERROR;
else // Break
RstTrigger = RESET_COMMAND;
while(1); // warten, bis Watchdog zuschlägt.
}
Die Schaltung besitzt einen Taster, über den ein manueller Reset ausgelöst werden kann. Der TWI-Host erkennt dieses Ereignis ebenfalls an der vom TWI-Master generierten Break-Bedingung. Der manuelle Reset kann an den entsprechenden Bits der Startmeldung erkannt werden.
In der Bereitschaftsmeldung (APP_START) während des Restarts wird im niederwertigen Nibble der Reset-Grund verschlüsselt.
Wert | Bedeutung |
---|---|
0x00 | Power-on Reset |
0x01 | Manueller Reset |
0x02 | Watchdog Reset |
0x03 | Reset-Kommando |
0x04 | Frame Error erkannt |
0x05 | Buffer Overrun der RS232 |
An der Ansteuerung der USART ist nichts besonderes. Es stehen Funktionen zur Initialisierung der Einheit und zum Einlesen und zur Ausgabe von Zeichen zur Verfügung. Die meisten Funktionen werden nur ein einziges Mal benutzt oder sind sehr kurz. Sie sind daher als inline-Methoden deklariert und werden über die Header Datei USART.h eingebunden.
Bei der Initialisierungsmethode ist zu beachten, dass diese gleich zu Beginn eine Break-Bedingung generiert. Diese dient dem TWI-Host zur Erkennung eines Reset und zur Wiederherstellung der Synchronisation (s. Reset-Einheit).
Die Ansteuerung der TWI-Einheit ist an vielen Stellen beschrieben (z.B. Mikrocontroller.net oder Roboternetz). In den folgenden Abschnitten wird deshalb nur auf die Besonderheiten eingegangen.
Zur Festlegung der Bitrate muss zum einen der Prescaler und zum anderen das TWI Bit Rate Register (TWBR) passend belegt werden, wozu eigentlich zwei Bytes notwendig wären. In der Praxis sind jedoch nicht alle Wertkombinationen sinnvoll. Deshalb werden elf typische Kombinationsmöglichkeiten (Enumeration TwiBitRate) vorgegeben. Der Datenaustausch mit dem TWI-Host erfolgt dann als Index eines Arrays (TwiBitRateCodes [ ]) mit diesen elf Möglichkeiten. Die eigentliche Übertagung dieses Index erfolgt im niederwertigen Nibble von SetBitRate bzw. SUCCESS_DATA.
Die aktuell gültige Bitrate (genauer: deren Code) ist in ActualBitRateCode hinterlegt. Bei der Initialisierung der TWI-Einheit wird diese Variable ausgewertet.
I.d.R. kann die Ausführungsdauer einer TWI-Funktion nicht genau vorhergesagt werden. Es besteht die Möglichkeit, dass der Bus aktuell von einem anderen TWI-Master arbitriert wird oder der Slave nutzt die Möglichkeit des Clock-Stretching oder es liegt eine Fehlfunktion vor. Auf jeden Fall wird nach etwa 250 ms der Watchdog zuschlagen. Dies ist jedoch nur bei einer Fehlfunktion der Firmware erwünscht, Probleme mit der Bus-Arbitrierung sollen abgefangen und geordnet behandelt werden.
An allen Stellen, an denen eine der o.g. Situationen auftreten können, wird deshalb die Zeitdauer kontrolliert und gegen TwiTimeout abgeglichen. TwiTimeout kann auf Werte zwischen 1 und 100 ms eingestellt werden. Die Voreinstellung ist 100 ms.
Hinweis: Das Timeout-Verhalten wurde nicht durchgetestet. Es ist also gelegentlich mit einem unvorhergesehenen Watchdog-Reset zu rechnen.
Es kann sein, dass zum Zeitpunkt, bei dem mit einer Übertragung über den I²C-Bus begonnen werden soll (Start-Bedingung), der Bus von einem anderen Master arbitriert wird. Die Firmware bietet die Möglichkeit, innerhalb des Rahmens den TwiTimeout vorgibt, mehrfach zu versuchen, den Bus zu belegen. Dies wird über den Parameter WaitState geregelt.
Bei Beendigung einer Übertragung über den I²C-Bus kann die Arbitrierung des Busses behalten werden. In diesem Fall wird keine Stopp-Bedingung erzeugt. Die nächste Übertragung beginnt dann mit dem Erzeugen einer Restart-Bedingung. Dieser Mechanismus ist z.B. wichtig, wenn mit I²C-Registern gearbeitet wird. Hierbei ist es notwendig, zunächst eine Registeradresse übertragen zu können und dann, ohne Verlust der Bus-Arbitrierung, in den Lese-Modus wechseln zu können. Das Verhalten der Methoden wird über den Parameter TwiEndAction geregelt.
Für Testzwecke werden elementare Methoden zum Betrieb des I²C-Busses bereit gestellt. Dies sind:
uint8_t doTwiStart(WaitState WaitState); // Start-Bedingung erzeugen.
uint8_t doTwiStop(void); // Stopp-Bedingung erzeugen.
uint8_t doTwiWrite(uint8_t Data); // Ein Byte ausgeben.
uint8_t doReadACK(uint8_t *Data); // Ein Byte einlesen mit anschließendem ACK (d.h. weitere Bytes sollen gelesen werden).
uint8_t doReadNAK(uint8_t *Data); // Ein Byte einlesen mit anschließendem NAK (d.h. der Lesevorgang ist abgeschlossen).
Hinweis: Die komplexeren TWI-Methoden wurden i.d.R. nicht aus den Elementarfunktion zusammengesetzt, sondern werden komplett neu codiert.
Die Firmware stellt Methoden zur Verfügung, die einen kompletten Datenaustausch über den I²C-Bus ermöglichen, ohne sich um die Details kümmern zu müssen. Dies sind zum einen
uint8_t doTwiReadPackage(uint8_t Address, uint8_t PackeLength, uint8_t *Data, WaitState WaitState, TwiEndAction EndAction);
uint8_t doTwiWritePackage(uint8_t Address, uint8_t PackeLength, uint8_t *Data, WaitState Wait, TwiEndAction EndAction);
die eine kontinuierlichen Datenstrom ermöglichen und zum anderen
uint8_t doTwiReadRegister(uint8_t Address, uint8_t Register, uint8_t PackeLength, uint8_t *Data, WaitState WaitState, TwiEndAction EndAction);
die den Register-Mechanismus implementieren. Anmerkung: Eine Funktion wie doTwiReadRegister(...) ist nicht erforderlich. Es kann doTwiWritePackage(...) verwandt werden. Das erste gesendete Byte muss die Registeradresse enthalten.
Die Länge eines Datenpakets ist auf 255 Bytes begrenzt.
I²C-Slaves, die nur ein Byte benötigen, wie der Port-Expander PCF8574, werden über WritePackage und ReadPackage mit der Paketlänge 1 angesprochen.
Um die Schaltung einfacher zu gestalten, wurde auf ein ISP-Interface verzichtet. Zum Firmware-Update wird ein serieller Bootloader eingesetzt. Wenn alles schief geht, kann man immer noch den ATmega aus seiner Fassung herausnehmen und extern umprogrammieren. Den Bootloader habe ich bei society of robots gefunden. Folgt man den Links, findet man den Bootloader zum Download und auch grafische Oberflächen zur Bedienung. Eine genaue Funktionsbeschreibung habe ich noch nicht entdeckt. Aber es stehen es Reihe von Programm-Quellen zur Verfügung, aus denen man alles Notwendige ableiten kann.
Zur Sicherheit hier alles über den Bootloader noch einmal in Kopie:
Verbessern könnte man folgendes: