LPC-Tutorial

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

von newGeneration

In diesem Artikel soll den Einstieg in die Programmierung der LPC-Mikrocontroller der Firma NXP mit dem arm-none-eabi-gcc-Compiler erleichtern.

Vorausgesetzt sind Grundkenntnisse der Programmiersprache C. Wer mit dieser Programmiersprache noch keine Erfahrungen hat, bzw. sie nicht sicher beherrscht, kann sich diese auch online erarbeiten, siehe dazu die Liste der C-Tutorials. Kenntnisse aus dem PC-Bereich reichen aus, man muss noch keine Mikrocontroller programmiert haben.

Das Tutorial befindet sich noch im Aufbau

Vorwort

Ziel

Dieses Tutorial soll als erstes einmal dem Leser helfen, ein eigenes Grundgerüst zu bauen, auf das dann später aufgebaut werden kann. Weiterhin soll auf Libraries verzichtet werden, die die Hardware-Ansteuerung verschleiern, um das Maximale Verständnis des verwendeten Chips zu bekommen. Eine solche Library wäre zum Beispiel LPCOpen.

Ferner soll das gesamte Projekt als Makefile-Projekt angelegt werden, um maximale Portabilität zu gewährleisten. Es steht natürlich jedem frei, seine bevorzugte IDE zu verwenden. An diesem Punkt sollte LPCXpresso erwähnt werden, eine auf Eclipse basierende IDE, die von NXP speziell an die LPC-Controller angepasst wurde. Vorteil hier ist das stark vereinfachte Debuggen, da zum Beispiel die gesamten Peripherie-Einheiten mit Erklärungen hinterlegt sind.

Aufbau

Grundgerüst

In diesem Kapitel soll das Grundlegende Gerüst erstellt werden, damit der Chip C-Standard-Konform programmiert werden kann. Damit ist gemeint: es kann ganz normal mit der Funktion main() begonnen werden.

Dazu braucht man im wesentlichen zwei Dateien:

  • das Linker-Skript
  • den Startup-Code

Diese werden nun Schritt für Schritt erstellt und erklärt.

Am Ende dieses Artikels gibt es auch noch ein kleines Testprogramm.

Peripherie

Die Peripherie-Einheiten werden in den folgenden Artikeln erklärt:

Die Artikel (und weitere) sollten nach und nach folgen

Warum LPC-Mikrocontroller

Die LPC-Serie basiert auf lizensierten Kernen von ARM, diese haben allgemein einige Vorteile:

  • Der 32bit ARM-Cortex-Architektur mit entsprechender Rechenleistung
  • Höherer Maximaltakt
  • optionale MPU und FPU
  • neuere Fertigung bedeuted energieeffizienter

Speziell die Mikrocontroller von NXP haben mehrere Vorteile:

  • mehr und stromsparendere Sleep-Modes
  • Einfach zu benutzende Hardwaremodule
  • Mächtige Hardware (z.B. State Configurable Timer)
  • Gute Dokumentation (für den Programmierer unabdingbar)
  • als einzige ARMs auch im DIP-Gehäuse erhältlich

Vorbereitungen

Als erstes benötigt man den Chip, den es zu programmieren gilt. Entweder man hat schon einen bestimmten, dann kann es direkt weitergehen, oder man muss sich eben noch einen bestellen. Auswahlhilfe gibt hier der Artikel LPC1xxx im Forum und der Product-Selector auf der NXP-Homepage.

Dieses Tutorial bezieht sich nur auf die LPC-Mikrocontroller mit Cortex-M-Kern, nicht auf die älteren LPC2000-/LPC3000-Serien mit älteren ARM-Kernen. Auf diese lässt sich das Tutorial möglicherweise übertragen, jedoch ist es nicht garantiert.

Dieser Artikel versucht möglichst unabhängig von einem speziellen Prozessor zu sein, allerdings wurden die gesamten Beispiele auf einem LPC54102(J512BD64) getestet. Hardware-Abhängigkeiten werden in Kommentaren (fast immer ;-) ) erwähnt.

Chip-Dokumentation

Als erstes braucht man zu seinem Chip eine Dokumentation, denn ohne die wird das Programmieren seeehr schwer. Deswegen sollte man sich diese möglichst schnell zugreifbar abspeichern.

Die benötigten Dateien sind

  • Das Datenblatt mitsamt Pinkonfiguration
  • Das Errata-Sheet mit Hardwarefehlern
  • Das User-Manual mit ausführlichen Erklärungen zum Nutzen der Hardware

Alle diese Dokumente sind auf der NXP-Homepage unter

Products -> LPC-Serie -> Produktreihe -> Documentation

zu finden, einfach die aktuellsten Dokumente herunterladen und "in Griffnähe" abspeichern.

Toolchain

Schritte der Kompilierung

Unter Toolchain versteht man die Sammlung von Programmen, die zu einem Compiler gehören, darunter fallen

  • der Compiler selbst
  • der Linker
  • Assembler
  • Debugger
  • C-Standard-Library
  • Utility-Programme

Es gibt Toolchains von verschiedenen Herstellern, kommerziell wie auch frei, zum Beispiel von IAR, Keil oder dem GNU-Projekt. Dieses Tutorial zielt auf den GCC, die GNU Compiler Collection ab, sollte aber auch auf die anderen Compiler übertragbar sein.

Da die LPC Mikrocontroller alle auf der ARM-Architektur basieren bietet sich zum Programmieren auch der offizielle (freie und kostenlose) GNU ARM Embedded Toolchain an, die direkt von ARM gepflegt wird.

Nach dem Herunterladen und Installieren einer (GCC-)Toolchain sollte man kurz einen Minimaltest durchführen, nämlich in der Kommandozeile

arm-none-eabi-gcc --version

Auf diesen Befehl hin sollte die Versions-Informationen ausgegeben werden. Wenn eine Fehler kommt stimmen wahrscheinlich die Pfade nicht und müssen angepasst werden.

Grundgerüst erstellen

Nun können wir mit der Programmierung loslegen.

Wie oben schon angesprochen brauchen wir zum "normalen" Programmieren mit der Funktion main() als Einsprungspunkt (mindestens) zwei Dateien, den Startup-Code und das Linkerskript. Diese werden nun erstellt.

Linker-Skript

An dieser Stelle sei erst einmal auf die Online-Dokumentation dazu verwiesen: Using LD, the GNU linker - Linker Scripts.

Starten wir erst einmal mit dem Grundgerüst: Dazu legt man eine Datei an, der Name und die Endung ist streng genommen egal, aber es empfiehlt sich ein aussagekräftiger Name (etwa die Bezeichnung des verwendeten Chips). Häufige Endungen sind .ld und .x.

Ganz oben im Linkerskript wird definiert, wo der Programmstart ist. Das ist nicht die main()-Funktion, da voher noch einiges zu tun ist. Deshalb nennen wir die Einsprungfunktion _reset. Der einfache vorangestellte Unterstrich symbolisiert Zugehörigkeit zur C-Runtime.

ENTRY(_reset)

Als nächstes werden die Speicher-Regionen beschrieben. Diese sind der Memory-Map im User Manual zu entnehmen. Darunter fallen alle Flash- und RAM-Bereiche. Jeder dieser Bereiche bekommt einen Namen, die Attribute (Read, Write, eXecute), den Beginn und die Länge des Bereichs.

MEMORY {
  /* memory map des LPC54102. Muss an den verwendeten Controller angepasst werden! */
  Flash (rx):  ORIGIN = 0x00000000, LENGTH = 0x00080000 /* 512kB */
  SRAM0 (rwx): ORIGIN = 0x02000000, LENGTH = 0x00010000 /*  64kB */
  SRAM1 (rwx): ORIGIN = 0x02010000, LENGTH = 0x00008000 /*  32kB */
  SRAM2 (rwx): ORIGIN = 0x03400000, LENGTH = 0x00002000 /*   8kB */
}

Danach kommt des (obere) Ende des Stacks (Achtung: der Stack muss je nach Konfiguration an einer 4 Byte oder 8 Byte Grenze ausgerichtet sein, und darf somit nicht an jeder Adresse beginnen):

_stack_end = ORIGIN(SRAM0) + LENGTH(SRAM0); /* stack end at end of SRAM0 (LPC54102) */

Bisher waren es alles einfach und einleuchtend. Jetzt kommt etwas, wofür man einen kleinen Einblick in den GCC braucht, nämlich seine Sections. Es gibt mehrere, jede für einen bestimmten Zweck:

  • .text - Code
  • .bss - Zu 0 initializierte Variablen
  • .data - Initialisierte Variablen (nicht mit 0)
  • .rodata - Read-Only-Varaiblen

Die Sections legt man dann in einen Memory-Bereich. Sinnvollerweise wird man die nicht-änderbaren Daten mitsamt den Code in den Flash legen, die Variablen in den RAM. Das geschieht mit dem > "Memorybereich".

Damit die .data-Section, die ja eigentlich im RAM liegt, auch mit den richtigen Werten vorbelegt werden kann, müssen diese irgendwo (im Flash) stehen. Das wird mit dem AT> Flash erreicht. Damit landen die Daten auch (nach dem Programmcode) im Flash.

SECTIONS {
  .text : {
    _text_section_start = .;
    KEEP(*(._vectors)) /* Erklaerung hierzu folgt spaeter */
    *(.text)
    *(.text*)
    *(.rodata .rodata.* .constdata .constdata.*)
    _text_section_end = .;
  } > Flash
  
  . = ALIGN(4);
  
  .bss : {
    _bss_section_start = .;
    *(.bss)
    *(.bss*)
    _bss_section_end = .;
  } > SRAM0
  	
  . = ALIGN(4);
  	
  .data : {
    _data_section_start = .;
    *(.data)
    *(.data*)
    _data_section_end = .;
  } > SRAM0 AT > Flash
}

Damit ist unser Linker-Skript auch schon fertig. Einige abschließende Erklärungen noch:

  • die _name_section_start/end = .;-Anweisungen erzeugen globale Variablen, die die aktuelle Adresse enthalten
  • . = ALIGN(4); sagt dem Linker, dass er die aktuelle Adresse an der nächsten 4 Byte-Grenze ausrichten soll
  • In diesem Beispiel werden alle Konstanten Daten in den Flash gelegt
  • Diesem Grundgerüst fehlen noch einige Dinge, wie zu Beispiel der Heap

Für genauere Erklärungen sollte das oben verlinkte Linker-Manual zu rate gezogen werden.

Startup-Code

So unser Linker-Skript ist nun fertig, jetzt können wir mit dem eigentlichen Programmieren beginnen. Dazu müssen wir den Code erstellen, der direkt nach dem Programmstart ausgeführt wird.

Klassisch wird dieser Code in Assembler geschrieben, jedoch bietet die ARM-Architektur einen perfekten Aufbau, um einen Startup-Code in C schreiben zu können.

Dazu legen wir eine neue Datei an, die hat meistens den Namen crt0.c für C-Runtime. Diese enthält als erstes die Prototypen für die aufgerufenen Funktionen, namentlich _reset(), main() und _stack_end() (ja, das Stackende wird als Funktion angesehen, macht aber nichts, weil es nur um die Adresse geht).

extern int main(void);
extern void _reset(void);
extern void _stack_end(void);

Daraufhin kommen die Prototypen für alle Faulthandler. Welche Faults auf dem entsprechenden Device implementiert sind, kann man dem User Manual unter Kapitel ARM Cortex Appendix und dem ARM Cortex-Mx Generic User Manual.

/* die implementierten Faulthandler des LPC54102 */
void NMI_handler(void);
void HardFault_handler(void);
void MemManage_handler(void);
void BusFault_handler(void);
void UsageFault_handler(void);
void SVC_handler(void);
void DebugMon_handler(void);
void PendSV_handler(void);
void SysTick_handler(void);

Danach kommen die Prototypen der Interrupts. Welche hier implementiert sind steht im User Manual unter dem Kapitel zum NVIC.

Das Define sagt dem Compiler/Linker, dass die Funktion weak ist, also von einer anderen Funktion mit selben Namen überschrieben werden kann, und einen alias hat, also, wenn sie nicht überschrieben wird, auf eine andere Funktion verweist.

#define ALIAS(f) __attribute__ ((weak, alias (#f)))

/* Standard-Handler fuer nicht implementierte Interrupt-Service-Routinen */
void Default_Interrupt_Handler(void) __attribute__ ((weak));

/* Ausschnitt der implementierten Interrupts des LPC54102 */
/* wenn es dir ISR nicht gibt, dann springe in den DefaultHandler*/
void WDT_handler(void) ALIAS(Default_Interrupt_Handler);
void BOD_handler(void) ALIAS(Default_Interrupt_Handler);
/* viele ISR-Protoypen mehr */
void SPI3_handler(void) ALIAS(Default_Interrupt_Handler);
void RIT_handler(void) ALIAS(Default_Interrupt_Handler);

Falls ein C++-Compiler genutzt wird, muss man alles oben stehende mit extern "C" { ... } kennzeichnen.

Bisher ist aber noch nichts passiert. Wir sagen dem Compiler zwar, dass es diese und jene Funktionen irgendwo gibt, aber weder benutzen wird diese, noch deklarieren wir sie.

Das benutzen kommt jetzt, nämlich in der Vektor-Tabelle. Diese wird mit Attributen als benutzt gekennzeichnet und in die Section "._vectors" gepackt. Diese Section wird im Linkerskript an die erste Adresse im Flash gesetzt und, da sie niemals direkt verwendet wird, mit KEEP in jedem Fall behalten.

Die Vektor-Tabelle an sich besteht aus einem Array aus Funktionspointern:

extern void (* const _vectors[])(void) __attribute__ ((used, section("._vectors")));
void (* const _vectors[])(void) = {
    &_stack_end,
    _reset,
    /* die 14 Fault Handler an die richtige Adresse setzen */
    /* siehe dazu das ARM Cortex-Mx Generic User Maual */
    /* bei nicht implementierte Handlern wird einfach eine 0 eingetragen */

    /* danach kommen alle Interrupts an der richtigen Stelle */
    /* dies steht wiederum im User Manual */
    WDT_handler,
    BOD_handler,
    /* ... */
    SPI3_handler,
    RIT_handler
};

So, damit haben wir schon einen wichtigen Teil abgearbeitet. Damit könnten wir nun Interrupts nutzen, wenn wir schon ein richtiges Programm schreiben könnten. Dazu fehlt uns aber noch die _reset()-Funktion, die alle Variablen initialisiert und main() aufrauft.

Die Reset-Funktion muss im wesentlichen eines machen: die Variablen initialisieren. Zwar können noch weitere Dinge dort erledigt werden, aber das ist von Anwendung zu Anwendung unterschiedlich. Es könnten zum Beispiel die Clocks zu verschiedenen Peripherie-Modulen aktiviert werden, oder der System-Takt eingestellt werden.

Des weiteren aktiviert man gerne noch die FPU (wenn vorhanden) im Startupcode und nimmt wenn nötig Prozessor-Einstellungen vor.

Die Funktion _reset() legen wir jetzt an:

void _reset(void) {
    /* hole die im Linker-Skript erstellen globalen Variablen */
    extern unsigned int _data_section_start;
    extern unsigned int _data_section_end;
    extern unsigned int _text_section_end;
    extern unsigned int _bss_section_start;
    extern unsigned int _bss_section_end;

    unsigned int *src;
    unsigned int *dest;
    /* jetzt überschreiben wir alle Varaiblen in der .bss-Section mit 0 */
    dest = &_bss_section_start;
    while (dest < &_bss_section_end) {
        *dest++ = 0;
    }
    
    /* jetzt initialisieren wir alle .data-Variablen mit den */
    /* Daten, die im Flash abgelegt wurden */
    src = &_text_section_end;
    dest = &_data_section_start;
    while (dest < &_data_section_end) {
        *dest++ = *src++;
    }
    
    /* und nun folgt endlich der Aufruf von main() */
	/* der return-wert wird nicht verwendet, man könnte also main als */
    /* void main(void) definieren und deklarieren, spart uU etwas Speicher */
    main();
    
    /* falls main() zurückkehrt: gehe in eine Endlosschleife */
    while(1) ;
}

Und als letztes legen wir noch die Fault-Handler, sowie die Funktion an, die angesprungen wird, wenn ein Interrupt auftritt, der keine Interrupt-Service-Routine hat:

void Default_Interrupt_Handler(void) {while(1);}
void NMI_handler(void) {while(1);}
void HardFault_handler(void) {while(1);}
void MemManage_handler(void) {while(1);}
void BusFault_handler(void) {while(1);}
void UsageFault_handler(void) {while(1);}
void SVC_handler(void) {while(1);}
void DebugMon_handler(void) {while(1);}
void PendSV_handler(void) {while(1);}
void SysTick_handler(void) {while(1);}

Diese Implementierungen sind natürlich nicht sinnvoll, sondern dienen einzig und alleine dem Zweck die Fault-Handler zu erstellen. Nur warten im Falle eines Faults ist nicht sinnvoll. Siehe Beitrag:Cortex Faults: Was macht man im Handler?.

... Und, ob man es glaubt, oder nicht: Damit ist das Grundgerüst fertig!

Diese beiden Dateien kann man einfach ohne sie anzupassen in jedes Projekt kopieren, sofern der Chip gleich ist.

Bei unterschiedlichen Chips muss man die Memory-Eintrage im Linker-Skript, sowie die Fault- und Interrupt-Handler im Startup-Code ändern. Der Rest sollte gleich bleiben.

Ab jetzt kann man endlich richtig losprogrammieren.

Makefile und Compiler-Settings

Um nicht alles ein x-tes mal zu erklären versuche ich mich hier kurz zu halten und auf weitere Artikel zu verweisen, diese sind Beispiel_Makefile für ein (AVR-)Beispiel, AVR-GCC-Tutorial/Exkurs_Makefiles für einen groben Überblick, arm-none-eabi-gcc Parameter für eine Liste wichtiger Compiler-Optionen. Eine gute Seite mit einer Einführung in Makefile findet sich hier.

Hier ist ein simples Makefile, das für kleine Projekte ausreicht und leicht erweitert werden kann: Diese Datei einfach unter dem Namen "Makefile" (ohne Endung im Projektordner speichern).

## Example Makefile for arm-none-eabi-gcc and LPC-microcontrollers

OUTPUT_NAME = lpc

## Used tools
# C compiler frontend
CC = arm-none-eabi-gcc
OBJDUMP = arm-none-eabi-objdump

## list files to compile here (suffix: *.o)
OBJS = crt0.o main.o

## Path and name of the linker script
LINKER_SCRIPT = lpc.ld

## Flags
# processor-dependent flags
MCU_FLAGS = -mthumb -mcpu=cortex-m0 -mtune=cortex-m0 -mfloat-abi=soft
# Compiler flags
CFLAGS = $(MCU_FLAGS) 
CFLAGS += -Wall -Og -g3 -MMD
CFLAGS += -ffunction-sections -fdata-sections -fsingle-precision-constant -fstack-usage # more options
# Linker flags
LDFLAGS = $(MCU_FLAGS) $(CFLAGS)
LDFLAGS += -T$(LINKER_SCRIPT)
LDFLAGS += -nostartfiles -Wl,--gc-sections,-print-memory-usage
LDFLAGS += # more options, e.g. libraries

## Rules
all: $(OUTPUT_NAME).elf $(OUTPUT_NAME).lss
.PHONY: clean

# Rule for creating the .elf file
$(OUTPUT_NAME).elf: $(OBJS) Makefile
	@echo Linking final file...
	@$(CC) $(CFLAGS) -o $(OUTPUT_NAME).elf $(OBJS) $(LDFLAGS)

# Rule for compiling every needed c file 
%.o: %.c Makefile
	@echo Compiling file $<...
	@$(CC) -c $(CFLAGS) -o $@ $<

# Rule for generating a disassembly
%.lss: %.elf
	@echo Generating disassembly...
	@$(OBJDUMP) -d -S $< > $@

# delete all created files
clean:
	@echo Cleaning up...
	@$(RM) $(OBJS) $(OUTPUT_NAME).elf $(OUTPUT_NAME).lss $(OBJS:.o=.d) $(OBJS:.o=.i)

# include C dependencies
-include $(OBJS:.o=.d)

Das Makefile ist einfach aufgebaut. Zuerst kommen die Variablen, die jeweilige Bedeutung sollte sich aus dem Namen ergben. Wichtig sind die Flags, bei denen man seine Compiler- und Linker-Option angibt. Die aktuellen Parameter sind selbstverständlich anzupassen.

Danach kommen die sogenannten Rules, die "Erstellungs-Regeln". Diese beschreiben was wann wie getan werden muss. TODO weiter ausführen

Zum Kompilieren muss man nun lediglich

make

aufrufen, zum Aufräumen, also zum Löschen aller erstellen Dateien

make clean

Device-Header

Eigentlich kann man jetzt schon den gesamten Chip programmieren, ohne irgendwelche Abstriche machen zu müssen. Um etwa eine LED blinken lassen muss man "nur" folgendes schreiben:

#include <stdint.h>

extern void delay_ms(uint32_t ms); /* implemented somewhere else */

int main(void) {
	/* enable GPIO0 (bit 14) and IOCON (bit 13) clock in the SYSCON->AHBCLKCTRLSET register */
	*(volatile uint32_t*)0x400000C8 = (1 << 13) | (1 << 14);
	/* setup PIO0_29: GPIO (bits [2:0]), Pull-Up (bits [4:3]), digital mode (bit 7) */
	*(volatile uint32_t*)(0x4001C000 + 29 * 0x4) = (0x0 << 0) | (0x2 << 3) | (0x1 << 7);
	/* set PIO0_29 to output using the GPIO->DIR0 register */
	*(volatile uint32_t*)(0x1C000000 + 0x2000 + 0) |= (1U << 29);

	while(1) {
		/* toggle LED at PIO0_29 using GPIO->NOT0 register */
		*(volatile uint32_t*)(0x1C000000 + 0x2300 + 0) = (1U << 29);
		delay_ms(500);
	}
}

Wer erkennt nicht auf den ersten Blick, was hier passiert? Okay, der Code ist einigermaßen gut kommentiert, und damit versteht man vas vorsich geht, aber diese kryptischen Adressen?! Auch die Bitwerte sind zu verbessern, zwar wird auch hier im Kommentar erklärt was mit welchen Bits gemacht wird, aber sprechende Namen wären deutlich schöner.

Wer will, kann den Code testen, die Adressen beziehen sich uaf einen LPC54102, der eine LED an PIO0_29 angeschlossen hat (so wie am Eval-Board für diesen Chip). Dieser Schipsel ist getestet und funktioniert. Für eine mögliche Implementierung einer einfachen delay_ms()-Funktion siehe diesen Thread.

Aber, wie allen klar sein dürfte: So will man nicht programmieren. Selbst beim konstruieren dieses einfachen Beispiels hatten sich ursprünglich zwei Fehler in den Adressen eingeschlichen. Also: eine andere Möglichkeit muss her.

Die Lösung ist die CMSIS. ARM hat einen Standard für alle möglichen Teile eines Mikrocontroller-Systems, unter anderem das sogenannte CMSIS-SVD. Das steht für System View Description. Das sind Dateien, die einen kompletten Mikrocontroller beschreiben, mit Registern, Interrupts, Bits, ihren Namen. Also genau das was wir brauchen. NAja fast, wir wollen schließlich eine .h-Datei, keine .svd-Datei, diese sind nämlich nicht einfach austauschbar. Aber auch dafür gibt es eine Lösung von ARM: SVDconv.exe ist ein Kommandozeilen-Programm für unter anderem die Generierung von Device-Headern.

Das Programm kann von GitHub aus dem ARM-Repository heruntergeladen werden und befindet sich unter CMSIS/Utilities/SVDConv.exe. Fehlt nur die .svd-Datei. Diese bekommt man im idealen Fall vom Hersteller des Chips. Wenn dieser seine SVDs nicht veröffentlicht gibt es auch dieses GitHub-Repository, das im Unterordner data eine Sammlung der Dateien von verschiedenen Herstellern beinhaltet, unter anderem eben auch die Beschreibungen für die LPC-Serie von NXP.

Dazu führt man in der Eingabeaufforderung folgendes aus:

SVDConv.exe <device-file.svd> --generate=header --fields=macro

Warnings wegen "RESERVED should not be defined" kann man getrost ignorieren. Fehler sollten keine auftreten.

Mit diesem Kommando generiert man eine Header-Datei für seinen Controller. Des weiteren muss man neben den generierten Header noch die von ARM gestellten Header-Dateien core_cmX.h, core_cmFunc.h und core_cmInstr.h aus dem Verzeichnis CMSIS-master\CMSIS\Include zu seinem Header-File kopieren. Wenn beim kompilieren weitere fehlende Dateien angemahnt werden, dann auch diese aus dem Verzeichnis kopieren. Beim Fehler "fatal error: system_<device>.h: No such file or directory" hat man zwei Optionen:

  • das Include löschen, damit ist man zwar nicht mehr CMSIS-kompatibel, aber hat keine arbeit
  • die Datei (mit einer dazugehörigen .c-Datei) erstellen. Der Inhalt sollte der folgende sein:
#ifndef __SYSTEM_<device>_H
#define __SYSTEM_<device>_H

#ifdef __cplusplus
extern "C" {
#endif

#include <stdint.h>

extern uint32_t SystemCoreClock;     /*!< System Clock Frequency (Core Clock)  */

/**
 * Initialize the system
 *
 * @param  none
 * @return none
 *
 * @brief  Setup the microcontroller system.
 *         Initialize the System and update the SystemCoreClock variable.
 */
extern void SystemInit (void);

/**
 * Update SystemCoreClock variable
 *
 * @param  none
 * @return none
 *
 * @brief  Updates the SystemCoreClock with current core Clock 
 *         retrieved from cpu registers.
 */
extern void SystemCoreClockUpdate (void);

#ifdef __cplusplus
}
#endif

#endif /* __SYSTEM_<device>_H */
Die .c-Datei implementiert dann die beiden Funktionen und definiert die globale Variable. Was die Funktionen tun kann man dem Doxygen-Kommentar über den Funktionen entnehmen.

Damit sind dann alle wichtigen Dateien fertig!

Das erste Programm

Nun können wir endlich so programmieren, wie man es von PC oder einem anderen Controller gewöhnt ist.

Also in einer neu angelegten Datei seinen normalen Code schreiben.

int i = 42;

int main(void) {
	
	while(1) {
		--i;
	}
	return 0;
}
Mikrocontroller-Programmierung

Dieses sollte man testweise einmal Kompilieren, um die bisherigen Schritte zu verifizieren. Wer einen Debugger hat kann auch testen, ob die Variable i mit dem richtigen Wert (42) initialisiert wird und auch schauen, ob die Variable dekrementiert wird.

Aber wie jedem klar sein sollte: das obige Programm macht nichts sinnvolles. Streng genommen macht es gar nichts. Schließlich zeichnet sich ein Mikrocontroller-Programm dadurch aus, dass es auf externe Ereignisse reagiert und Ausgaben tätigt. Diese Ein- und Ausgaben können gerade auf einem Mikrocontroller sehr vielfältig sein, von einfachen Tastern als Ein- und LEDs als Ausgabe, bis hin zu Sensordaten und ganzen Displays oder Motoren ist alles dabei.

Also muss unser Controller mit der Außenwelt kommunizieren, und genau dafür brauchen wir die I/O-Pins, beziehungsweise Peripherie im allgemeinen. Deswegen wenden wir uns als erstes dem GPIO-Modul zu.

Das neue einfache Programm sieht dann so aus:

#include <stdint.h>
#include "LPC5410x.h" /* die generierte Datei fuer euren Controller */

extern void delay_ms(uint32_t ms); /* implemented somewhere else */

int main(void) {
	LPC_SYSCON->AHBCLKCTRLSET0 = SYSCON_AHBCLKCTRL0_GPIO0_Msk | SYSCON_AHBCLKCTRL0_IOCON_Msk;
	LPC_IOCON->PIO0_29 = (0x0 << IOCON_PIO0_29_FUNC_Pos) | /* ! */
		(0x2 << IOCON_PIO0_29_MODE_Pos) |                  /* ! */
		IOCON_PIO0_29_DIGIMODE_Msk;
	LPC_GPIO->DIR0 |= (1U << 29);

	while(1) {
		LPC_GPIO->NOT0 = (1 << 29);
		delay_ms(500);
	}
}

Das sieht doch schon wesentlich besser aus! Die Funktion ist 1:1 die selbe, wie im Beispiel oben, allerdings ist das doch wesentlich lesbarer, obwohl alle Kommentare fehlen. Lediglich die beiden mit /* ! */ markierten Zeilen sollten entweder kommentiert werden, oder mit einem aussagekräftigen define beschrieben werden, damit man nicht im User-Manual nachschauen muss, was 0x0 und 0x2 in diesem Kontext bedeutet.

So, ab jetzt kann jeder mit dem User-Manual als Referenz seine Applikation programmieren. Hilfe zu den einzelnen Peripherie-Einheiten findet man oben unter Aufbau/Peripherie.

TODO

  • (evtl ARM-Core-Config)
  • Syscon
  • GPIO, weiterführendes Beispiel
  • Delay (AVR-Libc Style)
  • Bearbeiten der Header, IOCON->PIOn_x -> IOCON->PIO[n][x], etc.
  • Abschließendes Wort und Verweis auf die nächsten Kapitel