ACHTUNG: Dieses AppNote ist aus 2004 und nicht mehr 100% aktuell. Laut Meinung Dritter ist der Inhalt auch nicht in allen Bereichen auf den avr-gcc anwendbar.
Laut AVR035 wurden die AVRs extra so entwickelt, dass sie besonders effektiv die Instruktionen auswerten und ausführen können, die ein C Compiler erzeugt.
Ich werde einige Beispiele aus AVR035 übernehmen und frei übersetzten.
Die 32 Register des AVRs sind der Schlüssel für ein effektives C-Programm. Die Register haben die selbe Funktion wie der traditionelle Accumulator, nur das es davon 32 gibt. In einem Taktzyklus kann der AVR Daten aus den Registern laden, verarbeiten und das Ergebnis zurück in das Register schreiben. Wenn die Daten in den 32 Registern gespeichert sind, besteht keine Notwendigkeit sie für die Verarbeitung ins/ aus dem SRAM zu laden. Manche Register kann man zu 16 bit Pointerregister kombinieren, um effektiv auf die Daten im SRAM und Flash zugreifen zu können. Die AVR Architektur hat vier Speicherpointer. Der Stackpointer (SP) wird benutzt um die Rücksprungadresse einer Funktion zu speichern. Ein Pointer wird als Parameter Stack benutzt. Die letzten 2 Pointer werden benutzt um Daten zu laden und zu speichern.
Das folgende Beispiel zeigt, wie effektiv Pointer für typische C Operationen benutzt werden.
char *pointer1 = &table[0]; char *pointer2 = &table[49]; *pointer1++ = *--pointer2;
Das produziert folgenden Assembler Code
LD R16,-Z ; Pre-decrement Z pointer and load data ST X+,R16 ; Store data and post increment
Alle Pointer Operationen sind Single-Word Instruktionen, die in 2 Take abgearbeitet werden.
1. Indirektes adressieren. Zum adressieren von Arrays und Pointer Variablen:
*pointer = 0×00;
2. Indirekte Adressierung mit Offset: Erlaubt den Zugriff auf alle Elemente in einer Struktur, indem ein Zeiger auf das erste Element erstellt und ein Offset dazu addiert wird. Der Pointer selbst wird nicht verändert. Wird auch benutzt um auf Variablen im Stack und Arrays zuzugreifen.
3. Indirekte Adressierung mit nachfolgender Erhöhung des Zeigers: Für effektives Adressieren von Arrays und Pointer Variablen:
*pointer++ = 0xFF;
4. Indirekte Adressierung mit vorläufiger Erhöhung des Zeigers: Für effektives Adressieren von Arrays und Pointer:
*–pointer = 0xFF;
Die Pointer werden auch benutzt, um auf den Flash Speicher zuzugreifen. Der Speicher kann auch direkt adressiert werden. Das gibt Zugriff auf den gesamten Speicher mit einer 2-Word Instruktion.
Die AVR Instruktionen beinhalten Befehle für 16bit Variablen. Arithmetische Operationen und Vergleiche von 16bit Variablen werden mit 2 Instruktionen in 2 Takte erledigt. 32 bit Operationen und Vergleiche werden in 4 Instruktionen und 4 Takte bearbeitet.
Nach dem Einschalten oder einem Reset muss der Stack Pointer initialisiert werden, bevor irgendeine Funktion aufgerufen werden kann.
Folgender Code, zeigt wie der I/O Speicher benutzt werden kann. Der Assembler Code für jede Zeile steht unter der C Code Zeile.
#include <io8515.h> /* Include header file with symbolic names */ __C_task void main(void) { char temp; /* Declare a temporary variable */ /* To read and write to an I/O register */ temp = PIND; /* Read PIND into a variable */ // IN R16,LOW(16) ; READ I/O memory TCCRO = 0x4F; /* Write a value to an I/O location */ // LDI R17,79 ; Load value // OUT LOW(51),R17 ; Write I/O memory /* Set and clear a single bit */ PORTB |= (1<<PIND2); /* PIND2 is pin number(0..7) in port */ // SBI LOW(24),LOW(2) ; Set bit in I/O ADCSR &= ~(1<<ADEN); /* Clear ADEN bit in ADCCR register */ // CBI LOW(6),LOW(7) ; Clear bit in I/O /* Set and clear a bitmask */ DDRD |= 0x0C; /* Set bit 2 and 3 in DDRD register */ // IN R17,LOW(17) ; Read I/O memory // ORI R17,LOW(12) ; Modify // OUT LOW(17),R17 ; Write I/O memory ACSR &= ~(0x0C); /* Clear bit 2 and 3 in ACSR register */ // IN R17,LOW(8) ; Read I/O Memory // ANDI R17,LOW(243) ; Modify // OUT LOW(8),R17 ; Write I/O Memory /* Test if a single bit is set or cleared */ if(USR & (1<<TXC)) /* Check if UART Tx flag is set */ { PORTB |= (1<<PB0) // SBIC LOW(11),LOW(6) ; Test direct on I/O // SBI LOW(24),LOW(0) while(!(SPSR & (1<<WCOL))) { /* Wait for WCOL flag to be set */ // ?0003:SBIS LOW(14),LOW(6); Test direct on I/O // RJMP ?0003 /* Test of an I/O register equals a bitmask */ if(UDR & 0xF3) /* Check if UDR register "and" 0xF3 is non-zero { } // IN R16,LOW(12) ; Read I/O memory // ANDI R16,LOW(243) ; "And" value // BREQ ?0008 //?0008: } /* Set and clear bits in I/O registers can also be declared as macros */ #define SETBIT(ADRESS,BIT) (ADRESS |= (1<<BIT)) #define CLEARBIT(ADRESS,BIT) (ADRESS &= ~(1<<BIT)) /* Macro for testing of a single bit in an I/O location */ #define CHECKBIT(ADRESS,BIT) (ADRESS & (1<<BIT)) /* Example of usage */ if(CHECKBIT(PORTD,PIN1)) /* Test if PIN 1 is set */ { CLEARBIT(PORTD,PIN1); /* Clear PIN 1 on PORTD */ } if(!(CHECKBIT(PORTD,PIN!))) /* Test if PIN 1 is cleared */ { SETBIT(PORTD,PIN1); /* Set PIN 1 on PORTD */ }
Weil der AVR ein 8-bit Mikrocontroller ist, sollten 16- und 32-bit Variablen nur dort eingesetzt werden, wo es wirklich nötig ist. Das folgende Beispiel zeigt die Codegrösse für eine Endlosschleife mit 8- und 16-bit lokale Variablen.
8-Bit Counter:
unsigned char count8 = 5; /* Declare a variable, assing a value */ // LDI R16,5 ; Init variable do /* Start a loop */ { }while(--count8); /* Decrement loop counter and check for zero */ // ?0004:DEC R16 ; Decrement // BRNE ?0004 ; Branch if not equal 16-Bit Counter: unsigned int count16 = 6; /* Declare a varibale, assign a value */ // LDI R24,LOW(6) ; Init variable, low byte // LDI R25,0 ; Init variable, high byte do /* Start a loop */ { }while(--count16); /* Decrement loop counter and check for zero */ // ?0004:SBIW R24,LWRD(1) ;Subtract 16-bit value // BRNE ?0004 ; Branch if not equal
| Variable | Code Size (Bytes) |
|---|---|
| 8-bit | 6 |
| 16-bit | 8 |
Note: Benutze immer die kleinste mögliche Variablengrösse. Das ist besonders wichtig für globale Variablen.
Ein C-Programm ist unterteilt in Funktionen die kleine oder große Aufgaben erledigen. Die Funktion bekommt Daten durch Parameter übergeben und kann auch wieder Daten zurückgeben. Variablen in einer Funktion werden lokale Variablen genannt. Variablen außerhalb einer Funktion werden globale Variablen genannt. Wenn lokale Variablen zwischen Funktionsaufrufen erhalten bleiben sollen, müssen sie als static deklariert werden.
Globale Variablen die außerhalb einer Funktion deklariert werden, werden dem SRAM zugeordnet. Das SRAM ist für globale Variablen reserviert und kann für nichts anderes verwendet werden. Das ist eine bedenkliche SRAM Verschwendung. Zu viele globale Variablen machen den Code weniger lesbar und schwer zu modifizieren.
Lokale Variablen werden bevorzugt in den Registern gespeichert wenn sie deklariert werden. Die lokale Variable bleibt im selben Register bis die Funktion zuende ist, oder die Variabel nicht länger gebraucht wird. Globale Variablen müssen vom SRAM in die Register geladen werden, bevor sie benutzt werden können.
Das folgende Beispiel zeigt die Unterschiede in Codegrösse und Geschwindigkeit für lokale Variablen gegenüner globale.
char gloabal; /* This is a gloabl variable */ __C_task void main(void) { char local; /* This is a local variable */ gloabl -= 45; */ Subtraction with gloabl variable */ // LDS R16,LWRD(global) ; Load variable from SRAM ti register R16 // SUBI R16,LOW(45) ; Perform subtraction // STS LWRD(global),R16 ; Store data back in SRAM local -= 34; /* SUbtraction with local variable */ // SUBI R16,LOW(34) ; Perform subtraction directly on local variable in register R16 }
Das LDS und STST (Lade und Speichere direkt vom/zum SRAM) wird benutzt um auf Variablen im SRAM zuzugreifen. Es sind 2 Word Instruktionen die 2 Takte brauchen.
| Variable | Code Size (Bytes) | Execution Time (Cycles) |
|---|---|---|
| Global | 10 | 5 |
| Local | 2 | 1 |
Eine lokale statische Variable wird in ein Register beim Start der Funktion geladen. Ist die Funktion zu Ende, wird die Variable zurück in das SRAM gespeichert. Statische Variablen sind darum effizienter als globale Variablen, wenn sie mehr als einmal in der Funktion benutzt werden.
Um die Zahl der globalen Variablen gering zu halten, können Funktionen mit Parameter aufgerufen werden und einen Variable zurückgeben. Bis zu zwei Parameter von einfachen Variablen (char,int, long, float, double) können zwischen den Funktionen über die Register R16-R23 getauscht werden. Mehr als zwei Parameter und komplex Variablen (arrays, structs) sind entweder auf dem Stack oder werden über das SRAM mit einen Pointer getauscht.
Wenn globale Variablen benötigt werden, sollten sie wenn immer es passend ist in Strukturen abgelegt werden. So ist es dem Compiler möglich sie direkt zu adressieren. Das folgende Beispiel Zeigt Code für globale Variablen gegenüber globale Variablen in Datenstrukturen.
typedef struct { char sec; }t; t global; char min; __C_task void main(void) { t *time = &gloabl; // LDI R30,LOW(global) ; Init Z pointer // LDI R31,(global >> 8) ; Init Z high byte if(++time->sec == 60) { // LDD R16,Z+2 ; Load with displacement // INC R16 ; Increment // STD z+2,R16 ; Store with displacement // CPI R16,LOW(60) ; Compare // BRNE ?0005 ; Branch if not equal } if( ++min == 60) { // LDS R16,LWRD(min) ; Load direct from SRAM // INC R16 ; Increment // STS LWRD(min),R16 ; Store direct to SRAM // CPI R16,LOW(60) ; Compare // BRNE ?0005 ; Branch if not equal } }
Wenn globale Datenstrukturen genutzt werden benutzt der Compiler den Z Pointer und LDD und STD (Load/store with displacement) um die Daten zu erreichen. Ohne Datenstrukturen wird LDS und STS (Load/store direct to SRAM) benutzt.
Code Size for Gloable Variables
| Variable | Code Size (Bytes) |
|---|---|
| Structure | 10 |
| Non-structure | 14 |
Das beinhaltet nicht die Initialisierung des Z Pointers (4 Byte) für die globale Datenstrukturen. Wenn nur auf 1Byte zugegriffen wird ist die Code Grösse die selbe, aber wenn die Datenstruktur zwei oder mehr Byte enthält, wird der Code effizienter wenn man globale Datenstrukturen benutzt.
Die meisten Programme benötigen globale Flags um den Programmfluss zu kontrollieren. Platziert man die Flags in globale Variablen, ist es sehr ineffizient, weil die Variablen erst in die Register geladen werden müssen, bevor die Flags getestet werden können. Um den Flag Test zu optimieren, können sie entweder in ein fest zugeordnetes Register oder in eine unbenutzte I/O Speicherstelle abgelegt werden.
Anmerkung: To place a global variable in a dedicated register requiers that the register ist allocated in the compiler options setup. This can only be done with IAR Compiler v2.
Anmerkung von mir: Das lohnt sich nicht zu übersetzen. Wer nutzt denn schon den IAR ;)
Unbenutzte I/O Locations können für globale Variablen ausgenutzt werden, wenn die entsprechende Peripherie nicht benutzt wird. Z.B. wenn kein UART benutzt wird, ist das UART Baud Raten Register (UBRR) frei, oder beim EEPRM das EEPROM Daten Register (EEDR) und das EEPRIM Adresse Register (EEAR).
Das I/O Speicher kann sehr effizient addersiert werden und Locations kleiner als 0x1F sind besonders geeignet weil sie Bitweise zugreifbar sind. I/O Memory grösser als 0x1F sind weniger effizient, aber immer noch besser als globale SRAM Variablen.
Das folgende Beispiel zeigt den Unterschied in Codegröße für SRAM Variablen, I/O oder spezielle Register für globale Flags.
typedef struct bitfield { // Bitfield structure unsigned char bit0:1; unsigned char bit1:1; unsigned char bit2:1; }bitfield;
Global Flags in RAM
bitfield global_flags; // Bitfield in a global Variable global_flag.bit1 = 1; // LDI R30,LOW(global_flag) // LDI R31,(global_flag) >> 8 // LD R16,Z // ORI R16,0x02 // ST Z,R16
Code Size: 10 Bytes
Global Flags in Register R15. Note that bitfields are not allowed as register Variables.
__no_init __regvar unsigned char reg_flag@ 15; // Global register R15 reg_flags |= 0x01; // SET // BLD R15,0
Code Size: 4 Bytes
Global Flags in Free I/O registers Above 0x1F
__no_init volatile bitfield high_io_flags@0x55; // Bitfield in I/O above 0x1F, note that 0x30 offset is added to adress high_io_flag.bit2 = 1; // IN R16,0x35 // ORI R16,0x04 // OUT 0x35,R16
Code Size, 6 Bytes
Global Flags in Free I/O Register Below 0x1F
__no_init volatile bitfield low_io_flag@0x35; // Bitfield in I/O location below 0x1F low_io_flag.bit1 = 1; // SBI 0x015, 0x01
Code Size: 2 Byte
Diskussion
Hi, auf mikrocontroller.net läuft gerade eine Diskussion über diesen Artikel. Vielleicht sollte man den mal an den avr-gcc und die aktuellen AVR anpassen. Oder zumindest deutlicher darauf hinweisen das es sich um eine AppNote aus 2004 handelt und sich nicht auf den avr-gcc bezieht.
Gustav