martes, 14 de agosto de 2012

Registros GPI/O Avr-gcc en Arduino

 Registros  GPI/O Avr-gcc en Arduino.
Aventuras del Mundo AVR .



Nota 0.1 manipular puertos en Arduino con los registros DDRx, PORTx  y PINx.





     "Y Llegara el día en que digitalWrite(); no de abasto y los aventureros de corazón buscaran mas soluciones" a la hora de manipular todos los pines digitales del Arduino, probablemente  cuando quieras manipular muchos LEDs a diestra y siniestra o quieras armar un cubo LED, veras que digitalWrite(); no dará abasto. 

     "Y llegara el día en que quieras aprender a programar en C de AVR y la burbuja de Arduino no te dejara escapar tan fácilmente ", evidentemente algun día querrás conocer que hay detras de DigitalWrite(); o queras programar en lenguaje C para el ATMega328, querrás saber que significa PORTx, DDRx y PINx .

    Oh simplemente quieres aprender a programar en lenguaje C para micro controladores AVR y te encontraras con un sin fin de nuevos significados, procesamiento de datos, variables nuevas, en fin, este post surgió del hecho de que  tope con los mismo problemas anteriormente mencionados (por querer cambiar de el entorno mikroC) y de indagar  en Internet,  logre dar con toda la información necesaria, para paso a paso comprender y aprender a manipular los puertos de un micro controlador en este caso de los AVR. 
(agradecimientos y referencias al final).


comenzando desde cero:
    para los iniciados ya en la rama de los micro controladores, manipular los puertos de los mismos es el ejercicio inicial y mas fundamental para su aprendizaje. A continuación comenzare a detallar poco a poco las funciones ,códigos, registros y la manipulación de los mismos.


primero: como reconocemos los puertos de un Micro-controlador:





si vemos la imagen superior de un ATMega168 símil del 328, nos llamara la atención la cantidad de datos que nos proporciona cada pin del Micro, si vemos la primera designación para cada pin desde el interior hacia afuera nos toparemos con el nombre del pin y su puerto.

 si vemos el pin nº2 del Atmega328 veremos que la primera designación es PD0,¿que significa esto?,
PD0 = Puerto D pin nº0. pin numero 0 correspondiente al puerto D del micro-controlador.

    Cada micro-controlador tiene puertos de propósito general llamados GPI/O, o puertos de propósito general de entrada y salida, según el uControlador en cuestión, este tendrá desde un puerto a N cantidad. Un puerto consta de 8 pines, cada  puerto es identificado con una letra, en algunos casos comienzan de la A y en otros la letra B, como vemos en la imagen superior nuestro ATMega168 tiene tres puertos; el puerto B que va desde el pin PB0 al PB7, el puerto C que va desde el pin PC0 al PC7 y el el puerto D que va desde el pin PD0 al PD7.
   
   A la hora de programar en compiladores de C o C++ para Micros AVR para llamar a los puertos o utilizarlos se les llama con la designación de cada puerto B, C y D y ahora que sabemos como se identifican estos puertos y los pines de cada uno podemos pasar a ver como manipularlos.


   Para no dar un salto tan grande desde Arduino a Avr, podemos usar la misma IDE Arduino para manipular los puertos a nuestro antojo, ya que la IDE compila en la utilidad avr-gcc, podemos utilizar  las mismas funciones y registros que usamos para manipular puertos a bajo nivel.

bien el segundo paso sera conocer que para manipular los puertos GPI/O nosotros usaremos lo que se conoce como "registros de control de puertos" estos registros de control nos permiten operar la dirección, escritura y lecturas logicas en cada pin o puerto del microcontrolador. para manipular un puerto por completo Existe tres registros, los cuales son: DDRx, PORTx y PINx.


DDRx (Data Direction Register):

    Este registro lo podemos comparar en el entorno Arduino con pinMode();, ya que este registro se encarga de manipular la dirección de cada pin del puerto, esto significa que podemos indicar a cada pin del puerto x, que trabajen como escritura o lectura, salida o entrada, o como en Arduino pinMode(3,OUTPUT); DDRx se encarga de este trabajo, ahora la pregunta es ¿como funciona este registro?.

    DDRx es un registro que nos permite indicar el modo de operación de  cada pin del puerto, la x es para indicar el puerto, si quisiésemos manipular el puerto B del Atmega328 DDRx seria DDRB,si comparamos con el entorno Arduino DDRx es similar a PinMode en el cual se debe indicar el numero de pin y la modalidad de trabajo, en DDRx debes indicar en una instrucción ambas cosas.
 
 
Ejemplo:
    teniendo en cuenta la imagen del ATMega328, (arriba), podremos observar los datos de los pines correspondientes a cada puerto,( PB,PC,PD etc) y el nombre del pin en la placa Arduino. podemos observar que los pines del puerto D, PD0 al PD7 son los pines de la placa Arduino digital 0,1,2,3,4,5,6 y 7, por ende podemos usar este puerto como ejemplo para el registro DDRx.

sabemos que el puerto de esos pines es el D, por ende el registro sería DDRD, cabe mencionar que el pin PD0 y PD1 son los pines del Arduino RX y TX, estos pines debemos tratar de no ocuparlos demasiado  debido a que por ellos el atmega328 es programado desde la IDE Arduino, es decir estos pines deben ser siempre de entrada.

entonces: DDRD = 11111100; "00" son los pines PD0 y PD1, RX y TX en Arduino, son denominados 0 para indicarle al registro que estos pines serán de "lectura de datos",  y el resto de los pines serán de escritura o salida y por ende son designados como 1. 


NºPin   = 76543210;
DDRD  = 11111100;

1 = salida o write
0 = entrada o read;

En la función de Arduino pinMode(numero,modalidad) realiza el mismo trabajo, pero pin a pin, indicando el numero de pin y si este es de lectura o escritura. 

código ejemplo

void setup(){
  DDRD  = 11111100;
}




PORTx (Pin Output Register):
   Este registro en Arduino tendría su similar con la función digitalWrite(nºpin, estado); , este registro se encarga de escribir el estado de cada uno de los puertos al cual este controlando, como vimos en DDRx el cual designaba la modalidad de trabajo para cada Pin, PORTx se encarga por ahora de los pines que fueron configurados como salida "1", o en  Arduino pinMode(3,OUTPUT); para luego decir digitalWrite(3,HIGH);.
  Como observamos en el esquema de arriba, al aplicar PORTx todos los pines que fueron configurados como salidas, en ellos PORTx escribirá un valor lógico alto "1" o un valor lógico bajo "0", (el valor 1 depende del voltaje logico del circuito los mas comunes son 5 y 3.3 donde 5 y 3.3 son "1") 


Ejemplo:
   Siguiendo el esquema anterior donde ocupamos el puerto D, entonces:

   DDRD   = 11111100;// pin 0 y 1 como entrada, del pin 2 al 7 como salida;
  PORTD  = 10101000;// pin  3 , 5 y 7 tienen en su salida un valor lógico alto o 5v y el resto 0. 

si aplicásemos PORTD como en ejemplo, si conectamos LEDs  a los pines digitales 2,3,4,5,6,7; podríamos observar que los LEDs conectados a los pines 3,5,7 estarán encendidos.

código ejemplo

void setup(){
  DDRD  = 00000000;
  PORTD = 10101000;
}

nota(digitalWrite() digitalWrite consume 50 ciclos de reloj aproximadamente),
(imagen ejemplo solo para referencia, no conecten el diagrama ejemplo, le faltan resistencias de protección a cada LED)


PORTx para pines de Entrada.
   PORTx también se ocupa cuando los pines de los puertos son configurados como entrada o de lectura con el registro DDRx, y es empleado para activar resistencias de PULLUP, ¿que significa esto?, que los pines de entrada podemos reconectarlos a un uno lógico "1", y así evitar problemas de ruido o como también detectar el cambio de 1 a 0, esto se aplica conectando a cada pin de entrada una resistencia conectada a Vcc o 5v, en Arduino y por ende cada pin con PULLUP esta conectado a 1.

ejemplo:
   0 = sin pullup.
   1 = con pullup.

   NºPin  =  76543210;
   DDRD = 11100000; // pines 2,3,4 configurados como entrada.
  PORTD= 00011100; //pines 2,3,4 se habilita la resistencia de pullup. 

La imagen de referencia nos permite inferir como quedan habilitados  los pines cuando estos son configurados como lectura  y además habilitados con resistencias de PULLUP.   


PINx (Pin Input Register):
   solo va faltando una cosa por realizar y es la lectura con los pines GPI/O, leer un estado 1 o 0, el registro PINx nos permite obtener el estado del puerto en general como también de un pin en especifico.

para entender podemos seguir los ejemplos anteriores y usar el puerto D.

byte estadoPuerto;//creamos una variable que contendrá los datos de cada pin.
                             //en Arduino byte es la forma de referirse a la variable int8_t de las mismas características, variable de 8 bits. 

"como cada puerto solo posee 8 pines, usamos variables del mismo tamaño, en Arduino byte es una variable que puede almacenar datos de hasta 8 bits, por ende se vera en forma optima el estado de cada puerto representado en cada bit"

DDRD = 00000000;//todos los pines del puerto D como entrada.
PORTD = 00000000;//indicamos que los pines no tendrán pullup.

Si los pines 2,3 y 4 los conectamos a 5v y los pines 6,7 y 8 a 0v (GND), el estado de los pines en el puerto se verían de esta forma

Conexión = 00011100;

Por ende si realizamos una lectura.

EstadoPuerto = PIND.

El valor de estado puerto si lo mostramos en puerto serial con "serial.println(estadoPuerto,BIN)"
lo mostrado en el puerto serial serial 000111000.

El código seria:

byte estadoPuerto;

void setup(){
  DDRD  = 0B00000000;
  PORTD = 0B00000000;
  Serial.begin(9600);
}

void loop(){
  estadoPuerto = PIND;
  Serial.println(estadoPuerto,BIN);
  delay(1000);
}

Puerto serial =   56d =  111000b;

   Estos son los tres grandes registros de manipulación de puertos GPI/O a grandes rasgos, estos registros pueden verse toscos a la hora de usarlos , pero se pueden complicar mas o simplificar si tenemos un correcto uso de las operaciones lógicas digitales básicas.  las cuales son AND ("&"), OR ( "|" ) y NOT ("~"), y un extra , las operaciones shitfLeft y shiftRight "<< y >>"

resumen:

   La operación And  es una multiplicación lógica  binaria, la cual retorna un 1 solamente cuando ambos datos multiplicados son iguales a 1, "A and B = C" =>" A & B = C".
Ejemplo:

And = &.

0 & 0 = 0;
0 & 1 = 0;
1 & 0 = 0;
1 & 1 = 1;

La operación Or es una suma lógica  binaria , la cual retorna un 1 cuando cualquiera de datos sumados sea 1,  "A or B = C" =>" A | B = C".

Or = "|" ;
0 | 0 = 0;
0 | 1 = 1;
1 | 0 = 1;
1 | 1 = 1;

La operación NOT  nos retorna el inverso lógico de un dato binario.
"si A = 0" ==>  "Not(A)= 1" ==>"~A = 1".

Not = "~".
~0 = 1;
~1 = 0;

  Las operaciones shiftLeft y shiftRight son un tipo de instrucción para manipular datos, son usadas para desplazar bits a lo largo de un dato como por ejemplo un byte (dato compuesto de 8 bits), ahora como se usan:

   Supongamos que tenemos una variable del tipo byte llamada "data", data consta 8 bits de información los cuales son: "byte data = 00000000", pero nosotros dadas a circunstancias externas  queremos que data en ves de valer 0 tenga un valor binario de cuatro = "00000100", pero para llevar a cabo esto tendríamos que volver a re-asignar el nuevo valor a la variable, oh aplicar operaciones básicas para cambiar el valor de "data", pero contamos con la operación "shiftLeft" la cual nos permite ingresar y desplazar un bit n espacios a la izquierda, por ende ingresamos el bit 1 y lo desplazamos  hacia la izquierda para que data  sea igual a 4 en ves de 0, ejemplo:

shiftLeft "<<" 

Byte data = 00000000; //data = 0;
     data = 1<<2;   //ingresamos el bit 1  lo ubicamos en 0 y lo desplazamos a la                                                 
                    //izquierda dos espacios 
     data = 00000100; //data = 4;



   Cabe mencionar que al mover el bit hacia la derecha o a la izquierda, el vació dejado por el bit al desplazarse será ocupado  por "0" y esto ocurrirá tanto si desplazamos un "0" o un "1".

ejemplos:
        00000001; 1<<5 = 01100000;
    11111111; 0<<3 = 11110000;
    01010101; 1<<2 = 10101100;

para shiftRight;

    11111110; 0>>5 = 00000011;
    00011100; 1>>3 = 00010001;
     

   Ahora que refrescamos nuestra memoria con las operaciones básicas And, Or, Not y  ademas haber conocido las operaciones "shift" podemos comenzar  las "Buenas practicas" de manipulación de puertos I/O.

comenzaremos con DDRx:

Como ya sabemos DDRx nos permite configurar la modalidad de funcionamiento de los pines de un micro controlador AVR tanto como de lectura o escritura.

ej:

void setup(){
  DDRD  = 0xC;  (0xC hexadecimal = 12 binario = 00001100;// pin PD2 y PD3 =Output
  }

   Lo que acabamos de hacer es configurar el registro para que los pines 2 y 3  con un numero hexa-decimal y que en binario es igual a 12, esto concuerda con los pines 2  y 3 sean de escritura , pero ¿podemos representar de otra forma esta instrucción? la respuesta es si.

Ejemplos:
  A)- DDRD  = 0xC;//PD2 y PD3 =Output
  
  B)- DDRD  = 00001100;//PD2 y PD3 =Output
  
  C)- DDRD  |= (1<<2) | (1<<3);//PD2 y PD3 =Output

  D)- DDRD  |= 00000100 | 00001000;//PD2 y PD3 =Output

  E)- DDRD  = DDRD | 00000100 | 00001000;//PD2 y PD3 =Output

  F)- DDRD  = 00000000 | 00000100 | 00001000;//PD2 y PD3 =Output
  

    Todas las instrucciones  anteriores tienen el mismo resultado que los pines 2 y 3 sean de escritura, la diferencia esta en la facilidad de lectura  y lo optimizado de cada instrucción, por ende una instrucción   mas optimizada obtiene un mejor resultado en su ejecución, como también que  la instrucción sea mas clara y precisa, la que cumple con todo esto es la instrucción  C debido a que sabemos de forma clara que los pines 2 y 3 serán de salida en el puerto D, pero ¿porque?.

 C)- DDRD  |= (1<<2) | (1<<3);

   de ante mano sabemos que en 1<<2  el bit numero nº 2  equivalente a  PD2 sera de salida, como también el bit  nº 3, PD3, 1<<3 sera salida,  ademas 1<<2 es equivalente a 00000100 y 1<<3 es equivalente a 00001000, por lo tanto la instrucción C se descompone en:

DDRD  |= 00000100 | 00001000;

también sabemos que  el operando " |= " es el equivalente de un auto "OR"

DDRD |= x  ; es igual a  DDRD = DDRD | x ; lo que nos deja:

DDRD  = DDRD | 00000100 | 00001000;

que es igual a:

F)- DDRD  = 00000000 | 00000100 | 00001000;

en resumen la instrucción C es la mas precisa, clara de entender y de manipular.

todo lo anterior también es aplicable a los registros PORTx  y PINx. vamos a poner un ejemplo en código Arduino con instrucciones de Arduino y  avr.

ARDUINO
void setup()
{
 pinMode(2,OUTPUT);
}
void loop()
{
  digitalWrite(2,HIGH);
  delay(500);
  digitalWrite(2,LOW);
  delay(500);
}


en Avr en Arduino:

void setup()
{
 DDRD |= (1<<2); // bit nº2 equivalente a PD2 = 1, salida.
}
void loop()
{
  PORTD |=  (1<<2);// 00000100;2, HIGH 
  delay(500);
  PORTD &= ~(1<<2);// 00000000;2, LOW
  delay(500);
}

    En el código anterior en la sección "setup" podemos observar al registro  DDRD configurando el pin PD2 como salida, DDRD |= (1<<2);,lo interesante aparece a la hora de manipular el pin D2, ocupamos la siguiente instrucción para escribir un 1 o HIGH, PORTD |= (1<<2); sabemos que la operación 1<<2 es igual a 0010 y provocara en el puerto que el pin nº 2 pase de 0 a 1, de LOW a HIGH, ahora ¿como podemos invertir el proceso?. si miramos con atención la instrucción que envía al pin nº 2 a un estado LOW.

Si sabemos que en el estado anterior el puerto D fue configurado como, PORTD = 00000100;, la siguiente instrucción PORTD &= ~(1<<2); tendrá  este efecto en el registro.

void loop(){
PORTD |= (1<<2);

portd =  00000000;
  (or) + 00000100;// sumamos al registro anterior (1<<2)
       = 00000100;//resultado pin 2 esta en estado HIGH

delay(500);

PORTD &=  ~(1<<2);//= PORTD &= (0<<2);
            //multiplicamos al registro la negación de (1<<2) => ~(1<<2) = 0<<2;  
portd =  00000100;
  (and)* 00000000;// and al registro anterior con ~(1<<2). ~(1<<2) = (0<<2);
       = 00000000;//obtenemos del And que el pin 2 vuelve a un estado LOW 


delay(500);
}

Podemos observar claramente como en el código anterior se produce un HIGH en el pin dos por la instrucción OR que suma en forma binaria un 1 al registro en la posición nº2, y por ende el estado lógico cambia de 0 a 1, a continuación la instrucción que viene multiplica un cero al uno anteriormente configurado en PORTD por ende al realizar el  and (&) entre 1 y 0 ( 1 & 0) la resultante sera un cero y el pin pasara de tener un valor HIGH  a uno LOW.

Ahora vamos a revisar que sucede en el caso contrario, cuando quiero leer el estado lógico de un pin del Puerto D.

En el caso que quisiéramos ver el estado lógico del pin 7 del puerto D , (pin digital 7 en Arduino).


Arduino

boolean estadoPin;

void setup()
{
 Serial.begin(9600);
 pinMode(7,INPUT);
}
void loop()
{
  estadoPin = digitalRead(7);      
  Serial.println(estadoPin,BIN);
  delay(500);
}

Avr en Arduino
boolean estadoPin;

void setup()
{
 Serial.begin(9600);
 DDRD &= (0<<7); 
}
void loop()
{
  estadoPin = PIND7;
Serial.println(estadoPin,BIN);       
  delay(500);

}



   En el código anterior podemos ver las pequeñas diferencias entre las instrucciones en Arduino como en manipulación de registros, para obtener el estado actual de un determinado pin, ademas podemos apreciar las semejanzas entre ambos códigos a la hora de efectuarlo, se logra apreciar a simple vista que podemos preguntar con el registro PINx por un determinado pin de x puerto, "PIND7", pero las preguntas surgen a la hora de como puedo censar ese pin en un if o compararlo con el estado de otro pin.

los siguientes códigos son de ejemplo para IDE en Arduino:




detectar si pin 7 es = 1; 
void setup()
{
 Serial.begin(9600);
 pinMode(7,INPUT);
}
void loop()
{
  byte estadoPin = digitalRead(7);
   if(estadoPin == HIGH){
    Serial.println( "pin 7 = 1" );   
    delay(500);
   }
}
detectar si pin 7 es = 1;
void setup()
{
 Serial.begin(9600);
 DDRD &= (0<<7);
}
void loop()
{
    if(PIND & (1<<7)){
    Serial.println("pin 7 = 1");    
    delay(500);
   }
}

En ambos códigos la función principal es detectar el estado actual del pin PD7, en Arduino lo hacemos consultando a la instrucción "digitalRead(nº pin)" la cual nos retorna el estado del pin.
en el proceso mediante manipulación de registros la instrucción if(PIND & (1<<7)) compara el estado del puerto D "PIND" y lo multiplica por el dato (1<<7), el cual es equivalente a 01000000, si la operación and "&"  arroja un 1 el estado actual del pin PD7 es 1.

IF(PIND * 1<<7); = IF(01000000 & 01000000) = 01000000; SE CUMPLE
IF(PIND * 1<<7); = IF(00000000 & 01000000) = 00000000; NO SE CUMPLE



detectar si los pines 6 y 7 están en 1
void setup()
{
 Serial.begin(9600);
 pinMode(7,INPUT);
 pinMode(6,INPUT);
}
void loop()
{
 byte Pin6 = digitalRead(6);
 byte Pin7 = digitalRead(7);

   if(pin6&pin7 == HIGH){
    Serial.println("pin 6 y 7 = 1");
    delay(500);
   }
}
detectar si los pines 6 y 7 están en 1

void setup()
{
 Serial.begin(9600);
 DDRD &= (0<<7) | (0<<6);
}
void loop()
{
    if((PIND&((1<<7)|(1<<6)))){
    Serial.println("pin 6 y 7 = 1" );
    delay(500);
   }
}
En el siguiente caso se consulta cuando dos pines son de entrada y se envía una frase por puerto Serial solo cuando estos dos pines estén leyendo un valor logico 1, o 5v, en Arduino usamos nuevamente digitalRead() pero guardando el estado en dos variables, y estas son multiplicadas con un & y comparadas con el valor HIGH. mediante registros podemos obtener el mismo resultado sumando (1<<7)|(1<<6) y luego compararlo mediante un & con el estado actual del puerto PIND&.

if((PIND&((1<<7)|(1<<6)))) = 01100000 & (01000000 | 00100000); se cumple.
if((PIND&((1<<7)|(1<<6)))) = 01000000 & (00000000 | 00100000);  no se cumple.
if((PIND&((1<<7)|(1<<6)))) = 01000000 & (01000000 | 00000000);  no se cumple.
if((PIND&((1<<7)|(1<<6)))) = 00000000 & (00000000 | 00000000);  no se cumple.


encender y apagar pines 2,3,4,5     
void setup()
{
 pinMode(2,OUTPUT);
 pinMode(3,OUTPUT);
 pinMode(4,OUTPUT);
 pinMode(5,OUTPUT);
}
void loop()
{
  digitalWrite(2,HIGH);
  digitalWrite(3,HIGH);
  digitalWrite(4,HIGH);
  digitalWrite(5,HIGH);
  delay(500);

  digitalWrite(2,LOW);
  digitalWrite(3,LOW);
  digitalWrite(4,LOW);
  digitalWrite(5,LOW);
  delay(500);
}

encender y apagar pines 2,3,4,5


void setup()
{
 DDRD |= (1<<2)|(1<<3)|(1<<4)|(1<<5); 
}
void loop()
{
 PORTD |=  (1<<2)|(1<<3)|(1<<4)|(1<<5) ;
 delay(500);

 PORTD &=  (0<<2)&(0<<3)&(0<<4)&(0<<5);
 delay(500);
}

 Y para el final un simple código de encendido y apagado de pines en gran cantidad, usando digitalWrite(),  y la manipulación de puertos.

Bueno esperando que esta pequeña incursión mía en los registros GPI/O sea útil para quien se encuentre con las mismas dudas, o quiera indagar al respecto. vienen mas post respecto a diversos temas comunes de avr gcc, como lecturas análogas, puertos seriales y Pwm , se despide atte Ravc_cs.

referencias y agradecimientos:

El Blog de suhasm //ejemplos muy fáciles de seguir respecto a los registros GPI/O
la traduccion de  takashi de mikrocontroller.net/.// datos muy útiles y una gran biblioteca de datos respecto a avr-gcc