Mapear puertos en Arduino es una técnica muy utilizada en programación para facilitar la generación posterior de código.
En algunas situaciones requerimos la posibilidad de mapear una cierta cantidad de bits en forma directa a los puertos de Arduino o de un microcontrolador en general, por ejemplo modificar el estado de una cierta cantidad de salidas digitales a partir de algún método, que nos permita «enlazar» ( lo que llamo mapear) un bit de una variable de código con un bit físico de los puertos.
Supongamos que necesitamos controlar 12 pines digitales de salida de nuestro Arduino , para lo cual ya conocemos que deberíamos utilizar digitalWrite(pin, LOW/HIGH) para cada uno de los pines de salida.
Arduino simboliza los pines digitales desde el 0 al 13 , lo que nos facilita la construcción de Sketches “hardcodeando” el código. Hardcodear significa hacer referencia en el código en forma directa a un pin físico específico de nuestro Hardware. Cuando hacemos digitalWrite(3,HIGH) estamos hardcodeando, es decir refiriéndonos al pin físico 3 de nuestro Arduino Uno y mas precisamente el bit PD3 del PORTD.
Mapeo de los pines en el Atmega328
El micro Atmega328 o 168 que utiliza Arduino poseen los siguientes puertos con la consiguiente simbología en lo que respecta al Hardware de este chip:
Ciertos aspectos de lo que se verá en esta entrega ya fue tratado en otra entrada dentro de este mismo sitio. Para más información consultar: “El registro PORT (puerto) Arduino”
Si consideramos solo los pines digitales del 0 al 13, podemos extraer la siguiente tabla de relaciones entre los pines digitales de Arduino y su mapeo en los PORTS físicos del Atmega328:
Donde PDx y PBx son los puertos D y B del Atmega que indican los bits asociados a cada Puerto. Por ejemplo PD0 es el bit0 del PuertoD.
Planteo
Imaginen ahora que poseo conectados 12 Leds a cada pin con excepción de los pines 0 y 1 que se utilizan para el Serial del monitor de Arduino, y los dispongo de la siguiente manera:
Sabemos que en Arduino el mapeo previo de los pines es el siguiente:
dp0: rx, dp1: tx, dp2 PD2 , dp3 PD3, dp4:PD4, dp5 PD5, dp6 PD6, dp7 PD7, dp8 PB0, dp9 PB1, dp10 PB2, dp11 PB3
dp12 PB4, dp13(led13 Arduino) PB5.
Hay un libro muy interesante que trata el Hardware interno del Atmega pero muy entendible donde trata además el acceso a este Hardware con el lenguaje del skecth de Arduino lo que lo hace muy útil. Introduction to Embedded Systems: Using ANSI C and the Arduino Development Environment (Synthesis Lectures on Digital Circuits and Systems).
Se me ocurrieron Leds para facilitar la explicación, pero podrían ser cualquier tipo de otros dispositivos digitales.
Mi deseo es crear una función que podamos utilizar en nuestros sketches y que me permita modificar el estado de los 12 Leds sin tener que recurrir a las ya conocidas digitalWrite().
La función o Método de mapeo
Mi función podría ser algo como esto escribirLed(0b1111111111110000);
Como podrán notar la indicación “0b” indica que es un numero en formato binario y cada bit representa el estado de cada led, en este caso todos ON=1.
El formato del método es el siguiente:
escribirLed(0bLed1 Led2 Led3 Led4 Led5 Led6 Led7 Led8 Led9 Led10 Led11 Led12); donde Ledx representa el Número led cuyo valor puede ser “1” ON o “0” OFF
Implementación
Como primera medida el valor a pasar es un tipo de dato de 2 Bytes, entonces en Arduino podríamos utilizar el unsigned int , de manera que el prototipo de mi método sería:
1 2 3 |
void escribirLed( unsigned int leds) { ………………. } |
El método deberá ir tomando cada bit de la posición del valor pasado como variable Leds a la función y mapear directamente su valor al Port que corresponda y en la posición que corresponda. Por ejemplo si Leds =0b100………….., el Led1 está en ON y por lo tanto dicho “1” deberá ser escrito en el PortD en el tercer bit = PD2.
Si observamos el PortD es un Byte (8bits), al igual que el PortB, aunque este último solo usa 5 bits. En la siguiente tabla vamos a representar la variable leds (16 bits), el PORTD(8bits), PORTB(5 bits) y una variable auxiliar (aux) tipo byte de 8 bits
Se puede apreciar el MSB o byte más significativo que es la parte alta o Nible Alto de la variable leds y el LSB o byte menos significativo o Nible Bajo de la variable leds. El MSB de los PortD/B no existen.
¿Cómo hacemos entonces por ejemplo para llevar el BIT15 de la variable leds que representa el LED1 , al BIT2=PD2 del PuertoD ?,….. en eso se resume lo que va a hacer nuestro método, no solo con LED1 sino con el resto. Para esto hay que ir analizando pin a pin.
Vamos a utilizar las funciones que provee implementadas en el compilador de Arduino y que son las de desplazamiento de bits.
Crearemos una variable local, dentro del método, evitando el uso de Globales y optimizar memoria, ya que las Locales se destruirán al no ejecutarse el método y se crearán temporalmente dentro de nuestro método de mapear.
Vamos a explicar para el caso del LED1. Creamos la variable byte Led1; y otra variable auxiliar del tipo byte aux,.
aux= leds>>8; es decir que en aux ahora se guardaran los bits resultado de desplazar a la derecha 8 posiciones a la variable leds, esto es el BMSB ( Byte más significativo de leds) y que contiene desde el LED1 al LED8.
Si observamos ahora, LED1 en aux y LED1 en PortD, hay 5 espacios a la derecha, de esta manera hacemos:
led1= (aux >>5) & 0b00000100; logrando que led1 ya creada, tenga el estado solo del LED1, esto es 0 o 1. La operación AND con 0b00001000, permite descartar y asegurarse que solo el bit2 de aux tenga el valor del Led1. La operación AND se realiza bit a bit, de manera que 1.0=0 y 1.1=0.
De la misma manera observamos que :
- led2=(aux>>3) & 0b00001000; desplazo 3 a la derecha
- led3=(aux>>1) & 0b00010000; desplazo 1 a la derecha
- led4= (aux<<1) & 0b00100000; desplazo 1 a la izquierda
- led5=(aux <<3) & 0b01000000; desplazo 3 a la izquierda
- led6=(aux<<5) & 0b10000000; 5 a la izquierda
Hasta aquí tendremos logrado casi el mapeo en el PortD , cada variable ledx posee el estado de cada led, solo resta mapearlos en el puerto. Para esto creamos otra variable byte puertoD=0; y hacemos lo siguiente
puertoD=puertoD | led1|led2|led3|led4|led5|led6; puertoD se armará realizando las sucesivas operaciones OR bit a bit . Para ilustrar el caso supongamos que cada variable tenga seteado su bit
led1=00000100, led2=00001000,led3=00010000,led4=00100000,led5=01000000 y led6=10000000
Haciendo OR bit a bit portD=0B111111xx , el xx que queda son los bit de rx y tx y que no debemos afectar su valor. Antes de escribir directamente al PortD debemos cuidar de no afectar el valor de rx, tx. Para esto creamos una variable byte d; que va a leer los bits del PortD , de la siguiente manera:
- d= PIND; esta es una macro con acceso directo solo a la lectura del PortD. En el tutorial mencionado antes “El registro PORT (puerto) Arduino” , se describe muy bien. Para resumir, cada puerto del Arduino B,C y D, existen macros para su acceso directo de la forma:
- DDRx lectura escritura del registro de dirección (entrada o salida digital) del Port x
- PORTx lectura escritura sobre el puerto x
- PINx lectura de los pines del puerto x.
En mi caso utilicé PINx para la lectura del PortD y así ver en qué estado están los pines rx y tx.
Lo que hacemos ahora finalmente es
d=d&0b00000011; nos interesa rescatar solo los pines 0 y 1, su estado, AND 00000011
puertoD=puertoD|d; Hacemos OR bit a bit entre puertoD y d y guardamos en la misma variable , ahora está completo con el estado de los Leds y el estado real de rx/tx sin afectar
PORTD = puertoD; / escritura final al puerto D.
Resta ahora poder mapear los leds restantes del 7 al 12 sobre el PortB de PB0 a PB5.
Si volvemos a ver la tabla con la variable leds, PortD y PortB, y la variable aux que teníamos como resultado de leds>>8 , el LED7 deberá ir al PortB PB0 y el LED8 al PB1.
Para lograr esto hacemos:
- led7=(aux>>1) & 0b00000001;
- led8=(aux<<1) & 0b00000010;
Con lo cual ya nos quedan formateadas las variables led7 y 8 con la posición, al igual que los leds del 1 al 6 ya analizados.
El LED9 ya no lo podemos recuperar de la variable aux ya que posee 8 bits y el LED9 está en una novena posición que no existe, por este motivo hacemos:
Aux=leds; y aquí viene lo divertido, al igualar un byte a un int , la información se recorta ya que aux solo tomará el LSB de la variable leds, y de esta manera el nuevo aux queda como sigue:
Luego.
- led9=(aux>>5) & 0b00000100;
- led10=(aux>>3) & 0b00001000;
- led11=(aux>>1) & 0b00010000;
- led12=(aux<<1) & 0b00100000;
Creamos otra variable byte puertoD y la armamos con la OR entre lo leds que faltan.
puertoB=puertoB|led7|led8|led9|led10|led11|led12;
y finalmente enviamos la información al PuertoB
PORTB = puertoB & 0b00111111; con la máscara de esos 6 bits que nos interesan
Con esta explicación ya podrán seguir el código completo, para lo cual les dejo el mismo para este del método. Este método es muy útil, por ejemplo lo estoy usando para hacer un cubo de leds de 3×3.
Esta es una manera de hacerlo, puesto que en lugar de desplazar previamente aux , 8 posiciones a la derecha, quizás otro método podría ser lograr desplazamientos acordes al bit a mapear, por ejemplo si tomo LED1 ,y desplazo 13 posiciones a la derecha la variable leds y lo guardo directamente en la variable led1 ya estaríamos en la posición:
- led1=(leds>>13) & 0b00000100, ya estaría.
- led2=(leds>>11) & 0b00001000; led3=(leds>>9) & 0b00010000; led4=(leds>>7) & 0b00100000;
- led5=(leds>>5) & 0b01000000; led6=(leds>>3) & 0b10000000; led7=(leds>>9) & 0b00000001;
- led8=(leds>>7) & 0b00000010; led9=(leds>>5) & 0b00000100; led10=(leds>>3) & 0b00001000;
- led11=(leds>>1) & 0b00010000 y led12=(leds<<1) & 0b00100000.
Como ven este otro enfoque es mas sintético e incluso se podría optimizar usando vectores ya que los desplazamientos son impares, entonces se podría usar una variable índice que incrementara en forma impar, pero esa es otra historia. La idea aunque parece rebuscada fue la mejor manera de explicar que encontré para mostrar el tema de las posiciones en variables y puertos de diferente tamaño.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
void escribirLed2( unsigned int leds) { /* Esta funcion mapea los pines del 2 al 10 que son Led 1 al 10 en los puertos PORTD y PORTD PORTD =dp7,dp6,dp5,dp4,dp3,dp2,tx,rx =L6, L5, L4,L3, L2, L1, x, x PORTB = x,x,dp13, dp12, dp11 , dp10,dp9, dp8 = x,x,L12, L11, L10, L9 , L8 , L7 El parametro a pasar de lafuncion es el estado de los 9 bits 0bL1,L2,L3,L4,L5,L6,L7,L8,L9,L10,L11,L12,x,x,x,x es in int 16 bits */ byte led1,led2,led3,led4,led5,led6,led7,led8,led9,led10,led11,led12; // variables que van a mapear el estado en los puertos D y B byte puertoD=0; byte puertoB=0; byte aux; byte d; // la lectura del byte del puerto D aux=leds>>8; // BLSB del int tiene la info de los leds 2 al 8 led1= (aux >>5) & 0b00000100; led2=(aux>>3) & 0b00001000; led3=(aux>>1) & 0b00010000; led4= (aux<<1) & 0b00100000; led5=(aux <<3) & 0b01000000; led6=(aux<<5) & 0b10000000; //Serial.println(aux,BIN); led7=(aux>>1) & 0b00000001; led8=(aux<<1) & 0b00000010; // hasta aqui se mapearon del led 1 al 8 donde se desplazo el MSB de leds al LSB de aux // nuevo aux=leds; // aux tiene el LSB de leds : L9,L10,L11,L12,x,x,x,x led9=(aux>>5) & 0b00000100; led10=(aux>>3) & 0b00001000; led11=(aux>>1) & 0b00010000; led12=(aux<<1) & 0b00100000; puertoD=puertoD | led1|led2|led3|led4|led5|led6; puertoB=puertoB|led7|led8|led9|led10|led11|led12; d=PIND; // d lee el puertoD esto es para no afectar la escritura de rx/tx bit 1 y 0 d=d&0b00000011; puertoD=puertoD|d; PORTD = puertoD; // escritura al puerto D PORTB = puertoB & 0b00111111; |