C-Programmierung mit dem M_Dongle

C-Programmierung mit dem
M_Dongle
Andreas Reber Rev. 2.1
23.04.2015
Inhalt
Vorwort .....................................................................................................................................5
1
Grundlagen der C-Programmierung für einen Cortex........................................................ 6
1.1
Hardwareunabhängigkeit und CMSIS ....................................................................... 6
1.2
Nuvoton Bibliothek versus M_Dongle Bibliothek ....................................................... 6
1.3
Nomenklatur M_DongleLib ........................................................................................ 6
1.3.1
1.3.2
1.3.3
1.4
Modularität ................................................................................................................7
1.5
Grundstruktur für das C-File des Moduls ................................................................... 9
1.5.1
1.5.2
1.6
1.6.1
1.7
1.7.1
2
Makros .............................................................................................................................. 6
Funktionen ........................................................................................................................ 7
Module .............................................................................................................................. 7
Dokumentationskopf für das Modul (C-File) ................................................................... 9
Dokumentationskopf für die Funktion .............................................................................. 9
Grundstruktur für das Hauptfile ............................................................................... 10
@cpu .............................................................................................................................. 10
Basis-File für Projekte ............................................................................................. 10
Quellcode Vorlage für alle Projekte ................................................................................ 10
Grundlagen ..................................................................................................................... 11
2.1
Grundlagen Pin/Portprogrammierung...................................................................... 11
2.2
Pin/Portprogrammierung ......................................................................................... 12
2.2.1
2.2.2
2.2.3
2.2.4
2.2.5
2.2.6
2.3
2.3.1
2.3.2
2.3.3
2.3.4
2.3.5
2.3.6
2.3.7
2.4
2.4.1
2.4.2
2.4.3
2.4.4
2.5
2.5.2
Zuordnung Pinnamen-Definitionen-Portstrukturelemente .............................................. 12
Initialisierung per CMSIS ................................................................................................ 13
Initialisierung per M_Dongle Bibliothek .......................................................................... 13
Port-Pin Funktionen zur Bitmanipulation ........................................................................ 13
Tasterabfrage ................................................................................................................. 14
Unterscheidung Taster noch aktiv oder erneuter Tastendruck ...................................... 14
Grundlagen zu LCDs .............................................................................................. 15
Punktmatrix LCDs........................................................................................................... 15
Grafik LCDs .................................................................................................................... 15
Zeichendarstellung ......................................................................................................... 15
Zeichenerzeugung .......................................................................................................... 15
Zeichenausgabe ............................................................................................................. 16
COG_LCD Bibliothek (Modul) ........................................................................................ 17
Zahlen und Zeichen ........................................................................................................ 18
Grundlagen zum SysTick-Timer .............................................................................. 19
Initialisierung ................................................................................................................... 19
SysTick starten ............................................................................................................... 19
SysTick mit Interrupt ....................................................................................................... 19
Ein Nutzen des SysTicks ................................................................................................ 19
Grundlagen zu Interrupts ........................................................................................ 20
„Regeln“ für ISRs: ........................................................................................................... 20
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
-3-
2.6
2.6.1
2.6.2
2.6.3
2.6.4
2.6.5
2.6.6
2.6.7
2.7
2.7.1
2.7.2
2.7.3
2.7.4
2.7.5
2.7.6
2.7.7
3
Typ .................................................................................................................................. 21
Initialisierung ................................................................................................................... 21
Steuerung des ADC........................................................................................................ 21
Quantisierung ................................................................................................................. 21
Umwandlung in nutzbare Werte ..................................................................................... 21
Fehlerbetrachtung .......................................................................................................... 22
Grundlagen zur Dezimalzahlendarstellung .................................................................... 22
Grundlagen zum UART2 ......................................................................................... 23
Kennzeichen ................................................................................................................... 23
Übertragungsrate............................................................................................................ 23
Auswahl der Baudrate .................................................................................................... 23
Initialisierung des UARTs ............................................................................................... 23
Daten senden mit M_UART2_DATA_WRITE ................................................................ 24
Daten empfangen mit M_UART2_DATA_READ ........................................................... 24
Array senden .................................................................................................................. 24
Tipps und Tricks ............................................................................................................. 25
3.1
Zählschleifen........................................................................................................... 25
3.2
Abfragen ................................................................................................................. 26
3.2.1
3.3
3.3.1
3.3.2
3.3.3
4
Grundlagen zum ADC ............................................................................................. 21
Mischen von Abfragen .................................................................................................... 27
Variablen................................................................................................................. 27
Integerproblem ............................................................................................................... 27
Lösung ............................................................................................................................ 27
Strukturen ....................................................................................................................... 28
Vertiefende Peripherie Programmierung ......................................................................... 29
4.1
4.1.1
4.2
4.2.1
4.2.2
4.2.3
4.3
4.3.1
4.3.2
4.3.3
SysTick ................................................................................................................... 29
Beispiel SysTick als Manager ........................................................................................ 29
ADC ........................................................................................................................ 31
Interruptnutzung ............................................................................................................. 31
ISR für ADC .................................................................................................................... 31
Sinnvolles für die ISR ..................................................................................................... 31
UART2 .................................................................................................................... 33
Interruptnutzung ............................................................................................................. 33
ISR für den UART2......................................................................................................... 34
Sinnvolles für die ISR ..................................................................................................... 34
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
-4-
Vorwort
Dieses Usermanual ist entstanden, um Studenten ein kleines Nachschlagewerk an die Hand zu geben.
Die Kapitel beziehen sich dabei auf Grundlagen und Hinweise für die im Mikrocontroller-Labor zu lösenden Aufgaben.
Kapitel 4 ist für die jeweiligen Laborzusatzaufgaben bzw. Projekte gedacht und soll die Möglichkeiten
des M0 klarer machen.
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
-5-
1
Grundlagen der C-Programmierung für einen Cortex
Als die Cortex-Familie vor etwa 7 Jahren auf den Markt gekommen ist, war nicht nur der Prozessorkern
eine Neuerung, sondern auch das von ARM angedachte Software-Konzept, CMSIS genannt.
1.1
Hardwareunabhängigkeit und CMSIS
Sie sollen von Anfang an lernen, portablen, also wieder verwendbaren Code zu schreiben. Diese Thematik wurde von ARM durch die Einführung des Software-Konzeptes CMSIS angegangen. Das
Schlagwort steht für Cortex Microcontroller Software Interface Standard und beschreibt eine herstellerabhängige Hardwareabstraktionsschicht für die Cortex-M Familie. Der Sinn besteht darin, den hardwareabhängigen Softwareteil (CMSIS) von dem hardwareunabhängigen Teil aus aufzurufen.
Wenn die serielle Schnittstelle 1 mit der Funktion
Open_UART(1);
geöffnet werden kann und diese Funktion von jedem Hersteller in dessen CMSIS vorhanden ist, wird
bei einem Umstieg von einem NUC auf einen STM keine Anpassung notwendig, wenn der neue Mikrocontroller einen UART 1 hat. Das CMSIS wird vom Controllerhersteller mit geliefert, allerdings zeigt
sich, dass die Hersteller leider unterschiedliche gute CMSIS entwickelt haben. Nuvoton liefert zu einem
guten CMSIS auch noch Bibliotheken mit Funktionen mit, die den Umgang mit den komplexen Peripherie-Elementen erleichtern.
1.2
Nuvoton Bibliothek versus M_Dongle Bibliothek
Die von Nuvoton gelieferten Treiber und Funktionen haben leider den Nachteil, dass sie sehr viel Code
benötigen. Damit aber auch Projekte mit der freien Version der µVision übersetzt werden können, wurde eine eigene Bibliothek erzeugt. Diese Library ist noch im Entstehen, weshalb noch nicht alle benötigten Funktionen für die Peripherie existieren. Am Ende soll eine Bibliothek verfügbar sein, die alle Basisfunktionen für das M_Dongle enthält.
1.3
Nomenklatur M_Dongle Lib
Eine Nomenklatur ist wichtig, damit sich jeder Programmierer an auch mit fremden Funktionen zurecht
findet. Es gibt im Grunde nur drei „Regeln“ für die Namensgebung, die im Folgenden erklärt werden.
1.3.1
Makros
Makros sind textuelle Ersetzungen, d.h. eine komplexere Zeile in C wird durch eine Abkürzung ersetzt.
Zur besseren Abgrenzung von Funktionsnamen sind Makros mit einem vorangestellten „M_“ gekennzeichnet und werden wie folgt angelegt:
#define M_Peripheriegruppe_Aktion
z.B.
#define M_ADC_CONVERT_START
#define M_GPIO_BIT_GET(port)
ADC->ADCR.ADST = 1
*((volatile unsigned int *)(port))
Es ist üblich, Makros ähnlich wie Defines in Großbuchstaben zu schreiben.
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
-6-
1.3.2
Funktionen
Die Funktionen wurden von den Namen her an die Namensgebung von Nuvoton angelehnt, d.h. es wird
das Kürzel „Drv“ vor die „Peripheriegruppe“ gestellt und dann wieder die „Aktion“:
DrvGPIO_PortOpen(…);
In der Library für das M_Dongle, die im Ordner _Driver liegt, sind Funktionen für die unterschiedlichsten
Peripheriegruppen enthalten.
1.3.3
Module
Im Ordner _Module werden Funktionen in Dateienzusammen gefasst, die sich auf bestimmte Aufgaben
oder Peripherieelemente beziehen und als Basis die DrvXXX-Funktionen aus der M_Dongle-Bibliothek
benutzen. Dort sind zum Beispiel in der Datei GLCD.c alle Funktionen zu finden, die für den Zugriff auf
das Grafik-LCD des M_Dongles notwendig sind. Zur besseren Lesbarkeit sollten Teile des Modulnamens im Funktionsnamen für extern sichtbare Funktionen voran gestellt werden, z.B.:
GLCD_Init();
// Init the COG LCD
Da das Modul schon den Peripherienamen enthält, wird nur noch die Funktion nach dem Kürzel angegeben.
Die Funktionen der Module sind so zu schreiben, dass dort nur die Literale (Defines) aus dem Header
verwendet werden. Auf keinen Fall dürfen absolute Adressen und Werte benutzt werden, da sonst die
Module nicht mehr wieder verwendbar sind. Auf diesem Weg wird gewährleistet, dass die geschriebenen Funktionen für andere Plattformen (Hardware/CPU) portierbar sind, da nur die Funktionen/Makros
aus der Driver_M_Dongle angepasst werden müssen.
Modularität
1.4
Um bei größeren Projekten den Überblick zu behalten, ist es üblich, den Quellcode modular zu gestalten, d.h. Softwareteile werden in unterschiedlichen Dateien verwaltet.
Ein Modul besteht grundsätzlich aus zwei Dateien:
Modul.h enthält die Funktionsprototypen und Konstantendeklarationen
Modul.c enthält die eigentlichen Funktionen und die Variablendeklarationen
Die Schnittstelle bildet die Datei Modul.h, sie muss in die Datei eingebunden werden, in der das Modul verwendet werden soll.
Da Modul.h in mehrere Dateien eingefügt werden kann, muss die Mehrfacheinbindung verhindert
werden.
 Die verwendeten Headerfiles der Module stehen im C-Hauptfile des Projektes nicht im h-File.
Ein Modul muss für sich selbst fehlerfrei kompilierbar sein.
Beispiel
GLCD.h
#ifndef glcd_h
#define glcd_h
// Mehrfacheinbindungen verhindern
#include"..\_Driver\BoardConfig.h"
void GLCD_Init(void);
void GLCD_SetRow(uint8_t ui8Page);
void GLCD_SetColumn(uint8_t ui8Column);
void GLCD_PrintChar(uint8_t ui8Char);
#endif
//
//
//
//
Funktionsprototyp
Funktionsprototyp
Funktionsprototyp
Funktionsprototyp
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
-7-
GLCD.c
#include "GLCD.h"
#include "GLCD_Font_7x5.h"
// Modulheader einbinden
// zusätzliche Header einbinden
/*@-------------------------------------------------------------------------------------------@function: GLCD_SetRow
@descript: Send a command byte to LCD specifying the row (0..7)
@paramin:
uint8_t ui8Row: row in LCD
@paramback: none
@remarks:
Command code: 0xB | lower nibble row
--------------------------------------------------------------------------------------------@*/
void GLCD_SetRow(uint8_t ui8Row)
// Wie FKT Prototyp
{
M_LCD_SET_COMMAND;
// command mode
ui8Row &= 0x0F;
// to prevent values > 16
SPI3_SingleWrite_Data(0xB0 + ui8Row);
}
main.c
#include "GLCD.h"
// Modul einbinden
int main()
{
GLCD_Init();
// LCD Init mit Funktion aus GLCD
GLCD_SetRow(Zeile4);
GLCD_SetColumn(Spalte2);
// Zeile setzen mit Funktion aus GLCD
// Spalte setzen mit Funktion aus GLCD
GLCD_PrintChar(0x30);
// ‚0‘ ausgeben mit Funktion aus GLCD
while(1)
{}
}
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
-8-
1.5
1.5.1
Grundstruktur für das C-File des Moduls
Dokumentationskopf für das Modul (C-File)
Jedes File bekommt grundsätzlich einen Dokumentationskopf, der je nach File größer oder kleiner ausfallen kann. Der Dokumentationskopf für das Hauptfile ist größer als für die restlichen Files.
/**-------------------------------------------------------------------------------------------@brief
GLCD.c
@details
Library for LCD EA DOGL128
@global
GLCD_
@cpu
NUC130VE
@date
14.3.2013
@author
Andreas Reber
@history
--------------------------------------------------------------------------------------------**/
#include "GLCD.h"
// Modul-Header
#include "GLCD_Font_7x5.h"
// Font
#include"..\_Driver\Driver_M_Dongle.h"
@brief:
@details:
@global:
@cpu:
@date:
@author:
@history:
1.5.2
// Hardwareabhängigkeiten
Name für das C-Modul
Beschreibung
Kürzel für die extern sichtbaren Funktionen
verwendete CPU
Erstellungsdatum
Wer es geschrieben hat
Wer wann was geändert hat
Dokumentationskopf für die Funktion
Die Doku für den Funktionskopf ist deshalb wichtig, um sich möglichst einfach über die Funktion und die
Parameter klar zu werden.
/**-------------------------------------------------------------------------------------------@brief
GLCD_SetRow
@details
Send a command byte to LCD specifying the row (0..7)
@param[in] uint8_t ui8Row: row in LCD
@param[out] none
@remarks
Command code: 0xB | lower nibble row
--------------------------------------------------------------------------------------------**/
@brief:
@details:
@param[in]:
@param[out]:
@remarks:
Name der Funktion
Beschreibung
Variablen mit Typ, die die Funktion bekommt
Rückgabeparameter
Bemerkungen
Die verwendeten Schlüsselwörter mit der Kennung @ können von einem Programm gelesen und somit
eine automatische Dokumentation erstellt werden.
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
-9-
Grundstruktur für das Hauptfile
1.6
Jedes File bekommt grundsätzlich einen Dokumentationskopf, der je nach File größer oder kleiner ausfallen kann. Der Dokumentationskopf für das Hauptfile ist größer als für die restlichen Files.
/**-------------------------------------------------------------------------------------------@brief
Aufgabeexx
@details
Vorlage
@cpu
NUC130VE, 48 MHz, ca. 21 ns per CPU cycle
@date
@author
@history
--------------------------------------------------------------------------------------------**/
#include "..\_Driver\BoardConfig.h"
#include "..\_Module\GLCD.h"
#include "init.h"
1.6.1
@cpu
Da es unterschiedliche Prozessortypen innerhalb einer Familie gibt, ist es einfach im Projekthauptfile
notwendig, den Typ, für den das Programm geschrieben wurde, anzugeben. Dies ist deshalb wichtig,
da sich die einzelnen Typen in ihrer „Ausstattung“ unterscheiden und man nicht unbedingt ein Programm, welches für den NUC130VE3CN entwickelt wurde, auf einem NUC120 laufen lassen kann. Die
Angabe der Taktfrequenz (hier 48 MHz) ist dann von Bedeutung, wenn Zeit- und Zählschleifen enthalten sind, da sich das Programmverhalten bei einer anderen Taktfrequenz ändert.
Basis-File für Projekte
1.7
Bei einem modernen Mikrocontroller müssen zu Beginn einige Aufgaben erledigt werden, damit er
überhaupt mit dem eigentlichen Programm starten kann:
1.7.1
Quellcode Vorlage für alle Projekte
/**-------------------------------------------------------------------------------------------@brief
GLCD.c
@details
Vorlage
@cpu
NUC130VE, 12 MHz, ca. 84 ns per CPU cycle
@date
@author
@history
--------------------------------------------------------------------------------------------**/
#include "..\_Driver\BoardConfig.h"
#include "init.h"
int main(void)
{
DrvSystem_ClkInit();
Init_Board();
// Setup Clksystem
// Setup M_Dongle
// ab hier eigene Routinen aufrufen
}
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 10 -
2
Grundlagen
Grundlagen Pin/Portprogrammierung
2.1
Alle komplexen Mikrocontroller haben das gleiche Problem: Die meisten Portpins sind mehrfach verwendbar. Das bedeutet für den Programmierer einen größeren Aufwand, bis er einen Pin nutzen kann
und es besteht auch die Gefahr, dass bei falscher Programmierung der Pin zerstört wird. Diese Komplexität war mit ein Grund, warum das CMSIS entstanden ist. Beim NUC130 kann aus bis zu vier Funktionen pro Pin ausgewählt werden. Softwareseitig werden die Ports in Strukturen abgebildet, die über
Zeiger angesprochen werden. Es besteht die Möglichkeit, einzelne Portpins zu setzen oder zu löschen
und ein einzelner Pin kann auf seinen Zustand hin überprüft werden. Pingruppen werden mittels der
Portstruktur bearbeitet. Hier kommt nun eine Struktur zum Einsatz, die es ermöglicht, einen 32 Bit-Port
in 16 Bit- oder 8 Bit-Blöcke zu teilen. Das hat den Vorteil, dass nur Werte für den benötigten Block
transportiert werden müssen und somit wird Speicherplatz gespart.
GPIOE:
Deklaration für einen Zeiger auf die Basisadresse von Port E des Controllers
PMD:
Struktur für PortMultiplexData, hier kann aus 4 Funktionsmöglichkeiten gewählt werden
.PMDx:
Gibt die Portnummer innerhalb von PMD an
DOUT_BYTE1: Output 8 Bit innerhalb der Struktur für einen 32 Bit Port
PIN_BYTE0: Input 8 Bit innerhalb der Struktur für einen 32 Bit Port
Die beiden letzten Bezeichner soll etwas näher erläutert werden, da sie sehr gut nutzbar für eine effektive Programmierung sind. Die benötigten Strukturen sehen wie folgt aus:
union
{
__IO uint32_t u32DOUT;
__IO uint32_t DOUT;
struct
{
__IO uint16_t HW_L;
__IO uint16_t HW_H;
};
struct
{
__IO uint8_t DOUT_BYTE0;
__IO uint8_t DOUT_BYTE1;
__IO uint8_t DOUT_BYTE2;
__IO uint8_t DOUT_BYTE3;
};
};
union
{
__IO uint32_t u32PIN;
__IO uint32_t PIN;
struct
{
__IO uint16_t PIN_HW_L;
__IO uint16_t PIN_HW_H;
};
struct
{
__IO uint8_t PIN_BYTE0;
__IO uint8_t PIN_BYTE1;
__IO uint8_t PIN_BYTE2;
__IO uint8_t PIN_BYTE3;
};
};
Sie bieten nicht nur die Möglichkeit, 32 Bit zu verarbeiten, sondern können ohne Probleme für 16 oder 8
Bit genutzt werden.
Problem:
Ein Port hat 8 Eingänge und 8 Ausgänge, die auf Bytegrenzen liegen. Ist der Port nur
als Ganzes lesbar, so müssen nach dem Einlesen die Ausgänge die Bits für die Eingänge per Maske gelöscht werden.
Lösung:
Mit obigen Strukturen können die Ein- und Ausgänge getrennt ohne Aufwand angesprochen werden.
GPIOE->DOUT_BYTE1
= 0xFF;
u8Data = GPIOE->PIN_BYTE0;
// Schreiben
// Lesen
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 11 -
Pin/Portprogrammierung
2.2
„Licht an oder woher weiß der Cortex-M0, wie er die LED zum Leuchten bringt“
Er weiß leider gar nichts.
Der Cortex verrichtet nur seine Arbeit (er arbeitet das Programm ab), indem er unter anderem Werte
aus dem Speicher liest und an eine andere Speicheradresse schreibt.
Dass die beschriebene LED dann leuchtet ist schön, doch davon hat er keine Ahnung.
Dass die beschriebene Speicherstelle eine Verbindung mit der Außenwelt hat, ist eine Eigenschaft des
NUC130 auf dem M_Dongle, auf einem anderen Board kann eine komplett andere Funktion liegen.
2.2.1
Zuordnung Pinnamen-Definitionen-Portstrukturelemente
Byte
Register
GPIOE->
DOUT_BYTE1
GPIOE->
PIN_BYTE0
Bit
Register
PE15_PDIO
PE14_PDIO
PE13_PDIO
PE12_PDIO
PE11_PDIO
PE10_PDIO
PE9_PDIO
PE8_PDIO
PE7_PDIO
PE6_PDIO
PE5_PDIO
PE4_PDIO
PE3_PDIO
PE2_PDIO
PE1_PDIO
PE0_PDIO
Definition für
DrvGPIO_xxx
LED7
LED6
LED5
LED4
LED3
LED2
LED1
LED0
Definition für
M_GPIO_BIT_xxx
BIT_LED7
BIT_LED6
BIT_LED5
BIT_LED4
BIT_LED3
BIT_LED2
BIT_LED1
BIT_LED0
~ Pin
Value
JOY_DOWN
JOY_TASTER
BIT_JOY_DOWN
BIT_JOY_TASTER
0x80
0x40
JOY_L
JOY_R
JOY_UP
BIT_JOY_L
BIT_JOY_R
BIT_JOY_UP
0x10
0x08
0x04
Pin
Name
PE15
PE14
PE13
PE12
PE11
PE10
PE9
PE8
PE7
PE6
PE5
PE4
PE3
PE2
PE1
PE0
Folgende zwei Zeilen bewirken, dass alle Taster des Joysticks eingelesen werden und das Leseergebnis auf die LEDs ausgegeben wird:
u8Value = GPIOE->PIN_BYTE0;
GPIOE->DOUT_BYTE1 = u8Value;
Die folgenden Zeilen geben nun den Assembler-Code an, der durch das Compilieren entsteht:
LDR r0,[pc,#252] // Basis Adresse von GPIOE nach r0 laden
LDRB r0,[r0,#0x10] // Adresse für PIN_BYTE0 bilden und Byte lesen nach r0
STR r0,[sp,#0x00] // u8Value sichern
LDR r1,[pc,#248] // Basis Adresse für GPIOE nach r1 holen
LDR r0,[sp,#0x00] // u8Value nach r0 laden
STRB r0,[r1,#0x09] // Adresse für DOUT_BYTE1in r1 bilden und r0dorthin speichern
Die Zeilen verdeutlichen das Prinzip, nachdem ein Prozessor arbeitet.
Er bildet Adressen, von denen er Daten liest, bearbeitet dann diese Werte und speichert sie anschließend wieder an anderer Stelle ab.
Das im obigen Fall an der Adresse GPIOE->PIN_BYTE0 die Tasten des Joysticks liegen, ist für den Core
nicht wichtig. Auch das unter der Adresse GPIOE->DOUT_BYTE1 LEDs angeschlossen sind und keine
normale Speicherstelle, ist irrelevant. Nuvoton hat dafür gesorgt, dass die Adressen eine Verbindung
zur Außenwelt enthalten und damit bei korrekter Programmierung die gewünschte Funktion (Taster
aktiv, LED leuchtet) realisiert werden kann.
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 12 -
2.2.2
Initialisierung per CMSIS
Bevor Portpins genutzt werden können, muss ihre Funktion (Eingang, Ausgang, OpenDrain, BiDirektional) durch Beschreiben der zum Pin gehörenden Funktionskontrollregister eingestellt werden.
Diese Initialisierung kann auf unterschiedliche Weise durchgeführt werden.
// Pin E8 für LED0 als Ausgang initialisieren
GPIOE->PMD.PMD8 = PORT_DIR_OUT;
// Pin E2 für den Taster Joystick Down als Eingang initialisieren
GPIOE->PMD.PMD2 = PORT_DIR_IN;
So flexibel obige Zeile aus dem CMSIS auch ist, sie verlangt jedoch eine gute Kenntnis der jeweiligen
Header-Files der Prozessoren. Bei Portpins, die zu komplexen Peripherieeinheiten gehören, ist noch
einiges mehr zu tun.
2.2.3
Initialisierung per M_Dongle Bibliothek
Um das „Zeiger-Chaos“ aus der CMSIS zu umgehen, wurde eine Bibliothek angelegt, die einfacher zu
nutzen ist.
Damit die Funktionen aus der Bibliothek genutzt werden können, muss folgendes eingebunden sein:
#include „..\_Driver\Driver_M_Dongle.h“
Die zugehörige Initialisierungs-Funktion für Portpins aus der Bibliothek lautet:
DrvGPIO_Open(E_GPE, 8, E_IO_OUTPUT);
DrvGPIO_Open(E_GPE, 2, E_IO_INPUT);
// Pin für LED0 Output
// Pin für Joystick Input
Damit der Sourcecode lesbarer wird, sind Defines in der BoardConfig.h angelegt:
#define PORT_LEDS
#define PORT_JOY
E_GPE
E_GPE
// Portgruppe E
// Portgruppe E
#define LED0
#define JOY_DOWN
8
2
// Pin 8
// Pin 2
Es ist zwar mehr zu schreiben, doch der Code dokumentiert sich fast selbst:
DrvGPIO_Open(PORT_LEDS, LED0, E_IO_OUTPUT);
// LED0 als Output
DrvGPIO_Open(PORT_JOY, JOY_DOWN, E_IO_INPUT); // Joystick Down als Input
2.2.4
Port-Pin Funktionen zur Bitmanipulation
Funktionen zur Bitmanipulation und Abfrage wurden als Makros angelegt:
M_GPIO_BIT_SET(BIT_LCD_A0);
M_GPIO_BIT_CLEAR(BIT_LCD_A0);
M_GPIO_BIT_GET(BIT_TASTER_2);
// Setzt Pin für LCD_A0
// Löscht Pin für LCD_A0
// Einlesen Pin für SW2
Für manche Abläufe, z.B. Lauflichter, ist es einfacher, Portpins mit Zählern zu beeinflussen, weshalb
obige Routinen zur Bitmanipulation auch als Funktionen verfügbar sind:
DrvGPIO_SetBit(PORT_LCD, LCD_A0);
// Setzt Pin für LCD_A0
DrvGPIO_ClrBit(PORT_LCD, LCD_A0);
// Löscht Pin für LCD_A0
uint32_t = DrvGPIO_GetBit(P_TASTER, TASTER_SW2);// Einlesen Pin für SW2
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 13 -
2.2.5
Tasterabfrage
Das Hauptproblem bei der Tasterabfrage ist die Geschwindigkeit des Mikrocontrollers und die mechanischen Eigenschaften des Tasters. Mechanische Komponenten wie Taster, Schalter oder Relais erzeugen Störungen, bei Tastern und Schaltern wird dies Prellen genannt.
Ideales Tastersignal:
Reales Tastersignal:
Um dieses Prellen zu beseitigen, kann einfach eine bestimmte Zeit gewartet werden, bis die störenden
Signale abgeklungen sind. Dieses Warten kostet jedoch CPU-Zeit und bei Prellzeiten von bis zu 100 ms
würde dieses Verfahren unnötig Energie kosten. In 20 ms könnte die CPU 240000 Befehle abarbeiten,
weshalb andere Verfahren realisiert werden.
2.2.6
Unterscheidung Taster noch aktiv oder erneuter Tastendruck
In vielen Programmen sollen einzelne Tastendrücke registriert werden, also Taster drücken und loslassen. Für den Joystick ist folgende Variante möglich, der Vergleich alt mit neu. Dazu werden zwei Variablen deklariert:
uint8_t u8JoystickAlt;
uint8_t u8Joystick;
// letzter Wert des Joysticks
// aktueller Wert des Joysticks
Der aktuell Wert für u8Joystick wird durch das Lesen mittels
u8Joystick = GPIOE->PIN_BYTE0;
// Einlesen des ganzen Joystick Ports
ermittelt.
Mit Hilfe eines switch/case Konstrukts kann nur die aktuelle Joystick-Position ermittelt und die nötige
Aktion durchgeführt werden.
switch(u8Joystick)
{
case ??: ……
}
Obiger Code hat den Nachteil, dass er nicht in der Lage ist zu kennen, ob der Joystick immer noch gedrückt ist oder nicht.
Um dieses Problem zu beheben, wird der Code wie folgt erweitert:
u8Joystick = GPIOE->PIN_BYTE0;
// Einlesen des ganzen Joystick Ports
if(u8Joystick != u8JoystickAlt)
{
u8JoystickAlt = u8Joystick;
switch(u8Joystick)
{
case ??: ……
}
}
// Veränderung ?
// Speichern des aktuellen Wertes
Die Tastenbearbeitung wird nur dann ausgeführt, wenn der aktuelle Wert des Joysticks nicht dem gespeicherten entspricht.
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 14 -
Grundlagen zu LCDs
2.3
2.3.1
Punktmatrix LCDs
Punktmatrix LCDs sind heutzutage sehr häufig anzutreffen. Sie besitzen einen eigenen kleinen Controller, der die komplette Ansteuerung des LCDs übernimmt, so dass der Anwender ein sehr einfaches
Interface erhält. Für die weit verbreiteten LCDs mit HD44780 Displaycontroller besteht dieses Interface
aus den drei Registern Control, Data und Status. Zeichen und kleine grafische Elemente sind im LCD
Controller als Pixelmuster hinterlegt und werden per Befehlssequenz an die entsprechende Stelle im
LCD geschrieben.
2.3.2
Grafik LCDs
Hier gibt es viele unterschiedliche Ausführungen, weswegen es dort auch keine einheitlichen Controller
zur Displayansteuerung gibt. Die Ausführungen teilen sich in folgende Gruppen:
1. „Dumme“ LCDs: Bis auf Zeilen- und Spaltentreiber keine Elektronik vorhanden -> Anwender
muss alles selbst machen, inklusive der zeitlichen Ansteuerung -> hohe Rechenleistung.
2. LCDs mit Pixelspeicher: Das LCD enthält für jedes Pixel den notwendigen Speicherplatz und
die Ansteuerelektronik für die Pixel -> Anwender schreibt die Daten in den Displayspeicher,
die LCD-Elektronik kümmert sich um die Ausgabe inkl. zeitlicher Ansteuerung -> mittlere Rechenleistung, da er immer noch jedes aktive Pixel selber ermitteln muss.
3. LCDs mit Grafik-Controller: Eine Gruppe von LCDs, die immer weniger zu finden ist. Sie bietet die Möglichkeit, dem LCD komplexe Befehle zu übermitteln, der diese dann in die entsprechende Aktion umsetzt inkl. zeitlicher Ansteuerung -> kleine Rechenleistung.
2.3.3
Zeichendarstellung
Zeichen, also Buchstaben oder Zahlen werden grundsätzlich als Gruppen von Pixel dargestellt. Diese
Pixel sind in einer festen Matrix angeordnet, was eine einfachere Berechnung der Position ermöglicht.
Der Zeichensatz, auch Font genannt, ist entweder im Grafikcontroller enthalten oder wird bei der Programmierung festgelegt.
Für jeden Pixel auf dem LCD muss ein Speicherplatz vorgesehen werden. Einfarbige (monochrome)
LCDs benötigen pro Pixel ein Bit, bei mehrfarbigen LCDs hängt der Speicherbedarf von der Anzahl der
darzustellenden Farben ab.
Im Labor wird ein LCD aus der Gruppe 2 verwendet, also keine Funktionen, sondern nur interner Pixelspeicher, allerdings monochrom, d.h. Schwarz/Hintergrundfarbe. Der Zeichensatz beruht auf einer Größe von 6 x 8 Pixel (Breite x Höhe). Da jeder Pixel 1 Bit für die Farbe hat, müssen pro Zeichen 6Bytes
Daten übertragen werden (6 x 8 x1 /8).
Auf dem LCD lassen sich 8 Zeilen a 21 Zeichen darstellen (64 x 128 Pixel). Es ist möglich, eine komplette Ansicht im Speicher des Mikrocontrollers zu erzeugen und sie dann zum LCD zu übertragen. Dies
ist bei Grafiken sinnvoll, bei Text kann direkt in den Pixelspeicher des LCDs geschrieben werden.
2.3.4
Zeichenerzeugung
Woraus besteht ein Muster? Als Grundlage soll der Buchstabe H aus 6 x 8 Pixel dienen:
1
1
1
1
1
1
1
0
Pixelmuster
0
0
0
1
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
1
0
0
0
0
1
1
1
1
1
1
1
0
Pixelcodierung
0
0
0
0
0
0
0
0
1
1
1
1
1
1
1
0
0
0
0
1
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
1
0
0
0
0
1
1
1
1
1
1
1
0
Speicherablage 1
0
0
0
0
0
0
0
0
1
1
1
1
1
1
1
0
0
0
0
1
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
1
0
0
0
0
1
1
1
1
1
1
1
0
0
0
0
0
0
0
0
0
Speicherablage 2
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 15 -
•
Pixelmuster:
Als erstes fällt auf, dass die rechte Spalte und die unterste Zeile nicht benutzt werden. Die rechte Spalte dient als Mindestabstand zwischen zwei Buchstaben. Die unterste Zeile wird für kleine
Buchstaben wie g, q, und p benutzt.
•
Pixelcodierung:
Ein schwarzer Pixel wird als 1 gespeichert, ein weißes als 0, wobei es nur bedeutet, dass die
Bits, die eine 1 enthalten, einem aktiven Pixel entsprechen.
•
Speicherablage:
In Abhängigkeit vom verwendeten LCD werden die Daten entweder zeilen- oder spaltenweise
gespeichert. Die Anordnung sollte nach dem verwendeten Pixelspeicher gewählt werden, da es
sonst bei falscher Wahl zu einer Erhöhung der Rechenzeit kommt.
Damit werden 5 Bytes (5 x 8 Bit) für die Darstellung eines Buchstabens benötigt. Die Information, die in
diesen 5 Bytes enthalten sind, sagen also nur aus, welches der Pixel aktiv ist. Für die Darstellung auf
dem LCD wird nun diese Information interpretiert und mit der gewünschten Pixelfarbe hinterlegt und auf
das LCD geschrieben. Für unsere Hardware ist es recht einfach, da es sich um ein monochromes LCD
handelt, entspricht jede Pixelinformation einer Farbinformation und damit können die 5 Byte direkt auf
das LCD geschrieben werden. All diese Pixelinformationen werden in einem Font-Array im Programmspeicher hinterlegt, dessen Daten in der Datei „GLCD_Font_5x7.h“ angelegt sind. Da die sechste Spalte immer 0x0 ist, wurde sie in der Font-Datei nicht mit angelegt, stattdessen in der entsprechenden
LCD-Funktion gesendet.
2.3.5
Zeichenausgabe
Die Ausgabe eines Buchstabens erfolgt dadurch, dass die Startposition für das Zeichenmuster im FontArray berechnet wird. Der überwiegende Teil der Font-Dateien beginnt bei dem ASCII-Zeichen 0x0, das
eigentlich ein Steuerzeichen und deshalb nicht darstellbar ist. Der Start bei 0x0 hat allerdings den Vorteil, dass vom gewünschten Zeichen kein Offset abgezogen werden muss. Für Controller mit wenig
Programmspeicher ist es aber denkbar, die ersten 32 Elemente aus der Tabelle nicht zu verwenden
und damit Platz zu sparen. Viele Fonts verwenden auch nur die 7 Bit ASCII-Tabelle, also nur maximal
128 Zeichen.
Für unseren Font gilt:
Startposition = ASCII-Wert des Zeichens * 5
Die Zeichen-Position im LCD ist etwas komplexer zu ermitteln. Das LCD des Laborboards enthält einen
Display-Controller vom Typ ST7565R, der sich um die zeitliche Ansteuerung der Darstellung kümmert
und außerdem noch den Pixelspeicher enthält. Für die Darstellung von Zeichen besitzt er ein praktisches Feature, die Page-Adressierung. Das LCD ist in acht Zeilen aufgeteilt und jede Zeile entspricht
einer Page. Soll nun die Position für den Buchstaben gesetzt werden, wird einfach die gewünschte Zeile
(beginnend bei 0) gesetzt, nur die Spalte muss noch berechnet werden. Die Startposition für eine Spalte
sind Vielfache von 6 (Zeichenbreite inkl. freier Spalte), es könnten aber auch andere Werte verwendet
werden.
Folgender Code gibt den Buchstaben ‚H‘ in Zeile 2, Textspalte 7 aus:
GLCD_SetTextCursor(2,7);
GLCD_PrintChar(‚H‘);
// Cursor setzen
// Zeichen ausgeben
Zuvor müssen im Initialisierungsteil des Programms einige Funktionen aufgerufen werden, damit das
LCD funktioniert.
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 16 -
2.3.6
COG_LCD Bibliothek (Modul)
Da es schon Bibliotheks-Funktionen mit dem Namensanfang LCD_xxx gibt, wurden die neuen Funktionen um den Präfix ‚G‘ erweitert, damit der Unterschied zu den gleichlautenden Funktionen für TextLCDs gegeben ist.
2.3.6.1
LCD Initialisieren
void GLCD_Init(void);
Da es sich um ein komplexes grafisches LCD handelt, sind diverse Schritte notwendig, damit es korrekt
funktioniert. Das LCD selber wird per SPI vom NUC130 angesteuert, weshalb die notwendige Initialisierung der benutzten SPI-Schnittstelle innerhalb der Funktion stattfindet.
2.3.6.2
Cursor auf Textzeile setzen
void GLCD_SetRow(uint8_t ui8Row);
Diese Funktion setzt die Textzeile in den Bereichen von 0 bis 7.
2.3.6.3
Cursor auf Textspalte setzen
void GLCD_SetColumn(uint8_t ui8Column);
Diese Funktion setzt die Textspalte in den Bereichen von 0 bis 20.
2.3.6.4
Cursor auf Textposition setzen
void GLCD_SetTextCursor(uint8_t ui8Row, uint8_t ui8Column);
Mit dieser Funktion wird Zeile und Spalte gesetzt.
2.3.6.5
Ausgabe eines Zeichens
void GLCD_PrintChar(uint8_t ui8Char);
Gibt ein Zeichen an die Stelle aus, an der der Textcursor im LCD steht.
2.3.6.6
Ausgabe eines Textes
void GLCD_PrintText(uint8_t ui8Row, uint8_t ui8Column, uint8_t *aui8Text);
Gibt einen Text ab der Stelle aus, die durch ui8Row, ui8Column angegeben wird.
2.3.6.7
Löschen einer Zeile
void GLCD_ClearRow(uint8_t ui8Row);
Löscht die angegebene Zeile.
2.3.6.8
Löschen des LCDs
void GLCD_ClearLCD(void);
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 17 -
2.3.7
Zahlen und Zeichen
Unter Zeichen wird alles verstanden, was auf dem LCD ausgegeben werden kann. Im Mikrocontroller
sind die Zahlen im Binärformat vorhanden, die vor ihrer Ausgabe erst in einzelne Stellen eines Zahlenformates umgerechnet werden müssen. Zur Ausgabe auf das LCD ist noch eine Umwandlung der berechneten Stellen in das ASCII-Format notwendig. Die gebräuchlichsten Formate sind das Dezimalsystem und das Hexadezimalsystem.
Um eine Variable mit 8 Bit, also eine uint8_t in die für das LCD notwendige Zeichenkette zu zerlegen,
sind folgende Schritte notwendig:
Dezimal-Format:
uint8_t u8Test = 56;
uint8_t u8Zehner;
uint8_t u8Einer;
// Variable mit dem Inhalt 56
// Variable für die Zehner-Stelle
// Variable für die Zehner-Stelle
u8Zehner
u8Zehner
u8Einer
u8Einer
//
//
//
//
=
=
=
=
u8Test / 10;
u8Zehner+0x30;
u8Test %10;
u8Einer +0x30;
Abtrennen der Zehner-Stelle
Ergebnis in ein ASCII-Zeichen umwandeln
Abtrennen der Einer-Stelle
Ergebnis in ein ASCII-Zeichen umwandeln
Mit
GLCD_SetTextCursor(1,1);
GLCD_PrintChar(u8Zehner);
GLCD_PrintChar(u8Einer);
wird der Inhalt von u8Test im Dezimalformat ab der Position Zeile1 /Spalte1 ausgegeben.
Um die Anzahl der lokalen Variablen zu reduzieren, ist auch folgende Variante möglich:
GLCD_SetTextCursor(1,1);
GLCD_PrintChar((u8Test / 10)+0x30);
GLCD_PrintChar((u8Test % 10)+0x30);
Die Addition der 0x30 ist notwendig, da dies den Offset zwischen Zahl und dem entsprechenden darstellbarem Zeichen in der ASCII-Tabelle darstellt.
Diese Umwandlung ist noch nicht vollständig, da nur Zahlen kleiner Hundert dargestellt werden können.
Für Zahlen mit 8 Bit, also max. 255 muss noch der Teil für die Hunderter eingefügt werden.
Da solche Aufgaben elementar sind, werden Funktionen für die gängigen Variablenlängen und Zahlenformate angelegt. Dafür sollte eine eingängige Namensgebung gewählt werden, z.B.:
void u8toDezimal(uint8_t u8Wert, uint8_t *pu8Zeichen);
void u8toHEX(uint8_t u8Wert, uint8_t *pu8Zeichen);
Auffällig sind die Zeiger als zweiter Übergabe-Parameter. Dies hat den Vorteil, dass die Funktion keine
Variablen kennen muss, in der die gewandelten ASCII-Zeichen abgelegt werden müssen. Die Zeichen
werden einfach ab der Stelle 0 in den String geschrieben und nach der Rückkehr aus der Umwandlungsroutine können die Zeichen einfach per
u8toDezimal(56, stru8Dez);
GLCD_PrintText(1,1, stru8Dez);
ausgegeben werden.
Für das HEX-Format können die obigen Zeilen einfach angepasst werden, da nur der Divisor verändert
wird.
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 18 -
Grundlagen zum SysTick-Timer
2.4
Dieser kleine Timer ist in allen Cortex-Derivaten enthalten und dient primär dazu, einen Interrupt für den
Systemtimer zu erzeugen, wie er bei Betriebssystemen benötigt wird. Für einen etwas flexibleren Einsatz wurden in der Bibliothek für das M_Dongle folgende Funktionen realisiert.
2.4.1
Initialisierung
Mit Hilfe von
DrvSysTick_Init_ms(uint32_t msec);
wird er für Abstände in Millisekunden konfiguriert. Dabei werden die Clock-Einstellungen des Systems
verwendet und der SysTick wird damit mit einem Takt von 11,0592 MHz versorgt. Mit dieser Einstellung
können Zeiten bis 1,517 s erzeugt werden. Das ist nicht viel, aber der Trick des SysTicks ist die Möglichkeit, eine andere Taktquelle zu verwenden. Für seine Funktion als Systemtimer ist die Zeitspanne
ausreichend. Die SysTick-Einheit lädt automatisch nach dem Ablauf der eingestellten Zeit den alten
Wert.
2.4.2
SysTick starten
Das Makro
M_SYSTICK_ENABLE;
startet den Timer. Normalerweise wird der SysTick nur gestartet, aber nie wieder gestoppt.
2.4.3
SysTick mit Interrupt
Mit dem nächsten Makro
M_SYSTICK_INT_ENABLE;
wird der SysTick interruptfähig gemacht. Diese Betriebsart macht am meisten Sinn. Da es sich um einen Interrupt handelt, muss auch eine zugehörige Serviceroutine geschrieben werden, die bei Keil einen festgelegten Namen hat:
void SysTick_Handler (void) // SysTick Interrupt Handler
{
...Insert function here
}
2.4.4
Ein Nutzen des SysTicks
In vielen Anwendungen wird der SysTick als zyklischer Zeitgeber verwendet, d.h. er wird auf einen bestimmten Wert initialisiert und seine Interruptmöglichkeit genutzt. Nach Ablauf der Zeitspanne tritt nun
der Interrupt des SysTicks auf und innerhalb der ISR kann nun die Aufgabe erledigt oder angestoßen
werden. Wenn er die Aufgabe selber erledigen soll, so wird der Programmcode innerhalb der ISR geschrieben. Bei größeren Aufgaben stößt er die Bearbeitung nur an, d.h. er setzt eine Variable auf einen
dafür notwendigen Wert und ein anderer Programmteil, meistens die Main-Schleife, bearbeitet dann die
Anforderung.
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 19 -
Grundlagen zu Interrupts
2.5
Ein Interrupt ist eine Unterbrechung des normalen Programmablaufes. Einsatzgebiet kann grob in drei
Kategorien unterteilt werden:
1.
2.
3.
4.
Schnelle Reaktion auf Ereignisse
Wenn ein Softwareteil unbedingt ausgeführt werden soll
Auftretende Ereignisse in unregelmäßigen Abständen
Energie sparen
2.5.1.1 Schnelle Reaktion
Soll der SysTick als genauer Zeitgeber funktionieren, muss er seine Arbeit (seine Interrupt-Routine ISR)
so schnell es geht nach dem Ablauf der eingestellten Zeit verrichten. Durch die Freischaltung des Interrupts wird nun so schnell wie möglich (in ca. 6 bis 8 Maschinenzyklen) zu der ISR gewechselt und es
entsteht nur eine minimale Verzögerung.
2.5.1.2 Unbedingte Ausführung
Bestimmte Sicherheitsfunktionen werden gerne in ISRs verlagert, da eine freigeschaltete ISR immer
funktioniert, auch wenn sich das Programm aufgehängt hat.
2.5.1.3 Unregelmäßige Abstände
Für Ereignisse in unregelmäßigen Abständen sind Interrupts ebenfalls ein Vorteil, da das Hauptprogramm nicht ständig die Portpins oder Peripherieelemente abfragen muss, die diese Interrupts erzeugen könnten.
2.5.1.4 Energie sparen
Als weiteres kommt noch die neue Technologie dazu, die es erlaubt, den Prozessor in den Schlaf zu
versetzten und erst mittels Interrupt wieder zu wecken. Diese Funktion ermöglicht es, sehr energiesparende Systeme zu entwickeln.
Für alle freigeschalteten Interrupts gilt, dass die zugehörige Interrupt Service Routine, die ISR als Funktion im Programm angelegt sein muss, da sonst die CPU in eine Endlosschleife gerät und das Programm steht. Für Keil müssen die fest gelegten Namen der ISRs verwendet werden.
Aus der Sicht des Programmierers ist eine ISR ein Unterprogramm, allerdings mit folgenden Unterschieden zu einem normalen Unterprogramm:
•
•
•
•
2.5.2
•
•
•
•
•
Eine ISR hat keine Übergabe- und Rückgabe-Parameter
Für die einzelnen ISR existieren vordefinierte Namen (siehe startup_NUC1xx.s)
Es wird nie von der Software aufgerufen, sondern der Aufruf geschieht durch die Hardware
Eine ISR hat immer Vorrang gegenüber dem normalen Programmablauf
„Regeln“ für ISRs:
So kurz wie möglich
Ausgaben auf langsame Peripherie wie LCD sind verboten
Wenn notwendig, wird die Main-Schleife informiert, dass sie etwas zu tun hat
Jede ISR will gut überlegt sein, ob sie sinnvoll ist
Jede Interruptquelle muss im NVIC und in der eigenen Peripherie freigeschaltet werden
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 20 -
2.6
2.6.1
Grundlagen zum ADC
Typ
Der im Mikrocontroller verbaute Analog-Digital-Wandler kommt aus der Familie der ADCs mit sukzessiver Approximation mit einer Auflösung von 12 Bit und max. 700 000 Werten pro Sekunde. Da die Genauigkeit nicht so gut ist, rät Nuvoton, nur 10 Bit zu nutzen. Für die Laborversuche werden noch zwei
weitere Bits gestrichen, so dass lediglich 8 Bit für die Berechnung des Analogwertes betrachtet werden.
2.6.2
Initialisierung
Damit der ADC genutzt werden kann, müssen verschieden Einstellungen in den Registern des NUC130
gemacht werden. Die dafür notwendige Funktion ist in der Bibliothek vorhanden:
DrvADC_Init(CHANNEL_2_SELECT);
// Poti auf dem M_Dongle
Sie stellt alle notwendigen Register auf die korrekten Werte ein, als Einziges muss nur noch der verwendete Kanal angegeben werden.
2.6.3
Steuerung des ADC
Mit dem Makro kann der ADC gestartet:
M_ADC_CONVERT_START;
Das folgende Makro gibt den aktuellen Zustand des Wandlungsvorganges wieder:
u32Result = M_ADC_CONVERT_DONE;
// Lesen des ADF-Flags
Im Fall einer 0 ist die Wandlung noch nicht beendet, wird eine 1 zurück gegeben, so kann dann mit
u16Result = M_ADC_DATA_READ(CHANNEL_2);
// Poti auf dem M_Dongle
der Wert ausgelesen werden und anschließend muss mit
M_ADC_ADF_CLR;
// Löschen des ADF-Flags
die Meldung über die Beendigung der Wandlung gelöscht werden.
2.6.4
Quantisierung
Das Datenregister des ADCs gibt die Anzahl der Quantisierungsstufen an. Ein Wandler mit 12 Bit erzeugt 4096 Stufen seiner Referenzspannung. Da das Laborsystem eine Referenzspannung von 3.3 Volt
hat, ergibt sich eine Quantisierungsstufe von 3.3 Volt / 4095 = 0.806 mV. Das bedeutet, dass der Wandler Spannungsunterschiede von >0.806 mV erfassen kann. Um den absoluten Wert der Spannung zu
ermitteln, muss die Anzahl der Quantisierungsstufen mit 0.806 mV multipliziert werden. Da die Multiplikation mit Gleitkommazahlen für den µC eine große CPU-Last darstellt, kann hier mit dem Trick
u16ADCmV = u16ADCReg * 806 / 1000;
// Umwandlung in mV
mit viel weniger Rechenleistung ein Ergebnis erzielt werden.
2.6.5
Umwandlung in nutzbare Werte
Hier können nun zwei Wege beschritten werden. Der eine Weg multipliziert einfach die Quantisierungsstufe mit dem Datenregister des ADC, in unserem Fall ADC-Datenregister * 806 mV / 1000und als
Ergebnis kommt der ADC-Wert in mV heraus. Für einen Umrechnungsalgorithmus ist es unerheblich,
ob es Vielfache von Volt oder mV sind, da die ermittelten Zahlen für beide Einheiten gleich sind.
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 21 -
Der zweite Weg wurde in der Vorlesung schon behandelt und erhöht die Genauigkeit der Umrechnung
indem nicht mit mV sondern mit µV multipliziert wird.
2.6.6
Fehlerbetrachtung
Als große Fehlerquelle kommt der ADC selbst in Betracht, weshalb Nuvoton für den NUC130 angibt,
dass nur 10 Bit genutzt werden sollen. Es üblich ist, nie einem einzigen ADC-Wert zu vertrauen, sondern Mittelwerte über Vielfache von 2 zu nutzen. Der Trick mit den Vielfachen kommt daher, dass die
Division dann durch eine viel schnellere Schiebeoperation ersetzt werden kann.
2.6.7
Grundlagen zur Dezimalzahlendarstellung
Variablen oder Register-/Speicherinhalte werden entweder als dezimale oder hexadezimale Zahlen
dargestellt. Für beide Formate gilt, es müssen eigene Funktionen zur Umwandlung in darstellbare Zeichen geschrieben werden. Dafür ist es notwendig, sich vorher Gedanken über die Umwandlung in das
dezimale Zahlenformat bzw. die Darstellung der gewandelten Zahl zu machen. Als Beispiel soll ein Wert
aus dem Versuch mit dem ADC dienen:
ADC-Wert in Hex:
Nach Weg 2 ergibt sich die Dezimalzahl:
0x80
1,650
Die Stelle für das Komma ist nur aus der Aufgabe bekannt, d.h. wenn die Ausgabe in Volt erfolgen soll, muss das Komma zwischen der ersten und zweiten Stelle eingefügt werden. Soll
die Ausgabe in mV erfolgen, so wird kein Komma benötigt.
Das ADC-Register enthält ja eigentlich nur Vielfache der Quantisierungsstufe (0.806 mV), die
maximal den Wert von 3300 mV ergeben können. Für das Labor wird deshalb eine Umwandlungsfunktion benötigt, die eine Zahl mit vier Stellen berechnen kann. Der Programmierer entscheidet dann nach der Umwandlung, an welcher Stelle er das Komma setzt. Das setzt aber
voraus, dass nur die einzelnen Stellen erzeugt werden.
Der Hintergrund ist der, dass nur eine Funktion zur Umwandlung benötigt wird, die Skalierung
(das Komma) wird ja nur für die Ausgabe benötigt.
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 22 -
2.7
2.7.1
Grundlagen zum UART2
Kennzeichen
Der NUC130 verfügt über drei UARTs, die jedoch zum Teil unterschiedliche Features haben. Sie sind in
der Lage mit unterschiedlichen Datenbitlängen zu arbeiten und bieten zur Entlastung der CPU die Möglichkeit, Datenpakete mittels FIFO-Speicher senden bzw. empfangen zu können. Außerdem sind verschiede Erweiterungen für Busprotokolle eingebaut, die die Kommunikation per LIN, RS485 oder IrDA
erheblich vereinfachen.
UART0 und UART1 verfügen über viele Features, der UART2 in seinen Möglichkeiten etwas eingeschränkt ist. Diese Einschränkungen haben jedoch den Vorteil, dass seine Initialisierung nicht so komplex ist. Für alle UARTs gilt, sie werden mit dem gleichen Takt versorgt. Dieser Takt wird automatisch
beim Systemstart durch die Funktion
DrvSystem_ClkInit();
eingestellt und beträgt 22,1188 MHz. Der Vorteiler für den Takt wird nicht verwendet, was bei der Nutzung von UART 0 und 1 zu beachten ist. Da der UART2 für die PC-Kommunikation benutzt wird, ist er
auf das übliche 8N1-Protokoll eingestellt.
2.7.2
Übertragungsrate
Das Hauptproblem aller asynchronen Datenübertragungen ist die Frequenz, mit der die Daten- und
Kontrollbits gesendet werden. Das bedeutet, dass jeder Teilnehmer seinen eigenen Sende- und Empfangstakt erzeugen muss, da er im Datenframe nicht enthalten ist. Für UARTs beträgt die Abweichung
vom Takt maximal 5 %.
2.7.3
Auswahl der Baudrate
In Aufgabe 4 soll die Baudrate so bestimmt werden, dass es ohne Probleme möglich ist, vier gewandelte Werte pro Sekunde an das andere Board zu schicken. Bei dem normalen 8N1-Protokoll für die Übertragung benötigt ein Zeichen 10 Bit (Startbit, Zeichen & Stoppbit). Bei einer zu niedrigen Baudrate ist
schon wieder ein neuer ADC-Wert vorhanden, obwohl der letzte noch nicht gesendet ist. Da Zahlenumwandlung und LCD-Ausgabe auch Zeit benötigen, kann nicht davon ausgegangen werden, dass
es ausreicht, alle Zeichen innerhalb von 250 ms zu senden.
2.7.4
Initialisierung des UARTs
Für die Initialisierung einer einfachen Funktion des UART2 ist eine Funktion in der Bibliothek vorhanden:
void DrvUART2_Init(uint32_t u32Baudrate, uint8_t uiTrigLevelBytes);
Diese Funktion initialisiert den UART2 auf die gewünschte Baudrate und die Anzahl der Bytes, ab der
der Interrupt des UART2 aktiv wird, falls er benutzt wird.
Es wird mit Absicht eine Funktion benutzt, da die Initialisierung per Struktur doch sehr aufwändig und
damit fehlerträchtig sein kann. Für die Einstellung der Baudrate ist die Systemfrequenz der UARTs
maßgeblich, die je nach eingestellter Taktquelle variieren kann. Die vorgegebene Funktion beachtet alle
Parameter und stellt die gewünschte Baudrate ein:
DrvUART2_Init(38400,0);
// 38k4, 8N1, Meldung ab 1 Byte im Empfänger
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 23 -
2.7.5
Daten senden mit M_UART2_DATA_WRITE
Daten senden mit Warten bis Puffer frei:
while(UART2->FSR.TX_FULL ==TRUE)
{}
// wenn TRUE, Puffer besetzt
// warten
M_UART2_DATA_WRITE('T');
// Sendet ein T
Daten senden nur wenn Puffer frei, sonst weiter:
if(UART2->FSR.TX_FULL == FALSE)
{
M_UART2_DATA_WRITE('T');
}
2.7.6
// wenn Puffer frei
// Sendet ein T
Daten empfangen mit M_UART2_DATA_READ
Daten empfangen mit Warten bis Zeichen da:
while(UART2->FSR.RX_EMPTY ==TRUE)
{}
// wenn TRUE, keine Daten vorhanden
// warten
u8Empfang = M_UART2_DATA_READ;
//Daten aus Empfangsregister holen
Daten empfangen wenn Daten da, sonst weiter:
if(UART2->FSR.RX_EMPTY == FALSE)
{
u8Empfang = M_UART2_DATA_READ;
}
// wenn FALSE, Daten vorhanden
//Daten aus Empfangsregister holen
Die beiden Makros selbst beschreiben bzw. lesen nur das Register des UARTs, ohne auf die StatusFlags zu achten. Da dies aber zu Fehlern führen kann, sollten die Flags mit einbezogen werden. Werden jedoch zum Teil Warteschleifen verwendet (warten bis Zeichen da oder Puffer frei), so sollte ein
Timeout innerhalb der Funktion sicherstellen, dass das Programm nicht ewig wartet.
2.7.7
Array senden
Da das Senden von Arrays sehr häufig vorkommt, wurde die Funktion
uint32_t DrvUART2_Write(uint8_t* pu8Data, uint32_t u32Bytes);
angelegt. Sie sendet ein beliebiges uint8_t Array mit der angegebenen Länge. Die Längenangabe hat
den Vorteil, dass auch Arrays gesendet werden können, die keine darstellbaren Zeichen enthalten. Für
einen String muss nur seine Länge mittels Get_StringLen() ermittelt werden und schon ist die Funktion benutzbar.
2.7.7.1
Unterschied String- / uint8_t-Array
Ein String-Array hat nur als letztes Zeichen eine 0x0, ein uint8_t Zahlen-Array kann schon mit einer 0x0
beginnen, obwohl noch Zahlen != 0x0 kommen können.
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 24 -
3
3.1
Tipps und Tricks
Zählschleifen
Der erste Gedanke ist recht einfach, wir wollen eine Aktion 10 mal ausführen:
int16_t i;
for(i=0;i<= 9;i++)
{…}
Schon fangen die Probleme an, die Variable i ist vorzeichenbehaftet, also kann unter Umständen das
Zählergebnis nie erreicht werden und die Schleife zählt hoch. Außerdem wird auch noch auf <= abgefragt. Wird Problem 1 & 3 beseitig, sieht es so aus:
uint16_t ui;
for(ui=0;ui<10;ui++)
{…}
Viel hat sich nicht geändert, also warum soll man nicht hoch zählen. Wird obige Schleife übersetzt, so
kommt folgendes als Assembler heraus:
MOVS
STR
B
LDR
ADDS
UXTH
STR
LDR
ADDS
UXTH
STR
LDR
CMP
BLT
r0,#0x00
r0,[sp,#0x00]
0x000002CE
r0,[sp,#0x1C]
r0,r0,#1
r0,r0
r0,[sp,#0x1C]
r0,[sp,#0x00]
r0,r0,#1
r0,r0
r0,[sp,#0x00]
r0,[sp,#0x00]
r0,#0x0A
0x000002BE
Ohne die einzelnen Zeilen zu kommentieren, der Code ist recht lang geworden. Das hängt mit dem
Befehlssatz zusammen und der Tatsache, dass Compiler nur selten für eine Prozessorfamilie geschrieben werden.
Doch bauen wir die Schleife einfach mal um und lassen auf 0 zählen und fragen auf ungleich ab:
uint16_tui;
for(ui = 10; ui != 0; ui--)
{...}
MOVS
SUBS
UXTH
CMP
BNE
r0,#0x0A
r0,r0,#1
r0,r0
r0,#0x00
0x000002B6
Das Ergebnis überrascht schon, von 14 Befehlen auf 9 Befehle, die reine Schleife nur noch 4 Befehle.
Für einen 8051 wäre die Erklärung einfach, da er einen Befehl hat, der dekrementiert und springt, falls
das Register nicht 0 ist.
Der Compiler für ARM behandelt > 0 anders als != 0 und erzeugt damit andere Befehle.
 die Variablen als unsigned deklarieren
 Schleifen auf Null zählen lassen
Abfragen auf gleich oder ungleich, größer & kleiner vermeiden
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 25 -
Auch spielt die Reihenfolge der Anweisungen eine Rolle:
uint8_tuc;
while(uc != 0)
{
uc--;
(Anweisung)
}
Obige Struktur arbeitet einwandfrei, doch sie ergibt mehr Bytes als:
uint8_tuc;
while(uc != 0)
{
(Anweisung)
uc--;
}
 Reihenfolge beachten
3.2
Abfragen
Eine klassische if-else-Bedingung, hier innerhalb einer Impulszählung verwendet:
if(ucZaehler<= MAXWERT)
{
ucZaehler++;
Ausgabe();
}
else
{
ucZaehler = 0;
Ausgabe();
}
Schnell geschrieben, gut nachvollziehbar und es funktioniert auch, aber es erzeugt mehr Code, einfach
selber testen. Hier ist es so, dass der aktuelle Zählerstand ausgegeben wird, also wird es mit Zählen
auf Null etwas schwierig. Dennoch lässt sich der Code reduzieren:
ucZaehler++;
if(ucZaehler == (MAXWERT + 1))
{
ucZaehler = 0;
}
Ausgabe();
Wird die Erhöhung vorgezogen und die Abfrage auf die Resetbedingung umgestellt, so entfällt der elseZweig komplett. Die Ausgabe nach der Abfrage zu machen, spart Code für einen Funktionsaufruf. Das
MAXWERT + 1 ist wegen der Ausgabe nötig.
Wenn der Zähler nicht ausgegeben werden würde, könnte die if-Bedingung wie folgt lauten:
ucZaehler--;
if(ucZaehler != 0)
{
ucZaehler = MAXWERT;
...
}
 Immer prüfen, ob der else-Zweig vermeidbar ist
if-elseif-else sollten vermieden werden, indem sie in if und if-else geändert werden (im Assembler
kontrollieren, was besser ist)
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 26 -
3.2.1
Mischen von Abfragen
Werden Bits (Ports) mit Variablen bitweise verknüpft, so wird der Code länger als bei reinen BitVerknüpfungen, da Null oder Eins der Variable erst ermittelt werden muss:
if((bT1 == 0) && (ucSemaphor == 1))
{
LCD_OUT("Ende");
}
damit es kurz und schnell wird, immer nur gleiche Typen verknüpfen
Variablen
3.3
Hier kann viel gespart oder auch verschwendet werden.
 wo es geht, nur positiv ganzzahlige Variablen verwenden
Strukturen verwenden
3.3.1
Integerproblem
Bei der Erzeugung von plattformunabhängigem Code taucht immer ein Problem auf: Die Länge von
Integervariablen. Um dieses Problem zu umgehen, wird auf die Methode gesetzt, im Sourcecode anstelle normaler Variablentypen spezielle Definitionen zu verwenden.
Kleines Beispiel:
Für einen 8051 hat eine shortint die gleiche Länge wie eine int Variable, nämlich16 Bit. In der Welt der
Cortex µCs ist der int aber doppelt so groß. Wird nun ein Cortex Modul auf einem 8051 genutzt, können
Probleme auftauchen. Müssen in einem Modul 100 KB Daten per Schnittstelle ausgegeben werden, ist
es auf einem µC mit der Integer Größe 32 Bit kein Problem. Wird dieses Modul mit auf einem 8051 genutzt, können nur 64 KB Daten ausgegeben werden, da der Integer dort nur 16 Bit hat.
3.3.2
Lösung
Anstatt die Variablen wie folgt zu deklarieren:
unsigned int uiZeilenLaenge;
unsigned char ucBuchstabe;
// Anzahl Buchstaben pro Zeile
// Charakter
wird jetzt die Notation
uint32_t
uint8_t
ZeilenLaenge;
Buchstabe;
// Anzahl Buchstaben pro Zeile
// Charakter
verwendet.
In der stdint.h ist folgende Definition zu finden:
typedef unsigned int
typedef unsigned char
uint32_t;
uint8_t;
Damit kann im eigenen Sourcecode eine konkrete Länge angegeben werden, die für jede Plattform auf
den korrekten Typ umgesetzt werden kann, da der Compiler die Typdefinitionen aus der stdint.h verwendet.
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 27 -
3.3.3
Strukturen
Aktuelle Mikrocontroller kommen wegen ihrer Komplexität bei der Programmierung nicht ohne Strukturen aus.
typedef struct
{
__I uint32_t XTL12M_STB:1;
__I uint32_t XTL32K_STB:1;
__I uint32_t PLL_STB:1;
__I uint32_t OSC10K_STB:1;
__I uint32_t OSC22M_STB:1;
__I uint32_t RESERVE0:2;
__IO uint32_t CLK_SW_FAIL:1;
__I uint32_t RESERVE1:24;
} SYSCLK_CLKSTATUS_T;
Das Beispiel stellt das Clockstatus-Register des NUC130 dar. Der Programmierer hat die Möglichkeit,
aus einer von fünf Taktquellen den Mikrocontroller zu betreiben. Damit ist ein Taktspektrum von 10 KHz
bis 48 MHz möglich. Das Register liefert nun die Information, welche Quelle aktuell vorhanden ist, bzw
ob ein Wechsel von einer Quelle zur anderen funktioniert hat. Prinzipiell lassen sich die Informationen
auch mittels Maskierung aus dem 32-Bit Wert des Registers ermitteln, doch sie benötigt mehr Code und
Dokumentation, denn
if(SYSCLK->CLKSTATUS.XTL12M_STB == STABLE)
ist selbsterklärend.
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 28 -
4
Vertiefende Peripherie Programmierung
SysTick
4.1
Der SysTick kann in größeren Anwendungen als „Verwalter“ für Aufgaben verwendet werden. Er kontrolliert dann alle Aufgaben, die zyklisch durchgeführt werden sollen. Da die ISR des SysTick eine sehr
hohe Priorität hat, ist auch gewährleistet, dass die Aktionen (Programmcode) ausgeführt werden, selbst
wenn die Main-Loop Probleme hat (Hauptprogramm „hängt“).
Soll ein anderer Programmteil die Bearbeitung einer Aufgabe übernehmen, so muss dieser mit der ISR
kommunizieren können. Dies geschieht mit Hilfe einer globalen Variablen, damit beide Programmteile
auf sie zugreifen können. Die ISR setzt die Anforderung, der bearbeitende Programmteil löscht die Anforderung. Soll die Signalisierung durch den SysTick schaltbar sein, so wird dies mit Hilfe einer zweiten,
globalen Variablen realisiert.
void SysTick_Handler (void)
{
if(gu8ToggleOn == 1)
gu8LEDToggle = 1;
}
// SysTick Interrupt Handler
// Ist Freigabe da?
// Anforderung setzen
void main (void)
{
if(gu8LEDToggle == 1)
// Anforderung da ?
{
if(M_GPIO_BIT_GET(BIT_LED0) == 1)
M_GPIO_BIT_CLEAR(BIT_LED0);
else
M_GPIO_BIT_SET(BIT_LED0);
gu8LEDToggle = 0;
// Anforderung löschen
}
if(M_GPIO_BIT_GET(BIT_TASTER_3) == 0)
gu8ToggleOn = 1;
// Freigabe erteilen
if(M_GPIO_BIT_GET(BIT_TASTER_2) == 0)
gu8ToggleOn = 0;
// Freigabe sperren
}
4.1.1
Beispiel SysTick als Manager
Folgendes Beispiel soll den SysTick als multifunktionalen Manager für unterschiedliche Anforderungen
innerhalb eines kleinen Systems zeigen.
Seine Aufgaben:
•
•
•
Joystick-Einlesen mit 25 ms Wartezeit
Zeitgeber für AD-Wandlung, alle 250 ms eine Wandlung
Blinken einer LED als Alarmmeldung mit 2 Hz
Es wird eine Zeit von 25 ms als Wert für die Initialisierung des SysTicks gewählt, damit wird alle 300000
Taktzyklen die ISR ausgelöst. Für die obigen Aufgaben ist dies ausreichend.
Für die Kommunikation werden folgende Variablen benötigt:
gu8ADRun
gu8ErrorLed
gu8JoyStickFlag
// Signal vom Main-Loop, ADC soll Wandlung starten
// Signal für Fehlerblinken
// Signal für Joystick einlesen
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 29 -
Die ISR sieht nun wie folgt aus:
void SysTick_Handler (void)
{
static uint8_t u8ISRCount = 10;
u8ISRCount--;
// SysTick Interrupt Handler
// Wert für 250 ms
// 25 ms vorbei
// Codeabschnitt für Joystick einlesen
gu8JoyStickFlag = 1;
// Force read joystick
// Codeabschnitt für ADC und Fehler LED
if(u8ISRCount == 0)
// 250 ms erreicht ?
{
u8ISRCount = 10;
// Wert für weitere 250 ms laden
if(gu8ADRun == 1)
M_ADC_CONVERT_START;
// Soll ADC wandeln ? (Info von Main)
// Wandlung starten
if(gu8ErrorLed == 1)
// Fehlerblinken angefordert ?(s.o.)
M_GPIO_BIT_WRITE(ERROR_LED) = ~(M_GPIO_BIT_GET(ERROR_LED)); // blinken
}
}
void main (void)
{
if(gu8JoyStickFlag == 1)
// Joystick einlesen gefordert?
{
u8Joystick = ~(GPIOE->PIN_BYTE0); // Einlesen des ganzen Joystick Ports
if(u8Joystick != u8JoystickAlt) // Veränderung ?
{
u8JoystickAlt = u8Joystick; // Speichern des aktuellen Wertes
switch(u8Joystick)
{
case ??: ……
}
gu8JoyStickFlag = 0;
// Anforderung löschen
}
}
}
Der Manager verfolgt ein einfaches Ziel, er schaut alle 25 ms nach, ob er etwas zu tun hat.
Als erstes fordert er eine Abfrage des Joysticks an.
Im Falle der AD-Wandlung muss er bis 10 zählen, um die Wandlung anzustoßen.
Wann das Ergebnis des ADC vorliegt, ist für ihn unwichtig, er soll nur die Wandlung anstoßen.
Da dieses Zeitintervall auch noch für die Fehler-LED gilt, muss er nur noch nachschauen, ob das Blinken gewünscht ist und komplementiert die LED innerhalb der Routine. Dies ist sinnvoll, da es ja sein
kann, dass die Main-Schleife nicht mehr korrekt funktioniert.
Der Manager kann weitere Aufgaben erledigen. Es ist nur darauf zu achten, dass seine maximale „Arbeitszeit“, die Zeit also, die die ISR benötigt, in einem vernünftigen Verhältnis zum restlichen Programm
liegt. Er soll auf keinen Fall größere Aufgaben wie LCD-Ausgabe oder Kommunikation per Schnittstellen
innerhalb der ISR ausführen, da diese Aufgaben das restliche Programm ausbremsen. Er stößt etwas
an (Joystick, ADC) und führt sicherheitskritische Maßnahmen aus (Fehler-Blinken).
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 30 -
ADC
4.2
In komplexeren Programmen wird der ADC meist im Interruptbetrieb genutzt, d.h. er meldet sich, wenn
ein neuer Wert da ist. In der ISR kann nun noch eine Vorverarbeitung des Wertes gemacht werden, um
größere Genauigkeit und damit weniger Rauschen zu erhalten.
4.2.1
Interruptnutzung
Der Interrupt kann im NVIC mit
NVIC_EnableIRQ(ADC_IRQn);
NVIC_DisableIRQ(ADC_IRQn);
// Interrupt ein
// Interrupt aus
ein- bzw ausgeschaltet werden.
Nun muss noch die Quelle in der Peripherie eingestellt werden, die einen Interrupt auslösen soll:
ADC->ADCR.ADIE = 1;
4.2.2
// Quelle einschalten
ISR für ADC
void ADC_IRQHandler(void)
{
}
Diese ISR muss eingefügt und mit Programmcode gefüllt werden.
4.2.3
Sinnvolles für die ISR
4.2.3.1 Mittelung der Werte
Da man sich ja nicht auf einen ADC-Wert verlassen soll, ist ein Mittelwert über 2 Werte schon sehr nützlich. Es gibt nun mehrere Wege, diesen Mittelwert zu erhalten.
Weg 1:
Wenn alle Zeitabschnitte x ein ADC-Wert erzeugt werden soll, so muss für den Mittelwert über zwei
Werte die Abtastzeit des ADC halbiert werden, damit zum Zeitpunkt x der Mittelwert gültig ist. Die Verdoppelung der Abtastrate führt auch zur doppelten Anzahl von Interrupts. Beides kann zu Problemen
führen.
Weg 2:
Es wird ein gleitender Mittelwert benutzt, der sich immer aus dem letzten Mittelwert und dem aktuellen
Wert ergibt. Einziges Problem ist hierbei der Startwert des Mittelwertes.
Beispiel für eine ISR nach Weg 2
void ADC_IRQHandler(void)
{
static uint16_t u16Mittelwert = 0x7FF;
gu16ADC = M_ADC_DATA_READ(u8Channel);
gu16ADC = (gu16ADC + u16Mittelwert) / 2;
u16Mittelwert = gu16ADC;
// Startwert halber ADC-Wert (12 Bit)
// ADC Wert einlesen (12 Bit)
// Mittelwert erzeugen
// neuen Mittelwert sichern
}
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 31 -
4.2.3.2 Ringspeicher
Mittelwert über x Werte. Dieser Weg nutzt einen Ringspeicher für die Werte, der vom ADC gefüllt wird.
Ist nun die Anzahl x erreicht, wird ein Mittelwert über alle Werte berechnet. Das Problem hierbei ist die
benötigte Rechenzeit und die Tatsache, dass bei großem x die Signale sehr geglättet werden, was nicht
immer sinnvoll und gewünscht ist.
Es ist auch möglich, die Berechnung in die Hauptschleife zu verlagern.
Die globale Variable gu16ADC enthält außerhalb der ISR immer den aktuell gültigen Mittelwert.
Als Ringspeicher wird ein globales Array verwendet.
void ADC_IRQHandler(void)
{
static uint8_t u8RZW = 0;
// Schreib-Zeiger auf Elemente im Ringpuffer
uint8_t u8RZW; // Lese-Zeiger auf Elemente im Ringpuffer
gu16ADC_A[u8RZW] = M_ADC_DATA_READ(u8Channel);
// ADC Wert einlesen (12 Bit)
u8RZW++;
if(u8RZW == MAX_WERTE_ADC)
// maximale Anzahl für Mittelwert erreicht ?
{
gu16ADC = 0;
for(u8RZR = 0; u8RZR<MAX_WERTE_ADC; u8RZR ++)
{
gu16ADC += gu16ADC_A[u8RZR];
}
gu16ADC /= MAX_WERTE_ADC;
u8RZW = 0;
// Mittelwert erzeugen
// Schreibzeiger auf Startwert
}
}
Soll ein gleitender Mittelwert berechnet werden, wird die Berechnung des Mittelwertes in jedem Interrupt
durchgeführt:
void ADC_IRQHandler(void)
{
static uint8_t u8RZW = 0;
uint8_t u8RZW;
// Schreib-Zeiger auf Elemente im Ringpuffer
// Lese-Zeiger auf Elemente im Ringpuffer
gu16ADC_A[u8RZW] = M_ADC_DATA_READ(u8Channel);
// ADC Wert einlesen (12 Bit)
u8RZW++;
if(u8RZW == MAX_WERTE_ADC)
{
u8RZW = 0;
}
// maximale Anzahl für Mittelwert erreicht?
// Schreibzeiger auf Startwert
gu16ADC = 0;
for(u8RZR = 0; u8RZR <MAX_WERTE_ADC; u8RZR ++)
{
gu16ADC += gu16ADC_A[u8RZR];
}
gu16ADC /= MAX_WERTE_ADC;
// Mittelwert erzeugen
}
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 32 -
4.3
4.3.1
UART2
Grundlagen
Der UART (Universal Asynchronous Receiver Transmitter) ist eine Peripherieeinheit, die für das Senden und Empfangen von Daten zuständig ist. Eine Hauptaufgabe besteht darin, die Sendedaten in einen seriellen Bitstrom und die seriellen Datenstrom für den Empfang in parallele Daten zu wandeln. Da
es sich um eine asynchrone Schnittstelle handelt, wird kein Taktsignal mit übertragen. Der serielle Datenstrom besteht aus einem festen Rahmen, der neben den Datenbits, noch ein Startbit, ein optionales
Paritätsbit zur Übertragungsfehlererkennung und zwischen ein und zwei Stoppbit enthält.
4.3.2
Elektrisches Interface
Es gibt mehrere standardisierte Interface, die an einem UART betrieben werden können.
Übertragungsart
Spannung max.
Max. Leitungslänge
Max. Übertragungsrate
Steuersignale UART
Busabschluss
4.3.3
RS232
Gnd bezogen
+- 15 V
900 m
115200 (PC)
Ja
Nein
RS485
Differentiell
RS422
Differentiell
1200 m
12 MBaud
Ja
Ja
1200 m
10 MBaud
Ja
Ja
LIN
Gnd bezogen
+ 18 V
40m
20 KBaud
Nein
Nein
Interruptnutzung
In komplexeren Programmen werden die UARTs im Interruptbetrieb genutzt, d.h. sie melden sich, wenn
neue Daten da sind. In der ISR kann nun noch eine Vorverarbeitung der Daten gemacht werden. Der
NUC130 hat die Besonderheit, dass sich der UART2 seinen Interrupt mit dem UART0 teilt.
DerUART2-Interrupt kann im NVIC mit
NVIC_EnableIRQ(UART0_IRQn);
NVIC_DisableIRQ(UART0_IRQn);
// Interrupt ein
// Interrupt aus
ein- bzw ausgeschaltet werden.
Nun muss noch die Quelle in der Peripherie eingestellt werden, die einen Interrupt auslösen soll. Diese
Quellen sind in folgender Struktur zu finden:
union
{
__IO uint32_t u32ISR;
struct
{
__IO uint32_t RDA_IF:1;
__IO uint32_t THRE_IF:1;
__IO uint32_t RLS_IF:1;
__IO uint32_t MODEM_IF:1;
__IO uint32_t TOUT_IF:1;
__IO uint32_t BUF_ERR_IF:1;
__I uint32_t RESERVE0:1;
__IO uint32_t LIN_RX_BREAK_IF:1;
__IO uint32_t RDA_INT:1;
__IO uint32_t THRE_INT:1;
__IO uint32_t RLS_INT:1;
__IO uint32_t MODEM_INT:1;
__IO uint32_t TOUT_INT:1;
__IO uint32_t BUF_ERR_INT:1;
__I uint32_t RESERVE1:1;
__IO uint32_t LIN_RX_BREAK_INT:1;
__I uint32_t RESERVE2:2;
__IO uint32_t HW_RLS_IF:1;
// Empfangsdaten da
// Sendepuffer leer
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 33 -
__IO
__IO
__IO
__I
__IO
__I
__IO
__IO
__IO
__IO
__I
__IO
} ISR;
uint32_t
uint32_t
uint32_t
uint32_t
uint32_t
uint32_t
uint32_t
uint32_t
uint32_t
uint32_t
uint32_t
uint32_t
HW_MODEM_IF:1;
HW_TOUT_IF:1;
HW_BUF_ERR_IF:1;
RESERVE3:1;
HW_LIN_RX_BREAK_IF:1;
RESERVE4:2;
HW_RLS_INT:1;
HW_MODEM_INT:1;
HW_TOUT_INT:1;
HW_BUF_ERR_INT:1;
RESERVE5:1;
HW_LIN_RX_BREAK_INT:1;
};
Weitere Information sind in der Tabelle des Interrupt Status Control Register (UA_ISR) im Usermanual
zu finden (UM_NUC130.pdf).
Damit der Interrupt auslöst, wenn sich Daten im Empfangspuffer befinden, wird die Quelle mittels
UART2->IER.RDA_IEN = 1;
freigeschaltet.
Damit sind die nötigen Einstellungen abgeschlossen.
4.3.4
ISR für den UART2
void UART02_IRQHandler(void)
{
}
Diese ISR muss eingefügt und mit Programmcode gefüllt werden.
Wie schon am Namen zu sehen ist, werden mit einer ISR zwei UARTs bedient, weshalb die ISR etwas
komplexer ist, da eine Unterscheidung zu treffen ist, welcher der beiden UARTs die ISR ausgelöst hat.
4.3.5
Sinnvolles für die ISR
4.3.5.1 Sammeln von Empfangsdaten und automatisches Senden
Es ist nicht sinnvoll, wegen jedem empfangenen Byte die Main-Loop zu informieren. Folgende ISR stellt
ein Datenpaket zusammen und informiert erst am Ende das Hauptprogramm. Für das Abschicken von
Strings gilt ähnliches, weshalb die ISR das übernimmt, nachdem das Senden aktiviert wurde.
void UART02_IRQHandler(void)
{
static uint8_t u8RXCounter = 0;
static uint8_t u8TXCounter = 0;
if(UART2->ISR.RDA_IF == 1)
// Quelle Empfangen UART1?
{
gu8RXdata[u8RXCounter] = M_UART2_DATA_READ;
u8RXCounter++;
if(u8RXCounter == PAKETLEN)
{
u8RXCounter = 0;
gu8UART_Ready = 1;
}
// Anzahl erreicht?
// Flag für Main setzen
}
if(UART2->ISR.THRE_IF == 1)
{
if(gpu8TXdata[u8TXCounter] != 0)
{
UART2->DATA = (uint32_t)(gpu8TXdata[u8TXCounter]);
u8TXCounter ++;
}
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 34 -
else
{
M_UART2_TX_INT_DISABLE;
u8TXCounter = 0;
}
}
}
Damit die ISR funktioniert, sind folgende Zeilen vor der Hauptschleife nötig:
uint8_t gu8RXdata[9];
uint8_t* gpu8TXdata;
uint8_t gu8UART_Ready = 0;
// globales Empfangsarray
// globaler Zeiger für Sendedaten
// globales Flag für HP, neue Daten da
M_UART2_RX_INT_ENABLE;
NVIC_EnableIRQ(UART0_IRQn);
// UART2 RX freischalten
// UART2 im NVIC freischalten
void UART2_SendString(uint8_t Array[])
{
gpu8TXdata = Array;
M_UART2_TX_INT_ENABLE;
}
// Funktion bekommt String als Para
__________________________________________________________________________________________
C-Programmierung Rev. 2.1 Juni 2014
- 35 -