La manipulación de bits consiste en operar directamente sobre la representación binaria de un dato para controlar estados individuales, empaquetar información o realizar cálculos matemáticos de alto rendimiento. En lugar de tratar un uint32_t como un número entero de 32 bits, lo tratamos como un array de 32 interruptores independientes.
Esta técnica funciona porque los procesadores modernos poseen instrucciones de hardware dedicadas (como AND, OR, XOR y SHL/SHR) que operan sobre el registro en un solo ciclo de reloj, lo que permite una velocidad de ejecución que ninguna abstracción de alto nivel puede igualar. Debes usarla cuando trabajes con registros de hardware (donde un bit puede activar un motor o un LED), cuando diseñes protocolos de red donde cada bit cuenta para ahorrar ancho de banda, o cuando necesites optimizar el uso de memoria en sistemas embebidos mediante bitfields manuales. Si aplicas mal las máscaras o los desplazamientos, lo que romperás es la integridad de los datos adyacentes: un error de un solo bit puede corromper un campo de configuración vecino o causar un comportamiento indefinido (Undefined Behavior) si realizas desplazamientos fuera de los límites permitidos del tipo de dato.
#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>
/* Constantes de flags usando desplazamiento para evitar errores manuales */
#define FLAG_ERROR (1u << 0) // Bit 0
#define FLAG_READY (1u << 1) // Bit 1
#define FLAG_INTERRUPT (1u << 2) // Bit 2
#define FLAG_LED_VERDE (1u << 3) // Bit 3
#define FLAG_LED_ROJO (1u << 4) // Bit 4
/* Definición de un campo para un ID de sensor de 4 bits situado en el byte alto */
#define SENSOR_MASK 0x0Fu // 0000 1111
#define SENSOR_OFFSET 4
/**
* Rotación a la izquierda de 32 bits.
* Nota: El desplazamiento n debe ser 0 <= n < 32 para evitar UB.
*/
uint32_t rotl32(uint32_t x, int n) {
return (x << n) | (x >> (32 - n));
}
/**
* Algoritmo de Brian Kernighan para contar bits activos (popcount).
* Eficiente porque solo itera sobre los bits que son '1'.
*/
int popcount_kernighan(uint32_t n) {
int count = 0;
while (n) {
n &= (n - 1); // Elimina el bit más bajo que esté en 1
count++;
}
return count;
}
/**
* Intercambio de bytes (Endianness) para uint32_t.
* Útil para convertir entre Little-Endian y Big-Endian.
*/
uint32_t swap_endian32(uint32_t val) {
return ((val & 0x000000FFu) << 24) |
((val & 0x0000FF00u) << 8) |
((val & 0x00FF0000u) >> 8) |
((val & 0xFF000000u) >> 24);
}
int main(void) {
uint32_t registro_estado = 0;
/* 1. Set: Activar el bit de Ready y LED Verde */
registro_estado |= FLAG_READY | FLAG_LED_VERDE;
/* 2. Clear: Desactivar el bit de Error (aseguramos que esté en 0) */
registro_estado &= ~FLAG_ERROR;
/* 3. Toggle: Invertir el estado del LED Rojo */
registro_estado ^= FLAG_LED_ROJO;
/* 4. Check: Verificar si el LED Verde está activo */
if (registro_estado & FLAG_LED_VERDE) {
printf("Estado: LED Verde encendido\n");
}
/* 5. Extraer campo: Obtener el ID del sensor (4 bits tras el offset) */
// Simulamos que el registro tiene el ID 0xA (1010) en la posición 4
registro_estado |= (0xAu << SENSOR_OFFSET);
uint32_t id_sensor = (registro_estado >> SENSOR_OFFSET) & SENSOR_MASK;
printf("ID del sensor extraído: 0x%X\n", id_sensor);
/* 6. Insertar campo: Cambiar el ID del sensor a 0x5 (0101) sin tocar otros bits */
// Primero limpiamos el área del campo con una máscara invertida
// Luego aplicamos el nuevo valor con la máscara aplicada.
uint32_t nuevo_id = 0x5u;
registro_estado = (registro_estado & ~(SENSOR_MASK << SENSOR_OFFSET)) |
((nuevo_id & SENSOR_MASK) << SENSOR_OFFSET);
printf("Nuevo registro tras cambiar ID: 0x%08X\n", registro_estado);
/* 7. Operaciones adicionales */
printf("Popcount (bits en 0xFF): %d\n", popcount_kernighan(0xFFu));
printf("Rotación de 0x00000001 << 1: 0x%08X\n", rotl32(0x00000001u, 1));
printf("Swap Endian 0x12345678: 0x%08X\n", swap_endian32(0x12345678u));
return 0;
}
Análisis del código
En el ejemplo, la manipulación de registro_estado se basa en la aplicación de máscaras de bits. Al usar registro_estado |= FLAG_READY, aplicamos una operación OR bit a bit entre el registro actual y una máscara donde solo el bit de interés está en 1. Esto garantiza que el bit se ponga en 1 sin alterar el resto.
Para limpiar un bit, como en registro_estado &= ~FLAG_ERROR, primero aplicamos el operador NOT (~) a la constante. Si FLAG_ERROR es 0x00000001, su complemento es 0xFFFFFFFE. Al hacer un AND (&) con el registro, forzamos el bit 0 a 0 (porque x & 0 = 0) y preservamos todos los demás bits (porque x & 1 = x).
La operación de inserción de un campo (punto 6 del código) es la más crítica en sistemas de bajo nivel. No basta con hacer un OR. Si el registro ya tuviera datos en la posición del SENSOR_OFFSET, el OR simplemente mezclaría los bits viejos con los nuevos. La técnica correcta es:
1. Limpiar: registro &= ~(MASK << OFFSET). Esto crea un “hueco” de ceros en la posición del campo.
2. Insertar: registro |= (VALOR << OFFSET). Esto rellena ese hueco con el nuevo dato.
En la función rotl32, la rotación se implementa combinando un desplazamiento a la izquierda (<<) con uno a la derecha (>>). Esto es necesario porque el operador de desplazamiento estándar en C no es circular; los bits que salen por un extremo se pierden. El uso de uint32_t es obligatorio aquí para asegurar que el comportamiento sea predecible respecto al tamaño del registro.
Finalmente, el algoritmo de Kernighan para popcount es notablemente más eficiente que un bucle que recorre los 32 bits uno a uno. En cada paso, n &= (n - 1) anula el bit de menor peso que sea 1, permitiendo que el bucle termine en exactamente $k$ iteraciones, donde $k$ es el número de bits activos, independientemente del tamaño del tipo.
El error frecuente
Un error clásico en sistemas embebidos es el desplazamiento de bits con tipos con signo (signed integers) o desplazamientos que exceden el tamaño del tipo.
int32_t valor = 1; int32_t error = valor << 31; // ¡Peligro! Desplazamiento al bit de signo
En C, desplazar un bit hacia la posición del signo en un entero con signo es comportamiento indefinido (UB). Además, si desplazas un bit más allá de su ancho (por ejemplo, x << 32 para un uint32_t), el resultado es indefinido. En muchas arquitecturas (como x86), el procesador aplica un módulo al desplazamiento, por lo que x << 32 podría resultar en x << 0, devolviendo el valor original y provocando errores de lógica extremadamente difíciles de rastrear. Siempre usa tipos unsigned (uint32_t, uint8_t, etc.) para cualquier operación de bits.
Valgrind o AddressSanitizer no detectarán este tipo de errores lógicos de bit, por lo que el uso de herramientas como UBSan (Undefined Behavior Sanitizer) con la bandera -fsanitize=undefined es esencial durante el desarrollo.
N° 110