01 · Punto de partida
La interfaz del hardware
En el tema 3 vimos que los periféricos aparecen como direcciones de memoria y en el tema 6 conociste los registros de la CPU. Los registros de los periféricos son la tercera pieza: son la API del hardware. No hay funciones ni objetos; hay bytes, y cada bit de cada byte tiene un significado definido por el fabricante.
Si usás bibliotecas de alto nivel, esos registros están ocultos. Pero cuando algo no se comporta como esperás, o cuando necesitás una característica que la biblioteca no expone, la hoja de datos y sus tablas de bits son la única fuente de verdad. Por eso este curso insiste en ellas.
Vas a necesitar las operaciones con bits que vimos de pasada (|=, &= ~, <<); el tema 10 las estudia a fondo.
02 · Una clasificación útil
Control, estado y datos
Casi todos los periféricos usan tres clases de registros:
Registros de control
Configuran el periférico: lo habilitan, eligen el modo, el divisor del reloj, qué interrupciones disparar. El programa los escribe y el hardware los lee.
Registros de estado
Informan qué ocurrió: una conversión terminó, llegó un byte, hubo un desbordamiento. Los escribe el hardware y el programa los consulta (a veces, los borra).
Registros de datos
Intercambian información: el byte recibido o por enviar, el resultado de una conversión, el valor de un contador.
| Periférico | Control | Estado | Datos |
|---|---|---|---|
| Puerto de E/S | DDRx | PINx (lee el nivel real) | PORTx |
| Conversor analógico | ADMUX, ADCSRA, ADCSRB | Bits ADSC y ADIF de ADCSRA | ADCL, ADCH |
| UART | UCSR0B, UCSR0C, UBRR0 | UCSR0A | UDR0 |
| Temporizador 0 | TCCR0A, TCCR0B, TIMSK0 | TIFR0 | TCNT0, OCR0A, OCR0B |
Observá la regularidad en los nombres: ctrl, status, data, a veces con un sufijo numérico por periférico. Una vez que reconocés el patrón, un periférico nuevo se parece mucho a uno conocido.
03 · Cada bit tiene reglas
Los bits y su tipo de acceso
Un registro de 8 bits no es un número: son ocho bits independientes, cada uno con su nombre, su función y su tipo de acceso. La hoja de datos lo indica debajo de cada tabla:
| Notación | Significado | Qué pasa al escribir |
|---|---|---|
R/W | Lectura y escritura | El bit toma el valor escrito |
R | Solo lectura | La escritura se ignora; el hardware lo actualiza |
W | Solo escritura | Se acepta, pero leer devuelve otra cosa (a menudo 0) |
Reservado (–) | Sin función definida | Se escribe 0 para compatibilidad futura |
| «Se borra escribiendo un 1» | Bandera de evento (W1C) | Escribir 1 pone el bit en 0; escribir 0 no cambia nada |
También hay bits que el hardware modifica por sí solo. Un caso típico es ADSC: el programa lo escribe en 1 para iniciar una conversión, y el hardware lo pone en 0 cuando termina. Se puede leer para saber si la conversión sigue en curso.
04 · La habilidad central
Leer una descripción de registro
Veamos un registro real, ADCSRA (ADC Control and Status Register A), tal como lo presenta la hoja de datos del ATmega328P:
| Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| Nombre | ADEN | ADSC | ADATE | ADIF | ADIE | ADPS2 | ADPS1 | ADPS0 |
| Acceso | R/W | R/W | R/W | R/W | R/W | R/W | R/W | R/W |
| Inicial | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
- ADEN (bit 7): habilita el ADC. Con 0 el conversor está apagado y no consume.
- ADSC (bit 6): al escribir 1 empieza una conversión; se pone en 0 solo al terminar.
- ADATE (bit 5): permite disparar conversiones automáticamente con otro evento.
- ADIF (bit 4): bandera de «conversión terminada». Se borra al ejecutarse la interrupción o escribiendo un 1.
- ADIE (bit 3): habilita la interrupción del ADC.
- ADPS2:0 (bits 2 a 0): dividen el reloj de la CPU para obtener el reloj del ADC.
| ADPS2:0 | Divisor | Reloj del ADC a 16 MHz |
|---|---|---|
000 o 001 | 2 | 8 MHz |
010 | 4 | 4 MHz |
011 | 8 | 2 MHz |
100 | 16 | 1 MHz |
101 | 32 | 500 kHz |
110 | 64 | 250 kHz |
111 | 128 | 125 kHz |
La hoja de datos recomienda que el reloj del ADC esté entre 50 kHz y 200 kHz para obtener la resolución completa de 10 bits. A 16 MHz solo cumple el divisor 128, ADPS2:0 = 111. Con esos datos se arma el valor:
ADCSRA = (1 << ADEN) // encender el ADC
| (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0); // divisor 128 -> 125 kHz
// Valor resultante: 1000 0111 = 0x87
Los nombres ADEN, ADPS2 y los demás son constantes definidas en avr/io.h con el número de bit (por ejemplo, ADEN vale 7). La expresión (1 << ADEN) arma una máscara con un solo bit en 1, y el operador | combina varias. La macro _BV(ADEN) hace lo mismo. Este estilo es más legible y menos propenso a errores que escribir 0x87, porque el código documenta qué se está activando.
05 · Dónde están
Direcciones y registros de 16 bits
Los registros viven en las tres regiones del tema 3. Las dos que aquí importan son los registros de E/S (0x20–0x5F en el espacio de datos, 0x00–0x3F en el de E/S) y los de E/S extendida (0x60–0xFF). El compilador elige la instrucción según la zona:
- Los primeros 32 registros de E/S admiten
SBI,CBIy pruebas de bit en una sola instrucción. - Los 64 registros de E/S admiten
INyOUT(un ciclo). - Los extendidos, como
ADCSRA(0x7A) oUCSR0B(0xC1), solo se alcanzan conLDS/STS(dos ciclos y 4 bytes de código cada uno).
Algunos valores ocupan 16 bits: el resultado del ADC (ADCL y ADCH), el contador del temporizador 1 (TCNT1), el divisor de baudios de la UART (UBRR0). Como el bus de datos es de 8 bits, se accede en dos pasos, y el orden importa:
| Registro | Lectura | Escritura |
|---|---|---|
ADC (ADCL/ADCH) | Primero ADCL, después ADCH | No se escribe |
Temporizador 1 (TCNT1, OCR1A, etc.) | Primero el byte bajo, después el alto | Primero el byte alto, después el bajo |
El motivo es que la lectura del byte bajo congela el byte alto en un registro temporal compartido, para que ambos pertenezcan al mismo instante: si el contador pasa de 0x00FF a 0x0100 entre las dos lecturas, un orden incorrecto daría 0x01FF, un valor que nunca existió. Con el nombre de 16 bits (ADC, TCNT1), el compilador respeta el orden por vos, y además una interrupción que use el mismo registro temporal puede corromper el acceso (se resuelve con una sección crítica, que veremos con las interrupciones).
06 · Un error clásico
Banderas que se borran escribiendo un 1
Muchos registros de estado tienen banderas de eventos: «el temporizador se desbordó», «llegó un dato». Se borran con una regla poco intuitiva: escribir un 1 las borra; escribir un 0 no cambia nada. El motivo de diseño es práctico: permite borrar una bandera sin tocar las demás.
El problema aparece cuando se borra con la expresión equivocada. Supongamos tres banderas pendientes en TIFR0 (bits 2, 1 y 0) y queremos borrar solo TOV0 (bit 0):
| Código | Byte escrito | Resultado |
|---|---|---|
TIFR0 = (1 << TOV0); | 00000001 | 00000110: se borra solo TOV0 |
TIFR0 |= (1 << TOV0); (lee, modifica, escribe) | 00000111 | 00000000: se borran las tres, y se pierden eventos |
TIFR0 &= ~(1 << TOV0); | 00000110 | 00000001: TOV0 sigue activa y se borran las otras |
La forma correcta es la asignación directa, que escribe un 1 solo donde se quiere borrar. El caso del medio merece un matiz: en el ATmega328P, TIFR0 está en las primeras 32 direcciones de E/S, y el compilador traduce |= a SBI, una instrucción que afecta solo al bit indicado. Con ese registro el resultado es correcto, pero depende de la instrucción elegida; con registros extendidos se usa lectura, OR y escritura, y se pierden banderas. La asignación directa es correcta en todos los casos y es la costumbre que conviene adquirir.
07 · De dónde se parte
El estado después del reset
La tabla de cada registro incluye su valor inicial. En un AVR, casi todos los registros arrancan en cero. Eso significa:
- Los pines son entradas en alta impedancia (
DDRx = 0) y los pull-up están apagados. - Los periféricos están deshabilitados: el ADC apagado, la UART sin transmisor ni receptor, los temporizadores detenidos.
- Las interrupciones están deshabilitadas globalmente (bit I del SREG en 0).
Por eso todo programa comienza con una sección de inicialización que escribe los registros de control. No es una convención: es lo que obliga el hardware. Y es la razón por la que un LED no enciende si olvidás DDRB, como vimos en el tema 5.
Los registros de otras familias pueden arrancar con valores distintos de cero. Siempre hay que mirar la fila «valor inicial» de la hoja de datos antes de asumir nada.
08 · Analogía con programación
Un registro con reglas, como un objeto con invariantes
Un registro real se parece a un objeto cuyos campos tienen permisos distintos. Esta clase de Python modela tres tipos de bit: normal, solo lectura y W1C (se borra con un 1). Permite comprobar, sin hardware, qué pasa con cada forma de escribir.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
class Registro:
"""Registro de 8 bits con bits de lectura/escritura, solo lectura y borrado-con-1."""
def __init__(self, solo_lectura=0, w1c=0, valor=0):
self.solo_lectura = solo_lectura # el programa no puede cambiarlos
self.w1c = w1c # escribir 1 los borra; escribir 0 no hace nada
self.valor = valor
def leer(self):
return self.valor
def escribir(self, dato):
rw = ~(self.solo_lectura | self.w1c) & 0xFF
nuevo = (self.valor & ~rw & 0xFF) | (dato & rw) # bits normales: se copian
nuevo &= ~(dato & self.w1c) & 0xFF # banderas: se borran con 1
self.valor = nuevo
# Tres banderas pendientes (bits 2, 1 y 0) en un registro tipo TIFR0.
tifr = Registro(w1c=0b00000111, valor=0b00000111)
tifr.escribir(1 << 0) # borrar solo la bandera del bit 0
print(f"TIFR = (1 << 0) -> {tifr.leer():08b}")
tifr = Registro(w1c=0b00000111, valor=0b00000111)
tifr.escribir(tifr.leer() | (1 << 0)) # leer, poner el bit 0, escribir todo
print(f"TIFR |= (1 << 0) (R-M-W) -> {tifr.leer():08b} <- se borraron las tres")
tifr = Registro(w1c=0b00000111, valor=0b00000111)
tifr.escribir(tifr.leer() & ~(1 << 0)) # error clásico
print(f"TIFR &= ~(1 << 0) -> {tifr.leer():08b} <- el bit 0 sigue en 1")
# Un registro con un bit de solo lectura (bit 7 = ocupado) y uno normal (bit 6 = habilitar).
control = Registro(solo_lectura=0b10000000, valor=0b10000000)
control.escribir(0b01000000)
print(f"Escribo 01000000 con el bit 7 en solo lectura -> {control.leer():08b}")
Resultado: la asignación directa borra solo el bit 0 (00000110); la lectura-modificación-escritura con |= borra las tres (00000000); y &= ~ deja el bit 0 intacto (00000001). El bit de solo lectura ignora la escritura. Probá escribir 0b11111111 en el registro de control y observá qué bits cambian.
La analogía llega hasta un punto: en un registro real, algunos bits cambian solos por la acción del hardware, a diferencia de un objeto cuyo estado solo cambia por sus métodos. Y el acceso a un registro puede tener efectos secundarios incluso al leerlo, como congelar el byte alto de un registro de 16 bits.
09 · Exploración interactiva
Editor de registros y borrado de banderas
La primera herramienta te deja armar el valor de cinco registros del ATmega328P marcando bits, o escribiendo el valor en hexadecimal, y genera el código C con los nombres de la hoja de datos. La segunda reproduce el error de las banderas W1C.
Banco de registros
1. Armar un registro bit a bit
2. Borrar banderas pendientes
Cada botón alterna un evento pendiente en TIFR0 (desbordamiento y comparaciones del temporizador 0). Después probá los tres métodos de borrado de TOV0, modelados como lectura-modificación-escritura de un byte.
Los nombres de bit y las descripciones del editor están resumidos de la hoja de datos; para los detalles completos (por ejemplo, las combinaciones válidas de MUX3:0) hay que consultarla. Los bits reservados aparecen deshabilitados.
Recorrido guiado
- Elegí
ADCSRAy marcáADEN,ADPS2,ADPS1yADPS0. El valor es87y el código C coincide con el de la sección 4. - Escribí
FFen el campo de valor deADMUX: el bit 4 queda en 0 porque es reservado, y el resultado esEF. - Elegí
DDRBy marcáDDB5: el código esDDRB = (1 << DDB5);, la línea del parpadeo del tema 5. Fijate en las dos direcciones:0x24en datos y0x04en E/S. - En
TCCR0B, activáCS02yCS00(valor05): es un divisor del temporizador, que usaremos pronto. - En la segunda herramienta, presioná «Tres eventos pendientes» y luego cada método de borrado, restableciendo los eventos entre uno y otro. Solo la asignación directa deja las otras dos banderas.
Desafío: configurar la UART sin mirar el código
Elegí UCSR0B y activá lo mínimo para enviar y recibir bytes con interrupciones de recepción: RXEN0, TXEN0 y RXCIE0. El valor es 0x98 (1001 1000). Cuando lleguemos al tema de la UART, este registro te va a resultar familiar.
10 · Método de trabajo
Cómo configurar un periférico nuevo
- Buscá el capítulo del periférico y leé la descripción funcional y el diagrama de bloques: qué hace y qué necesita.
- Ubicá la lista de registros («Register Description») y clasificalos en control, estado y datos.
- Anotá el valor inicial de cada uno y qué hay que cambiar para que el periférico funcione.
- Armá el valor con nombres de bit, con
(1 << BIT), nunca con números mágicos. - Respetá las reglas de acceso: banderas con asignación directa, registros de 16 bits con el nombre completo, bits reservados en 0.
- Verificá en el hardware. En el simulador o el depurador mirá el valor del registro; en la mesa, comprobá con el multímetro o el osciloscopio el efecto en el pin.
Una inspección rápida con el depurador muestra si el registro tiene el valor que pensás. Muchos «fallos de código» son un bit equivocado en un registro.
11 · Comprensión
Confusiones frecuentes
«Un registro de 8 bits es un número de 0 a 255»
Aritméticamente lo es, pero funcionalmente son ocho bits con significados independientes. Interpretarlo como número solo tiene sentido en los registros de datos.
«Puedo escribir el registro completo con un número y listo»
Sobrescribe todos los bits, incluso los que no querías tocar. A veces se hace a propósito en la inicialización, pero en registros compartidos entre funciones conviene modificar solo los bits necesarios.
«Para borrar una bandera hay que escribir un 0»
En las banderas W1C ocurre lo contrario: un 1 la borra y un 0 no hace nada. Si dudás, la hoja de datos lo dice bit por bit.
«Los bits reservados no importan»
Hay que escribirlos en 0. Un bit reservado hoy puede tener una función en otro modelo de la familia; escribir 1 puede activar algo inesperado en el futuro.
«Si dos chips tienen el mismo periférico, los registros son iguales»
A menudo tienen nombres y bits parecidos, pero con diferencias que importan. Copiar la configuración de un chip a otro sin leer su hoja de datos es una fuente clásica de errores.
12 · Práctica guiada
Predecí antes de comprobar
1. Armar un valor
Querés habilitar el ADC, usar un divisor de 64 e iniciar una conversión a la vez. ¿Qué bits de ADCSRA pones en 1 y cuál es el valor?
Ver solución
Divisor 64 = 110: ADPS2 y ADPS1. Más ADEN (bit 7) y ADSC (bit 6). Valor: 1100 0110 = 0xC6. En C: ADCSRA = (1 << ADEN) | (1 << ADSC) | (1 << ADPS2) | (1 << ADPS1);.
2. Reloj del ADC
Con un reloj de CPU de 8 MHz, ¿qué divisor del ADC cumple el rango recomendado de 50 a 200 kHz?
Ver solución
8 MHz / 64 = 125 kHz y 8 MHz / 128 = 62,5 kHz; ambos entran. El divisor 32 daría 250 kHz, fuera de rango. Conviene 64 (125 kHz), que convierte más rápido.
3. Detectar el error
Una rutina intenta limpiar una bandera W1C con FLAGS &= ~(1 << 3);, pero el evento sigue pendiente. ¿Por qué y cómo se corrige?
Ver solución
Esa expresión escribe un 0 en el bit 3, y escribir 0 no borra una bandera W1C. Se corrige con FLAGS = (1 << 3);, que escribe un 1 solo en ese bit.
4. Orden de lectura
Leés ADCH y después ADCL con un ADC de 10 bits. ¿Qué puede salir mal?
Ver solución razonada
La lectura de ADCL es la que bloquea la actualización del resultado hasta que se lee ADCH. Si invertís el orden, una conversión nueva podría actualizar el valor entre ambas lecturas y combinarías partes de dos resultados distintos. Lo seguro es leer el nombre de 16 bits ADC.
5. Estado inicial
Un programa lee PINB justo después del reset, sin configurar nada. ¿Qué lee y por qué?
Ver solución razonada
Los pines son entradas (DDRB = 0) sin pull-up (PORTB = 0), así que están flotantes y leen el nivel que haya en el pin; si nada está conectado, el valor es impredecible. Para tener lecturas confiables hay que activar el pull-up o conectar una resistencia externa.
13 · Síntesis
Ideas para recordar
- Los registros de periféricos son la interfaz del hardware: de control, de estado y de datos.
- Cada bit tiene nombre, función y tipo de acceso: R/W, R, W, reservado o W1C.
- La hoja de datos describe cada registro con una tabla de bits, su tipo de acceso y su valor inicial.
- El valor se arma con
(1 << BIT)combinados con|, en lugar de números mágicos. - Los registros de 16 bits se acceden en dos pasos con un orden definido; el nombre de 16 bits lo resuelve.
- Las banderas W1C se borran con asignación directa, no con
|=ni&= ~. - Tras el reset, casi todo está en cero: pines como entrada, periféricos apagados e interrupciones deshabilitadas.
¿Listo para el tema 10?
Antes de avanzar, armá a mano el valor de un registro a partir de una lista de bits, explicá la diferencia entre una bandera W1C y un bit normal y justificá el orden de lectura de ADCL y ADCH. Si podés justificar las tres respuestas, ya podés leer cualquier hoja de datos.
En el próximo tema vamos a ver la manipulación de bits en C con rigor: máscaras, desplazamientos, volatile y las expresiones para activar, borrar, invertir y probar bits.