01 · Punto de partida
Bits sin convenio no dicen nada
Un registro de un sensor te devuelve 0xA5. ¿Cuánto vale? Si es una cuenta sin signo, 165. Si es una temperatura con signo, −91. Si es la hora de un reloj en BCD, ni siquiera es válido (el nibble A no es un dígito). Y si llegó por una UART como texto, no es ni siquiera un número: es un byte fuera de ASCII. Los bits son los mismos; lo que cambia es el convenio con que se leen.
Una escena típica. Medís temperatura con un sensor digital. Un día de invierno la lectura salta de 3 °C a 4093 °C. El cable está bien, el sensor está bien; lo que pasó es que el valor era negativo y tu código lo leyó como número sin signo. O al revés: mostrás en un display la hora del reloj de tu placa y aparecen las 35:89 cuando son las 23:59, porque leíste un registro BCD como si fuera binario.
En programación esto es el tipo: uint8_t, int8_t y char guardan el mismo byte. En el hardware no hay compilador que te avise: el tipo vive en el datasheet y en tu cabeza.
- ¿Cómo se representan los números negativos con solo ceros y unos?
- ¿Por qué casi todo el mundo usa complemento a dos?
- ¿Qué pasa cuando una suma no cabe en su ancho, con y sin signo?
- ¿Cómo se guardan decimales pensados para un display?
- ¿Cómo viajan las letras por un cable?
02 · Números con signo
Tres formas de escribir un negativo
Con 8 bits sin signo hay 256 combinaciones y todas se usan para 0…255. Para tener negativos hay que repartir esas 256 combinaciones entre positivos y negativos. Se probaron tres convenios; solo uno sobrevivió.
- Signo-magnitud
- El bit más alto es el signo (0 = positivo, 1 = negativo) y el resto es el valor absoluto. Es lo más intuitivo y lo que hacemos en papel («−5»). Problema: existen dos ceros (+0 = 00000000 y −0 = 10000000) y sumar exige mirar los signos y decidir si sumar o restar.
- Complemento a uno
- El negativo se obtiene invirtiendo todos los bits. Suma bien casi siempre, pero sigue teniendo dos ceros (00000000 y 11111111) y la suma pide un «acarreo circular» extra.
- Complemento a dos
- El negativo se obtiene invirtiendo todos los bits y sumando 1. Un solo cero, una sola regla de suma y el mismo circuito sirve para números con y sin signo. Es el convenio universal en CPU y microcontroladores.
| Convenio | +5 | −5 | Rango | Ceros |
|---|---|---|---|---|
| Signo-magnitud | 0000 0101 | 1000 0101 | −127 … +127 | Dos (±0) |
| Complemento a uno | 0000 0101 | 1111 1010 | −127 … +127 | Dos (±0) |
| Complemento a dos | 0000 0101 | 1111 1011 | −128 … +127 | Uno |
El complemento a dos además gana un valor: el 11111111 que en los otros dos convenios era un «−0» sobrante, aquí es −1, y el rango queda como −128…+127 (un negativo más que positivos). Esa asimetría trae una consecuencia que vas a ver más abajo: −128 no tiene opuesto.
03 · Complemento a dos
Negar es invertir y sumar 1
El truco del complemento a dos es cambiar el peso del bit más alto: en lugar de valer +128, vale −128. Todo lo demás queda igual que en el Tema 4. Un byte con signo se lee sumando los pesos de sus unos, con el MSB restando.
| Bit | 7 (MSB) | 6 | 5 | 4 | 3 | 2 | 1 | 0 (LSB) |
|---|---|---|---|---|---|---|---|---|
| Peso sin signo | +128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
| Peso con signo | −128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
Con esa regla, 0xA5 = 1010 0101 vale −128 + 32 + 4 + 1 = −91. Y de ahí sale la regla práctica para negar:
La razón por la que funciona es aritmética de reloj. En 8 bits todo es módulo 256: 0xFF + 1 da 0, así que 0xFF se comporta como −1; 0xFE + 2 da 0, así que 0xFE es −2; y en general x y 256 − x suman 0. El complemento a dos de x es exactamente 256 − x.
| Hex | Binario | Sin signo | Con signo | Qué es |
|---|---|---|---|---|
| 0x00 | 0000 0000 | 0 | 0 | El único cero |
| 0x01 | 0000 0001 | 1 | +1 | Positivo mínimo |
| 0x7F | 0111 1111 | 127 | +127 | Positivo máximo |
| 0x80 | 1000 0000 | 128 | −128 | Negativo mínimo (sin opuesto) |
| 0xFE | 1111 1110 | 254 | −2 | Anteúltimo |
| 0xFF | 1111 1111 | 255 | −1 | Todo en uno = −1 |
- Reconocer el signo
- Mirando el MSB: 0 es positivo o cero, 1 es negativo. El MSB funciona como bit de signo, pero no es «signo + magnitud»: pesa −128.
- Una suma, dos lecturas
- El sumador del Tema 23 suma bits sin saber si tienen signo. 5 + (−3) = 0x05 + 0xFD = 0x102; descartando el noveno bit queda 0x02 = 2. Es el mismo circuito que suma 5 + 253 sin signo.
- Restar es sumar el opuesto
- a − b = a + (~b + 1). Por eso una CPU no necesita un circuito restador aparte: invierte b, suma y pone el acarreo de entrada en 1.
- El caso −128
- Negar 0x80 da ~0x80 + 1 = 0x7F + 1 = 0x80: el mismo byte. No hay +128 en 8 bits con signo. Es el único valor sin opuesto, y la fuente de bugs clásicos (
abs(INT8_MIN)sigue siendo negativo).
04 · Ancho y desborde
Extensión de signo y desborde
Dos situaciones aparecen en cuanto mezclás bytes de distinto ancho o sumás valores grandes. Las dos se resuelven con una idea: el bit de signo manda.
Extender sin cambiar el valor
Para pasar un valor de 8 a 16 bits sin que cambie, hay que rellenar los bits nuevos copiando el signo: se llama extensión de signo. Sin signo se rellena con ceros.
| Byte | Lectura | A 16 bits (ceros) | A 16 bits (signo) | Valor correcto |
|---|---|---|---|---|
| 0x25 | +37 | 0x0025 | 0x0025 | 37 en ambos casos |
| 0xA5 | −91 con signo / 165 sin signo | 0x00A5 (165) | 0xFFA5 (−91) | Depende del convenio |
| 0xFF | −1 con signo / 255 sin signo | 0x00FF (255) | 0xFFFF (−1) | Depende del convenio |
Esto es lo que pasa cuando un sensor entrega un valor de 16 bits en dos bytes: el alto y el bajo. Con 0xFF y 0x38 se arma 0xFF38, que con signo vale −200, no 65 336. Los acelerómetros y giróscopos tipo MPU-6050 entregan así sus lecturas.
Cuando no cabe: desborde
Sumá dos positivos de 8 bits con signo: 100 + 50 = 150. En binario: 0110 0100 + 0011 0010 = 1001 0110, que con signo es −106. La suma dio «mal» sin avisar. Como el hardware solo tiene ocho cables, el resultado verdadero no entra y el bit de signo queda con un valor que no le corresponde.
Hay una regla simple para detectar desborde en suma con signo: solo ocurre cuando ambos sumandos tienen el mismo signo y el resultado tiene el signo contrario. Positivo + negativo nunca desborda: el resultado siempre cae entre los dos.
- Acarreo (carry)
- El bit que «sale» por el extremo izquierdo. Indica que una suma sin signo no cupo. 255 + 1 genera acarreo.
- Desborde (overflow)
- El resultado de una suma con signo no cupo en el rango. 127 + 1 desborda aunque no haya salido nada por la izquierda.
- No son lo mismo
- 5 + (−3) = 0x05 + 0xFD genera acarreo (hay un noveno bit) y no desborda: el resultado, 2, es correcto. Una CPU guarda las dos señales en banderas separadas (C y V) para que decidas cuál mirar.
05 · BCD
Decimales pensados para displays
El binario puro es excelente para calcular y malo para mostrar: para sacar el dígito de las decenas hay que dividir por 10, y dividir es caro en hardware. El BCD (Binary-Coded Decimal) elige otro compromiso: cada dígito decimal ocupa un nibble. Dos dígitos caben en un byte.
Binario puro
Compacto, bueno para calcular
25 = 0x19. Un byte guarda 0…255. Sumar es directo; mostrarlo en dos dígitos exige dividir por 10.
BCD
Legible, bueno para mostrar
25 = 0x25. Un byte guarda 00…99 (100 de 256 combinaciones). Cada nibble va directo a un display; sumar pide un ajuste.
De las 16 combinaciones de cada nibble solo 10 son válidas (0…9). Los códigos A…F no existen en BCD: si aparecen, el dato está corrupto o se leyó con otro convenio. Por eso 0xA5 es «inválido» en BCD y por eso 0x25 BCD y 0x25 binario son números distintos.
Sumar en BCD: la corrección +6
Si sumás 0x25 + 0x17 con un sumador binario obtenés 0x3C. Pero 25 + 17 = 42, no «3C». El problema es que un nibble binario llega hasta 15 y el BCD se «acaba» en 9: los seis códigos A…F sobran. La solución es sumar 6 al nibble que se pasó, para saltarse esos seis valores.
| Suma | Binaria cruda | Corrección | Resultado BCD |
|---|---|---|---|
| 25 + 17 | 0x25 + 0x17 = 0x3C | nibble bajo C > 9 → +0x06 | 0x42 = «42» ✓ |
| 38 + 29 | 0x38 + 0x29 = 0x61 | 8 + 9 = 17 > 9 → +0x06 | 0x67 = «67» ✓ |
| 99 + 01 | 0x99 + 0x01 = 0x9A | +0x06 → 0xA0 → alto > 9 → +0x60 | 0x00 con acarreo = «100» ✓ |
Muchas CPU tienen una instrucción dedicada para esto (DAA, decimal adjust, en el Z80 y el 8051). Hoy el BCD se usa donde el dato se muestra más de lo que se calcula: relojes de tiempo real (los registros de los chips DS1307 y DS3231 guardan horas, minutos y segundos en BCD), multímetros, balanzas y los decodificadores de display de 7 segmentos (74LS47, CD4511), que reciben un nibble BCD y encienden los segmentos correctos.
06 · ASCII y texto
Letras por cable
Un cable solo transporta ceros y unos, así que el texto necesita una tabla acordada de antemano: cada carácter tiene un número. Esa tabla es ASCII (American Standard Code for Information Interchange, 1963), con 128 códigos que caben en 7 bits. Cuando una UART envía «A» no manda una letra: manda el byte 0x41.
| Rango | Contenido | Notas |
|---|---|---|
| 0x00 – 0x1F | Caracteres de control | NUL 0x00, BEL 0x07, TAB 0x09, LF 0x0A (salto de línea), CR 0x0D (retorno), ESC 0x1B |
| 0x20 | Espacio | Primer carácter imprimible |
| 0x30 – 0x39 | Dígitos ‘0’ a ‘9’ | El valor está en el nibble bajo: ‘7’ = 0x37 |
| 0x41 – 0x5A | Mayúsculas ‘A’ a ‘Z’ | ‘A’ = 0x41, ‘Z’ = 0x5A |
| 0x61 – 0x7A | Minúsculas ‘a’ a ‘z’ | ‘a’ = 0x61: 0x20 más que su mayúscula |
| 0x7F | DEL | Borrar; es el último código de ASCII |
| 0x80 – 0xFF | Fuera de ASCII | Extensiones (Latin-1) o bytes de UTF-8 |
La tabla tiene diseño de ingeniero, no de azar. Dos regularidades valen oro:
- Dígito ↔ valor
- Los dígitos empiezan en 0x30, así que
c - 0x30(oc & 0x0F) da el número, yn + 0x30da el carácter. Es el corazón deatoiyitoa. - Mayúscula ↔ minúscula
- Difieren en un solo bit, el 0x20.
c | 0x20pasa a minúscula,c & ~0x20a mayúscula yc ^ 0x20las alterna. ‘G’ = 0x47 y ‘g’ = 0x67. - Los números llegan como texto
- Si un sensor manda «1234» por serie, llegan los bytes 0x31 0x32 0x33 0x34, no el número 1234. Hay que reconstruirlo:
v = v * 10 + (c - 0x30)por cada carácter. - Saltos de línea
- Una «línea» puede terminar en LF (0x0A, Unix), CR (0x0D, Mac clásico) o CR LF (0x0D 0x0A, Windows y muchos protocolos). Un módulo AT que responde
OK\r\nmanda esos dos bytes de control al final.
Más allá de ASCII: Latin-1 y UTF-8
ASCII no tiene «ñ», «é» ni «€». Se resolvió con dos caminos. Latin-1 usa el octavo bit para otros 128 caracteres (‘ñ’ = 0xF1): un byte por carácter, pero solo europeo occidental. UTF-8 mantiene ASCII intacto para 0x00–0x7F y codifica el resto en 2 a 4 bytes que empiezan con un patrón de bits especial (‘ñ’ = 0xC3 0xB1). Por eso un texto ASCII puro es también UTF-8 válido, y por eso mostrar UTF-8 con una tabla Latin-1 produce «Ã±» en lugar de «ñ».
07 · Otros códigos
Gray y punto fijo: dos trucos que vas a cruzar
Hay muchos más convenios. Dos aparecen seguido en electrónica y merecen una mención.
Código Gray: cambiar un solo bit por paso
Al contar en binario, algunas transiciones cambian varios bits a la vez: de 3 (011) a 4 (100) cambian los tres. Si los bits provienen de sensores mecánicos que no conmutan exactamente a la vez, durante un instante la lectura puede ser 111 o 000: un salto falso. El código Gray reordena la secuencia para que de un número al siguiente cambie exactamente un bit.
| Decimal | Binario | Gray | Decimal | Binario | Gray |
|---|---|---|---|---|---|
| 0 | 000 | 000 | 4 | 100 | 110 |
| 1 | 001 | 001 | 5 | 101 | 111 |
| 2 | 010 | 011 | 6 | 110 | 101 |
| 3 | 011 | 010 | 7 | 111 | 100 |
La conversión es una línea: gray = n ^ (n >> 1). Los encoders rotativos absolutos y varios diseños de contadores y FIFO asíncronos usan Gray precisamente para que una lectura en pleno cambio sea, a lo sumo, el valor anterior o el siguiente, nunca uno disparatado.
Punto fijo: decimales sin coma flotante
Un microcontrolador pequeño no tiene unidad de coma flotante. Para guardar 25,0625 °C se usa punto fijo: se acuerda que el entero guardado está multiplicado por una potencia de dos (o de diez) y se divide al final. El sensor DS18B20, por ejemplo, entrega la temperatura en 16 bits con signo en unidades de 1/16 °C: la lectura 0x0191 es 401 / 16 = 25,0625 °C, y 0xFF5E es −162 / 16 = −10,125 °C. Es complemento a dos y punto fijo en un solo paquete.
08 · Simulador
Laboratorio: el camaleón
El simulador muestra un byte como ocho interruptores y lo lee a la vez en cuatro convenios: sin signo, con signo en complemento a dos, BCD y ASCII. Cambiá un bit y mirá cómo se mueve cada lectura; las que no tienen sentido para ese byte se marcan como inválidas.
- Cajas amarillas: los bits en 1. El peso de cada posición está arriba; el del MSB es −128 (en naranja).
- Línea inferior del gráfico: la suma de pesos con signo que produce el valor con signo.
- Corchetes naranjas: los dos nibbles con su dígito hexadecimal; a la vez, los dos dígitos que leería un BCD.
- Botones de operación: invertir, negar, sumar y restar 1, alternar mayúscula/minúscula. El mensaje de abajo explica qué pasó, incluidos los desbordes.
Un byte, cuatro lecturas
0x25 · cuatro lecturas
0x25: 37 / +37 / BCD 25 / ‘%’.
Recorrido guiado (6 minutos)
- 1Mirá el byte inicial (0x25).
Las cuatro lecturas dan algo válido: 37, +37, BCD «25» y ‘%’. Es el camaleón: un único byte con cuatro identidades.
- 2Pasá a 0xA5 (botón de ejemplo).
Ahora el MSB está en 1: sin signo vale 165 y con signo −91 (−128 + 37). BCD es inválido (el nibble A no es dígito) y ASCII ya no aplica (mayor que 127). Dos convenios fallan.
- 3Apretá «Negar (NOT + 1)» sobre 0x25.
Obtenés 0xDB, que con signo es −37. Volvé a negar: regresás a +37. Negar dos veces es la identidad.
- 4Probá 0x80 y negalo.
Sigue siendo 0x80 = −128. Es el único byte que no tiene opuesto. Después probá con 0x00: negar el cero devuelve el cero (un solo cero en Ca2).
- 5Con 0x7F apretá «+ 1».
Pasa de +127 a −128: desborde con signo. Con el mismo gesto sobre 0xFF obtenés 0x00: sin signo es un desborde (255 + 1 = 0), con signo es correcto (−1 + 1 = 0). Mismo byte, mismo circuito, veredictos distintos.
- 6Con 0x41 apretá «Alternar caso».
‘A’ (0x41) pasa a ‘a’ (0x61) y viceversa: un solo bit, el 0x20. Recorré el deslizador de 0x30 a 0x39 y verificá que los dígitos ASCII tienen el valor en el nibble bajo.
Preguntas para pensar
- Llevá el byte a 0xA5. ¿Qué lecturas son válidas y cuáles no? ¿Por qué?
- Buscá 0x41 y 0x61. ¿Qué caracteres son y cuánto difieren en valor?
- ¿En qué rango coinciden «sin signo» y «con signo»? ¿Y en qué rango coinciden con ASCII imprimible?
- ¿Para qué bytes BCD es válido? ¿Cuántos son de los 256 posibles?
Ver respuestas sugeridas
- Sin signo (165) y con signo (−91) son válidas; BCD no (el nibble alto es A) y ASCII no (mayor que 127). Dos convenios numéricos siempre aplican; los otros dos dependen de que el byte caiga en su dominio.
- ‘A’ y ‘a’; difieren en 0x20 = 32, un solo bit. Alternar caso es XOR con 0x20.
- Coinciden de 0 a 127 (0x00–0x7F): con el MSB en 0 el peso −128 no participa. Los imprimibles de ASCII son 0x20–0x7E, dentro de ese tramo; a partir de 128 divergen los números y ASCII deja de existir.
- Para los bytes con ambos nibbles entre 0 y 9: 10 × 10 = 100 de los 256 (el 39 %). El resto son códigos inválidos.
09 · Convenios en código
Cuatro lecturas, ocho experimentos
Reproducí en Python lo que viste en el simulador y agregá lo que el simulador no muestra: extensión de signo, desborde con banderas, suma BCD, texto y Gray. Los bloques usan solo enteros: lo que ves es lo que haría un microcontrolador.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
Paso 1: un byte, cuatro lecturas
def lecturas(b):
s = b - 256 if b >= 128 else b
hi, lo = b >> 4, b & 0xF
bcd = f"{hi}{lo}" if hi <= 9 and lo <= 9 else "invalido"
if b > 127:
asc = "no-ASCII"
elif b < 32 or b == 127:
asc = f"control 0x{b:02X}"
else:
asc = repr(chr(b))
print(f"0x{b:02X} {b:08b} U={b:>3} S={s:+d} BCD={bcd} ASCII={asc}")
for b in (0x25, 0xA5, 0xFF, 0x41, 0x00, 0x0A):
lecturas(b)
0x25 → 37 / +37 / «25» / ‘%’. 0xA5 → 165 / −91 / inválido / no-ASCII. ¿Qué byte es válido en los cuatro convenios y no es el 0x25? Probá 0x41 y 0x30.
Paso 2: a complemento a dos y de vuelta
def a_ca2(n, bits=8):
lo, hi = -(1 << (bits - 1)), (1 << (bits - 1)) - 1
if not lo <= n <= hi:
print(f"{n} no cabe en {bits} bits con signo (rango {lo}..{hi})")
return None
v = n & ((1 << bits) - 1)
print(f"{n:>5} -> {v:0{bits}b} = 0x{v:0{bits // 4}X}")
return v
def de_ca2(v, bits=8):
return v - (1 << bits) if v >> (bits - 1) else v
for n in (37, -37, -1, -91, 127, -128, 128):
a_ca2(n)
print(de_ca2(0xA5), de_ca2(0xFF), de_ca2(0x80))
El truco n & 0xFF convierte cualquier entero de Python en su byte de complemento a dos. 128 no cabe: ¿qué rango tendría con bits=16?
Paso 3: negar paso a paso y restar sumando
def negar8(x):
inv = ~x & 0xFF
r = (inv + 1) & 0xFF
print(f"-{x}: {x:08b} -> invertir {inv:08b} -> +1 {r:08b} = 0x{r:02X}")
return r
for x in (91, 37, 1, 0, 128):
negar8(x)
a, b = 5, 3
r = (a + negar8(b)) & 0xFF
print(f"5 - 3 = 5 + 0x{negar8(b):02X} = {r}")
91 → 0xA5, 37 → 0xDB, 0 → 0x00. Fijate en 128 (0x80): negarlo da el mismo byte. ¿Por qué 0 no cambia?
Paso 4: sumar con signo, acarreo y desborde
def suma8(a, b):
ua, ub = a & 0xFF, b & 0xFF
total = ua + ub
r = total & 0xFF
acarreo = total >> 8
sr = r - 256 if r >= 128 else r
desborde = (a >= 0) == (b >= 0) and (sr >= 0) != (a >= 0)
marca = "SI" if desborde else "no"
print(f"{a:>4} + {b:>4} = {sr:>4} 0x{ua:02X}+0x{ub:02X}=0x{r:02X} acarreo={acarreo} desborde={marca}")
suma8(5, -3)
suma8(100, 50)
suma8(127, 1)
suma8(-128, -1)
suma8(-70, -70)
suma8(-1, 1)
5 + (−3) tiene acarreo y no desborda. 100 + 50 desborda y no tiene acarreo. Son dos cosas distintas. ¿Hay alguna suma con ambas a la vez?
Paso 5: extensión de signo y valores de dos bytes
def extender(v, de=8, a=16):
m = (1 << a) - 1
s = v - (1 << de) if v >> (de - 1) else v
print(f"0x{v:0{de // 4}X}: sin signo -> 0x{v:0{a // 4}X} ({v}), "
f"con signo -> 0x{s & m:0{a // 4}X} ({s})")
for v in (0x25, 0xA5, 0xFF, 0x80):
extender(v)
def leer16(alto, bajo):
raw = (alto << 8) | bajo
return raw - 65536 if raw & 0x8000 else raw
print(leer16(0xFF, 0x38), leer16(0x01, 0x2C))
print(int.from_bytes(bytes([0xFF, 0x38]), "big", signed=True))
print(leer16(0xFF, 0x5E) / 16, "grados (DS18B20)")
0xFF38 vale −200 y 0x012C vale +300. Un DS18B20 que entrega 0xFF5E marca −10,125 °C. ¿Qué pasa si leés el mismo par sin signo?
Paso 6: BCD, conversión y suma con corrección
def a_bcd(n):
if not 0 <= n <= 99:
raise ValueError("un byte BCD guarda 0..99")
return ((n // 10) << 4) | (n % 10)
def de_bcd(b):
hi, lo = b >> 4, b & 0xF
return None if hi > 9 or lo > 9 else hi * 10 + lo
def suma_bcd(a, b):
s = a + b
if (a & 0xF) + (b & 0xF) > 9:
s += 0x06
if s > 0x99:
s += 0x60
res, acarreo = s & 0xFF, s >> 8
print(f"{a:02X} + {b:02X} (BCD) = {acarreo}{res:02X} -> {de_bcd(res)} con acarreo {acarreo}")
for n in (25, 9, 59, 99):
print(n, "->", f"0x{a_bcd(n):02X}")
print(de_bcd(0x59), de_bcd(0xA5))
suma_bcd(0x25, 0x17)
suma_bcd(0x38, 0x29)
suma_bcd(0x99, 0x01)
0x25 + 0x17 = 0x42 y 0x99 + 0x01 = 0x00 con acarreo. Probá suma_bcd(0x59, 0x01): es lo que hace un reloj al pasar de 59 a 60 segundos.
Paso 7: texto, dígitos y mayúsculas
texto = "Hola 42!"
datos = texto.encode("ascii")
print(" ".join(f"{c:02X}" for c in datos))
# de texto a número, como haría un microcontrolador al recibir por UART
v = 0
for c in b"1234":
v = v * 10 + (c - 0x30)
print(v, "y de vuelta:", str(v).encode().hex(" "))
# un solo bit separa mayúscula y minúscula
for ch in "aG":
print(ch, f"0x{ord(ch):02X}", "^0x20 ->", chr(ord(ch) ^ 0x20))
# fuera de ASCII: la misma letra, dos codificaciones
print("ñ".encode("latin-1").hex(), "ñ".encode("utf-8").hex())
«Hola 42!» son los bytes 48 6F 6C 61 20 34 32 21. El 1234 llega como 31 32 33 34 y se reconstruye. ‘ñ’ ocupa un byte en Latin-1 (F1) y dos en UTF-8 (C3 B1).
Paso 8: código Gray
def a_gray(n):
return n ^ (n >> 1)
def de_gray(g):
n = 0
while g:
n ^= g
g >>= 1
return n
previo = 0
for n in range(8):
g = a_gray(n)
cambios = bin(g ^ previo).count("1")
print(f"{n} {n:03b} -> Gray {g:03b} bits que cambian: {cambios} vuelta: {de_gray(g)}")
previo = g
En Gray siempre cambia un único bit entre filas consecutivas (salvo la vuelta del 7 al 0, que también cambia uno). Comparalo con binario: de 3 a 4 cambian tres.
10 · Analogía de programador
Tipos, casts y encodings
Todo lo que viste en este tema ya existe en tu lenguaje favorito, con otros nombres. Ponerlos en correspondencia es lo que vuelve intuitiva la electrónica.
| En software | En electrónica digital |
|---|---|
uint8_t vs int8_t | El mismo byte, leído sin signo o en complemento a dos |
(int8_t)0xA5 (cast) | No cambia ningún bit: solo cambia el convenio de lectura |
Promoción de char a int (printf("%x", (char)0xA5) muestra ffffffa5) | Extensión de signo: copiar el MSB en los bits nuevos |
int.from_bytes(b, "big", signed=True) | Armar un valor de 16 bits con signo desde dos registros de 8 |
c - '0' y atoi / itoa | Restar o sumar 0x30 entre el carácter y el valor del dígito |
str.encode("utf-8") | Elegir la tabla que convierte letras en bytes |
| Un número guardado como string «25» | BCD: legible para mostrar, incómodo para operar |
INT_MAX + 1 (comportamiento indefinido o envuelto) | Desborde con signo: el resultado da la vuelta |
- 1Ca2 = int8_t.
Mismo byte que
uint8_t, otra lectura. El tipo vive en el datasheet y en tu código; el hardware no lo guarda. - 2BCD = formato de presentación.
Como conservar «25» como cadena de dígitos: cada dígito es independiente y se muestra directo, pero para operar hay que convertir.
- 3ASCII = encoding.
Una tabla acordada entre emisor y receptor. Sin acuerdo, una UART que manda ‘A’ recibe cualquier cosa.
La diferencia con un lenguaje de alto nivel es que ahí hay tipos, conversiones explícitas y a veces advertencias del compilador. En el hardware solo hay cables y un convenio por escrito: si el convenio se pierde, los bits siguen siendo bits perfectamente válidos, solo que sin sentido.
11 · Comprensión
Confusiones frecuentes
«El bit alto es el signo, el resto es el valor»
Eso es signo-magnitud, que casi no se usa. En complemento a dos el MSB pesa −128 y participa en el valor: 1000 0001 vale −127, no «−1». Si ves −1 es que lo leíste como signo-magnitud.
«Para negar un número alcanza con invertir los bits»
Eso es complemento a uno: da −x − 1. Falta sumar 1. Invertir 0x25 da 0xDA = −38; negar 0x25 da 0xDB = −37.
«BCD 0x25 es el número 37»
BCD 0x25 se lee «25»; en binario puro 0x25 es 37. Mezclarlos da horas imposibles. Siempre hay que saber qué convenio usa el registro.
«Acarreo y desborde son lo mismo»
El acarreo mira la suma sin signo (se salió un bit por la izquierda); el desborde mira la suma con signo (el resultado cambió de signo sin motivo). 5 + (−3) acarrea y no desborda; 100 + 50 desborda y no acarrea.
«Un byte con signo se amplía rellenando con ceros»
Solo si no tiene signo. Con signo hay que copiar el MSB: 0xA5 pasa a 0xFFA5 (−91), no a 0x00A5 (165). Es el bug típico al armar un valor de 16 bits desde dos registros.
«ASCII son 8 bits»
ASCII son 7 bits (0…127). Lo que pasa de 127 depende de la extensión (Latin-1, Windows-1252) o de UTF-8, donde los bytes ≥ 0x80 forman parte de caracteres de varios bytes.
«Si leo «5» por la UART tengo el número 5»
Tenés el byte 0x35 (53). Para obtener 5 hay que restar 0x30. Con varios dígitos, además hay que reconstruir el valor sumando en base 10.
12 · Práctica guiada
Ejercicios de convenios
Ejercicio 1: negar a mano
Representá −1, −37 y −128 en 8 bits con complemento a dos. Verificá con a_ca2() y negar8(). ¿Qué pasa si intentás negar −128?
Ver solución razonada
0xFF, 0xDB y 0x80. Para −37: 37 = 0x25 = 0010 0101 → invertir 1101 1010 (0xDA) → +1 = 1101 1011 (0xDB). Negar 0x80 devuelve 0x80: no existe +128 en 8 bits con signo, y por eso el rango es asimétrico.
Ejercicio 2: leer con signo
Interpretá como int8_t los bytes 0xF6 y 0x8C. Calculalo de dos formas: sumando pesos con el MSB en −128, y negando para obtener la magnitud.
Ver solución razonada
−10 y −116. 0xF6 = 1111 0110 = −128 + 64 + 32 + 16 + 4 + 2 = −10; comprobación: invertir da 0x09, +1 = 0x0A = 10. 0x8C = 1000 1100 = −128 + 8 + 4 = −116; invertir da 0x73 = 115, +1 = 116.
Ejercicio 3: detectar el desborde
Calculá en 8 bits con signo 90 + 60 y (−70) + (−70). ¿Cuál es el resultado de cada una, el acarreo y si hay desborde? Verificá con suma8().
Ver solución razonada
90 + 60 = 0x5A + 0x3C = 0x96 = −106: sin acarreo, con desborde (dos positivos dan un negativo). (−70) + (−70) = 0xBA + 0xBA = 0x174 → byte 0x74 = +116, con acarreo y con desborde (dos negativos dan un positivo). Un código que suma sensores con signo en un int8_t debe ampliar el ancho o comprobar el rango.
Ejercicio 4: el reloj en BCD
Un RTC marca 0x23 horas, 0x59 minutos y 0x59 segundos. ¿Qué hora es? ¿Qué hora vería tu programa si lo leyera en binario puro? ¿Qué valor BCD tiene el registro de segundos un instante después?
Ver solución razonada
Es 23:59:59. En binario puro serían 35, 89 y 89: una hora que no existe. Un segundo después, el RTC suma 1 al registro de segundos con corrección: 0x59 + 1 = 0x5A → +6 = 0x60 → el registro llega a 60, que vuelve a 0x00 con acarreo al minuto; las horas se reinician y todo queda en 00:00:00.
Ejercicio 5: un sensor de temperatura
Un DS18B20 entrega 16 bits en complemento a dos con resolución de 1/16 °C. Primero llega el byte bajo y después el alto. Recibís 0x5E y 0xFF, y más tarde 0x91 y 0x01. ¿Qué temperaturas son?
Ver solución razonada
Se arma el valor con el alto primero: 0xFF5E y 0x0191. Con signo, 0xFF5E = 65 374 − 65 536 = −162 → −162 / 16 = −10,125 °C. 0x0191 = 401 → 401 / 16 = 25,0625 °C. Si hubieras leído 0xFF5E sin signo habrías obtenido 4085 °C: el bug del invierno que vimos al principio.
Ejercicio 6: texto por la UART
Llegan los bytes 0x31 0x32 0x33. ¿Qué número se transmitió? ¿Y cómo se mandaría «−5»? Después alternás el caso de ‘g’ (0x67) con una sola operación.
Ver solución razonada
0x31 0x32 0x33 son los caracteres «123»: 123, reconstruido con v = v * 10 + (c - 0x30). «−5» se manda como 0x2D 0x35 (el signo menos es un carácter, ‘-’ = 0x2D). Para ‘g’: 0x67 ^ 0x20 = 0x47 = ‘G’.
13 · Síntesis
Ideas para recordar
- Un byte no trae su significado: lo da el convenio. 0x25 = 37 = +37 = BCD «25» = ‘%’.
- Complemento a dos: el MSB pesa −128; negar = invertir + 1; rango −128…+127 con un único cero.
- El mismo sumador sirve con y sin signo; restar es sumar el opuesto.
- Extensión de signo: para ampliar un valor con signo se copia el MSB, no ceros.
- Acarreo = no cupo sin signo; desborde = no cupo con signo (mismo signo, resultado de signo contrario).
- BCD: un dígito por nibble; A…F son inválidos; suma con corrección +6.
- ASCII: 7 bits; ‘0’ = 0x30, ‘A’ = 0x41, ‘a’ = 0x61; mayúscula y minúscula difieren en el bit 0x20.
- Gray cambia un bit por paso; punto fijo guarda decimales como entero escalado.
- Mezclar convenios es un bug silencioso: siempre hay que anotar cuál se usa.
¿Listo para el Tema 6?
Podés avanzar si sos capaz de responder esto sin mirar:
- Escribir −50 en complemento a dos de 8 bits.
- Leer 0xC8 como número sin signo y con signo.
- Decir por qué 127 + 1 en un
int8_tda −128 y cómo detectarlo. - Convertir 0xFF a 16 bits con signo y sin signo.
- Explicar por qué 0x59 + 1 en BCD no es 0x5A.
- Convertir el carácter ‘7’ en el número 7 y el 7 en el carácter ‘7’.
Ya sabés qué significan los bits. Ahora hay que empezar a decidir con ellos: en el próximo tema, tablas de verdad, el lenguaje con el que se describe qué hace un circuito digital.