Forum: Compiler & IDEs [C++] UART-Files: UART per Nummer wählen


von nga (Gast)


Lesenswert?

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:
1
class USART {
2
    // die ganzen Variablen etc
3
public:
4
    USART(const uint8_t num, uint16_t baudrate);
5
    USART(const uint8_t num, uint16_t baudrate, bool doublespeed);
6
    //weitere Methoden
7
}
Und dann:
1
USART u0 = USART(0, 9600, true);

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

von Tom (Gast)


Lesenswert?

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_t n, uint16_t baudrate>
5
class UART
6
{
7
public:
8
    UART()
9
    {
10
        std::cout << "opening USART" << int(n) << " at " << baudrate << "bd\n";
11
    }
12
    void write(uint8_t data)
13
    {
14
        switch(n)
15
        {
16
            case 1: std::cout << "UDR1 =  " << int(data) << ";\n"; break;
17
            case 2: std::cout << "UDR2 =  " << int(data) << ";\n"; break;
18
            case 3: std::cout << "UDR3 =  " << int(data) << ";\n"; break;
19
            default: break;
20
        }
21
    }
22
};
23
24
25
int main()
26
{
27
    UART<2, 14400> foo;
28
    foo.write(42);
29
}

von Hans (Gast)


Lesenswert?

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.

von jgdo (Gast)


Lesenswert?

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.

von Tom (Gast)


Lesenswert?

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 ;).

von tictactoe (Gast)


Lesenswert?

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.

von nga (Gast)


Lesenswert?

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 ;)

von Programmierer (Gast)


Lesenswert?

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 ):
1
uint8_t& USARTx_DR = *reinterpret_cast <uint8_t*>(0x400 * x + 0x46000000);
2
USARTx_DR = 42;
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ß.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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.

von Programmierer (Gast)


Lesenswert?

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.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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

von Programmierer (Gast)


Lesenswert?

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

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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.

von Programmierer (Gast)


Lesenswert?

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.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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.

von Programmierer (Gast)


Lesenswert?

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...

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.