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

12 minutos de lectura

avatar de usuario
J Faucher

La cuestión

en un incrustado de metal desnudo de bajo nivel contexto, 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.

Ahora mismo lo he conseguido poniendo un feo uint32_t :96; bitfield que convenientemente tomará el lugar de tres palabras, pero generará una advertencia de GCC (Bitfield demasiado grande para caber en uint32_t), lo cual es bastante legítimo.

Si bien funciona bien, no es 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 toda una línea 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 de destino.

Un ejemplo simple para un periférico bastante simple es el siguiente: una entrada/salida de uso 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 todos GPIO_MAPx_YYY es una macro, definida como uint32_t :32 o el tipo de registro (una estructura dedicada).

Aquí ves el uint32_t :192; que funciona bien, pero activa una advertencia.

Lo que he considerado hasta ahora:

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

El mensaje de advertencia exacto es algo así 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 ha señalado en este hilo, gcc/cp/class.c con esta triste parte del código:

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

Lo que nos dice que no hay -Wxxx marcar para eliminar esta advertencia…

  • has considerado char unused[12]; ¿y así?

    –MM

    1 de noviembre de 2018 a las 21:56

  • Simplemente suprimiría la advertencia. [class.bit]/1 garantiza el comportamiento de uint32_t :192;.

    – NathanOliver

    1 de noviembre de 2018 a las 22:07

  • @NathanOliver Yo también lo haría con mucho gusto, pero parece que esta advertencia no se puede suprimir (usando GCC) o no encontré cómo hacerlo. Además, todavía no es una forma limpia de hacerlo (pero sería bastante satisfactorio). Logré encontrar el indicador “-W” correcto pero no logré aplicarlo solo en mis propios archivos (no quiero que el usuario elimine este tipo de advertencias para su trabajo)

    – J Faucher

    1 de noviembre de 2018 a las 22:13


  • Por cierto puedes escribir :42*32 en vez de :1344

    –MM

    1 de noviembre de 2018 a las 22:15

  • ¿Intentar esto para suprimir las advertencias? gcc.gnu.org/onlinedocs/gcc/…

    – Hitobat

    1 de noviembre de 2018 a las 22:26

avatar de usuario
gueza

¿Qué tal una forma de C++-ish?

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 autocompletado debido a la GPIO espacio de nombres, y no hay necesidad de relleno ficticio. Incluso, es más claro lo que está pasando, ya que puede ver la dirección de cada registro, no tiene que confiar en absoluto en el comportamiento de relleno del compilador.

  • Esto podría optimizarse menos que una estructura para acceder a múltiples registros MMIO desde la misma función. Con un puntero a la dirección base en un registro, el compilador puede usar instrucciones de carga/almacenamiento con desplazamientos inmediatos, como ldr r0, [r4, #16], mientras que es más probable que los compiladores pierdan esa optimización con cada dirección declarada por separado. GCC probablemente cargará cada dirección GPIO en un registro separado. (De un grupo literal, aunque algunos de ellos se pueden representar como inmediatos rotados en la codificación Thumb).

    – Peter Cordes

    2 de noviembre de 2018 a las 2:12

  • Resulta que mis preocupaciones eran infundadas; ARM GCC también se optimiza de esta manera. godbolt.org/z/ztB7hi. Pero ten en cuenta que quieres static volatile uint32_t &MAP0_MODERno inline. Un inline variable no se compila. (static evita tener cualquier almacenamiento estático para el puntero, y volatile es exactamente lo que desea para MMIO para evitar la eliminación de almacenamiento inactivo o la optimización de la escritura/relectura).

    – Peter Cordes

    2 de noviembre de 2018 a las 2:59

  • @PeterCordes: las variables en línea son una característica nueva de C++17. Pero usted está en lo correcto, static funciona igualmente bien para este caso. gracias por mencionar volatilelo agregaré a mi respuesta (y cambiaré en línea a estático, para que funcione antes de C++ 17).

    – geza

    2 de noviembre de 2018 a las 3:07

  • Este no es un comportamiento estrictamente bien definido ver este hilo de twitter y tal vez este sea útil

    – Shafik Yaghmour

    2 de noviembre de 2018 a las 6:39

  • @JFaucher: cree tantos espacios de nombres como estructuras tenga y use funciones independientes en ese espacio de nombres. Entonces, tendrás GPIOA::togglePin().

    – geza

    2 de noviembre de 2018 a las 13:44

avatar de usuario
clifford

Utilice varios 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;

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

Si tiene grandes espacios para llenar, puede ser más claro y menos propenso a errores 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;)
    ...
};

  • Gracias por la respuesta. Ya lo consideré, sin embargo, agregaría más de 200 líneas en algunos de mis archivos (uint32_t :1344; está en el lugar) así que preferiría no tener que ir por este camino…

    – J Faucher

    1 de noviembre de 2018 a las 23:11

  • @JFaucher Agregó una posible solución a su requisito de conteo de líneas. Si tiene tales requisitos, puede mencionarlos en la pregunta para evitar obtener respuestas que no los cumplan.

    – Clifford

    2 de noviembre de 2018 a las 12:30

  • Gracias por la edición y perdón por no mencionar el número de líneas. Mi punto es que mi código ya es doloroso para sumergirse ya que hay muchas líneas y prefiero evitar agregar demasiadas más. Por lo tanto, estaba preguntando si alguien conocía una forma “limpia” u “oficial” de evitar el uso de un campo de bits anónimo adyacente (incluso si funciona bien). Sin embargo, el enfoque macro me parece bien. Por cierto, en tu ejemplo, ¿no tienes un espacio de 36*32 bits?

    – J Faucher

    2 de noviembre de 2018 a las 13:15

  • @JFaucher – corregido. Los archivos de mapeo de registros de E/S son necesariamente grandes debido a la gran cantidad de registros; normalmente se escribe una vez y el mantenimiento no es un problema porque el hardware es una constante. Excepto al “ocultar” los registros, usted está haciendo el trabajo de mantenimiento por sí mismo si luego necesita acceder a ellos. ¿Sabe, por supuesto, que todos los dispositivos STM32 ya tienen un encabezado de mapa de registro proporcionado por el proveedor? Sería mucho menos propenso a errores usar eso.

    – Clifford

    2 de noviembre de 2018 a las 14:13


  • Estoy de acuerdo con usted y, para ser justos, creo que seguiré uno de esos dos métodos que se muestran en su respuesta. Solo quería asegurarme de que C++ no proporcione una mejor solución antes de hacerlo. Soy muy consciente de que ST proporciona esos encabezados, sin embargo, se basan en el uso masivo de macros y operaciones bit a bit. Mi proyecto es construir un C++ equivalente a esos encabezados que serán menos propenso a errores (usando clases de enumeración, campos de bits, etc.). Es por eso que usamos un script para “traducir” los encabezados CMSIS a nuestras estructuras C++ (y encontramos algunos errores en los archivos ST por cierto)

    – J Faucher

    2 de noviembre de 2018 a las 14:33


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

No se recomienda el modelado por estructura porque el compilador puede agregar relleno entre los miembros con fines 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 puede usar 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).

  • Gracias por tu respuesta. Sin embargo, elijo usar un enfoque de estructura para facilitar el trabajo del usuario cuando obtuvo una función de autocompletar (solo se mostrarán los atributos correctos) y no quiero “mostrar” al usuario las ranuras reservadas como señalado en un comentario de mi primer post.

    – J Faucher

    1 de noviembre de 2018 a las 23:01

  • Todavía puede tener eso haciendo la dirección anterior static miembros de una estructura, suponiendo que el autocompletado pueda mostrar miembros estáticos. Si no, también pueden ser funciones miembro en línea.

    – Phil1970

    2 de noviembre de 2018 a las 1:09

  • @JFaucher No soy una persona de sistemas integrados y no he probado esto, pero ¿no se resolvería el problema de autocompletar declarando privado al miembro reservado? (Puede declarar miembros privados en una estructura y puede usar public: y private: tantas veces como desee, para obtener el orden correcto de los campos).

    – N. Virgo

    2 de noviembre de 2018 a las 6:13


  • @Nathaniel: No es así; si una clase tiene ambos public y private miembros de datos no estáticos, entonces no es un tipo de diseño estándar, por lo que no proporciona las garantías de pedido que está pensando. (Y estoy bastante seguro de que el caso de uso del OP requiere un tipo de diseño estándar).

    – ruakh

    2 de noviembre de 2018 a las 6:42


  • no olvides volatile en esas declaraciones, por cierto, para registros de E/S mapeados en memoria.

    – Peter Cordes

    2 de noviembre de 2018 a las 9:11

avatar de usuario
Carreras de ligereza en órbita

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

Pero, si tuviera que insistir, la mejor manera de agregar un miembro no utilizado de norte ancho de bytes, es simplemente hacerlo:

char unused[n];

Si agrega un pragma específico de la implementación para evitar la adición de 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 demostración offsetof(GPIO, b) = 7 bytes.)

avatar de usuario
mosvy

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

  • He incorporado una variante de su sugerencia en mi respuesta siguiendo más requisitos en un comentario. Crédito a quien crédito merece.

    – Clifford

    2 de noviembre de 2018 a las 12:34

Para ampliar la respuesta de Clifford, siempre puede eliminar los campos de bits anónimos.

Entonces, en lugar de

uint32_t :160;

usar

#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 úsalo como

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

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

  • He incorporado una variante de su sugerencia en mi respuesta siguiendo más requisitos en un comentario. Crédito a quien crédito merece.

    – Clifford

    2 de noviembre de 2018 a las 12:34

avatar de usuario
jxh

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

¿Ha sido útil esta solución?