volatile als member variable nicht möglich?



  • Hi!

    Ich bin Entwickler im embedded Bereich und implementiere gerade eine Klasse, die wiederkehrende Aufgaben bei Registerzugriffen erledigen soll bzw. mir generell das Leben etwas erleichtern soll.
    Eigentlich funktioniert auch schon alles so, wie ich es gerne hätte. Nur sollte man ja Speicher, der memorymapped ist, als volatile markieren, damit bei dummy reads der compiler nix wegoptimiert.
    Und genau hier scheitert es bei mir. Es kompiliert einfach nicht. 😞

    Hier eine stark vereinfachte Version meiner Klasse:

    template <typename T>
    class Register {
    public:
      Register(void* pMem) {
        pRegister_ = reinterpret_cast<T*>(pMem);
      }
    
      volatile const T& read() const {    
        return *pRegister_;
      }
    
      void write(const T& bitField) {   
        *pRegister_ = bitField;
      }
    
    private:
      volatile T* pRegister_;
    };
    

    Und hier ein Beispiel, wie ich sie benutzen will:

    struct CONTROL {
      uint8_t
      enableTransmitter : 1,
      status            : 4,
                        : 1,
      crcMode           : 2;                  
    };
    
    int main(int argc, char* argv[]) {
      char hardwareMem[1024]; // Simuliertes mapped I/O memory
      Register<CONTROL> r(hardwareMem);
    
      CONTROL c;
      c.enableTransmitter = 1;
      c.crcMode = 0;
    
      r.write(c);
    
      // Folgende Zeilen dürfen nicht wegoptimiert werden:
      r.read();
      r.read();
      r.read();
    
      return 0;
    }
    

    Wenn ich das so kompilieren will, bekomme ich für Zeile 13 der Klasse folgenden Fehler:

    Fehler 1 error C2678: Binärer Operator '=': Es konnte kein Operator gefunden werden, der einen linksseitigen Operanden vom Typ 'volatile CONTROL' akzeptiert (oder keine geeignete Konvertierung möglich)

    Ich verstehe das Problem nicht ganz. Warum soll ich den = operator für die Bitfield struct überladen, nur weil sie als volatile markiert ist? Was mache ich da falsch? 😕



  • Mit dem Schlüsselwort volatile verhält sich sehr ähnlich wie mit const :
    Wenn du eine Variable vom Typ const T , dann kannst du mit dieser nur Member-Funktionen von T aufrufen, die const -qualifiziert sind - genau so wie du mit deinem pRegister_ nur volatile -qualifizierte Member-Funktionen aufrufen kannst.
    Der Default-Assignment-Operator, den der Compiler für CONTROL angelegt hat, ist aber nicht volatile -qualifiziert und ist daher für den Ausdruck *pRegister_ = bitField; nicht zulässig.

    Die Lösung ist also tatäschlich ein eigener, volatile -qualifizierter Assignment-Operator:

    volatile CONTROL& operator=(const CONTROL& control) volatile;
    

    Gruss,
    Finnegan



  • Mist, sowas habe ich mir schon fast gedacht...
    Das würde bedeuten, dass ich für jede bit field struct eine Überladung für operator= machen müsste und zwar jeweils individuell für jedes Member. Das wäre sehr unpraktikabel.

    Ich habe mal etwas herumgecasted und scheine nun eine Lösung gefunden zu haben, bei der der Compiler read() und write() nicht mehr wegoptimiert und bei der ich keine Überladungen machen muss. Der Code meiner Klasse sieht nun so aus:

    template <typename T>
    class Register {
    public:
      Register(void* pMem) {
        pRegister_ = reinterpret_cast<T*>(pMem);
      }
    
      const volatile T& read() const volatile {
        return *const_cast<const volatile T*>(pRegister_);
      }
    
      void write(const T& bitField) volatile {   
        *const_cast<T*>(pRegister_) = bitField;
      }
    
    private:
      volatile T* pRegister_;
    };
    

    Gibt es da eventuell eine bessere Lösung? z.B. aus der operator= überladung heraus den compiler generierten bitwise operator= aufrufen? Oder ist der weg, sobald man selber eine überladung definiert, analog wie bei den Konstruktoren?
    Oder kann ich das einfach so lassen, wie ich es derzeit habe?

    Gruß
    Stephan



  • Stephan++ schrieb:

    Ich habe mal etwas herumgecasted und scheine nun eine Lösung gefunden zu haben, bei der der Compiler read() und write() nicht mehr wegoptimiert und bei der ich keine Überladungen machen muss. Der Code meiner Klasse sieht nun so aus:

    Das ist höchstwahrscheinlich Undefiniertes Verhalten. Der Standard sagt dazu:

    §7.1.6.1 - 6. What constitutes an access to an object that has volatile-qualified type is implementation-defined. If an
    attempt is made to refer to an object defined with a volatile-qualified type through the use of a glvalue with
    a non-volatile-qualified type, the program behavior is undefined.
    

    Gibt es da eventuell eine bessere Lösung? z.B. aus der operator= überladung heraus den compiler generierten bitwise operator= aufrufen? Oder ist der weg, sobald man selber eine überladung definiert, analog wie bei den Konstruktoren?
    Oder kann ich das einfach so lassen, wie ich es derzeit habe?

    Zuerst wollte ich memcpy vorschlagen, aber damit hast du leider dasselbe Problem, da die Argumente von memcpy ebenfalls nicht volatile sind.
    Mein Bauchgefühl sagt mir zwar, dass der Compiler in diesem speziellen Fall wahrscheinlich das richtige macht, wenn man das volatile wegcastet,
    für ein korrektes Programm fällt mir allerdings auch keine simplere Lösung ein als den Operator zu implementieren und die ganzen Zuweisungen
    auszuschreiben. Falls hier nicht noch jemand anderes eine bessere Idee hat, kann ich nur noch ein Makro empfehlen um die Arbeit zu erleichtern.

    Finnegan

    P.S.: Die Member read() und write() von Register müssen wahrscheinlich nicht volatile -qualifiziert sein, da ich davon ausgehe,
    dass die Register -Objekete selbst nicht im Hardware-Speicher liegen werden ( r liegt bei deinem geposteten Code auf dem Stack),
    sondern lediglich der Member pRegister_ auf selbigen verweist (und der ja auch richtigerweise volatile ist).
    D.h. du wirst wahrscheinlich kein volatile Register haben, bei dem du read() / write() aufrufen möchtest.



  • Finnegan schrieb:

    Falls hier nicht noch jemand anderes eine bessere Idee hat, kann ich nur noch ein Makro empfehlen um die Arbeit zu erleichtern.

    Wie würde so ein Makro aussehen? Die Member sehen ja bei jeder struct anders aus. Also bräuchte ich irgendwas, was mir generisch sowas baut:

    volatile CONTROL& operator=(const CONTROL& control) volatile {
      enableTransmitter = control.enableTransmitter;
      status = control.status;
      crcMode = control.crcMode;
    }
    

    Aber eigentlich ist das doch alles gar nicht nötig, da ich nirgendwo mit dynamischem Speicher arbeite. Ich benötige wie gesagt nur ein stinknormales bitwise copy, so wie es der Compiler normalerweise generiert. Bei const verstehe ich ja, warum man da unterscheiden muss. Bei volatile erschließt sich mir der Sinn jedoch nicht. Das Schlüsselwort sagt doch lediglich aus, dass die Variable vor jeder Verwendung neu eingelesen werden soll. Warum muss dann alles Andere auch volatile sein?

    Finnegan schrieb:

    P.S.: Die Member read() und write() von Register müssen wahrscheinlich nicht volatile -qualifiziert sein, da ich davon ausgehe,
    dass die Register -Objekete selbst nicht im Hardware-Speicher liegen werden [...]

    Das ist korrekt. Ich möchte lediglich den Pointer, der auf den I/O Adressbereich zeigt, als volatile markieren.

    Finnegan schrieb:

    [...] ( r liegt bei deinem geposteten Code auf dem Stack),
    sondern lediglich der Member pRegister_ auf selbigen verweist (und der ja auch richtigerweise volatile ist).
    D.h. du wirst wahrscheinlich kein volatile Register haben, bei dem du read() / write() aufrufen möchtest.

    Damit es bei mir (visual studio 2013 compiler) und aktivierten Compileroptimierungen funktioniert, müssen die Funktionen wie folgt definiert sein:

    const T& read() const volatile {
      return *const_cast<const T*>(pRegister_);
    }
    
    void write(const T& bitField) volatile {   
      *const_cast<T*>(pRegister_) = bitField;
    }
    

    Das volatile jeweils hinter dem Funktionsnamen ist wichtig. Lässt man sie weg, werden die Aufrufe wegoptimiert.

    Gruß
    Stephan



  • Stephan++ schrieb:

    Finnegan schrieb:

    Falls hier nicht noch jemand anderes eine bessere Idee hat, kann ich nur noch ein Makro empfehlen um die Arbeit zu erleichtern.

    Wie würde so ein Makro aussehen? Die Member sehen ja bei jeder struct anders aus. Also bräuchte ich irgendwas, was mir generisch sowas baut:

    Hi nochmal!

    Bin nicht so ein Profi was Makros angeht, ich denke du löst sowas auch lieber mit Templates o.ä. wenn es sich irgendwie machen lässt.
    Ich habe allerdings trotzdem mal etwas mithilfe von Boost.Preprocessor zusammengestrickt (für etwas komplett eigenständiges fehlte mir da die Muße).

    Das ist jetzt bestimmt nicht die eleganteste Lösung, aber sie erledigt ihren Job - ich persönlich würde das allerdings zu Fuß und ohne Makros machen,
    auch wenn sich da einiges wiederholt. Der Code ist dann generell besser verständlich:

    #include <boost/preprocessor.hpp>
    
    // Erzeugt "TYPE NAME : LENGTH;", wenn MEMBER_TUPLE = (TYPE, NAME, LENGTH)
    // und "TYPE : LENGTH;" wenn MEMBER_TUPLE = (TYPE, LENGTH).
    #define BITFIELD_DECLARE_MEMBER(R, DATA, MEMBER_TUPLE) \
        BOOST_PP_TUPLE_ELEM(BOOST_PP_TUPLE_SIZE(MEMBER_TUPLE), 0, MEMBER_TUPLE) \
        BOOST_PP_IF(BOOST_PP_EQUAL(BOOST_PP_TUPLE_SIZE(MEMBER_TUPLE), 3), \
            BOOST_PP_TUPLE_ELEM(BOOST_PP_TUPLE_SIZE(MEMBER_TUPLE), 1, MEMBER_TUPLE) \
                : BOOST_PP_TUPLE_ELEM(BOOST_PP_TUPLE_SIZE(MEMBER_TUPLE), 2, MEMBER_TUPLE), \
            : BOOST_PP_TUPLE_ELEM(BOOST_PP_TUPLE_SIZE(MEMBER_TUPLE), 1, MEMBER_TUPLE));
    
    // Erzeugt "A = B;" wenn MEMBER_TUPLE = (TYPE, NAME, LENGTH),
    // ansonsten wird ein leerer Ausdruck erzeugt.
    #define BITFIELD_ASSIGN_MEMBER(R, DATA, MEMBER_TUPLE) \
        BOOST_PP_EXPR_IF(BOOST_PP_EQUAL(BOOST_PP_TUPLE_SIZE(MEMBER_TUPLE), 3), \
            BOOST_PP_TUPLE_ELEM(BOOST_PP_TUPLE_SIZE(MEMBER_TUPLE), 1, MEMBER_TUPLE) \
                = other.BOOST_PP_TUPLE_ELEM(BOOST_PP_TUPLE_SIZE(MEMBER_TUPLE), 1, MEMBER_TUPLE);)
    
    // Definiert Bitfield-struct mit den im Boost.Preprocessor-Tupel MEMBERS
    // angegebenen Typen, Member-Namen und Bitlängen sowie einen volatile-qualifizierten
    // Assignment-Operator mit Zuweisungen für diejenigen Member, die einen Namen haben.
    #define BITFIELD_DEFINE(NAME, MEMBERS) \
        struct NAME \
        { \
            BOOST_PP_SEQ_FOR_EACH(BITFIELD_DECLARE_MEMBER, _, BOOST_PP_TUPLE_TO_SEQ(MEMBERS)); \
            \
            inline volatile NAME& operator=(const NAME& other) volatile \
            { \
                BOOST_PP_SEQ_FOR_EACH(BITFIELD_ASSIGN_MEMBER, _, BOOST_PP_TUPLE_TO_SEQ(MEMBERS)) \
                return *this; \
            } \
        }
    
    // Beispiel:
    BITFIELD_DEFINE(CONTROL, // Definiert struct CONTROL
    ( // "MEMBERS-Tupel"
        (std::uint8_t, enableTransmitter, 1), // (TYPE, NAME, LENGTH)-Tupel (s.o.)
        (std::uint8_t, status, 4), 
        (std::uint8_t, 1),                    // (TYPE, LENGTH)-Tupel, für nicht verwendete Member.
        (std::uint8_t, crcMode, 2)
    ));
    

    Aus dem Beispiel erzeugt der Präprozessor bei mir (VS2015) dann:

    struct CONTROL
    {  
        std::uint8_t enableTransmitter : 1;
        std::uint8_t status : 4;
        std::uint8_t : 1;
        std::uint8_t crcMode : 2;
    
        inline volatile CONTROL& operator=(const CONTROL& other) volatile
        { 
            enableTransmitter = other.enableTransmitter;
            status = other.status;
            crcMode = other.crcMode;
            return *this;
        }
    };
    

    Anmerkung: Der dem Bit Field zugrunde liegende Typ wird hier für jeden Member explizit angegeben, so ist das Makro etwas flexibler,
    da ich nicht weiss wie deine anderen "Register" so aussehen (vielleicht auch mal ein uint8_t gefolgt von einem uint16_t ?).

    Frag mich aber nicht, wie Boost.Preprocessor das alles genau bewerkstelligt, ich habe da nur mal kurz reingeschaut wie man sowas umsetzt,
    ich glaube das will man nicht wirklich alles wissen (ich zumindest nicht) - es sieht furchtbar aus 😮.

    Stephan++ schrieb:

    Das volatile jeweils hinter dem Funktionsnamen ist wichtig. Lässt man sie weg, werden die Aufrufe wegoptimiert.

    Ich tippe mal darauf, dass das daran liegt, dass du das volatile einfach so wegcastest - damit castest du auch das Verbot für den Compiler weg, die Zugriffe zu optimieren.

    Gruss,
    Finnegan



  • Also ich würde bei memory-mapped Registern (bzw. auch RAM auf Erweiterungskarten o.Ä.) einfach keine Bitfields verwenden, sondern ganz normale uintXX_t .

    Allein schon deswegen, weil memory-mapped Zugriffe meist recht langsam sind. Über PCI bzw. PCIe kann sowas - verglichen mit normalen RAM-Zugriffen - eine kleine Ewigkeit dauern.

    Und ich vermute dass der von dir gezeigte CONTROL::operator= bei drei Bitfields zu drei Load-Modify-Store Sequenzen compilieren wird. Das ist erstmal langsamer als nötig, und dann kann es sogar sein dass es zu Fehlverhalten führt. z.B. wenn in einem Register ein "Start-/Stop-Bit" und weitere Steuerbits enthalten sind.

    Und wenn du die einzelnen Bits unbedingt als Bitfields ansprechen möchtest, kannst du immer noch Load/Store Templates machen, die den passenden uintXX_t verwenden um auf den memory-mapped Bereich zuzugreifen, und dann memcpy bzw. ne char-Zeiger-Kopierschleife verwenden um die Daten aus bzw. in die Bitfield struct zu kopieren.

    Beispiel:

    #include <iostream>
    #include <cstring>
    #include <cstdint>
    
    /////////////////////////////////////////////////////////////////////////////
    
    template <class T, std::size_t N>
    struct LoaderStorer;
    
    template <class T, class U>
    struct LoaderStorerImpl
    {
    	static T Load(void const* mem)
    	{
    		static_assert(sizeof(T) == sizeof(U), "size mismatch");
    		U const u = *static_cast<U const volatile*>(mem);
    		T t;
    		std::memcpy(&t, &u, sizeof(T));
    		return t;
    	}
    
    	static void Store(void* mem, T const& t)
    	{
    		static_assert(sizeof(T) == sizeof(U), "size mismatch");
    		U u;
    		std::memcpy(&u, &t, sizeof(T));
    		*static_cast<U volatile*>(mem) = u;
    	}
    };
    
    /////////////////////////////////////////////////////////////////////////////
    
    template <class T>
    struct LoaderStorer<T, sizeof(std::uint8_t)> : LoaderStorerImpl<T, std::uint8_t> {};
    
    template <class T>
    struct LoaderStorer<T, sizeof(std::uint16_t)> : LoaderStorerImpl<T, std::uint16_t> {};
    
    template <class T>
    struct LoaderStorer<T, sizeof(std::uint32_t)> : LoaderStorerImpl<T, std::uint32_t> {};
    
    template <class T>
    struct LoaderStorer<T, sizeof(std::uint64_t)> : LoaderStorerImpl<T, std::uint64_t> {};
    
    /////////////////////////////////////////////////////////////////////////////
    
    template <class T>
    T Load(void const* mem)
    {
    	return LoaderStorer<T, sizeof(T)>::Load(mem);
    }
    
    template <class T>
    void Store(void* mem, T const& t)
    {
    	LoaderStorer<T, sizeof(T)>::Store(mem, t);
    }
    
    /////////////////////////////////////////////////////////////////////////////
    
    struct Foo
    {
    	uint32_t A : 2;
    	uint32_t B : 3;
    	uint32_t C : 4;
    };
    
    int main()
    {
    	char mem[1024];
    
    	Foo f = {};
    	f.A = 1;
    	f.B = 2;
    	f.C = 3;
    
    	Store(&mem[32], f);
    
    	Foo f2 = Load<Foo>(&mem[32]);
    	f2.A++;
    	f2.B++;
    	f2.C++;
    	Store(&mem[32], f2);
    
    	Foo f3 = Load<Foo>(&mem[32]);
    
    	std::cout << "A = " << f3.A << ", B = " << f3.B << ", C = " << f3.C << ".\n";
    }
    

    Zumindest MSVC ist hier schlau genug die memcpy Aufrufe komplett wegzuoptimieren. Die Load/Store Aufrufe werden inlined und zu einem einzigen mov reg, dword ptr [address] bzw. mov dword ptr [address], reg compiliert.
    Und selbst wenn nicht, ist der Overhead vermutlich kleiner, als der von mehreren unnötigen memory-mapped Zugriffen.



  • ps:
    Ich hab' gerade erst deinen ersten Beitrag gelesen - du wolltest dir ja eh im Prinzip sowas bauen. Nur halt mit der Adresse des Registers in eine Klasse eingewickelt. Mit den von mir gezeigten Load/Store Funktionen ist die Implementierung dann trivial:

    // ... Load und Store definiert wie im obigen Beispiel ...
    
    /////////////////////////////////////////////////////////////////////////////
    
    template <typename T>
    class Register
    {
    public:
    	explicit Register(void* location)
    		: m_location(location)
    	{
    	}
    
    	T read() const
    	{
    		return Load<T>(m_location);
    	}
    
    	void write(T const& t)
    	{
    		Store(m_location, t);
    	}
    
    private:
    	void* m_location;
    };
    
    /////////////////////////////////////////////////////////////////////////////
    
    struct CONTROL {
    	uint8_t
    		enableTransmitter : 1,
    		status : 4,
    		: 1,
    		crcMode : 2;
    };
    
    /////////////////////////////////////////////////////////////////////////////
    
    int main(int argc, char* argv[])
    {
    	char hardwareMem[1024]; // Simuliertes mapped I/O memory 
    	Register<CONTROL> r(hardwareMem);
    
    	CONTROL c = {}; // Initialisieren würde ich das Ding schon...
    	c.enableTransmitter = 1;
    	c.crcMode = 0;
    
    	r.write(c);
    
    	// Folgende Zeilen WERDEN nicht wegoptimiert
    	r.read();
    	r.read();
    	r.read();
    
    	return 0;
    }
    

    Funktioniert bei mir wunderbar:

    char hardwareMem[1024]; // Simuliertes mapped I/O memory 
    	Register<CONTROL> r(hardwareMem);
    00BB1013  lea         eax,[hardwareMem]  
    00BB1019  mov         dword ptr [r],eax  
    
    	CONTROL c = {}; // Initialisieren würde ich das Ding schon...
    00BB101F  xor         al,al  
    
    	CONTROL c = {}; // Initialisieren würde ich das Ding schon...
    00BB1021  and         al,3Eh  
    	c.enableTransmitter = 1;
    	c.crcMode = 0;
    00BB1023  or          al,1  
    00BB1025  mov         byte ptr [c],al  
    
    	r.write(c);
    00BB102B  mov         byte ptr [hardwareMem],al  
    
    	// Folgende Zeilen WERDEN nicht wegoptimiert
    	r.read();
    00BB1031  mov         al,byte ptr [hardwareMem]  
    	r.read();
    00BB1037  mov         al,byte ptr [hardwareMem]  
    	r.read();
    00BB103D  mov         al,byte ptr [hardwareMem]
    

    Ein paar Dinge sind da komisch, z.B. die Variable c überhaupt im Speicher angelegt wird, bzw. das übermässig komplizierte rummachen mit al . Ich hätte mir erwartet dass jeder halbwegs brauchbare Optimizer es hinbekommt das wegzumachen. K.a. was MSVC sich dabei denkt.
    Der Rest sieht aber ganz gut aus. Also alles bis auf die eigentlichen Speicherzugriffe wegoptimiert, kein Problem wegen der 3 Ebenen an Hilfsfunktionen, dem memcpy oder sonstwas.
    Und das was bleiben muss ... ist geblieben 🙂



  • ps2:
    GCC und Clang sind "etwas" schlauer:

    mov     byte ptr [rsp - 8], 1
            mov     al, byte ptr [rsp - 8]
            mov     al, byte ptr [rsp - 8]
            mov     al, byte ptr [rsp - 8]
            xor     eax, eax
            ret
    

    😃
    https://goo.gl/CuVzXa



  • hustbaer schrieb:

    Und ich vermute dass der von dir gezeigte CONTROL::operator= bei drei Bitfields zu drei Load-Modify-Store Sequenzen compilieren wird.

    Hi!

    Hatte mir das grad auch nochmal angesehen. Noch nicht viel mit Bitfields gemacht, war aber eigentlich davon ausgegangen, dass er die Member alle wie ein einzelnes,
    zusammenhängendes Byte behandelt. Du hast recht, der erzeugt tatsächlich Code um 3 mal hintereinander dasselbe Byte zu lesen und zu schreiben (VS2015, /Ox maximale Optimierung):

    char mem[100];
        CONTROL c;
        volatile CONTROL& cv = *reinterpret_cast<CONTROL*>(mem);
        cv = c;
    
    00007FF68FA41582  and         dl,0FEh  
    00007FF68FA41585  movzx       ecx,al  
    00007FF68FA41588  and         cl,1  
    00007FF68FA4158B  or          dl,cl  
    00007FF68FA4158D  movzx       ecx,al  
    00007FF68FA41590  mov         byte ptr [mem],dl  
    00007FF68FA41594  and         cl,1Eh  
    00007FF68FA41597  movzx       edx,byte ptr [mem]  
    00007FF68FA4159C  and         dl,0E1h  
    00007FF68FA4159F  or          dl,cl  
    00007FF68FA415A1  mov         byte ptr [mem],dl  
    00007FF68FA415A5  movzx       ecx,byte ptr [mem]  
    00007FF68FA415AA  xor         cl,al  
    00007FF68FA415AC  and         cl,3Fh  
    00007FF68FA415AF  xor         cl,al  
    00007FF68FA415B1  mov         byte ptr [mem],cl
    

    Wenn ich ein wenig drüber nachdenke, macht das sogar Sinn. Schliesslich könnte ich einer Hardware in einigen Bits eine Anfrage übermitteln,
    deren Antwort ich dann postwendend in ein paar anderen Bits des selben Bytes erhalte.
    Da die Zuweisungen separate Anweisungen sind, müssen sie sowas natürlich auch korrekt abbilden.

    Also: Hustbaers Lösung gefällt mir auch deutlich besser als der ganze redundante Code oder das Makro-Geraffel.

    Gruss,
    Finnegan



  • Wow, da habt ihr euch aber Mühe gegeben, lol. Danke!

    Finnegan schrieb:

    Ich habe allerdings trotzdem mal etwas mithilfe von Boost.Preprocessor zusammengestrickt

    Das wäre ziemlich genau das, was ich suche. Nur leider darf ich in meiner Firma kein boost und keine stl benutzen, daher fällt diese Lösung leider flach. 😞

    Finnegan schrieb:

    Frag mich aber nicht, wie Boost.Preprocessor das alles genau bewerkstelligt, ich habe da nur mal kurz reingeschaut wie man sowas umsetzt,
    ich glaube das will man nicht wirklich alles wissen (ich zumindest nicht) - es sieht furchtbar aus 😮.

    Hihi, hat irgendwie was vom signal / slot System von Qt. Das ist auch schwarze Magie.

    hustbaer schrieb:

    Also ich würde bei memory-mapped Registern (bzw. auch RAM auf Erweiterungskarten o.Ä.) einfach keine Bitfields verwenden, sondern ganz normale uintXX_t.

    Genau das möchte ich ja vermeiden. Mir geht es vor allem um die Lesbarkeit und Wartbarkeit des Codes. Und:

    CONTROL.crc = 2;
    CONTROL.enableTransmitter = 1;
    

    Lässt sich hundert mal besser lesen als irgendwelche wilden binären Operationen wie:

    int control = 0x02 | 0x04
    

    Ich habe tatsächlich nur einen Bruchteil meiner Implementierung gezeigt. Die ist in der tat sehr ähnlich zur Lösung von hustbaer. Man kann sich quasi aussuchen, ob man direkt auf dem gememory gemappten bereich arbeiten will oder erst alles in einem temporärem Objekt zwischenspeichern und anschließend in einem Rutsch schreiben will (Bei manchen Registern muss man das sogar).
    Und wie ihr bereits herausgefunden habt, optimiert der Compiler die Zugriffe in der Regel recht gut hin, daher sehe ich das mit den Performanceienbußen eher unkritisch. Ich finde die Vorteile der besseren Lesbarkeit und Wartbarkeit überwiegen hier.

    Mit der Klasse arbeite ich bereits eine weile und möchte sie eigentlich nicht mehr missen. Einzig das Problem mit dem volatile ist noch etwas nervig. Zwar scheint meine dreckige cast Lösung erst einmal zu funktionieren, aber sobald meine Firma den compiler wechselt stehe ich eventuell dumm da. 😞 Notfalls führe ich eine explizite dummyRead() funktion ein, die dann einfach über einen volatile int Zeiger das Register liest. Dann müsste ich auch keine Zuweisungsoperatoren mehr überladen und der Code wäre an stellen, wo dummy reads gefordert sind, zugleich selbsterklärender.

    Gruß
    Stephan



  • Stephan++ schrieb:

    hustbaer schrieb:

    Also ich würde bei memory-mapped Registern (bzw. auch RAM auf Erweiterungskarten o.Ä.) einfach keine Bitfields verwenden, sondern ganz normale uintXX_t.

    Genau das möchte ich ja vermeiden. Mir geht es vor allem um die Lesbarkeit und Wartbarkeit des Codes.

    Leider ist auch so ziemlich alles was Bitfields angeht Implementation defined. Ob du also das von dir gewünsche Speicherlayout kriegst ist also keinesfalls garantiert.



  • Stephan++ schrieb:

    CONTROL.crc = 2;
    CONTROL.enableTransmitter = 1;
    

    Lässt sich hundert mal besser lesen als irgendwelche wilden binären Operationen wie:

    int control = 0x02 | 0x04
    

    Niemand hast gesagt dass du Magic-Numbers im Code verstreuen sollst.
    Man munkelt dass man in C++ auch sowas wie Konstanten definieren kann. Google mal, vielleicht findest du nen Artikel wo das beschrieben wird.



  • Stephan++ schrieb:

    Ich habe tatsächlich nur einen Bruchteil meiner Implementierung gezeigt. Die ist in der tat sehr ähnlich zur Lösung von hustbaer. Man kann sich quasi aussuchen (...)

    OK. Schön dass du eine so praktische toll funktionierende Lösung hast.

    Nur... was soll dann die Frage und "es compiliert einfach nicht *wein* *wein*"? Willst du uns bloss verarschen?



  • Verarschen? Es geht mir von Anfang an um das volatile Problem. Ich habe nie danach gefragt, ob mir jemand eine komplette Lösung vorkauen will. Mit dem reduzierten Code habe ich mich lediglich an die Forenregeln gehalten... Wie mans macht, macht mans falsch. 😞



  • OK. Whatever.

    Gibt es jetzt noch ein für dich ungelöstes Problem, oder hast du alles was du brauchst?
    Ich werde nämlich aus deinem Beitrag so überhaupt nicht schlau.


Anmelden zum Antworten