c++ c memory-management low-level bare-metal

¿Cómo creo un "espaciador" en una estructura de memoria de clase C++?



memory-management low-level (10)

¿Qué tal un C ++ - ish manera?

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

Obtiene autocompletar debido al espacio de nombres de GPIO , y no hay necesidad de relleno ficticio. Incluso, está más claro lo que está sucediendo, ya que puede ver la dirección de cada registro, no tiene que confiar en el comportamiento de relleno del compilador en absoluto.

La cuestión

En un contexto incrustado de bajo nivel integrado , me gustaría crear un espacio en blanco en la memoria, dentro de una estructura C ++ y sin ningún nombre, para prohibir al usuario acceder a dicha ubicación de memoria.

En este momento, lo he logrado al poner un feo uint32_t :96; El campo de bits que convenientemente tomará el lugar de tres palabras, pero generará una advertencia de GCC (Campo de bits demasiado grande para caber en uint32_t), que es bastante legítimo.

Si bien funciona bien, no está muy limpio cuando desea distribuir una biblioteca con varios cientos de esas advertencias ...

¿Cómo hago eso correctamente?

¿Por qué hay un problema en primer lugar?

El proyecto en el que estoy trabajando consiste en definir la estructura de memoria de diferentes periféricos de una línea completa de microcontroladores (STMicroelectronics STM32). Para hacerlo, el resultado es una clase que contiene una unión de varias estructuras que definen todos los registros, dependiendo del microcontrolador seleccionado.

Un ejemplo simple para un periférico bastante simple es el siguiente: una entrada / salida de propósito general (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; }; };

Donde todo GPIO_MAPx_YYY es una macro, definida como uint32_t :32 o el tipo de registro (una estructura dedicada).

Aquí puedes ver el uint32_t :192; que funciona bien, pero se dispara una advertencia.

Lo que he considerado hasta ahora:

Podría haberlo reemplazado por varios uint32_t :32; (6 aquí), pero tengo algunos casos extremos en los que tengo uint32_t :1344; (42) (entre otros). Por lo tanto, preferiría no agregar unas cien líneas por encima de otras 8k, aunque la generación de la estructura esté en un script.

El mensaje de advertencia exacto es algo como: width of ''sool::ll::GPIO::<anonymous union>::<anonymous struct>::<anonymous>'' exceeds its type (me encanta lo sombrío que es).

Preferiría no resolver esto simplemente eliminando la advertencia, pero el uso de

#pragma GCC diagnostic push #pragma GCC diagnostic ignored "-WTheRightFlag" /* My code */ #pragma GCC diagnostic pop

puede ser una solución ... si encuentro TheRightFlag . Sin embargo, como se señaló en este hilo , gcc/cp/class.c con esta parte del código triste:

warning_at (DECL_SOURCE_LOCATION (field), 0, "width of %qD exceeds its type", field);

Lo que nos dice que no hay -Wxxx indicador -Wxxx para eliminar esta advertencia ...


Anti-solución.

NO HAGA ESTO: mezcle campos privados y públicos.

Tal vez una macro con un contador para generar nombres de variables uniqie sería útil?

#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]; };


Aunque estoy de acuerdo en que las estructuras no deben usarse para el acceso del puerto de E / S de MCU, la pregunta original se puede responder de esta manera:

struct __attribute__((packed)) test { char member1; char member2; volatile struct __attribute__((packed)) { private: volatile char spacer_bytes[7]; } spacer; char member3; char member4; };

Es posible que deba reemplazar __attribute__((packed)) con #pragma pack o similar, según la sintaxis de su compilador.

La combinación de miembros públicos y privados en una estructura normalmente resulta en que el diseño de la memoria ya no está garantizado por el estándar C ++. Sin embargo, si todos los miembros no estáticos de una estructura son privados, todavía se considera POD / diseño estándar, y también lo son las estructuras que los integran.

Por alguna razón, gcc produce una advertencia si un miembro de una estructura anónima es privado, así que tuve que darle un nombre. Alternativamente, envolverlo en otra estructura anónima también elimina la advertencia (esto puede ser un error).

Tenga en cuenta que el miembro spacer no es en sí mismo privado, por lo que aún se puede acceder a los datos de esta manera:

(char*)(void*)&testobj.spacer;

Sin embargo, tal expresión parece un truco obvio, y es de esperar que no se use sin una buena razón, y mucho menos como un error.


Creo que sería beneficioso introducir algo más de estructura; Lo que puede, a su vez, resolver el problema de los espaciadores.

Nombra las variantes

Si bien los espacios de nombres planos son agradables, el problema es que terminas con una colección variada de campos y no hay una forma sencilla de pasar todos los campos relacionados juntos. Además, al usar estructuras anónimas en una unión anónima, no puede pasar referencias a las estructuras en sí mismas, ni usarlas como parámetros de plantilla.

Como primer paso, consideraría, por lo tanto, dividir la 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 ...

Y finalmente, el encabezado global:

// Gpio.h #pragma once #include "GpioMap0.h" #include "GpioMap1.h" // ... other headers ... namespace Gpio { union Gpio { Map0 map0; Map1 map1; // ... others ... }; } // namespace Gpio

Ahora, puedo escribir un void special_map0(Gpio:: Map0 volatile& map); , así como obtener una visión general rápida de todas las arquitecturas disponibles de un vistazo.

Simples espaciadores

Con la definición dividida en múltiples encabezados, los encabezados son individualmente mucho más manejables.

Por lo tanto, mi enfoque inicial para cumplir exactamente con sus requisitos sería seguir con std::uint32_t:32; . Sí, agrega unas pocas líneas de 100s a las 8k existentes, pero como cada encabezado es individualmente más pequeño, puede que no sea tan malo.

Si estás dispuesto a considerar soluciones más exóticas, aunque ...

Introduciendo $.

Es un hecho poco conocido que $ es un carácter viable para los identificadores de C ++; Es incluso un carácter inicial viable (a diferencia de los dígitos).

Un $ aparece en el código fuente probablemente provocaría problemas, y $$$$ definitivamente atraerá la atención durante las revisiones del código. Esto es algo que puedes aprovechar fácilmente:

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

Incluso puede juntar una "pelusa" simple como un gancho de confirmación previa o en su CI que busca $$$$ en el código de C ++ confirmado y rechazar tales confirmaciones.


En el ámbito de los sistemas integrados, puede modelar hardware utilizando una estructura o definiendo punteros a las direcciones de registro.

No se recomienda el modelado por estructura porque al compilador se le permite agregar relleno entre miembros para propósitos de alineación (aunque muchos compiladores para sistemas integrados tienen un pragma para empaquetar la estructura).

Ejemplo:

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

También podría utilizar la notación de matriz:

uint16_t status = UART1[UART_STATUS_OFFSET];

Si debe usar la estructura, en mi humilde opinión, el mejor método para omitir direcciones sería definir un miembro y no acceder a él:

struct UART1 { uint16_t status; uint16_t reserved1; // Transmit register uint16_t receive_register; };

En uno de nuestros proyectos tenemos constantes y estructuras de diferentes proveedores (el proveedor 1 usa constantes, mientras que el proveedor 2 usa estructuras).


Geza tiene razón en que realmente no quieres usar clases para esto.

Pero, si insistiera, la mejor manera de agregar un miembro no utilizado de n bytes de ancho, es simplemente hacerlo:

char unused[n];

Si agrega un pragma específico de la implementación para evitar que se agregue un relleno arbitrario a los miembros de la clase, esto puede funcionar.

Para GNU C / C ++ (gcc, clang y otros que admiten las mismas extensiones), uno de los lugares válidos para colocar el atributo es:

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

(Ejemplo en el explorador del compilador Godbolt que muestra offsetof(GPIO, b) = 7 bytes.)


Para ampliar las respuestas de @Clifford y @Adam Kotwasinski:

#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; }


Para definir un espaciador grande como grupos de 32 bits.

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


Para expandir la respuesta de Clifford, siempre puede macro macroizar los campos de bits anónimos.

Así que en lugar de

uint32_t :160;

utilizar

#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

Y luego usarlo como

struct A { EMPTY_UINT32(3); /* which resolves to EMPTY_32_3, which then resolves to real declarations */ }

Desafortunadamente, necesitará tantas variantes de EMPTY_32_X como tantos bytes que tenga :( Aún así, le permite tener declaraciones individuales en su estructura.


Utilice múltiples campos de bits anónimos adyacentes. Así que en lugar de:

uint32_t :160;

por ejemplo, tendrías:

uint32_t :32; uint32_t :32; uint32_t :32; uint32_t :32; uint32_t :32;

Una por cada registro que quieras que sea anónimo.

Si tiene espacios grandes para llenar, puede ser más claro y menos propenso a errores al usar macros para repetir el espacio único de 32 bits. Por ejemplo, dado:

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

Luego se puede agregar un espacio de 1344 (42 * 32 bits) así:

struct { ... REPEAT_32(uint32_t :32;) REPEAT_8(uint32_t :32;) REPEAT_2(uint32_t :32;) ... };