martes, 20 de marzo de 2012

Programando PIC con C++: Switch VS if {...} else if {..}

Cuando se programa microcontroladores en C++. no hay que perder de vista la arquitectura con la que se está trabajando. El código que se escriba en C++ condicionará el código ensamblador que generará el compilador empleado. Un claro ejemplo de esto es el caso de la estructura switch frente a una estructura if else if. Mucha gente pensará que los siguientes códigos en C++ son equivalentes:

void main(void)
{
 int a=0;
 while(1){
  a++;
  if(a==0){
   a=1;
  }else if(a==1){
   a=2;
  }else if(a==2){
   a=3;
  }else if (a==3){
   a=4;
  }else{
   a=0;
  }
 }
}







void main(void)
{
 int a=0;
 while(1){
  a++;
  switch(a){
   case 0:
    a=1;
   break;
   case 1:
    a=2;
   break;
   case 2:
    a=3;
   break;
   case 3:
    a=4;
   break;
   default:   
    a=0;
   break;
  }
 }
}

No obstante el código que realmente genera el compilador no es el mismo. Por lo general el código que se genera para un switch es, en termino medio, más eficiente que en el caso de los if else. Cuando se trabaja con sistemas de tiempo real que deben interaccionar con el entorno, hay que tener en cuenta que se deben realizar ciclos con las siguientes fases :
  1. Lectura de sensores
  2. Ejecución del código y toma de decisiones
  3. Actuación (transmisión de las decisiones a los actuadores)
Este código debería ejecutarse con unos tiempos de ciclo lo mas constantes que sea posible, es decir, que la en el tiempo de ejecución de diferentes ciclos no haya grandes diferencias. Si se utilizan las estructuras if else, intuitivamente se puede sospechar que no será la misma cantidad de comprobaciones las que se harán para ejecutar el primer if que si se ejecuta el último. Esto se debe a que para que se ejecute el último caso primero de deben haber descartado todos los anteriores. El problema es que se está haciendo ineficiente la ejecución de acciones para los últimos casos de la estructura, y además sin que esto aporte ninguna ventaja. Este tipo de códigos se pueden construir mediante estructuras switch como se muestra en el ejemplo y esta creará un código bastante más eficiente y con unos costes temporales menos extremos, los mejores y los peores casos tendrán costes temporales similares.

Para ilustrar los ejemplos veamos el código ensamblador que se genera en cada uno de ellos para el PIC16F876A con el compilador SDCC. Para entender el código, se recomienda leer la entrada anterior, pero en todo caso al menos se habrá de tener en cuenta las siguientes indicaciones:

  • el registro r0x1000 se refiere a la parte baja del valor de a y r0x1001 a la parte alta de la misma variable.
  • La variable a es una variable entera de 16 bits, 8 por registro.
  • Cuando se habla de L(a) y H(a) en los comentarios se refiere a la parte baja y alta de la variable a respectivamente.
  • La instrucción BTFSC <reg>,<num> se salta la instrucción contigua cuando el bit <num> del registro <reg> está a 0 (clear)
  • La instrucción BTFSS <reg>,<num> se salta la instrucción contigua cuando el bit <num> del registro <reg> está a 1 (set) 
  • W es el registro acumulador de la ALU
Código 1: estructura if else if

_main ;Function start
; 2 exit points
; .line 3; ------ int a=0;
 BANKSEL r0x1000 
 CLRF r0x1000 ;se borra la parte baja de a
 CLRF r0x1001 ;se borra la parte alta de a
_00118_DS_
; .line 5; ------ a++;
 BANKSEL r0x1000
 INCF r0x1000,F ;incremento de a
 BTFSC STATUS,2 ;si el resultado es cero
   ;se ejecutará la siguiente linea que 
 INCF r0x1001,F  ;incrementa la parte alta
; .line 6; ------ if(a==0){
 MOVF r0x1000,W ;se pone H(a) de a en W
 IORWF r0x1001,W ; OR entre W y la L(a)
 BTFSS STATUS,2 ;si el resultado no es cero
 GOTO _00115_DS_ ;se salta a esta etiqueta
; .line 9; ------ a=1;
 MOVLW 0x01
 MOVWF r0x1000
 CLRF r0x1001
 GOTO _00118_DS_
_00115_DS_
; .line 10; ---- }else if(a==1){
 BANKSEL r0x1000
 MOVF r0x1000,W
 XORLW 0x01      ;XOR entre L(a) y 0x01
 BTFSS STATUS,2  ;si el reslutado no es 0
 GOTO _00112_DS_ ;se salta a la etiqueta
 MOVF r0x1001,W  ;de lo contrario se
 XORLW 0x00 ;se comprueba la parte alta 
 BTFSS STATUS,2  ;si H(a) no es cero
 GOTO _00112_DS_ ;se salta a la etiqueta
; .line 11; ---- a=2;
 MOVLW 0x02
 MOVWF r0x1000
 CLRF r0x1001
 GOTO _00118_DS_
_00112_DS_

; .line 12; ---- }else if(a==2){
 BANKSEL r0x1000
 MOVF r0x1000,W
 XORLW 0x02
 BTFSS STATUS,2
 GOTO _00109_DS_
 MOVF r0x1001,W
 XORLW 0x00
 BTFSS STATUS,2
 GOTO _00109_DS_
; .line 13; ---- a=3;
 MOVLW 0x03
 MOVWF r0x1000
 CLRF r0x1001
 GOTO _00118_DS_
_00109_DS_
; .line 14; ---- }else if(a==3){
 BANKSEL r0x1000
 MOVF r0x1000,W
 XORLW 0x03
 BTFSS STATUS,2
 GOTO _00106_DS_
 MOVF r0x1001,W
 XORLW 0x00
 BTFSS STATUS,2
 GOTO _00106_DS_
; .line 15; ---- a=4;
 MOVLW 0x04
 MOVWF r0x1000
 CLRF r0x1001
 GOTO _00118_DS_
_00106_DS_
; .line 17; ---- a=0;
 BANKSEL r0x1000
 CLRF r0x1000
 CLRF r0x1001
 GOTO _00118_DS_
 RETURN 


Cómo se puede observar en el código anterior, hasta que se ejecuta el caso por defecto, se han tenido que realizar unos 4 saltos condicionales. Teniendo en cuenta el coste de ciclos de cada una de las operaciones se tiene un mejor caso (a=0) de 11 ciclos hasta que se llega a ejecutar el código de dicha opción. El conteo de los ciclos se incluye a continuación de forma ilustrativa. Nótese que en el caso de las instrucciones de salto condicional BTFSC y BTFSS valen 2 ciclos ya que el mejor caso se considera que no se cumplen, también derivado de este hecho la operación contigua a estas tiene un coste 0 ya que en su lugar se ha ejecutado una operación nop es decir un ciclo vacío.

 (1) BANKSEL r0x1000 
 (1) CLRF r0x1000 ;se borra la parte baja de a
 (1) CLRF r0x1001 ;se borra la parte alta de a
_00118_DS_
; .line 5; ------ a++;
 (1) BANKSEL r0x1000
 (1) INCF r0x1000,F ;incremento de a
 (2) BTFSC STATUS,2 ;si el resultado es cero
   ;se ejecutará la siguiente linea que 
 (0) INCF r0x1001,F  ;incrementa la parte alta
; .line 6; ------ if(a==0){
 (1) MOVF r0x1000,W ;se pone H(a) de a en W
 (1) IORWF r0x1001,W ; OR entre W y la L(a)
 (2) BTFSS STATUS,2 ;si el resultado no es cero
 (0) GOTO _00115_DS_ ;se salta a esta etiqueta

Si se considera el peor caso, es decir, en el que se debe ejecutar el caso por defecto pasando por el camino más largo, se tiene un total de 42 ciclos ya que cada comparación (excepto la de a==0) tiene un coste de 10 ciclos de ejecución en su peor caso y la de a==0 tarda un ciclo más en el caso en que se ejecute el GOTO que tiene un coste de 2 ciclos, aunque el BTFSS pase a costar 1 ciclo.

Codigo 2 : Estructura switch

_main ;Function start
; 2 exit points
; .line ---- int a=0;
 BANKSEL r0x1000
 CLRF r0x1000
 CLRF r0x1001
_00112_DS_
; .line 7; "ejemplo1-2.c" a++;
 BANKSEL r0x1000
 INCF r0x1000,F
 BTFSC STATUS,2
 INCF r0x1001,F
;---------------------------
;Hasta aquí es igual que antes
;---------------------------

A partir de aquí se muestra a la derecha el caso default y a la izquierda la continuación del código que ejecutaría el caso general.

;signed compare: 
;left < lit(0x0=0), size=2, mask=ffff
; .line 8; "ejemplo1-2.c" switch(a){
 BSF STATUS,0    ;se borra el flag C
 BTFSS r0x1001,7 ;se mira signo de a
        ;si el signo de a es negativo
 BCF STATUS,0    ;se activa el flag C
 BTFSC STATUS,0  ;si el flag C está activo
 GOTO _00109_DS_ ;se manda al caso default
;genSkipc:3083: created from rifx:0x286044
;swapping arguments (AOP_TYPEs 1/2)
;signed compare:
; left >= lit(0x4=4), size=2, mask=ffff
 MOVF r0x1001,W  ;Se carga H(a) en W
 ADDLW 0x80  ;se le suma dos veces 0x80
 ADDLW 0x80  ;con esto se elimina el signo
 BTFSS STATUS,2 ;si el resultado no es cero
 GOTO _00119_DS_ ;se pasa al caso default
 MOVLW 0x04      ; se carga un 4 en W
 SUBWF r0x1000,W ;se resta L(a)
_00119_DS_
 BTFSC STATUS,0  ;si el flag C está activo
 GOTO _00109_DS_ ;se va al default
;Hasta aquí sólo se ha analizado el default
; y se puede comprobar un mejor caso de 
;13 ciclos (salir por el primero GOTO)
; y un peor caso de 22 ciclos
; saliendo por el último

;genSkipc:3083: created from rifx:0x286044
 MOVLW HIGH(_00120_DS_);se carga la parte H
 MOVWF PCLATH ;de la direc. 120 al PCLATCH
    ;esto establece la parte alta del valor
    ;del contador de programa (PC)
 MOVLW _00120_DS_ ;mueve la parte baja de 
 BANKSEL r0x1000  ;la misma dirección a W 
 ADDWF r0x1000,W  ;y se le suma a.
 BTFSC STATUS,0  ;Si hay accarreo
 INCF PCLATH,F  ;se incrementa el PCLATCH
 BANKSEL PCL   ;Con esto se ha establecido
 MOVWF PCL ;el nuevo valor para el contador 
    ; de programa. Este ahora estará
    ; apuntando a uno de los GOTO 
    ; siguientes. Cada GOTO representa
un case del switch.
_00120_DS_
 GOTO _00105_DS_
 GOTO _00106_DS_
 GOTO _00107_DS_
 GOTO _00108_DS_
    ; este código consume 11 ciclos incluyendo
    ;el último GOTO




Como se puede observar en el caso del switch, el peor caso ya no es el default sino cualquiera de los demás casos. Además todos tardarán la misma cantidad de ciclos en llegar al código que deben ejecutar que en este caso es de 7 (inicialización de a e incremento) 5+9 (comprobación del default) + 11 (caso general) = 32 ciclos. Nótese que esa cantidad de ciclos representa aproximadamente tres cuartas partes del peor caso del ejemplo inicial con la estructura if else, además este coste es constante para cualquiera de los diferentes casos del switch. Existan la cantidad de casos que existan este coste permanecerá constante siempre y cuando estos casos tengan valores consecutivos, en caso contrario se iría añadiendo complejidad a la parte que evalúa el default. En un caso general en el que se implementa una maquina de estados, esta solución es la solución idónea para programar dicho sistema ya que evitará problemas y proporcionará tiempos equivalentes para alcanzar los diferentes casos y proporcionar una respuesta adecuada, por lo que el funcionamiento global mejorará considerablemente. Además de esto, con maquinas de estados complejas, la ganancia al emplear una estructura switch frente al uso de una estructura if else crecerá linealmente.

Por último el resto del código para este caso, donde están las etiquetas de cada uno de los case del switch:

_00105_DS_
; .line 10; "ejemplo1-2.c" a=1;
 MOVLW 0x01
 BANKSEL r0x1000
 MOVWF r0x1000
 CLRF r0x1001
; .line 11; "ejemplo1-2.c" break;
 GOTO _00112_DS_
_00106_DS_
; .line 13; "ejemplo1-2.c" a=2;
 MOVLW 0x02
 BANKSEL r0x1000
 MOVWF r0x1000
 CLRF r0x1001
; .line 14; "ejemplo1-2.c" break;
 GOTO _00112_DS_
_00107_DS_
; .line 16; "ejemplo1-2.c" a=3;
 MOVLW 0x03
 BANKSEL r0x1000
 MOVWF r0x1000
 CLRF r0x1001
; .line 17; "ejemplo1-2.c" break;
 GOTO _00112_DS_
_00108_DS_
; .line 19; "ejemplo1-2.c" a=4;
 MOVLW 0x04
 BANKSEL r0x1000
 MOVWF r0x1000
 CLRF r0x1001
; .line 20; "ejemplo1-2.c" break;
 GOTO _00112_DS_
_00109_DS_
; .line 22; "ejemplo1-2.c" a=0;
 BANKSEL r0x1000
 CLRF r0x1000
 CLRF r0x1001
; .line 24; "ejemplo1-2.c" }
 GOTO _00112_DS_
 RETURN