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