Skip to content
Volver al blog
Tutoriales

Coma fija Q15: conversión, redondeo y desbordamiento

0,1 en Q15 se almacena como 3277 y 1,0 salta a -1,0 si no saturas. Aprende a leer la notación Q, convertir a mano y comprobarlo en línea gratis.

15 min de lectura

Coma fija Q15: conversión, redondeo y desbordamiento

La coma fija Q15 (o punto fijo, según la región) guarda una fracción dentro de un entero con signo de 16 bits corriente, con una escala binaria implícita de 2^15 = 32768. Codificar son dos pasos: una multiplicación y un redondeo.

raw = round(value × 32768)

Decodificar es una sola división:

value = raw ÷ 32768

Ahí termina toda la aritmética. 0.5 × 32768 = 16384, así que 0.5 vive en memoria como el entero 16384, y 16384 ÷ 32768 devuelve exactamente 0.5. No hay campo de exponente ni bit implícito. La coma binaria es un acuerdo entre tú y quien lea la palabra; el hardware nunca ve otra cosa que un int16.

De esa fórmula salen dos consecuencias directas, y en cualquiera de las dos se pierde una tarde entera.

0.1 × 32768 = 3276.8, que no es un entero. Q15 se queda con 3277, y el valor que lees de vuelta es 0.100006103515625, no 0.1.

1.0 × 32768 = 32768, uno más que el mayor entero con signo de 16 bits. Es decir, 1.0 no tiene ninguna codificación en Q15, y lo que haga tu código al respecto depende de una política que casi ningún proyecto deja por escrito. Si saturas, obtienes 0.999969482421875. Si envuelves, obtienes -1.0.

Esos dos fallos son el tema del resto del artículo: cómo leer la notación Q cuando dos hojas de datos se contradicen, cómo convertir a mano en ambos sentidos, y cómo averiguar qué regla de redondeo y de desbordamiento eligió tu cadena de herramientas sin avisarte.

Q15, Q1.15, Qm.n: ¿son la misma disposición?

Pregunta a tres referencias qué significa Q15 y puedes recibir tres respuestas distintas. No las estás leyendo mal. La notación nunca llegó a estandarizarse de verdad, y la discrepancia es por un solo bit.

Cómo se lee Qm.n

La forma de dos números es la honesta. En Qm.n, n es la cantidad de bits fraccionarios y m la de bits enteros. Este artículo cuenta la posición del signo dentro de m, que es la lectura con la que Q16.16 da una palabra de 32 bits: 16 bits enteros contando el signo, 16 bits fraccionarios y escala 2^16 = 65536. Codifica 1.5 en Q16.16 y obtienes 1.5 × 65536 = 98304, en hexadecimal 0x00018000, sin ningún error de redondeo.

El lío empieza con el bit de signo. Algunos autores lo cuentan dentro de m y otros lo suman aparte. Con la primera convención, Q1.15 es una palabra de 16 bits: una posición de signo/entero más 15 bits de fracción. Con la segunda, la misma etiqueta describe 17 bits, que no existen en ninguna máquina.

Por qué la misma etiqueta Q15 significa anchos distintos según el documento

La forma de un solo número, Q15, elimina m y deja el ancho implícito. En la práctica del DSP casi siempre significa una palabra con signo de 16 bits en complemento a dos con 15 bits fraccionarios, y ese es el sentido que los ecosistemas de TI y ARM fijaron hace décadas.

Pero vas a encontrar explicaciones muy copiadas que dicen algo así: Q15 significa 15 bits fraccionarios, así que si definimos un número de 32 bits, hay 1 bit de signo y 16 bits enteros. Lee esa frase con calma y verás que suma el bit de signo por encima de m en vez de contarlo dentro: la otra convención, no la que usamos aquí. La disposición que describe es real, y lo normal es escribirla Q16.15. Lo que está mal es la etiqueta que le pone: una palabra de 32 bits con 16 bits enteros no es Q15 con ninguna de las dos convenciones. Ese mismo párrafo se ha reproducido en tantos blogs que hoy posiciona por encima de la definición correcta en algunas búsquedas.

Trata un Q15 a secas en un documento desconocido como una hipótesis pendiente de confirmar. Contrástalo con algo medible: el ancho del registro en el mapa de memoria, el tipo de C en la cabecera del driver o un valor de muestra conocido.

La única descripción que sobrevive al contacto con el código ajeno

Anota tres cosas y la ambigüedad desaparece:

  • Signo: con signo en complemento a dos, o sin signo
  • W: bits totales de la palabra
  • F: bits fraccionarios

con signo, W=16, F=15 no se puede malinterpretar. Tampoco sin signo, W=32, F=16. Todo lo demás se deriva: la escala es 2^F, la resolución es 2^-F y el rango es el rango entero de la palabra dividido entre 2^F. Pon esos tres valores en el documento del protocolo y en el comentario de la struct y no vuelves a tener esta discusión.

El conversor de formato Q en línea imprime el signo, W, F y la escala junto a cada resultado justo por eso. Cuando una hoja de datos dice «Q15» y el parser de un colega dice otra cosa, decodificar una palabra conocida zanja el asunto en unos segundos.

La fórmula de conversión, hecha a mano

Ambos sentidos son lo bastante cortos como para hacerlos en papel, algo que importa cuando estás mirando un volcado hexadecimal en la pantalla de un osciloscopio.

De flotante a fijo: round(x × 2^F)

Llevemos 0.5 a Q15.

  1. Escalar: 0.5 × 32768 = 16384
  2. Redondear: ya es un entero, así que 16384 se queda tal cual
  3. Comprobar el rango: con signo de 16 bits abarca de -32768 a 32767, y 16384 cabe
  4. Almacenar: 16384, en hexadecimal 0x4000, en binario 0100000000000000

Solo el paso 2 puede perder información y solo el paso 3 puede fallar. Todo lo interesante de la coma fija ocurre en uno de esos dos sitios.

De fijo a flotante: raw ÷ 2^F

Ahora al revés, partiendo de una captura de registro que muestra 0xC000 en un campo Q15 con signo.

  1. Interpretar el texto como un código sin signo de 16 bits: 0xC000 = 49152
  2. El formato es con signo y el bit superior está activo, así que restamos 2^16: 49152 - 65536 = -16384
  3. Escalar hacia abajo: -16384 ÷ 32768 = -0.5

El paso 2 es el que se salta la gente. Sin la corrección de complemento a dos, 0xC000 se lee como +1.5, que ni siquiera está dentro del rango de Q15: una señal de alarma muy útil. Si un valor decodificado cae fuera del rango del formato, casi seguro que olvidaste el paso del signo.

from fractions import Fraction


def encode_q15(x):
    """Decimal value -> signed Q15 stored integer (ties to even)."""
    return round(Fraction(str(x)) * 32768)


def decode_q15(word):
    """Unsigned 16-bit code -> exact Q15 value."""
    if word & 0x8000:
        word -= 0x10000
    return Fraction(word, 32768)


print(encode_q15(0.5))              # 16384
print(encode_q15(0.1))              # 3277
print(float(decode_q15(0xC000)))    # -0.5
print(float(decode_q15(0x0CCD)))    # 0.100006103515625

Fraction está haciendo trabajo de verdad aquí. Escalar pasando primero por un float reintroduciría el redondeo binario justo en el momento en que intentas medirlo, y Fraction(str(x)) lee el literal decimal que escribiste en lugar del double más cercano a él.

Leer y escribir la palabra hexadecimal

El hexadecimal es la forma en que estos valores aparecen realmente en las vistas de registros, y la conversión es mecánica: 3277 en hexadecimal es 0xCCD, rellenado hasta W/4 dígitos como 0x0CCD. Rellena siempre. Una palabra Q15 son cuatro dígitos hexadecimales y una Q31 son ocho; quitar el cero inicial es la forma habitual de que un valor quede desalineado en un decodificador por lotes.

Guarda también los códigos crudos sin signo en hexadecimal. 0x8000 es la palabra Q15 más negativa, no +32768, y escribirla como -0x8000 no ayuda a nadie. El orden de bytes es otra cuestión, porque el formato Q especifica el escalado numérico y no dice nada sobre endianness: un volcado little-endian de 0x0CCD llega como los bytes CD 0C. Para cambios de base a secas mientras lees un volcado, el conversor de bases numéricas maneja binario, octal y hexadecimal sin escala ni ancho con signo de por medio.

Q7, Q15, Q31 y Q16.16: rango y resolución

Cada número de esta tabla es -2^(W-1) dividido entre 2^F en un extremo y (2^(W-1) - 1) dividido entre 2^F en el otro. Los valores son exactos, no están redondeados para mostrarlos.

FormatoAnchoBits fraccionarios FEscala 2^FMínimoMáximo (exacto)Resolución
Q787128-1127/128 = 0.99218751/128 = 0.0078125
Q15161532768-132767/32768 = 0.9999694824218751/32768 = 0.000030517578125
Q3132312147483648-1(2^31−1)/2^31 = 0.99999999953433871269226074218751/2^31 ≈ 4.6566128730773926e-10
Q16.16321665536-32768(2^31−1)/65536 = 32767.99998474121093751/65536 = 0.0000152587890625

Pega cualquiera de estos valores límite en el conversor de formato Q y te devolverá el entero almacenado, la palabra hexadecimal y el decimal exacto. Es la comprobación que conviene hacerle a una constante de firmware antes de que salga a producción.

Por qué el tope del rango de Q15 es 0.999969482421875

El rango del formato Q15 es asimétrico, y la asimetría viene del complemento a dos, no de nada propio de la coma fija. Una palabra con signo de 16 bits cubre los enteros de -32768 a 32767. Divide ambos extremos entre 32768 y el rango pasa a ser de -32768/32768 a 32767/32768, es decir, de -1 a 0.999969482421875.

Así que a 1.0 le falta exactamente un LSB. El valor sencillamente no tiene codificación en este formato, por mucho que se lea por ahí que el máximo es «aproximadamente 1.0». Un conversor que informe 1.0 en Q15 sin decir que saturó te está mintiendo.

Por qué -1 sí está incluido

Muchas referencias escriben el rango de Q15 como -1 < X < 0.9999695, con intervalo abierto en los dos lados. La cota inferior está mal. -32768 ÷ 32768 = -1 exactamente, así que -1 sí es representable, y los dos extremos de [-1, 0.999969482421875] son valores alcanzables. También verás el rango escrito como [-1, 1), que dice lo mismo pero medido contra el fondo de escala: 1.0 queda fuera de alcance, y el mayor valor al que sí se llega es 0.999969482421875, no algo que se quede corto.

Esto importa mucho más que una discusión de notación. -1 es el valor que rompe la multiplicación, porque -1 × -1 = 1 y 1 está fuera de rango. Quien dé por hecho que -1 es inalcanzable no escribirá la rama de saturación que lo atrapa.

La cota superior truncada 0.9999695 de esas mismas referencias es un artefacto de presentación. El valor no tiene nada de periódico: es 32767/32768, y 32768 es una potencia de dos, así que el desarrollo decimal termina a los 15 dígitos en 0.999969482421875.

0.1 no cabe en Q15

Escálalo y el problema se ve enseguida: 0.1 × 32768 = 3276.8. La coma fija solo puede guardar enteros, así que algo tiene que ceder.

Con redondeo al más cercano, la palabra almacenada es 3277, en hexadecimal 0x0CCD. El valor que esa palabra representa es:

3277 ÷ 32768 = 0.100006103515625

El error de cuantización es la diferencia entre lo que pediste y lo que la rejilla pudo darte:

0.100006103515625 - 0.1 = +0.000006103515625

Son unas seis millonésimas, o una quinta parte de un LSB. En un control de volumen eso no lo nota nadie. En un integrador que vuelve a sumar el mismo valor mil veces por segundo sí, porque el sesgo se acumula en una deriva de aproximadamente 0.006 por segundo.

El error en coma fija es uniforme; en coma flotante no

Q15 tiende una rejilla de 65536 puntos separados exactamente 1/32768 a lo largo de [-1, 0.999969482421875]. El espaciado cerca de 0.9 es el mismo que cerca de 0.0001, así que el peor error absoluto es medio LSB en todas partes. Eso vuelve aburrido el análisis de error, en el mejor sentido: puedes acotar el piso de ruido de un filtro con una aritmética que cualquier ingeniero junior puede verificar.

IEEE 754 hace lo contrario. Mantiene un número fijo de bits significativos y mueve el exponente, así que la distancia absoluta entre doubles vecinos crece con la magnitud mientras el error relativo se mantiene casi constante. Cerca de 1.0 esa distancia es de unos 2.2e-16; cerca de 1e12 es de unos 0.0001220703125.

Mismo fallo, distinta forma. Un double tampoco puede guardar 0.1. Almacena 0.1000000000000000055511151231257827021181583404541015625, que es la razón por la que 0.1 + 0.2 devuelve 0.30000000000000004. Ese caso está desarrollado en el artículo complementario sobre la precisión de la coma flotante. La coma fija no resuelve las fracciones decimales en binario; solo hace que el tamaño del error sea predecible.

Tres modos de redondeo, separados por un LSB

La regla de redondeo pertenece al contrato de datos, al mismo nivel que W y F. Dos implementaciones correctas que no se pongan de acuerdo en el redondeo producirán vectores de prueba que difieren en el último bit para siempre, y rastrear eso es un trabajo miserable.

ModoRegla10911.744-3276.8
Redondeo al más cercano, empates al parEl punto de rejilla más cercano; las mitades exactas van al entero par10912-3277
Truncar hacia ceroDescarta la fracción, la magnitud solo puede encoger10911-3276
Piso hacia menos infinitoSiempre hacia abajo en la recta numérica10911-3277

Las dos columnas muestran por qué un solo ejemplo nunca basta. Con un valor positivo, truncar y piso coinciden. Con un valor negativo se separan un LSB entero, porque el truncamiento tira de -3276.8 hacia cero y el piso lo empuja hacia abajo.

El ejemplo resuelto en el que todo el mundo tropieza

0.333 en Q15 es un buen caso de prueba porque queda cerca de una frontera. 0.333 × 32768 = 10911.744.

  • Truncar: 10911, que se lee de vuelta como 0.332977294921875
  • Redondear al más cercano: 10912, que se lee de vuelta como 0.3330078125

Los tutoriales antiguos imprimen 10911 y siguen adelante sin decir qué regla lo produjo. Lleva ese número a un proyecto cuyo codificador redondea y tus vectores de referencia fallan en la primera ejecución, con una diferencia de una unidad que parece una errata en vez de una discrepancia de política.

Con los negativos es donde se separan las tres reglas. -0.1 × 32768 = -3276.8 da -3276 con truncamiento (valor -0.0999755859375) y -3277 con piso o con el más cercano (valor -0.100006103515625). El truncamiento y el más cercano tratan igual a los dos signos, ±3276 y ±3277 respectivamente, así que un par de coeficientes simétrico sigue siendo simétrico. El piso no: manda +0.1 a 3276 pero -0.1 a -3277, y el par sale descuadrado en una unidad.

Qué hace tu lenguaje por omisión

Ninguno de estos comportamientos por omisión está mal; simplemente son distintos y ninguno se anuncia.

  • En C y C++, convertir un valor de coma flotante a un tipo entero trunca hacia cero, así que (int16_t)(0.333f * 32768) da 10911.
  • La función round() integrada de Python usa empates al par, así que round(3276.8) es 3277 y round(2.5) es 2.
  • Math.round de JavaScript resuelve los empates hacia más infinito, lo cual no es simétrico: Math.round(2.5) es 3 pero Math.round(-2.5) es -2.
  • Muchas rutas de multiplicación-acumulación de DSP redondean en el desplazamiento y ofrecen el más cercano al par como bit de modo, y por eso el modelo de referencia en C y el silicio pueden discrepar hasta que alguien lee el registro de modo.

Elige una regla, escríbela en la especificación del formato junto a W y F, y haz que los vectores de prueba la lleven consigo.

Desbordamiento: saturación frente a envolvente

Un desbordamiento en Q15 hace una de tres cosas: rechazar el valor, recortarlo a 0.999969482421875 o envolverlo hasta -1.0. Cuál de ellas te toca es una política que eligió tu cadena de herramientas, y la tercera opción invierte el signo en silencio.

Toma 1.0. Al escalar queda 1.0 × 32768 = 32768, y el mayor entero con signo de 16 bits es 32767. El valor se sale de rango por exactamente uno.

PolíticaPalabra almacenadaValor leído de vueltaCómo se ve aguas abajo
Errorningunaconversión rechazadaRuidoso, capturable, casi siempre lo correcto para herramientas
Saturar0x7FFF = 327670.999969482421875Indistinguible de 1.0 al oído o a la vista
Envolver0x8000 = -32768-1.0Inversión de signo a fondo de escala

La fila de saturación pierde 0.000030517578125 y nadie se entera. La fila de envolvente convierte una muestra positiva a fondo de escala en una negativa a fondo de escala, y en una cadena de audio eso es un chasquido que se oye desde el otro lado de la sala. En un lazo de control es una orden a fondo de escala en la dirección equivocada.

La propiedad peligrosa de la envolvente es que produce una palabra de aspecto perfectamente válido. 0x8000 es una codificación Q15 legítima de -1.0. Nada aguas abajo puede distinguirla de una muestra genuina de -1.0, así que no queda ninguna firma que buscar después; solo lo descubres instrumentando el punto exacto donde ocurrió el desbordamiento.

Por qué el silicio de DSP trae instrucciones con saturación

La saturación es el comportamiento que quiere el procesamiento de señales, así que los procesadores lo implementan en lugar de dejarlo en manos de una bifurcación. ARM tiene QADD/QSUB y las instrucciones de desplazamiento con saturación SSAT/USAT, NEON tiene VQADD y compañía, y SSE de x86 tiene sumas empaquetadas con saturación como paddsw. Las familias C6000 y C55x de TI exponen la saturación como un bit de modo en la ruta del acumulador.

En un filtro o en un mezclador, una muestra recortada ocasional es una distorsión local pequeña. Una muestra envuelta es una discontinuidad con energía repartida por todo el espectro. El hardware elige por omisión lo ligeramente incorrecto antes que lo catastróficamente incorrecto, pero solo si lo activaste, y la aritmética entera de C a secas en ese mismo chip sigue envolviendo.

Por qué una multiplicación en Q15 necesita un desplazamiento de 15 bits a la derecha

Multiplica dos palabras Q15 como enteros y el resultado es correcto, pero ya no es Q15. Los bits fraccionarios se suman al multiplicar: Q15 × Q15 da Q30.

Sigamos paso a paso 0.5 × 0.5, donde la respuesta correcta es evidentemente 0.25:

16384 × 16384 = 268435456          ← this is Q30, not Q15
268435456 ÷ 2^30 = 0.25            ← read as Q30, correct
268435456 >> 15 = 8192             ← realign to Q15
8192 ÷ 32768 = 0.25                ← same answer, back in Q15

Interpreta 268435456 como Q15 y leerías 8192.0, desviado por un factor de 32768. Ese factor es todo el bug, y explica por qué los filtros de coma fija que «casi funcionan» suelen estar desviados por una potencia de dos.

El producto también necesita espacio. Dos valores de 16 bits se multiplican hasta dar 32 bits, así que el intermedio tiene que ser int32_t. Acumular muchos productos necesita todavía más margen, y por eso los acumuladores de DSP tienen 40 bits en piezas como el C55x.

Redondea el desplazamiento, no te limites a tirar los bits

Un >> 15 pelado descarta los 15 bits bajos, lo que equivale a truncar hacia menos infinito para valores con signo. Sumar antes medio LSB de la precisión saliente lo convierte en redondeo al más cercano:

#include <stdint.h>
#include <stdio.h>

static int16_t sat_q15(int32_t v) {
    if (v >  32767) return  32767;
    if (v < -32768) return -32768;
    return (int16_t)v;
}

static int16_t mul_q15(int16_t a, int16_t b) {
    int32_t prod = (int32_t)a * (int32_t)b;   /* Q30 */
    int32_t back = (prod + (1 << 14)) >> 15;  /* round, then Q30 -> Q15 */
    return sat_q15(back);
}

int main(void) {
    printf("0.5*0.5   -> %d\n", mul_q15(16384, 16384));
    printf("0.1*0.1   -> %d\n", mul_q15(3277, 3277));
    printf("-1*-1     -> %d\n", mul_q15(-32768, -32768));
    printf("no-round  -> %d\n", (int)(((int32_t)3277 * 3277) >> 15));
    return 0;
}

Compilado con cc -std=c11 -Wall -o q15 q15.c && ./q15, esto imprime:

0.5*0.5   -> 8192
0.1*0.1   -> 328
-1*-1     -> 32767
no-round  -> 327

La tercera línea es el caso de -1 de antes. -32768 × -32768 = 1073741824, que es 1.0 en Q30 y queda fuera de rango para Q15, así que sat_q15 lo recorta a 32767. Quita el recorte y la conversión a int16_t lo envuelve a -32768, convirtiendo -1 × -1 en -1.

Las dos últimas líneas son la diferencia de redondeo. 3277 × 3277 = 10738729, y un desplazamiento pelado da 327 (0.009979248046875) mientras que el desplazamiento redondeado da 328 (0.010009765625). El producto verdadero es 0.01, así que la versión redondeada queda más del doble de cerca. Todo eso cuesta una suma más.

Una advertencia sobre el desplazamiento en sí: desplazar a la derecha un entero con signo negativo queda definido por la implementación en C antes de C23, aunque todos los compiladores que te vas a encontrar hacen un desplazamiento aritmético. Si eso te incomoda, divide entre 32768 y deja que el compilador emita el desplazamiento, o haz el desplazamiento sobre un tipo sin signo después de aplicar un sesgo. La guía de operaciones bit a bit cubre la mecánica general de desplazamientos y máscaras.

La suma exige primero que los valores Q coincidan

La multiplicación cambia el valor Q de forma predecible. La suma no tolera ninguna discrepancia: sumar una palabra Q7 a una palabra Q15 produce un disparate, porque los operandos no comparten escala.

Alinéalos primero con un desplazamiento. 0.5 en Q7 es 64, y 64 << 8 es 16384, que es 0.5 en Q15. La cantidad a desplazar es la diferencia de bits fraccionarios, 15 - 7 = 8.

Desplazar hacia arriba es exacto pero cuesta margen, ya que un valor Q7 promovido a Q15 necesita el contenedor más ancho. Desplazar hacia abajo pierde información y exige la misma decisión de redondeo que la multiplicación. En cualquier caso, escribe el valor Q de cada intermedio en un comentario. El código de coma fija donde las escalas viven solo en la cabeza del autor se vuelve inmantenible en menos de un mes.

Coma fija Q15 o coma flotante IEEE 754: cómo elegir

Los dos son sistemas binarios posicionales, así que en principio ninguno tiene ventaja de exactitud. La elección se reduce a lo que te cobra el hardware de destino y a qué garantías necesitas.

PreguntaApunta a coma fijaApunta a coma flotante
¿Hay una FPU por hardware?Sin FPU, o con una biblioteca de coma flotante por softwareFPU por hardware con operaciones de un ciclo
¿Qué tan amplio es el rango dinámico?Conocido y acotado, como el audio normalizadoAbarca muchos órdenes de magnitud
¿El formato de la interfaz define una escala?El protocolo o el registro fija la escala binariaEl campo es un flotante genuino
¿Los resultados deben ser exactos bit a bit entre compilaciones?Sí, los enteros se reproducen en todas partesSe tolera que varíen con FMA y con las optimizaciones
¿Hay poca memoria o poco ancho de banda?Las muestras de 16 bits reducen a la mitad la huella de los flotantes de 32 bitsNo es una restricción
¿Quién mantiene el código?Un equipo que ya domina la notación QUn equipo heterogéneo, donde los errores de escala son el riesgo mayor

La última fila no es una broma. La coma fija traslada el error del tiempo de ejecución a la fase de diseño, y ese cambio solo sale a cuenta cuando alguien está haciendo el trabajo de diseño. En un Cortex-M4F con FPU por hardware, el flotante de precisión simple suele ser la opción más rápida y más segura, y el reflejo tradicional de echar mano de Q15 es una costumbre heredada de piezas que ya no dominan el mercado.

Dónde toma el relevo IEEE 754

Cambia a coma flotante cuando la palabra misma lleve un signo, un exponente y una mantisa en lugar de una escala fija. Ese es el momento en que el formato Q deja de aplicar: no hay un único 2^F entre el que dividir, porque el exponente varía en cada valor.

Las dos representaciones se cruzan constantemente en la práctica. Los datos de un sensor llegan como palabras Q15 de registro, se promueven a flotante para un cálculo largo y vuelven a Q15 para el DAC. Para inspeccionar bit a bit la mitad en coma flotante de esa ruta, el conversor IEEE 754 descompone un valor en signo, exponente y mantisa e imprime el decimal exacto almacenado, el mismo trabajo que hace el conversor de formato Q para una escala fija.

Los dos se apoyan en el mismo cimiento

El formato Q, IEEE 754 y los enteros a secas leen todos los mismos bits con reglas distintas sobre dónde va la coma y si puede moverse. Si la parte del valor posicional te resulta poco firme, o quieres ganar velocidad leyendo 0x0CCD como 0000 1100 1100 1101 sin recurrir a una calculadora, la introducción a la conversión entre binario, hexadecimal y octal cubre las bases sobre las que se construyen ambos formatos.

Preguntas frecuentes sobre la coma fija Q15

¿Qué significa Q15?

En la convención habitual del DSP, Q15 es una palabra con signo de 16 bits en complemento a dos con 15 bits fraccionarios: una posición de signo y 15 posiciones de fracción. La escala es 2^15 = 32768, la resolución es 1/32768 = 0.000030517578125 y el rango va de -1 a 32767/32768. Como las etiquetas Q varían entre documentos, confirma el ancho total y el signo en lugar de fiarte solo de la etiqueta.

¿Q15 y Q1.15 son lo mismo?

Normalmente describen la misma disposición con signo de 16 bits, donde el 1 de Q1.15 cuenta la posición de signo. Pero la notación no es universal y algunos autores suman el bit de signo por encima de m en vez de contarlo dentro. La descripción fiable es el signo más los bits totales W más los bits fraccionarios F: para esta disposición, con signo, W=16, F=15.

¿Cuánto vale 0.5 en Q15?

0.5 en Q15 es el entero almacenado 16384, 0x4000 en hexadecimal. La aritmética es 0.5 × 32768 = 16384, que ya es un entero, así que no hay redondeo ni error de cuantización. Decodificar lo confirma: 16384 ÷ 32768 = 0.5 exactamente.

¿Cuáles son los valores máximo y mínimo de Q15?

El mínimo es -1, y sí está incluido, porque -32768 ÷ 32768 es exactamente -1. El máximo es 32767/32768 = 0.999969482421875. Los dos son alcanzables, así que el rango es [-1, 0.999969482421875]; el valor que se queda fuera es 1.0. Las referencias que escriben la cota inferior como un intervalo abierto están equivocadas, y ese error esconde el caso de desbordamiento de -1 × -1.

¿Qué pasa cuando Q15 se desborda?

Depende de la política vigente. Una política de error rechaza la conversión. La saturación recorta al extremo más cercano, así que 1.0 se convierte en 0.999969482421875, una pérdida de un LSB que suele ser inaudible. La envolvente aplica módulo 2^16, así que 1.0 escala a 32768, que se lee de vuelta como -32768 y por tanto como -1.0: una inversión de signo completa. El hardware de DSP usa saturación por omisión justo por esto, pero la aritmética entera de C a secas envuelve.

¿Por qué dos números Q15 necesitan un desplazamiento de 15 bits a la derecha después de multiplicarse?

Porque los bits fraccionarios se suman. Q15 × Q15 produce un producto Q30, así que el resultado entero arrastra 30 bits fraccionarios en lugar de 15. Desplazar 15 bits a la derecha lo realinea a Q15: 16384 × 16384 = 268435456, y 268435456 >> 15 = 8192, que decodifica a 0.25. Suma 1 << 14 antes del desplazamiento para redondear al más cercano en vez de truncar, y mantén el intermedio en un int32_t para que el producto de 32 bits no se desborde.

¿Cuándo conviene usar el formato Q en lugar de IEEE 754?

Usa el formato Q cuando un protocolo, un mapa de registros, un algoritmo de DSP o un códec ya haya fijado una escala binaria para una palabra entera: la escala forma parte de la interfaz y no te toca a ti elegirla. Usa IEEE 754 cuando el valor necesite un rango dinámico amplio, cuando el destino tenga FPU por hardware o cuando el campo guarde de verdad un signo, un exponente y una mantisa. Para un simple cambio de base sin escala asociada no aplica ninguno de los dos; eso es conversión de bases corriente.

Lo que conviene dejar por escrito

La coma fija Q15 es una multiplicación, una decisión de redondeo y una comprobación de rango. raw = round(value × 32768) a la entrada, value = raw ÷ 32768 a la salida. La fórmula es trivial; los fallos viven todos en las partes que nadie documenta.

Así que documéntalas. Escribe el signo, W y F junto a cada etiqueta Q en lugar de suponer que la etiqueta basta, y nombra ahí mismo el modo de redondeo, porque truncar y piso se separan un LSB entero con los negativos. Queda una tercera cosa, la que más caro sale callarse: si el desbordamiento da error, satura o envuelve. El caso de la envolvente convierte 1.0 en -1.0 y no deja ni una prueba detrás.

Cuando una palabra de registro y una hoja de cálculo no coincidan, decodifica un valor conocido en el conversor de formato Q: muestra el entero almacenado, el decimal exacto, el error de cuantización y el rango representable uno al lado del otro, todo calculado en el navegador. Con eso suele bastar para saber de quién era la suposición equivocada sobre W y F.

Etiquetas: fixed-point dsp embedded q-format number-representation

Artículos relacionados

Ver todos los artículos