Bezpieczny i ściśle typowany port w C++ (2 z n)

Tu poruszamy tematy związane z pisaniem programów w języku C++ dla AVR.
Awatar użytkownika
mokrowski
User
User
Posty: 166
Rejestracja: czwartek 08 paź 2015, 20:50
Lokalizacja: Tam gdzie Centymetro

Bezpieczny i ściśle typowany port w C++ (2 z n)

Postautor: mokrowski » sobota 22 wrz 2018, 22:26

Po opracowaniu obsługi dostępu do portu w trybie zapis-odczyt, czas na jego specjalizację związaną z dostępem w trybie wyłącznie do odczytu. Warto tu zauważyć że słowo "specjalizacja", z reguły w językach obiektowych pociąga za sobą pojęcie dziedziczenia.

Na poziomie logiki aplikacji dziedziczenie ma bardzo wiele destrukcyjnych cech. W pracy z szablonami, gdzie występuje "składanie kodu", te wady nie dają się tak we znaki. Stąd bardzo odważna propozycja dziedziczenia wielobazowego i ujętego w ... diament (straszy się nim młodych programistów). Z drugiej strony niewiele mi grozi bo to kod ćwiczebny i być może pokażę sposób wychodzenia z takiej "diamentowej matni" :-)

Struktura będzie więc wyglądała następująco:
1. Na samym szczycie hierarchii występuje klasa Port.
2. Z niej dziedziczy PortWO oraz PortRO.
3. Z portu PortWO i PortRO dziedziczy PortRW.

Taka hierarhia, pozwoli w przyszłości łatwo implementować porty wyzwalające działania. Wspominałem o nich w 1 części.

Istnieje wprawdzie inny sposób obsługi takich zależności. Można to zrobić poprzez wstrzykiwanie polityk dostępu. Tu jednak go nie użyję bo ... jestem zmuszony coś wybrać. Nic tak nie uczy jak potencjalne popełnienie błędu :-)

Oto jeszcze niedoskonały kod:

Kod: Zaznacz cały

#include <stdint.h>

// Oblicza przesunięcie czyli ilość zer kończących maskę.
constexpr static uint8_t calculate_mask_shift(uint8_t mask) {
        uint8_t counter = 0;
        while (!(mask & 0x01)) {
                ++counter;
                mask >>= 1;
        }
        return counter;
}

// Klasa nadrzędna wszystkich portów
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct Port {
        // Stałe opisujące atrybuty portu
        constexpr static uint8_t io_address = IO_Address + 0x20;
        constexpr static uint8_t mask = Mask;
        constexpr static uint8_t shift = Shift;
};

// Port wyłącznie do odczytu
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct PortRO: Port<IO_Address, Mask, Shift> {

        // Aliasy na typ i numer portu
        using port_type = const volatile uint8_t * const;
        static port_type port_ptr;

        // Alias na typ rodzica
        using parent_type = Port<IO_Address, Mask, Shift>;

        static uint8_t read() {
                return (*port_ptr & parent_type::mask)
                                >> parent_type::shift;
        }
};

// Konieczna definicja statyczna numeru portu.
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
typename PortRO<IO_Address, Mask, Shift>::port_type
        PortRO<IO_Address, Mask, Shift>::port_ptr =
                        reinterpret_cast<port_type>(parent_type::io_address);

// Port wyłącznie do zapisu
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct PortWO: Port<IO_Address, Mask, Shift> {

        // Aliasy na typ i numer portu
        using port_type = volatile uint8_t * const;
        static port_type port_ptr;

        // Alias na typ rodzica
        using parent_type = Port<IO_Address, Mask, Shift>;

        static void write(uint8_t value) {
                auto masked_value = *port_ptr & ~parent_type::mask;
                *port_ptr = masked_value | (value << parent_type::shift);
        }
};

// Konieczna definicja statyczna numeru portu.
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
typename PortWO<IO_Address, Mask, Shift>::port_type
        PortWO<IO_Address, Mask, Shift>::port_ptr =
                        reinterpret_cast<port_type>(parent_type::io_address);

// Port do zapisu i odczytu
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct PortRW: PortWO<IO_Address, Mask, Shift>, PortRO<IO_Address, Mask, Shift> {

        // Aliasy na typ i numer portu
        using port_type = volatile uint8_t * const;
        static port_type port_ptr;

        // Alias na typ rodzica
        using parent_type = Port<IO_Address, Mask, Shift>;

        static void write(uint8_t value) {
                auto masked_value = *port_ptr & ~parent_type::mask;
                *port_ptr = masked_value | (value << parent_type::shift);
        }
};

// Konieczna definicja statyczna numeru portu.
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
typename PortRW<IO_Address, Mask, Shift>::port_type
        PortRW<IO_Address, Mask, Shift>::port_ptr =
                        reinterpret_cast<port_type>(PortWO<IO_Address, Mask, Shift>::io_address);

// Definicje numerów portów w oodzielnej przestrzeni adresowej
namespace io_port {

constexpr static uint8_t io_portpina = 0x19;
constexpr static uint8_t io_portddra = 0x1A;
constexpr static uint8_t io_porta = 0x1B;

} // namespace io_port


// Definicje szablonów portów. Będą kompletne dla danego typu MCU
template<uint8_t Mask, uint8_t Shift = calculate_mask_shift(Mask)>
using PortPINA = PortRO<io_port::io_portpina, Mask, Shift>;

template<uint8_t Mask, uint8_t Shift = calculate_mask_shift(Mask)>
using PortDDRA = PortWO<io_port::io_portddra, Mask, Shift>;

template<uint8_t Mask, uint8_t Shift = calculate_mask_shift(Mask)>
using PortA = PortWO<io_port::io_porta, Mask, Shift>;

// Nazwy portów odzwierciedlające rzeczywiste użycie, właściwe dla zastosowania (Led)
using PortLedDirection = PortDDRA<0x3C>;
using PortLedIO = PortA<PortLedDirection::mask>;

// I program główny..
__attribute__((noreturn)) int main() {
        PortLedDirection::write(0x0F);
        PortLedIO::write(0x0F);
}

Już na pierwszy rzut oka widać szaloną liczbę powtórzeń. W zasadzie dana klasa portu, różni się tylko nazwą oraz subtelnościami w definiowaniu dostępu do portu. Może Ci tu przyjść na myśl: kod się powtarza, zrobię do tego makro. Ja jednak twierdzę: jeśli się powtarza, świadczy to o nieadekwatnym modelu.

Spróbuję więc czegoś innego. Warto zapoznać się ze składaniem klas, poprzez idiom językowy CRTP (zadziwiająco często powracający wzorzec czyli ang. curiously recurring template pattern) ( https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern ). Ten idiom ma prostą postać:

Kod: Zaznacz cały

struct X: Y<X> { ...


Chodzi o klasę X dziedziczącą od Y szablonowaną X. Taka konstrukcja pozwala w trakcie budowania źródeł "wstrzykiwać" zależności z Y pracujące na rzecz X. Jest to implementacja statycznego polimorfizmu który jest mało kosztowny w trakcie wykonania.

Implementuję więc podany fragment kodu:

Kod: Zaznacz cały

#include <stdint.h>

// Oblicza przesunięcie czyli ilość zer kończących maskę.
constexpr static uint8_t calculate_mask_shift(uint8_t mask) {
        uint8_t counter = 0;
        while (!(mask & 0x01)) {
                ++counter;
                mask >>= 1;
        }
        return counter;
}

// Klasa nadrzędna wszystkich portów
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct Port {
        // Stałe opisujące atrybuty portu
        constexpr static uint8_t io_address = IO_Address + 0x20;
        constexpr static uint8_t mask = Mask;
        constexpr static uint8_t shift = Shift;

};

// Polityki czyli interfejsy portów
template<typename T, uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct PortROPolicy: Port<IO_Address, Mask, Shift> {
        static uint8_t read();
};

template<typename T, uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct PortWOPolicy: Port<IO_Address, Mask, Shift> {
        static void write(uint8_t value);
};

template<typename T, uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct PortRWPolicy: Port<IO_Address, Mask, Shift> {
        static uint8_t read();
        static void write(uint8_t value);
};

// Definicje klas portów
// Port do odczytu
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct PortRO: PortROPolicy<PortRO<IO_Address, Mask, Shift>, IO_Address, Mask, Shift> {
        // Aliasy na typ i numer portu
        using port_type = const volatile uint8_t * const;
        static port_type port_ptr;

        // Alias na typ rodzica
        using parent_type = Port<IO_Address, Mask, Shift>;
};


// Konieczna definicja statyczna numeru portu.
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
typename PortRO<IO_Address, Mask, Shift>::port_type
        PortRO<IO_Address, Mask, Shift>::port_ptr =
                        reinterpret_cast<port_type>(parent_type::io_address);

// Port do zapisu
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct PortWO: PortWOPolicy<PortWO<IO_Address, Mask, Shift>, IO_Address, Mask, Shift> {
        // Aliasy na typ i numer portu
        using port_type = volatile uint8_t * const;
        static port_type port_ptr;

        // Alias na typ rodzica
        using parent_type = Port<IO_Address, Mask, Shift>;
};

// Konieczna definicja statyczna numeru portu.
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
typename PortWO<IO_Address, Mask, Shift>::port_type
        PortWO<IO_Address, Mask, Shift>::port_ptr =
                        reinterpret_cast<port_type>(parent_type::io_address);

// Port do oczytu i zapisu.
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct PortRW: PortRWPolicy<PortRW<IO_Address, Mask, Shift>, IO_Address, Mask, Shift> {
        // Aliasy na typ i numer portu
        using port_type = volatile uint8_t * const;
        static port_type port_ptr;

        // Alias na typ rodzica
        using parent_type = Port<IO_Address, Mask, Shift>;
};

// Konieczna definicja statyczna numeru portu.
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
typename PortRW<IO_Address, Mask, Shift>::port_type
        PortRW<IO_Address, Mask, Shift>::port_ptr =
                        reinterpret_cast<port_type>(parent_type::io_address);

// Implementacje metod polityk
template<typename T, uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
uint8_t PortROPolicy<T, IO_Address, Mask, Shift>::read() {
        return (*T::port_ptr & T::mask)
                        >> T::shift;
}

template<typename T, uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
void PortWOPolicy<T, IO_Address, Mask, Shift>::write(uint8_t value) {
        auto masked_value = *T::port_ptr & ~T::mask;
        *T::port_ptr = masked_value | (value << T::shift);
}

template<typename T, uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
uint8_t PortRWPolicy<T, IO_Address, Mask, Shift>::read() {
        return (*T::port_ptr & T::mask)
                        >> T::shift;
}

template<typename T, uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
void PortRWPolicy<T, IO_Address, Mask, Shift>::write(uint8_t value) {
        auto masked_value = *T::port_ptr & ~T::mask;
        *T::port_ptr = masked_value | (value << T::shift);
}

// Definicje numerów portów w oodzielnej przestrzeni adresowej
namespace io_port {

constexpr static uint8_t io_portpina = 0x19;
constexpr static uint8_t io_portddra = 0x1A;
constexpr static uint8_t io_porta = 0x1B;

} // namespace io_port

// Definicje szablonów portów. Będą kompletne dla danego typu MCU
template<uint8_t Mask, uint8_t Shift = calculate_mask_shift(Mask)>
using PortPINA = PortRO<io_port::io_portpina, Mask, Shift>;

template<uint8_t Mask, uint8_t Shift = calculate_mask_shift(Mask)>
using PortDDRA = PortWO<io_port::io_portddra, Mask, Shift>;

template<uint8_t Mask, uint8_t Shift = calculate_mask_shift(Mask)>
using PortA = PortWO<io_port::io_porta, Mask, Shift>;

// Nazwy portów odzwierciedlające rzeczywiste użycie, właściwe dla zastosowania (Led)
using PortLedDirection = PortDDRA<0x3C>;
using PortLedIO = PortA<PortLedDirection::mask>;

// I program główny..
__attribute__((noreturn)) int main() {
        PortLedDirection::write(0x0F);
        PortLedIO::write(0x0F);
}

Nadmiarowość definicji jeszcze występuje. Jednak definiowanie następnych polityk (np. przesłania przez szynę I2C, RS232 itp...), będzie już o wiele łatwiejsze. Jeszcze mam przecież wyzwanie związane z definicją podobnych portów dla wersji 16-bitowej na tym mikrokontrolerze!

Kod osiągnął już takie rozmiary (140 linii), że należy go modularyzować. Przy tej okazji także da się obalić kilka mitów krążących tu i ówdzie. Np. ten o konieczności definiowania całości implementacji w nagłówkach jeśli mamy kod szablonowy. Otórz nie jest to prawda ( https://isocpp.org/wiki/faq/templates#t ... fn-vs-decl ). Bez zmian funkcjonalnych, dokonam więc modularyzacji kodu. Dodatkowo osadzę go w przestrzeni nazewniczej avr.

Do oddzielnych plików nagłówkowych trafią:
1. Definicje wartości portów IO zależne od danego MCU. Będą rezydować w przestrzeni nazewniczej avr::io_ports.
2. Definicje polityk portów trafią do przestrzeni nazewniczej avr::io_policy.
3. Definicje klas portów powędrują do przestrzeni avr.
4. Funkcje pomocnicze (tu calculate_mask_shift(...)), będą rezydowały w przestrzeni nazewniczej avr::utility.
5. Wydzielę pojęcie portu bazowego. Będzie nim poprzedni szablon Port. Będzie rezydował w przestrzeni nazewniczej avr.
6. W pliku numerów portów, dodam detekcję rodzaju MCU, tak aby określić w przyszłości jakie numery portów powinny być dostępne.
7. Definicje konkretnych portów dla ATmega16 (PortDDRA, PortPINA, PortA), trafią do przestrzeni nazewniczej avr i głównego nagłówka io.hpp.

Usuwam także definicję artybutu noreturn dla main(). Na ten moment nie ma już najmniejszego sensu udowadnianie że program jest niewielkich rozmiarów.

Oto kod:
main.cpp:

Kod: Zaznacz cały

#include "io.hpp"

// Nazwy portów odzwierciedlające rzeczywiste użycie, właściwe dla zastosowania (Led)
using PortLedDirection = avr::PortDDRA<0x3C>;
using PortLedIO = avr::PortA<PortLedDirection::mask>;

// I program główny..
int main() {
        PortLedDirection::write(0x0F);
        PortLedIO::write(0x0F);
}


io.hpp:

Kod: Zaznacz cały

#ifndef IO_HPP_
#define IO_HPP_

#include <stdint.h>
#include "io_ports.hpp"
#include "port_base.hpp"
#include "utility.hpp"

namespace avr {

// Definicje szablonów portów. Będą kompletne dla danego typu MCU
template<uint8_t Mask, uint8_t Shift = avr::utility::calculate_mask_shift(Mask)>
using PortPINA = PortRO<io_ports::io_portpina, Mask, Shift>;

template<uint8_t Mask, uint8_t Shift = avr::utility::calculate_mask_shift(Mask)>
using PortDDRA = PortRW<io_ports::io_portddra, Mask, Shift>;

template<uint8_t Mask, uint8_t Shift = avr::utility::calculate_mask_shift(Mask)>
using PortA = PortWO<io_ports::io_porta, Mask, Shift>;

}

#endif /* IO_HPP_ */


utility.hpp:

Kod: Zaznacz cały

#ifndef UTILITY_HPP_
#define UTILITY_HPP_

#include <stdio.h>

namespace avr::utility {

// Oblicza przesunięcie czyli ilość zer kończących maskę.
constexpr static uint8_t calculate_mask_shift(uint8_t mask) {
        uint8_t counter = 0;
        while (!(mask & 0x01)) {
                ++counter;
                mask >>= 1;
        }
        return counter;
}

} // namespace avr::utility

#endif /* UTILITY_HPP_ */


port_base.hpp:

Kod: Zaznacz cały

#ifndef PORT_BASE_HPP_
#define PORT_BASE_HPP_

#include <stdint.h>

namespace avr {

// Klasa nadrzędna wszystkich portów
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct PortBase {
        // Stałe opisujące atrybuty portu
        constexpr static uint8_t io_address = IO_Address + 0x20;
        constexpr static uint8_t mask = Mask;
        constexpr static uint8_t shift = Shift;
};

}

#endif /* PORT_BASE_HPP_ */


port.hpp:

Kod: Zaznacz cały

#ifndef PORT_HPP_
#define PORT_HPP_

#include <stdint.h>
#include "io_policy.hpp"
#include "port_base.hpp"

namespace avr {

using avr::io_policy::PortROPolicy;
using avr::io_policy::PortWOPolicy;
using avr::io_policy::PortRWPolicy;

// Definicje klas portów
// Port do odczytu
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct PortRO: PortROPolicy<PortRO<IO_Address, Mask, Shift>, IO_Address, Mask, Shift> {
        // Aliasy na typ i numer portu
        using port_type = const volatile uint8_t * const;
        static port_type port_ptr;

        // Alias na typ rodzica
        using parent_type = PortBase<IO_Address, Mask, Shift>;
};

// Konieczna definicja statyczna numeru portu.
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
typename PortRO<IO_Address, Mask, Shift>::port_type
        PortRO<IO_Address, Mask, Shift>::port_ptr =
                        reinterpret_cast<port_type>(parent_type::io_address);

// Port do zapisu
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct PortWO: PortWOPolicy<PortWO<IO_Address, Mask, Shift>, IO_Address, Mask, Shift> {
        // Aliasy na typ i numer portu
        using port_type = volatile uint8_t * const;
        static port_type port_ptr;

        // Alias na typ rodzica
        using parent_type = PortBase<IO_Address, Mask, Shift>;
};

// Konieczna definicja statyczna numeru portu.
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
typename PortWO<IO_Address, Mask, Shift>::port_type
        PortWO<IO_Address, Mask, Shift>::port_ptr =
                        reinterpret_cast<port_type>(parent_type::io_address);

// Port do oczytu i zapisu.
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct PortRW: PortRWPolicy<PortRW<IO_Address, Mask, Shift>, IO_Address, Mask, Shift> {
        // Aliasy na typ i numer portu
        using port_type = volatile uint8_t * const;
        static port_type port_ptr;

        // Alias na typ rodzica
        using parent_type = PortBase<IO_Address, Mask, Shift>;
};

// Konieczna definicja statyczna numeru portu.
template<uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
typename PortRW<IO_Address, Mask, Shift>::port_type
        PortRW<IO_Address, Mask, Shift>::port_ptr =
                        reinterpret_cast<port_type>(parent_type::io_address);

} // namespace avr

#endif /* PORT_HPP_ */


io_policy.hpp:

Kod: Zaznacz cały

#ifndef IO_POLICY_HPP_
#define IO_POLICY_HPP_

#include <stdint.h>

#include "port_base.hpp"

namespace avr::io_policy {

using avr::PortBase;

// Polityki czyli interfejsy portów
template<typename T, uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct PortROPolicy: PortBase<IO_Address, Mask, Shift> {
        static uint8_t read();
};

template<typename T, uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct PortWOPolicy: PortBase<IO_Address, Mask, Shift> {
        static void write(uint8_t value);
};

template<typename T, uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
struct PortRWPolicy:
                PortROPolicy<T, IO_Address, Mask, Shift>,
                PortWOPolicy<T, IO_Address, Mask, Shift> {
};

// Implementacje metod polityk
template<typename T, uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
uint8_t PortROPolicy<T, IO_Address, Mask, Shift>::read() {
        return (*T::port_ptr & T::mask)
                        >> T::shift;
}

template<typename T, uint8_t IO_Address, uint8_t Mask, uint8_t Shift>
void PortWOPolicy<T, IO_Address, Mask, Shift>::write(uint8_t value) {
        auto masked_value = *T::port_ptr & ~T::mask;
        *T::port_ptr = masked_value | (value << T::shift);
}

} // namespace avr

#endif /* IO_POLICY_HPP_ */


io_ports.hpp:

Kod: Zaznacz cały

#ifndef IO_PORTS_HPP_
#define IO_PORTS_HPP_

#include <stdint.h>

namespace avr::io_ports_atmega16 {

constexpr static uint8_t io_portpina = 0x19;
constexpr static uint8_t io_portddra = 0x1A;
constexpr static uint8_t io_porta = 0x1B;

} // namespace avr

namespace avr {

#if defined (__AVR_ATmega16__)
namespace io_ports = avr::io_ports_atmega16;
#endif

} // namespace avr

#endif /* IO_PORTS_HPP_ */

Jak widać modularyzacja rozrastającego się projektu to złożony proces.

Pozostaje krok ostatni ale nie w tym już odcinku...

Definiowanie portu wejścia wyjścia na poziomie funkcjonalnym

Pod tym tytułem, kryje się łatwy w użyciu port (w tym przypadku A), do którego podając maskę, będzie można pisać i/lub z niego czytać. Trzeba zauważyć że port IO w AVR, ma możliwości podciągnięcia do stanu wysokiego i taką właściwość należy uwzględnić. Ma także warianty RO/WO/RW. Ciągle także nie ma jeszcze portu 16-bitowego. Na razie nie ma jednak takiej potrzeby.

Od przyszłego odcinka, zaprzestanę publikowania pełnego kodu pełnych zmian w każdym z etapów, poprzestając na umieszczeniu zmienianych plików a na koniec archiwum z wersją kończącą etap.
,,Myślenie nie jest łatwe, ale można się do niego przyzwyczaić" - Alan Alexander Milne: Kubuś Puchatek

Wróć do „Programowanie AVR w C++”

Kto jest online

Użytkownicy przeglądający to forum: Obecnie na forum nie ma żadnego zarejestrowanego użytkownika i 1 gość