martes, 20 de marzo de 2012

Programando PIC con C++: ¿Porque programar en C++?

Tradicionalmente, cuando se enseña programación de microcontroladores, se emplea como lenguaje de programación el ensamblador del microcontrolador seleccionado. Esto tiene particular interés, desde el punto de vista didáctico, ya que al utilizar las funcionalidades del microcontrolador en ensamblador se está obligado a entender perfectamente el funcionamiento del propio microcontrolador. Se podría decir que la cercanía del lenguaje a la arquitectura de la maquina fuerza a su comprensión para poder programarlo. Sin embargo, como es evidente para cualquier persona que haya programado en lenguajes de alto nivel, la programación en lenguajes de bajo nivel no es eficiente en cuanto a la generación de código. Algunas de las tareas más sencillas como la programación de una estructura for o un simple if pueden llegar a variar mucho de un microcontrolador a otro en función de su arquitectura, por lo que hasta para este tipo de funcionalidades tan elementales habría que estudiar el repertorio de instrucciones, con el riesgo de no crear el código más óptimo.

Los microcontroladores más típicos (arquitecturas de 8 a 16 bits) suelen ser maquinas con arquitectura RISC, es decir, computadoras con un reducido conjunto de instrucciones. El motivo de esto es que se persigue construir máquinas lo más sencillas que sea posible. Las arquitecturas RISC permiten crear máquinas con un pequeño conjunto de instrucciones atómicas que permiten crear cualquier programa. La sencillez de estas instrucciones, a su vez, las hace muy rápidas, requiriendo sólo de uno o dos ciclos para ser ejecutadas. Sin embargo, esto tiene ciertas implicaciones dependiendo del criterio con que el fabricante haya diseñado/seleccionado dicho repertorio de instrucciones. Según las instrucciones incorporadas y su funcionamiento, la creación de estructuras sencillas puede variar mucho y por tanto se ha de invertir un tiempo en dominar la arquitectura en cuestión antes de comenzar a programar. Si a esto se le añade los problemas comunes inherentes a la programación de microcontroladores como conocer el patillaje del microcontrolador, realizar la correcta configuración de los puertos y de los distintos periféricos, funcionamiento de los registros especiales, etc., cambiar de fabricante o de microcontrolador se convierte en una ardua tarea.

No obstante, el repertorio de instrucciones es muy limitado, para abordar un ejemplo se ha elegido para el caso, el repertorio de instrucciones de la familia PIC16F87X. Este repertorio de instrucciones consta de aproximadamente 35 instrucciones. Además de estas existe instrucciones como nop que realiza un ciclo sin operación y algunas otras con fines muy concretos, y que no nos interesan para el tema que abordamos. En las siguientes tablas se muestran las instrucciones más importantes. En ellas se puede ver sucesivamente 18 operaciones orientadas a byte, 4 orientadas a bit y 13 operaciones con literales e instrucciones de control.


Mnemonic Operands Description Cycles Status Affected
ADDWF f, d Add W and f 1 C,DC,Z
ANDWF f, d AND W with f 1 Z
CLRF f Clear f 1 Z
CLRW - Clear W 1 Z
COMF f, d Complement f 1 Z
DECF f, d Decrement f 1 Z
DECFSZ f, d Decrement f, Skip if 0 1(2) -
INCF f, d Increment f 1 Z
INCFSZ f, d Increment f, Skip if 0 1(2) -
IORWF f, d Inclusive OR W with f 1 Z
MOVF f, d Move f 1 Z
MOVWF f Move W to f 1 -
NOP - No Operation 1 -
RLF f, d Rotate Left f through Carry 1 C
RRF f, d Rotate Right f through Carry 1 C
SUBWF f, d Subtract W from f 1 C,DC,Z
SWAPF f, d Swap nibbles in f 1 -
XORWF f, d Exclusive OR W with f 1 Z


Mnemonic Operands Description Cycles Status Affected
BCF f, b Bit Clear f 1 -
BSF f, b Bit Set f 1 -
BTFSC f, b Bit Test f, Skip if Clear 1 (2) -
BTFSS f, b Bit Test f, Skip if Set 1 (2) -


Mnemonic Operands Description Cycles Status Affected
ADDLW k Add Literal and W 1 C,DC,Z
ANDLW k AND Literal with W 1 Z
CALL k Call Subroutine 2 -
CLRWDT - Clear Watchdog Timer 1 TO,PD
GOTO k Go to Address 2 -
IORLW k Inclusive OR Literal with W 1 Z
MOVLW k Move Literal to W 1 -
RETFIE - Return from Interrupt 2 -
RETLW k Return with Literal in W  2 -
RETURN - Return from Subroutine 2 -
SLEEP - Go into Standby mode 1 TO,PD
SUBLW k Subtract W from Literal 1 C,DC,Z
XORLW k Exclusive OR Literal with W 1 Z


Para más detalles acerca de dichas instrucciones se puede recurrir a la sección 15 del datasheet de los PIC de la familia 16F78X. Por poner un ejemplo, el código requerido para la programación de un if debería ser como el siguiente:

Código C++:

        .....
        if(a==0){
            f();
        }else{
           ....

Código Generado con compilador SDCC:

Ciclos   Instrucción
  (1)     BANKSEL    _a  ;Se selecciona el banco de memoria donde está la variable a
  (1)     MOVF    _a,W    ;Se mueve a al acumulador
  (1)     IORWF    (_a + 1),W  ;Se realiza una OR lógica entre (a+1) parte alta H(a) de a 
                                        ; y el acumulador parte baja de a L(a)
  (1,2)  BTFSS    STATUS,2    ;Se comprueba el estado del bit de Zero (si es 1 se salta la siguiente
                                               ; instrucción) si el bit es set (1 ciclo sino 2 ciclos)
  (2)     GOTO    _00110_DS_  ;Si la OR lógica entre H(a) y L(a) no es cero se salta al else del if
  (2)     CALL    _f         ; De lo contrario significa que a vale 0 y por lo tanto se ejecuta el interior del if


Como se puede comprobar en el código anterior, el funcionamiento de la comparación de la variable a con cero, se realiza de una forma muy dependiente de la maquina y que porbablemente se haya elegido por rendimiento frente a otras opciones.  En el mejor caso este código tarda 6 ciclos y en el peor caso el código tarda 7 ciclos. Sin embargo al realizar una comparación con uno se puede comprobar que el código generado no es el mismo:

Código C++:
        .....
        if(a==1){
            f();
        }else{
           ....

Código Generado con compilador SDCC:

 Ciclos   Instrucción
  (1)     BANKSEL    _a  ;Se selecciona el banco de memoria donde está la variable a
  (1)     MOVF    _a,W    ;Se mueve a al acumulador la parte baja de a
  (1)     XORLW    0x01  ;Se realiza una XOR entre el acumulador y el valor 1 de manera que si difiere en 
                                       ;algún bit el resultado será disinto de 0
  (1,2)  BTFSS    STATUS,2    ;Se comprueba el flag de cero Z 
                                                ;(si está activo se salta la siguiente instrucción)
  (2)     GOTO    _00112_DS_   ;Si es distinto de cero quiere decir que no vale 
                                                  ; uno y portanto sale del if con esta instrucción GOTO

           ;A continuación se realiza la misma opreación para la parte alta de a

  (1)     MOVF    (_a+1),W       ;Se mueve la parte alta de a al acumulador
  (1)     XORLW    0x00            ;Se realia una XOR entre el acumulador y cero
  (1,2)  BTFSS    STATUS,2      ;Se comprueba el estado del flag cero Z 
                                                  ;Si está activo se salta la siguiente  instrucción y ejecutaría el código del if
  (2)    GOTO    _00112_DS_  ;Si el flag de cero no está activo significa que la parte de a no vale cero
  (2)     CALL    _f         ; Este sería el código interno del if

Como se puede ver este último código se puede generalizar y se podría emplear para realizar la comparación también con cero simplemente sustituyendo el valor de la segunda instrucción por 0x00 al igual que se hace con la parte alta de la variable a. Sin embargo, el primer código que generó el compilador es más óptimo ya que este último tiene un mejor caso de 6 ciclos (correspondería al caso en que la parte baja de a no vale 1) y un peor caso de 11 ciclos (se ejecutarían las instrucciones 1,2,3,4x2,6,7,8x2 y 10). Al programar directamente en ensamblador, este tipo de optimizaciones corren por cuenta del programador y puede que se le ocurra realizar las tareas de esta forma pero también corre el riesgo de que no sea así. Además, por lo general estas optimizaciones dependen enormemente del microcontrolador que se está empleando por lo que requeriría de un profundo estudio para encontrar las técnicas más eficientes y una gran disciplina para ser metódico y hacer las cosas siempre de la misma forma, y no usar diferentes métodos según se le vayan ocurriendo.

En conclusión, al programar en C++ un programador tiene la ventaja de abstraerse de este tipo de detalles dejándolos para el compilador. En C++ las estructuras if, for o while siempre son de la misma forma y por lo tanto el programador no necesita preocuparse por estos detalles. Además, como se ha demostrado en muchos casos el compilador va a realizar códigos muy eficientes de manera automática. 

No obstante, cabe destacar que no es oro todo lo que reluce. Cuando se programa un microcontrolador en C++ hay que tener en cuenta ciertos detalles, que pueden pasar desapercibidos por un programador experto que desconozca la arquitectura sobre la que se está trabajando. La mayoría de estos detalles, se refieren a la forma de trabajar de un compilador. Es muy importante que el programador tenga conocimientos de bajo nivel sobre compiladores para que tenga en cuenta estos detalles, sin perder de vista que no todos los compiladores tienen porque generar el mismo código, pero que sí es frecuente que sigan unas ciertas pautas comunes a todos ellos. A groso modo, se puede adelantar que por regla general un switch es más eficiente que una serie de estructuras if else, que un if else if es mejor que varios if independientes consecutivos cuando los casos son excluyentes, que las llamadas a funciones consumen pila del microcontrolador y que en el caso del 16F876X esta pila está limitada a 8 niveles. En futuras entradas se comentarán estos y otros detalles acerca de la programación de este microcontrolador en C++ intentando que la mayoría de ellos se pueden extrapolar a otros casos.