Posibilidades para asignar memoria para el diseño de firmware modular en C

16

Los enfoques modulares son bastante útiles en general (portátiles y limpios), por lo que trato de programar los módulos de la forma más independiente posible. La mayoría de mis enfoques se basan en una estructura que describe el módulo en sí. Una función de inicialización establece los parámetros primarios, luego se pasa un controlador (puntero a la estructura descriptiva) a cualquier función dentro del módulo que se llame.

En este momento, me pregunto cuál puede ser el mejor enfoque de la memoria de asignación para la estructura que describe un módulo. Si es posible, me gustaría lo siguiente:

  • Estructura opaca, por lo que la estructura solo puede alterarse mediante el uso de funciones de interfaz proporcionadas
  • Múltiples instancias
  • memoria asignada por el enlazador

Veo las siguientes posibilidades, que todos entran en conflicto con uno de mis objetivos:

declaración global

múltiples instancias, todas citadas por el enlazador, pero struct no es opaco

(#includes)
module_struct module;

void main(){
   module_init(&module);
}

malloc

estructura opaca, múltiples instancias, pero allcotion en el montón

en module.h:

typedef module_struct Module;

en la función init module.c, malloc y el puntero de retorno a la memoria asignada

module_mem = malloc(sizeof(module_struct ));
/* initialize values here */
return module_mem;

en main.c

(#includes)
Module *module;

void main(){
    module = module_init();
}

declaración en módulo

estructura opaca, asignada por el vinculador, solo un número predefinido de instancias

mantenga toda la estructura y la memoria internas del módulo y nunca exponga un controlador o estructura.

(#includes)

void main(){
    module_init(_no_param_or_index_if_multiple_instances_possible_);
}

¿Existe una opción para combinar estos de alguna manera para estructura opaca, enlazador en lugar de asignación de montón y múltiples / cualquier número de instancias?

solución

Como se propone en algunas respuestas a continuación, creo que la mejor manera es:

  1. reservar espacio para módulos MODULE_MAX_INSTANCE_COUNT en el archivo fuente de módulos
  2. no defina MODULE_MAX_INSTANCE_COUNT en el módulo en sí
  3. agregue un #ifndef MODULE_MAX_INSTANCE_COUNT #error al archivo de encabezado de los módulos para asegurarse de que el usuario de los módulos esté al tanto de esta limitación y defina el número máximo de instancias deseadas para la aplicación
  4. en la inicialización de una instancia, devuelva la dirección de memoria (* void) de la estructura descriptiva o el índice de módulos (lo que quiera más)
L. Heinrichs
fuente
12
La mayoría de los diseñadores de FW integrados están evitando la asignación dinámica para mantener el uso de memoria determinista y simple. Especialmente si es de metal desnudo y no tiene un SO subyacente para administrar la memoria.
Eugene Sh.
Exactamente, por eso quiero que el vinculador haga las asignaciones.
L. Heinrichs
44
No estoy seguro de entender ... ¿Cómo podría tener la memoria asignada por el enlazador si tiene un número dinámico de instancias? Eso me parece bastante ortogonal.
jcaron
¿Por qué no dejar que el vinculador asigne un gran conjunto de memoria y haga sus propias asignaciones a partir de eso, lo que también le brinda el beneficio de un asignador de sobrecarga cero? Puede hacer que el objeto de grupo esté estático en el archivo con la función de asignación para que sea privado. En algunos de mis códigos, hago todas las asignaciones en las diversas rutinas de inicio, luego imprimo cuánto se asignó, por lo que en la compilación de producción final establecí el grupo en ese tamaño exacto.
Lee Daniel Crocker
2
Si se trata de una decisión en tiempo de compilación, simplemente puede definir el número en su Makefile o equivalente, y ya está todo listo. El número no estaría en la fuente del módulo, sino que sería específico de la aplicación, y solo usará un número de instancia como parámetro.
jcaron

Respuestas:

4

¿Existe alguna opción para combinar estos de alguna manera para una estructura anónima, un enlazador en lugar de una asignación de montón y múltiples / cualquier número de instancias?

Claro que lo hay. Primero, sin embargo, reconozca que "cualquier número" de instancias debe ser fijo, o al menos un límite superior establecido, en tiempo de compilación. Este es un requisito previo para que las instancias se asignen estáticamente (lo que se llama "asignación de vinculador"). Puede hacer que el número sea ajustable sin modificación de fuente al declarar una macro que lo especifique.

Luego, el archivo fuente que contiene la declaración de estructura real y todas sus funciones asociadas también declara una matriz de instancias con enlace interno. Proporciona una matriz, con enlace externo, de punteros a las instancias o una función para acceder a los diversos punteros por índice. La variación de la función es un poco más modular:

módulo.c

#include <module.h>

// 4 instances by default; can be overridden at compile time
#ifndef NUM_MODULE_INSTANCES
#define NUM_MODULE_INSTANCES 4
#endif

struct module {
    int demo;
};

// has internal linkage, so is not directly visible from other files:
static struct module instances[NUM_MODULE_INSTANCES];

// module functions

struct module *module_init(unsigned index) {
    instances[index].demo = 42;
    return &instances[index];
}

Supongo que ya está familiarizado con cómo el encabezado declararía la estructura como un tipo incompleto y declararía todas las funciones (escritas en términos de punteros a ese tipo). Por ejemplo:

módulo.h

#ifndef MODULE_H
#define MODULE_H

struct module;

struct module *module_init(unsigned index);

// other functions ...

#endif

Ahora struct modulees opaco en unidades de traducción distintas a *module.c , y puede acceder y utilizar hasta el número de instancias definidas en tiempo de compilación sin ninguna asignación dinámica.


* A menos que copie su definición, por supuesto. El punto es que module.hno hace eso.

John Bollinger
fuente
Creo que es un diseño extraño pasar el índice desde fuera de la clase. Cuando implemento agrupaciones de memoria como esta, dejo que el índice sea un contador privado, aumentando en 1 por cada instancia asignada. Hasta llegar a "NUM_MODULE_INSTANCES", donde el constructor devolverá un error de falta de memoria.
Lundin
Ese es un punto justo, @Lundin. Ese aspecto del diseño supone que los índices tienen un significado inherente, que puede o no ser el caso. Que es el caso, aunque sea trivial por lo que, para el caso a partir de la OP. Tal importancia, si existe, podría respaldarse aún más proporcionando un medio para obtener un puntero de instancia sin inicializar.
John Bollinger
Básicamente, usted reserva memoria para n módulos, sin importar cuántos se usarán, luego devuelve un puntero al siguiente elemento no utilizado si la aplicación lo inicializa. Supongo que eso podría funcionar.
L. Heinrichs
@ L.Heinrichs Sí, ya que los sistemas integrados son de naturaleza determinista. No existe la "cantidad interminable de objetos" ni la "cantidad desconocida de objetos". Los objetos a menudo también son singletons (controladores de hardware), por lo que a menudo no hay necesidad de la agrupación de memoria, ya que solo existirá una única instancia del objeto.
Lundin
Estoy de acuerdo en la mayoría de los casos. La pregunta también tenía cierto interés teórico. Pero podría usar cientos de sensores de temperatura de 1 cable si hay suficientes IO disponibles (como el único ejemplo que puedo encontrar ahora).
L. Heinrichs
22

Programa pequeños microcontroladores en C ++, que logra exactamente lo que quieres.

Lo que llama un módulo es una clase C ++, puede contener datos (accesibles externamente o no) y funciones (del mismo modo). El constructor (una función dedicada) lo inicializa. El constructor puede tomar parámetros de tiempo de ejecución o (mi favorito) parámetros de tiempo de compilación (plantilla). Las funciones dentro de la clase obtienen implícitamente la variable de clase como primer parámetro. (O, a menudo, mi preferencia, la clase puede actuar como un singleton oculto, por lo que se accede a todos los datos sin esta sobrecarga).

El objeto de clase puede ser global (para que sepa en el momento del enlace que todo encajará), o local de pila, presumiblemente en el principal. (No me gustan los globales de C ++ debido al orden de inicialización global indefinido, por lo que prefiero stack-local).

Mi estilo de programación preferido es que los módulos son clases estáticas, y su configuración (estática) es por parámetros de plantilla. Esto evita casi todo exceso y permite la optimización. Combina esto con una herramienta que calcula el tamaño de la pila y puedes dormir sin preocupaciones :)

Mi charla sobre esta forma de codificación en C ++: ¿Objetos? ¡No, gracias!

A muchos programadores integrados / microcontroladores no les gusta C ++ porque piensan que los obligaría a usar todo C ++. Eso no es absolutamente necesario, y sería una muy mala idea. (¡Probablemente tampoco use todo C! Piense en montón, coma flotante, setjmp / longjmp, printf, ...)


En un comentario, Adam Haun menciona RAII e inicialización. IMO RAII tiene más que ver con la deconstrucción, pero su punto es válido: los objetos globales se construirán antes de que comience su inicio principal, por lo que podrían funcionar en suposiciones no válidas (como una velocidad de reloj principal que se cambiará más adelante). Esa es una razón más para NO usar objetos globalizados con código inicializado. (Uso un script de enlazador que fallará cuando tenga objetos globalizados con código inicializado). OMI, tales 'objetos' deberían crearse y pasarse explícitamente. Esto incluye un 'objeto' de facilidad de 'espera' que proporciona una función wait (). En mi configuración, este es un 'objeto' que establece la velocidad de reloj del chip.

Hablando de RAII: esa es una característica más de C ++ que es muy útil en sistemas integrados pequeños, aunque no por la razón (desasignación de memoria) que más se usa en sistemas grandes (los sistemas integrados pequeños en su mayoría no usan desasignación de memoria dinámica). Piense en bloquear un recurso: puede hacer que el recurso bloqueado sea un objeto contenedor y restringir el acceso al recurso para que solo sea posible a través del contenedor de bloqueo. Cuando el contenedor queda fuera de alcance, el recurso se desbloquea. Esto impide el acceso sin bloqueo y hace que sea mucho más improbable que olvide el desbloqueo. con algo de magia (plantilla) puede ser cero sobrecarga.


La pregunta original no mencionaba C, de ahí mi respuesta centrada en C ++. Si realmente debe ser C ...

Puede usar el truco de macros: declare sus objetos públicamente, de modo que tengan un tipo y se puedan asignar globalmente, pero destruya los nombres de sus componentes más allá de la usabilidad, a menos que alguna macro se defina de manera diferente, como es el caso en el archivo .c de su módulo. Para mayor seguridad, puede utilizar el tiempo de compilación en la destrucción.

O tenga una versión pública de su estructura que no tenga nada útil, y tenga la versión privada (con datos útiles) solo en su archivo .c, y afirme que son del mismo tamaño. Un poco de truco de creación de archivos podría automatizar esto.


@Lundins comenta sobre programadores malos (incrustados):

  • El tipo de programador que describas probablemente haría un desastre en cualquier idioma. Las macros (presentes en C y C ++) son una forma obvia.

  • Las herramientas pueden ayudar hasta cierto punto. Para mis alumnos, solicito una secuencia de comandos integrada que especifique no-excepciones, no-rtti, y da un error de enlace cuando se usa el montón o están presentes los valores globales inicializados por código. Y especifica advertencia = error y habilita casi todas las advertencias.

  • Recomiendo el uso de plantillas, pero con constexpr y conceptos, la metaprogramación es cada vez menos necesaria.

  • "programadores confundidos de Arduino" Me gustaría mucho reemplazar el estilo de programación Arduino (cableado, replicación de código en bibliotecas) con un enfoque moderno de C ++, que puede ser más fácil, más seguro y producir código más rápido y más pequeño. Si tan solo tuviera el tiempo y el poder ...

Wouter van Ooijen
fuente
Gracias por esta respuesta! Usar C ++ es una opción, pero estamos usando C en mi empresa (lo que no he mencionado explícitamente). He actualizado la pregunta para que la gente sepa que estoy hablando de C.
L. Heinrichs
¿Por qué estás usando (solo) C? Tal vez esto te dé la oportunidad de convencerlos de que al menos consideren C ++ ... Lo que quieres es esencialmente (una pequeña parte de) C ++ realizado en C.
Wouter van Ooijen
Lo que hago en mi (primer proyecto de pasatiempo incrustado 'real') es inicializar por defecto simple en el constructor, y usar un método Init separado para las clases relevantes. Otro beneficio es que puedo pasar punteros para pruebas unitarias.
Michel Keijzers
2
@Michel para un proyecto de pasatiempo, ¿eres libre de elegir el idioma? ¡Toma C ++!
Wouter van Ooijen
44
Y si bien es perfectamente posible escribir buenos programas de C ++ para embebidos, el problema es que alrededor del 50% de todos los programadores de sistemas embebidos son charlatanes, programadores confusos de PC, aficionados a Arduino, etc., etc. Este tipo de personas simplemente no pueden usar un subconjunto limpio de C ++, incluso si se lo explica a la cara. Dales C ++ y antes de que te des cuenta, utilizarán todo el STL, la metaprogramación de plantillas, el manejo de excepciones, la herencia múltiple, etc. Y el resultado es, por supuesto, basura completa. Por desgracia, así es como terminan 8 de cada 10 proyectos C ++ integrados.
Lundin
7

Creo que FreeRTOS (¿tal vez otro sistema operativo?) Hace algo como lo que estás buscando al definir 2 versiones diferentes de la estructura.
El 'real', utilizado internamente por las funciones del sistema operativo, y uno 'falso' que tiene el mismo tamaño que el 'real', pero que no tiene miembros útiles en su interior (solo un montón de int dummy1y similares).
Solo la estructura 'falsa' está expuesta fuera del código del sistema operativo, y esto se usa para asignar memoria a instancias estáticas de la estructura.
Internamente, cuando se llaman funciones en el sistema operativo, se les pasa la dirección de la estructura externa 'falsa' como un identificador, y esto se convierte en un puntero a una estructura 'real' para que las funciones del sistema operativo puedan hacer lo que necesitan hacer.

brhans
fuente
Buena idea, supongo que podría usar --- #define BUILD_BUG_ON (condición) ((nulo) sizeof (char [1 - 2 * !! (condición)])) --- BUILD_BUG_ON (sizeof (real_struct)! = Sizeof ( fake_struct)) ----
L. Heinrichs
2

Estructura anónima, por lo que la estructura solo puede modificarse mediante el uso de funciones de interfaz proporcionadas

En mi opinión, esto no tiene sentido. Puede poner un comentario allí, pero no tiene sentido tratar de ocultarlo más.

C nunca proporcionará un aislamiento tan alto, incluso si no hay una declaración para la estructura, será fácil sobrescribirla accidentalmente con, por ejemplo, memcpy () o desbordamiento de búfer.

En cambio, solo dale un nombre a la estructura y confía en otras personas para que también escriban un buen código. También facilitará la depuración cuando la estructura tenga un nombre que pueda usar para referirse a ella.

jpa
fuente
2

Las preguntas de SW puro se hacen mejor en /programming/ .

El concepto de exponer una estructura de tipo incompleto a la persona que llama, como usted describe, a menudo se llama "tipo opaco" o "punteros opacos": la estructura anónima significa algo completamente diferente.

El problema con esto es que la persona que llama no podrá asignar instancias del objeto, solo punteros a él. En una PC, usarías mallocdentro de los objetos "constructor", pero malloc es un no-go en sistemas embebidos.

Entonces, lo que haces en incrustado es proporcionar un grupo de memoria. Tiene una cantidad limitada de RAM, por lo que restringir la cantidad de objetos que se pueden crear generalmente no es un problema.

Consulte Asignación estática de tipos de datos opacos en SO.

Lundin
fuente
Gracias por aclarar la confusión de nombres de mi parte, ajustaré el OP. Estaba pensando en apilar el desbordamiento, pero decidí que me gustaría dirigirme específicamente a los programadores integrados.
L. Heinrichs