Hardware Abstraction Layer



  • In meinem derzeitigen Projekt muss ich ein HAL für den MCU des K64F von NXP entwickeln. Programmiert wird das ganze in C++ und es wird mindestens C++11 vorausgesetzt.

    Der HAL soll möglichst nah an der Hardware sein, d.h. er abstrahiert die Registerzugriffe nur ein wenig. Der HAL soll möglichst geringen Overhead erzeugen.

    Zuzeit habe ich das z.B. für die GPO's folgendermaßen umgesetzt:

    namespace mcu {
    	namespace gpo {
    
    		const static uint8_t k_num_ports = 5;
    
    		static GPIO_Type* const k_port_map[k_num_ports] = {
    			PTA,
    			PTB,
    			PTC,
    			PTD,
    			PTE,
    		};
    
    		static inline void init(uint8_t port_no, uint8_t pin_no) {
    			k_port_map[port_no]->PDDR |= 1<<pin_no;
    		}
    
    		static inline void set(uint8_t port_no, uint8_t pin_no) {
    			k_port_map[port_no]->PSOR = 1<<pin_no;
    		}
    		static inline void clr(uint8_t port_no, uint8_t pin_no) {
    			k_port_map[port_no]->PCOR = 1<<pin_no;
    		}
    		static inline void tog(uint8_t port_no, uint8_t pin_no) {
    			k_port_map[port_no]->PTOR = 1<<pin_no;
    		}
    
    		static inline bool is_set(uint8_t port_no, uint8_t pin_no) {
    			return (k_port_map[port_no]->PDIR&(1<<pin_no)) != 0;
    		}
    
    	} // namespace gpo
    } // namespace mcu
    

    Ein GPO kann nun z.B. wie folgt benutzt werden:

    int main() {
    	const static uint8_t k_port = 1;
    	const static uint8_t k_pin = 13;
    	mcu::gpo::init(k_port, k_pin);
    	for(;;) {
    		mcu::gpo::set(k_port, k_pin);
    		nops(10000);
    		mcu::gpo::clr(k_port, k_pin);
    		nops(10000);
    	}
    	return 0;
    }
    

    Was mich daran stört ist, dass man nun immer k_port und k_pin mitschleppen muss, wenn man den GPO benutzen will, was auf dauer sehr nervig wird und zudem sehr fehleranfällig ist.

    Ich habe nun für die GPO Funktionen eine statische template Klasse erstellt (Klasse die nur statische Methoden besitzt).

    namespace mcu {
    
    	template<uint8_t t_port_no, uint8_t t_pin_no>
    	class Gpo {
    	public:
    		static inline void init() {
    			gpo::init(t_port_no, t_pin_no);
    		}
    
    		static inline uint8_t get_port_no() {
    			return t_port_no;
    		}
    
    		static inline uint8_t get_pin_no() {
    			return t_pin_no;
    		}
    
    		static inline void set() {
    			gpo::set(t_port_no, t_pin_no);
    		}
    		static inline void clr() {
    			gpo::clr(t_port_no, t_pin_no);
    		}
    		static inline void tog() {
    			gpo::tog(t_port_no, t_pin_no);
    		}
    
    		static inline bool is_set() {
    			return gpo::is_set(t_port_no, t_pin_no);
    		}
    	};
    
    } // namespace mcu
    

    Den GPO kann man nun z.B. so verwenden:

    int main() {
    	using Led = mcu::Gpo<1, 13>;
    	Led::init();
    	for(;;) {
    		Led::set();
    		nops(10000);
    		Led::clr();
    		nops(10000);
    	}
    	return 0;
    }
    

    Der Compiler (Cross ARM Gcc) optimiert die ganzen inline Funktionen/Methoden weg. Am Ende wird Code erzeugt der lediglich die Register setzt, ohne Overhead. D.h. meine Anforderungen werden erfüllt, aber nun zu meiner Frage:

    Ist diese Vorgehensweise üblich? Ich finde leider sehr wenig Ressourcen im Netz zu dem Thema, und die wenigen die ich finde verwenden C. Da ich eher aus der PC Ecke komme und erst den letzten Monate angefangen habe mich intensiver mit MCU's zu beschäftigen ist dieses C++ gehacke für mich relativ normal, doch die Frage ist, wie kommen damit klassische MCU Programmierer klar, die eher aus der C Ecke kommen (mit einfachen C++ Kenntnissen), wenn Sie nun meine HAL's verwenden wollen/müssen? Kenn ihr gute Ressourcen zu diesem Thema (Artikel, Bücher, meinetwegen auch YouTube-Videos 🙄 ).

    Ich fand die folgenden Ressourcen zu dem Thema bisher sehr hilfreich:
    https://books.google.de/books?id=3sEDCwAAQBAJ&pg=PA157&lpg=PA157&dq=c%2B%2B+microcontroller+device+driver&source=bl&ots=ka93ioudlF&sig=VG_O1ovZ6K_SoWJjE3wa82XZr9M&hl=de&sa=X&ved=0ahUKEwjM0IyY8qrNAhUEUBQKHUbSANgQ6AEIQzAF#v=onepage&q=c%2B%2B microcontroller device driver&f=false
    https://www.embeddedrelated.com/showarticle/101.php



  • 1. Hardware Abstraction Layer

    2. C++/Templates in embedded - kommt auf das Projekt an

    ich habe mal für einen grossen Automobil-Steuerung-usw.-Zulieferer gearbeitet und da war in den ARM und MIPs Systemen sehr, sehr viel C++/Template Code zu finden - dort wurde es aus Performanzgründen gemacht und auch sehr viel gebenchmarkt

    in anderen Projekten wurden C++/Templates eher wegen purem-C-Willen/Kompilernotwendigkeit oder Mitarbeiter-Kompatibilität vermieden - Codebloat habe ich bisher kaum als Grund gehört

    also kommt drauf an



  • mit "sehr sehr viel" meine ich >200-300... kLOC mit C++/Templates



  • 1. Hardware Abstraction Layer

    Ups, ich meinte natürlich Layer ... 🙂



  • fenrayn schrieb:

    Ist diese Vorgehensweise üblich? Ich finde leider sehr wenig Ressourcen im Netz zu dem Thema, und die wenigen die ich finde verwenden C. Da ich eher aus der PC Ecke komme und erst den letzten Monate angefangen habe mich intensiver mit MCU's zu beschäftigen ist dieses C++ gehacke für mich relativ normal, doch die Frage ist, wie kommen damit klassische MCU Programmierer klar, die eher aus der C Ecke kommen (mit einfachen C++ Kenntnissen), wenn Sie nun meine HAL's verwenden wollen/müssen?

    Ich finde deine zweite Lösung sehr gelungen.

    Aber zu deiner eigentlichen Frage: einen HAL der einem C++ aufzwingt, wird kaum ein MCU-Coder verwenden wollen. Anders sieht es schon aus, wenn du eine C-Schnittstelle anbietest.



  • Aber zu deiner eigentlichen Frage: einen HAL der einem C++ aufzwingt, wird kaum ein MCU-Coder verwenden wollen. Anders sieht es schon aus, wenn du eine C-Schnittstelle anbietest.

    Ja da hast du vermutlich recht! Ich habs nun so abgeändert, dass der erste Teil C99 konform ist.

    #ifdef __cplusplus
    extern "C" {
    #endif
    
    static inline GPIO_Type* gpo_base(uint8_t port_no) {
    	switch(port_no) {
    		case 0: return PTA;
    		case 1: return PTB;
    		case 2: return PTC;
    		case 3: return PTD;
    		case 4: return PTE;
    	}
    	return 0;
    }
    
    static inline void gpo_init(uint8_t port_no, uint8_t pin_no) {
    	gpo_base(port_no)->PDDR |= 1<<pin_no;
    }
    
    static inline void gpo_set(uint8_t port_no, uint8_t pin_no) {
    	gpo_base(port_no)->PSOR = 1<<pin_no;
    }
    static inline void gpo_clr(uint8_t port_no, uint8_t pin_no) {
    	gpo_base(port_no)->PCOR = 1<<pin_no;
    }
    static inline void gpo_tog(uint8_t port_no, uint8_t pin_no) {
    	gpo_base(port_no)->PTOR = 1<<pin_no;
    }
    
    static inline bool gpo_is_set(uint8_t port_no, uint8_t pin_no) {
    	return (gpo_base(port_no)->PDIR&(1<<pin_no)) != 0;
    }
    
    #ifdef __cplusplus
    } // extern "C"
    
    template<uint8_t t_port_no, uint8_t t_pin_no>
    class Gpo {
    public:
    	static constexpr uint8_t get_port_no() {
    		return t_port_no;
    	}
    
    	static constexpr uint8_t get_pin_no() {
    		return t_pin_no;
    	}
    
    	static inline void init() {
    		gpo_init(t_port_no, t_pin_no);
    	}
    
    	static inline void set() {
    		gpo_set(t_port_no, t_pin_no);
    	}
    	static inline void clr() {
    		gpo_clr(t_port_no, t_pin_no);
    	}
    	static inline void tog() {
    		gpo_tog(t_port_no, t_pin_no);
    	}
    
    	static inline bool is_set() {
    		return gpo_is_set(t_port_no, t_pin_no);
    	}
    };
    
    #endif // __cplusplus
    


  • Ich teste gerade ob der Compiler das ganze auch mit einer "normalen" Klasse optimiert bekommt.

    class GpoTest {
    public:
    	inline GpoTest(uint8_t port_no, uint8_t pin_no) : k_port_no(port_no), k_pin_no(pin_no) {
    	}
    
    	inline GPIO_Type* get_base() const {
    		switch(k_port_no) {
    			case 0: return PTA;
    			case 1: return PTB;
    			case 2: return PTC;
    			case 3: return PTD;
    			case 4: return PTE;
    		}
    		return 0;
    	}
    
    	inline void init() const {
    		get_base()->PDDR |= 1<<k_pin_no;
    	}
    
    	inline void set() const {
    		get_base()->PSOR = 1<<k_pin_no;
    	}
    	inline void clr() const {
    		get_base()->PCOR = 1<<k_pin_no;
    	}
    
    private:
    	const uint8_t k_port_no;
    	const uint8_t k_pin_no;
    };
    

    Wenn ich das Objekt lokal erzeuge, optimiert der Compiler die ganzen this-Zeiger weg.

    int main() {
    	GpoTest led(0, 0);
    	led.init();
    	for(;;) {
    		led.set();
    		led.clr();
    	}
    	return 0;
    }
    

    Wenn ich das Objekt stattdessen global erzeuge, optimiert der Compiler da bis auf das inlining garnix.

    const static GpoTest led(0, 0);
    
    int main() {
    	led.init();
    	for(;;) {
    		led.set();
    		led.clr();
    	}
    	return 0;
    }
    

    Mir ist klar, dass das Objekt, da es global ist, jederzeit verändert werden könnte, aber da es static ist, sollte es doch nur im Modul sichtbar sein. Sollte dem Compiler nicht auffallen, dass k_port_no und k_pin_no sich niemals ändern? Zudem ist das ganze auch noch const ...

    Jemand ne Idee, wie ich den Compiler trotzdem noch dazu bewegen kann, das ganze zu optimieren? Oder sollte ich dann doch eher bei den "statischen" Klassen bleiben?



  • Wenn ich den Konstruktor mit constexpr versehe, greift die Optimierung wieder:

    constexpr GpoTest(uint8_t port_no, uint8_t pin_no) : k_port_no(port_no), k_pin_no(pin_no) {
    	}
    

    Der Compiler macht mich grad wahnsinnig ...



  • Ick würde nicht auf einen bestimmten Compiler setzen, wenn der Code universell einsetzbar sein soll.

    Versuch den Code lieber so primitiv wie möglich zu gestalten.

    Weniger ist mehr.
    C schlägt C++.
    Jedenfalls immer dann, wenn Speicherbedarf und Taktfrequenz eine Rolle spielen.
    Und das ist bei Embedded Systems fast immer der Fall. 🙂


  • Mod

    Andromeda schrieb:

    Weniger ist mehr.
    C schlägt C++.
    Jedenfalls immer dann, wenn Speicherbedarf und Taktfrequenz eine Rolle spielen.

    Bist du bescheuert?



  • Arcoth schrieb:

    Andromeda schrieb:

    Weniger ist mehr.
    C schlägt C++.
    Jedenfalls immer dann, wenn Speicherbedarf und Taktfrequenz eine Rolle spielen.

    Bist du bescheuert?

    Klar doch, 😃



  • Weniger ist mehr. 
    C schlägt C++. 
    Jedenfalls immer dann, wenn Speicherbedarf und Taktfrequenz eine Rolle spielen. 
    Und das ist bei Embedded Systems fast immer der Fall.
    

    Also die Erfahrung habe ich bisher noch nicht gemacht, eher im Gegenteil 😛



  • fenrayn schrieb:

    Ist diese Vorgehensweise üblich? Ich finde leider sehr wenig Ressourcen im Netz zu dem Thema

    Ich denke, dass wird der Weg sein, den wir gehen sollten. Was bei Deiner Lösung noch fehlt, ist die Berücksichtigung von globalen Initialisierungen (peripheral clocks) und die Berücksichtigung von konkurrierenden Recourcen-Belegungen (z.B. dass ein Pin nicht gleichzeitig als GPIO und als Anschluss für irgend ein Peripheral genutzt werden kann).

    Dazu müsste man die gesamte Controller-Nutzung der Library zur Verfügung stellen, dann wüsste die, welche Ports verwendet werden und wie die Initialisierung aussehen muss.

    Ich hatte das mal in einem Projekt ausprobiert, dabei ist dann so etwas heraus gekommen:

    typedef hammer::gpio::output_pin< device::P0_30 > puls;
    typedef hammer::gpio::output_pin< device::P0_15 > ami_enabled;
    
    typedef hammer::gpio::output_pin< device::P0_08, hammer::gpio::inverted, hammer::gpio::toggle > green_led;
    typedef hammer::gpio::output_pin< device::P0_09, hammer::gpio::inverted > red_led;
    
    typedef hammer::gpio::output_pin< device::P0_21, hammer::gpio::toggle > debug_pin;
    
    typedef hammer::timer::interval_timer<
        base_timer_interval,
        hammer::callback_member< device_logic, &device_logic::timer_callback, &logic > > main_timer;
    
    typedef hammer::serial::i2c_master<
        hammer::serial::scl_pin_with_pullup< device::P0_01 >,
        hammer::serial::sda_pin_with_pullup< device::P0_02 >,
        hammer::serial::i2c_bus_speed_400k > i2c_master;
    
    typedef radio_receiver radio;
    
    typedef lm75a< i2c_master > temperatur_sensor;
    
    struct hardware : hammer::device< hardware, device > {
        typedef boost::mpl::vector< red_led, green_led, debug_pin > debug_pins;
        typedef boost::mpl::vector< puls, debug_pins > pins;
    
        typedef boost::mpl::vector<
            pins,
            main_timer,
            i2c_master,
            temperatur_sensor
        > peripherals;
    };
    

    Dabei müsste man einen Ansatz wählen, die gewünschte Funktionseinheit sehr konkret und auf höchstem Level zu beschreiben, sodass sich die Library eine passende Implementierung mit den zur Verfügung stehenden pheriperals zusammen suchen kann.

    Hier noch ein Talk von der Meeting C++ von 2014 zum Thema: https://www.youtube.com/watch?v=k8sRQMx2qUw

    Hier habe ich einen C++ Bluetooth LE stack, der auf einem Cortex-M0 läuft und mit sehr wenig resourcen auskommt, das geht in etwas in die selbe Richtung: https://github.com/TorstenRobitzki/bluetoe

    mfg Torsten



  • fenrayn schrieb:

    Weniger ist mehr. 
    C schlägt C++. 
    Jedenfalls immer dann, wenn Speicherbedarf und Taktfrequenz eine Rolle spielen. 
    Und das ist bei Embedded Systems fast immer der Fall.
    

    Also die Erfahrung habe ich bisher noch nicht gemacht, eher im Gegenteil 😛

    C ist gewissermaßen sowas wie eine portable Assemblersprache. Für einen HAL gibt es quasi nicht Besseres.

    In C++ kanst du zu 99.9% Plain-C programmieren, wenn du willst. Von daher kann man nicht sagen, dass C++ irgendwie weniger geeignet ist.

    Problematisch wird es aber immer dann, wenn man die erweiterten Features von C++ einsetzen will. Du siehst ja selbst, auf welche Hindernisse du stößt. Aber vielleicht ist es ja gerade diese intellektuelle Herausforderung, die dich besonders anspornt.



  • fenrayn schrieb:

    Weniger ist mehr. 
    C schlägt C++. 
    Jedenfalls immer dann, wenn Speicherbedarf und Taktfrequenz eine Rolle spielen. 
    Und das ist bei Embedded Systems fast immer der Fall.
    

    Also die Erfahrung habe ich bisher noch nicht gemacht, eher im Gegenteil 😛

    Das Problem ist eher, dass im Embedded Bereich wenige ausgebildete SW-Entwickler arbeiten. Die meisten sind E-Techniker, die da mehr oder weniger rein geworfen wurden. Mit wachsenden Projektgrößen wird die Notwendigkeit von Abstraktionen bestimmt noch weiter steigen und da hat C wenig zu bieten (vor allem, wenn es keine Recourcen kosten darf).



  • Andromeda schrieb:

    Problematisch wird es aber immer dann, wenn man die erweiterten Features von C++ einsetzen will.

    Naja, es gibt sehr viele features in C++, die überhaupt keine Ressourcen kosten (bzw. nur welche kosten, wenn man das feature auch verwendet) und C++ genau deshalb zur besseren Wahl für embedded systems machen:

    - namespaces
    - templates
    - strenger Typenprüfung
    - gezieltere casts
    - class enums
    - Verhinderungen von narrowing conversions ({})
    - constexpr
    - const
    - constructors
    usw.

    mfg Torsten



  • Problematisch wird es aber immer dann, wenn man die erweiterten Features von C++ einsetzen will. Du siehst ja selbst, auf welche Hindernisse du stößt. Aber vielleicht ist es ja gerade diese intellektuelle Herausforderung, die dich besonders anspornt.
    

    Ja natürlich, wenn ich OO programmiere, ist dies mit gewissen Overhead verbunden, aber ich kann ja auch in C OO programmieren ... das ist also keine Frage der Programmiersprache, sonders des Konzepts. Verwendet man Sachen wie RTTI oder Exceptions, dann kann es schon mal dazu führen das der C Code schneller ist als der äquivalente C++ Code. Aber da ich diese Features beim Compiler eh komplett deaktiviert habe, mach ich mir da keine Sorgen.

    Großes Potential besonders für den Embedded Bereich sehe ich für C++ in Templates/Metaprogrammierung, constexpr und Lambdas. Inline-Funktionen und Const sind ja auch schon eine weile im C-Standard.

    Auch wenn ich für den PC programmiert habe stand ich immer dem Dilemma: Performance oder Flexibilität. Meine Hoffnung ist, dass ich dieses Dilemma mit den oben genannten C++ Features nicht mehr habe.

    Das Problem ist eher, dass im Embedded Bereich wenige ausgebildete SW-Entwickler arbeiten. Die meisten sind E-Techniker, die da mehr oder weniger rein geworfen wurden.

    Da ich gerade am Ende meines E-Technik Studiums bin, kenne ich dieses Phänomen 😃 Was wir an der Hochschule an Programmiertechniken gelernt haben war nen Witz. Kein C++, nur "einfaches" C, und je nach Vertiefungsrichtung durfte man noch ein wenig Java für Webanwendungen (LOL) lernen^^



  • Torsten Robitzki schrieb:

    Das Problem ist eher, dass im Embedded Bereich wenige ausgebildete SW-Entwickler arbeiten. Die meisten sind E-Techniker, die da mehr oder weniger rein geworfen wurden.

    Ja, das sehe ich auch so.

    Wobei in den letzen 5-10 Jahren durchaus eine Tendenz spürbar ist, für die Softwareentwicklung eher Informatiker und ähnliche Code-Monkeys anzuheuern.

    Das hängt wohl auch mit dem erweiterten Leistungsspektrum moderner MCUs zusammen. Daher lässt sich inzwischen auch bei Embedded Devices zwischen hardwarenaher Programmierung und "Userland" trennen.



  • @Torsten:
    Alle Pins in einem Enum anzugeben finde ich interessant. Vllt. werde ich das auch so übernehmen.

    Was bei Deiner Lösung noch fehlt, ist die Berücksichtigung von globalen Initialisierungen (peripheral clocks) und die Berücksichtigung von konkurrierenden Recourcen-Belegungen (z.B. dass ein Pin nicht gleichzeitig als GPIO und als Anschluss für irgend ein Peripheral genutzt werden kann)

    Das ein Pin zur selben Zeit von zwei unterschiedlichen Modulen verwendet wird sollte auf jeden Fall vermieden werden, aber es soll schon die Möglichkeit geben einen Pin zu muxen.



  • fenrayn schrieb:

    Das ein Pin zur selben Zeit von zwei unterschiedlichen Modulen verwendet wird sollte auf jeden Fall vermieden werden, aber es soll schon die Möglichkeit geben einen Pin zu muxen.

    Das war auch nur ein Beispiel, warum ich der Meinung bin, dass das ganze am besten funktionieren wird, wenn man so einer Library die gesamte "Wahrheit" mitteilt. Wenn die die Möglichkeit hätte, konkurrierende Ressourcenverwendung anzuzeigen, dann sollte es auch möglich sein, Ihr mitzuteilen, dass das an der Stelle so gewollt ist.


Anmelden zum Antworten