Forum: Mikrocontroller und Digitale Elektronik Sinnvolle Programmgliederung


von Jack (Gast)


Lesenswert?

Hallo,
ich überlege gerade wie ich ein mittelgroßes Projekt für den ATmega am 
sinnvollsten aufteile.

Sinnvollerweise wird der Quellcode ja aufgeteilt in mehrere .c und .h 
Dateien, z.B. UART.h für die UART-Hardware.

Wiederkehrende Aufgaben, zB Ringpuffer oder StringToInt könnte man ja 
auch auslagern.

Was aber wenn ich im UART z.B. einen Ringpuffer einrichten will?

Das UART-Modul wäre nicht mehr eigenständig lauffähig, ich könnte es 
auch in Zukunft nur noch mit meiner "myLib" verwenden.

Wie würdet ihr das handhaben? Wie wird das eigentlich bei gängigen 
Betriebssystemen gemacht (nur so aus Interesse)?

von Karl H. (kbuchegg)


Lesenswert?

Jack schrieb:

> Was aber wenn ich im UART z.B. einen Ringpuffer einrichten will?

Dann tu das.

> Das UART-Modul wäre nicht mehr eigenständig lauffähig,

Dann hast du eben 2 UART Module in deinem Vorrat zur Auswahl. Eines mit 
und eines ohne Ringbuffer.

> ich könnte es
> auch in Zukunft nur noch mit meiner "myLib" verwenden.

Die 1:1 Wiederverwertbarkeit von Software auf dieser Ebene ist des 
öfteren schon mal eine Illusion.

Das Problem:
Du kannst die Dinge schon 'voll konfigurierbar' machen. Und auf einem PC 
wird man das sogar tun. Nur hast du auf einem µC der Leistungsklasse 
eines AVR nicht die Resourcen (Speicher, Laufzeit), mit denen du wie 
wild um dich werfen kannst. Da akzeptiert man dann so manche Krücke, 
solange sie einigermassen akzeptabel ist.

Im Idealfall hast du zum Beispiel eine vorkompilierte Lib, zb für ein 
LCD und konfigurierst diese von main() aus
1
int main()
2
{
3
  lcd_SetMode( LCD_MODE_4_BIT );
4
  lcd_DataPins_4Bit( &PORTD, PD3, &PORTD, PD8, &PORTD, PD2, &PORTD, PD0 );
5
  lcd_ControlPins( &PORTC, PC2, &PORTA, PA1, &PORTB, PB0 );
6
7
  lcd_Init();
8
  ...

nur willst du die Konsequenzen davon nicht haben. Jeder Portzugriff 
dauert dann statt 1 Takt plötzlich ca. 10 Takte. Eine Konsequenz, die 
also zu teuer ist, als das man sie praktisch akzeptieren kann.

von xfr (Gast)


Lesenswert?

Jack schrieb:
> Das UART-Modul wäre nicht mehr eigenständig lauffähig, ich könnte es
> auch in Zukunft nur noch mit meiner "myLib" verwenden.

Naja, Du brauchst halt zu uart.c und uart.h noch ringbuffer.c und 
ringbuffer.h. Was ist denn daran schlimm?

Dass es gewisse Abhängigkeiten zwischen Modulen gibt, lässt sich nicht 
vermeiden. Wichtig ist, dass die Aufgaben der Module klar getrennt sind 
und man saubere Schnittstellen dazwischen hat, nachdem Prinzip "Lose 
Kopplung, starke Bindung".

Die Module organisiert man dann hierarchisch oder nach Schichten. Zum 
Beispiel könnte uart.c Teil des Interpreters für Benutzereingaben sein, 
der selber ein Modul mit einer Schnittstelle zum Hauptprogramm ist. Das 
Hauptprogramm nutzt dann nur noch diese Schnittstelle und nicht mehr 
direkt uart.c.

Die Konfiguration zur Laufzeit braucht man auf Mikrocontrollern dagegen 
eher nicht. Ich finde es ganz praktisch, dafür ein eigenes Headerfile je 
Modul einzusetzen. Also hier z.B. uart_config.h, das man in uart.c 
einbindet. Die Konfigurationsdatei legt man nicht in den Library-Ordner, 
sondern in seine Anwendung/Projekt. Dort kann man dann die 
hardwarespezifischen Dinge (Ports, Register, Baudrate usw.) definieren. 
Entweder als defines oder imo noch besser als Inline-Funktionen.

Für maximale Wiederverwendbarkeit kann man auch noch zwischen 
Anwendungs- und hardwarespezifischer Konfiguration unterscheiden. Also 
etwa so:

1
// uart.h
2
3
void uart_putchar(char c);

1
// uart.c
2
3
#include "uart.h"
4
#include "uart_config.h"
5
6
void uart_putchar(char c)
7
{
8
  while(!uart_hw_tx_ready()) {
9
    // Busy waiting
10
  }
11
  uart_hw_putchar(c);
12
}

1
// uart_config.h
2
3
#define BAUD 115200
4
5
#include "uart_hw_atxmega_portd1.h"

1
// uart_hw_atxmega_portd1.h
2
3
static inline bool uart_hw_tx_ready(void)
4
{
5
  return bit_is_set(USARTD1_STATUS, USART_DREIF_bp);
6
}
7
8
static inline void uart_hw_putchar(char c)
9
{
10
  USARTD1_DATA = c;
11
}

von Jack (Gast)


Lesenswert?

Karl Heinz Buchegger schrieb:
> nur willst du die Konsequenzen davon nicht haben

Ja, wie war :(

Ich hab mir auch schon überlegt meine bisher geschriebenen Module 
vorzukompilieren und zentral abzulegen. Vorteil: Änderungen/Bugfixes 
wären überall immer up-to-date.

Aber zur du hast recht, Konfiguration zur Laufzeit ist in manchen Fällen 
nicht so praktikabel.

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Karl Heinz Buchegger schrieb:
> Im Idealfall hast du zum Beispiel eine vorkompilierte Lib, zb für ein
> LCD und konfigurierst diese von main() aus

Wie Du richtig darstellst, ist eine vorkompilierte Lib für einen µC 
nicht sinnvoll, weil dies massive Performance-Einbrüche zur Folge haben 
kann.

Man kann aber durchaus den Quellcode selbst als Lib verwenden, indem man 
nämlich die Konfiguration at-compile-time und nicht at-runtime 
durchführt. Dabei kann einem der Preprocessor die Arbeit abnehmen.

Hier ein Beispiel:

main.c:
1
#define USE_RINGBUFFER    1
2
#define RINGBUFFER_SIZE  32
3
4
#include "../lib/libuart.c"
5
6
main ()
7
{
8
  ...
9
}

libuart.c:
1
/*--------------------------------------------------------------
2
 * libuart.c
3
 * required constants:
4
 *  USE_RINGBUFFER            1 or 0
5
 *  RINGBUFFER_SIZE           >0 if USE_RINGBUFFER is 1.
6
 *--------------------------------------------------------------
7
 */
8
9
#ifndef USE_RINGBUFFER
10
#  error USE_RINGBUFFER not defined
11
#else
12
#  if ! defined (RINGBUFFER_SIZE) || RINGBUFFER_SIZE <= 0
13
#    error wrong value of RINGBUFFER_SIZE
14
#  endif
15
#endif
16
17
...

Ich weiß natürlich, dass sich da jetzt welche wegen dem Include-Befehl 
angeekelt abwenden werden, aber ich halte dies für einen µC legitim. Ich 
konfiguriere also meine Lib at compile-time, nicht at-runtime. So kann 
ich libuart.c einmal mit, einmal ohne Ringbuffer verwenden, ohne dass 
ich libuart.c ändern muss.

Ich benutze bei größeren Projekten ca. ein Dutzend dieser 
"Lib-Includes.". Die werden dann aber nicht alle im main.c ausgeführt, 
sondern so:

uart.c:
1
#define ..
2
#include "../lib/libuart.c"

realtime-clock.c
1
#define ..
2
#include "../lib/librtc.c"

i2c.c
1
#define ..
2
#include "../lib/i2c-master.c"

usw.

Dann linke ich uart.c, realtime-clock.c, i2c.c mit meinem main.c 
zusammen und bin fertig.

von Peter D. (peda)


Lesenswert?

Jack schrieb:
> Das UART-Modul wäre nicht mehr eigenständig lauffähig

Warum nicht?
Ob mit oder ohne FIFO, die Funktionen zum Main heißen gleich und 
funktionieren gleich.
Du kannst den FIFO immer drin lassen.

Willst Du ein Verhalten, wie ohne FIFO, setze einfach die Puffergröße 
auf 2. Da immer ein Byte Lücke sein muß, paßt nur ein Byte rein. Beim 
Senden muß also nach jedem Byte gewartet werden und beim Empfang können 
Bytes verloren gehen.

In der Regel aber, hat man der UART erstmal einen FIFO verpaßt, möchte 
man ihn nicht mehr missen.


Peter

von Jack (Gast)


Lesenswert?

Das ist mir durchaus klar :-)

  Der FiFo beim UART war eben nur ein spontanes beispiel.
  Mache ich grundlegende Module wie UART oder Lcd von Libs abhängig (als 
beispiel eben ringpuffer.h) sind diese nicht mehr eigenständig, darum 
gehts mir.
  Hab eben unglücklicherweise ein Beispiel gewählt das du wiederlegen 
konntest :-)

von Peter D. (peda)


Lesenswert?

Man sollte nicht trennen, was zusammen gehört.
Ein Puffer bei der UART sieht anders aus, als bei I2C, SPI, LCD, ADC 
usw.
Daher macht man den Puffer sinnvoller Weise in den Devicetreiber mit 
rein.

Einen Universalpuffer, der unabhängig vom zu puffernden Interface ist, 
gibt es nicht.


Peter

von scherzkeks (Gast)


Lesenswert?

Was Du machen kannst und sinnvoll ist sind Funktionen die immer wieder 
verwendet werden, z.B. einen PID-Regler oder FIFO o.ä. in einzelne .c 
Dateien mit dazu passendem Header abzulegen.
"Universeller" Code wird auf'm µC wie schon erwähnt schnell 
unübersichtlich und viel zu aufwändig.
Es macht allerdings schon Sinn bei einem LCD-Modul die einzelnen 
Funktionen extern im .c zu haben.
Allerdings nicht als Eierlegendewollmilchsau, sondern diskret für 4bit, 
8bit oder SPI, dann nur das verwenden was auch angeschlossen ist und der 
Compiler nimmt auch nur das !
Eine vorcompilierte Bibliothek ist aus schon genannten Gründen sinnfrei.

von Jack (Gast)


Lesenswert?

Peter Dannegger schrieb:
> Einen Universalpuffer, der unabhängig vom zu puffernden Interface ist,
> gibt es nicht.

Ich versuch da grad ein sinnvolles Beispiel zu finden.

Ein Puffer funktioniert doch immer gleich: Byte reinschreiben wenn nötig 
(z.B. in der ISR, Callback etc...); bei Bedarf Byte lesen.

Bei einem Wortorientierten Interface ruft man die Schreibfunktion eben 
zweimal auf und liest auch 2 Bytes.
Bei einem LCD stellt man die Puffergröße eben auf Zeilenbreite und legt 
2 oder 4 Puffer an. (ergibt Overhead durch 2 oder 4 Lese- und 
Schreibindizies). Oder man nimmt einen großen Puffer und kümmert sich 
dann händisch um Zeilenumbrüche.
Oder versteh ich dich falsch?

Natürlich könnte man, je nach LCD-Refresh-Strategie auch einen Typen 
(Struktur) definieren der neben dem gepufferten Wert auch die 
Koordinaten auf dem Display enthält. Dann funktioniert der "klassische 
Ringpuffer" in der Tat nicht mehr.
Dann müsste man diese Strukturen in einer (verketteten) Liste ablegen.
Aber dann wirds sowieso unnötig umständlich.

Aber eigentlich wollt ich mich nicht so auf Puffer festfahren, das 
Beispiel war nur grade zur Hand. Geht natürlich genauso mit einer 
StringToInt-Funktion.

von Peter D. (peda)


Lesenswert?

Also mir ist es nicht gelungen, alle Puffer gleich zu behandeln.

UART:
2 getrennte Puffer byteweise

I2C:
Paketpuffer mit je 1 Array (schreiben+lesen gleichzeitig) + diverse 
Zusatzinformationen (Adresse, Schreibanzahl, Leseanzahl, Retry bei NACK) 
je Paket

SPI:
Paketpuffer mit je 1 Array (schreiben+lesen gleichzeitig) + diverse 
Zusatzinformationen (Chipselect, Frequenz, Bitorder, Schreibanzahl, 
Leseanzahl) je Paket

Text-LCD:
1 Display-RAM
- schreiben des Zeichens direkt an die Position im RAM
- Ausgabe zyklisch je ein Byte per Timerinterrupt + Zeilenumschaltung

ADC:
Puffer für jeden ADC-Kanal (+ Mittelwertbildung)
- Einlesen zyklisch per Timerinterupt
- direkte Auslesen des Mittelwerts des gewünschten Kanals.

Das ist also alles so unterschiedlich, das paßt unter keine gemeinsame 
Kappe.


Peter

von Peter D. (peda)


Lesenswert?

Man sollte es mit der Modularisierung nicht übertreiben, sondern ein 
gesundes Mittelmaß finden.

Ein separates super duper Puffermodul lohnt sich in den seltensten 
Fällen.
Insbesondere wenn Interrupts mit im Spiel sind, macht es alles nur 
unnötig kompliziert.


Peter

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.