La mayoría de los pines de los microcontroladores son multipropósito, es decir, en función de su configuración se comportan de una forma u otra, tal y como se muestra en la figura.
El ATmega328p como cualquier otro microcontrolador tiene registros, algunos de estos registros están conectados con los puertos de entrada/salida, cada puerto tiene un nombre específico y sus registros asociados, de hecho, el 328p tiene los puertos B, C y D, y cada puerto 8 pines de la MCU conectados. En función del encapsulado puede haber una restricción en el número de pines. Por ejemplo en el paquete de 28 pines PDIP.
Cada pin puede tener múltiples funciones, como la generación de PWM, o las capacidades de ADC, los pines 6 y 7 del PORTB son los pines de entrada para el oscilador de cristal, y el pin 6 del PORTC corresponde al botón de reinicio. En esta imagen se puede ver todas las funciones alternativas que cada pin puede tener.
NOTA: Esta nomenclatura del los GPIO luego el IDE de Arduino se ha encargado de nombrarlos de una forma más sencilla y consecutiva, más fácil de entender.
Por ejemplo en el Wemos D1 basado en el ESP8266, internamente los GPIO tiene una nomenclatura que puede llevar a error, pero luego el soporte para estas placas en el IDE de Arduino hace que coincida con la nomenclatura de Arduino y con lo etiquetado en la placa.
Entradas y Salidas Digitales Arduino a Fondo
Una señal digital es un tipo de señal generada por algún tipo de fenómeno electromagnético en que cada signo que codifica el contenido de la misma puede ser analizado en término de algunas magnitudes que representan valores discretos, en lugar de valores dentro de un cierto rango. Por ejemplo, el interruptor de la luz sólo puede tomar dos valores o estados: abierto o cerrado, o la misma lámpara: encendida o apagada.
Más información:
Los sistemas digitales, como por ejemplo un microcontrolador, usan la lógica de dos estados representados por dos niveles de tensión eléctrica, uno alto, H y otro bajo, L (de High y Low, respectivamente, en inglés). Por abstracción, dichos estados se sustituyen por ceros y unos, lo que facilita la aplicación de la lógica y la aritmética binaria. Si el nivel alto se representa por 1 y el bajo por 0, se habla de lógica positiva y en caso contrario de lógica negativa.
Cabe mencionar que, además de los niveles, en una señal digital están las transiciones de alto a bajo y de bajo a alto, denominadas flanco de bajada y de subida, respectivamente. En una señal digital, se denomina flanco a la transición del nivel bajo al alto (flanco de subida) o del nivel alto al bajo (flanco de bajada).
Para interactuar con los pines digitales de Arduino ya conocemos las funciones que nos ofrece Arduino en https://www.arduino.cc/en/Tutorial/DigitalPins como digitalRead() y digitalWrite().
Pero a bajo nivel estas funciones están manejando registros de los puestos B, C y D. Los estados y comportamiento de cada puerto y sus pines se maneja mediante tres registros. Hay un registro dedicado para cada puerto que define si cada pin es una entrada o una salida, que es el registro de DDRX, donde x es la letra del puerto que queremos configurar, en el caso de la Arduino hay DDRB, DDRC y DDRD. Como toda variable lógica, cada bit en los registros DDRX puede ser 1 ó 0, poner un bit específico de DDRX a 1 configura el pin como salida y ponerla a 0 configura el pin como una entrada.
Los pines usados en la placa Arduino poseen tres puertos en el caso de ATmega328p (Arduino Uno):
- B (pines digitales del 8 al 13)
- C (entradas analógicas)
- D (pines digitales del 0 al 7)
El Arduino Mega presenta varios puertos B,C,D,E,F, etc.
Cada puerto es controlado por tres registros, los cuales también están definidos como variables en el lenguaje del Arduino.
- El registro DDR, determina si el pin es una entrada o una salida (1 salida, 0 entrada).
- El registro PORT controla si el pin está en nivel alto (1) o en nivel bajo (0).
- El registro PIN permite leer el estado de un pin. (solo lectura)
Dentro del Microcontrolador ATmega328P esta es la relación de los puestos con los pines:
Cada bit de estos registros corresponden con un solo pin; por ejemplo el bit menos significativo de los registros DDRB, PORTB, y PINB hace referencia al pin PB0 (pin digital 8)
Dentro de la avr-libc tenemos un fichero de definición de los registros para cada microcontrolador. Estos fichero se pueden encontrar en la ruta: C:\Program Files (x86)\Arduino\hardware\tools\avr\avr\include\avr
Para ATmega328p el fichero iom328p.h es donde se encuentran las definiciones para el microcontrolador. Ver que las direcciones de los registros van desplazados 0x20 posiciones de memoria por los fuses que son 4 bytes de memoria permanente llamados fuses que determinan cómo se va a comportar el microcontrolador, si tiene bootloader, a que velocidad y voltaje va a funcionar, etc… .
El fichero <avr/sfr_defs.h> está incluido en todos los ficheros <avr/ioxxxx.h> que usan unas macros definidas en sfr_defs.h que hacen que los special function registers parezcan variables de C a las que podemos llamar para obtener su valor:
#define PORTA _SFR_IO8(0x02) #define EEAR _SFR_IO16(0x21) #define UDR0 _SFR_MEM8(0xC6) #define TCNT3 _SFR_MEM16(0x94) #define CANIDT _SFR_MEM32(0xF0)
Por ejemplo, las limitaciones eléctricas en el microcontrolador son por puerto y por pin:
Práctica: Registros PORT Arduino
Un ejemplo que configura pines 0,1,2,3 como entradas digitales y los pines 4,5,6,7 como salidas digitales, se hace mediante el comando: DDRD = 0b11110000;
Si queremos poner todos los pines como salidas: DDRD = 0b11111111;
Al utilizar Registros DDR tenemos la ventaja de que con solo una instrucción podemos declarar el pin como entrada o salida, sin embargo con pinMode() necesitaríamos 8 instrucciones.
La relación entre los registros B, C y D y los pines es:
Leer los registros al poner varios pines como INPUT, INPUT_PULLUP y OUTPUT.
Probar con este código:
void setup() { //PORTD maps to Arduino digital pins 0 to 7 pinMode(2, INPUT_PULLUP); pinMode(3, OUTPUT); digitalWrite(3, HIGH); pinMode(4, INPUT); pinMode(5, OUTPUT); digitalWrite(5, LOW); pinMode(6, INPUT_PULLUP); pinMode(7, OUTPUT); digitalWrite(7, HIGH); Serial.begin(9600); } void loop() { Serial.println("DDRD - The Port D Data Direction Register. DDRX, 1=OUTPUT, 0=INPUT"); Serial.println(DDRD, DEC); Serial.println(DDRD, HEX); Serial.println(DDRD, BIN); Serial.println("PIND - The Port D Input Pins Register. Lectura INPUT"); Serial.println(PIND, DEC); Serial.println(PIND, HEX); Serial.println(PIND, BIN); Serial.println("PORTD - The Port D Data Register. Escritura OUTPUT"); Serial.println(PORTD, DEC); Serial.println(PORTD, HEX); Serial.println(PORTD, BIN); delay(10000); }
Poner los pines de input (2, 4 y 6) a 5V y GND y ver como cambia el contenido del registro PIN.
Solución Ejercicio07: https://github.com/jecrespo/aprendiendoarduino-Curso_Arduino_Avanzado_2017/tree/master/Ejercicio07-Registos_PORT_simple
Manejo Registros de Pines Arduino
Par poder manejar registros disponemos de unas funciones especiales para registros.
Operadores Bit a Bit:
- & (bitwise and)
- | (bitwise or)
- ^ (bitwise xor)
- ~ (bitwise not)
- << (bitshift left) (IMPORTANTE)
- >> (bitshift right)
Tutorial de matemáticas con bits: http://playground.arduino.cc/Code/BitMath
Referencia de <avr/sfr_defs.h> Special Function Registers:
- http://www.nongnu.org/avr-libc/user-manual/group__avr__sfr.html
- http://www.nongnu.org/avr-libc/user-manual/group__avr__sfr__notes.html
- http://www.atmel.com/webdoc/AVRLibcReferenceManual/group__avr__sfr__notes.html
Manipulación de puertos: https://www.arduino.cc/en/Reference/PortManipulation
Tutorial de Bit Math: http://playground.arduino.cc/Code/BitMath
Escribir en un pin
Ya podemos decir al Atmega cómo serán utilizados sus pines, pero queremos saber cómo leer y escribir datos en dichos pines, de modo que para escribir datos en un determinado puerto, se utiliza el registro PORTx, éste es fácil de recordar, donde x es el nombre del puerto, y después de la configuración de un pin como salida es sólo una cuestión de poner 0 o 1 en el registro PORTx para controlar que el pin de este en estado alta o baja.
Un ejemplo sería:
DDRD = 0b11111111; // Todos los pines de PORTD son salidas. PORTD = 0b11111111; // Todos los pines de PORTD están en estado alto. DDRD = 0b11111111; // Todos los pines de PORTD son salidas. PORTD = 0b00000000; // Todos los pines de PORTD están estado bajo.
Se debe tener cuidado cuando se utiliza PORTD y el puerto serie porque los pines 0 y 1 del PORTD son los utilizados por la USART y si se pone estos dos como entradas o salidas, la USART será incapaz de leer o escribir datos en los pines. Este es un ejemplo del cuidado que se debe tener al usar esta programación en lugar de la capa de programación que nos ofrece Arduino.
Leer un Pin
Para leer en el pin y poder leer los datos de los sensores o cuando se pulsa un botón de un pin digital configurado como entrada, vamos a utilizar un tercer registro llamado PINX, donde de nuevo x es el nombre del puerto donde se encuentra el pin, así que primero con DDRX decimos al microcontrolador que queremos algunos pines como entradas digitales, y luego usando PINX leemos sus valores
Ejemplo:
DDRD = 0b00000000; // Todos los pines del PORTD son entradas char my_var = 0; // variable para guardar la información leída en PORTD my_var = PIND; // Lee PORTD y pone la información en la variable
Es casi tan fácil como puede ser usar el digitalWrite o digitalRead de Arduino, pero con acceso directo al puerto se puede ahorrar espacio en el la memoria flash y también puede ganar mucha velocidad, porque las funciones Arduino puede tomar más de 40 ciclos de reloj para leer o escribir un solo bit en un puerto, además para leer un solo bit el código es bastante complejo con un montón de líneas que ocupan por lo menos unos 40 bytes, que podría ser un pequeño ahorro en flash y es un gran paso para acelerar cualquier programa.
No es normal que se necesite leer o escribir en un puerto completo en cada momento, por ejemplo, si queremos encender un LED, o leer un botón sólo tendrá que utilizar un pin, y escribir todos los bits uno a uno cada vez que queremos cambiar un valor en un puerto, es una tarea aburrida, pero la librería C de AVR tiene algunas pocas palabras definidas como Px(0..7), donde x es de nuevo el puerto que desea utilizar y 0..7 es el valor del pin individual de dicho puerto, por lo que para iluminar un LED debemos hacer algo como esto:
DDRD = (1<<PD2); // Configura el pin 2 de PORTD como salida. PORTD = (1<<PD2); // El pin 2 de PORTD tiene ahora un valor lógico 1.
La operación << es un operador binario que desplaza hacia la izquierda el número de posiciones especificadas.
Para leer el estado de un botón:
DDRD = 0b11111101; //Configura pin 1 de PORTD como entrada y el resto salida char my_var = 0; //Variable para guardar la información leída en PORTD my_var = (PIND & (1<<PD1)); /* Lee el pin 1 de PORTD y lo coloca en la variable.*/
Explicación de la operación binaria bit a bit: https://www.arduino.cc/en/Reference/Bitshift
También se puede utilizar la macro Px0..7 varias veces en una misma instrucción, por ejemplo, en este código, se ejecutará algo de código sólo si al menos uno de los botones está pulsado:
DDRD = 0b11111100; // Los pines 0 y 1 de PORTD son entradas, y el resto salidas. if (PIND & ((1<<PD0) | (1<<PD1))) { /* Algún código dentro del if() que se ejecutará solo si al menos uno de los dos botones se encuentra activado. */ }
Mediante los registros también podemos controlar las resistencias internas de pullup. Cuando hay un botón que puede tener dos estados, uno es desconectado, y cuando se presiona hará una conexión entre los pines del microcontrolador y permite por ejemplo, conectarse a masa, pero cuando se desconecta, no hay nada que fuerce un valor estable en el pin de entrada, y el pin puede leer 1 ó 0 ya que el pin es muy sensible al ruido electromagnético, como una pequeña antena. Se puede resolver este problema de dos maneras similares, una es para conectar una resistencia de 10 Kohms o más entre el Vcc (+5 v) y el pin de entrada, o usar los pull-ups del microcontrolador que tienen integrados, también hace más simples los circuitos.
Para habilitar las resistencias pullup tenemos que hacer algo que puede resultar un poco extraño, no existe un registro dedicado para activar o desactivar el pull-ups, estos son activados o desactivados escribiendo 1 o 0 respectivamente en el registro PORTx cuando el registro DDRX se configuran como entradas. Ejemplo:
DDRD = 0b00000000; // Todos los pines de PORTD son entradas. PORTD = 0b00001111; // Habilito las Pull-ups de los pines 0,1,2 y 3 char my_var = 0; // Creo una variable para guardar la información leída en PORTD. my_var = PIND; // Leo PORTD y colocó la información en la variable.
Si se ejecuta este código, sin tener nada conectado a PORTD, los cuatro bits más altos de la variable my_var puede ser 0 ó 1, cualquier combinación posible de ellos porque son flotantes (actúan como pequeñas antenas), pero los cuatro bits más bajos leerá todos un 1 debido a que el pull-ups imponen una señal de 5V débil que se lee como un valor lógico 1.
En un sentido básico esto es todo lo que se necesita saber para dominar la manipulación directa de los puertos. La manipulación de bits enseña cosas más ingeniosas como las máscaras de bits, las operaciones AND, OR, NOT y XOR y cómo configurar y limpiar los bits en un registro y algunos buenos trucos con las operaciones de desplazamiento derecho e izquierdo, esta técnica puede acelerar un sketch y son muy útiles cuando se utilizan los puertos digitales.
Ventajas y Desventajas que nos ofrece al utilizar los registros:
- Desventajas:
- El código es mucho más difícil de depurar y mantener, y es mucho más difícil de entender. Solo lleva algunos microsegundos al procesador ejecutar código, pero podría llevar horas descubrir por qué no funciona y arreglarlo.
- Es mucho más fácil causar mal funcionamiento no intencionado usando el acceso directo a un puerto. Con DDRD = B11111110, el pin 0 se debe dejar como una entrada. El pin 0 la línea de recepción (RX) en el puerto serial. Podría ser muy fácil causar el puerto serial deje de funcionar por cambiar el pin 0 a una salida.
- Ventajas:
- Se puede cambiar los pines de estado muy rápido, en fracciones de microsegundos. Las funciones digitalRead() y digitalWrite() se componen cada una de ellas de cerca de una docena de líneas de código, lo cual se convierte en unas cuantas instrucciones máquina.
- Cada instrucción máquina necesita un ciclo de reloj a 16MHz, lo cual puede sumar mucho tiempo en aplicaciones muy dependientes del tiempo. El Registro PORT (Puerto) puede hacer el mismo trabajo en muchos menos ciclos de trabajo.
- Algunas veces necesitamos configurar muchos pines exactamente al mismo tiempo. Por lo que usar las funciones digitalWrite (10,HIGH), seguida de la función digitalWrite (11,HIGH), causará que el pin 10 se ponga en nivel alto varios microsegundos después que el pin 11, lo cual puede confundir circuitos digitales conectados al Arduino, cuyo funcionamiento dependa del tiempo preciso del cambio de esos bits.
- Si te estás quedando sin memoria, se pueden usar estos trucos para hacer que tu código sea más pequeño. Usando este método se necesitan muchos menos bytes de código compilado que si se hace un bucle para que se vaya cambiando cada pin uno por uno.
En las librerías podemos ver que usan la manipulación de registros en lugar de las instrucciones que nos ofrece el core de Arduino lo que las hace más rápidas.
En el siguiente enlace de github puede verse el fichero wiring_digital.c que es parte del core de Arduino donde se puede ver el código de las instrucciones pinMode(), digitalWrite() y digitalRead(): https://github.com/arduino/Arduino/blob/master/hardware/arduino/avr/cores/arduino/wiring_digital.c
Y en el fichero pins_arduino.h está la definición de pines: https://github.com/arduino/Arduino/blob/master/hardware/arduino/avr/variants/standard/pins_arduino.h
Más información en:
- https://www.arduino.cc/en/Reference/PortManipulation
- https://hekilledmywire.wordpress.com/2011/02/23/direct-port-manipulation-using-the-digital-ports-tutorial-part-3/
- https://maxembedded.wordpress.com/2011/06/10/port-operations-in-avr/
- http://panamahitek.com/registro-port-puerto/
- http://sistdig.wikidot.com/wiki:puertos
- http://rufianenlared.com/mascaras-arduino/
- http://siatienfalla.altervista.org/atmega328p-port-registers/
- http://courses.cs.washington.edu/courses/csep567/10wi/lectures/Lecture6.pdf (página 44)
- http://wittyrobo.com/mcu328p/c01_p09.html
- https://coolcapengineer.wordpress.com/2012/07/31/avr-configuring-and-using-ports-in-c/
- http://thepiandi.blogspot.com.es/2016/01/programming-atmega328p-registers-from.html
Práctica: Operaciones con Entradas y Salidas Digitales
Leer Pin
- Configurar mediante el registro DDRB el pin 9 como entrada y el resto de pines del PORT B como salida.
- Configurar el pin 9 como INPUT_PULLUP mediante el registro PORTB
- Usando el registro PORTB poner a LOW todos los pines de OUTPUT salvo el pin 10
- Sacar por el monitor serie el estado de los pines del PORTB mediante el registro PINB
Solución Ejercicio08: https://github.com/jecrespo/aprendiendoarduino-Curso_Arduino_Avanzado_2017/tree/master/Ejercicio08-Leer_pin
Velocidad Read
Para demostrar que la operación digitalRead() es lenta hacer un ejercicio que lea 1000, 10000 y 100000 veces el puerto 9 configurado como entrada, mediante digitalRead() y mediante PIND y saque el tiempo que ha costado cada operación.
Solución Ejercicio 09: https://github.com/jecrespo/aprendiendoarduino-Curso_Arduino_Avanzado_2017/tree/master/Ejercicio09-Velocidad_Read
Pingback: Saber Más Avanzado 2017 | Aprendiendo Arduino
Pingback: Saber más Arduino Avanzado 2018 | Aprendiendo Arduino
Creo que el termino Arduino Avanzado en realidad no es más que programar los micros AVR como siempre se venía haciendo, hoy día se envuelve a estos chip en una capa tan alta que hablar de una simple programación a nivel de código maquina lo llamáis Arduino avanzado, cuando en realidad esto era por donde se comenzaba a aprender a programar, esto ha sido así de toda la vida hasta que se empezó a usar los lenguajes de alto nivel para programarlos, en realidad aunque esto facilita mucho, en algunos aspectos también evita conocer realmente afondo a todos los microcontroladores
Me gustaLe gusta a 1 persona
Así lo explico en los cursos, en la universidad estudiamos la programación de microcontroladores, pero gracias a Arduino la programación de MCUs se ha democratizado y cualquiera puede programarlos con lenguajes de alto nivel. Pedagógicamente es mucho mejor empezar a trabajar con lenguajes de alto nivel y luego usar funciones de más bajo nivel. ¿Alguna vez te has preguntado como implementarías una comunicación MQTT a traves de un chip wifi como ESP826 con la programación propia de una MCU? Seguro que te costaría varias decenas de horas trabajando, ahora es sencillo y si quieres saber más no tienes más que leer el código de las librerías, e incluso puedes modificarlo a tu gusto.
Me gustaMe gusta
Creo que no me has entendido yo no pretendo decir que se deba aplicar el bajo nivel en todo, lo que digo es que hay que empezar por ello al estudiar pues si no se hace así no digo que no pueda hacer todo perfectamente en alto nivel, solo digo que será siempre un completo ignorante de lo que ocurre internamente, para usted será una caja negra y en muchas ocasiones en donde prime la velocidad y la precisión siento decirle que el lenguaje de alto nivel no le servirá o no rendirá al 100%, no estoy de acuerdo en que sea mejor empezar antes en alto nivel es un gran error, es como hacer que un niño empiece a escribir pero no sepa leer, de hecho conozco muchos jóvenes que lo que hacen solo lo hace automáticamente por lo aprendido, no lo hacen porque lo hayan entendido les basta algo diferente para estar perdidos, si desde el principio hacen un estudio a bajo nivel no les ocurre jamás, el lenguaje de alto nivel esta creado con el bajo nivel por lo que para entenderlo bien no es posible empezar a entenderlo al revés
Me gustaLe gusta a 1 persona
Está bien tener opiniones diversas. Sería un debate interesante en el foro adecuado…
Me gustaMe gusta