← Índice del curso

Microcontroladores · Tema 9

Registros y mapa de memoria

Cada periférico se maneja con unos pocos bytes mapeados en memoria. Quien sabe leer la descripción de un registro en la hoja de datos puede configurar cualquier chip, aunque nunca lo haya usado. Este tema enseña esa lectura y los errores que se evitan con ella.

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.

Ejemplos reales del ATmega328P.
PeriféricoControlEstadoDatos
Puerto de E/SDDRxPINx (lee el nivel real)PORTx
Conversor analógicoADMUX, ADCSRA, ADCSRBBits ADSC y ADIF de ADCSRAADCL, ADCH
UARTUCSR0B, UCSR0C, UBRR0UCSR0AUDR0
Temporizador 0TCCR0A, TCCR0B, TIMSK0TIFR0TCNT0, 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:

Tipos de acceso habituales en la documentación del AVR.
NotaciónSignificadoQué pasa al escribir
R/WLectura y escrituraEl bit toma el valor escrito
RSolo lecturaLa escritura se ignora; el hardware lo actualiza
WSolo escrituraSe acepta, pero leer devuelve otra cosa (a menudo 0)
Reservado (–)Sin función definidaSe 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:

ADCSRA, dirección de datos 0x7A. Valor inicial: 0x00.
Bit76543210
NombreADENADSCADATEADIFADIEADPS2ADPS1ADPS0
AccesoR/WR/WR/WR/WR/WR/WR/WR/W
Inicial00000000
  • 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.
Selección del divisor del reloj del ADC y frecuencia resultante con un reloj de CPU de 16 MHz.
ADPS2:0DivisorReloj del ADC a 16 MHz
000 o 00128 MHz
01044 MHz
01182 MHz
100161 MHz
10132500 kHz
11064250 kHz
111128125 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, CBI y pruebas de bit en una sola instrucción.
  • Los 64 registros de E/S admiten IN y OUT (un ciclo).
  • Los extendidos, como ADCSRA (0x7A) o UCSR0B (0xC1), solo se alcanzan con LDS/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:

Orden de acceso a registros de 16 bits (el compilador lo maneja si usás el nombre de 16 bits).
RegistroLecturaEscritura
ADC (ADCL/ADCH)Primero ADCL, después ADCHNo se escribe
Temporizador 1 (TCNT1, OCR1A, etc.)Primero el byte bajo, después el altoPrimero 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):

Tres formas de intentar borrar TOV0 con las banderas 111 pendientes, cuando la operación es lectura-modificación-escritura de un byte.
CódigoByte escritoResultado
TIFR0 = (1 << TOV0);0000000100000110: se borra solo TOV0
TIFR0 |= (1 << TOV0); (lee, modifica, escribe)0000011100000000: se borran las tres, y se pierden eventos
TIFR0 &= ~(1 << TOV0);0000011000000001: 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

Dirección—
Binario00000000
Decimal y hexadecimal—

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.

TIFR0 (bits 7 a 0)00000001

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

  1. Elegí ADCSRA y marcá ADEN, ADPS2, ADPS1 y ADPS0. El valor es 87 y el código C coincide con el de la sección 4.
  2. Escribí FF en el campo de valor de ADMUX: el bit 4 queda en 0 porque es reservado, y el resultado es EF.
  3. Elegí DDRB y marcá DDB5: el código es DDRB = (1 << DDB5);, la línea del parpadeo del tema 5. Fijate en las dos direcciones: 0x24 en datos y 0x04 en E/S.
  4. En TCCR0B, activá CS02 y CS00 (valor 05): es un divisor del temporizador, que usaremos pronto.
  5. 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

  1. Buscá el capítulo del periférico y leé la descripción funcional y el diagrama de bloques: qué hace y qué necesita.
  2. Ubicá la lista de registros («Register Description») y clasificalos en control, estado y datos.
  3. Anotá el valor inicial de cada uno y qué hay que cambiar para que el periférico funcione.
  4. Armá el valor con nombres de bit, con (1 << BIT), nunca con números mágicos.
  5. Respetá las reglas de acceso: banderas con asignación directa, registros de 16 bits con el nombre completo, bits reservados en 0.
  6. 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.