42
C-Programmierung mit dem M_Dongle Andreas Reber Rev. 5.0 22.01.2020

C-Programmierung mit dem M Dongle

  • Upload
    others

  • View
    8

  • Download
    0

Embed Size (px)

Citation preview

Page 1: C-Programmierung mit dem M Dongle

C-Programmierung mit dem M_Dongle

Andreas Reber Rev. 5.0 22.01.2020

Page 2: C-Programmierung mit dem M Dongle
Page 3: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 3 -

Inhalt

Vorwort ..................................................................................................................................... 5

1.1 Das Problem mit den Klammern ............................................................................... 5

2 Grundlagen der C-Programmierung für einen Cortex ........................................................ 6

2.1 Hardwareunabhängigkeit und CMSIS ....................................................................... 6

2.2 Nuvoton Bibliothek versus M_Dongle Bibliothek ....................................................... 6

2.3 Nomenklatur M_Dongle Lib ....................................................................................... 6

2.3.1 Makros .............................................................................................................................. 6 2.3.2 Funktionen ........................................................................................................................ 7 2.3.3 Module .............................................................................................................................. 7

2.4 Dokumentation .......................................................................................................... 7

2.4.1 Grundstruktur für das Hauptfile ........................................................................................ 7 2.4.2 Dokumentationskopf für ein Modul (h-File) ...................................................................... 8 2.4.3 Dokumentationskopf für ein Modul (C-File) ...................................................................... 8 2.4.4 Dokumentationskopf einer Funktion ................................................................................. 8

2.5 Basis-File für Projekte ............................................................................................... 9

2.5.1 Quellcode Vorlage für alle Projekte .................................................................................. 9

2.6 Modularität ................................................................................................................ 9

2.6.1 Beispiel ........................................................................................................................... 10

2.7 Probleme beim Modularisieren ................................................................................ 11

2.7.1 Parameterübergabe an die Funktion .............................................................................. 11

3 Grundlagen ..................................................................................................................... 12

3.1 Grundlagen Pin/Portprogrammierung ...................................................................... 12

3.2 Pin/Portprogrammierung ......................................................................................... 19

3.2.1 Zuordnung Pinnamen-Definitionen-Portstrukturelemente .............................................. 19 3.2.2 Initialisierung per CMSIS ................................................................................................ 20 3.2.3 Initialisierung per M_Dongle Bibliothek .......................................................................... 20 3.2.4 Port-Pin Funktionen zur Bitmanipulation ........................................................................ 20 3.2.5 Tasterabfrage ................................................................................................................. 21 3.2.6 Programmablaufsteuerung mit Tasten ........................................................................... 21 3.2.7 Unterscheidung Taster noch aktiv oder erneuter Tastendruck ...................................... 22 3.2.8 Taster schaltet ein und aus ............................................................................................ 22

3.3 Grundlagen zu LCDs .............................................................................................. 23

3.3.1 Punktmatrix LCDs........................................................................................................... 23 3.3.2 Grafik LCDs .................................................................................................................... 23 3.3.3 Zeichendarstellung ......................................................................................................... 23 3.3.4 Zeichenerzeugung .......................................................................................................... 23 3.3.5 Zeichenausgabe ............................................................................................................. 24 3.3.6 COG_LCD Bibliothek (Modul) ........................................................................................ 25 3.3.7 Zahlen und Zeichen ........................................................................................................ 26

3.4 Grundlagen zum SysTick-Timer .............................................................................. 27

3.4.1 Initialisierung ................................................................................................................... 27 3.4.2 SysTick starten ............................................................................................................... 27 3.4.3 SysTick mit Interrupt ....................................................................................................... 27 3.4.4 Ein Nutzen des SysTicks ................................................................................................ 27

Page 4: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 4 -

3.5 Grundlagen zu Interrupts ........................................................................................ 28

3.5.2 „Regeln“ für ISRs: ........................................................................................................... 29

3.6 Grundlagen zum ADC ............................................................................................. 30

3.6.1 Typ .................................................................................................................................. 30 3.6.2 Initialisierung für einen Kanal ......................................................................................... 30 3.6.3 Initialisierung für mehrere Kanäle, der Single Cycle Mode des ADC ............................. 30 3.6.4 Steuerung des ADC........................................................................................................ 30 3.6.5 Quantisierung ................................................................................................................. 31 3.6.6 Umwandlung in nutzbare Werte ..................................................................................... 31 3.6.7 Fehlerbetrachtung .......................................................................................................... 31 3.6.8 Grundlagen zur Dezimalzahlendarstellung .................................................................... 31

3.7 Grundlagen zum UART2 ......................................................................................... 32

3.7.1 Kennzeichen ................................................................................................................... 32 3.7.2 Übertragungsrate............................................................................................................ 32 3.7.3 Auswahl der Baudrate .................................................................................................... 32 3.7.4 Initialisierung des UARTs ............................................................................................... 32 3.7.5 Daten senden mit M_UART2_DATA_WRITE und TX_FULL ........................................ 33 3.7.6 Daten senden mit M_UART2_DATA_WRITE und TX_EMPTY ..................................... 33 3.7.7 Daten empfangen mit M_UART2_DATA_READ ........................................................... 33

4 Vertiefende Peripherie Programmierung ......................................................................... 34

4.1 SysTick ................................................................................................................... 34

4.1.1 Beispiel SysTick als Manager ........................................................................................ 34

4.2 ADC ........................................................................................................................ 36

4.2.1 Interruptnutzung ............................................................................................................. 36 4.2.2 ISR für ADC .................................................................................................................... 36 4.2.3 Sinnvolles für die ISR ..................................................................................................... 36

4.3 UART ...................................................................................................................... 38

4.3.1 Grundlagen ..................................................................................................................... 38 4.3.2 Elektrisches Interface ..................................................................................................... 38 4.3.3 Interruptnutzung ............................................................................................................. 38 4.3.4 ISR für den UART2......................................................................................................... 38 4.3.5 Sinnvolles für die ISR ..................................................................................................... 39

4.4 Timer ...................................................................................................................... 40

4.4.1 Gated Timer-Funktion (External Capture Mode) ............................................................ 40 4.4.2 Bekannte Lösungsmöglichkeiten .................................................................................... 40

5 Tipps und Tricks ............................................................................................................. 43

5.1 Zählschleifen ........................................................................................................... 43

5.2 Abfragen ................................................................................................................. 44

5.2.1 Mischen von Abfragen .................................................................................................... 45

5.3 Variablen................................................................................................................. 45

5.3.1 Integerproblem ............................................................................................................... 45 5.3.2 Lösung ............................................................................................................................ 45 5.3.3 Strukturen ....................................................................................................................... 46 5.3.4 Funktion hat eigene Strukturen ...................................................................................... 47

5.4 Statische Variablen und weitere Zeitverschwendungen .......................................... 48

Page 5: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 5 -

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ösen-den Aufgaben. Kapitel 4 und 5 ist für die jeweiligen Laborzusatzaufgaben bzw. Projekte gedacht und soll die Möglichkei-ten des M0 klarer machen.

1.1 Das Problem mit den Klammern Da die µVision keine Unterstützung bei der korrekten Anordnung der geschweiften Klammern bietet (wie von der Microsoft IDE gewohnt), kommt es leider zu vielen Problemen. Diese Probleme kommen daher, dass beim Codeschreiben die Klammersetzung nicht beachtet wird. Um dieses Problem zu umgehen, kann in die µVision das kleine Hilfstool AStyle eingebunden werden, welches die Klammern und Abstände korrekt setzt. Dazu wird in den Ordner Keil einfach der Ordner AStyle kopiert. Nun muss nur noch unter „Tools“ der Menüpunkt „Customize Tools Menu“ gewählt und das Fenster mit den Daten komplettiert werden:

New drücken und AStyle eintragen Den korrekten Pfad einstellen --style=allman „!E“ –indent-switches / -S eintragen (beide Hochkomma oben)

Nach dem Druck auf „OK“ ist das Tool jetzt eingetragen und kann unter dem Menüpunkt „Tools“ ausge-wählt werden. Es wird das formatiert, was gefunden wird. Wer die Klammern falsch setzt, bekommt keine Feh-lermeldung und kein korrektes Ergebnis.

Page 6: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 6 -

2 Grundlagen der C-Programmierung für einen Cortex Als die Cortex-Familie 2004 auf den Markt gekommen ist, war nicht nur der Prozessorkern eine Neuerung, sondern auch das von ARM angedachte Software-Konzept, CMSIS genannt.

2.1 Hardwareunabhängigkeit und CMSIS Sie sollen von Anfang an lernen, portablen, also wieder verwendbaren Code zu schreiben. Diese The-matik wurde von ARM durch die Einführung des Software-Konzeptes CMSIS angegangen. Das Schlag-wort 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 Mikrocon-troller 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-Ele-menten erleichtern.

2.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, da ihre Ursprünge bei ARM7 und ARM9 Kernen liegen. 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.

2.3 Nomenklatur M_Dongle Lib Eine Nomenklatur ist wichtig, damit sich jeder Programmierer auch mit fremden Funktionen zurecht findet. Es gibt im Grunde nur drei „Regeln“ für die Namensgebung, die im Folgenden erklärt werden.

2.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_“ gekenn-zeichnet und werden wie folgt angelegt: #define M_Peripheriegruppe_Aktion

z.B. #define M_ADC_CONVERT_START ADC->ADCR |= (ADC_ADCR_ADST_Msk)

#define M_GPIO_BIT_GET(port) *((volatile unsigned int *)(port))

Es ist üblich, Makros ähnlich wie Defines in Großbuchstaben zu schreiben.

Page 7: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 7 -

2.3.2 Funktionen Die Funktionen wurden von den Namen her an die Namensgebung von Nuvoton angelehnt, d.h. es wird die „Peripheriegruppe“ genannt und dann wieder die „Aktion“:

GPIO_PortOpen(…);

In der Library für das M_Dongle, die im Ordner _Driver liegt, sind Funktionen für die unterschiedlichsten Peripheriegruppen enthalten.

2.3.3 Module Im Ordner _Module werden Funktionen in Dateien zusammengefasst, 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 angege-ben. 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 Datei „Driver_M_Dongle.c“ angepasst werden müssen.

2.4 Dokumentation Software ohne Dokumentation ist nutzlos, weil sie weder lesbar noch wartbar ist. Ein Anfang ist die Ver-wendung von Dokumentationsköpfen für Files und Funktionen. Werden diese dann noch in einem be-stimmten Stil gemacht, kann dann automatisch mit Hilfe von Tool eine Übersichts- und Abhängigkeitsdo-kumenation von Files und Funktionen erzeugt werden. Aus diesem Grund sind alle Files des Labors im Doxygen Stil dokumentiert.

2.4.1 Grundstruktur für das Hauptfile Jedes File bekommt grundsätzlich einen Dokumentationskopf, der je nach File größer oder kleiner aus-fallen 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 "BoardConfig.h"

#include "..\_Module\GLCD.h"

#include "init.h"

2.4.1.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.

Page 8: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 8 -

2.4.2 Dokumentationskopf für ein Modul (h-File) /**--------------------------------------------------------------------------------------------

@file GLCD.h

@version 3.0

@cpu NUC130VE

@brief Headerfile for GLCD Lib

@date 14.3.2013

@author Andreas Reber

@history

---------------------------------------------------------------------------------------------*/

#ifndef glcd_h // Mehrfacheinbindungen verhindern

#define glcd_h

#include"BoardConfig.h"

void GLCD_Init(void); // Funktionsprototyp

void GLCD_SetRow(uint8_t ui8Page); // Funktionsprototyp

void GLCD_SetColumn(uint8_t ui8Column); // Funktionsprototyp

void GLCD_PrintChar(uint8_t ui8Char); // Funktionsprototyp

#endif

2.4.3 Dokumentationskopf für ein Modul (C-File)

/**--------------------------------------------------------------------------------------------

@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" // Hardwareabhängigkeiten

@brief Name für das C-Modul @details Beschreibung @global Kürzel für die extern sichtbaren Funktionen @cpu verwendete CPU @date Erstellungsdatum @author Wer es geschrieben hat @history Wer wann was geändert hat

2.4.4 Dokumentationskopf einer 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 Name der Funktion @details Beschreibung @param[in] Variablen mit Typ, die die Funktion bekommt @param[out] Rückgabeparameter @remarks Bemerkungen

Page 9: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 9 -

2.5 Basis-File für Projekte Bei einem modernen Mikrocontroller müssen zu Beginn einige Aufgaben erledigt werden, damit er über-haupt mit dem eigentlichen Programm starten kann:

2.5.1 Quellcode Vorlage für alle Projekte /**--------------------------------------------------------------------------------------------

@brief Projektx.c

@details Vorlage

@cpu NUC130VE, 12 MHz, ca. 84 ns per CPU cycle

@date

@author

@history

--------------------------------------------------------------------------------------------**/

#include "BoardConfig.h"

#include "init.h"

int main(void)

{

// Platz für Variablendeklaration

DrvSystem_ClkInit(); // Setup Clksystem

Init_Board(); // Setup M_Dongle

// Platz für Initialisierungen (ADC, UART, SysTick, Timer)

// Platz für Startansicht des LCDs

// Platz für Freischaltungen von SysTick, Timern und Interrupts

while(1)

{

}

}

2.6 Modularität 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 wer-den. ➔ 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.

Page 10: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 10 -

2.6.1 Beispiel

GLCD.h

/**--------------------------------------------------------------------------------------------

@file GLCD.h

@version 3.0

@cpu NUC130VE

@brief Headerfile for GLCD Lib

@date 17.06.2016

@author Andreas Reber

@history

---------------------------------------------------------------------------------------------*/

#ifndef glcd_h // Mehrfacheinbindungen verhindern

#define glcd_h

#include"BoardConfig.h"

void GLCD_Init(void); // Funktionsprototyp

void GLCD_SetRow(uint8_t ui8Page); // Funktionsprototyp

void GLCD_SetColumn(uint8_t ui8Column); // Funktionsprototyp

void GLCD_PrintChar(uint8_t ui8Char); // Funktionsprototyp

#endif

GLCD.c

#include "GLCD.h" // Modulheader einbinden

#include "GLCD_Font_7x5.h" // zusätzliche Header einbinden

/**--------------------------------------------------------------------------------------------

@function GLCD_SetRow

@descript 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

--------------------------------------------------------------------------------------------**/

void GLCD_SetRow(uint8_t ui8Row) // Wie FKT Prototyp

{

M_GLCD_SET_COMMAND; // command mode

M_GLCD_CS_LOW; // start SPI cycle

GLCD_Write_Byte((ui8Data & 0x0F) + 0xB0);

END SPI_Cycle();

}

Projekt.c /**--------------------------------------------------------------------------------------------

@brief Projekt.c

@details Vorlage

@cpu NUC130VE, 12 MHz, ca. 84 ns per CPU cycle

@date

@author

@history

--------------------------------------------------------------------------------------------**/

#include "BoardConfig.h"

#include "init.h"

#include "..\_Module\GLCD.h" // Modul einbinden

int main() {

System_ClkInit(); // Setup clk system

Board_Init(); // Init Peripherie

GLCD_Init(); // LCD Init mit Funktion aus GLCD

GLCD_SetRow(Zeile4); // Zeile setzen mit Funktion aus GLCD

GLCD_SetColumn(Spalte2); // Spalte setzen mit Funktion aus GLCD

GLCD_PrintChar(0x30); // ‚0‘ ausgeben mit Funktion aus GLCD

while(1)

{} }

Page 11: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 11 -

2.7 Probleme beim Modularisieren Das Verschieben von Funktionen in Module, also in eine eigene Datei, macht nur Sinn, wenn sie auch unabhängig programmiert wurden.

2.7.1 Parameterübergabe an die Funktion Damit dies funktioniert, müssen Parameter (Variablen, Konstanten, Arrays usw.) an die Funktion überge-ben werden.

2.7.1.1 Fehlerhaftes Beispiel

void Umwandlung_mV(void)

{

uint8_t i = 0;

uint16_t x = (Hexzahl * 806) / 1000;

for(i=0; i<=4; i=i+1)

{

Milivolt[i] = x / 1000 + '0';

x = (x % 1000) * 10;

}

}

Fehler:

• Aus dem Namen der Funktion ist nicht erkennbar, wie viele Stellen es sind

• Es werden keine Übergabeparameter verwendet, sondern globale Variablen

• Die Umwandlungs-Parameter gelten nur für 12 Bit und 3.3V Referenzspannung Damit ist diese Funktion nicht weiter verwendbar.

2.7.1.2 Korrigiertes Beispiel

void Umwandlung_u16to4BCD(uint16_t u16Value, UCHAR au8BCD[])

{

uint8_t i = 0;

uint16_t x = u16Value;

for(i=0; i<=4; i=i+1)

{

au8BCD[i] = x / 1000 + '0';

x = (x % 1000) * 10;

}

}

Der Unterschied ist nicht groß, vor dem Aufruf der Funktion muss nun allerdings die Zeile

u16ADC = u16Hexzahl * 806 / 1000;

eingefügt werden, um einen Millivoltwert zu erhalten. Der Name wurde so erweitert, dass klar erkennbar ist, was in welches Format gewandelt wird. Eine weitere Info ist die Angabe des Variablentyps im Namen. Viele Programmierer bezeichnen dies als unnötig, da moderne IDEs die Länge automatisch angeben, wenn der Mauszeiger über der Variablen steht. Problem nur, ist das File ausgedruckt oder mit einem normalen Editor geöffnet worden, funktioniert das nicht mehr. Es ist völlig problemlos, Parameter an eine Funktion zu übergeben. Bei Arrays genügt die obige Schreib-weise, damit der Compiler einen Zeiger aus der Angabe UCHAR au8BCD[] erzeugt. Sollen struct oder

union übergeben werden, muss dies mit einem Zeiger geschehen (* Operator in Funktionskopf, & Ope-

rator beim Aufruf).

Page 12: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 12 -

3 Grundlagen

3.1 Grundlagen Pin/Portprogrammierung Alle komplexen Mikrocontroller haben das gleiche Problem: Die meisten Portpins sind mehrfach verwend-bar. 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 angespro-chen 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 angespro-

chen werden.

GPIOE->DOUT_BYTE1 = 0xFF; // Schreiben

u8Data = GPIOE->PIN_BYTE0; // Lesen

Page 13: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 19 -

3.2 Pin/Portprogrammierung „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.

3.2.1 Zuordnung Pinnamen-Definitionen-Portstrukturelemente

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_BYTE1 in r1 bilden und r0 dorthin 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.

Byte Register

Bit Register

Definition für DrvGPIO_xxx

Definition für M_GPIO_BIT_xxx

~ Pin Value

Pin Name

GPIOE-> DOUT_BYTE1

PE15_PDIO LED7 BIT_LED7 PE15

PE14_PDIO LED6 BIT_LED6 PE14

PE13_PDIO LED5 BIT_LED5 PE13

PE12_PDIO LED4 BIT_LED4 PE12

PE11_PDIO LED3 BIT_LED3 PE11

PE10_PDIO LED2 BIT_LED2 PE10

PE9_PDIO LED1 BIT_LED1 PE9

PE8_PDIO LED0 BIT_LED0 PE8

GPIOE->

PIN_BYTE0

PE7_PDIO JOY_DOWN BIT_JOY_DOWN 0x80 PE7

PE6_PDIO JOY_TASTER BIT_JOY_TASTER 0x40 PE6

PE5_PDIO PE5

PE4_PDIO JOY_L BIT_JOY_L 0x10 PE4

PE3_PDIO JOY_R BIT_JOY_R 0x08 PE3

PE2_PDIO JOY_UP BIT_JOY_UP 0x04 PE2

PE1_PDIO PE1

PE0_PDIO PE0

Page 14: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 20 -

3.2.2 Initialisierung per CMSIS Bevor Portpins genutzt werden können, muss ihre Funktion (Eingang, Ausgang, OpenDrain, Bi-Direktio-nal) durch Beschreiben der zum Pin gehörenden Funktionskontrollregister eingestellt werden. Diese Ini-tialisierung kann auf unterschiedliche Weise durchgeführt werden.

// Pin E8 für LED0 als Ausgang initialisieren

PE->PMD |= GPIO_PMD_OUTPUT << GPIO_PMD_PMD8_Pos;

// Pin E2 für den Taster Joystick Down als Eingang initialisieren

PE->PMD &= ~(GPIO_PMD_INPUT << GPIO_PMD_PMD2_Pos);

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 ei-niges mehr zu tun.

3.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.

Die zugehörige Initialisierungs-Funktion für Portpins aus der Bibliothek lautet: GPIO_Open(E_GPE, 8, E_IO_OUTPUT); // Pin für LED0 Output

GPIO_Open(E_GPE, 2, E_IO_INPUT); // Pin für Joystick Input

Damit der Sourcecode lesbarer wird, sind Defines in der BoardConfig.h angelegt: #define PORT_LEDS E_GPE // Portgruppe E

#define PORT_JOY E_GPE // Portgruppe E

#define LED0 8 // Pin 8

#define JOY_DOWN 2 // Pin 2

Es ist zwar mehr zu schreiben, doch der Code dokumentiert sich fast selbst: GPIO_Open(PORT_LEDS, LED0, E_IO_OUTPUT); // LED0 als Output

GPIO_Open(PORT_JOY, JOY_DOWN, E_IO_INPUT); // Joystick Down als Input

3.2.4 Port-Pin Funktionen zur Bitmanipulation Funktionen zur Bitmanipulation und Abfrage wurden als Makros angelegt: M_GPIO_BIT_SET(BIT_LCD_A0); // Setzt Pin für LCD_A0

M_GPIO_BIT_CLEAR(BIT_LCD_A0); // Löscht Pin für LCD_A0

M_GPIO_BIT_GET(BIT_TASTER_2); // 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: GPIO_SetBit(PORT_LCD, LCD_A0); // Setzt Pin für LCD_A0

GPIO_ClearBit(PORT_LCD, LCD_A0); // Löscht Pin für LCD_A0

uint32_t = GPIO_GetBit(P_TASTER, TASTER_SW2); // Einlesen Pin für SW2

Page 15: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 21 -

3.2.5 Tasterabfrage Das Hauptproblem bei der Tasterabfrage ist die Geschwindigkeit des Mikrocontrollers und die mechani-schen 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 das Programm blockieren. In 20 ms könnte die CPU 240000 Befehle abarbeiten, weshalb andere Verfahren realisiert werden.

3.2.6 Programmablaufsteuerung mit Tasten Wird eine Taste mit Hilfe einer if-Abfrage geprüft, so wird die in der Klammer enthalte Funktionalität

dann je nach Art des Vergleiches (Taster aktiv oder inaktiv als Vergleichswert) einmalig ausgeführt. if(M_GPIO_BIT_GET(BIT_TASTER_2) == AKTIV) // Gedrückt ?

{

M_GPIO_BIT_CLEAR(BIT_LED0); // LED0 an

}

Oft ist aber auch gewünscht, den Benutzer an einer bestimmten Stelle zu einer Tastenaktion zu zwingen, bevor es weitergehen soll, wie zum Beispiel der Ok-Button bei einer Grafikoberfläche. Dies wird mit Hilfe von while() erreicht:

while(M_GPIO_BIT_GET(BIT_TASTER_2) == INAKTIV) // Solange nicht gedrückt {} // warten

Die Verwendung dieses Konstruktes sollte überlegt werden, weil sie das Programm an dieser Stelle an-hält und bis zum Druck auf Taste 2 wartet. Werden 2 dieser Abfragen hintereinander geschaltet, so kann eine „Passwort-Abfrage“ in Form von Tas-tendrücken realisiert werden. Ein weiterer Einsatz ist die Ausführung eines bestimmten Programmteils bis zum Druck oder dem Los-lassen einer Taste: do

{

u8Counter++; // hochzählen

}

while(M_GPIO_BIT_GET(BIT_TASTER_2) == AKTIV); // Solange gedrückt

Es wird solange hochgezählt, bis Taste 2 nicht mehr aktiv ist. Hier ist zu beachten, dass mindestens eine Erhöhung von u8Counter erfolgt, da es sich um eine fußge-steuerte Schleife handelt. Soll vor dem ersten erhöhen schon die Taste überprüft werden, muss die while() über dem Klammerblock angeordnet werden.

Page 16: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 22 -

3.2.7 Unterscheidung Taster noch aktiv oder erneuter Tastendruck In vielen Programmen sollen einzelne Tastendrücke registriert werden, also Taster drücken und loslas-sen. Für den Joystick ist folgende Variante möglich, der Vergleich alt mit neu. Dazu werden zwei Variablen deklariert: uint8_t u8JoystickAlt; // letzter Wert des Joysticks

uint8_t u8Joystick; // 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 ge-drückt ist oder nicht. Dies führt dazu, dass die Aktion solange ausgeführt wird, wie die entsprechende Joystick-Position gedrückt ist. Um dieses Problem zu beheben, wird der Code wie folgt erweitert: u8Joystick = GPIOE->PIN_BYTE0; // Einlesen des ganzen Joystick Ports

if(u8Joystick != u8JoystickAlt) // Veränderung ?

{

u8JoystickAlt = u8Joystick; // Speichern des aktuellen Wertes

switch(u8Joystick)

{

case ??: ……

}

}

Die Tastenbearbeitung wird nur dann ausgeführt, wenn der aktuelle Wert des Joysticks nicht dem gespei-cherten entspricht. Damit wird erreicht, dass die gewünschte Aktion nur einmalig ausgeführt wird.

3.2.8 Taster schaltet ein und aus Oft werden für ein- bzw. ausschalten zwei unterschiedliche Taster verwendet. Mit einer kleinen Erweite-rung des case Abschnittes für eine Taste kann dies realisiert werden:

switch(u8Joystick)

{

case ??: if(u8SysTickFreigabe == FALSE)

{

u8SysTickFreigabe = TRUE;

}

else

{

u8SysTickFreigabe = FALSE;

}

break;

}

Page 17: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 23 -

3.3 Grundlagen zu LCDs

3.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 geschrie-ben.

3.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 Rechen-leistung, 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 entspre-chende Aktion umsetzt inkl. zeitlicher Ansteuerung -> kleine Rechenleistung.

3.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 Pro-grammierung 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 Pixel-speicher, 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.

3.3.4 Zeichenerzeugung Woraus besteht ein Muster? Als Grundlage soll der Buchstabe H aus 6 x 8 Pixel dienen:

1 0 0 0 1 0 1 0 0 0 1 0 1 0 0 0 1 0

1 0 0 0 1 0 1 0 0 0 1 0 1 0 0 0 1 0

1 0 0 0 1 0 1 0 0 0 1 0 1 0 0 0 1 0

1 1 1 1 1 0 1 1 1 1 1 0 1 1 1 1 1 0

1 0 0 0 1 0 1 0 0 0 1 0 1 0 0 0 1 0

1 0 0 0 1 0 1 0 0 0 1 0 1 0 0 0 1 0

1 0 0 0 1 0 1 0 0 0 1 0 1 0 0 0 1 0

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Pixelmuster Pixelcodierung Speicherablage 1 Speicherablage 2

Page 18: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 24 -

• 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 Buch-staben 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 ge-speichert. 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 Programm-speicher 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.

3.3.5 Zeichenausgabe Die Ausgabe eines Buchstabens erfolgt dadurch, dass die Startposition für das Zeichenmuster im Font-Array 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 Pro-grammspeicher 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 (be-ginnend 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); // Cursor setzen

GLCD_PrintChar(‚H‘); // Zeichen ausgeben

Zuvor müssen im Initialisierungsteil des Programms einige Funktionen aufgerufen werden, damit das LCD funktioniert.

Page 19: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 25 -

3.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 Text-LCDs gegeben ist.

3.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 NUCx30 angesteuert, weshalb die notwendige Initialisie-rung der benutzten SPI-Schnittstelle innerhalb der Funktion stattfindet.

3.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.

3.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.

3.3.6.4 Cursor auf Textposition setzen

void GLCD_SetTextCursor(uint8_t ui8Row, uint8_t ui8Column);

Mit dieser Funktion wird Zeile und Spalte gesetzt.

3.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.

3.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.

3.3.6.7 Löschen einer Zeile

void GLCD_ClearRow(uint8_t ui8Row);

Löscht die angegebene Zeile.

3.3.6.8 Löschen des LCDs

void GLCD_ClearLCD(void);

Page 20: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 26 -

3.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 Zahlen-formates umgerechnet werden müssen. Zur Ausgabe auf das LCD ist noch eine Umwandlung der be-rechneten Stellen in das ASCII-Format notwendig. Die gebräuchlichsten Formate sind das Dezimalsys-tem 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; // Variable mit dem Inhalt 56

uint8_t u8Zehner; // Variable für die Zehner-Stelle

uint8_t u8Einer; // Variable für die Zehner-Stelle

u8Zehner = u8Test / 10; // Abtrennen der Zehner-Stelle

u8Zehner = u8Zehner+0x30; // Ergebnis in ein ASCII-Zeichen umwandeln

u8Einer = u8Test %10; // Abtrennen der Einer-Stelle

u8Einer = u8Einer +0x30; // 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 darstell-baren 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 Zahlen-formate 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 können. Die Zeichen werden einfach ab der Stelle 0 in den String geschrieben und nach der Rückkehr aus der Umwandlungs-routine 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.

Page 21: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 27 -

3.4 Grundlagen zum SysTick-Timer 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.

3.4.1 Initialisierung Mit Hilfe von SysTick_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öglich-keit, eine andere Taktquelle zu verwenden. Für seine Funktion als Systemtimer ist die Zeitspanne aus-reichend. Die SysTick-Einheit lädt automatisch nach dem Ablauf der eingestellten Zeit den alten Wert.

3.4.2 SysTick starten Das Makro M_SYSTICK_ENABLE;

startet den Timer. Normalerweise wird der SysTick nur gestartet, aber nie wieder gestoppt.

3.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 fest-gelegten Namen hat: void SysTick_Handler (void) // SysTick Interrupt Handler

{

...Insert function here

}

3.4.4 Ein Nutzen des SysTicks In vielen Anwendungen wird der SysTick als zyklischer Zeitgeber verwendet, d.h. er wird auf einen be-stimmten 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 not-wendigen Wert und ein anderer Programmteil, meistens die Main-Schleife, bearbeitet dann die Anforde-rung.

Page 22: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 28 -

3.5 Grundlagen zu Timern Die vier Timereinheiten des NUCx30 sind sehr komplexe Peripherie-Elemente. Im Labor werden sie meist nur zur Erzeugung von genauen Delay benutzt.

3.5.1 Initialisierung Jeder Timer muss vor der Benutzung initialisiert werden. Dazu steht die Funktion

Timer_Init_us(Timernummer,Arbeitsmode);

zur Verfügung. Die einzelnen Arbeitsmodi werden im Folgenden erklärt.

3.5.2 Timer-Modes

• TIMER_ONE_SHOT Der Timer erzeugt ein Intervall mit der Länge, die durch den im Zählre-

gister hinterlegten Wert.

• TIMER_PERIODIC Der Timer funktioniert wie der SysTick, d.h. er gibt in periodischen Ab-

ständen per Flag oder Interrupt ein Signal, dass die eingestellte Zeit abgelaufen ist.

• TIMER_TOGGLE Der Timer-Pin TMx wird nach der eingestellten Zeit umgeschaltet.

• TIMER_CONTI In diesem Mode wird der Timer nach dem Erreichen des Zählerstandes

nicht auf 0 zurückgesetzt, sondern er zählt weiter.

3.5.3 Timer Value Register

Mit dem Makro M_TIMER_VALUE_SET(Timernummer,Zeitwert);

wird der Zeitwert in µs für den jeweiligen Timer initialisiert.

3.5.4 Timer Interrupt Jede Timereinheit kann Interrupts auslösen. Wichtig ist nur, dass in der ISR dann das zugehörige Flag wieder gelöscht wird. Der Rest in main ist einfach: NVIC_EnableIRQ(TMR0_IRQn); // hier Timer0 im NVIC freischalten

M_TIMER_INT_ENABLE(TIMER0); // Interrupt in Timereinheit freischalten

Die ISR:

void TMR0_IRQHandler(void)

{

M_TIMER_TIF_CLEAR(TIMER0);

…..

}

Page 23: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 29 -

3.6 Grundlagen zu Interrupts Ein Interrupt ist eine Unterbrechung des normalen Programmablaufes. Einsatzgebiet kann grob in drei Kategorien unterteilt werden:

1. Schnelle Reaktion auf Ereignisse 2. Wenn ein Softwareteil unbedingt ausgeführt werden soll 3. Auftretende Ereignisse in unregelmäßigen Abständen 4. Energie sparen

3.6.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 Inter-rupts wird nun so schnell wie möglich (in ca. 12 bis 16 Maschinenzyklen) zu der ISR gewechselt und es entsteht nur eine minimale Verzögerung.

3.6.1.2 Unbedingte Ausführung

Bestimmte Sicherheitsfunktionen werden gerne in ISRs verlagert, da eine freigeschaltete ISR immer funk-tioniert, auch wenn sich das Programm aufgehängt hat.

3.6.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önn-ten.

3.6.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 energiespa-rende Systeme zu entwickeln. Für alle freigeschalteten Interrupts gilt, dass die zugehörige Interrupt Service Routine, die ISR als Funk-tion 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 Unterschie-den zu einem normalen Unterprogramm:

• 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

3.6.2 „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

• Ein Interrupt wird unmittelbar vor dem Eintritt in die Mainloop freigeschaltet, nicht vorher

Page 24: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 30 -

3.7 Grundlagen zum ADC

3.7.1 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.

3.7.2 Initialisierung für einen Kanal 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: ADC_Init(CHANNEL_2_SELECT); // Poti auf dem M_Dongle

Sie stellt alle notwendigen Register auf die korrekten Werte ein, es muss nur noch der verwendete Kanal angegeben werden. Mit dieser Initialisierung ist es nur möglich, einen Kanal abzutasten.

3.7.3 Initialisierung für mehrere Kanäle, der Single Cycle Mode des ADC In manchen Anwendungen ist es notwendig, mehrere ADC-Kanäle zu benutzen. Dies muss dem ADC mit einer zusätzlichen Konfiguration mitgeteilt werden. Dazu wird im Kontrollregister ein weiterer Eintrag gemacht: ADCR |= ADC_ADCR_ADMD_SINGLE_CYCLE; // Umstellen auf Single Cycle Mode

Im Übergabewert der ADC_Init-Funktion müssen nun alle die Kanäle eine 1 bekommen, die benutzt wer-den sollen. Wenn die Umwandlung erfolgt ist, kann jeder Kanal mittels des M_ADC_DATA_READ(CHANNEL_X)-Makros

ausgelesen werden.

3.7.4 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ückgegeben, so kann dann mit

u16Result = M_ADC_DATA_READ(CHANNEL_2); // Poti auf dem M_Dongle

der Wert ausgelesen werden. Da durch das Lesen des ADC-Wertes das ADF-Flag nicht gelöscht wird, muss es mit M_ADC_ADF_CLR; // Löschen des ADF-Flags

zurückgesetzt werden.

Page 25: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 31 -

3.7.5 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 ermit-teln, 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. ** funktioniert nur, weil die CPU intern mit 32 Bit rechnet, nur in der dargestellten Form nutzbar

3.7.6 Umwandlung in nutzbare Werte Hier können nun zwei Wege beschritten werden. Der eine Weg multipliziert einfach die Quantisierungs-stufe mit dem Datenregister des ADC, in unserem Fall ADC-Datenregister * 806 mV / 1000 und 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. 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.

3.7.7 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 Mit-telwerte ü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.

3.7.8 Grundlagen zur Dezimalzahlendarstellung

Variablen oder Register-/Speicherinhalte werden entweder als dezimale oder hexadezimale Zahlen dar-gestellt. 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 dezi-male 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: 0x80 Nach Weg 2 ergibt sich die Dezimalzahl: 1,650

Die Stelle für das Komma ist nur aus der Aufgabe bekannt, d.h. wenn die Ausgabe in Volt erfol-gen 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 Umwand-lungsfunktion benötigt, die eine Zahl mit vier Stellen berechnen kann. Der Programmierer ent-scheidet 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.

Page 26: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 32 -

3.8 Grundlagen zum UART2

3.8.1 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ög-lichkeit, Datenpakete mittels FIFO-Speicher senden bzw. empfangen zu können. Außerdem sind ver-schiede 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 einge-schrä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 System_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.

3.8.2 Übertragungsrate Das Hauptproblem aller asynchronen Datenübertragungen ist die Frequenz, mit der die Daten- und Kon-trollbits gesendet werden. Das bedeutet, dass jeder Teilnehmer seinen eigenen Sende- und Empfangs-takt erzeugen muss, da er im Datenframe nicht enthalten ist. Für UARTs beträgt die Abweichung vom Takt maximal 5 %.

3.8.3 Auswahl der Baudrate Hier sind einige Punkte zu beachten: Soll mit einem Terminalprogramm auf dem PC kommuniziert werden, sind nur einige bestimmte Baudar-ten möglich. Sollen sich zwei M_Dongles Daten schicken, können auch andere Baudraten verwendet werden, aller-dings darf die Geschwindigkeit die 120 kB nicht überschreiten, da sonst der Interface-Baustein für die serielle Schnittstelle (den Sub-D-9 Stecker) Fehler erzeugt.

3.8.4 Initialisierung des UARTs Für die Initialisierung einer einfachen Funktion des UART2 ist eine Funktion in der Bibliothek vorhanden: void UART2_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. 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: UART2_Init(38400,0); // 38k4, 8N1, Meldung ab 1 Byte im Empfänger

Page 27: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 33 -

3.8.5 Daten senden mit M_UART2_DATA_WRITE und TX_FULL Daten senden nur wenn mindestens ein Byte im Puffer frei, sonst weiter: if(M_UART_TX_FULL(UART2) == FALSE) // wenn ein Byte im Puffer UART2 frei

{

M_UART_DATA_WRITE(UART2, 'T'); // Sendet ein T über UART2

}

Obiger Code fragt ab, ob mindestens ein Zeichen in den Puffer geschrieben werden darf. Sollen mehrere Bytes gesendet werden, ist diese Abfrage etwas umständlich, weil viel Code benötigt wird.

3.8.6 Daten senden mit M_UART2_DATA_WRITE und TX_EMPTY Daten senden nur wenn Puffer frei (16 Bytes), sonst weiter: if(M_UART_TX_EMPTY(UART2) == TRUE) // wenn Puffer UART2 frei

{

M_UART_DATA_WRITE(UART2,'H'); // Sendet ein H

M_UART_DATA_WRITE(UART2,'a'); // Sendet ein a

M_UART_DATA_WRITE(UART2,'l'); // Sendet ein l

M_UART_DATA_WRITE(UART2,'l'); // Sendet ein l

M_UART_DATA_WRITE(UART2,'0'); // Sendet ein o

}

3.8.7 Daten empfangen mit M_UART2_DATA_READ

Daten empfangen wenn Daten da, sonst weiter: if(M_UART_RX_EMPTY(UART2) == FALSE) // wenn FALSE, Daten vorhanden

{

u8Empfang = M_UART_DATA_READ(UART2); // Daten aus Empfangsregister holen

}

Die Lese- bzw. Schreib- Makros selbst lesen bzw. beschreiben nur das Register des UARTs, ohne auf die Status-Flags zu achten. Da dies aber zu Fehlern führen kann, müssen die Flags mit einbezogen werden.

Page 28: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 34 -

4 Vertiefende Peripherie Programmierung

4.1 SysTick Der SysTick kann in größeren Anwendungen als „Verwalter“ für Aufgaben verwendet werden. Er kontrol-liert 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 Anforde-rung. Soll die Signalisierung durch den SysTick schaltbar sein, so wird dies mit Hilfe einer zweiten, glo-balen Variablen realisiert.

void SysTick_Handler (void) // SysTick Interrupt Handler

{

if(gu8ToggleOn == 1) // Ist Freigabe da?

gu8LEDToggle = 1; // Anforderung setzen

}

void main (void)

{

if(gu8LEDToggle == 1) // Anforderung da ?

{

gu8LEDToggle = 0; // Anforderung löschen

M_GPIO_BIT_WRITE(BIT_LED0) = ~M_GPIO_BIT_GET(BIT_LED0);

}

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 Abstand

• 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 // Signal vom Main-Loop, ADC soll Wandlung starten

gu8ErrorLed // Signal für Fehlerblinken

gu8JoyStickFlag // Signal für Joystick einlesen

Page 29: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 35 -

Die ISR sieht nun wie folgt aus: void SysTick_Handler (void) // SysTick Interrupt Handler

{

static uint8_t u8ISRCount = 10; // Wert für 250 ms

u8ISRCount--; // 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) // Soll ADC wandeln ? (Info von Main)

M_ADC_CONVERT_START; // 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?

{

gu8JoyStickFlag = 0; // Anforderung löschen

u8Joystick = ~(GPIOE->PIN_BYTE0); // Einlesen des ganzen Joystick Ports

if(u8Joystick != u8JoystickAlt) // Veränderung ?

{

u8JoystickAlt = u8Joystick; // Speichern des aktuellen Wertes

switch(u8Joystick)

{

case ??: ……

}

}

}

}

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 „Ar-beitszeit“, 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).

Page 30: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 36 -

4.2 ADC 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); // Interrupt ein

NVIC_DisableIRQ(ADC_IRQn); // Interrupt aus

ein- bzw. ausgeschaltet werden. Nun muss noch die Quelle in der Peripherie eingestellt werden, die einen Interrupt auslösen soll: M_ADC_INT_ENABLE; // Interrupt Quelle einschalten

4.2.2 ISR für ADC void ADC_IRQHandler(void)

{

M_ADC_ADF_CLR; // Flag löschen

}

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ütz-lich. 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; // Startwert halber ADC-Wert (12 Bit)

M_ADC_ADF_CLR; // Flag löschen

gu16ADC = M_ADC_DATA_READ(u8Channel); // ADC Wert einlesen (12 Bit)

gu16ADC = (gu16ADC + u16Mittelwert) >> 1; // Mittelwert erzeugen

u16Mittelwert = gu16ADC; // neuen Mittelwert sichern

}

Page 31: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 37 -

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 hier ein globales Array verwendet. void ADC_IRQHandler(void)

{

static uint8_t u8Write = 0; // Schreib-Zeiger auf Elemente im Ringpuffer

uint8_t u8Read; // Lese-Zeiger auf Elemente im Ringpuffer

M_ADC_ADF_CLR; // Flag löschen

gu16ADC_A[u8Write] = M_ADC_DATA_READ(u8Channel); // ADC Wert einlesen (12 Bit)

u8Write ++;

if(u8Write == MAX_WERTE_ADC) // maximale Anzahl für Mittelwert erreicht ?

{

gu16ADC = gu16ADC_A[0];

for(u8Read = 1; u8Read < MAX_WERTE_ADC; u8Read ++)

{

gu16ADC += gu16ADC_A[u8Read];

}

gu16ADC /= MAX_WERTE_ADC; // Mittelwert erzeugen

u8Write = 0; // 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 u8Write = 0; // Schreib-Zeiger auf Elemente im Ringpuffer

uint8_t u8Read; // Lese-Zeiger auf Elemente im Ringpuffer

M_ADC_ADF_CLR; // Flag löschen

gu16ADC_A[u8Write] = M_ADC_DATA_READ(u8Channel);// ADC Wert einlesen (12 Bit)

u8Write ++;

if(u8Write == MAX_WERTE_ADC) // maximale Anzahl für Mittelwert erreicht?

{

u8Write = 0; // Schreibzeiger auf Startwert

}

gu16ADC = gu16ADC_A[0];

for(u8Read = 1; u8Read < MAX_WERTE_ADC; u8Read ++)

{

gu16ADC += gu16ADC_A[u8Read];

}

gu16ADC /= MAX_WERTE_ADC; // Mittelwert erzeugen

}

Page 32: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 38 -

4.3 UART

4.3.1 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 se-riellen 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 Interfaces, die an einem UART betrieben werden können.

RS232 RS485 RS422 LIN

Übertragungsart Gnd bezogen Differentiell Differentiell Gnd bezogen

Spannung max. +- 15 V + 18 V

Max. Leitungslänge 900 m 1200 m 1200 m 40m

Max. Übertragungsrate 115200 (PC) 12 MBaud 10 MBaud 20 KBaud

Steuersignale UART Ja Ja Ja Nein

Busabschluss Nein Ja Ja Nein

4.3.3 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. Der UART2-Interrupt kann im NVIC mit

NVIC_EnableIRQ(UART0_IRQn); // Interrupt ein

NVIC_DisableIRQ(UART0_IRQn); // 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 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 M_UART_RX_INT_ENABLE(UART2);

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.

Page 33: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 39 -

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(M_UART_RX_INT_ACTIVE(UART2) == 1)// Quelle Empfangen UART2?

{

gu8RXdata[u8RXCounter] = M_UART_DATA_READ(UART2);

u8RXCounter++;

if(u8RXCounter == TX_PAKETLEN) // Anzahl erreicht?

{

u8RXCounter = 0;

gu8UART_Ready = 1; // Flag für Main setzen

}

}

if(M_UART_TX_INT_ACTIVE(UART2) == 1)// Quelle Senden UART2?

{

if(gpu8TXdata[u8TXCounter] != 0) // Stringende erreicht ?

{

UART2->DATA = (gpu8TXdata[u8TXCounter]);

u8TXCounter ++;

}

else

{

M_UART_TX_INT_DISABLE(UART2); // Senden abschalten

u8TXCounter = 0;

}

}

}

Damit die ISR funktioniert, sind folgende Zeilen vor der Hauptschleife nötig: uint8_t gu8RXdata[9]; // globales Empfangsarray

uint8_t* gpu8TXdata; // globaler Zeiger für Sendedaten

uint8_t gu8UART_Ready = 0; // globales Flag für HP, neue Daten da

M_UART_RX_INT_ENABLE(UART2); // UART2 RX freischalten

NVIC_EnableIRQ(UART0_IRQn); // UART2 im NVIC freischalten

void UART2_SendString(uint8_t Array[]) // Funktion bekommt String als Parameter

{

gpu8TXdata = Array; // Übergabe Arrayzeiger

M_UART_TX_INT_ENABLE(UART2); // Sende Interrupt ein

}

Die einzige Einschränkung ist jetzt, dass die Funktion UART2_SendString()wieder aufgerufen wird, wäh-

rend der letzte Auftrag noch bearbeitet wird. Dies kann bei langsamer Übertragung durchaus passieren.

Page 34: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 40 -

4.4 Timer

4.4.1 Gated Timer-Funktion (External Capture Mode) Die Timer des NUC besitzen neben den im Labor verwendeten „normalen“ Zähleigenschaften (Delays) weitere nutzbare Betriebsmodi. Einer dieser Modi ist der External Capture Mode, der die Möglichkeit bie-tet, einen „Gated Timer“ zu realisieren. Diese externe Steuerungsmöglichkeit wird über einen speziellen Portpin realisiert. Jeder Timer hat dafür einen TMx_EX-Pin, der dieses ermöglicht. Als Beispiel soll die Realisierung einer Ultraschall-Entfernungsmessung mit Hilfe eines HC-SR04 Moduls dienen. Die Messung wird über den Trigger-Pin aktiviert und das Modul gibt dann an seinem Echo-Pin einen High-Impuls aus, aus dessen Länge der gemessene Abstand errechnet werden kann.

4.4.2 Bekannte Lösungsmöglichkeiten

4.4.2.1 Lösung 1: Polling

Es wird solange gewartet, bis der mit dem Echo-Pin verbundene Eingang auf high geht und dann wird ein Timer gestartet. Der Timer wird dann gestoppt, wenn ein low am selben Eingang erkannt wird. Aus der Timerzeit kann dann die Länge errechnet werden. Nachteil: Es muss die ganze Zeit mit der Beobachtung des Echo-Pin Eingangs verbracht werden, damit die Mes-sung einigermaßen genau wird. Außerdem können auftretende Interrupts die Ungenauigkeit vergrößern. Da zwischen Start der Messung (Triggersignal) und Impulsende des Echo-Signals eine erhebliche Zeit (viele Maschinenzyklen) aus Sicht des µCs gewartet werden müssen, ist diese Lösung nicht nutzbar.

4.4.2.2 Lösung 2: Pin-Interrupt

Der Echo-Pin löst einen Interrupt für die steigende Flanke aus, der wiederum den Timer startet. Nun wird der Interrupt auf fallende Flanke umgestellt. Tritt nun dieser Interrupt zum zweiten Mal auf, wird der Timer wieder gestoppt, die Flanke auf steigend umgestellt und aus dem Timerwert kann der Abstand errechnet werden. Nachteil: Pin-Interrupts haben eine sehr niedere Priorität und werden deshalb von anderen Interrupts unterbrochen. Außerdem wird der Timer erst gestartet / gestoppt, wenn die Pin-ISR bearbeitet wird. Dies geht auf Kosten der Genauigkeit und kann damit bei mobilen Roboterapplikation nur beschränkt eingesetzt werden.

4.4.2.3 Lösung 3: Gated Timer

Der zuvor zum Zählen verwendete Timer wird im External Capture Mode initialisiert und die Hardware des Timers löst das Problem. Da starten, stoppen und Reset des Timers von der Hardware übernommen werden, treten keine Ungenauigkeiten auf. Die erzeugten Flags sind nur Informationen, deren Abfrage die Messwerte nicht beeinflussen. Da die Initialisierung des External Capture Modes komplexer ist, soll sie auf der folgenden Seite anhand des Timer-Blockschaltbildes aus dem Datenblatt des NUCx30/x40 erklärt werden.

Page 35: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 41 -

Damit der in orange gezeichnete Signalweg verwendet werden kann, müssen die blau umrahmten Re-gister korrekt initialisiert sein. Da diese Funktion eine spezielle Pinkonfiguration für den jeweiligen TxEX-Pin des NUC benötigt, muss diese mit Hilfe der beiden System-Register konfiguriert werden: SYS->ALT_MFP = Define für den jeweiligen Pin

SYS->GPx_MFP = Define für den jeweiligen Pin

Normale Initialisierung Timer: Timer_Init_us(TIMERx,TIMER_CONTI); // Timer normal im Continous Mode initialisieren

M_TIMER_START(TIMERx); // Timer starten

Zusätzliche Initialisierung Timer:

M_TIMER_TDR_ENABLE(TIMERx); // Freigabe für TDR Register, damit aktueller

Wert gelesen werden kann (TDR_EN)

M_TIMER_CAPTURE_EXT_PIN_ENABLE(TIMERy); // ext. Pin für Timerfreigabe einschalten (TEXEN)

In der Mainloop ist nun der folgende Abschnitt für die Erzeugung des Triggerpulses und der Erfassung des Echos zuständig (am Beispiel Timer0 für den Echo-Puls): // Flanke und Funktion für den T0EX-Pin einstellen M_TIMER_CAPTURE_SET_EDGE(TIMER0, TIMER_CAPTURE_RISING_EDGE); // Steigende Flanke (TEX_EDGE)

M_TIMER_RSTCAPSEL_RESET(TIMER0); // Steigende Flanke setzt Timer0 auf 0 (RSTCAPSEL)

// Trigger-Puls mit 10 µs erzeugen

M_GPIO_BIT_SET(BIT_LED0);

Timer_Delay_us(TIMER1, 10);

M_GPIO_BIT_CLEAR(BIT_LED0);

while(M_TIMER_TEXIF_GET(TIMER0) == 0) // warten bis Flanke da

{

}

M_TIMER_TEXIF_CLEAR(TIMER0); // Flag löschen (TEXIF)

Page 36: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 42 -

// Flanke und Funktion für den T0EX-Pin einstellen M_TIMER_CAPTURE_SET_EDGE(TIMER0,TIMER_CAPTURE_FALLING_EDGE);// fallende Flanke (TEX_EDGE)

M_TIMER_RSTCAPSEL_LOAD(TIMER0); // fallende Flanke lädt Counter (RSTCAPSEL)

while(M_TIMER_TEXIF_GET(TIMER0) == 0)// warten bis Flanke da (TEXIF)

{}

M_TIMER_TEXIF_CLEAR(TIMER0); // Flag löschen (TEXIF)

gu32Echo = TIMER0->TCAP; // Zählwert abholen für Weiterverabeitung

Obiger Code hat leider das Problem, dass das Programm wegen der beiden Warte-While und dem Delay doch sehr verlangsamt wird, was bei einem Einsatz in einer Fahrroboter-Applikation ebenfalls zu erheb-lichen Problemen führen würde.

4.4.2.4 Lösung 4: Umstellung auf Interrupt-Betrieb

void SysTick_Handler (void) // Zyklisch den Triggerimpuls starten

{

if(gu8US_on) // Freigabe Messung da ?

{

M_GPIO_BIT_SET(ULTRA_TRIGGER); // Triggerimpuls setzen

M_TIMER_START(TIMER3); // Timer für 10 µs starten

}

}

void TMR3_IRQHandler (void) // 10 µs um

{

M_TIMER_TIF_CLEAR(TIMER3); // Int-Flag löschen

M_GPIO_BIT_CLEAR(ULTRA_TRIGGER); // Triggerimpuls löschen

}

void TMR0_IRQHandler (void) // jede Flanke am TxEX-pin erzeugt einen Interrupt

{

static uint8_t Toggle = 1; // Steuerflag für Umschaltung steigende/fallende Flanke

M_TIMER_TEXIF_CLEAR(TIMER0); // Int-Flag löschen

if(Toggle) // steigende Flanke vom Echo-Pin

{

Toggle = 0;

M_TIMER_CAPTURE_SET_EDGE(TIMER0,TIMER_CAPTURE_FALLING_EDGE); // umstellen fallende Flanke

M_TIMER_RSTCAPSEL_LOAD(TIMER0); // als nächstes muss Zählwert geladen werden

}

else // fallende Flanke vom Echo-Pin

{

Toggle = 1;

gu32Echo = TIMER0->TCAP; // Entfernung in µs sichern

M_TIMER_CAPTURE_SET_EDGE(TIMER0,TIMER_CAPTURE_RISING_EDGE); // umstellen steigende Flanke

M_TIMER_RSTCAPSEL_RESET(TIMER0); // auf Reset des Zählregisters umstellen

}

}

Damit bekommt das System zyklisch in gu32Echo die zuletzt ermittelte Entfernung bereitgestellt.

Damit der Interruptbetrieb funktioniert, müssen folgende Initialisierungsschritte noch gemacht werden:

Timer_Init_us(TIMER3,TIMER_ONE_SHOT); // für 10 us Trigger-Impuls vom US-Modul

M_TIMER_VALUE_SET(TIMER3, 8);

M_TIMER_TIF_CLEAR(TIMER3);

M_TIMER_INT_ENABLE(TIMER3);

M_TIMER_TDR_ENABLE(TIMER0); M_TIMER_CAPTURE_EXT_PIN_ENABLE(TIMER0);

M_TIMER_TEXIF_CLEAR(TIMER0);

M_TIMER_TEX_INT_ENABLE(TIMER0);

M_TIMER_CAPTURE_SET_EDGE(TIMER0,TIMER_CAPTURE_RISING_EDGE);

M_TIMER_RSTCAPSEL_RESET(TIMER0);

NVIC_EnableIRQ(TMR3_IRQn);

NVIC_EnableIRQ(TMR0_IRQn);

Page 37: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 43 -

5 Tipps und Tricks

5.1 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 r0,#0x00

STR r0,[sp,#0x00]

B 0x000002CE

LDR r0,[sp,#0x1C]

ADDS r0,r0,#1

UXTH r0,r0

STR r0,[sp,#0x1C]

LDR r0,[sp,#0x00]

ADDS r0,r0,#1

UXTH r0,r0

STR r0,[sp,#0x00]

LDR r0,[sp,#0x00]

CMP r0,#0x0A

BLT 0x000002BE

Ohne die einzelnen Zeilen zu kommentieren, der Code ist recht lang geworden. Das hängt mit dem Be-fehlssatz zusammen und der Tatsache, dass Compiler nur selten für eine Prozessorfamilie geschrieben werden. Doch bauen wir die Schleife einfach um und lassen auf 0 zählen und fragen auf ungleich ab:

uint16_tui;

for(ui = 10; ui != 0; ui--)

{...}

MOVS r0,#0x0A

SUBS r0,r0,#1

UXTH r0,r0

CMP r0,#0x00

BNE 0x000002B6

Das Ergebnis überrascht schon, von 14 Befehlen auf 9 Befehle, die reine Schleife nur noch 4 Befehle. 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

Page 38: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 44 -

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

5.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 else-Zweig 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)

Page 39: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 45 -

5.2.1 Mischen von Abfragen Werden Bits (Ports) mit Variablen bitweise verknüpft, so wird der Code länger als bei reinen Bit-Verknüp-fungen, 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

5.3 Variablen Hier kann viel gespart oder auch verschwendet werden. ➔ wo es geht, nur positiv ganzzahlige Variablen verwenden ➔Strukturen verwenden

5.3.1 Integerproblem Bei der Erzeugung von plattformunabhängigem Code taucht immer ein Problem auf: Die Länge von Inte-gervariablen. Um dieses Problem zu umgehen, wird auf die Methode gesetzt, im Sourcecode an- stelle 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.

5.3.2 Lösung Anstatt die Variablen wie folgt zu deklarieren:

unsigned int uiZeilenLaenge; // Anzahl Buchstaben pro Zeile

unsigned char ucBuchstabe; // Charakter

wird jetzt die Notation

uint32_t u32ZeilenLaenge; // Anzahl Buchstaben pro Zeile

uint8_t u8Buchstabe; // Charakter

verwendet.

In der stdint.h ist folgende Definition zu finden:

typedef unsigned int uint32_t;

typedef unsigned char 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 verwen-det.

Page 40: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 46 -

5.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. Auch in eigenen Programmen können Strukturen sinnvoll eingesetzt werden. Oft wird mit Hilfe von einigen Variablen eine Kommunikation zwischen verschiedenen Softwareteilen aufgebaut. Dabei kann es sich um Kommunikation zwischen Main und Unterprogrammen oder Main und ISR handeln. Wird mit einem Unterprogramm kommuniziert, so kann einfach der Zeiger der Struktur als Parameter übergeben werden. Als Beispiel kann Kap. 2.7.2 dienen. Soll mit einer ISR kommuniziert werden, wird die Struktur global angelegt und beide Seiten können damit arbeiten. Beim Programmieren hat es den Vorteil, dass die Texterkennung dann nach 3 oder 4 Buchstaben einem schon die Variablen aus der Struktur anzeigt. struct Control

{

uint8_t Freigabe; // SysTick soll etwas machen

uint8_t Anforderung; // SysTick fordert etwas an

}SysTick_Control;

Da noch beide Variablen ohne Werte sind, werden sie in Main initialisiert: SysTick_Control.Freigabe = FALSE; // Keine Freigabe

SysTick_Control.Anforderung = FALSE; // Keine Anforderung

Page 41: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 47 -

5.3.4 Funktion hat eigene Strukturen Bei komplexeren Peripherie-Einheiten kommt es vor, dass in der Funktion spezielle Arrays oder Structs benutzt werden, die in der Treiber-Datei selber angelegt wurden. Wie bekommt Anwender nun Zugriff auf diese Elemente? Als Beispiel soll der Zugriff auf eine Treiberfunktion für das LIN-Protokoll dienen. LIN ist eine Kommuni-kationsform, die auf einem speziellen UART basiert und aktuell im Automobilbereich verwendet wird. Das h-File des LIN-Treibers enthält die benötigte Struktur: typedef struct

{

union

{

struct

{

uint32_t LIN_Control;

uint32_t LIN_Data32_1;

uint32_t LIN_Data32_2;

};

struct

{

uint8_t PID;

uint8_t Len;

uint8_t CRC;

uint8_t Status;

uint8_t Data[8];

};

};

}LIN_T;

Im C-File des LIN-Treibers wurde ein globaler Zeiger auf die Struktur deklariert

LIN_T *pLIN_Frame;

und mit der Zeile

extern LIN_T *pLIN_Frame;

im h-File des Treibers für alle anderen Dateien bekannt gemacht, die das h-File des LIN-Treibers einbin-den. Damit kann diese Struktur verwendet werden. Im eigenen Programm muss die Struktur nur initialisiert (C-File)

LIN_T LIN1;

und anschließend mit Daten gefüllt werden LIN1.LIN_Control = 0x00000212;

LIN1.LIN_Data32_1 = 0x44332211;

LIN1.LIN_Data32_2 = 0x88776655;

LIN1.Len = 8;

LIN1.Status = 0;

Was welche Bedeutung hat, ist der Beschreibung zum Treiber zu entnehmen. Nun kommt der wichtige Teil, die Verbindung von der gefüllten Struktur LIN1 mit dem Zeiger aus dem LIN-Treiber: pLIN_Frame = &LIN1;

Anschließend kann die gewünschte Funktion aufgerufen werden: LIN_UART_MasterSendFrame(&LIN1);

Es besteht auch die Möglichkeit, mehrere dieser Strukturen zu erzeugen. Dann muss vor dem Aufruf einer Treiber-Funktion immer nur die Adresse der jeweiligen Struktur an den pLIN_Frame Zeiger überge-

ben werden.

Page 42: C-Programmierung mit dem M Dongle

__________________________________________________________________________________________ C-Programmierung Rev. 5.0 Januar 2020 - 48 -

5.4 Statische Variablen und weitere Zeitverschwendun-gen

Zeitverschwendung bedeutet, dass der Mikrocontroller mehr Befehle ausführen muss als nötig. In Kapitel 4.1 werden Zählschleifen und deren Codelänge betrachtet. Doch auch das Deklarieren von Variablen und die Unkenntnis von Schlüsselwörtern kann zur Zeitverschwendung führen. Mit dem Schlüsselwort static wird Variablen ein besonderes Feature angefügt. Wird in einem Unterpro-gramm oder einer Interrupt Service Routine das Schlüsselwort static verwendet, so entsteht eine Variable, die auch noch nach dem Verlassen der Routine ihren Wert behält. Damit erhält die Variable aber auch einen anderen Speicherort, sie wird im Datenspeicher des Prozessors abgelegt und nicht auf dem Stack. Dies bedeutet wiederum nun für den Zugriff auf die Variable, dass erst ein 32 Bit Zeiger aufgebaut werden muss. Diese Zeigererzeugung benötigt Platz (und damit Zeit), weshalb der Zugriff länger dauert als bei einer lokalen Variablen, die bereits auf dem Stack liegt. Damit wird klar, dass statische Variablen in Main keinen Sinn machen und in Unterprogrammen / ISRs nur dann, wenn notwendig, benutzt werden sollten. Die folgende Deklaration innerhalb von Main: static uint8_t u8LCDUpdate = 1;

führt zu folgendem Code, wenn die Variable innerhalb von Main genutzt wird:

336: u8LCDUpdate = 1;

0x00000EAA 2001 MOVS r0,#0x01 ; 1 Laden

0x00000EAC 4954 LDR r1,[pc,#336] ; Offset für Adresse 0x00001000 bilden

0x00000EAE 7008 STRB r0,[r1,#0x00] ; Wert speichern

Wird dagegen die Variable ohne static deklariert, so sieht der Code wie folgt aus: 336: u8LCDUpdate = 1;

0x00000EAA 2701 MOVS r7,#0x01 ; Wert speichern

Ähnliches passiert, wenn die von Main genutzten Variablen unnötigerweise global deklariert werden. Werden Strukturen ohne Notwendigkeit global (und damit statisch) deklariert, wird auch sehr viel Platz (Code) und Zeit verschwendet. Daher sollten Strukturen nur als Typdefinitionen am Anfang deklariert werden. In Main wird dann eine Struktur initialisiert, die platzmäßig auf dem Stack angelegt wird. Sollen Unterprogramme mit dieser Struktur arbeiten, so kann dem Unterprogramm einfach der Zeiger auf die Struktur übergeben werden. Der Vergleich zweier Werte aus einer lokalen Struktur sieht wie folgt aus: 0x00000E7A 4668 MOV r0,sp ; Stackpointer laden

0x00000E7C 8AC1 LDRH r1,[r0,#0x16] ; Stackpointer + Offset Wert1 nach R1

0x00000E7E 8A80 LDRH r0,[r0,#0x14] ; Stackpointer + Offset Wert2 nach R0

0x00000E80 4281 CMP r1,r0 ; Vergleich R0 & R1

0x00000E82 D025 BEQ 0x00000ED0 ; Springe nach Adresse wenn gleich

Die globale Struktur benötigt mehr Code: 0x00000E9C 482B LDR r0,[pc,#172] ; @0x00000F4C

0x00000E9E 8880 LDRH r0,[r0,#0x04]

0x00000EA0 492A LDR r1,[pc,#168] ; @0x00000F4C

0x00000EA2 8809 LDRH r1,[r1,#0x00]

0x00000EA4 4288 CMP r0,r1

0x00000EA6 D018 BEQ 0x00000EDA