Hallo Forum,
mir ist kein besserer Titel eingefallen...
Zur genaueren Definition meines Problems:
ich habe vor kurzem angefangen C++ zu verwenden, um meine AVRs zu
programmieren (bitte keine Aussagen, wie: "C++ ist overkill", etc.). Nun
wollte ich für jede Peripherie eine eigene klasse erstellen: ADC, SPI,
etc.
Jetzt bin ich beim UART angekommen: Mein Controller (Atmega2560) hat 4
USARTs, also wollte ich es so gestallten, dass ich im Konstruktor
auswählen kann, welchen USART ich verwenden will, etwa so:
Aber ich komme nicht auf eine Lösung, wie mann es möglichst
speichersparend umsetzt (der ATmega2560 hat eigentlich genug Platz, aber
ich will diesen Code auch auf anderen AVRs benutzen können, die vllt
nicht so viel Speicher haben).
Gibt es überhaupt eine schöne Lösung oder muss ich den entsprechenden
Code für jeden USART schreiben?
ich hoffe ich habe mein Problem verständlich beschreiben können (bin mir
da aber nicht so sicher ;) )
mfG
nga
nga schrieb:> wie mann es möglichst> speichersparend umsetzt
Mit UART-Nummer und Baudrate als Template-Parameter sollten Dinge wie
das switch in write() schon beim Compilieren rausfliegen, da n bekannt
und fest ist:
1
#include<iostream>
2
#include<inttypes.h>
3
4
template<uint8_tn,uint16_tbaudrate>
5
classUART
6
{
7
public:
8
UART()
9
{
10
std::cout<<"opening USART"<<int(n)<<" at "<<baudrate<<"bd\n";
Die Templatelösung dürfte die RAM-sparsamste sein, allerdings wird dabei
für jeden USART eigener Code generiert. Wobei das beim USART vermurtlich
nicht groß ins Gewicht fällt, da ja letzlich nur ein paar
Registerzugriffe gekapselt werden.
Als Alternative könntest Du die Registeradressen in Membervariablen
ablegen, die im Konstruktor befüllt werden. Eventuell reicht auch eine
Variable mit einem Offset, an dem die Register beginnen, wenn sie für
alle USARTs relativ zueinander gleich liegen. Das ist allerdings weniger
flexibel/portabel und braucht natürlich RAM für die Membervariablen.
Ich nehme an, dem TE geht es auch darum, ein UART-Objekte an Funktionen
übergeben zu können. Dabei würde die Lösung mit UART-Nr. als
Template-Parameter nicht funktionieren.
Stattdessen würde ich im Flash eine Tabelle mit Zeigern auf die
verschiedenen UART-Register anlegen, und im UART-Objekt nur den Index
des Eintrags zum entsprechenden UART speichern.
jgdo schrieb:> ein UART-Objekte an Funktionen> übergeben zu können. Dabei würde die Lösung mit UART-Nr. als> Template-Parameter nicht funktionieren.
Konsequent wäre es, den Objekten, die mit der UART sprechen müssen, so
ein UART-Objekt wiederum als Template-Parameter zu verpassen (a la
dependency injection zur Compile-Zeit). Wenn man das richtig macht,
inlinet/optimiert sich alle Objektorientierung weg und das Resultat ist
auf Assembler-Niveau. Mit einem derartigen Programmierstil wird man aber
(teilweise zu recht) von den Bitschubser-Kollegen verprügelt ;).
Wohl eher zu unrecht. Habe meine Wortuhr in genau diesem Stil
programmiert. Aus 1800 Zeilen C++11-Code mit vielen Templates werden
grade mal 5200 Byte Hex-File.
Mann, das geht ja wieder schnell hier.
Ich habe leider gerade ein paar kleine Probleme mit meinem PC bzw. der
IDE (PC-Virus von Kollegen eingeschleppt, auf Zweit-PC meldet eclipse
mal wieder Fehler, wo keine sind), also kann ich die Vorschläge erst
später ausprobieren. Aber wenn die Template-Methode so funktioniert und
auch so gut optimiert wird, wie von tictactoe beschrieben, werde ich
wohl dabei bleiben.
Schon mal danke an alle und nen schönen Rest-Sonntag ;)
Auf ARM's wie dem STM32 kann man die Adressen der diversen Register
eines UART mit der Nummer x einfach berechnen (ala a * x + b für
gerätespezifische a,b ):
Vielleicht geht das beim AVR genauso. An Stellen, wo dem Compiler das x
bekannt ist, wird er die Berechnung wegoptimieren. So kann man UART
Instanzen an Funktionen übergeben, die sind dann auch nur 1 Byte groß.
Programmierer schrieb:> Vielleicht geht das beim AVR genauso.
Auf Standard-AVRs geht das nicht (nicht garantiert). Xmegas kommen
da dem ARM näher.
Aber eine Berechnung der Adressen im eigenen Code statt aus dem
Headerfile, welches zum Controller geliefert wird, ist ziemlicher
Murks. Bei nächsten Hersteller kann das Layout der Peripherie
schließlich selbst mit gleichen Bitnamen ganz anders aussehen.
Jörg Wunsch schrieb:> Bei nächsten Hersteller kann das Layout der Peripherie schließlich> selbst mit gleichen Bitnamen ganz anders aussehen
Wie viele verschiedene Hersteller von zB einem ATmega2560 oder
STM32F103RBT6 gibt es denn?
Und warum ist das Murks, wenn man das, was in den Header Files des
Herstellers gemacht wird, in den eigenen nachbildet? Der OP möchte
offensichtlich sein eigenes Hardware- API bauen, warum muss er das auf
Grundlage der Hersteller- Header machen? Zumindest bei den Headern für
die Cortex- M hat man allen Grund sie loszuwerden wegen der Unmengen an
Makros, weiß jetzt nicht ob die AVR Header da besser sind.
Programmierer schrieb:> Wie viele verschiedene Hersteller von zB einem ATmega2560 oder> STM32F103RBT6 gibt es denn?
Falsche Frage.
Wofür braucht man eine Hardwareabstraktion, wenn man sie dann für
jedes neue Device neu schreiben muss? Gerade bei ARM wird doch
gern damit argumentiert, dass es sie von verschiedenen Herstellern
gibt. Wenn ich mich dann auf einen STM32F103RBT6 festnagele, dann
brauch ich auch die Abstraktion nicht, denn dann schreibe ich beim
nächsten ja doch alles neu.
> Zumindest bei den Headern für> die Cortex- M hat man allen Grund sie loszuwerden wegen der Unmengen an> Makros, weiß jetzt nicht ob die AVR Header da besser sind.
Was stören denn die Unmengen von Makros? Machen sie deinen Compiler
zu langsam?
Ich kenne jedenfalls keinen vernünftigen Grund, ein gewissermaßen
autoratives Headerfile eines Herstellers (sofern es nicht gerade buggy
ist) durch ein eigenes zu ersetzen, sondern ich würde stets nur darauf
aufbauen wollen. Alles andere ist doch sinnlose Doppelarbeit
Jörg Wunsch schrieb:> Wofür braucht man eine Hardwareabstraktion, wenn man sie dann für> jedes neue Device neu schreiben muss?
Wie soll denn eine funktionsfähige Hardwareabstraktion allein in der
Register-Definition (in den Headerfiles vom Hersteller) liegen? Allein
dadurch dass die Register-Adressen und Bitnamen in diesem Header sind,
ist der sie nutzende Code noch lange nicht hardware-unabhängig.
Verschiedene UART's haben verschiedene Funktionalitäten und damit
verschiedene Bits und Register. d.h. die UART-Klasse muss diese
notwendigerweise kennen, und ist damit zwangsweise hardware-abhängig.
Das wird auch nicht schlechter, wenn man die Register-Adressen in der
Klasse berechnet, anstelle von sie aus den Hersteller-Headern zu nehmen.
Das API der Klasse nach außen kann, falls gewünscht,
hardware-unabhängig sein.
Jörg Wunsch schrieb:> Wofür braucht man eine Hardwareabstraktion, wenn man sie dann für> jedes neue Device neu schreiben muss?
Wie soll bitte eine Hardware-Abstraktion funktionieren, die alle Devices
kennt? Irgendwo muss man mal auf Device-Spezifika eingehen.
Jörg Wunsch schrieb:> Gerade bei ARM wird doch> gern damit argumentiert, dass es sie von verschiedenen Herstellern> gibt.
Ja, und sie alle unterscheiden sich, auch im API nach außen.
Jörg Wunsch schrieb:> Wenn ich mich dann auf einen STM32F103RBT6 festnagele, dann> brauch ich auch die Abstraktion nicht, denn dann schreibe ich beim> nächsten ja doch alles neu.
Man nagelt die Hardware-Abstraktion auf den STM32F103RBT6 fest, und der
sie nutzende Code sieht nur die Abstraktion. So wird es seit Jahr und
Tag in der Treiber-Entwicklung gemacht: Ein Treiber kennt ein bestimmtes
Gerät eines bestimmten Herstellers, und gibt dem Betriebssystem
hardwareunabhängigen Zugriff.
Jörg Wunsch schrieb:> Was stören denn die Unmengen von Makros?
Die stören, weil sei keinen Typ und keinen Scope haben. So gibt es zB
beim STM32 Makros der Namen "RCC" und "USART" etc. Schreibt man also
nichtsahnend "class UART" in seinem Code, gibt es kryptische
Compilerfehler. Da ARM noch in Zeiten der Makroassembler lebt und noch
nicht von Hochsprachen gehört hat, verwenden sie lieber Makros als die
korrekten C-Elemente wie "typedef" und "const" (die solche Probleme
nicht haben).
Außerdem sind diese Makros ziemlich ungenerisch, denn sie führen nur
dumme Textersetzung durch, erlauben aber keine generische Berechnung von
Bits&Registeradressen; habe ich zB beim STM32 eine Instanz von
"GPIO_TypeDef*" (zB GPIOB), die somit einen Peripherieblock
referenziert, so kann ich daraus nicht die Nummer des Bits im
RCC-Register berechnen (-> RCC_AHB1ENR_GPIOBEN), um den Takt für diesen
GPIOB einzuschalten.
Ein vernünftiges Hardware-Abstraktions-API würde statt Pointer auf die
Peripherieblöcke einen Integer verwenden, der die Nummer des Blocks
angibt (0=GPIOA, 1=GPIOB, 2=GPIOC, ...), und daraus die verschiedenen
Informationen berechnen:
* Die Adressen der Register (&GPIOx_MODER = 0x40020000 + (x * 0x0400) )
* Die Bits im RCC-Register - entspricht in dem Fall direkt "x"
So könnte man Funktionen schreiben, die nur mit 1 Information (der
Nummer) einen Peripherie-Block anspricht; aber mit den ARM-API's (CMSIS)
ist das leider nicht möglich ohne manuelle Bitfummelei. Das wäre ein
Grund:
Jörg Wunsch schrieb:> Ich kenne jedenfalls keinen vernünftigen Grund
Programmierer schrieb:> Man nagelt die Hardware-Abstraktion auf den STM32F103RBT6 fest, und der> sie nutzende Code sieht nur die Abstraktion. So wird es seit Jahr und> Tag in der Treiber-Entwicklung gemacht: Ein Treiber kennt ein bestimmtes> Gerät eines bestimmten Herstellers, und gibt dem Betriebssystem> hardwareunabhängigen Zugriff.
Ich kenne es seit Jahr und Tag (genauer: seit mehr als 20 Jahren in
FreeBSD) durchaus anders. Der Treiber arbeitet nur mit symbolischen
Namen, erst die Konfiguration bindet ihn dann an konkrete Adressen.
So war es in FreeBSD schon zu Zeiten von ISA, also mit manueller
Adresszuweisung, und bei automatischer Adresszuweisung (PCI) geht es
sowieso nicht anders.
Mit deiner Methode verhinderst du ja selbst das Nachnutzen der
Implementierung auf einem anderen ARM von STM, sofern sich STM für
diesen entschieden hat, die UART-Blöcke aus irgendeinem Grund auf
andere Adressen abzubilden.
>> Was stören denn die Unmengen von Makros?> Die stören, weil sei keinen Typ und keinen Scope haben.
Das kenne ich in der Tat auch. Andererseits: es geht um eine
Abstraktion in C++, in diesem Falle muss ja nur die entsprechende
Implementierungsdatei mit den daraus resultierenden Einschränkungen
leben. Diejenigen, die die Klasse dann verwenden, brauchen die
Hardware-Details mit ihrem schröcklichen Hersteller-Headerfile nicht
mehr zu kennen.
(Allerdings sind beim AVR die Registernamen zum Glück in der Tat
zumeist etwas spezieller, sodass man über sowas wie UART nicht
stolpert.)
> Schreibt man also> nichtsahnend "class UART" in seinem Code, ...
Was allerdings ohnehin nach gängigen Konventionen schlechter Stil ist,
denn alles in GROSSBUCHSTABEN hat man seit Jahr und Tag für Makros
benutzt um explizit daran zu erinnern, dass diese auch Seiteneffekte
jenseits normaler Objekte haben können. Wenn man "class Uart" oder
"class uart" schreibt, stolpert man schon nicht mehr drüber.
> Da ARM noch in Zeiten der Makroassembler lebt und noch> nicht von Hochsprachen gehört hat, verwenden sie lieber Makros als die> korrekten C-Elemente wie "typedef" und "const" (die solche Probleme> nicht haben).
Ich weiß nicht, wie es bei ARM ist, aber zumindest bei AVR taugen die
Hardware-Headerfiles auch noch für direktes Einbinden in Assembler.
Da hat man dann keine "const" oder "typedef" mehr.
Jörg Wunsch schrieb:> Der Treiber arbeitet nur mit symbolischen> Namen, erst die Konfiguration bindet ihn dann an konkrete Adressen.
Ich bin fasziniert. Wie funktioniert das? Kannst du mir zeigen, wie ein
UART-Treiber funktionieren kann, der lediglich durch anpassen der
Adressen 1000 verschiedene Uart's ansprechen kann? Der also zB die
grundverschiedenen UART-Periphals auf ATmega8, STM32F103RBT6 und auf
einem PC-Mainboard ansprechen kann, nur durch Anpassen der Adressen?
Jörg Wunsch schrieb:> sofern sich STM für> diesen entschieden hat, die UART-Blöcke aus irgendeinem Grund auf> andere Adressen abzubilden.
Das tut ST, ja. Und nein, meine Methode verhindert das nicht, denn die
Parameter "a" und "b" (Multiplikator und Offset) müssen natürlich nicht
zwangsweise im Klassenheader fix stehen, sondern könnten ja auch als
"static const" in eine seperate Datei kommen, von der man dann pro
Mikrocontroller eine hat. Und der Hersteller-Header taugt dazu nicht,
denn zumindest der Multiplikator steht da nicht als Konstante (Makro)
drin.
Jörg Wunsch schrieb:> in diesem Falle muss ja nur die entsprechende> Implementierungsdatei mit den daraus resultierenden Einschränkungen> leben.
#include ist leider transitiv, d.h. wenn die Header-Datei der Klasse den
Hersteller-Header einbindet (zB um die Definition von USART_TypeDef zu
bekommen), sieht der gesamte Usercode sie auch.
Jörg Wunsch schrieb:> Was allerdings ohnehin nach gängigen Konventionen schlechter Stil ist,
"UART" ist eine gängige Abkürzung, und Abkürzungen werden gängigerweise
(zB in der Doku) komplett großgeschrieben. Derartige Einschränkungen
sind doch eine fiese Fummelei.
Jörg Wunsch schrieb:> Ich weiß nicht, wie es bei ARM ist, aber zumindest bei AVR taugen die> Hardware-Headerfiles auch noch für direktes Einbinden in Assembler.
Tun sie sowieso nicht, da sie "struct" verwenden. Aber Assembler braucht
man beim Cortex-M sowieso nicht.
Programmierer schrieb:> Kannst du mir zeigen, wie ein UART-Treiber funktionieren kann, der> lediglich durch anpassen der Adressen 1000 verschiedene Uart's> ansprechen kann?
Nein, nicht 1000 verschiedene UARTs, aber für die gängigen
8250-Derivate kommt man am Ende in der Tat mit einer einzigen
Treiberimplementierung aus. Die Anpassung an die Derivate
(Tiefe des FIFOs und dergleichen) erfolgt dynamisch.
Klar, wenn jemand eine völlig andere UART-Hardware nimmt (Z80 SCC),
braucht man eine neue Implementierung. Dennoch: Adressen stehen
nicht im Treiber, nur die internen Offsets der Register relativ zur
Basisadresse.
> #include ist leider transitiv, d.h. wenn die Header-Datei der Klasse den> Hersteller-Header einbindet (zB um die Definition von USART_TypeDef zu> bekommen), sieht der gesamte Usercode sie auch.
Im Header muss sie ja aber nicht stehen, schließlich trennt C++
zwischen Interface und Implementierung in zwei verschiedene Dateien.
Jörg Wunsch schrieb:> Nein, nicht 1000 verschiedene UARTs
Aha, also muss man doch pro UART-Typ alles neu schreiben.
Jörg Wunsch schrieb:> Adressen stehen> nicht im Treiber, nur die internen Offsets der Register relativ zur> Basisadresse.
Dann trennt man die Zahlen halt. Trotzdem keine Argument die Adressen
nicht zu berechnen.
Jörg Wunsch schrieb:> Im Header muss sie ja aber nicht stehen, schließlich trennt C++> zwischen Interface und Implementierung in zwei verschiedene Dateien.
Leider nur die Implementierung der Funktions-Bodys. Wenn eine (zB
private) Funktion ein UART_TypeDef* zurückgeben soll, oder man ein
UART_TypeDef als (private) Member-Variable haben will, muss man den
Hersteller-Header im Klassen-Header #include'n.
Zu solchen Methoden zurückgreifen zu müssen spricht nicht für ein gutes
Design...