Variables globales frente a punteros en diseño integrado [cerrado]

He escrito algunos sistemas integrados de 8 bits y la base de código que heredé y he ampliado es básicamente un 80 % de variables globales (volátiles externas), y luego banderas de control no globales y variables lógicas según sea necesario.

El resultado final es que terminas con muchas funciones void() para modificar las variables globales.

Los sistemas funcionan bien y el software es bastante legible y fácil de usar, pero siempre tengo esa molestia de diseño en la parte de atrás de mi cabeza de que debería estar refactorizando todo y señalando el próximo diseño.

No tengo ninguna religiosidad sobre el aspecto del uso de la memoria, la memoria estática está ahí para usar tanto como el montón. Es más una cuestión de que a medida que los sistemas se hacen más grandes, sospecho que tal vez los globales comiencen a ser un obstáculo mayor de lo que piensas.

¿Muchos de ustedes, programadores experimentados de software embebido, usan punteros en sus sistemas de 8 bits?

No usamos listas enlazadas ni nada sofisticado donde realmente necesite punteros y para asignar memoria dinámicamente. Pude ver el próximo sistema que escribo, uso estructuras en lugar de variables para agrupar la información de manera un poco más lógica y usaría punteros en lugar de modificar las estructuras.

Además, las funciones pueden reutilizarse si señala su código, pero en cierto sentido las funciones son bastante triviales y únicas en los sistemas integrados que he construido. Cosas muy específicas de la aplicación.

PARA LA POSTERIDAD: También publicado en el foro ARM KEIL C51

DESDE QUE LA PUBLICACIÓN FUE UN POCO 8051 ÉNFASIS: LA MEJOR GUÍA DEL COMPILADOR C51 QUE SE HA ESCRITO

Los punteros se utilizan en su caso. La pregunta es poco clara, demasiado amplia y basada en opiniones.
¿Dónde es apropiado?
Ver la segunda parte del comentario. Esto es algo que normalmente aprendes cuando aprendes a programar y adquieres algo de experiencia.
Suena como una respuesta bastante religiosa. "Hijo mío, el sol brillará y el señor se mostrará, y sabrás cuándo ha llegado la 'conveniencia'". Súper condescendiente. Podría malloc todo y apuntar todo, no creo que haya ninguna ganancia.
Esto está cubierto por la cosa "demasiado amplia" y "basada en opiniones". Puedes llamarlo "religioso" y "condescendiente".
Usted dice que pueden surgir problemas a medida que el sistema crece. Sin embargo, la mayoría de los sistemas integrados no pueden crecer demasiado debido a los recursos limitados disponibles. Evitaría las variables globales si programara un sistema Linux integrado, pero no veo un problema grave con ellas en un sistema básico de pocos kB.
Ni siquiera puedo comenzar a contar la cantidad de veces que he visto esto debatido durante meses seguidos. Una de las preguntas más importantes que debo hacer cuando veo que alguien comienza este tipo de cosas es: "¿Qué consideras que es 'programación integrada' y cómo lo distingues de otra programación?" La respuesta a eso suele ser bastante reveladora.
Creo que Photon tiene una buena demarcación, unos pocos kB de memoria son un buen punto de partida para la integración. No ejecutar un sistema operativo es otro buen indicador. Personalmente, he estado trabajando en pequeños PIC de 8 bits y 8051. Ahora, si ejecuta un RTOS realmente básico en un 32 bit, comprenda si eso es parte del canon sagrado de los dispositivos integrados.
@ Leroy105 Algunos parecen definir incrustado como "solo otra forma de decir cualquier programación dirigida a cualquier producto que un usuario no usa como un dispositivo de propósito general destinado a ejecutar programas". Lo que hace que la definición sea una cuestión de lo que un usuario piensa/hace y elimina cualquiera de las distinciones muy importantes que les importan a los programadores. Para mí, se trata de la variedad de herramientas requeridas y la variedad de habilidades y conocimientos personales y variados que se requieren del programador (no solo sobre programación) que marcan la diferencia. (Espero un buen conocimiento del enlazador, por ejemplo).
También hay argumentos en la programación integrada para minimizar o incluso eliminar por completo el uso de punteros, especialmente cuando la confiabilidad es importante. MISRA-C, por ejemplo, impone restricciones en el uso del puntero.

Respuestas (2)

Creo que puedo ver de dónde vienes. (Después de leer sus comentarios.) Este es más revelador para mí:

... ¿ve gente diseñando software y simplemente evitando los globales a toda costa en estos diminutos sistemas integrados? Puedes despellejar al gato de cualquier manera, los globales me parecen más rápidos.

(También estoy imaginando los núcleos de la serie 8051/8031/8052/8032, por ahora).

Tomemos un método muy simple para crear colas de sistema operativo para un sistema operativo muy simple. Necesitamos una cola lista y una cola de suspensión, como mínimo. (Podríamos agregar colas de semáforos, pero no lo hagamos porque quiero mantener esto al mínimo). También queremos admitir una cantidad limitada de procesos. (Después de todo, esta es una pequeña MCU sin mucha memoria disponible).


Para empezar, definamos algunas constantes:

#define NPROC 10
#define TAILPRIORITY 10000  /* you will see the need later */

Para las colas, utilicemos un enfoque de lista enlazada y reconozcamos el hecho de que necesitamos inserción y eliminación y nos gustaría que estas operaciones fueran bastante fáciles de lograr. Entonces llegamos a la conclusión de que queremos punteros anteriores y siguientes para cada entrada de la cola. También queremos apoyar un campo prioritario:

typedef struct proc_s proc_t;
typedef struct proc_s {
    int priority;
    proc_t *next;
    proc_t *prev;
} proc_t;

Ahora, podemos simplemente definir esto, estáticamente (el alcance podría mantenerse a nivel de archivo):

proc_t readyhead, readytail, sleephead, sleeptail, freehead, freetail, proc[NPROC];

Veamos ahora lo que se necesita para eliminar e insertar un proceso en una cola (tenga en cuenta que las variables de la cola lista y de suspensión son tipos de proc_t completos, no solo punteros a ellos. Esto simplifica el siguiente código). También agregaré un " getfirst" porque lo necesitamos para algunos propósitos.

/* Assumes that 'item' actually resides within a queue. */
proc_t * remove( proc_t * item ) {
    item->prev->next= item->next;
    item->next->prev= item->prev;
    return item;
}
/* Not to be used if 'item' is already in another queue. */
/* Priority insertion assumes that all queues end in a tail */
proc_t * insert( proc_t * queue, proc_t * item, int priority ) {
    proc_t *n, *p;
    for ( n= queue->next; n->priority < priority; n= n->next ) ;
    item->next= n;
    item->prev= p= n->prev;
    item->priority= priority;
    p->next= item;
    n->prev= item;
    return item;
}
proc_t * getfirst( proc_t * queue ) {
    if ( queue->next->priority == TAILPRIORITY )
        return NULL;
    return remove( queue->next );
}

Todo lo anterior supone, por supuesto, que los punteros de cabeza y cola de la cola se inicializan correctamente y que las entradas en proc[] se insertan primero en la cola libre y se eliminan secuencialmente. (También asume que la cola siempre tiene una "prioridad" que es el mayor valor posible y es más grande que cualquier proceso válido que pueda poseer: TAILPRIORITY).


¿Qué más podriamos hacer? Sin dividir esto en pedazos como arriba, aquí hay otra oportunidad:

#define NPROC (10)
#define TAILPRIORITY (10000)
#define READYQUEUE (NPROC)
#define SLEEPQUEUE (NPROC+2)
int next[PROC+4];
int prev[PROC+4];
int prio[PROC+4];
int remove( int item ) {
    int n= next[item], p= prev[item];
    next[p]= n;
    prev[n]= p;
    return item;
}
int insert( int queue, int item, int priority ) {
    int n, p;
    for ( n= next[queue]; prio[n] < priority; n= next[n] ) ;
    next[item]= n;
    prev[item]= p= prev[n];
    prio[item]= priority;
    next[p]= item;
    prev[n]= item;
    return item;
}
int getfirst( int queue ) {
    if ( next[queue] > NPROC )
        return -1;
    return remove( next[queue] );
}

El compilador de C ahora conoce de antemano la dirección de las matrices next[], prev[] y prio[]. getfirst() ya no necesita depender de un valor de prioridad especial (aunque insert() aún requiere algo así, pero eso también podría cambiarse ahora).


¿Importa esto desde el tamaño del código y/o el argumento de rendimiento? Tal vez. Depende del compilador. Para sonreír, pruebe estos dos enfoques diferentes con el compilador SDCC y eche un vistazo al código ensamblador generado para cada uno. ¿Qué opinas?

¿Qué pasa con el caso de la legibilidad? ¿Cuál es más legible para ti? (No busqué "particularmente legible" o "particularmente ilegible", sino "coherentes entre sí").

¿Qué hay de mantenible? ¿Qué sucede si necesita ampliar el tipo de nodo de lista vinculada? ¿Sería más fácil de mantener agregar otra matriz (segundo ejemplo de código fuente)? ¿O más fácil de mantener para agregar otro elemento a una estructura (primer ejemplo de código fuente)? ¿Haría tanta diferencia, en absoluto?

Suponga que está pasando un nodo de lista enlazada. ¿Es mejor pasar un puntero o un índice? Tenga en cuenta que pasar un puntero permite que una función acceda a cualquier elemento dentro de la estructura, incluso si se supone que no debe hacerlo. Pero pasar un índice podría permitir colocar la visibilidad de partes específicas "en otro lugar" para que, si hubiera un intento, el compilador pudiera emitir un error. Pero hay otras consideraciones, por supuesto. ¿Qué argumentos ves, a favor y en contra?

Y así continúa.


¿Personalmente? Creo que la consistencia del estilo es quizás lo más importante. Puedo acostumbrarme a casi cualquier estilo de codificación, incluso a los que no me gustan mucho. Siempre que la programación sea consistente , se trata principalmente de acostumbrarse y luego seguir adelante. Sin embargo, si la programación cambia de una mentalidad a otra y luego a otra y hay poca o ninguna consistencia en el código, me resulta bastante difícil de leer y/o mantener bien. Así que esto es probablemente lo más importante para mí. Establezca un estilo y luego manténgase consistente con el estilo.

Hay algunas áreas que están llenas de problemas. Por ejemplo, usar el montón en un sistema integrado en lugar de memoria estática. (Especialmente cierto, supongo, para la familia 8051). Es muy, muy fácil para un compilador y enlazador calcular el espacio total requerido para matrices estáticas y hacerle saber si cabe en el procesador que está usando en ese momento. . Pero es muy difícil encontrar errores de memoria similares si la única forma de averiguarlo es ejecutando el programa y asegurándose de ejercer todo el código condicional necesario para forzar la combinación correcta de eventos para exceder la memoria. , cuando se usa el montón.

No hay nada intrínsecamente malo con el montón. Pero en la práctica con sistemas integrados, creo que necesita una buena justificación. Así que buscaría criterios explícitos que justificaran su uso, si se usa.

Así que también busco pensamiento elaborado en código. ¿Por qué se tomaron ciertas decisiones? ¿Muestran buen juicio? En los casos en que se requiere evidencia extraordinaria para algún uso, ¿se ha proporcionado esa evidencia y tiene sentido? Etc.


Sin embargo, en el caso de la familia 8051, las instrucciones necesarias para hacer referencia directa a las direcciones específicas asignadas son mucho más pequeñas (espacio de código) y mucho más rápidas, que todos los buenos compiladores de C (un número cada vez menor de empresas, por cierto) proporcionará un mecanismo para el análisis de ruta de llamada estática para que las variables de funciones locales puedan ser asignadas (con alguna probabilidad de éxito) direcciones fijas. Entonces, en este caso particular, esperaría encontrar un uso mucho mayor de la estática, ya sea local en el archivo o disponible globalmente en el alcance. Tiene demasiado sentido en el 8051.

Las transiciones de las que he sido parte han pasado de ALU de 4-8 bits y anchos de memoria a 32/64/128, con 32 bits bastante comunes ahora. La definición de qué tipo de controlador puede encontrar en un dispositivo pequeño también ha cambiado y no me sorprende en absoluto encontrar que Linux se usa para poco más que parpadear algunos LED en un núcleo 16/32 ARM7TDMI. (O incluso una superescala Cortex-A53). Entonces, lo que es apropiado en el nivel de codificación C variará un poco.

Espero consistencia en el código fuente y un razonamiento bien considerado para las elecciones de diseño que se toman. Eso es lo más importante, para mí. Más allá de eso, soy un poco más flexible con mi opinión.

Esa es una gran idea sobre el montón. No había pensado en ese riesgo. Además, no había pensado en las instrucciones de la máquina en términos de sobrecarga del puntero. Personalmente, la sintaxis del puntero me resulta más difícil de leer. Si observa las MCU Cortex M más nuevas, ¿pueden manejar mejor las operaciones de puntero? Supongo que sí, porque la mayoría de los SDK de ARM que he visto son esencialmente estructuras, punteros y funciones que toman punteros.
Me estoy riendo entre dientes al leer este análisis en el 8051. Veo lo que estabas diciendo en cuanto al compilador si realmente quieres hablar sobre punteros versus estáticas.... barrgroup.com/Embedded-Systems/How-To/Optimal-C-Code- 8051
@ Leroy105 Me alegra que lo hayas disfrutado un poco. Quería pensar en algo a la vez semi-interesante pero también lo suficientemente simple como para no enterrar las cosas demasiado profundamente en el barro.

Lo que 'quieres' es refactorizar. Si es para un negocio (por lo que hay dinero involucrado), solo hazlo cuando el tiempo (por lo tanto, el dinero) te esté dando más:

  • mantenibilidad
  • legibilidad
  • posibilidad de ampliar

Si es un proyecto de 'hobby' y 'sientes' que es mejor usar punteros, adelante. De lo contrario (y los elementos anteriores no son aplicables o no tanto para dedicarle tiempo), deje el código como está.

Es para un negocio, así que no, no queremos refactorizar. Tenemos un fuerte sentimiento al respecto. ;) No sé si esta es su timonera: en su trabajo, ¿ve gente diseñando software y simplemente evitando globales a toda costa en estos pequeños sistemas integrados? Puedes despellejar al gato de cualquier manera, los globales me parecen más rápidos.
Soy ingeniero de software profesional, pero no en AVR, sino en software integrado (software de bits, como> 50 millones de líneas). Dado que allí la modularización (debido a la mantenibilidad, la posibilidad de expandirse) es más importante, se usan muchos puntos, pero también en algunos casos globales (pero estáticos dentro de un archivo).
Eso tiene sentido en bases de código más grandes para priorizar la modularización... ¡Es realmente útil escuchar a un profesional sobre lo que ven en su organización!