Programacion avanzada
La mayoría de los puertos de los uC son multipropósito, es decir, en función de su configuración se comportan de una forma u otra. El ATmega 328p como cualquier otro uC tiene registros para cada puerto donde define si sera usado como entrada o salida. ATmega 328p tiene 3 bancos o grupos de puertos: PB (D8~D13), PC (A0~A5) y PD (D0~D7), es decir a PB y PC le faltan puertos debido a que no se dispone de pines suficientes al ser ATmega 328p un DIP-28 (en comparación del tradicional DIP-40 de la mayoria de uC).
Banco | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 |
---|---|---|---|---|---|---|---|---|
PB | - | - | D13 | D12 | D11 | D10 | D9 | D8 |
PC | - | - | A5 | A4 | A3 | A2 | A1 | A0 |
PD | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
ATmega 328p tiene 3 registro de 8 bits con los que administra estos 3 bancos:
- DDRx, determina si es entrada (0) o salida (1)
- PORTx, controla si el pin esta en nivel HIGH o LOW. También define Pull-up si es entrada.
- PINx, permite leer estado (solo lectura).
Contenido
Registros
Antes de meterme de lleno a explicarte los registros, quiero que primero veamos para que podría servir este esfuerzo.
Ventajas de usar registros:
- Cada instrucción máquina necesita un ciclo de reloj a 16 MHz. Las funciones digitalRead() y digitalWrite() se componen cada una de ellas de varias instrucciones máquina, lo que puede influir negativamente en aplicaciones muy dependientes del tiempo. El Registro PORTx puede hacer el mismo trabajo en muchos menos ciclos de reloj.
- Si necesitas cambiar de estado muchos pines en lugar de hacer un loop for que tomara mucho tiempo puedes echar mano directamente al registro y establecer varios pines de una sola vez.
- Si estas al limite de memoria Flash, puedes usar este truco para hacer que tu código use menos bytes de programación.
Desventajas de usar registros:
- El código no es fácil de entender para novatos.
- Es mucho mas fácil que cometas errores de difícil depuración. Por ejemplo con DDRD = B11111111, pone todos los pines (D0~D7) como salida, incluso el pin D0 (Rx) lo que causar que el puerto serial deje de funcionar.
Nota: En las librerías es muy recomendable usar la manipulación directa de registros, así se hacen mucho mas rápidas.
Digitales
Bien ahora que ya sabes para que nos meteremos en como manejar estos registros. En principio tenemos dos métodos: masivo es decir todos los puertos de un banco a la vez o puntual es decir un puerto de un banco en particular.
Masivo
Ya sabemos que ATmega 326P maneja 3 bancos (B, C y D) y que cada uno agrupa a un numero de puertos. Por otro lado tenemos 3 registros (DDRx, PORTx y PINx) que podemos manejar.
DDRB = 255; //D8~D13 como salidas. DDRD = B11111111; //D0~D7 como salidas. DDRD = B00000000; //D0~D7 como entradas. DDRB = B00000111; //D8~D10 como salida D11~D13 como entradas. PORTD = B11111111; //D0~D7 en HIGH. PORTD = 0; //D0~D7 en LOW. PORTB = B00000101; //D8+D10 en HIGH, D8, D11~D13 en LOW. byte variable = PIND; //Guarda en una variable los estados de D0~D7 como un byte.
Se debe tener cuidado cuando se usa el PORTD y el puerto serie D0 (Rx) y D1 (Tx) son los usados por la UART y si se pone estos puertos como entradas o salidas, la UART será incapaz de leer o escribir datos en estos puertos. Este es un ejemplo de cuidado que se debe tener al usar esta programación en lugar de la capa de programación que nos ofrece Arduino.
Mediante los registros también podemos controlar las resistencias internas de pull-up que se usan basicamente para no dejar los pines al aire ya que esto genera ruido eléctrico. Se puede resolver este problema de dos maneras: poner una resistencia externa de 10K a Vcc (+5V) o usar los Pull-up internos del uC que producen el mismo efecto y hacen mas simple el circuitos.
Para habilitar las resistencias pull-up tenemos primero que configurar como entrada (0) el puerto mediante registro DDRx y luego escribir un 1 en el registro PORTx.
DDRD = 0; //Configura todos los puertos del banco PORTD (D0~D7) como entradas. PORTD = B00001111; //Habilitar las Pull-ups de los puertos D0~D3.
Es casi tan fácil como usar las funciones digitalRead() y digitalWrite() de Arduino, pero con acceso directo al puerto se puede ahorrar espacio en la memoria flash porque cada operación de leer el estado de un solo puerto ocupa 40 bytes de instrucciones 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.
Puntual
No es normal que se necesite definir, leer o escribir en un banco completo siempre, por ejemplo, si queremos encender un LED tendremos que actual sobre un solo puerto y no sobre todo el banco. Para eso podemos usar la macro Px donde x estará entre 0~7 para definir el puerto en particular. Ver tabla arriba.
DDRD = (1<<PD2); //Configura el D2 como salida. PORTD = (1<<PD4); //Pone D4 en HIGH. byte variable = (PIND & (1<<PD1)); //Lee D1 y almacena su valor en la variable.
También podemos usar la macro Px varias veces en una misma instrucción, por ejemplo, en este código, se ejecutará algo de código sólo si los puertos D10 y D11 están en HIGH al mismo tiempo.
DDRB = B00001100; // Los pines D10 y D11 son entradas, y el resto (D8+D9+D12+D13) salidas. if (PINB & ((1<<PD2) | (1<<PD3))) { //Algún código que se ejecutará solo si los dos botones (D10+D11) se encuentran en HIGH. }
La manipulación directa de los puertos puede ser acompañada de las máscaras de bits, los operadores lógicos (NOT, AND, OR, y XOR) y operaciones de desplazamiento a la derecha e izquierda.
El siguiente ejemplo lee alternativamente 100 veces el pin D9 con digitalRead() y mediante el registro PINB y calcula el tiempo en microsegundos de ambas operaciones. Te sorprenden los resultados ?
unsigned long t,t1;
bool x = true;
int y;
void setup(){
Serial.begin(115200);
pinMode(9, INPUT_PULLUP);
}
void loop(){
t = micros();
if (x){
for (byte n=0; n<100; n++){
y = digitalRead(9);
}
}else{
for (byte n=0; n<100; n++){
y = PINB & (1 << PB1);
}
}
t1 = micros() - t;
if (x){
Serial.print("DigitalRead() = ");
}else{
Serial.print("PINx = ");
}
x = !x;
Serial.println(t1);
}
Analogicos
La forma de manejar con registros las entradas analógicas correspondientes al puerto C con POR y PIN es para usar esos pines como I/O digitales, puesto que los pines de los uC son multipropósito como se ha dicho anteriormente.
En las entradas analógicas entran en juego los conversores Analógico Digital (ADC) y en las salidas analógicas entra el PWM que usa uno de los temporizadores del uC para hacer la forma de onda PWM.
ADC
Un uC solo entiende señales digitales (0+1), por lo que para leer señales analógicas necesitamos un conversos análogo a digital (ADC). El ATmega 329P tiene incluido un conversor analógico a digital (ADC) de 6 canales (por multiplexion) y 10 bits (1024 pasos), por lo que devuelva enteros entre 0~1023.
Esto significa que a pesar de que ATmega 328P tiene disponibles 6 pines analogicos (A0~A5) solo puede leer un pin analogico a la vez (por la multilexion), de modo de si requiere lecturas de alta velocidad se podría necesitar el uso de ADC externos. El datasheet de ATmega advierte de hacer lecturas rápidas entre pines analógicos (mas de 200 KHz) puede causar ruido eléctrico, por lo que se recomienda incluir retardos de 1 milisegundo entre lecturas.
También es posible alterar la tensión máxima (siempre por debajo de Vcc) que usa el ADC. Es Aref, que es la tensión contra la que todas las entradas analógicas hacen las conversiones. Reducir Aref tiene sentido para mejorar la resolución del ADC. Con Aref = 5V la resolución es de 4,88 mV (5/1023) por paso. Si tienes un sensor de 3V3 mejor pon Vref = 3V3 y obtendrás una resolución de 3.22 mV (3.3/1023) por paso.
El ADC interno también se puede usar en un modo de 8 bits (), donde sólo se usan los 8 bits más significativos de la resolución de 10 bits completa, esto podría ser útil cuando se trabaja en ambientes ruidosos y se ahorra tiempo de uC. El ADC también puede configurarse para que lleve a cabo una conversión y detenerse o puede ser configurado para funcionar en un modo de funcionamiento libre, la primera opción es la mejor opción cuando queremos leer diferentes pines, y el segundo es mejor cuando sólo tenemos que leer un pin y esto puede ahorrar algo de tiempo entre las conversiones.
PWM
Las Salidas Pulse Width Modulation permiten simular salidas analógicas con ciertos pines digitales (D3, D5, D6, D9, D10, D11). PWM es una técnica para modificar el ancho de pulsos (duty cycle) de una señal. Lo que modificamos no es la frecuencia que siempre sera 490 Hz, sino que cambiaremos la proporción de la parte positiva y negativa de cada periodo. Sus usos son:
- Generar tensiones analógicas entre 0~100% de Vcc
- Generar señales de audio
- Controlar velocidad de motores
- Varias intensidad de un LED
En la familia de uC AVR, el PWM se controla por hardware y ATmega 328p cuenta con 3 timers. Timer 0 (D5 + D6) y timer 2 (D3 + D11) dan una resolución de 8 bit (256 pasos) mientras que el timer 1 (D9 + D10) ofrece una resolución de 16 bits (65536 pasos). Todo lo que necesita hacer es inicializar e iniciar el temporizador y establecer el ciclo de trabajo. Estos temporizadores generan interrupciones cuando alcanzan el overflow o cuando alcanzan el registro de comparación. Los registros de control del timer/counter n (donde n 0~2) son TCCRnA y TCCRnB y tienen los principales controles de los temporizadores.
Ejemplo
Se requiere definir el pin como salida.
Pin | Definición |
---|---|
D0 | PIND = bit(0); |
D1 | PIND = bit(1); |
D2 | PIND = bit(2); |
D3 | PIND = bit(3); |
D4 | PIND = bit(4); |
D5 | PIND = bit(5); |
D6 | PIND = bit(6); |
D7 | PIND = bit(7); |
D8 | PINB = bit(0); |
D9 | PINB = bit(1); |
D10 | PINB = bit(2); |
D11 | PINB = bit(3); |
D12 | PINB = bit(4); |
D13 | PINB = bit(5); |
A0 | PINC = bit(0); |
A1 | PINC = bit(1); |
A2 | PINC = bit(2); |
A3 | PINC = bit(3); |
A4 | PINC = bit(4); |
A5 | PINC = bit(5); |
void setup(){ pinMode(13, OUTPUT); } void loop(){ PINB = bit(5); delay(500); }
Ejemplo 2
void setup(){
DDRB = B11111111; //Lo mismo que DDRB = 255;
}
void loop(){
PORTB = 255; //Prende todos los LED D8~D13
delay(500);
PORTB = 0; //Apaga todos los LED D8~D13
delay(500);
}
Ejemplo 3
Parpadea el LED a bordo.
void setup() {
DDRB = DDRB | 32; //Configura el pin digital 13 como OUTPUT, no altera el resto. 32=B00100000;
}
void loop() {
PORTB = PORTB ^ 32; //Invertido estado del bit 5 (pin digital 13), no altera el resto.
delay(500);
}