Logotipo de CCS

CCS es un compilador de C para microcontroladores, pese a que en esencia es un programa en C normal presenta particularidades pues en definitiva estamos programando un micro y no un ordenador. Primero veamos un ejemplo y luego lo desgranaremos:

Logotipo de CCS

#include "16F887.h"
#include "ieeesb-uniovi.h"
#include "ieeesb-uniovi.c"
#use delay (clock=2000000)

int variableGlobal=0;

void mi_configuracion()
{
   int variableSoloDeEstaFuncion=1;
   LED2=1;
}

void main()
{
   configuracion_inicial();
   mi_configuracion();

   int variableEnMain=2;

   while (Boton1)
   {
       //No hago nada
   }//Fin Boton1

   while(1){

       //LED1 parpadea cada segundo
       LED1=!LED1;
       delay_ms(1000);

   }//FIN bucle infinito
}//FIN main()

Estructura típica

  • Includes y variables globales.
  • Nuestras funciones.
  • Función Main.

Includes y variables globales

Dentro de la sección superior esa estructura se divide de la siguiente manera:

  • Includes
  • La cabecera propia del microcontrolador: “16F887.h
  • Una cabecera más concreta de nuestra aplicación: “ieeesb-uniovi.h
  • Un archivo con funciones y declaraciones más concretas: “ieeesb-uniovi.c
  • Velocidad del reloj a la que queramos trabajar: #use delay (clock=2000000)
  • Variables globales:

Variables globales

En C para microcontroladores como la única aplicación que va a estar funcionando es la nuestra no pasa nada por utilizar las variables globales pues nadie más va a venir a modificarlas.

int variableGlobal=0;

Otra ventaja que existe es que al escribir directamente sobre las variables globales los valores se ahorran ciclos de procesamiento mejorando el rendimiento.
Aunque existe un gran PERO: hay que prestar especial atención a no modificar esa variable cuando no se debería. Por lo que pese a que se pueden usar variables globales conviene no abusar de ellas.

Nuestras funciones

En este lugar se pondrán las funciones que necesitemos para nuestro programa. La función más tradicional es la de una pequeña configuración inicial, por ejemplo si queremos que nuestro robot tenga el LED2 encendido desde el principio es el mejor lugar para hacerlo.
En nuestras funciones pueden crearse nuevas variables auxiliares que se borrarán al finalizar dicha función (y la próxima vez que se entre en la función el valor de la variable no es el mismo de la vez anterior).

void mi_configuracion()
{
     int variableSoloDeEstaFuncion=1;
     LED2=1;
}

Función Main

De esta función hay que asegurarse que nunca se salga, es decir, necesitamos crear un bucle infinito para evitar que se salga de la función.
Antes de crear el bucle infinito ejecutaremos unas instrucciones previas (como por ejemplo llamar a la función de configuración mencionada anteriormente).
Cabe destacar que gracias al bucle infinito las variables que se hagan dentro de main() las dispondremos durante todo el tiempo, con la salvedad que no pueden ser modificadas desde otras funciones (esa es la principal diferencia con las variables globales).
Algo muy común es poner un botón para iniciar el bucle infinito, en el ejemplo, encendemos nuestro robot y hasta que no se pulse Boton1 el programa no hará absolutamente nada. Una vez pulsado el botón entrará en el bucle infinito y allí permanecerá hasta que el robot se apague.

void main()
{
     configuracion_inicial();
     mi_configuracion();

     int variableEnMain=2;

     while (Boton1)
     {
          //No hago nada
     }//Fin Boton1

     while(1){
          //Contenido bucle infinito
     }//FIN bucle infinito
}//FIN main()

Bucle infinito

Usualmente, por comodidad, se escribe while(true){codigo} ó while(1){código} pero vale cualquier forma de generar un bucle infinito, como por ejemplo for(;;){codigo}
Dependiendo de la complejidad de la aplicación en este bucle habrá código o incluso puede llegar a estar vacío.

while(1){

     //LED1 parpadea cada segundo
     LED1=!LED1;
     delay_ms(1000);
}//FIN bucle infinito

En nuestro ejemplo sólo parpadeamos un LED por lo que no necesitamos nada externo para funcionar.

Pero lo más habitual es ejecutar cierto código cada X tiempo (por ejemplo comprobar el estado de los sensores blanco/negro cada 100 milisegundos), se podría hacer poniendo un delay de 100ms pero la forma más correcta es escribir ese código dentro de la interrupción de un temporizador.

De hecho lo más correcto es dejar el bucle infinito para tareas que no tengan importancia (por ejemplo parpadear el LED) y las acciones importantes se harán en las interrupciones de los timers.

Interrupciones

Son funciones normales y corrientes con la única particularidad que no se ejecutan cuando las llamamos sino que se ejecutan al ocurrir un evento.

La más típica son las llamadas “timers”, que explicado con pocas palabras es un “cronómetro” que al llegar al valor que deseemos ejecutará su código asociado.
En CCS las funciones de las interrupciones llevarán la siguiente línea (por ejemplo para el timer2):

#int_timer2
void funcion_del_timer2{codigo}

Interrupciones de timers

Para hacer funcionar un Timer se necesita indicar el origen de los pulsos de reloj y de cada cuántos pulsos de reloj se va a incrementar el contador, estas dos operaciones se hacen mediante setup_timer_X.

El funcionamiento de un Timer es contar desde 0 hasta su máximo (65535 o 256 según el Timer, este valor viene indicado en el datasheet). Normalmente no se va a querer contar todo, y para eso existe set_timerX donde se indica el valor desde el que empieza a contar. Ojo a este valor, pues no significa lo que va a contar sino desde dónde empieza, es decir, para uno que cuenta hasta 256 si se introduce setup_timer_0(56) contará 200 pulsos antes de saltar la interrupción.

Para calcular el tiempo de una temporización se puede utilizar la siguiente fórmula:

Tiempo = [(65536-carga) * PS + 2] * Tinst

  • Tiempo => Temporización en segundos del timer
  • 65536 ó 256 => Tamaño del timer, el datasheet nos dirá si es de 8 (256) o de 16 bits (65536).
  • carga => Valor que se pondrá en la función set_timer_X
  • PS => Prescaler, múltiplos de 2, consultar datasheet para ver valores.
  • Tinst => Tiempo de instrucción es 4 dividido de la frecuencia del micro: 4/focs=4/20MHz=0.2μs

Antes de empezar con la fórmula hay que ver el tamaño en bits del Timer, de esta forma se asigna carga=0 y se puede calcular el tiempo máximo que es capaz de temporizar el timer dando valores al Prescaler (PS). Una vez averiguado el PS que se ajusta a nuestro tiempo buscado se despejará de la fórmula el valor de carga y será el valor que usaremos en nuestro programa. NOTA: El +2 sólo es necesario aplicarlo si PS=1 ó PS=2.

Para que funcione hay que activar las interrupciones del Timer correspondiente y habilitar las interrupciones en el programa, se hace mediante enable_interrupts. El siguiente código configura el Timer1, ese código está pensado para estar escrito antes del bucle infinito:

void configura_TMR1()
{
     setup_timer_1(T1_INTERNAL|T1_DIV_BY_8);
     set_timer1(3036); //65536-62500=3036
     enable_interrupts(INT_TIMER1);
     enable_interrupts(GLOBAL);
}

Cuando el timer llega al final de la cuenta saltará un flag interno en el PIC que activará la rutina de interrupción (el propio CCS se encargará de borrar dicho flag) y además pondrá el contador del timer a 0. Por esta última razón lo primero que hay que hacer en la rutina de interrupción es volver a poner el número desde el que queremos que cuente nuestro timer con la instrucción set_timer_X() introduciendo el valor que nos proporcionó la fórmula del timer.

El nombre de la función es indiferente, la única condición que ha de cumplir es que la línea anterior sea #INT_TIMERx. El siguiente ejemplo es para el timer1 y sólo habría que añadir el código que se quiere ejecutar cada vez que salte la interrupción.

#INT_TIMER1      //Directiva del compilador CCS
  void timer1()  //Función de la interrupción del TIMER1
  {
     set_timer1(3036); //Precarga (cuenta 65535-3036=62500 pulsos de reloj)
     //////////CODIGO
  }

PWM para los motores

El PWM (pulse-width modulation ó modulación por ancho de pulsos) se basa en modificar el ciclo de trabajo de una señal cuadrada (en nuestro caso), con la variación del ciclo se consigue variar la tensión media que recibe la carga (en nuestro caso el motor). En la siguiente imagen se ven 3 ejemplos de tensiones medias conseguidas según el ancho del pulso:

3 ejemplos de PWM

3 ejemplos de PWM

En otras palabras, cuanto más tiempo permanezca Ton activo más tensión media se conseguirá. Los casos (teóricos) extremos serían:

  • Ton=0 es decir, la tensión media es 0.
  • Ton=T es decir, la tensión media es la de alimentación.

Para un PWM de 10 bits significará que admitirá valores entre 0 y 1023, internamente el PIC lo que hará es ajustar el tiempo al ciclo de trabajo que se haya escogido, por ejemplo si se elige 512 para el PWM de 10bits significará que la tensión media es la mitad de la tensión de alimentación.

Se recomienda consultar en la página del robot qué significan los valores de la función Motores(pwm,Número,LetraMovimiento). El siguiente ejemplo lleva durante 1 segundo el robot hacia delante (ambos motores avanzan) a la mitad de velocidad y durante medio rota máxima velocidad (rotar: un motor avanza y el otro retrocede), finalmente se apagan ambos motores:

Motores(512,1,'A');
Motores(512,2,'A');
delay_ms(1000);
Motores(1023,1,'A');
Motores(1023,2,'R');
delay_ms(500);
Motores(0,0,'L');

Tal y como se aprecia, para avanzar, retroceder, frenar y liberar ambos motores simultáneamente se puede se puede usar la función 2 veces (1 por cada motor) o enviar el valor 0 para que actúe sobre ambos a la vez.

Cálculo del PWM

Para variar el periodo (T) de trabajo de un PWM se utiliza la siguiente fórmula:

T = (PR2 + 1) * 4 * Tosc * PRESCALERtimer

  • T => Periodo del PWM
  • PR2 => Valor que introduce en la función setup_ccpX();.
  • Tosc => Tiempo de instrucción del PIC (1/fosc=1/20MHz=50nanosegundos)
  • PRESCALER => Este valor permite obtener frecuencias más elevadas a cambio de perder bits de resolución. Concretamente lo que hace este número es definir el número de overflows (veces que se llega al final de la cuenta en el timer) necesarios para genererar la interrupción.

En la siguiente imagen (se usa una frecuencia de reloj de 20MHz) hay un ejemplo en el que T vale 45μs (es decir, una frecuencia de 22,22kHz para el PWM). Habiendo utilizado las 2 ecuaciones:

Tpwm = (PR2 + 1) * 4 * Tosc * PRESCALERtimer ===> 45μ = (PR2 + 1) * 4 * 1/20M ===> PR2=224

TiempoTimer = [(256-carga) * PS + 2] * Tinst ===> 20μ = [(256-carga)*1+2]*4/20M ===> carga=158

Ejemplo de un PWM

El código para configurar el PWM es sencillo, pues sólo hay que configurar el Timer asociado a los PWM e indicar mediante setup_ccpX que se va a utilizar el PWM.

setup_timer_2(T2_DIV_BY_1,158,1);
setup_ccp1(CCP_PWM); //CCP1 y CCP2 serán utilizados y
setup_ccp2(CCP_PWM); //sus puertos son salidas PWM

Desde este momento tan sólo hay que utilizar la función set_pwmX_duty(ValorDelPWM); y opcionalmente en el robot de los talleres 2012 la función Motores(); los dos siguientes bloques de programación son equivalentes y hacen lo mismo.

Forma genérica:

set_pwm1_duty(pwm);
    M1In1=1; //Para que avance
M1In2=0; //son estos valores

Forma particular del robot de los talleres 2012:

int16 pwm;
Motores(pwm,1,'A');

En ambos casos la variable pwm se habrá declarado como un entero de 16 bits mediante la siguiente instrucción:

int16 pwm;

Y se le habrá dado un valor entre 0 y 1023 antes de utilizarla.

Más información

Para más información de cómo utilizar el CCS se recomienda el PDF oficial de CCS (página de descargas oficial), también se copia en el disco duro durante la instalación del compilador oficial.