5 votos

Variables Globales vs. Punteros en el Diseño Embebido

He escrito unos cuantos sistemas incrustados 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 que banderas de control no globales y variables lógicas según sea necesario.

El resultado final, es que terminas con un montón de funciones void() para modificar las variables globales.

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

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

¿Muchos de ustedes experimentados programadores de software embebido utilizan ampliamente los punteros en sus sistemas de 8 bits?

No usamos listas enlazadas ni nada sofisticado donde realmente se necesitan punteros y para asignar dinámicamente la memoria. Podría ver el siguiente sistema que escribo, uso estructuras en lugar de variables para agrupar la información de forma un poco más lógica y utilizarías punteros que para modificar las estructuras.

Además, las funciones pueden ser reutilizadas si se puntea el código, pero en cierto sentido las funciones son bastante triviales y una sola en los sistemas integrados que he construido. Cosas muy específicas de la aplicación.

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

DESDE QUE EL POST SE PUSO UN POCO DE ÉNFASIS EN 8051: LA MEJOR GUÍA DEL COMPILADOR C51 JAMÁS ESCRITA

1 votos

W

4 votos

Se utilizan punteros cuando procede. La pregunta es poco clara, demasiado amplia y basada en opiniones.

0 votos

Véase la segunda parte del comentario. Esto es algo que se suele aprender cuando se aprende a programar y se adquiere cierta experiencia.

3voto

AitorTheRed Puntos 241

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

...¿ves a la gente que hace la arquitectura del software y evita los globales en todo el costo en estos diminutos sistemas integrados? Puedes despellejar al gato o los globos 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 sueño, como mínimo. (Podríamos añadir colas de semáforos, pero no lo hagamos porque quiero mantener esto al mínimo.) También queremos apoyar un número limitado de procesos. (Esto es, después de todo, un pequeño MCU sin mucha memoria disponible.)


Para empezar, definamos algunas constantes:

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

En cuanto a las colas, vamos con un enfoque de lista vinculada y reconocemos el hecho de que necesitamos la inserción y la supresión y nos gustaría que estas operaciones fueran bastante fáciles de realizar. Así que concluimos que queremos que ambos siguiente y prev 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 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 quitar e insertar un proceso en una cola (Nótese que las variables de cola lista y dormida son tipos de proc_t completo, no sólo punteros a ellas. Esto simplifica el siguiente código). También añadiré 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 estén debidamente inicializados y que las entradas en proc[] se inserten primero en la cola libre y se eliminen secuencialmente. (También se supone que la cola siempre tiene una "prioridad" que es el valor más grande posible y es mayor que cualquier proceso válido puede poseer: PRIORIDAD DE LA COLA).


¿Qué más podríamos hacer? Sin romper 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 C ahora conoce de antemano sobre la dirección de las matrices next[] y prev[] y prio[]. getfirst() ya no necesita depender de un valor de prioridad especial (aunque insert() sigue requiriendo algo parecido, aunque eso también podría cambiarse ahora).


¿Importa esto desde el punto de vista del tamaño del código y/o del 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é te parece?

¿Qué hay del caso de la legibilidad? ¿Cuál es más legible para ti? (No me refería a "particularmente legible" o "particularmente ilegible" sino a "consistente con cada uno".) ¿Qué hay del caso de la legibilidad? ¿Cuál es más legible para ti? (No me refería a "particularmente legible" o "particularmente ilegible", sino a "consistentes entre sí".)

¿Y qué hay de lo sostenible? ¿Y si necesitara ampliar el tipo de nodo de la lista de enlaces? ¿Sería más sostenible añadir otro array (2º ejemplo de código fuente)? ¿O más sostenible añadir otro elemento a una estructura (1er ejemplo de código fuente)? ¿Haría tanta diferencia, en absoluto?

Supongamos que estás pasando un nodo de lista enlazado. ¿Es mejor pasar un puntero o un índice? Tenga en cuenta que pasar un puntero permite a una función acceder a cualquier elemento de la estructura, incluso si no se supone que lo haga. Pero pasar un índice podría permitir colocar la visibilidad de partes específicas "en otro lugar" de modo que si hubiera un intento, el compilador podría emitir un error. Pero hay otras consideraciones, por supuesto. ¿Qué argumentos ve, a favor y en contra?

Y así va.


¿Personalmente? Encuentro 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, en absoluto. Siempre y cuando la programación sea consistente es mayormente una cuestión de acostumbrarse a ello y seguir después de eso. Sin embargo, si la programación cambia de un modo de pensar a otro y luego a otro y hay poca o ninguna consistencia en el código, encuentro bastante difícil de leer y/o mantener bien. Así que esto es probablemente lo más importante para mí. Establecer un estilo y luego mantener la coherencia 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 la 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 los arreglos estáticos y hacerte saber si puede caber en el procesador que estás usando en ese momento. Pero es muy difícil encontrar errores de memoria similares si la única forma de averiguarlo es ejecutar el programa y asegurarse de que se ejercita todo el código condicional variado necesario para forzar la combinación correcta de eventos para superar la memoria, cuando se utiliza heap.

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

Así que también busco el pensamiento artesanal en clave. ¿Por qué se hicieron ciertas elecciones? ¿Muestran buen juicio? En los casos en que se requieren pruebas extraordinarias para algún uso, ¿se han proporcionado esas pruebas y tienen sentido? Etc.


Sin embargo, en el caso de la familia 8051, las instrucciones necesarias para referenciar directamente las direcciones específicas asignadas son tan pequeñas (espacio de código) y tan rápidas, que todos los buenos compiladores de C para ello (un número muy reducido de empresas, por cierto) proporcionarán un mecanismo para el análisis estático de la ruta de llamada, de modo que se puedan asignar a las variables de función locales (con alguna posibilidad de éxito) direcciones fijas. Así que en este caso particular, esperaría encontrar un uso mucho mayor de la estática, ya sea en el ámbito de los archivos locales o en el ámbito global. Simplemente tiene demasiado sentido en el 8051.

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

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

0 votos

Es una gran visión del montón. No había pensado en ese riesgo. Además, no había pensado en las instrucciones de máquina en términos de sobrecarga del puntero. Personalmente, la sintaxis de puntero me parece más difícil de leer. Si nos fijamos en las nuevas MCU Cortex M, ¿pueden manejar mejor las operaciones de puntero? Supongo que sí, porque la mayoría de los ARM SDKs, que he visto son esencialmente todos los structs y punteros y funciones que toman punteros.

0 votos

Me estoy riendo leyendo este análisis sobre el 8051. Veo lo que estabas diciendo compilador sabio si realmente quieres hablar de punteros vs estática.... barrgroup.com/Sistemas integrados/How-To/Optimal-C-Code-8051

0 votos

@Leroy105 Me alegro de que te haya gustado un poco. Quería pensar en algo a la vez semi-interesante pero también lo suficientemente sencillo como para no enterrar las cosas demasiado en el fango.

2voto

Spike Puntos 304

Varios conocidos por sus trabajos "puros" han aplicado más bien contribuciones a la óptica geométrica:

  • Carathéodory ( 1937 ) Geometrische Optik
  • Chaundy ( 1919 ) Las aberraciones de un sistema óptico simétrico
  • Whittaker ( 1907 ) La teoría de los instrumentos ópticos
  • Hausdorff ( 1896 ) Imágenes infinitesimales de la óptica
  • Hensel ( 1888 ) Teoría de los paquetes de rayos infinitamente finos
  • Kummer ( 1861 ) Sobre la refracción atmosférica
  • Weierstrass ( 1856 ) Zur Dioptrik
  • Sturm ( 1845 ) Tesis sobre la teoría de la visión
  • Listado ( 1845 ) Contribución a la óptica fisiológica
  • Gauss ( 1843 ) Exámenes dióptricos
  • Liouville ( 1842 ) Demostración del Teorema del Dr. Biot sobre Refracciones Astronómicas
  • Möbius ( 1830 ) Breve descripción de las principales propiedades de un sistema de lentes de gafas
  • Monge ( 1798 ) Memoria sobre el Fenómeno Óptico, conocido como Mirage

0 votos

Es para un negocio, así que no no queremos refactorizar. Tenemos un fuerte presentimiento al respecto. ;) No sé si este es tu campo: en tu trabajo, ¿ves a gente diseñando software y evitando los globales a toda costa en estos pequeños sistemas embebidos? Puedes despellejar al gato de cualquier manera, los globales me parecen más rápidos.

1 votos

Soy ingeniero de software profesional, pero no en AVR, sino en software embebido (software de bits, como > 50 millones de líneas). Desde allí modularización (debido a la capacidad de mantenimiento, la posibilidad de ampliar) es más importante, una gran cantidad de puntos se utilizan, sino también en algunos casos globales (pero estática dentro de un archivo).

2 votos

Eso tiene sentido en bases de código más grandes para dar prioridad a la modularización... ¡es realmente útil oír de un profesional lo que ve en su organización!

i-Ciencias.com

I-Ciencias es una comunidad de estudiantes y amantes de la ciencia en la que puedes resolver tus problemas y dudas.
Puedes consultar las preguntas de otros usuarios, hacer tus propias preguntas o resolver las de los demás.

Powered by:

X