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)?
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
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.
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:
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.
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:
# 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.
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
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 :-)
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
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.
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.
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
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