Das sieht nicht komisch aus. So sieht normaler MCU-Code tatsächlich aus.
Was Sie hier haben, ist ein Beispiel für das Konzept von Peripheriegeräten mit Speicherzuordnung . Grundsätzlich hat die MCU-Hardware spezielle Positionen im SRAM-Adressraum der ihr zugewiesenen MCU. Wenn Sie in diese Adressen schreiben, steuern die Bits des Bytes, das in die Adresse n geschrieben wurde, das Verhalten des Peripheriegeräts m .
Grundsätzlich haben bestimmte Speicherbänke buchstäblich kleine Drähte, die von der SRAM-Zelle zur Hardware verlaufen. Wenn Sie in dieses Byte eine "1" in dieses Bit schreiben, wird diese SRAM-Zelle auf ein logisches Hoch gesetzt, wodurch dann ein Teil der Hardware eingeschaltet wird.
Wenn Sie sich die Header für die MCU ansehen, gibt es große Tabellen mit Schlüsselwort- <-> Adresszuordnungen. So werden Dinge wie TCCR1B
etc ... beim Kompilieren gelöst.
Dieser Speicherzuordnungsmechanismus wird in MCUs äußerst häufig verwendet. Die ATmega-MCU im Arduino verwendet sie ebenso wie die MCU-Serien PIC, ARM, MSP430, STM32 und STM8 sowie viele MCUs, mit denen ich nicht sofort vertraut bin.
Arduino- Code ist das seltsame Zeug, mit Funktionen, die indirekt auf die MCU-Steuerregister zugreifen. Dies sieht zwar etwas "schöner" aus, ist aber auch viel langsamer und benötigt viel mehr Programmraum.
Die mysteriösen Konstanten sind alle im ATmega328P-Datenblatt ausführlich beschrieben , das Sie unbedingt lesen sollten, wenn Sie mehr als nur gelegentlich das Umschalten von Stiften auf einem Arduino tun möchten.
Wählen Sie Auszüge aus dem oben verlinkten Datenblatt aus:
So setzt zum Beispiel TIMSK1 |= (1 << TOIE1);
das Bit TOIE1
in TIMSK1
. Dies wird erreicht, indem die binäre 1 ( 0b00000001
) um TOIE1
Bits nach links verschoben wird , wobei TOIE1
sie in einer Header-Datei als 0 definiert wird. Dies wird dann bitweise in den aktuellen Wert von ODER-verknüpft TIMSK1
, wodurch dieses Bit effektiv hoch gesetzt wird.
Wenn TIMSK1
wir uns die Dokumentation für Bit 0 von ansehen, können wir sehen, dass es als beschrieben wird
Wenn dieses Bit in eins geschrieben wird und das I-Flag im Statusregister gesetzt ist (Interrupts global aktiviert), wird der Timer / Counter1-Überlauf-Interrupt aktiviert. Der entsprechende Interrupt-Vektor (siehe „Interrupts“ auf Seite 57) wird ausgeführt, wenn das in TIFR1 befindliche TOV1-Flag gesetzt ist.
Alle anderen Zeilen sollten auf die gleiche Weise interpretiert werden.
Einige Notizen:
Sie können auch Dinge wie sehen TIMSK1 |= _BV(TOIE1);
. _BV()
ist ein häufig verwendetes Makro, das ursprünglich aus der AVR libc-Implementierung stammt . _BV(TOIE1)
ist funktional identisch mit (1 << TOIE1)
, mit dem Vorteil einer besseren Lesbarkeit.
Möglicherweise sehen Sie auch Zeilen wie: TIMSK1 &= ~(1 << TOIE1);
oder TIMSK1 &= ~_BV(TOIE1);
. Dies hat die entgegengesetzte Funktion TIMSK1 |= _BV(TOIE1);
, indem er führt zum Löschen des Bits TOIE1
in TIMSK1
. Dies wird erreicht, indem die von erzeugte Bitmaske genommen _BV(TOIE1)
, eine bitweise NOT-Operation an ihr ausgeführt wird ( ~
) und dann TIMSK1
durch diesen NOTed-Wert (0b11111110) UND-verknüpft wird.
Beachten Sie, dass in all diesen Fällen der Wert von Dingen wie (1 << TOIE1)
oder _BV(TOIE1)
zur Kompilierungszeit vollständig aufgelöst wird , sodass sie funktional auf eine einfache Konstante reduziert werden und daher zur Laufzeit keine Ausführungszeit für die Berechnung benötigen.
Richtig geschriebener Code enthält im Allgemeinen Kommentare, die dem Code entsprechen und genau angeben, wozu die Register zugewiesen werden. Hier ist eine ziemlich einfache Soft-SPI-Routine, die ich kürzlich geschrieben habe:
uint8_t transactByteADC(uint8_t outByte)
{
// Transfers one byte to the ADC, and receives one byte at the same time
// does nothing with the chip-select
// MSB first, data clocked on the rising edge
uint8_t loopCnt;
uint8_t retDat = 0;
for (loopCnt = 0; loopCnt < 8; loopCnt++)
{
if (outByte & 0x80) // if current bit is high
PORTC |= _BV(ADC_MOSI); // set data line
else
PORTC &= ~(_BV(ADC_MOSI)); // else unset it
outByte <<= 1; // and shift the output data over for the next iteration
retDat <<= 1; // shift over the data read back
PORTC |= _BV(ADC_SCK); // Set the clock high
if (PINC & _BV(ADC_MISO)) // sample the input line
retDat |= 0x01; // and set the bit in the retval if the input is high
PORTC &= ~(_BV(ADC_SCK)); // set clock low
}
return retDat;
}
PORTC
ist das Register, das den Wert der Ausgangspins im PORTC
ATmega328P steuert. PINC
ist das Register, in dem die Eingabewerte von PORTC
verfügbar sind. Grundsätzlich passieren solche Dinge intern, wenn Sie die Funktionen digitalWrite
oder digitalRead
verwenden. Es gibt jedoch eine Suchoperation, die die "PIN-Nummern" des Arduino in tatsächliche Hardware-Pin-Nummern umwandelt, was irgendwo im Bereich von 50 Taktzyklen dauert. Wie Sie wahrscheinlich erraten können, ist es ein bisschen lächerlich, 50 Taktzyklen für eine Operation zu verschwenden, für die nur 1 erforderlich sein sollte, wenn Sie versuchen, schnell zu fahren.
Die obige Funktion benötigt wahrscheinlich irgendwo im Bereich von 100-200 Taktzyklen, um 8 Bits zu übertragen. Dies beinhaltet 24 Pin-Schreibvorgänge und 8 Lesevorgänge. Dies ist um ein Vielfaches schneller als die Verwendung der digital{stuff}
Funktionen.