Programmierkurs für PIC-Mikrocontroller in C (CC5X Compiler)
Teil 2

Bit-Operationen

ODER - Funktion
Zweck der Funktion ist es, bestimmte Bits in einem Byte zu setzen, und dabei andere Bits nicht zu beeinflussen.
a = 0b.0000.1000; // a=8
b = 0b.0000.0101; // Bit 0 und 2 setzen
a ||= b ; // "a" und "b" ODER-verknüpfen und das Ergebnis in "a" speichern
//__0b.0000.1101 //Dies ist Ergebnis in "a"

Anstatt “ a ||= b “ kann man auch „ a = a || b “ oder „ a = a || 0b.0000.0101 “ schreiben.

UND - Funktion
Zweck der Funktion ist es, bestimmte Bits in einem Byte zurückzusetzen, und dabei andere Bits nicht zu beeinflussen.

a = 0b.0100.1010;
a &= 0b.1111.0011;

Hier werden Bits 2 und 3 zurückgesetzt. Das Ergebnis in "a" lautet 0b.0100.0010

Schiebeoperationen
Alle Bits in einem Register (Variable) werden in eine Richtung verschoben.

a = 0b.0000 .0001;
a = a << 1; //Schieben nach links um eine Stelle Ergebnis in "a" lautet 0b.0000 .0010
In dem obigen Beispiel wird die "1" nach Bit 1 geschoben und Bit 0 wird auf den Wert "0" gesetztt.
a = 0b.1000 .0000;
a = a >> 1; //Schieben nach rechts um eine Stelle Ergebnis in "a" lautet 0b.0100 .0000

Das Bit 6 erhält den vorherigen Wert des Bit 7 und Bit7 wird "0".

Ich habe herausgefunden, dass in dem Programm "CC5x" das Carry-Flag vor Schiebeoperationen auf 0 zurückgesetzt wird.
Man kann auch mit Hilfe einer Funktion schieben, die im Compiler eingebaut ist.

a = rr(a);			 // Alle Bits um eine Stelle nach rechts verschieben.
a = rl(a);			 // Alle Bits um eine Stelle nach links verschieben. 
Dabei wird auch das Carry-Flag beeinflusst, indem "durch" das Carry-Flag geschoben wird :
- beim Rechts-Schieben erhält Bit 7 den Wert des Carry-Flags und das Carry-Flag erhält den Wert von Bit 0.
- beim Links-Schieben erhält Bit 0 den Wert des Carry-Flags und das Carry-Flag erhält den Wert von Bit 7 .

Flags sind spezielle Bits, dessen Wert automatisch bei einigen Operationen geändert wird.
"Carry" ist ein Übertrags-Flag und wird immer gesetzt, wenn ein 8-Bit-Register überläuft (255+5 = 4).
"Zero" wird immer gesetzt, wenn ein 8-Bit-Register nach einer mathematischer Operation den Wert 0 annimmt (250+6=0).
Flags sind wichtig bei Programmierung mit Assembler, in C werden sie automatisch berücksichtigt.

Als Übung machen wir ein Lauflicht-Programm.

Unser Unterprogramm mit Zeitschleife wird zur Funktion. Diese Funktion erhält einen Wert. Den übergebenen Wert finden wir in der Variable "ms". Die wird in "void pause(uns16 ms)" als 16 Bit Variable deklariert, d.h. sie kann Werte im Bereich 0...65535 annehmen. "uns" -heißt "Unsigned", also eine Zahl ohne Vorzeichen, man kann also keine negativen Zahlen in "ms" speichern.


// Lauflicht,  Taktfrequenz 4MHz

#include <C:\cc5\16F84.h>		// Prozessor-Typ definieren
#pragma config |= 0b.1111.1111.0010	// Konfigurations-Wort

void pause(uns16 ms)			// Unterprogramm zum Abwarten einer
{					// angegebenen Anzahl von Millisekunden
while(ms)			// Schleife verlassen, wenn ms=0 ist
	{
	OPTION = 2;		// Vorteiler auf 8 einstellen
	TMR0 = 131;		// 125 * 8 = 1000 (= 1 ms)
	while (TMR0);		// Abwarten einer Millisekunde
	ms--;			// "ms" mit jeder Millisekunde erniedrigen
	}
}

void main(void)				// Hier beginnt das Hauptprogramm
{
char i;
TRISB = 0b.0000.0000;		// PortB als Ausgang definieren
PORTB = 0;
Sprungmarke:
PORTB.0 = 1 ;			//  Bit 0 von PortB setzen
pause(500);			// LED 0 halbe Sekunde leuchten lassen
for (i=0 ;i<7 ;i++)		// folgende Befehle 8-mal wiederholen
	{
	PORTB = PORTB << 1 ;	// 1 kommt ins nächste Bit links
	pause(500);		// jede LED  halbe Sekunde leuchten lassen
	}
PORTB = 0;			// Alle LEDs löschen
goto Sprungmarke;		// Alles wiederholen
}
Bits Maskieren
An PortA sind 4 Schalter angeschlossen. Wir möchten die Schalterzustände als Zahl (2^4=16 Zustände) darstellen. Das Problem ist, dass PortA ein 8-Bit-Register ist und die Bits 4..7 als "1" gelesen werden. Somit erhält man Zahlen höher als 16, wenn man PortA ausliest.. Die Bits 4..7 muss man also von Hand auf "0" setzen.
char Schaltern ;
Schaltern = PORTA & 0b. 0000.1111 ; 

Nachdem wir in der Variable "Schalter" eine Zahl haben, entsprechend dem Schalterzustand, können wir es mit Hilfe folgender Anweisung auswerten:

switch(Schaltern)
{
	case 1 :	// wenn nur Schalter 1 betätigt ist
			// tu dies
	break;

	case 2 :	// wenn nur Schalter 2 betätigt ist
			// tu das
	break;

	case 15 :	// wenn  alle Schalter  betätigt sind
			// tu jenes
	break;
}

Hier ist ein Beispiel: ein komplizierteres Lauflicht zum Herunterladen. Mit Auswertung der Schalterzustände und verschiedener Lichteffekte.

Programmstruktur und Interruptanwendung

Grundsetzlich gint es 3 arten von Steuerungen.

1 Ablaufsteuerung
Programm Abarbeitet befehle nacheinander, Zeitschleifen können verwendet werden um Ablaufzeiten einzustellen.

2 EreignisOrientirteSteuerung
Programm läuft in einer Schleife wo alle mögliche Ereignisse abgefragt werden. Wenn ein Ereignis passiert Schleife
wird verlassen, entsprechenden Handlungen ausgeführt. Dann wird wieder zurück zu Schleife übergegangen um weitere Ereignisse abzufangen.

3 Mischung von Ablaufsteuerung und EreignisOrientirteSteuerung.
Erst hier braucht man ein Interrupt wo Ablaufsteuerung durch ein Ereignis unterbrochen werden soll und man nicht weis wann dieser Ereignis passiert.
Bei nicht zeitkritischen Programmen wird Interrupt (Ereignis) direkt nach dem Eintritt abgearbeitet.
Bei zeitkritischen Programmen Iterruptroutine erkennt welche genau Ereignis passiert ist und setzt eine Variable(Flag). Das Hautprogramm an bestimmten Stellen abfragt ob der Flag gesetzt ist und falls "ja" führt entsprechende befehle aus.
Wenn Programm nicht mahl für 20 Mikrosekunden unterbrochen werden darf (softwaremäßige serielle Übertragung ist der Fall) sollen alle Inteerups vor dieser Routine ausgeschaltet werden und nach der Routine
wieder eingeschaltet. So wird auch Interrupt während Routine empfangen aber erst nach Beendigung abgearbeitet.

Wenn etwas ohne Interruptanwendung realisierbar ist empfehle ich auch keine Interrupts zu verwenden. Weil Interrupts sporadische und schwer erklärbare Fehlern verursachen können oder Zeitplan des Programms durcheinander bringen können.

Interrupt
Mit ganz einfachen Worten : Wenn ein bestimmtes Ereignis passiert, dann wird das Hauptprogramm unterbrochen und eine speziell zu diesem Ereignis zugewiesenes Unterprogramm ausgeführt. Danach wird das Hauptprogramm fortgesetzt. Unterprogramme, die Interrupts bearbeiten, fangen immer bei Adresse 0x0004 an. Wenn ein Interrupt passiert, wird das Programm an Adresse 0x0004 aufgerufen. Seine Aufgabe ist es, zu unterscheiden, welches Ereignis aufgetreten ist und entsprechend zu handeln. Durch Setzen von speziellen Flags kann man Interrupts einschalten. Wenn ein Interrupt aufgerufen wird, kann man durch andere Flags abfragen, welcher Interrupt genau aufgetreten ist .

Beispiel:

Pegeländerung an RB0 , steigende Flanke.

GIE=1;		// - Interrupts überhaupt erlauben.

INTE=1;		// - Interrupt von RB0 erlauben.

INTEDG=1;	// - Bei steigender Flanke.

Beim Programmieren in C ist es gegenüber Assembler vorteilhaft, dass man sich nicht um Flags und Speicherbanke kümmern muss. Man braucht lediglich aus dem Datenblatt des Mikrocontrollers die Flag-Namen und ihre Funktion zu entnehmen. Wenn nur ein Interrupt erlaubt ist, dann ist es nicht erforderlich, die Interruptursache zu ermitteln, denn es ist ja nur eine möglich.


#include <C:\cc5\16F84.h>			// Prozessor-Typ definieren
#include "C:\cc5\int16CXX.H"			// Ist notwendig um Interrupts zu nutzen
#pragma config |= 0b.1111.1111.0010		// Konfigurations-Wort
bit beep @ PORTB.1 ;				// Pin 7 erhält Name "beep"

void Ton(void);			// Unterprogramm deklarieren, damit sie in 
				// "InterruptRoutine" bekannt wird
char i;				// 8-Bit Variable (0...255)

#pragma origin 4		// Der folgende Code steht ab Adresse 4

interrupt InterruptRutine(void)		// Interruptroutine
{
int_save_registers		// W, STATUS (und  PCLATH) retten
if (INTF)			// Ursache des Interrupts feststellen
	{
	Ton();			// Reaktion auf Interrupt ist "Piepsen"
	INTF = 0;		// Flag zurücksetzen
	}
int_restore_registers		// W, STATUS (und PCLATH) Wiederherstellen
}

void Ton(void)				// Erzeugung des Piepstones mit 500Hz, Dauer 0,5sek
{
for(i=0;i<250;i++)
	{
	beep=1;			// Impuls Generieren
	OPTION = 2;		// Vorteiler auf 8 einstellen
	TMR0 = 131;             // 125 * 8 = 1000 (= 1 ms)
	while (TMR0);           // warten, bis TMR0=0 wird
	beep=0;			// Pause Generieren
	OPTION = 2;             // Vorteiler auf 8 einstellen
	TMR0 = 131;             // 125 * 8 = 1000 (= 1 ms)
	while (TMR0);           // warten, bis TMR0=0 wird
	}
}

void main(void)				// Hier beginnt das Hauptprogramm
{
TRISB = 0b.1111.1101;		// Pin 6 als Ausgang deklarieren
INTEDG=0;			// Fallende Flanke am RB0
GIE=1;				// Interrupts erlauben
INTE=1;				//  Interrupt von RB0 erlauben.

Ton();				// Ein Mal Piepsen beim Einschalten
Sprungmarke:			// Unendliche Programm-Schleife
goto Sprungmarke;
}					// Ende des Hauptprogramms

Die Hauptprogramm kann an beliebiger Stelle durch den Interrupt unterbrochen werden. Dabei können Flags z. B. "Carry" oder "Zero" durch die Interruptroutine geändert werden und dadurch im Hauptprogramm zu Fehler führen. Damit das nicht passiert, müssen einige Register am Anfang der Interruptroutine gespeichert werden und am Ende wiederhergestellt werden. Diesen Vorgang erledigen die Befehle "int_save_registers" und "int_restore_registers" Diese Befehle sind in der "int16CXX.H" - Datei definiert.