Catto / Mapa de Temas · Informática I 1er nivel
Informática I · 120 h · Contenido 6 de 6

Uso del lenguaje en aplicaciones de bajo nivel

El mismo C con el que se escribe un programa de escritorio maneja las patas de un microcontrolador: alcanza con los operadores de bits, un puntero a una dirección fija y saber por qué hace falta volatile.

Operadores de bits Máscaras volatile stdint.h Interrupciones

01Qué quiere decir «bajo nivel»

Un programa de bajo nivel es el que habla directamente con el hardware: prende un LED escribiendo un bit en un registro, lee un sensor esperando que un bit de estado cambie, arma una trama bit por bit para mandarla por un cable. En la pila de capas del primer tema, es trabajar justo arriba de la arquitectura del repertorio, sin sistema operativo en el medio.

C sirve para eso porque tiene tres cosas que los lenguajes de alto nivel esconden: operadores de bits, punteros a direcciones fijas y tipos de tamaño conocido. Con ellas, el mismo lenguaje con el que se escribe una planilla de cálculo manda sobre las patas de un chip.

02Operadores de bits y máscaras

OperadorQué hacePara qué se usa
a & bY bit a bitPoner bits en 0 y preguntar por un bit
a | bO bit a bitPoner bits en 1
a ^ bO exclusivo bit a bitInvertir bits
~aComplemento de todos los bitsArmar la máscara para borrar
a << n, a >> nDesplazamientoArmar máscaras y extraer campos

Una máscara es un número que tiene en 1 los bits sobre los que se quiere actuar. (1 << 5) es 0010 0000: sólo el bit 5. Con ella salen las cuatro operaciones que se usan todo el tiempo:

reg |=  (1 << n);     // poner en 1 el bit n
reg &= ~(1 << n);     // poner en 0 el bit n
reg ^=  (1 << n);     // invertir el bit n
if (reg & (1 << n))   // ¿está en 1 el bit n?

Las cuatro tocan sólo el bit elegido y dejan los demás como estaban. Es lo que importa cuando cada bit de un registro controla una pata distinta: prender un LED no puede apagar el de al lado.

Laboratorio · un puerto de 8 LEDs

El registro PORTB maneja ocho LEDs. Elegí la operación y el bit, y mirá la línea de C, la cuenta en binario y el resultado.


        
Campos de más de un bit

Para leer un campo, se corre y se enmascara: los bits 4 a 6 de reg son (reg >> 4) & 0x07. Para escribirlo sin tocar el resto, primero se borra y después se pone: reg = (reg & ~(0x07 << 4)) | ((v & 0x07) << 4). Así se configuran, por ejemplo, el divisor de reloj de un temporizador o el canal de un conversor A/D.

03Registros en direcciones fijas

En un microcontrolador, los periféricos se manejan con registros que están en direcciones fijas de memoria, publicadas en la hoja de datos. En el ATmega328P (el del Arduino Uno), el registro de salida del puerto B está en la dirección de datos 0x25. En C se lo alcanza con un puntero a esa dirección:

#include <stdint.h>
#define PORTB (*(volatile uint8_t *)0x25)
#define DDRB  (*(volatile uint8_t *)0x24)

DDRB  |= (1 << 5);    // PB5 como salida (el LED de la placa)
PORTB |= (1 << 5);    // y encendido

Leído de adentro hacia afuera: 0x25 es un número; (volatile uint8_t *) lo convierte en la dirección de un byte; el * de afuera accede al byte que está ahí. A partir de eso, PORTB se usa como una variable más. Los archivos de cabecera del fabricante (<avr/io.h>, los de CMSIS en ARM) están hechos exactamente así.

Por qué volatile

El compilador optimiza suponiendo que una variable sólo cambia cuando el programa la escribe. Un registro de hardware cambia solo: el bit de «dato recibido» de una UART se pone en 1 cuando llega un byte. Sin volatile, un ciclo while (!(UCSR0A & (1 << RXC0))); puede compilarse leyendo el registro una sola vez y quedar colgado para siempre. volatile obliga a leer y escribir la memoria real en cada acceso. Hace falta en los registros de hardware y en las variables que modifica una rutina de interrupción.

04Tipos de tamaño exacto

El estándar de C sólo garantiza mínimos: un int tiene al menos 16 bits. En un AVR tiene 16; en una PC, 32. Un programa que supone 32 bits se rompe al llevarlo a un microcontrolador. Para el trabajo con hardware se usan los tipos de <stdint.h>, que dicen su tamaño en el nombre:

TipoBitsRango
uint8_t / int8_t80 … 255 / −128 … 127
uint16_t / int16_t160 … 65 535 / −32 768 … 32 767
uint32_t / int32_t320 … 4 294 967 295 / ±2 147 483 647
Ejemplo: escalar la lectura de un conversor A/D

Un conversor de 10 bits con referencia de 5 V entrega de 0 a 1023. Para pasarlo a milivoltios: uint32_t mv = (uint32_t)lectura * 5000 / 1023;. Sin la conversión a 32 bits, en un AVR la multiplicación se haría en 16 bits: 1023 · 5000 = 5 115 000 no entra en 65 535 y el resultado sale mal sin ningún aviso. Con la conversión, 1023 da exactamente 5000 mV. Los conversores se ven en conversores A/D y D/A.

05Orden de los bytes y campos de bits

Un uint32_t ocupa cuatro bytes, pero ¿en qué orden? En un procesador little-endian (x86, casi todos los ARM, AVR) el byte menos significativo va en la dirección más baja; en uno big-endian, al revés. El valor 0x12345678 guardado en 0x100 queda así:

Dirección0x1000x1010x1020x103
Little-endian78563412
Big-endian12345678

Dentro de un programa no se nota. Se nota al mandar datos a otra máquina: los protocolos de red usan big-endian, y por eso existen htons y ntohl. Lo portable es armar los bytes con desplazamientos, b[0] = x >> 24; b[1] = x >> 16; …, que da el mismo resultado en cualquier procesador. Aplica a cualquier trama de comunicación serie.

C también permite declarar campos de bits dentro de una estructura, unsigned modo : 3;. Son cómodos, pero el estándar deja al compilador el orden de los bits dentro del byte. Para describir registros de hardware que tienen que coincidir bit a bit con la hoja de datos, las máscaras son más seguras.

06El programa frente al hardware

Un programa para un microcontrolador no termina nunca: inicializa los periféricos y entra en un ciclo infinito. Dentro del ciclo consulta entradas y actualiza salidas; lo urgente lo atiende una rutina de interrupción, que se comunica con el ciclo principal a través de variables volatile:

volatile uint8_t hay_dato = 0;
volatile uint8_t dato;

ISR(USART_RX_vect)           // la llama el hardware al llegar un byte
{
    dato = UDR0;
    hay_dato = 1;
}

int main(void)
{
    inicializar();
    for (;;) {
        if (hay_dato) {
            hay_dato = 0;
            procesar(dato);
        }
        /* resto del trabajo, sin demoras largas */
    }
}
Accesos que se cortan a la mitad

En un procesador de 8 bits, leer un uint16_t lleva dos instrucciones. Si la interrupción lo modifica entre las dos, el programa lee la mitad vieja y la mitad nueva. Esas lecturas se hacen con las interrupciones deshabilitadas por un instante (una sección crítica). Es un error que aparece una vez cada mil ejecuciones y no se encuentra probando: se evita sabiendo que existe.

Las interrupciones y los periféricos se desarrollan en interrupciones y en periféricos del microcontrolador.

07Qué hace el compilador con esto

La mejor manera de convencerse de que C está cerca de la máquina es mirar lo que genera. Para el AVR, con optimización:

CEnsamblador AVRComentario
PORTB |= (1 << 5);sbi 0x05, 5Una instrucción que pone en 1 un bit de un registro de E/S
PORTB &= ~(1 << 5);cbi 0x05, 5Y otra que lo pone en 0
if (PINB & (1 << 0))sbis 0x03, 0Salta la instrucción siguiente si el bit está en 1

El 0x05 es la dirección de PORTB en el espacio de E/S, que en el espacio de datos es la 0x25 del ejemplo anterior: hay 0x20 de diferencia porque los primeros 32 bytes del espacio de datos son los registros de trabajo. Una línea de C, una instrucción de máquina: por eso no hace falta programar en ensamblador para tener control fino. Los microcontroladores se ven en arquitectura de microprocesadores y los buses que se manejan con estas técnicas, en buses serie.

08En el laboratorio

Práctica 1 · Parpadeo sin bibliotecas

En un Arduino Uno, hacer parpadear el LED de PB5 escribiendo directamente en DDRB y PORTB, sin pinMode ni digitalWrite. Medir con el osciloscopio la frecuencia máxima que se logra con PORTB ^= (1 << 5) en un ciclo vacío y compararla con la de digitalWrite. Se puede practicar el osciloscopio antes en el simulador de instrumental.

Práctica 2 · Funciones de bits en la PC

Escribir y probar en la PC set_bit, clr_bit, toggle_bit, get_field y set_field sobre un uint8_t, imprimiendo en binario cada resultado. Probar con los casos extremos: bit 0, bit 7, campo que ocupa el byte entero.

Práctica 3 · Descubrir el orden de los bytes

Guardar 0x12345678 en un uint32_t, apuntarlo con un uint8_t * e imprimir los cuatro bytes. Deducir si la máquina es little o big-endian.

09Errores frecuentes

  • reg = (1 << n) en lugar de reg |= (1 << n): prende un bit y apaga todos los demás.
  • reg &= (1 << n) para apagar: sin el ~ borra todos los bits menos el n.
  • Olvidar volatile en un registro o en una variable compartida con una interrupción.
  • Desplazar más bits de los que tiene el tipo: 1 << 31 con int de 16 bits no está definido; se escribe 1UL << 31.
  • Confundir && con &: 4 && 2 es 1; 4 & 2 es 0.
  • Escribir los números mágicos sueltos: PORTB |= 0x20 funciona, pero (1 << PB5) se entiende.

10Autoevaluación

¿Qué línea pone en 0 los bits 2 y 3 de reg sin tocar los demás?

reg &= ~((1 << 2) | (1 << 3));, o sea reg &= ~0x0C;

Si reg = 0xB4, ¿qué valen los bits 4 a 6?

0xB4 = 1011 0100. (0xB4 >> 4) & 0x07 = 1011 & 111 = 011 = 3.

¿Por qué el ciclo que espera un bit de estado necesita volatile?

Porque sin él el compilador puede leer el registro una sola vez, suponer que no cambia y convertir la espera en un ciclo infinito.

¿Cómo queda 0xA1B2 en memoria en una máquina little-endian?

Primero B2 y después A1.

¿Por qué lectura * 5000 puede dar mal en un AVR si lectura es uint16_t?

Porque la cuenta se hace en int, que en AVR tiene 16 bits, y el producto supera 65 535. Hay que convertir a 32 bits antes de multiplicar.

¿Qué es una sección crítica?

Un tramo de código que no puede ser interrumpido a la mitad sin dejar los datos inconsistentes. En un microcontrolador se protege deshabilitando las interrupciones durante ese tramo.

11Para ampliar

  • Michael Barr y Anthony Massa. Programming Embedded Systems: With C and GNU Development Tools. 2.ª ed., O'Reilly, 2006 (en inglés). Registros, interrupciones y periféricos en C, con ejemplos sobre hardware real.
  • Elecia White. Making Embedded Systems. 2.ª ed., O'Reilly, 2024 (en inglés). Cómo se diseña el software de un sistema embebido de punta a punta, con criterio de ingeniería.
  • Randal E. Bryant y David R. O'Hallaron. Computer Systems: A Programmer's Perspective. 3.ª ed., Pearson, 2016 (en inglés). El capítulo 3 muestra, línea por línea, qué genera el compilador a partir de C.
  • Microchip Technology. ATmega328P Datasheet. Hoja de datos oficial, en el sitio del fabricante. El mapa de registros de E/S y el repertorio de instrucciones que se usaron en este tema.
Desarrollo del contenido «Uso del lenguaje en aplicaciones de bajo nivel» de Informática I (primer nivel), según el diseño curricular de Ingeniería Electrónica, Plan 2023 — Ordenanza N° 1849 del Consejo Superior de la UTN. Volver al Mapa de Temas · catto.ar