01 · Punto de partida
El periférico más usado
Un microcontrolador sin pines no sirve para nada: lo que lo distingue de una computadora es que sus señales salen del chip y llegan a sensores y actuadores. Los pines de propósito general (GPIO, general-purpose input/output) pueden ser entradas o salidas digitales según se configure. En el parpadeo del tema 5 ya usaste uno; en el tema 9 viste que sus registros son los más simples del chip.
Este tema explica con rigor cómo se configura un pin como salida y qué ocurre en cada combinación de registros; el siguiente estudia la electrónica del pin y el posterior, las entradas. Es la base de todo lo que sigue: sin entender los GPIO, los demás periféricos (temporizadores, UART, SPI) no se pueden conectar.
Retomamos la lectura de registros y la manipulación de bits de los temas 9 y 10, y el mapa de pines del tema 4.
02 · La estructura
Tres registros por puerto
El ATmega328P agrupa sus pines en tres puertos de hasta 8 bits: B, C y D. Cada puerto tiene tres registros de 8 bits, uno por cada letra del puerto:
| Registro | Nombre | Función | Acceso |
|---|---|---|---|
DDRx | Data Direction Register | Bit en 1: el pin es salida. Bit en 0: entrada | Lectura y escritura |
PORTx | Port Data Register | Como salida: el nivel que se impone. Como entrada: activa (1) o no (0) el pull-up | Lectura y escritura |
PINx | Port Input Pins | Lee el nivel real de cada pin. Escribir un 1 invierte el bit de PORTx | Lectura (y escritura con efecto especial) |
| Puerto | PINx | DDRx | PORTx |
|---|---|---|---|
| B | 0x23 (0x03) | 0x24 (0x04) | 0x25 (0x05) |
| C | 0x26 (0x06) | 0x27 (0x07) | 0x28 (0x08) |
| D | 0x29 (0x09) | 0x2A (0x0A) | 0x2B (0x0B) |
Los tres están en las primeras 32 direcciones de E/S, de modo que el compilador puede usar SBI, CBI y IN/OUT (tema 7): operar sobre un bit cuesta una sola instrucción de 2 ciclos. Tras el reset, los tres registros valen cero: todos los pines son entradas sin pull-up.
03 · Lo que ocurre en cada pin
Los cuatro estados de un pin
Dos bits, uno de DDRx y otro de PORTx, definen el comportamiento de cada pin:
| DDRxn | PORTxn | Estado del pin | Qué significa |
|---|---|---|---|
| 0 | 0 | Entrada, alta impedancia (Hi-Z) | El pin solo escucha; no consume ni impone corriente. Si no hay nada conectado, su nivel es indefinido |
| 0 | 1 | Entrada con pull-up | Una resistencia interna (decenas de kΩ) lo lleva a VCC si nadie lo maneja |
| 1 | 0 | Salida en bajo | El pin se conecta a masa: puede absorber corriente |
| 1 | 1 | Salida en alto | El pin se conecta a VCC: puede entregar corriente |
Son las cuatro combinaciones de dos bits, y cada una tiene una función distinta. Observá que PORTxn significa cosas distintas según la dirección: el nivel si es salida, el pull-up si es entrada. Es la fuente de muchas confusiones.
Un detalle del fabricante: el pull-up puede deshabilitarse globalmente con el bit PUD del registro MCUCR; si está en 1, ningún pin tiene pull-up aunque PORTxn valga 1. Además, la hoja de datos advierte que pasar directamente de entrada flotante (00) a salida en alto (11) exige un estado intermedio: el pull-up (01) o la salida en bajo (10). En la práctica, hay una forma segura de hacerlo, que vemos a continuación.
04 · El código
Configurar y escribir
Los patrones de la manipulación de bits (tema 10) se aplican directamente:
// Configurar PB5 como salida
DDRB |= (1 << PB5);
// Escribir el nivel
PORTB |= (1 << PB5); // en alto
PORTB &= ~(1 << PB5); // en bajo
PORTB ^= (1 << PB5); // invertir
// Varios pines a la vez: PB0, PB1 y PB5 como salidas, PB2 como entrada con pull-up
PORTB = (1 << PB2); // primero el nivel / pull-up
DDRB = (1 << PB0) | (1 << PB1) | (1 << PB5); // después la dirección
El orden importa. Si primero se hace DDRB |= (1 << PB5) y recién después se escribe PORTB, el pin se vuelve salida con el valor que tuviera PORTB (por ejemplo 1, si antes se había activado como pull-up) y se produce un pulso no deseado: un LED destella o un relé da un golpe. La regla de buena práctica es escribir primero el nivel y después la dirección, así el pin nace en el estado correcto.
Esa misma regla resuelve el estado intermedio de la hoja de datos: antes de pasar un pin a salida en alto, se lo lleva a salida en bajo o a pull-up, es decir, se escribe PORTB mientras todavía es entrada.
Otras formas útiles:
PINB = (1 << PB5);invierte el pin en una sola instrucción (SBI), la forma más rápida de generar una onda cuadrada por software.DDRB = 0xFF;yPORTB = 0x55;configuran los 8 pines como salidas y los dejan en01010101, útil para un bus.DDRD &= ~(1 << PD2);vuelve a PD2 una entrada.
Cada pin tiene dos capacidades que veremos en el tema 16: cuánta corriente puede entregar y absorber, y a qué nivel se considera «alto» o «bajo» como entrada. Por ahora basta recordar que un pin maneja unos pocos miliamperios, no cargas de potencia.
05 · Un byte de una vez
El puerto como bus de 8 bits
Como los 8 pines de un puerto comparten un registro, se pueden escribir todos a la vez con una sola instrucción. Es una ventaja grande sobre cambiar los pines de uno en uno: los ocho cambian en el mismo ciclo, sin un estado intermedio en que algunos tengan el valor viejo y otros el nuevo.
DDRD = 0xFF; // PD0 a PD7 como salidas
// Corredor de luces: un LED encendido que recorre los ocho
while (1) {
for (uint8_t i = 0; i < 8; i++) {
PORTD = 1 << i; // un solo bit en 1
_delay_ms(100);
}
}
Esta técnica sirve para manejar una barra de ocho LED, los segmentos de un display de siete segmentos (con dos puertos, un dígito por puerto), un bus de datos de 8 bits hacia otro chip o el valor de un conversor digital-analógico R-2R. Con lo que viste en el curso de electrónica digital, el puerto es un registro de salida de 8 bits: lo que escribís se mantiene hasta que lo cambiás.
Para usar solo algunos pines de un puerto sin tocar los demás, hay que enmascarar: PORTD = (PORTD & 0x0F) | (valor << 4) cambia los cuatro pines altos y conserva los bajos (la fórmula del campo del tema 10).
06 · Cuidados con el resto
Pines sin usar y funciones alternativas
Pines sin usar. Una entrada flotante (sin pull-up y sin nada conectado) capta ruido, y la lógica de entrada, al oscilar entre niveles, consume más corriente de lo normal. En un diseño cuidadoso los pines sobrantes se configuran con un nivel definido: entradas con pull-up (la opción más segura si pueden quedar conectados a algo) o salidas en bajo (si seguro no están conectados). Es una de las medidas de ahorro de energía más baratas.
Funciones alternativas. Casi todos los pines comparten el GPIO con otros periféricos (tema 4): la UART en PD0 y PD1, las salidas PWM en PD6 y PD5, el bus SPI en PB3 a PB5. Cuando se habilita el periférico, toma el control del pin y el valor de PORTx deja de mandar. Por ejemplo, con COM0A1 o COM0A0 activados en TCCR0A, el pin PD6 es la salida del temporizador y no responde a PORTD, aunque DDRD lo marque como salida.
Dos reglas prácticas. La primera: la dirección (DDRx) sigue siendo responsabilidad del programa incluso con una función alternativa, porque el periférico no la cambia por vos (hay que poner el pin como salida para que la señal salga). La segunda: leer PINx funciona siempre, incluso si el pin está en manos de otro periférico, lo que permite observar su estado.
07 · El costo de un cambio
Qué tan rápido conmuta un pin
En el tema 7 viste que escribir un bit de un puerto cuesta 2 ciclos con SBI. Con el cálculo de ciclos del tema 14 se obtiene la máxima frecuencia de una onda cuadrada generada por software con un reloj de 16 MHz:
| Método | Ciclos por período | Frecuencia |
|---|---|---|
PORTB ^= (1 << PB0) en un bucle | 12 | 1,33 MHz |
PINB = (1 << PB0) en un bucle | 8 | 2 MHz |
SBI y CBI en un bucle | 6 | 2,67 MHz (ciclo de trabajo del 33 %) |
PINB = … repetido sin salto | 4 | 4 MHz |
| Salida del temporizador (por hardware) | 2 | 8 MHz |
La biblioteca de Arduino ofrece digitalWrite(), que traduce un número de pin de la placa a un puerto y un bit y comprueba si el pin tiene PWM activo. Esa generalidad cuesta decenas de ciclos por llamada, de modo que conmutar con ella es mucho más lento que con el registro. En un LED no importa; en un bus rápido o una señal de temporización, sí.
Para señales más rápidas que lo que alcanza el código (varios MHz), la solución es delegarlas a un periférico: el temporizador invierte el pin por hardware sin ejecutar ninguna instrucción, y puede llegar a la mitad del reloj de la CPU. Es una señal periódica; los datos arbitrarios, a esas velocidades, los maneja el bus SPI (tema 34).
08 · Compartir un puerto
Atomicidad al compartir un puerto
Cuando una rutina de interrupción y el programa principal escriben bits distintos del mismo puerto, hay un riesgo. Una operación de lectura, modificación y escritura se hace en tres pasos: leer el registro, cambiar el bit en un registro de trabajo y escribir de vuelta. Si una interrupción cambia otro bit del puerto entre la lectura y la escritura, la escritura posterior pisa el cambio con el valor viejo.
La instrucción SBI/CBI es atómica: modifica un solo bit y no puede interrumpirse a medias. Por eso PORTB |= (1 << PB5) es seguro cuando el compilador la usa, lo que ocurre con un bit constante en las primeras 32 direcciones de E/S. En cambio, PORTB ^= mascara, o una máscara variable, se compila como lectura, operación y escritura, y no es atómico.
Dos soluciones: usar siempre operaciones de un bit con constantes (que producen SBI/CBI) o deshabilitar las interrupciones durante la modificación. Lo veremos en detalle con las interrupciones.
09 · Analogía con programación
Un puerto como objeto con tres registros
Esta clase modela un puerto de 8 pines: ddr y port definen el modo de cada pin, leer_pin() combina los niveles aplicados desde afuera con lo que cada pin impone, y escribir_pin() reproduce el atajo de invertir con PINx. Los «?» son pines de entrada sin pull-up y sin nada conectado.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
class Puerto:
"""Un puerto de 8 pines: DDR elige la dirección, PORT el nivel o el pull-up, PIN lee."""
def __init__(self):
self.ddr = 0 # tras el reset: todos entradas
self.port = 0
def modo(self, bit):
ddr, port = (self.ddr >> bit) & 1, (self.port >> bit) & 1
if ddr:
return "salida en alto" if port else "salida en bajo"
return "entrada con pull-up" if port else "entrada flotante (Hi-Z)"
def leer_pin(self, externo):
"""externo: nivel aplicado desde afuera a cada pin (0, 1 o None si no hay nada conectado)."""
valor = ""
for bit in range(7, -1, -1):
ddr, port = (self.ddr >> bit) & 1, (self.port >> bit) & 1
if ddr:
nivel = port # una salida impone su nivel
elif externo.get(bit) is not None:
nivel = externo[bit] # la señal externa manda
else:
nivel = 1 if port else "?" # pull-up: 1; sin pull-up: indefinido
valor += str(nivel)
return valor
def escribir_pin(self, valor):
self.port ^= valor # escribir 1 en PINx invierte PORTx
b = Puerto()
b.ddr = 0b00100011 # PB0, PB1 y PB5 como salidas
b.port = 0b00000100 # PB2 con pull-up; las salidas en bajo
for bit in range(7, -1, -1):
print(f"PB{bit}: {b.modo(bit)}")
externo = {2: 0} # un pulsador conecta PB2 a masa
print("PINB con el pulsador de PB2 apretado:", b.leer_pin(externo))
print("PINB sin nada conectado :", b.leer_pin({}))
for _ in range(4):
b.escribir_pin(1 << 5) # PINB = (1 << PB5)
print(f"toggle de PB5 -> PORTB = {b.port:08b}")
Resultado: con la configuración del ejercicio 1 (DDRB = 0b00100011, PORTB = 0b00000100), PB0, PB1 y PB5 son salidas en bajo, PB2 es entrada con pull-up y el resto está flotando. Con el pulsador de PB2 apretado, PINB marca 0 en ese bit; con nada conectado, 1. Cuatro escrituras en PINB invierten PB5 de ida y vuelta. Probá agregar un nivel externo a PB3 o cambiar PORTB.
El modelo es un caso ideal: no considera la corriente que circula si una salida lucha contra una fuente externa, ni los tiempos de propagación, ni el bit PUD. En el hardware real, un conflicto entre una salida y un nivel externo contrario es un cortocircuito.
10 · Exploración interactiva
Pines, luces y velocidad
Tres herramientas: un configurador que traduce el modo de cada pin a los valores de DDRB y PORTB y avisa de conflictos eléctricos, un corredor de luces que muestra cómo se escriben patrones en un puerto completo, y una tabla de la velocidad máxima de conmutación por método.
Banco de GPIO
1. Configurar los pines del puerto B
2. Corredor de luces en el puerto D
3. Velocidad máxima de conmutación
El ensamblador de la herramienta 3 es el equivalente típico (no la salida de un compilador concreto) y los ciclos siguen la tabla de instrucciones del tema 7. Los pines de la herramienta 1 se asumen conectados directamente a GND o a VCC, sin resistencia en el medio.
Recorrido guiado
- En la herramienta 1, poné PB0, PB1 y PB5 en «Salida en bajo» y PB2 en «Entrada con pull-up»:
DDRB = 00100011yPORTB = 00000100, la configuración del ejemplo. - Con «Nada» conectado, PINB muestra
??0??100: las entradas flotantes son indefinidas y el pull-up lee 1. Cambiá a GND: el pull-up lee 0 y las flotantes también. - Poné PB5 en «Salida en alto» y conectá GND: aparece el aviso de cortocircuito. Una salida no debe luchar contra una fuente externa.
- En el corredor, probá los cuatro patrones con «Un paso» y mirá el valor de
PORTD. El contador binario se escribe tal cual en el puerto. - En la herramienta 3, pasá de XOR a
PINB =: la frecuencia sube de 1,33 a 2 MHz. Con la salida del temporizador llegás a 8 MHz, el doble de lo que logra cualquier código.
Desafío: el pulso al arrancar
Configurá PB5 como «Entrada con pull-up» y luego cambiá el modo a «Salida en bajo». Fijate en el código que genera: PORTB primero y DDRB después. Si hicieras al revés, el pin pasaría a ser salida con PORTB aún en 1 (el valor del pull-up) y presentaría un pulso en alto hasta que se corrija. Con un LED sería un destello; con un relé, un golpe que quizá sea inaceptable.
11 · Método de trabajo
Cómo configurar un pin sin sorpresas
- Decidí qué es cada pin en una tabla: nombre, dirección, nivel inicial, función. Revisá que no esté ocupado por un periférico que vayas a usar.
- Escribí primero el nivel (
PORTx) y después la dirección (DDRx), para que cada pin nazca en el estado correcto. - Definí nombres para los pines con
#define(LED,BOTON), de modo que cambiar un pin sea editar una línea. - Dejá los pines sobrantes en un estado definido: entradas con pull-up o salidas en bajo.
- Para ráfagas de datos, escribí el puerto completo en lugar de bit por bit.
- Verificá en el hardware: el multímetro en un pin de salida debe marcar cerca de 0 V o de la tensión de alimentación; el osciloscopio muestra la forma y el tiempo.
Si un LED no enciende, el orden de sospecha es: DDRx sin configurar, polaridad o resistencia del LED, pin ocupado por otra función y, solo al final, el programa.
12 · Comprensión
Confusiones frecuentes
«Escribir PORTB alcanza para encender un LED»
Solo si el pin ya es salida. Con DDRB en 0, el pin sigue siendo una entrada y PORTB solo activa el pull-up: el LED no enciende o brilla apenas.
«PORTx siempre indica el nivel del pin»
Si el pin es salida, sí. Si es entrada, activa el pull-up. Y no refleja lo que ocurre afuera: para leer el nivel real hay que usar PINx.
«Un pin sin usar se puede dejar como está»
Como entrada flotante capta ruido y consume corriente de más. Conviene fijarlo en un nivel definido, sobre todo en aplicaciones a batería.
«digitalWrite y PORTB hacen lo mismo, así que da igual»
El resultado en el pin es el mismo, pero el costo no: la función de biblioteca tarda decenas de ciclos y el registro, uno o dos. Para un LED no importa; para señales rápidas o código en una interrupción, sí.
«Una salida en alto y una en bajo conectadas entre sí no pasa nada»
Es un cortocircuito entre VCC y GND a través de los transistores de los dos pines. La corriente solo la limita el propio circuito de salida y puede dañar el chip. Para conectar salidas hay que usar resistencias o lógica adecuada.
13 · Práctica guiada
Predecí antes de comprobar
1. Armar los registros
Configurá el puerto B así: PB0, PB1 y PB5 salidas en bajo; PB2 entrada con pull-up; el resto, entradas sin pull-up. ¿Qué valen DDRB y PORTB?
Ver solución
DDRB = 0b00100011 (0x23): salidas en los bits 0, 1 y 5. PORTB = 0b00000100 (0x04): pull-up en el bit 2 y salidas en bajo. En C: PORTB = (1 << PB2); DDRB = (1 << PB0) | (1 << PB1) | (1 << PB5);.
2. Leer una configuración
Con DDRB = 0x20 y PORTB = 0xFF, ¿qué hace cada pin?
Ver solución
PB5 es una salida en alto (DDRB bit 5 = 1, PORTB bit 5 = 1). Los demás pines (0 a 4, 6 y 7) son entradas con pull-up (DDRB = 0 y PORTB = 1).
3. La conmutación más rápida
Querés generar la onda cuadrada más rápida posible en PB0 con código y un reloj de 16 MHz. ¿Qué escribirías y qué frecuencia esperás?
Ver solución
while (1) { PINB = (1 << PB0); }: un SBI de 2 ciclos y un RJMP de 2 ciclos = 4 ciclos por inversión, 8 por período: 2 MHz. Repetir la asignación en línea (sin salto) llega a 4 MHz. Para ir más rápido hay que usar el temporizador.
4. El destello inicial
Un relé conectado a PB3 se activa un instante cada vez que arranca el programa, que hace DDRB |= (1 << PB3); PORTB &= ~(1 << PB3);. ¿Por qué y cómo se evita?
Ver solución razonada
Si PORTB bit 3 vale 1 en el momento de configurar la dirección, el pin pasa a salida en alto antes de que la segunda línea lo baje. Se evita invirtiendo el orden: primero PORTB &= ~(1 << PB3) (con el pin aún como entrada) y después DDRB |= (1 << PB3).
5. Dos salidas enfrentadas
Dos pines de salida de un mismo chip se conectan por error con un cable. Uno está en alto y otro en bajo. ¿Qué pasa?
Ver solución razonada
Se forma un cortocircuito entre VCC y GND a través de los transistores de salida de ambos pines. La corriente excede los límites (20 mA recomendados por pin) y puede dañar los pines o el chip. Es un error de conexión que debe evitarse, no un caso que el programa pueda corregir.
14 · Síntesis
Ideas para recordar
- Cada puerto tiene tres registros:
DDRx(dirección),PORTx(nivel o pull-up) yPINx(lectura). - Los cuatro estados de un pin: entrada flotante (00), entrada con pull-up (01), salida en bajo (10) y salida en alto (11).
- Tras el reset todos los pines son entradas sin pull-up.
- Se escribe primero el nivel y después la dirección, para que el pin nazca en el estado correcto.
- Un puerto se puede escribir entero con una instrucción: los ocho pines cambian a la vez.
- Los pines sin usar se dejan en un nivel definido; las funciones alternativas toman el control del pin al habilitarse.
PINB = máscarainvierte pines con una instrucción;SBI/CBIson atómicas y el resto de las operaciones no lo son.
¿Listo para el tema 16?
Antes de avanzar, calculá los registros para una configuración de pines dada, explicá por qué se escribe PORTx antes que DDRx y decí qué estado tiene un pin con DDR = 0 y PORT = 1. Si podés justificar las tres respuestas, dominás los GPIO como salida.
En el próximo tema vemos la electrónica de un pin: cómo está construido por dentro, cuánta corriente maneja, qué tensiones reconoce como alto y bajo, y qué significa la alta impedancia.