Aus RN-Wissen.de
Wechseln zu: Navigation, Suche
Laderegler Test Tueftler Seite

Source-Vergleich

Es sollen hier einige Beispiele gebracht werden, wie einige Dinge in verschiedenen Sprachen gelöst werden können. Es sollte nicht als Wettbewerb der Compiler gesehen werden, welcher denn nun der "Bessere" sei. Über Vorzüge und Nachteile der verschiedenen Sprachen gibt es durchaus kontroversielle Ansichten. Daran will ich hier gar nicht rütteln, über Geschmack kann man nicht streiten.


Hello, world

Ein ganz einfaches Programm, oft auch das erste, ist, den Text "Hello, world !" auf dem Terminal erscheinen zu lassen.

Was so trivial klingt, hat den Zweck, mehrere Dinge zu überprüfen:

  • Komme ich mit der Entwicklungsoberfläche zurecht
  • funktioniert das Kompilieren und das Übertragen eines Programms auf den Microcontroller
  • stimmen die Einstellungen Quartz und Baudrate
  • funktioniert die RS232-Verbindung mit dem PC
Natürlich ist der Text vollkommen egal, er hat sich ganz einfach eingebürgert und fast jeder 
weiß, was damit gemeint ist.

BasCom

Die folgenden vier Zeilen lassen ahnen, warum Bascom speziell für Einsteiger geradezu ein Segen ist:

 $Crystal=8000000
 $Baud=9600

 Print "Hello, world !"

 End

GCC

 #include <avr/io.h> 

 #define F_CPU 			8000000 
 #define USART_BAUD_RATE 	9600 
 #define USART_BAUD_SELECT 	(F_CPU/(USART_BAUD_RATE*16L)-1) 

 char cText[] = "Hello, world !\r\n";

 //-----------------------------------------------------
 void _writeChar (char c)
 {
     while (!(UCSRA & (1<<UDRE))) {} 
         UDR = c; 
 }
 //-----------------------------------------------------
 void _writeString (unsigned char *string) 
 { 
      while (*string) 
          _writeChar (*string++); 
 } 
 //-----------------------------------------------------
 void main()
 {
      UCSRB |= (1<<TXEN); 
      UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0); 
      UBRRL = (unsigned char) USART_BAUD_SELECT; 
      _writeString(cText); 

      // Endlossschleife: nichts mehr tun 
      while (1)
      {}
  }

Wenn jemanden der Aufwand bei GCC erschrecken sollte:

GCC hat keine Standard-Annahme darüber, wie und wo eigentlich der Output stattfinden sollte. Es ist für ihn nicht selbstverständlich, die UART zu verwenden. Dadurch muß natürlich mehr definiert werden. Dem BasCom kommt zugute, daß er die ganze Konfiguration mit AVR, RS232 und PC-Terminal erstmal als gegeben nimmt.


AVRStudio (Assembler)

 .NOLIST                    ; List-Output unterdrücken
 .INCLUDE <m32def.inc>       ; das gibt es für jeden Controllertyp
 .LIST                      ; List-Output wieder aufdrehen
 .CSEG                      ; was nun folgt, gehört in den FLASH-Speicher

 #define F_CPU  		8000000 
 #define USART_BAUD_RATE 	9600 

 ;------------------------------------------------------
 ;     Start Adresse 0000
 ;------------------------------------------------------
 RESET:
     jmp INIT           ; springen nach "INIT"

 ;------------------------------------------------------
 ;     ISR VECTORS
 ;------------------------------------------------------
 ;    .....    hier kommen dann die Sprungadressen für die Interrupts rein
 ;             brauchen wir aber jetzt nicht
 .ORG INT_VECTORS_SIZE    ; dadurch haben wir aber trotzdem für die Vektoren Platz gelassen
 INIT:  
 ;------------------------------------------------------
 ;     INITIALIZE
 ;------------------------------------------------------
     ldi r24,high(RAMEND)     ; Stack Pointer setzen 
     out SPH,r24              ; "RAMEND" ist in m32def.inc (s.o.) festgelegt
     ldi r24,low(RAMEND)      ; 
     out SPL,r24              ;
 
        ldi	r24, (F_CPU/(USART_BAUD_RATE * 16)-1) 
	out	UBRRL,r24
	ldi	r24, (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0)
	out	UCSRC,r24
	ldi	r24,(1<<TXEN) |(1<<RXEN)
	out	UCSRB,r24
 ;------------------------------------------------------
 ;   HAUPTSCHLEIFE
 ;------------------------------------------------------
 Hauptschleife: 
	ldi	ZL,low(Hello << 1)	; ZL:ZH ASCIZ String addr
	ldi	ZH,high(Hello << 1)	; ZL:ZH ASCIZ String addr
	call	printflash
	call	PrintCrLf
 ;------------------------------------------------------
 ;   ENDE
 ;------------------------------------------------------
 Ende:  
        rjmp Ende

 ;------------------------------------------------------
 ;   PRINT String from Flash
 ;------------------------------------------------------
Printflash:
	call	GetFlash	; get byte to R24
	breq	PrintXit	; zero  (string end) exit
	rcall	PrintR24	; send byte
	rjmp	Printflash	; loop
PrintXit:
	ret
; -----------------------------------------------------------------------
;  PRINT  CRLF
; -----------------------------------------------------------------------
PrintCrLf:
	ldi	r24,0x0D
	rcall	PrintR24
	ldi	r24,0x0A
; -----------------------------------------------------------------------
;  PRINT  R0
; -----------------------------------------------------------------------
PrintR24:
	sbis	UCSRA,UDRE
	rjmp	PrintR24
	out	UDR,r24
	ret
; -----------------------------------------------------------------------
;  Get one Flash Char
; -----------------------------------------------------------------------
GetFlash:
	lpm
	adiw	ZL,0x0001
	mov	r24, r0
	and	r24, r24
	ret
Hello:
	.db "Hello, world !", 0x00 , 0x00 



Tastatur-Echo

Wenn das vorhergegangene Beispiel funktioniert hat, wird man natürlich auch überprüfen wollen, ob auch die umgekehrte Richtung VOM Terminal klappt. Die einfachste Methode ist es, ganz einfach jedes Zeichen, das eingegeben wird, sofort zurückzusenden.

BasCom

 $Crystal=8000000
 $Baud=9600

 DIM Zeichen as Byte

 Do
   inputbin Zeichen
   Printbin Zeichen
 Loop
 End

GCC

 #include <avr/io.h> 

 #define F_CPU 			8000000 
 #define USART_BAUD_RATE 	9600 
 #define USART_BAUD_SELECT 	(F_CPU/(USART_BAUD_RATE*16L)-1) 

 char bZeichen;
 
 //-----------------------------------------------------
 void main()
 {
      UCSRB = (1<<RXEN)|(1<<TXEN); 
      UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0); 
      UBRRL = (unsigned char) USART_BAUD_SELECT; 
      while (1) 
      { 
         while ( !(UCSRA & (1<<RXC)) ) {}
         bZeichen = UDR; 
         while (!(UCSRA & (1<<UDRE)))  {} 
         UDR = bZeichen; 
      }
 }

AVRStudio (Assembler)

 .NOLIST                    ; List-Output unterdrücken
 .INCLUDE <m32def.inc>       ; das gibt es für jeden Controllertyp
 .LIST                      ; List-Output wieder aufdrehen
 .CSEG                      ; was nun folgt, gehört in den FLASH-Speicher

 #define F_CPU  			8000000 
 #define USART_BAUD_RATE 	9600 

 ;------------------------------------------------------
 ;     Start Adresse 0000
 ;------------------------------------------------------
 RESET:
     jmp INIT           ; springen nach "INIT"


 .ORG INT_VECTORS_SIZE    ; dadurch haben wir für die Vektoren Platz gelassen
 INIT:  
 ;------------------------------------------------------
 ;     INITIALIZE
 ;------------------------------------------------------
     ldi r24,high(RAMEND)     ;Stack Pointer setzen 
     out SPH,r24              ; "RAMEND" ist in m32def.inc (s.o.) festgelegt
     ldi r24,low(RAMEND)      ; 
     out SPL,r24              ;


	ldi	r24, (F_CPU/(USART_BAUD_RATE * 16)-1) 
	out	UBRRL,r24
	ldi	r24, (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0)
	out	UCSRC,r24
	ldi	r24,(1<<TXEN) |(1<<RXEN)
	out	UCSRB,r24
 
 ;------------------------------------------------------
 ;   HAUPTSCHLEIFE
 ;------------------------------------------------------
 Hauptschleife: 
	sbis	UCSRA,RXC
	rjmp	Hauptschleife
 WaitUDR:
	sbis	UCSRA,UDRE
	rjmp	WaitUDR
	out	UDR,r24
    rjmp 	Hauptschleife         ; immer wiederholen 

Bemerkungen

Hier ist der Unterschied des Codes nicht mehr so groß. Ganz klar, hier konnte BasCom wegen der Aufgabenstellung nicht auf eine vorgefertigte komplexe Funktion zurückgreifen.
Bei Assembler und GCC fallen große Ähnlichkeiten auf, von den Befehlscodes mal abgesehen.


Signallänge messen

Sehr oft ist es bei Controllern nötig, die Länge eines kurzen Impulses zu messen. Zum Beispiel wenn man einen RC-Empfänger (Modellbauempfänger) an ein Controllerboard anschließt. Ein RC-Empfänger sendet alle 20 Millisekunden ein High-Impuls von 1 bis 2 Millisekunden Länge aus. Die Länge dieses Impules bestimmt die Position des Steuerknüppels. Man braucht also nur diese Impulsdauer zu messen, um ein Fernsteuersignal auszuwerten.

BasCom

Bascom vefügt für diesen Zweck über den PULSEIN-Befehl, daher wird ein Programm extrem kurz. Die Messung erfolgt in etwa in 100 Abstufungen, da Pulsein in 10 µSek Schritten die Zeit ermittelt (läßt sich in Libary ändern)

 $regfile = "m32def.dat"   
 $framesize = 32
 $swstack = 32
 $hwstack = 32
 $crystal = 16000000        'Quarzfrequenz
 $baud = 9600

 Dim Rckanal As Word

 Do
   Pulsein Rckanal , Pind , 2 , 1    'Messung Zeit zwischen 1 und 0 Pegel
   Print "RC Signal: " ; Rckanal ; "0 uS"
   Wait 2
 Loop
 End

GCC

Das GCC-Beispiel, das die gleiche Aufgabe erfüllt:
Das ist nun nicht so einfach, da "pulsein" ja eine komplexe Library-Funktion von BasCom ist, die für andere Sprachen normalerweise nicht verfügbar ist. Da BasCom für die Zählung keinen Timer verwendet, sondern die Maschinencycles als Maßstab nimmt, ist es in vergleichbarer Form eigentlich nur mit "inline-assembler" zu lösen, in Abhängigkeit von der Quartz-Frequenz. Trotzdem soll zu Demonstration die Sache mit möglichst C-Style Mitteln durchgezogen werden
Im folgenden Beispiel ist aber nur die "PulseIn" Funktion ausgeführt, das "print" des Ergebnisses hatten wir ja schon.

#define BAUD_RATE    9600 

#include <stdlib.h> 
#include <stdio.h> 
#include <avr/io.h> 


#define PULS_C_LEV_LOW	0          // Die "LOW" Zeit des Pins soll gemessen werden 
#define PULS_C_LEV_HIGH	0xFF       // Die "HIGH" Zeit des Pins soll gemessen werden 

typedef struct {
volatile unsigned char bPin;              // PINx Register
volatile unsigned char bDdr;              // DDRx Register
volatile unsigned char bPort;             // PORTx Register
} IO_REG;


unsigned short PulseIn(IO_REG* pPort, unsigned char PinNr, unsigned char Level);


int main (void) 
{ 
unsigned short RcKanal;

	RcKanal = PulseIn((IO_REG*)&PIND, 2, PULS_C_LEV_HIGH);
	return 0;
}

Hier ist ersteinmal die Verwendung der Funktion dargestellt. Es wird

  • das Port mit Adresse angegeben (PIND)
  • die Pin-Nummer als Zahl 0-7 (2)
  • Der Level, der gezählt werden soll (PULS_C_LEV_HIGH)
// -------------------------------------------------------------------------------------
// 
// -------------------------------------------------------------------------------------
unsigned short PulseIn(IO_REG* pPort, unsigned char PinNr, unsigned char Level)
{
unsigned char iX;				// temporärer Zähler
unsigned short wCounter = 0;			// Time Zähler
unsigned short wIdle;				// Zusätzliche NOP cycles für 10 µS 
unsigned char mMask = 1;	                // Die PIN-Maske  
unsigned char mLev = Level;	
	for (iX = 0; iX < PinNr; iX++)
		mMask <<= 1;		       // BitNr --> Bit-maske
	pPort->bDdr &= ~mMask;  	       // define PINx as Input
	mLev &= mMask;			       // Level (0 or 1)
	while (!((pPort->bPin & mMask) ^ mLev))	// Warten auf PIN != Level
	{
		wCounter++;
		if (!wCounter) return (0);	// overflow --> return 0
	}				
	wCounter = 0;				// reset
	while ( (pPort->bPin & mMask) ^ mLev)	// wait for starting edge PIN == Level
	{
		wCounter++;
		if (!wCounter) return (0);	// overflow --> return 0
	}				
	wCounter = 0;				// reset

Soweit alles klar. Bei dem folgenden Code muß man je nach verwendetem Optimizer die Verzögerung anpassen. Allerdings muß die gesamte Schleife berücksichtigt werden. Ziel war:

(F_CPU / 100000) Anzahl der notwendigen cycles für 10 µS
Bei 8 MHZ ergibt das z.B. 80 Cycles

Das Schleifen-Grundmuster wurde erstmal übersetzt (der Optimizer war auf "Standard" gestellt), und die *.LSS-File wurde kontrolliert. Man konnte die Cycles nachzählen, die gesamte Schleife würde 16 Cylcles brauchen, + 6 Cycles für jede zusätzliche innere "Idle" Schleife. Dei erste Rechnung ergab also

16 + (n - 1) * 6 = 80  cycles  also  6 * n = 70 --> n = 70 / 6 

das wäre eine Dezimalzahl, die man nicht brauchen kann. Kürzer geht die Schleife nicht, aber länger, durch 4 zusätzlich "NOP" (4 * 1 Cycle). Das ergab dann

20 + (n - 1) * 6 = 80  cycles  also  6 * n = 66 --> n = 66 / 6 --> 11

Somit das konkrete Endprodukt:

20 + (n - 1) * 6 = F_CPU / 100000
// start counting -------------------------------------------------------
	while (!((pPort->bPin & mMask) ^ mLev))	// zählen bis PIN != Level
	{
		wIdle =  11;   // Idle cycles  für 10µS 8MHZ
		while (wIdle)
		{
			wIdle--;
			__asm__ __volatile ("; placeholder");
		}
		__asm__ __volatile ("nop");
		__asm__ __volatile ("nop");
		__asm__ __volatile ("nop");
		__asm__ __volatile ("nop");
		wCounter++;
		if (!wCounter) return (0);	// overflow (too long) --> return 0
	}
	return (wCounter);       // ergebnis (1 - 65535) 
}

Diese Rechnung läßt sich wahrscheinlich nicht wirklich gut zu einer einfachen Formel für jede CPU-Frequenz verallgemeinern. Aber es sollte gezeigt werden, wie man zum Ziel kommen könnte, ohne gleich ein Assembler-Programm zu schreiben. Die Cycles der Befehle findet man übrigens im Datasheet des Controllers.


Externe Interrupts

Dieses kleine Beispiel demonstriert, wie man in Bascom und GCC Interrupt-Routinen anlegt. Also Programmzeilen, die nur dann ausgeführt werden, wenn ein Low/High Pegel an einem bestimmten PIN eines Controllers wechselt. Bei einem solchen Wechsel wird das Hauptprogramm unterbrochen und die Interrupt-Routine aufgerufen. Anschließend wird das Hauptprogramm wieder weiter ausgeführt als sei nix gewesen. In diesem Beispiel macht das Hauptprogramm garnichts, es ist nur eine Endlosschleife. Die Interruptroutine schalten bei jedem Aufruf den Zustand einer LED um.

BasCom

  $regfile = "m32def.dat"  'z.B. rn-control 
  $framesize = 32 
  $swstack = 32 
  $hwstack = 32 
  $crystal = 16000000                 'Quarzfrequenz 
  $baud = 9600 

  Config Pinc.2 = Output  'An dem PIN sollte LED sein 
  Led3 Alias Portc.2 'Hier geben wir der Definition einen schöneren Namen 
  Config Int0 = RISING  'Interrupt bei steigender Flanke

  On Int0 Irq0  'Festlegen wo bei externem Interrupt hin gesprungen wird
  Enable Int0   'Diesen Interrupt aktivieren
  Enable Interrupts  'Alle aktivierten Interrupts einschalten
  Do  'Endlosschleife
  Loop 
  End 

  'Interrupt Routine wird immer ausgelöst wenn der Pegel von 0 auf 1 am
  'INT0 (Pin 18 PD2) Eingang wechselt 
  Irq0: 
    Toggle Led3 
  Return

GCC

Das GCC-Beispiel, das die gleiche Aufgabe erfüllt:

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/signal.h>

#define LED3 (1 << PC2)                         

/* Interrupt Routine wird immer ausgelöst wenn der Pegel von 0 auf 1 am
   INT0 (Pin 18 PD2) Eingang wechselt */
SIGNAL (SIG_INTERRUPT0)
{
  PORTC ^= LED3;				// Pin wechseln
}

//-----------------------------------------------------
int main()
{
  /* Ports und Interrupts initialisieren */
  DDRC |= LED3;					// PC2 als Ausgang
  DDRD &= ~(1 << DDD2);				// PD2 als Eingang (ext. Interrupt 0)
  MCUCR |= ((1 << ISC01) | (1 <<ISC00));	// steigende Flanke an INT0 erzeugt einen Interrupt  
  GICR  |= (1 << INT0);				// Diesen Interrupt aktivieren 

  sei();        				// Alle aktivierten Interrupts einschalten

  while(1);					// Endlosschleife 
}

AVRStudio (Assembler)

.NOLIST                    ; List-Output unterdrücken
 .INCLUDE <m32def.inc>       ; das gibt es für jeden Controllertyp
 .LIST                      ; List-Output wieder aufdrehen
 .CSEG                      ; was nun folgt, gehört in den FLASH-Speicher

 #define LED3 		PC2                         

 ;------------------------------------------------------
 ;     Start Adresse 0000
 ;------------------------------------------------------
 RESET:
 	jmp INIT           ; springen nach "INIT"

 .ORG  INT0ADDR			; Vector für INT0
	jmp		IsrInt0

 .ORG INT_VECTORS_SIZE    ; dadurch haben wir für die Vektoren Platz gelassen
 INIT:  
 ;------------------------------------------------------
 ;     INITIALIZE
 ;------------------------------------------------------
 	ldi 	r24,high(RAMEND)     ;Stack Pointer setzen 
 	out 	SPH,r24              ; "RAMEND" ist in m32def.inc (s.o.) festgelegt
 	ldi 	r24,low(RAMEND)      ; 
 	out 	SPL,r24              ;

	sbi	DDRC,  LED3			; PC2 als Ausgang
	cbi 	DDRD,  PD2			; PD2 als Eingang (ext. Interrupt 0)
	in	r24, MCUCR			; steigende Flanke an INT0 erzeugt einen Interrupt  
	ori	r24, (1 << ISC01) | (1 << ISC00) 
	in	r24, GICR
  	ori	r24,  INT0			; Diesen Interrupt aktivieren 
	out	GICR, r24

	sei        				// Alle aktivierten Interrupts einschalten

 ;------------------------------------------------------
 ;   HAUPTSCHLEIFE
 ;------------------------------------------------------
 Hauptschleife: 
    rjmp 	Hauptschleife         ; immer wiederholen 

 ;------------------------------------------------------
 ;   INTERRUPT
 ;------------------------------------------------------
IsrInt0:
	push	r22                   ; In diesem Beispiel eigentlich nicht notwendig
	push	r24                   ; In diesem Beispiel eigentlich nicht notwendig
	in	r24, SREG             ; In diesem Beispiel eigentlich nicht notwendig
	push	r24                   ; In diesem Beispiel eigentlich nicht notwendig

	ldi	r22, (1 << LED3)
	in	r24, PORTC
	eor	r24, r22
   	out	PORTC, r24

	pop	r24                   ; In diesem Beispiel eigentlich nicht notwendig
	out	SREG, r24             ; In diesem Beispiel eigentlich nicht notwendig
	pop	r24                   ; In diesem Beispiel eigentlich nicht notwendig
	pop	r22                   ; In diesem Beispiel eigentlich nicht notwendig
	reti

Bemerkung: die PUSH / POP Befehle sind in diesem Sonderfall, daß in der Hauptschleife nichts geschieht, überflüssig. Aber man sollte sich angewöhnen, in eine ISR-Routine den Status und die verwendeten Register zu sichern.

Siehe auch


LiFePO4 Speicher Test