INTERRUPTS, USING, REGISTERBANKS, NOAREGS EN C51
Las interrupciones juegan un papel importante en la mayoría de las aplicaciones del 8051, y afortunadamente C51 permite escribir las funciones de interrupción enteramente en C. Aunque es perfectamente posible escribir código que funcione, respetando el estándar ANSI C, si se quiere mejorar la eficacia del código es conveniente comprender la utilidad de los siguientes controles:
Para que una función de interrupción pueda ser alcanzada es necesario generar el vector de interrupción adecuado. El compilador C51 lo hace de forma automática, basándose en el argumento de la palabra interrupt. Posteriormente, el linker impide que las variables locales de las funciones de interrupción se solapen con las del programa principal, creando secciones especiales en RAM. Ejemplo:
/* Rutina de atención a la interrupción por desbordamiento del Timer 0*/ timer0_int() interrupt 1 { unsigned char temp1 ; unsigned char temp2 ; /* Sentencias C ejecutables ; */ }
Tomando como ejemplo la función de atención a la interrupción por desbordamiento del timer 0, y suponiendo que esta no utilice registros:
Código de entrada a la función timer0_int
void timer0_int(void) interrupt 1 { RSEG ?PR?timer0_int?TIMER0 USING 0 timer0_int: ; LINEA DE CODIGO FUENTE # 2 |
Si la función de interrupción llama a su vez a otra función llamada "sys_interp", el código de entrada cambia a:
Código de entrada a la función timer0_int que llama a otra función
; void timer0_int(void) interrupt 1 { RSEG ?PR?timer0_int?TIMER0 USING 0 timer0_int: PUSH ACC PUSH B PUSH DPH PUSH DPL PUSH PSW PUSH AR0 PUSH AR1 PUSH AR2 PUSH AR3 PUSH AR4 PUSH AR5 PUSH AR6 PUSH AR7 |
Ahora, al entrar a la función de interrupción se guardan en la pila todos los registros, ya que C51 supone que la función llamada (sys_interp) puede hacer uso de los mismos. Si se observa la entrada a sys_interp, se hace evidente un truco importante del compilador:
Código de entrada a sys_interp()
; unsigned char sys_interp(unsigned char x_value, RSEG ?PR?_sys_interp?INTERP USING 0 _sys_interp: MOV y_value?10,R5 MOV map_base?10,R2 MOV map_base?10+01H,R3 ;--Variable 'x_value?10' assigned to Register 'R1' -- MOV R1,AR7 |
Puede observarse la forma tan eficiente para mover el contenido de R7 a R1, utilizando AR7. Este tipo de direccionamiento absoluto de registros proporciona un código muy rápido.
El 8051 no dispone de la instrucción MOV Reg,Reg, por ello Keil utiliza el truco de considerar un registro como una dirección data absoluta:
Simulación de la instrucción MOV Reg,Reg:
En el banco de registros 0 - MOV R0,AR7, es idéntica a - MOV R0,07H.
Este truco solo puede utilizarse cuando el compilador conoce el banco de registros que se está utilizando. Cuando se utiliza el control USING, pueden surgir problemas. Véanse las siguientes secciones...
El control using n ordena al compilador que utilice el banco de registros n. De esta forma las rutinas de interrupción con exigencias de tiempo muy críticas pueden cambiar de "contexto", con mayor rapidez que empilando los registros. Además las funciones de interrupción de igual prioridad pueden compartir el mismo banco de registros, al no existir el riesgo de que se interrumpan entre ellas.
Direcciones de base de los bancos de registros del 8051
Los registros R0...R7 ocupan direcciones consecutivas en la RAM interna del 8051, siendo la dirección base, o dirección correspondiente al registro R0, variable en función del banco de registros que se encuentre activo. Así la dirección base puede ser: 0x00, 0x08, 0x10, o 0x18 según que el banco de registros activo sea el: 0, 1, 2, o 3.
Si a una función de interrupción se le añade el control "USING 1", se sustituye el empilado de los registros por la instrucción "MOV PSW,#08H" que conmuta el banco de registros. El tiempo de entrada a la interrupción disminuye considerablemente, pero puede fallar el direccionamiento absoluto de registros si no se tiene cuidado. Si la función de interrupción no hace uso de registros, y no llama a ninguna otra función, el optimizador elimina el código de banco de registros.
Código de entrada a timer0_int con USING
Con USING 1 ; void timer0_int(void) interrupt 1 using 1 { RSEG ?PR?timer0_int?TIMER0 USING 1 <--- Nuevo banco de registros timer0_int: PUSH ACC PUSH B PUSH DPH PUSH DPL PUSH PSW MOV PSW,#08H |
Código de entrada a sys_interp()
Usando todavía el banco de registros 0
; unsigned char sys_interp(unsigned char x_value, RSEG ?PR?_sys_interp?INTERP USING 0 _sys_interp: MOV y_value?10,R5 MOV map_base?10,R2 MOV map_base?10+01H,R3; --Variable 'x_value?10' assigned to Register 'R1' -- MOV R1,AR7 <----- FALLA!!!! |
El direccionamiento absoluto de registros supone que el banco de registros activo es el 0, y el programa falla.
C51 utiliza un cierto grado de inteligencia al entrar en las funciones de interrupción. Además de sustituir el RET del final de la función por un RETI, automáticamente empila los registros que utilice la función.
Sin embargo, hay algunos aspectos a considerar:
Direccionamiento absoluto de registros con C51.
Ya se ha comentado que el 8051 no dispone de la instrucción MOV Reg,Reg, por lo que C51 utiliza MOV R1,AR7 donde AR7 es la dirección absoluta del registro R7 en uso. Para que la sustitución funcione correctamente el compilador debe conocer el banco de registros que está utilizando. Si una función se llama desde una interrupción que utiliza el control USING, hay dos posibilidades:
Las funciones compiladas con NOAREGS sirven siempre, independientemente del banco de registros que se utilice, aunque pueden ser más lentas que las que utilicen un banco de registros definido.
El control #pragma REGISTERBANK(n) informa a C51 sobre el banco de registros utilizado, haciendo posible el direccionamiento absoluto de registros.
/* Rutina de atención a la interrupción por desbordamiento del Timer 0 */ timer0_int() interrupt 1 USING 1 { unsigned char temp1 ; unsigned char temp2 ; /* Sentencias C ejecutables ; */ } |
Función llamada por timer0_int:
#pragma SAVE // Recuerda el banco de registros actual #pragma REGISTERBANK(1) // Informa a C51 sobre el nuevo banco de registros void sys_interp(char x) {// Función llamada desde interrupción con "using 1" /* Código */ } #pragma RESTORE // Repone el banco de registros original |
Al utilizar #pragma REGISTERBANK(1) con sys_interp() se restaura el direccionamiento absoluto de registros ya que C51 conoce el banco de registros utilizado.
Nota: Utilícese siempre el control REGISTERBANK(n) para las funciones llamadas desde interrupciones con USING n.
Código de entrada de sys_interp() con REGISTERBANK(n)
; unsigned char sys_interp(unsigned char x_value, RSEG ?PR?_sys_interp?INTERP USING 1 _sys_interp: MOV y_value?10,R5 MOV map_base?10,R2 MOV map_base?10+01H,R3;-- Variable 'x_value?10' assigned to Register 'R1' -- MOV R1,AR7 |
Expresado en pseudo-código
if(interrupt routine = USING 1){ las funciones llamadas desde aquí deben usar #pragma REGISTERBANK(1) }
Nota: las funciones llamadas por la interrupción, sólo pueden llamadas desde funciones que utilicen el banco de registros 1.
A veces una misma función se llama desde interrupciones y desde el programa principal. Para que la llamada desde dos lugares diferentes no tenga consecuencias desastrosas, la función llamada debe ser re-entrante. El usuario puede especificar funciones re-entrantes utilizando el atributo reentrant en la definición de las mismas. El aviso del linker "MULTIPLE CALL TO SEGMENT" es el primer signo de que se está tratando de utilizar una función de forma re-entrante.
La razón por la cual una función no re-entrante no puede ser llamada a la vez desde el programa principal y desde una interrupción, es que C51 asigna lugares de almacenamiento en RAM, para las variables locales y para los parámetros de las funciones ordinarias.
El valor asignado a ?C_IBP en el fichero startup.a51, le dice a C51 el lugar donde debe colocar la pila artificial para las funciones re-entrantes. Cada vez que se llama a una función re-entrante, los parámetros de entrada a la misma se llevan desde los registros al área de RAM que comienza en la dirección indicada por ?C_IBP. De igual forma, cualquier variable local utilizada por una función re-entrante se coloca en esta pila especial.
Cuando, antes de main( ), se ejecuta startup.a51, la línea:
IF IBPSTACK <> 0 EXTRN DATA (?C_IBP) MOV ?C_IBP,#LOW IBPSTACKTOP ENDIF
inicializa ?C_IBP al valor que se le haya asignado anteriormente a IBPSTACKTOP. A medida que se "empilan" variables locales en la pila re-entrante, se decrementa ?C_IBP. Así, si se produce una interrupción que llama a la función de nuevo, las variables locales se guardan en el valor actual de ?C_IBP, sin destruir los valores anteriores.
Obtención de una variable local con offset 2 desde el valor actual de la pila re-entrante:
MOV R0,?C_IBP ; Obtener la base de la pila re-entrante MOV A,@R0 ADD A,#002 ; Añadir el offset MOV A,@R0 ; Obtener la variable local por direccionamiento indirecto MOV R7,A ; Guardar su valor ...
?C_IBP actúa como un puntero a la base de la pila re-entrante, utilizado para acceder a las variables locales de las funciones re-entrantes. Al salir de la función se restaura el valor de ?C_IBP a su valor inicial, sumando el tamaño de las variables locales y parámetros utilizados. Esto representa una sobrecarga de trabajo, que indica que la re-entrancia solo debe utilizarse cuando sea absolutamente necesaria.
Si se añade el atributo reentrant a la función sys_interp(), todavía se necesita el control NOAREGS ya que se ha cambiado el banco de registros con USING 1. De echo, cualquier función re-entrante debe llevar el atributo NOAREG para hacerla totalmente independiente del banco de registros.
Código de entrada de sys_interp()
; unsigned char interp_sub(unsigned char x, RSEG ?PR?_?interp_sub?INTERP USING 0 _?interp_sub: DEC ?C_IBP DEC ?C_IBP MOV R0,?C_IBP XCH A,@R0 MOV A,R2 XCH A,@R0 INC R0 XCH A,@R0 MOV A,R3 XCH A,@R0 DEC ?C_IBP MOV R0,?C_IBP XCH A,@R0 MOV A,R5 XCH A,@R0 DEC ?C_IBP MOV R0,?C_IBP XCH A,@R0 MOV A,R7 XCH A,@R0 DEC ?C_IBP ; |
Código de salida de sys_interp()
?C0009: MOV A,?C_IBP ADD A,#010H <-- Restaura ?C_IBP a su valor original position MOV ?C_IBP,A RET ; END OF _?sys_interp |
Utilizando las siguientes combinaciones de controles se evitan los avisos del linker y el código potencialmente peligroso.
Atributo de función de Interrupc. | Atributo de la función llamada: ----------------------------------|----------------------------------- | "no re-entrante" No USING | no requiere ningún atributo ---------------------------------------------------------------------- USING n | USING n | o | #pragma REGISTERBANK(n) | o | #pragma NOAREGS ----------------------------------------------------------------------
La mayoría de las funciones de libreria de C51 son re-entrantes y pueden utilizarse libremente desde el programa principal y desde las interrupciones. Sin embargo, algunas de las funciones de mayor tamaño, tales como printf(), scanf() etc. no son re-entrantes. Si se utiliza una función no re-entrante de manera re-entrante, se obtiene el aviso "MULTIPLE CALL TO SEGMENT", como cabía esperar.
Las funciones de librería "ocultas" que se encargan de las operaciones con enteros (multiplicación, división, etc.) son todas re-entrantes, lo cual hace que se puedan realizar libremente divisiones de 16 bits en las rutinas de interrupción o en el programa principal.
En cualquier caso, cuando se utilicen funciones de librería de forma re-entrante, es conveniente consultar el manual de C51 para obtener detalles del carácter de esas funciones.