ARM-elf-GCC-Tutorial

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Dieses Tutorial behandelt die Programmierung von ARM Mikrocontrollern mithilfe des ARM-elf-GCC Compilers. Die meisten Codebeispiele wurden mit WinARM übersetzt. Vorerst wird sich dieses Tutorial an die LPC-Reihe von NXP richten.

Bezugsquellen

Komplette Boards mit ARM7-Kern kann man von folgenden Webseiten beziehen:

Wenn man allerdings selbst ein ARM7-Board herstellen möchte kann man die Schaltpläne der Olimex-Boards als gute Grundlage nehmen.

Benötigte Programme

  • Optional: lpc21isp Kommandozeilen-Programmiertool für Windows & Linux (in WinARM und Linux-/Mac-Paket bereits enthalten)

Startprobleme

Wenn man vor den ersten Versuchen mit AVR-Mikrocontrollern mit WinAVR und ähnlichen Entwicklungsumgebungen programmiert hat, musste man außer seinem C-Code und dem daraus entstehenden HEX-File nicht viel beachten. Bei der Programmierung von ARM-Mikrocontrollern muss man aber bedenken, dass es (noch? ;-) ) keine Standard-Linkerscripte und -Startupcodes in WinARM gibt. WinAVR nimmt einem diese Arbeit mit Standarddateien ab, so dass man bei WinAVR meistens nicht in Berührung damit kommt. Im Netz kursieren viele dieser Linkerscripte und Startupcodes. Wir empfehlen die von uns getesteten Scripte zu verwenden, damit keine unschönen Phänomene auftreten (zum Beispiel fehlende Interrupts). // Scripte befinden sich vorerst in den WinARM-Examples.

Bevor wir starten

Im AVR-GCC-Tutorial werden Grundlagen erklärt, die sicherlich nützlich sind (Makefiles, Programmablauf in einem Mikrocontroller, etc.). Wir möchten hiermit auf das AVR-GCC-Tutorial verweisen und gehen nicht nochmal auf diese Themen ein.

Außerdem sollte man sich vergewissern dass der Controller mit Hilfe des Bootloaders angesprochen werden kann. Hierzu verwendet man am besten das Flash-Tool von Philips um die DEVICE PART ID auszulesen. Falls dies fehlschlägt, sollte man die Verbindung zum Mikrocontroller überprüfen.

Länge von Variablentypen

Die Länge der jeweiligen Variablentypen unterscheidet sich durchaus von denen des AVRs. Folgendes ist beim ARM-elf-GCC Compiler gültig:

char 1 Byte
short 2 Bytes
int 4 Bytes
long 4 Bytes
long long 8 Bytes
float 4 Bytes
double 8 Bytes

Das erste Programm

Die meisten Sourcecodes wurden für einen LPC2124 mit WinARM compiliert. Hierbei muss man beachten, dass in einigen Header-Dateien die Register (z. B. IOSET, IOCLR etc.) unterschiedliche Namen haben. Ein Blick in die einzubindende Header-Datei ist daher ratsam.

Beispiel:

Ein Ausschnitt aus der Header-Datei lpc2114.h des GNUARM-Projekts (bei WinARM in <arch/philips/lpc2114.h>) (auch gültig für LPC2124):

/*##############################################################################
## GPIO - General Purpose I/O
##############################################################################*/

#define GPIO0_IOPIN     (*(REG32 (0xE0028000)))
#define GPIO0_IOSET     (*(REG32 (0xE0028004)))
#define GPIO0_IODIR     (*(REG32 (0xE0028008)))
#define GPIO0_IOCLR     (*(REG32 (0xE002800C)))

#define GPIO1_IOPIN     (*(REG32 (0xE0028010)))
#define GPIO1_IOSET     (*(REG32 (0xE0028014)))
#define GPIO1_IODIR     (*(REG32 (0xE0028018)))
#define GPIO1_IOCLR     (*(REG32 (0xE002801C)))

Verwendet man z. B. einen LPC2106 und die von der Keil GmbH bereitgestellte Header-Datei für diesen Controller, weichen die Bezeichnungen von denen in der GNUARM-Definition zum Teil deutlich ab. In WinARM ist die Datei von Keil enhalten und entsprechend benannt ("lpc210x_keil.h" aus <arch/philips/lpc210x_keil.h>).

/* General Purpose Input/Output (GPIO) */
#define IOPIN          (*((volatile unsigned long *) 0xE0028000))
#define IOSET          (*((volatile unsigned long *) 0xE0028004))
#define IODIR          (*((volatile unsigned long *) 0xE0028008))
#define IOCLR          (*((volatile unsigned long *) 0xE002800C))

Daher: Wenn Registernamen beim Compilieren nicht bekannt sind, hilft ein Blick in die Header-Datei des Controllers weiter, oder man passt eine projektspezifische Kopie der Datei an.

Nutzung der I/O Ports

Zum Behandeln von I/O-Ports sind die Register "IOPIN", "IOSET", "IOCLR" und "IODIR" nötig.

IOPIN Liest die Zustände des angegebenen Ports ein
IOSET Setzt die angegebenen Pins auf 1
IOCLR Setzt den Zustand des angegebenen Pins auf 0
IODIR Bestimmt welcher Pin ein Ausgang(1) bzw. ein Eingang(0) ist.

In dem folgenden Code wird Pin 25 von PORT0 auf Ausgang geschaltet und danach auf HIGH gelegt:

#include <arch/philips/lpc2114.h>

int main( void ) 
{
  GPIO0_IODIR |= ( 1<<25 ); // Pin 25 auf Ausgang
  GPIO0_IOSET  = ( 1<<25 ); // Pin 25 auf HIGH schalten

  while( 1 ) {  // Endlos-Schleife
  }
  return 0;
}

Wenn man einen Pin wieder auf LOW schalten will, setzt man das entsprechende Bit in IOCLR.

#include <arch/philips/lpc2114.h>

int main( void )
{
  GPIO0_IODIR |= ( 1<<25 ); // Pin 25 auf Ausgang
  GPIO0_IOSET  = ( 1<<25 ); // Pin 25 auf HIGH schalten

  for( int i = 0; i < 300000; i++ ){ // Etwas warten
    asm volatile("nop");
  }

  GPIO0_IOCLR = ( 1<<25 );  // Pin 25 auf LOW schalten

  while( 1 ){  // Endlos-Schleife
  }
  return 0;
}

Doch wie geht man nun mit dem Setzen und Löschen einzelner Bits um? In Variablen/Register benutzt man am besten logische Verknüpfungen, wie sie bereits im AVR-GCC-Tutorial beschrieben werden:

 x |= (1 << Bitnummer);  // wird ein Bit in x gesetzt
 x &= ~(1 << Bitnummer); // wird ein Bit in x geloescht

Wenn allerdings mit den I/O-Ports gearbeitet wird, sollten die IOSET- und IOCLR-Register benutzt werden:

GPIO0_IOSET  = ( 1<<25 );  // Pin 25 auf HIGH schalten
GPIO0_IOCLR  = ( 1<<25 );  // Pin 25 auf LOW schalten
GPIO0_IOCLR |= ( 1<<25 );  // Falsch! IOCLR darf nur geschrieben werden

Die Technik der IOSET-/IOCLR-Register an Stelle klassischer Port-Register vermeidet das in Interrupt-sichere Programmierung von I/O-Ports beschriebene Problem.

Systemeinstellungen (System Control Block)

Dass ein ARM generell komplexer als ein handelsüblicher AVR oder PIC ist, sollte jedem geläufig sein. Hier kann man am Controller jede Menge (falsch) einstellen. In diesem Abschnitt werden die verschiedenen Register und ihre Bedeutungen beim LPC2xxx erklärt:

Phase locked loop

PLLCFG (SCB_PLLCFG) Das PLL Configuration Register hält den Multiplikator für die interne PLL-Schaltung. Diese ermöglicht es, die Taktrate des Prozessors zu erhöhen. Zum Beispiel: Ein angeschlossener Quarz mit 10 MHz x 4 (mit Hilfe der PLL) = 40 MHz Prozessortakt. Bei Bedarf kann auch ein Teiler eingestellt werden.
PLLCON (SCB_PLLCON) Mit dem PLL Control Register kann die PLL aktiviert werden
PLLSTAT (SCB_PLLSTAT) Im PLL Status Register werden Informationen bezüglich der PLL gespeichert, z. B. der aktuelle Multiplikator-Wert.
PLLFEED (SCB_PLLFEED) Damit Änderungen an PLLCON und PLLCFG übernommen werden, muss erst in dieses Register eine "Feed-Sequenz" geschrieben werden. Die Feed-Sequenz wird im Codebeispiel weiter unten dargestellt

Hier ein Codebeispiel zur Initialisierung:

#define FOSC	14745000	// Die Frequenz der Taktquelle
#define PLL_M	4		// Der Multiplikator für den CPU-Takt
#define PLL_P	2		// Der Teiler für F_CCO (muss zwischen 156 MHz und 320 MHz liegen)

#define CCLK	(PLL_M * FOSC) //Die CPU frequenz nochmal als Zahlenwert definiert

// Das PLOCK-Bit im PLLSTAT-Register gibt an, ob die PLL auf die konfigurierte  Frequenz eingestellt ist.
#define PLOCK (1<<10)

/* 
    InitPLL
*/
void InitPLL(void) 
{
  SCB_PLLCFG = (PLL_M-1)|((PLL_P-1)<<5); // M=4 und P=2 (Multiplikatoren von 0 sind nicht erlaubt)

  /*
  Nochmal nach rechnen:
  CPU TAKT = PLL_M * FOSC = 4 * 14745000 Hz = 58980000 Hz
  CCO TAKT = 2 * PLL_P * PLL_M * FOSC = 2 * 2 * 4 * 14745000 Hz = 235920000 Hz

  Mit diesen Werten ist alles innerhalb der Spezifikationen aus dem Datenblatt.  

  Der CCO (Current Controlled Oscillator) ist ein Bestandteil der PLL.
  */

  SCB_PLLCON = 0x01; // PLL aktivieren

  SCB_PLLFEED = 0xAA;  //PLL Feed-Sequenz
  SCB_PLLFEED = 0x55;
  while ( !( SCB_PLLSTAT & PLOCK ) ); // Darauf warten, dass die Änderungen übernommen werden 

  /*
  Mit MAMTIM werden die Waitstates beim Flashspeicherzugriff eingestellt, das Datenblatt empfiehlt folgende Werte:
  1 - bei unter 20 Mhz
  2 - bei 20-40 Mhz 
  3 - bei über 40 Mhz
  */
  MAM_MAMTIM = 3; 

  SCB_PLLCON = 0x03; // PLL aktivieren und mit dem internen Taktgeber verbinden
  SCB_PLLFEED = 0xAA;  //PLL Feed Sequence
  SCB_PLLFEED = 0x55;
}

Warum schreiben wir als Multiplikator (PLL_M-1) und (PLL_P-1) in SCB_PLLCFG? Der Multiplikator 1 wird mit 0 dargestellt. Ein 2x Multiplikator wäre 1 , ein 3x Multiplikator wäre 2, usw.

Man muss also immer "1" von dem gewünschten PLL-Wert abziehen.

Weitere Informationen zur PLL befinden sich im Controller-Handbuch (beim LPC2124 ab Seite 60 und beim LPC2106 ab Seite 43).

VPBDivider

Die gesamte Peripherie ( SPI, UART, etc. ) des ARMs hängt am sogenannten "VLSI Peripheral Bus". Mithilfe des VPBDIV-Registers kann man die Taktfrequenz dieses Busses einstellen.

SCB_VPBDIV = 1; // Teiler auf 1 stellen; Prozessor-Takt=Peripherie-Takt

Gebrauchen kann man das, wenn man die gesamte Peripherie des Systems drosseln möchte. Der Peripherie-Takt kann ohne Probleme so schnell sein wie der Prozessor-Takt.

Zwischenstand

Generell sollte man Dinge wie Multiplikator und die Quarz-Taktfrequenz am Anfang seines Programms definieren, z. B. so:

#define FOSC 14745000 // Quarzfrequenz
#define PLL_M 4  // PLL Multiplikator
#define VPBDIV_VAL 1 // Teiler des Peripherie-Takts

Memory Accelerator Module

Mit den MAM-Registern lässt sich der Speicherzugriff des LPC noch etwas optimieren. Die nötigen Register:

MAMCR Das Control-Register des MAMs beinhaltet den MAM-Modus. Hier kann eingestellt werden, ob die MAM-Funktionen gar nicht, teilweise oder vollständig aktiviert sind.
MAMTIM Im Timing-Register wird bestimmt, wieviele Prozessortakte benutzt werden, um auf den Flashspeicher zuzugreifen. Was für welchen Controller geeignet ist, wird in der nächsten Tabelle erklärt.

Die Einstellungen an den MAM-Register könnten in eurem Code beispielsweise so aussehen:

MAM_MAMCR = 0; // MAM aus
MAM_MAMTIM = 3; // MAM fetch cycle to 3 cclk (>40MHz)
MAM_MAMCR = 2; // MAM vollständig aktiviert

Für die korrekte Einstellung von MAMTIM gibt es auf Seite 77 im LPC2124-Handbuch einen Hinweis, der die Einstellungen erklärt. Kurze Zusammenfassung:

System Clock bis 20MHz MAMTIM=1;
System Clock von 20MHz bis 40MHz MAMTIM=2;
System Clock ab 40 MHz MAMTIM=3;

UART

Um den UART zu aktivieren, sind in der Minimalkonfiguration folgende Register nötig (n steht für den jeweiligen UART):

UARTn_LCR Das Line Control Register bestimmt das Format, in dem Daten empfangen oder gesendet werden.
UARTn_DLL/UARTn_DLM In diesen Registern wird der Frequenzteiler hinterlegt, damit der Baudratengenerator den richtigen Ausgabetakt erzeugt. Der Teiler lässt sich durch die Formel [math]\displaystyle{ Teiler = PCLK / (Baudrate * 16) }[/math] errechnen, wobei PCLK (der Peripherietakt) nicht unbedingt mit dem CPU-Takt identisch sein muss (Siehe VPBDIV).
UARTn_FCR Im FIFO Control Register kann man Einstellungen am FIFO des jeweiligen UARTs vornehmen.
UARTn_LSR Im Line Status Register stehen Status- und Fehler-Informationen des jeweiligen UARTs.

Für den Datenverkehr sind folgende Register definiert:

UARTn_RBR Das Recieve Buffer Register beinhaltet alle empfangenen Datenbytes.
UARTn_THR Im Transmitter Holding Register werden die Daten abgelegt, die über den UART versendet werden sollen.

Folgende Funktion initialisiert den UART0 des LPC:

void InitUART0(u32 baud) {

  PINSEL0 &= ~((0x3<<2) | (0x3<<0)); // Pin-Funktion löschen (zur Sicherheit)
  //         rxd 0     txd 0
  PINSEL0 |= (1<<2) | (1<<0);        // Pin-Funktion zuweisen
	
  // BAUD RATE EINSTELLEN
  // Divisor Latch Access Bit setzen (DLAB)
  // damit erhalten wir Zugriff auf den Baudraten-Teiler
  UART0_LCR |= (1<<7);

  // Nun haben wir Zugriff auf DLL (untere 8 bit des Teilers)
  // und DLM (obere 8 bit des Teilers). Auffällig ist hier, 
  // dass der 16-Bit-Wert auf zwei 32-Bit-Addressen aufgeteilt ist

  // Teiler = PCLK / (baudrate * 16)
  // Im Beispiel haben wir CCLK und PCLK gleich gesetzt
  // Untere 8 Bit des Teilers
  UART0_DLL = (CCLK / (baud*16)) & 0xFF;
  // Obere 8 Bit des Teilers
  UART0_DLM = ((CCLK / (baud*16)) & 0xFF00)>>8;
	
  // DLAB wieder löschen
  UART0_LCR &= ~(1<<7);
	
  // 9 bit, 1 stop bit, keine parität
  UART0_LCR = (1<<1) | 1;

  // UART0-FIFO aktivieren
  UART0_FCR = 1;
}

Nachdem man die Funktion aufgerufen hat, kann man ganz einfach ein Byte senden, der Code dafür ist so ähnlich wie beim AVR:

// Hier fragen wir das "Transmitter Holding Register Empty"-Bit
// im "Line Status Register" ab und ermitteln, ob sich noch ein Byte im 
// Sendepuffer befindet (der Sendepuffer ist ein FIFO-Stack)
while (!(UART0_LSR & (1<<5))) continue; // Warten, bis der Sendepuffer geleert ist

// Neuen Wert in das "Transmitter Holding Register" schreiben
UART0_THR = 'a';

Um ein Byte zu empfangen, muss erst einmal überprüft werden, ob sich ein ungelesenes Byte am Anfang des FIFO-Stacks befindet:

  //Auf "Receiver Data Ready"-Bit (RDR) im "Line Status Register" (LSR) warten
  while((U0LSR & 1) == 0); 
  
  //Byte vom Stack einlesen ("Receiver Buffer Register", RBR)
  x  = U0RBR;

Realtime Clock (RTC)

Die RTC der LPC-Controller ist eines der am einfachsten zu nutzenden Peripherie-Bestandteile. Mit ein paar Registerzugriffen lässt sie sich aktivieren und einstellen.

Um sie zu aktivieren, muss man zunächst einen Teiler für die Systemfrequenz ermitteln. CCLK ist der aktuelle CPU-Takt in Hertz.

// Integerteil des Teilers berechnen
RTC_PREINT = (CCLK / 32768)-1;
// Fließkommateil des Teilers berechnen
RTC_PREFRAC = CCLK - ((RTC_PREINT+1) * 32768);

Jetzt kann man die RTC ganz einfach aktivieren:

RTC_CCR = 1;

Nun läuft die RTC schon! Nur müssen wir natürlich noch eine andere Zeit und ein anderes Datum einstellen:

//Stunde, Minute und Sekunde einstellen
RTC_HOUR = 11; 
RTC_MIN = 55;
RTC_SEC = 0;

//Tag (Day Of Month), Monat und Jahr einstellen
RTC_DOM = 12; 
RTC_MONTH = 3;
RTC_YEAR = 2005;

Die RTC hat noch weitere Register zum Auslesen weiterer Werte wie z. B. dem Tag des Jahres, dem Tag der Woche usw.

Ausserdem bietet die RTC viele Interrupt-Funktionen, die z. B. dazu genutzt werden können, den Controller nach einer bestimmten Zeit aus dem Ruhezustand zu wecken. Weitere Informationen gibt es im LPC2106-Benutzerhandbuch ab Seite 157.

Interrupts

In diesem Kapitel wird das Interruptsystem der LPCs erklärt.

Die wichtigsten Komponenten sind:

Vectored Interrupt Controller (VIC)

Interrupt-Register und -Bits der jeweiligen Peripherie

Im VIC werden die generellen Einstellungen vorgenommen, die alle Interrupts betreffen. Außerdem gibt es bei der meisten Peripherie auch ein Register, welches ein Interrupt Clear Bit beinhaltet; hierzu später mehr.

Interruptarten

Grundsätzlich unterscheidet man hier zwischen IRQ und FIQ (Fast Interrupt Request). Diese unterscheiden sich darin, wie schnell in die ISR gesprungen wird. //TODO: Die benötigte Zeit wiederfinden, Quelle leider nicht mehr auffindbar. Ob ein Interrupt ein IRQ oder ein FIQ ist wird in dem Register "VICIntSelect" deklariert.

Interruptcontroller

Grundsätzlich muss neben der Interruptart nur noch die Adresse und die dazugehörige Peripherie eingestellt werden. In einem der VICVectAddrn-Register wird die Adresse der jeweiligen Interrupt-Serviceroutine angegeben. Im passenden VICVectCntln-Register gibt man die Peripherie an, die diesen Interrupt auslösen soll ( z. B. ist in VICVectCntln ein UART-Interrupt ?).

Um alle eingestellten Interrupts zu aktivieren, benutzt man das VICIntEnable-Register.

Hier etwas Beispielcode:

#define VIC_UART0 6

VICIntEnClear = 0xFFFFFFFF;           // Alle Interrupts löschen
VICIntSelect = 0x00000000;            // Alle Interrupts als IRQ

VICVectAddr0=(unsigned long)ISR; // ISR ist die Funktion die ausgeführt wird, wenn der Interrupt auslöst.
VICVectCntl0=(1<<5) | VIC_UART0;
VICIntEnable=(1<<VIC_UART0);

Was bedeutet VIC_UART0? Alle Interrupts können von einer anderen Quelle ihren "Auslöser" bekommen. Damit ein Interrupt weiss, welche Quelle er nutzen soll, gibt man ihm die Peripherie an.

Block VIC Channel #
Watchdog 0
RESERVED 1
ARM Core (DbgCommRx) 2
ARM Core (DgbCommTx) 3
Timer0 4
Timer1 5
UART0 6
UART1 7
PWM0 8
I2C 9
SPI0 10
SPI1 11
PLL 12
RTC 13
EINT0 14
EINT1 15
EINT2 16
EINT3 17
A/D-Wandler 18
RESERVED 19

Für weitere Informationen hilft ein Blick ins Datenblatt.

Die ISR wird wie folgt deklariert:

void __attribute__ ((interrupt("IRQ"))) isr(void); // Prototyp

void __attribute__ ((interrupt("IRQ"))) isr(void){
  // A lot of Work
  VICVectAddr = 0;       // Acknowledge Interrupt
}


Falls die Interrupts nicht funktionieren, hilft ein Blick in die Startup-Datei. Es müssen einige Vorbereitungen getroffen werden, damit Interrupts aus C heraus funktionieren können. Einen passenden Startup-Code gibt es bei den WinARM-Beispielen mit IRQ-Beispielen.

SPI

SPI lässt sich ähnlich simpel wie bei einem AVR initialisieren.

SPCCR Das SPCCR bestimmt die Taktfrequenz der jeweiligen SPI-Schnittstelle
SPCR Im SPCR werden die nötigen Einstellungen wie z. B. Master-Modus vorgenommen.
SPDR Das SPDR ist ein bidirektionales Register, welches entweder zum Senden oder Lesen eines Bytes benutzt werden kann.

Ein Codebeispiel für die Initialisierung:

PCB_PINSEL0 |= (1<<8)|(1<<10)|(1<<12)|(1<<14); //Pin Select für SPI0

//Init SPI0
SPI0_SPCCR = 16; // Jeden 16. Clock - 1 SPI-Takt
SPI0_SPCR = (1<<5); //Master-Modus

Das SPCCR-Register hält die Taktfrequenz des jeweiligen SPI bereit. Grundsätzlich gilt: Jeden n. Takt vom Prozessortakt kommt ein SPI-Takt. Beispiel: 60 MHz Systemclock / 16 = 3,75 MHz SPI-Takt. Allerdings muss der Teiler größer oder gleich 8 sein.

Das Senden eines Bytes per SPI sieht so aus:

SPI0_SPDR = x; // Schreibe 'x' ins Datenregister
while (!(SPI_SPSR & (1<<7))); // Warte, bis der Datentransfer beendet ist

I2C

In der "Codesammlung" befindet sich eine I2C-Master-Bibliothek für den Polling-Betrieb. Diese Bibliothek findet ihr unter http://www.mikrocontroller.net/forum/read-4-281865.html.

Die Bedeutung der Status-Codes findet ihr unter http://www.semiconductors.philips.com/acrobat_download/various/8XC552_562OVERVIEW_2.pdf

Verwendung

Im folgenden Codebeispiel wird die Verwendung der Bibliothek erklärt:

#include <arch/philips/lpc2114.h> // Replace this file with your own header file
#include "i2c.h"

#define DEVICEADDR 112

int main (void){
  unsigned char i2c_messages[5],readbyte;
  
  i2c_init();
  
  i2c_messages[0]=55;
  i2c_messages[1]=44;
  i2c_messages[2]=99;

  //Write 3 bytes
  i2c_start(DEVICEADDR);
  i2c_write(i2c_messages,3);
  i2c_stop();
  
  //Read 1 byte
  i2c_start(DEVICEADDR+1);
  readbyte=i2c_readlast();
  i2c_stop();
  
  //Read 3 bytes
  i2c_start(DEVICEADDR+1);
  i2c_read();
  i2c_read();
  i2c_readlast();
  i2c_stop();
  
  while(1){
    asm volatile("nop");         
  }
}


Weitere Informationsquellen

Nützliche Threads

Hier landen Threads aus dem Mikrocontroller.net-Forum, die sich mit dem Thema ARM beschäftigen. Zum größten Teil sind dies besonders nützliche Threads oder solche, die über den Threadtitel nicht als ARM-Thread identifiziert werden können