SOLAPAMIENTO DE VARIABLES DATA REALIZADO POR L51
Una de las principales características de C51 es la función de solapamiento, o mecanismo mediante el cual diferentes variables del programa utilizan las mismas posiciones de RAM interna. La posibilidad de utilizar solapamiento surge cuando se declaran variables automáticas. Estas variables tienen una vida muy corta, se crean al entrar a la función o bloque que las declara, y desaparecen después de la llave '}' de final de bloque o función. En consecuencia, el área ocupada por las variables automáticas queda libre y puede ser utilizada por otras funciones. Lo mismo sucede con el área de memoria utilizada por C51 para el paso de parámetros.
Si a lo largo de un programa, cada función conservara el área de memoria asignada a sus variables y parámetros, la RAM interna del 8051 se agotaría incluso con pequeños programas
En C51, el linker es quien se encarga de aplicar el mecanismo de solapamiento. El linker examina las necesidades de RAM interna de todas las funciones, y las llamadas que estas realizan entre sí, y con esa información determina los segmentos data y bit que pueden solaparse. El uso de los registros para ubicación de variables temporales también tiende a reducir las necesidades en el segmento data.
La función de solapamiento se basa en que si la función 1 llama a la función 2, sus áreas data no pueden solaparse, ya que ambas se encuentran activas al mismo tiempo. Una tercera función 3, también llamada por la 1, si puede solaparse con la 2, ya que las dos no pueden correr a la vez.
main | funcA - func2 - func3 - func4 | funcB - func5 - func6 - func7 | funcC - func8 - func9 - func10 |
Como la función A llama a func2, y func2 llama a func3 etc., A, 2, 3 y 4 no pueden solapar sus áreas data. De igual forma B, 5, 6, y 7 tampoco pueden solaparse. Sin embargo los grupos 2,3,4; 5,6,7 y 8,9,10 si pueden solapar sus áreas data, ya que pertenecen a ramas distintas. Esta es la base de la estrategia de solapamiento.
Sin embargo las funciones de interrupción pueden producirse en cualquier momento, y si no se tomase un cuidado especial con ellas, podrían sobre-escribir las áreas data del programa principal (o de las interrupciones de menor prioridad). Para evitarlo, C51 identifica a las funciones de interrupción y a las funciones llamadas por estas, y las ubica en áreas de memoria individuales.
La regla general utilizada por L51 es que si dos funciones nunca se ejecutan a la vez, sus áreas data pueden solaparse. En el 99% de los casos el mecanismo de solapamiento trabaja correctamente, pero en algunas ocasiones pueden obtenerse resultados inesperados. Estos pocos casos son:
Cuando se dan estas condiciones el linker puede emitir los siguientes avisos:
MULTIPLE CALL TO SEGMENT UNCALLED SEGMENT RECURSIVE CALL TO SEGMENT
En el siguiente ejemplo, func4 y func5 se llaman desde main por medio de una función llamada EJECUTA que recibe como parámetro un puntero a la función a llamar. Cuando L51 analiza el programa, no es capaz de establecer un vínculo entre las funciones 4 y 5 porque el puntero a función que recibe como parámetro rompe la cadena de referencias (El valor del puntero a función es indeterminado durante el linkado). Por ello, L51 solapa los segmentos data de func4, func5 y ejecuta como si todas ellas fueran llamadas desde main.
Como resultado de lo anterior, las variables locales de func4 y func5 pueden destruir las variables locales de ejecuta, lo cual es MUY peligroso, especialmente cuando la sobre-escritura de variables no resulta obvia, y se produce solamente bajo condiciones anormales de operación. Ejemplo:
/***************************************************************** * Riesgo de solapamiento 1 - Funciones llamadas indirectamente * ******************************************************************/ #include<reg51.h> char func1(void) { // Función llamada directamente char x, arr[10] ; for(x = 0 ; x < 10 ; x++) { arr[x] = x ; } return(x) ; } char func2(void) { // Función llamada directamente //(.... Código C ...) return 2; } char func3(void) { // Función llamada directamente //(.... Código C ...) return(3); } char func4(void) { // Función llamada indirectamente char x4, arr4[10] ; // variables locales for(x4 = 0 ; x4 < 10 ; x4++) { arr4[x4] = x4 ; } return(x4) ; } char func5(void) { // Función llamada indirectamente char x5, arr5[10] ; // variables locales for(x5 = 0 ; x5 < 10 ; x5++) { arr5[x5] = x5 ; } return(x5) ; } /*** Función que llama a otras funciones ***/ char ejecuta(char (*fptr)()) // { // char (*fptr)(void) fptr es un puntero a función que retorna un char char tex ; // variables locales de ejecuta char arrex[10] ; for(tex = 0 ; tex < 10 ; tex++) { arrex[tex] = (*fptr)() ; } return(tex) ; } /*** Declaración de array de punteros a función ***/ char (code *fp[3])(void) ; /*** Llamadas a función desde main ***/ void main(void) { char am ; fp[0] = func1 ; // Elementos del array de punteros a funciones fp[1] = func2 ; fp[2] = func3 ; am = (* fp[0])() ; // Ejecución de las funciones am = (* fp[1])() ; am = (* fp[2])() ; if(P1) { // Control para llamadas indirectas a función am = ejecuta(func4) ; } else { am = ejecuta(func5) ; } } |
Resultados de condiciones peligrosas en el fichero '.M51' de salida del linker.
MS-DOS L51 LINKER/LOCATOR V3.70c, INVOKED BY: E:\KEIL\BIN\L51.EXE MAIN.OBJ MEMORY MODEL: SMALL INPUT MODULES INCLUDED: MAIN.OBJ (MAIN) E:\KEIL\LIB\C51S.LIB (?C_STARTUP) E:\KEIL\LIB\C51S.LIB (?C?ICALL) LINK MAP OF MODULE: MAIN (MAIN) TYPE BASE LENGTH RELOCATION SEGMENT NAME ----------------------------------------------------- * * * * * * * D A T A M E M O R Y * * * * * * * REG 0000H 0008H ABSOLUTE "REG BANK 0" DATA 0008H 000FH UNIT _DATA_GROUP_ DATA 0017H 0006H UNIT ?DT?MAIN IDATA 001DH 0001H UNIT ?STACK * * * * * * * C O D E M E M O R Y * * * * * * * CODE 0000H 0003H ABSOLUTE CODE 0003H 0049H UNIT ?PR?MAIN?MAIN ...................................................... OVERLAY MAP OF MODULE: MAIN (MAIN) SEGMENT DATA_GROUP +--> CALLED SEGMENT START LENGTH ---------------------------------------------- ?C_C51STARTUP ----- ----- +--> ?PR?MAIN?MAIN ?PR?MAIN?MAIN 0008H 0001H +--> ?PR?FUNC1?MAIN +--> ?PR?FUNC2?MAIN +--> ?PR?FUNC3?MAIN +--> ?PR?FUNC4?MAIN +--> ?PR?_EJECUTA?MAIN +--> ?PR?FUNC5?MAIN ?PR?FUNC1?MAIN 0009H 000AH ?PR?FUNC4?MAIN 0009H 000AH // Peligro de solapamiento ?PR?_EJECUTA?MAIN 0009H 000EH // con FUNC4, EJECUTA y FUNC5 ?PR?FUNC5?MAIN 0009H 000AH SYMBOL TABLE OF MODULE: MAIN (MAIN) VALUE TYPE NAME ---------------------------------- ------- MODULE MAIN D:0090H PUBLIC P1 D:0017H PUBLIC FP ------- PROC FUNC1 D:0007H SYMBOL X D:0009H SYMBOL ARR ------- ENDPROC FUNC1 ------- PROC FUNC4 D:0007H SYMBOL X4 D:0009H SYMBOL ARR4 // peligro ------- ENDPROC FUNC4 ------- PROC FUNC5 D:0007H SYMBOL X5 D:0009H SYMBOL ARR5 // peligro ------- ENDPROC FUNC5 ------- PROC _EJECUTA D:0009H SYMBOL FPTR // peligro D:000CH SYMBOL TEX D:000DH SYMBOL ARREX ------- ENDPROC _EJECUTA ------- PROC MAIN D:0008H SYMBOL AM ------- ENDPROC MAIN LINK/LOCATE RUN COMPLETE. 0 WARNING(S), 0 ERROR(S)
El problema señalado en el apartado anterior se soluciona mediante el uso del comando OVERLAY durante el proceso de linkado:
L51 main.obj OVERLAY(main ~ (func4,func5), _ejecuta ! (func4,func5))
Nota: El signo tilde '~' significa: "Ignorar las referencias a func4/5 desde main" El signo '!' significa: "Añadir a la lista las referencias entre la función 'ejecuta' y func4/5 para evitar el solapamiento de las variables locales de estas funciones."
La nueva salida del linker es:
MS-DOS L51 LINKER/LOCATOR V3.70c, INVOKED BY: E:\KEIL\BIN\L51.EXE MAIN.OBJ OVERLAY (MAIN ~(FUNC4, FUNC5),_EJECUTA !(FUNC4, FUNC5)) MEMORY MODEL: SMALL INPUT MODULES INCLUDED: MAIN.OBJ (MAIN) E:\KEIL\LIB\C51S.LIB (?C_STARTUP) E:\KEIL\LIB\C51S.LIB (?C?ICALL) LINK MAP OF MODULE: MAIN (MAIN) TYPE BASE LENGTH RELOCATION SEGMENT NAME ----------------------------------------------------- * * * * * * * D A T A M E M O R Y * * * * * * * REG 0000H 0008H ABSOLUTE "REG BANK 0" DATA 0008H 0019H UNIT _DATA_GROUP_ DATA 0021H 0006H UNIT ?DT?MAIN IDATA 0027H 0001H UNIT ?STACK * * * * * * * C O D E M E M O R Y * * * * * * * CODE 0000H 0003H ABSOLUTE OVERLAY MAP OF MODULE: MAIN (MAIN) SEGMENT DATA_GROUP +--> CALLED SEGMENT START LENGTH ---------------------------------------------- ?C_C51STARTUP ----- ----- +--> ?PR?MAIN?MAIN ?PR?MAIN?MAIN 0008H 0001H +--> ?PR?FUNC1?MAIN +--> ?PR?FUNC2?MAIN +--> ?PR?FUNC3?MAIN +--> ?PR?_EJECUTA?MAIN ?PR?FUNC1?MAIN 0009H 000AH ?PR?_EJECUTA?MAIN 0009H 000EH +--> ?PR?FUNC4?MAIN +--> ?PR?FUNC5?MAIN ?PR?FUNC4?MAIN 0017H 000AH ?PR?FUNC5?MAIN 0017H 000AH SYMBOL TABLE OF MODULE: MAIN (MAIN) VALUE TYPE NAME ---------------------------------- ------- MODULE MAIN D:0090H PUBLIC P1 D:0021H PUBLIC FP ------- PROC FUNC1 D:0007H SYMBOL X D:0009H SYMBOL ARR ------- ENDPROC FUNC1 ------- PROC FUNC4 D:0007H SYMBOL X4 D:0017H SYMBOL ARR4 ------- ENDPROC FUNC4 ------- PROC FUNC5 D:0007H SYMBOL X5 D:0017H SYMBOL ARR5 ------- ENDPROC FUNC5 ------- PROC _EJECUTA D:0009H SYMBOL FPTR D:000CH SYMBOL TEX D:000DH SYMBOL ARREX ------- ENDPROC _EJECUTA ------- PROC MAIN D:0008H SYMBOL AM ------- ENDPROC MAIN ------- ENDMOD MAIN LINK/LOCATE RUN COMPLETE. 0 WARNING(S), 0 ERROR(S)
En el siguiente ejemplo se hacen llamadas a dos funciones por medio de un array de punteros a funciones. La tabla "tabla_saltos" existe en un segmento llamado "?CO?MAIN", es decir en el área de constantes asignada al módulo main. El problema surge debido a que los argumentos cadena de printf, también residen aquí. lo cual conduce una definición recursiva de la dirección de comienzo de las funciones en la tabla de saltos. Aunque ello no es peligroso, evita que se establezcan las referencias reales de las funciones y se inhibe el proceso de solapamiento.
#include <stdio.h> #include <reg517.h> void func1(void) { unsigned char i1 ; for(i1 = 0 ; i1 < 10 ; i1++) { printf("Esta es la función 1\n") ; // Cadena almacenada en // el segmento ?CO?MAIN } } void func2(void) { unsigned char i2 ; for(i2 = 0 ; i2 < 10 ; i2++) { printf("Esta es la función 2\n") ; // Cadena almacenada en // el segmento ?CO?MAIN } } code void(*tabla_saltos[])()={func1,func2}; // Tabla de saltos a // funciones, almacenada // en el segmento ?CO?MAIN /*** Llamada a las funciones ***/ void main(void) { (*tabla_saltos[P1 & 0x01])() ; // Llamada a función por medio // de la tabla en ?CO?MAIN }/* FIN DE main */ |
La salida '.MAP' del linker es:
MS-DOS L51 LINKER/LOCATOR V3.70c, INVOKED BY: E:\KEIL\BIN\L51.EXE MAIN.OBJ OVERLAY MAP OF MODULE: MAIN (MAIN) SEGMENT BIT_GROUP DATA_GROUP +--> CALLED SEGMENT START LENGTH START LENGTH ------------------------------------------------------------------ ?C_C51STARTUP ----- ----- ----- ----- +--> ?PR?MAIN?MAIN ?PR?MAIN?MAIN ----- ----- ----- ----- +--> ?CO?MAIN ?CO?MAIN ----- ----- ----- ----- +--> ?PR?FUNC1?MAIN +--> ?PR?FUNC2?MAIN ?PR?FUNC1?MAIN ----- ----- 0008H 0001H +--> ?PR?PRINTF?PRINTF ?PR?PRINTF?PRINTF 0020H.0 0001H.1 0009H 0014H +--> ?PR?PUTCHAR?PUTCHAR ?PR?FUNC2?MAIN ----- ----- 0008H 0001H +--> ?PR?PRINTF?PRINTF *** WARNING 13: RECURSIVE CALL TO SEGMENT SEGMENT: ?CO?MAIN CALLER: ?PR?FUNC1?MAIN *** WARNING 13: RECURSIVE CALL TO SEGMENT SEGMENT: ?CO?MAIN CALLER: ?PR?FUNC2?MAIN LINK/LOCATE RUN COMPLETE. 2 WARNING(S), 0 ERROR(S)
La solución es usar el comando OVERLAY durante el linkado de la siguiente forma:
L51 main.obj OVERLAY(?CO?MAIN ~ (func1,func2), main ! (func1,func2))
La línea anterior borra las referencias desde ?CO?MAIN hacia func1 y func2, e inserta las verdaderas referencias desde main hacia func1 y func2. La salida que produce ahora el linker es:
MS-DOS L51 LINKER/LOCATOR V3.70c, INVOKED BY: E:\KEIL\BIN\L51.EXE MAIN.OBJ OVERLAY(?CO?MAIN ~(FUNC1, FUNC2), MAIN !(FUNC1, FUNC2)) OVERLAY MAP OF MODULE: MAIN (MAIN) SEGMENT BIT_GROUP DATA_GROUP +--> CALLED SEGMENT START LENGTH START LENGTH ------------------------------------------------------------------ ?C_C51STARTUP ----- ----- ----- ----- +--> ?PR?MAIN?MAIN ?PR?MAIN?MAIN ----- ----- ----- ----- +--> ?CO?MAIN +--> ?PR?FUNC1?MAIN +--> ?PR?FUNC2?MAIN ?PR?FUNC1?MAIN ----- ----- 0008H 0001H +--> ?CO?MAIN +--> ?PR?PRINTF?PRINTF ?PR?PRINTF?PRINTF 0020H.0 0001H.1 0009H 0014H +--> ?PR?PUTCHAR?PUTCHAR ?PR?FUNC2?MAIN ----- ----- 0008H 0001H +--> ?CO?MAIN +--> ?PR?PRINTF?PRINTF LINK/LOCATE RUN COMPLETE. 0 WARNING(S), 0 ERROR(S)
Este aviso sucede generalmente cuando se llama a una función desde el programa principal y desde una interrupción. Significa que potencialmente la interrupción puede llamar a una función que se encuentra corriendo por haber sido llamada desde el programa principal. La solución más sencilla es declarar a la función como REENTRANT para que el compilador genere una pila local para sus parámetros y variables. Así en cada llamada a la función se crea un nuevo conjunto parámetros y de variables locales sin destruir el conjunto existente.
Esta solución aumenta de forma significativa el tiempo de ejecución y el código generado. Otra posibilidad es la creación de una segunda versión de la función, una para el programa principal y otra para la interrupción, lo cual puede plantear un problema de mantenimiento, ya que se tienen dos versiones del mismo código.
En algunos casos la situación no es problemática, si el usuario ha procurado que el uso re-entrante de la función no suceda nunca, y se puede ignorar el aviso del linker. Esto puede lograrse permitiendo la interrupción solo cuando la llamada a la función sea segura. Sin embargo esta situación es peligrosa sobre todo si intervienen otros programadores. Ejemplo:
void func1(void) { unsigned char i1,a1[15] ; for(i1 = 0 ; i1 < 10 ; i1++) { a1[i1] = i1 ; } } void func2(void) { // ( Código C de func2) } void timer0_int(void) interrupt 1 { func1() ; } main() { func1() ; func2() ; }/* FIN DE main */ |
El linker produce la siguiente salida:
OVERLAY MAP OF MODULE: MAIN (MAIN) SEGMENT DATA_GROUP +--> CALLED SEGMENT START LENGTH ---------------------------------------------- ?PR?TIMER0_INT?MAIN ----- ----- +--> ?PR?FUNC1?MAIN ?PR?FUNC1?MAIN 0017H 000FH *** NEW ROOT *************************************************** ?C_C51STARTUP ----- ----- +--> ?PR?MAIN?MAIN ?PR?MAIN?MAIN ----- ----- +--> ?PR?FUNC1?MAIN +--> ?PR?FUNC2?MAIN *** WARNING 15: MULTIPLE CALL TO SEGMENT SEGMENT: ?PR?FUNC1?MAIN CALLER1: ?PR?TIMER0_INT?MAIN CALLER2: ?C_C51STARTUP LINK/LOCATE RUN COMPLETE. 1 WARNING(S), 0 ERROR(S)
Las posibles soluciones son:
(i) Declarar la función func1 como re-entrante:
void func1(void) reentrant { }
(ii) Usar la opción OVERLAY del linker:
L51 main.obj OVERLAY(main ~ func1,timer0_int ~ func1)
para romper las conexiones entre func1 y las funciones main y timer0_int. En este segundo caso se obtiene la siguiente salida del linker:
OVERLAY MAP OF MODULE: MAIN (MAIN) SEGMENT +--> CALLED SEGMENT --------------------- ?C_C51STARTUP +--> ?PR?MAIN?MAIN ?PR?MAIN?MAIN +--> ?PR?FUNC2?MAIN *** WARNING 16: UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS SEGMENT: ?PR?FUNC1?MAIN LINK/LOCATE RUN COMPLETE. 1 WARNING(S), 0 ERROR(S)
Lo que significa que no se producirá solapamiento entre func1 y otras funciones del programa principal. El aviso puede evitarse eliminando solamente el vínculo entre func1 y la interrupción:
L51 main.obj OVERLAY(timer0_int ~ func1)
Lo que produce la siguiente salida del linker:
OVERLAY MAP OF MODULE: MAIN (MAIN) SEGMENT DATA_GROUP +--> CALLED SEGMENT START LENGTH ---------------------------------------------- ?C_C51STARTUP ----- ----- +--> ?PR?MAIN?MAIN ?PR?MAIN?MAIN ----- ----- +--> ?PR?FUNC1?MAIN +--> ?PR?FUNC2?MAIN ?PR?FUNC1?MAIN 0008H 000FH LINK/LOCATE RUN COMPLETE. 0 WARNING(S), 0 ERROR(S)
También se puede deshabilitar totalmente el solapamiento, aunque se trata de una mala solución que puede agotar rápidamente la RAM interna:
main2.obj to main2.abs NOOVERLAY
Resumiendo, con el aviso "MULTIPLE CALL TO SEGMENT WARNING" la única solución realmente segura es declarar func1 como REENTRANT, quedando la opción de duplicar la función como la segunda mejor opción. El uso del comando OVERLAY corre el peligro de que un nuevo programador con menos experiencia no se de cuenta de que la interrupción se encuentra restringida a los instantes en que pueda llamar con seguridad a la función.
Los ejemplos anteriores se refieren exclusivamente al solapamiento de los parámetros y variables locales de las funciones. Otra situación diferente se planteó recientemente con un programa dividido en dos mitades. El 8051 debía de ejecutar una u otra mitad en función de la entrada que le proporcionara un usuario durante la fase de inicialización.
Cada mitad del programa tenía un gran número de variables públicas, algunas de las cuales eran comunes a las dos mitades del programa.
Este tipo de estructura de programa necesita una clase de almacenamiento nueva a la que llamaremos "PUBLICA" o conocida por ciertos módulos y que difiere de la clase de almacenamiento "GLOBAL" que resulta conocida por todos los módulos. El nuevo C166 soporta esta nueva clase de almacenamiento, pero no sucede así con el C51, por lo que se requiere algún tipo de ajuste.
El comando OVERLAY del linker no ayuda, ya que solo controla el solapamiento de los parámetros y variables locales de funciones. Una posible solución pasa por utilizar módulos especiales para declarar las variables públicas. Así el módulo 1 declara la variables públicas del programa 1; el módulo 2 declara la variables públicas del programa 2; y finalmente el módulo 3 declara la variables comunes a ambos programas.
El truco consiste en utilizar el linker para ubicar los segmentos de datos de los módulos 1 y 2 en las mismas direcciones, mientras se permite que las variables del módulo 3 sean colocadas automáticamente por el linker. Esta solución usa tres módulos especiales para declarar las variables:
/* Ejemplo de creación de dos conjuntos de datos */ /* públicos en el mismo espacio de memoria */ extern void main1(void) ; extern void main0(void) ; /* Módulo principal que rompe el programa en dos partes */ void main(void) { bit flag ; if(flag) { main0() ; // RAMA 0 } else { main1() ; // RAMA1 1 } } ^^^^^^^^^^^^^^^^^^^^^^^ FINAL DEL MODULO PRINCIPAL /* Módulo que declara las variables públicas para la RAMA 0 */ /* Públicos en la rama 0 */ unsigned char x2,y2 ; unsigned int z2 ; char a2[0x30] ; /* Una variable accesible desde las dos ramas */ extern int comun ; void main0(void) { unsigned char c0 ; x2 = 0x80 ; y2 = x2 ; c0 = y2 ; z2 = x2*y2 ; a2[2] = x2 ; comun = z2 ; } ^^^^^^^^^^^^^^^^^^^^^^^ FIN DEL MODULO /* Módulo que declara las variables públicas para la RAMA 1 */ /* Públicos en la rama 1 */ unsigned char x1,y1 ; unsigned int z1 ; char a1[0x30] ; /* Una variable accesible desde las dos ramas */ extern int comun ; void main1(void) { char c1 ; x1 = 0x80 ; y1 = x1 ; c1 = y1 ; z1 = x1*y1 ; a1[2] = x1 ; comun = z1 ; } ^^^^^^^^^^^^^^^^^^^^^^^ FIN DEL MODULO /* Módulo que declara las variables comunes a las dos RAMAS */ int comun ; /* Una variable accesible desde las dos ramas */ ^^^^^^^^^^^^^^^^^^^^^^^ FIN DEL MODULO /* Llamada al linker */ l51 t1.obj,t2.obj,com.obj to t.abs DATA(?DT?T1(20H),?DT?T2(20H)) |
La elección de la posición "20H" coloca los segmentos combinados justo encima de los bancos de registros.
El problema de esta solución es que el linker emite un aviso de solapamiento de segmentos data, que siempre resulta molesto, aunque en esta ocasión no resulte peligroso.