Problem
W kontekście osadzonym na niskim poziomie typu bare-metal chciałbym utworzyć w pamięci puste miejsce w ramach struktury C ++ i bez nazwy, aby uniemożliwić użytkownikowi dostęp do takiej lokalizacji w pamięci.
W tej chwili osiągnąłem to, umieszczając brzydkie pole uint32_t :96;
bitowe, które wygodnie zajmie miejsce trzech słów, ale podniesie ostrzeżenie z GCC (Bitfield zbyt duży, aby zmieścić się w uint32_t), co jest całkiem uzasadnione.
Chociaż działa dobrze, nie jest zbyt czysty, gdy chcesz rozpowszechniać bibliotekę z kilkuset takimi ostrzeżeniami ...
Jak mam to zrobić poprawnie?
Dlaczego pojawia się problem w pierwszej kolejności?
Projekt, nad którym pracuję, polega na zdefiniowaniu struktury pamięci różnych peryferiów całej linii mikrokontrolerów (STMicroelectronics STM32). W tym celu powstaje klasa, która zawiera sumę kilku struktur definiujących wszystkie rejestry w zależności od docelowego mikrokontrolera.
Prosty przykład całkiem prostego urządzenia peryferyjnego jest następujący: wejście / wyjście ogólnego przeznaczenia (GPIO)
union
{
struct
{
GPIO_MAP0_MODER;
GPIO_MAP0_OTYPER;
GPIO_MAP0_OSPEEDR;
GPIO_MAP0_PUPDR;
GPIO_MAP0_IDR;
GPIO_MAP0_ODR;
GPIO_MAP0_BSRR;
GPIO_MAP0_LCKR;
GPIO_MAP0_AFR;
GPIO_MAP0_BRR;
GPIO_MAP0_ASCR;
};
struct
{
GPIO_MAP1_CRL;
GPIO_MAP1_CRH;
GPIO_MAP1_IDR;
GPIO_MAP1_ODR;
GPIO_MAP1_BSRR;
GPIO_MAP1_BRR;
GPIO_MAP1_LCKR;
uint32_t :32;
GPIO_MAP1_AFRL;
GPIO_MAP1_AFRH;
uint32_t :64;
};
struct
{
uint32_t :192;
GPIO_MAP2_BSRRL;
GPIO_MAP2_BSRRH;
uint32_t :160;
};
};
Gdzie wszystko GPIO_MAPx_YYY
jest makrem, zdefiniowane jako uint32_t :32
lub typ rejestru (dedykowana struktura).
Tutaj widzisz, uint32_t :192;
który działa dobrze, ale wyzwala ostrzeżenie.
Co do tej pory rozważałem:
Mogłem go zastąpić kilkoma uint32_t :32;
(6 tutaj), ale mam kilka skrajnych przypadków, w których mam uint32_t :1344;
(42) (między innymi). Więc wolałbym nie dodawać około stu wierszy do 8k innych, mimo że generowanie struktury jest skryptowane.
Dokładny komunikat ostrzegawczy jest taki:
width of 'sool::ll::GPIO::<anonymous union>::<anonymous struct>::<anonymous>' exceeds its type
(po prostu uwielbiam to, jak jest podejrzane).
Wolałbym nie rozwiązywać tego przez proste usunięcie ostrzeżenia, ale użycie
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-WTheRightFlag"
/* My code */
#pragma GCC diagnostic pop
może być rozwiązaniem ... jeśli znajdę TheRightFlag
. Jednak, jak wskazano w tym wątku , gcc/cp/class.c
z tą smutną częścią kodu:
warning_at (DECL_SOURCE_LOCATION (field), 0,
"width of %qD exceeds its type", field);
Co mówi nam, że nie ma -Wxxx
flagi, która usuwałaby to ostrzeżenie ...
źródło
char unused[12];
i tak dalej?uint32_t :192;
.:42*32
zamiast tego możesz napisać:1344
Odpowiedzi:
Użyj wielu sąsiednich anonimowych pól bitowych. Więc zamiast:
uint32_t :160;
na przykład miałbyś:
uint32_t :32; uint32_t :32; uint32_t :32; uint32_t :32; uint32_t :32;
Po jednym dla każdego rejestru, w którym chcesz zachować anonimowość.
Jeśli masz duże przestrzenie do wypełnienia, może być wyraźniejsze i mniej podatne na błędy, aby użyć makr do powtórzenia pojedynczej 32-bitowej przestrzeni. Na przykład, biorąc pod uwagę:
#define REPEAT_2(a) a a #define REPEAT_4(a) REPEAT_2(a) REPEAT_2(a) #define REPEAT_8(a) REPEAT_4(a) REPEAT_4(a) #define REPEAT_16(a) REPEAT_8(a) REPEAT_8(a) #define REPEAT_32(a) REPEAT_16(a) REPEAT_16(a)
Następnie można dodać przestrzeń 1344 (42 * 32 bity) w ten sposób:
struct { ... REPEAT_32(uint32_t :32;) REPEAT_8(uint32_t :32;) REPEAT_2(uint32_t :32;) ... };
źródło
uint32_t :1344;
jest na miejscu), więc wolałbym nie iść tą drogą ...A co powiesz na sposób w C ++?
namespace GPIO { static volatile uint32_t &MAP0_MODER = *reinterpret_cast<uint32_t*>(0x4000); static volatile uint32_t &MAP0_OTYPER = *reinterpret_cast<uint32_t*>(0x4004); } int main() { GPIO::MAP0_MODER = 42; }
Otrzymujesz autouzupełnianie ze względu na
GPIO
przestrzeń nazw i nie ma potrzeby uzupełniania fikcyjnego. Nawet, jest bardziej jasne, co się dzieje, ponieważ możesz zobaczyć adres każdego rejestru, nie musisz w ogóle polegać na zachowaniu wypełniania kompilatora.źródło
ldr r0, [r4, #16]
, podczas gdy kompilatory są bardziej skłonne do pominięcia tej optymalizacji z każdym adresem deklarowanym osobno. GCC prawdopodobnie załaduje każdy adres GPIO do oddzielnego rejestru. (Z literalnej puli, chociaż niektóre z nich można przedstawić jako obrócone natychmiast w kodowaniu Thumb.)static volatile uint32_t &MAP0_MODER
, nieinline
.inline
Zmienna nie kompiluje. (static
unika posiadania statycznej pamięci dla wskaźnika ivolatile
jest dokładnie tym, czego chcesz dla MMIO, aby uniknąć eliminacji martwego magazynu lub optymalizacji zapisu / odczytu zwrotnego).static
równie dobrze radzi sobie w tym przypadku. Dziękuję za wzmiankęvolatile
, dodam to do mojej odpowiedzi (i zmienię inline na static, więc działa dla wersji C ++ 17).GPIOA::togglePin()
.Na arenie systemów wbudowanych można modelować sprzęt za pomocą struktury lub definiując wskaźniki do adresów rejestrów.
Modelowanie według struktury nie jest zalecane, ponieważ kompilator może dodawać wypełnienie między elementami w celu wyrównywania (chociaż wiele kompilatorów dla systemów osadzonych ma pragmę pakowania struktury).
Przykład:
uint16_t * const UART1 = (uint16_t *)(0x40000); const unsigned int UART_STATUS_OFFSET = 1U; const unsigned int UART_TRANSMIT_REGISTER = 2U; uint16_t * const UART1_STATUS_REGISTER = (UART1 + UART_STATUS_OFFSET); uint16_t * const UART1_TRANSMIT_REGISTER = (UART1 + UART_TRANSMIT_REGISTER);
Możesz również użyć notacji tablicowej:
uint16_t status = UART1[UART_STATUS_OFFSET];
Jeśli musisz użyć struktury, IMHO, najlepszą metodą pomijania adresów byłoby zdefiniowanie członka i brak dostępu do niego:
struct UART1 { uint16_t status; uint16_t reserved1; // Transmit register uint16_t receive_register; };
W jednym z naszych projektów mamy zarówno stałe, jak i struktury od różnych dostawców (sprzedawca 1 używa stałych, podczas gdy sprzedawca 2 używa struktur).
źródło
static
elementy adresu w strukturze, przy założeniu, że autouzupełnianie może wyświetlać statyczne elementy członkowskie. Jeśli nie, mogą to być również wbudowane funkcje składowe.public:
iprivate:
tyle razy, ile chcesz, aby uzyskać prawidłową kolejność pól.)public
iprivate
niestatyczne elementy składowe danych, to nie jest to standardowy typ układu , więc nie zapewnia kolejności, o której myślisz. (I jestem prawie pewien, że przypadek użycia OP wymaga standardowego typu układu.)volatile
o tych deklaracjach, BTW, dla rejestrów I / O mapowanych w pamięci.geza ma rację, że naprawdę nie chcesz do tego używać klas.
Ale gdybyś nalegał, najlepszym sposobem na dodanie nieużywanego elementu o szerokości n bajtów jest po prostu zrobienie tego:
char unused[n];
Może to zadziałać, jeśli dodasz pragmę specyficzną dla implementacji, aby zapobiec dodawaniu arbitralnego dopełnienia do elementów członkowskich klasy.
W przypadku GNU C / C ++ (gcc, clang i innych, które obsługują te same rozszerzenia), jednym z prawidłowych miejsc na umieszczenie atrybutu jest:
#include <stddef.h> #include <stdint.h> #include <assert.h> // for C11 static_assert, so this is valid C as well as C++ struct __attribute__((packed)) GPIO { volatile uint32_t a; char unused[3]; volatile uint32_t b; }; static_assert(offsetof(struct GPIO, b) == 7, "wrong GPIO struct layout");
(przykład w eksploratorze kompilatora Godbolt pokazujący
offsetof(GPIO, b)
= 7 bajtów.)źródło
Aby rozwinąć odpowiedzi @ Clifforda i @Adama Kotwasinskiego:
#define REP10(a) a a a a a a a a a a #define REP1034(a) REP10(REP10(REP10(a))) REP10(a a a) a a a a struct foo { int before; REP1034(unsigned int :32;) int after; }; int main(void){ struct foo bar; return 0; }
źródło
Aby rozwinąć odpowiedź Clifforda, zawsze możesz makro anonimowe pola bitowe.
Więc zamiast
uint32_t :160;
posługiwać się
#define EMPTY_32_1 \ uint32_t :32 #define EMPTY_32_2 \ uint32_t :32; \ // I guess this also can be replaced with uint64_t :64 uint32_t :32 #define EMPTY_32_3 \ uint32_t :32; \ uint32_t :32; \ uint32_t :32 #define EMPTY_UINT32(N) EMPTY_32_ ## N
A potem użyj tego jak
struct A { EMPTY_UINT32(3); /* which resolves to EMPTY_32_3, which then resolves to real declarations */ }
Niestety, będziesz potrzebować tylu
EMPTY_32_X
wariantów, ile masz bajtów :( Mimo to pozwala ci mieć pojedyncze deklaracje w twojej strukturze.źródło
#define EMPTY_32_2 EMPTY_32_1; EMPTY_32_1
i#define EMPTY_32_3 EMPTY_32_2; EMPTY_32_1
itd.Aby zdefiniować duży odstępnik jako grupy 32-bitowe.
#define M_32(x) M_2(M_16(x)) #define M_16(x) M_2(M_8(x)) #define M_8(x) M_2(M_4(x)) #define M_4(x) M_2(M_2(x)) #define M_2(x) x x #define SPACER int : 32; struct { M_32(SPACER) M_8(SPACER) M_4(SPACER) };
źródło
Myślę, że korzystne byłoby wprowadzenie większej struktury; co z kolei może rozwiązać problem elementów dystansowych.
Nazwij warianty
Chociaż płaskie przestrzenie nazw są fajne, problem polega na tym, że otrzymujesz pstrokaty zbiór pól i nie ma prostego sposobu na przekazanie wszystkich powiązanych pól razem. Ponadto, używając anonimowych struktur w anonimowej unii, nie można przekazywać odwołań do samych struktur ani używać ich jako parametrów szablonu.
Dlatego w pierwszej kolejności rozważyłbym wyłamanie
struct
:// GpioMap0.h #pragma once // #includes namespace Gpio { struct Map0 { GPIO_MAP0_MODER; GPIO_MAP0_OTYPER; GPIO_MAP0_OSPEEDR; GPIO_MAP0_PUPDR; GPIO_MAP0_IDR; GPIO_MAP0_ODR; GPIO_MAP0_BSRR; GPIO_MAP0_LCKR; GPIO_MAP0_AFR; GPIO_MAP0_BRR; GPIO_MAP0_ASCR; }; } // namespace Gpio // GpioMap1.h #pragma once // #includes namespace Gpio { struct Map1 { // fields }; } // namespace Gpio // ... others headers ...
I na koniec globalny nagłówek:
// Gpio.h #pragma once #include "GpioMap0.h" #include "GpioMap1.h" // ... other headers ... namespace Gpio { union Gpio { Map0 map0; Map1 map1; // ... others ... }; } // namespace Gpio
Teraz mogę napisać
void special_map0(Gpio:: Map0 volatile& map);
, a także uzyskać szybki przegląd wszystkich dostępnych architektur.Proste elementy dystansowe
Dzięki rozdzieleniu definicji na wiele nagłówków, poszczególne nagłówki są znacznie łatwiejsze do zarządzania.
Dlatego moim początkowym podejściem, aby dokładnie spełnić Twoje wymagania, byłoby trzymanie się powtarzania
std::uint32_t:32;
. Tak, dodaje kilka 100s linii do istniejących 8k linii, ale ponieważ każdy nagłówek jest indywidualnie mniejszy, może nie być tak zły.Jeśli jednak chcesz rozważyć bardziej egzotyczne rozwiązania ...
Przedstawiamy $.
Jest to mało znany fakt, że
$
jest to odpowiedni znak dla identyfikatorów C ++; jest to nawet odpowiedni znak początkowy (w przeciwieństwie do cyfr).$
Pojawiające się w kodzie źródłowym najprawdopodobniej podniesie brwi, a$$$$
na pewno będzie przyciągać uwagę podczas przeglądów kodu. To jest coś, z czego możesz łatwo skorzystać:#define GPIO_RESERVED(Index_, N_) std::uint32_t $$$$##Index_[N_]; struct Map3 { GPIO_RESERVED(0, 6); GPIO_MAP2_BSRRL; GPIO_MAP2_BSRRH; GPIO_RESERVED(1, 5); };
Możesz nawet złożyć prosty "lint" jako punkt zaczepienia przed zatwierdzeniem lub w swoim CI, który szuka
$$$$
w zatwierdzonym kodzie C ++ i odrzuca takie zatwierdzenia.źródło
GPIO_MAP0_MODER
przypuszczalnie każdy członek jest podobnyvolatile
). Możliwe, że odniesienie lub użycie parametru szablonu przez wcześniej anonimowych członków może być jednak przydatne. I oczywiście w ogólnym przypadku struktur wypełniających. Ale przypadek użycia wyjaśnia, dlaczego PO pozostawił ich anonimowych.$$$padding##Index_[N_];
aby nazwa pola była bardziej zrozumiała na wypadek, gdyby pojawiła się podczas autouzupełniania lub podczas debugowania. (Lubzz$$$padding
żeby sortować poGPIO...
nazwach, ponieważ celem tego ćwiczenia zgodnie z OP jest ładniejsze autouzupełnianie dla mapowanych w pamięci nazw lokalizacji we / wy.)volatile
kwalifikatora w referencji, który został poprawiony. Jeśli chodzi o nazewnictwo; Zwolnię to do OP. Istnieje wiele odmian (dopełnienie, zarezerwowane, ...), a nawet „najlepszy” prefiks do automatycznego uzupełniania może zależeć od dostępnego IDE, chociaż doceniam pomysł poprawiania sortowania.struct
) i żeunion
ostatecznie są one propagowane wszędzie, nawet w bitach specyficznych dla architektury, które mniej przejmować się innymi.Chociaż zgadzam się, że struktury nie powinny być używane do dostępu do portu we / wy MCU, na oryginalne pytanie można odpowiedzieć w ten sposób:
struct __attribute__((packed)) test { char member1; char member2; volatile struct __attribute__((packed)) { private: volatile char spacer_bytes[7]; } spacer; char member3; char member4; };
Być może trzeba wymienić
__attribute__((packed))
z#pragma pack
lub podobny w zależności od składni kompilatora.Mieszanie prywatnych i publicznych elementów członkowskich w strukturze zwykle powoduje, że układ pamięci nie jest już gwarantowany przez standard C ++. Jednak jeśli wszystkie niestatyczne elementy członkowskie struktury są prywatne, to nadal jest uważane za układ POD / standardowy, podobnie jak struktury, które je osadzają.
Z jakiegoś powodu gcc wyświetla ostrzeżenie, jeśli członek anonimowej struktury jest prywatny, więc musiałem nadać mu nazwę. Alternatywnie, umieszczenie go w kolejnej anonimowej strukturze również usuwa ostrzeżenie (może to być błąd).
Pamiętaj, że
spacer
członek nie jest sam w sobie prywatny, więc nadal można uzyskać dostęp do danych w ten sposób:(char*)(void*)&testobj.spacer;
Jednak takie wyrażenie wygląda jak oczywisty hack i miejmy nadzieję, że nie zostanie użyte bez naprawdę dobrego powodu, nie mówiąc już o błędzie.
źródło
Anty-rozwiązanie.
NIE ROBIĆ TEGO: Mieszaj pola prywatne i publiczne.
Może przyda się makro z licznikiem do generowania unikalnych nazw zmiennych?
#define CONCAT_IMPL( x, y ) x##y #define MACRO_CONCAT( x, y ) CONCAT_IMPL( x, y ) #define RESERVED MACRO_CONCAT(Reserved_var, __COUNTER__) struct { GPIO_MAP1_CRL; GPIO_MAP1_CRH; GPIO_MAP1_IDR; GPIO_MAP1_ODR; GPIO_MAP1_BSRR; GPIO_MAP1_BRR; GPIO_MAP1_LCKR; private: char RESERVED[4]; public: GPIO_MAP1_AFRL; GPIO_MAP1_AFRH; private: char RESERVED[8]; };
źródło