C für PICs: PIC spezifisches



zurück zu C für PICs , C-Compiler , PIC-Prozessoren , Elektronik , Homepage

C für PICs
Grundlagen von C
Variablen
Funktionen
Operationen
Programmablaufsteuerung
Arrays und Strings
Pointer
Strukturierte Typen
PIC-spezifisches


zurück zu C für PICs


PIC spezifisches

Nun haben wir eine grobe Vorstellung von der Programmiersprache C , wie ein Compiler Quelltext in ein HEX-File wandelt und wie man den C18-Compiler einsetzt.
Normalerweise wird C auf einem Computer eingesetzt, der aus der Standard-Eingabe Daten bekommt und an die Standard-Ausgabe daten sendet. Aber was mache ich damit nun in einem PIC, der weder Tastatur noch Drucker oder Terminal besitzt?

Und damit kommen wir zurück zur Hardware.




Konfiguration


Ein PIC braucht Konfigurationseinstellungen, die beim "Brennen" in den PIC geschrieben werden. Sie sind für die grundlegende Funktion des PICs essentiell, und können nicht vom normalen Programmcode während der Laufzeit verändert werden.
Aus diesem Grunde muss diese Konfiguration am Besten schon in den C-Quelltext hineingeschrieben werden. Damit der C-Compiler dann auch erkennt, dass es sich um Konfigurationseinstellungen handelt, werden sie durch

#pragma config

eingeleitet. Die Konnfiguration ist sehr vom PIC-Typ abhängig, so dass ein Blick in das Datenblatt unerlässlich ist. Wie die gewünschte Konfiguration dann in den C-Quelltext geschrieben wird, steht in der Dokumentation zum Compiler. So gibt es zum C18-Compiler ein Dokument namens PIC18-Config-Settings-Addendum_51537x.pdf (anstelle des x steht ein Serienbuchstabe). Es beschreibt die dem C18 bekannten zum PIC-Typ passenden Konfigurationseinstellungen.
In aktuellen MPLAB-Versionen findet man die Konfigurationsoptionen ganz praktisch unter "Help - Topics - PIC18 Config Settings".

Der folgende Codeabschnitt zeigt exemplarisch eine Konfiguration für einen PIC18F2550.

// Takt vom 8 MHz Resonator
/** Configuration ********************************************************/
#pragma config PLLDIV = 2, CPUDIV = OSC1_PLL2, USBDIV = 2, FOSC = HSPLL_HS    //CPU=48 MHz
#pragma config PWRT   = ON
#pragma config BOR    = OFF         // BrownOutReset: off
#pragma config VREGEN = ON          // USB Voltage Regulator Enable:
#pragma config WDT    = OFF         // Watchdog Timer: off
#pragma config MCLRE  = ON          // MCLR Enable
#pragma config PBADEN = OFF         // PORTB<4:0> pins are configured as digital I/O on Reset
#pragma config LVP    = OFF         // Low Voltage ICSP: off

Damit ist die generelle Schreibweise klar, aber die Konfiguration selbst ist natürlich je nach PIC-Typ und Anwendung völlig verschieden.

nach oben


SFR (special function register)

Viele Funktionen eines PICs (und ganz besonders die Hardware der diversen Interfaces) werden bekanntlich mit Hilfe von Registern (SFR - special function register) gesteuert, die im normalen Adressbereich des Datenspeichers liegen. Auch in einem C-Programm kommt man natürlich um die Benutzung dieser Register nicht herum. Damit man nicht direkt mit den RAM-Adressen herumhantieren muss, existiert für jeden PIC-Typ ein spezielles include-File, in dem für alle SFR geeignete Variablennamen definiert sind. Dieses File sollte also unbedingt mittels include in das Programm eingebunden werden. Für den PIC18F2550 heist das passende File z.B.  p18f2550.h.
Um das Einbinden des richtigen Files etwas einfacher zu machen, gibt es das universelle include-File p18cxxx.h. Es sorgt dafür, dass automatisch das passende File für den PIC-Typ eingebunden wird, der im MPLAB-Projekt ausgewählt wurde.

#include <p18cxxx.h>

nach oben


IO-Pins

Die Grundlagen der I/O-Pins wurden an anderer Stelle bereits beschrieben. Wie konfiguriert und benutzt man I/O-Pins nun in der Sprache C?

Der obige Link bezieht sich auf I/O-Pins der PIC16-Familie. Wir programmieren aber die fortschrittlicheren PICs ab der PIC18-Familie in C. Deren I/O-Pins haben noch einen kleinen aber feinen Vorteil - das LATx-Register. (Latch) Was das bringt, das sehen wir weiter unten.

Die IO-Pins sind in  bis zu 8 Bit breiten Ports organisiert. Jeweils ein 8-Bit-Register dient den Schreiben zum Port wie auch dem Lesen vom Port. Diese Register heißen PORTA, PORTB, PORTC usw. für die Ports A, B und C. Ich verwende im weiteren beispielhaft das PORTB-Register des Ports B, die Aussagen gelten aber für alle PORTx. Schreibt man eine Zahl in das Register PORTB, dann wird dieser Zahlenwert in ein Register (Latch) geschrieben, dessen Ausgänge über Treiberstufen die Ausgangspins des PIC ansteuern. PORTB steuert also maximal 8 Pins. Eine 0 in einem Bit entspricht der Ausgangsspannung 0V (Vss) während eine 1 dem Vdd-Pegel (z.B. 5V) entspricht.

Damit PORTB die Pins steuern kann, muss aber auch der Ausgangstreiber eingeschaltet sein. Die 8 Ausgangstreiber der 8 Pins des PORTB werden durch die 8 Bits des Registers TRISB (TRISA, TRISC ...) gesteuert. Jedem Treiber ist dabei ein Bit in TRISB zugeordnet. Nur wenn das Bit den Wert 0 hat, dann ist der Treiber auch eingeschaltet, und das Pin hat den in PORTB gespeicherten Wert. Ist das Bit dagegen auf 1, dann ist der Treiber inaktiv. In diesem Fall kann das Pin als Eingang benutzt werden, da ein externes Signal das Pin auf 0 oder 1 ziehen kann, ohne das der Treiber dabei stören würde. Im Grundzustand (power on reset) sind alle TRIS-Bits auf 1 gesetzt, also alle Pins als Eingang konfiguriert.
Aufbau der Ports
Greift man lesend auf PORTB zu, dann werdern die Pegel an den 8 Pins des Ports B in 8 Bit gewandelt, und diese 8 Bit als Zahlenwert gelesen. (Man liest also nicht das Ausgaberegister sondern die wirklichen Portpegel.) Von Pins, die auf Input stehen, liest man dabei den Pegel, der von außen eingespeist wird. Von Pins, die auf Output stehen, liest man natürlich den Wert des Ausgangsregisters, da der ja dann am Pin ausgegeben wird.

Man kann auch das Ausgangsregister lesen. Es hat den Namen LATB (ensprechend LATA, LATC ...). In diesem Fall liest man exakt den Wert zurück, den man zuvor in PORTB geschrieben hatte. Die Input-Pins haben nun keinen Einfluss. Wozu das gut ist, sehen wir in Kürze.
Man kan LATB auch beschreiben, das ist aber mit dem Beschreiben von PORTB identisch.

Die nebenstehende Skizze zeigt das Prinzip für ein beliebiges Portpin.

Der folgende Auszug stammt aus dem include-File p18f2550.h, und zeigt die Definition von PORTB, LATB und TRISB exemplarisch für das Port B des PIC18F2550. LATB und TRISB sind als Structure definiert, und bestehen aus jeweils 8 einzelnen Bits. Das gilt prinzipiell auch für PORTB, nur hat man hier noch mit Hilfe einer Union-Definition für einige der Bits alternative Bezeichner definiert, was momentan aber nicht so wichtig ist.

....
extern volatile near unsigned char       PORTB;

extern volatile near union {
  struct {
    unsigned RB0:1;
    unsigned RB1:1;
    unsigned RB2:1;
    unsigned RB3:1;
    unsigned RB4:1;
    unsigned RB5:1;
    unsigned RB6:1;
    unsigned RB7:1;
  };
  struct {
    unsigned INT0:1;
    unsigned INT1:1;
    unsigned INT2:1;
  };
  struct {
    unsigned :5;
    unsigned PGM:1;
    unsigned PGC:1;
    unsigned PGD:1;
  };
} PORTBbits;
....
extern volatile near unsigned char       LATB;
extern volatile near struct {
  unsigned LATB0:1;
  unsigned LATB1:1;
  unsigned LATB2:1;
  unsigned LATB3:1;
  unsigned LATB4:1;
  unsigned LATB5:1;
  unsigned LATB6:1;
  unsigned LATB7:1;
} LATBbits;
....
extern volatile near unsigned char       TRISB;
extern volatile near struct {
  unsigned TRISB0:1;
  unsigned TRISB1:1;
  unsigned TRISB2:1;
  unsigned TRISB3:1;
  unsigned TRISB4:1;
  unsigned TRISB5:1;
  unsigned TRISB6:1;
  unsigned TRISB7:1;
} TRISBbits;
....


Damit kennt C18 nun schöne Namen für die Portregister und deren Bits. Das genügt dem Compiler, um damit einen Assemblertext zu erstellen. Dieser wird dann dem Assembler MPASM zur Weiterverarbeitung vorgelegt. MPASM benutzt nun sein include-File (z.B. P18F2550.INC für den PIC18F2550) um diesen Namen reale Adressen und Werte zuzuordnen. Der folgende Ausschnitt zeigt die entsprechende Abschnitte aus dem Assembler-include-File.

; aus P18F2550.INC
....
PORTA            EQU  H'0F80'

PORTB            EQU  H'0F81'
PORTC            EQU  H'0F82'
PORTE            EQU  H'0F84'
LATA             EQU  H'0F89'
LATB             EQU  H'0F8A'
LATC             EQU  H'0F8B'
DDRA             EQU  H'0F92'
TRISA            EQU  H'0F92'
DDRB             EQU  H'0F93'
TRISB            EQU  H'0F93'
DDRC             EQU  H'0F94'
TRISC            EQU  H'0F94'
....
;----- PORTB Bits -----------------------------------------------------
RB0              EQU  H'0000'
RB1              EQU  H'0001'
RB2              EQU  H'0002'
RB3              EQU  H'0003'
RB4              EQU  H'0004'
RB5              EQU  H'0005'
RB6              EQU  H'0006'
RB7              EQU  H'0007'

INT0             EQU  H'0000'
INT1             EQU  H'0001'
INT2             EQU  H'0002'

PGM              EQU  H'0005'
PGC              EQU  H'0006'
PGD              EQU  H'0007'
....
;----- LATB Bits -----------------------------------------------------
LATB0            EQU  H'0000'
LATB1            EQU  H'0001'
LATB2            EQU  H'0002'
LATB3            EQU  H'0003'
LATB4            EQU  H'0004'
LATB5            EQU  H'0005'
LATB6            EQU  H'0006'
LATB7            EQU  H'0007'
....
;----- TRISB Bits -----------------------------------------------------
TRISB0           EQU  H'0000'
TRISB1           EQU  H'0001'
TRISB2           EQU  H'0002'
TRISB3           EQU  H'0003'
TRISB4           EQU  H'0004'
TRISB5           EQU  H'0005'
TRISB6           EQU  H'0006'
TRISB7           EQU  H'0007'
....


Besonderheit Bitmanipulation

Da bei I/O-Ports oft einzelne Pin Steuersignale ausgeben, ist es oft nötig, einzelne Pins auf 0 oder 1 zu setzen. Dabei sollten die anderen Pins des Ports nicht verändert werden. In Assembler erledigt man das mit BSF- oder BCF-Befehlen. Die Hardware des PIC erlaubt es aber nicht, nur ein Bit eines Registers zu verändern. Deshalb muss erst das ganze Register ausgeselen werden, dann verändert man ein Bit (in der ALU) und schreibt das manipulierte Byte wieder in das Register zurück. Dabei tappt man schnell in die In/Out-Falle, wenn man so eine Bitmanipulation auf ein PORT-Register anwendet. Das kann nicht passieren, wenn man stattdessen die Bitmanipulation mit dem zugehörigen LAT-Register erledigt. Aus diesem Grunde sollten sämtliche Manipulationen von Portpins immer mit Hilfe des LAT-Registers durchgeführt werden. Nur zum Lesen der Pegel von IO-Pins sollte das PORT-Register Verwendung finden.

LATBbits.LATB0 = 0;

nach oben


Interfaces

Neben den gerade besprochenen einfachen IO-Pins haben PICs oft eine ganze Reihe von weiteren Interfaces als Hardwaremodul integriert. Deren Steuerung erfolgt durch das Beschreiben/Lesen von Steuerregistern, die im Daten-Adressbereich liegen (SFR).
Zur Vereinfachung des Zugriffs auf Interfaces liefert Microchip mit dem C-Compiler auch spezielle Bibliotheken aus.


nach oben


Interrupts

PIC18F-Typen haben zwei Interrupts, einen mit hoher Priorität (High) und einen mit niedrigerer Priorität (Low). Den unterschiedlichen Interruptquellen kann zugeordnet werden, welchen der beiden Interrupts sie auslösen sollen. Bei einem High-Interrupt springt der Prozessor zur Programmspeicheradresse 0x0008 und beim Low-Interrupt zur Adresse 0x0018. In C18 lassen sich Interruptroutinen nun z.B. wie folgt schreiben:

...
/** I N T E R R U P T S  *******************************************/

#pragma interruptlow InterruptHandler
void InterruptHandler (void)
{
    ....
    ....
    ....
}

#pragma code _HIGH_INTERRUPT_VECTOR = 0x000008
void _high_ISR (void)
{
    _asm
        goto InterruptHandler       // Sprung zur Interruptroutine
    _endasm
}

#pragma code _LOW_INTERRUPT_VECTOR = 0x000018
void _low_ISR (void)
{
    _asm
        goto InterruptHandler       // Sprung zur Interruptroutine
    _endasm
}
#pragma code
...

Man erkennt, dass beide Interrupts (0x000008 und 0x000018) einen Sprung (mit einem Assemblerbefehl) zur eigentlichen Interruptroutine auslösen, die in diesem Fall InterruptHandler heißt.. Ich benutze im Beispiel die selbe Interruptroutine für beide Interrupttypen (High und Low) man kann aber natürlich auch zwei separate Routinen verwenden. Die andere Routine müsste dann mit #pragma interrupthigh eingeleitet werden.
Ein Retten und Zurückschreiben von Prozessorregistern (wie es in Assembler an Anfang bzw. Ende der Interruptroutine unbedingt nötig wäre) muss hier nicht extra programmiert werden. Aufgrund der #pragma interruptlow Direktive kümmert sich der Compiler selbstätig darum.

Diverse PIC18-Typen haben einen Bug, der nur bei #pragma interrupthigh auftritt. Aus diesem Grunde verzichte ich in der Regel auf die unterschiedlichen Interruptprioritäten, und verwende (wie oben gezeigt) nur einen Interrupthandler für beide Interruptvektoren. Wenn der eigene Chip von diesem Problem betroffen ist (siehe Errata-Dokument), dann leitet man einfach auch die Interruptroutine des High-Interrupts mit #pragma interruptlow ein.

nach oben


Bibliotheken

Zum C-Compiler gehören eine ganze Reihe von Bibliotheken, die der Anwender in sein eigenes C-Projekt einbinden kann. Man kann sie grob in vier Gruppen einteilen:
Eine Beschreibung aller Funktionen findet man im Dokument MPLAB-C18-Libraries_51297x.pdf  (das x steht für einen Serienbuchstaben, der sich mit der Dokumentversion ändert).

Mathematische Funktionen
C stellt jenseits der Grundrechenarten natürlich noch eine ganze Reihe von mathematischen Funktionen zur Verfügung. Um sie nutzen zu können, muss man allerdings die Standard-Mathematik-Bibliothek einbinden. Das erfolgt durch:
#include math.h

-
acos(x) Arcuscosinus (der inverse Cosinus)
-
asin(x) Arcussinus (der inverse Sinus)
-
atan(x) Arcustangens (der inverse Tangens)
-
atan2(y, x)
Ermittelt den Winkel eines Vektors aus der Länge seiner Projektionen auf die Koordinatenachsen x und y
-
ceil(x) Ermittelt den kleinsten Ganzzahlwert, der größer oder gleich einer Zahl ist
-
cos(x) Cosinus
-
cosh(x) Sinus Hyperbolicus
-
exp(x) Ermittelt e hoch x
-
fabs(x) Absolutwert
-
floor(x) Größter enthaltener Ganzzahlwert
-
fmod(x, y) Ermittelt x Modulo y
-
frexp
-
ieeetomchp(x) Wandlung aus dem IEEE-754 32-bit Fließkommaformat in das Microchip 32-bit Fließkommaformat
-
ldexp(x, n)
Ermittelt x * (2 hoch n)
-
log(x) Natürlicher Logarithmus
-
log10(x) Logarithmus zur Basis 10
-
mchptoieee(x) Wandlung aus dem Microchip 32-bit Fließkommaformat in das IEEE-754 32-bit Fließkommaformat
-
modf(x)
-
pow CErmittelt x hoch y
-
sin(x) Sinus
-
sinh(x) Sinus Hyperbolicus
-
sqrt(x) Quadratwurzel
-
tan(x) Tangens
-
tanh(x) Tangens Hyperbolicus


interne Funktionen
Eine Reihe weiterer Bibliotheksfunktionen beschäftigen sich mit Zeichentypisierung (ctype.h), Datenumwandlung (stdlib.h), Speicher- und Stringmanipulationen (string.h), Warteschleifen (delays.h), Erkennung von Resetauslösern (reset.h) und Zeichenausgaben (diverse print- und put-Abkömmlinge) (stdio.h).


Hardware Interface-Funktionen
Es gibt Bibliotheken, für die diversen Hardware-Interfaces der PICs. Allerdings ist deren Benutzung nicht besonders einfach. In der Regel ist es nicht komplizierter, die Hardware auf Grundlage der Informationen aus dem Datenblatt des PIC "zu Fuß" anszusteuern, als sich in die Fülle von Funktionsparametern der Bibliotheken einzuarbeiten. Wer PICs noch nicht mit Assembler programmier hat, sollte diese Bibliotheken nutzen. Dagegen können "alte Assembler-Hasen" oft auch ganz gut ohne diese Bibliotheken auskommen.


Software Interface-Funktionen
Auch ohne spezielle Hardware lassen sich einige Interfaces per Software nachbilden. Ich habe das z.B. für die RS232-Schnittstelle und für I2C gezeigt. Die C-Bibliotheken bieten fertige Funktionen zur Emulation von SPI (sw_spi.h), I2C (sw_i2c.h) und UART (sw_uart.h). Außerdem unterstützen sie den Anschluss eines Dotmatrix-LCD-Displays (xlcd.h) sowie eines CAN-Controllers vom Typ MCP2510 (can2510.h).



--> zurück zu C für PICs
nach oben

zurück zu C für PICs , C-Compiler , PIC-Prozessoren , Elektronik , Homepage



Autor: sprut
erstellt: 01.10.2007
letzte Änderung: 23.10.2012