Нотация Q — это способ указания параметров двоичного формата чисел с фиксированной точкой . Например, в нотации Q формат чисел, обозначенный как Q8.8
означает, что числа с фиксированной точкой в этом формате имеют 8 бит для целой части и 8 бит для дробной части.
Для той же цели использовался ряд других обозначений .
Обозначение Q, определенное Texas Instruments [1] , состоит из буквы Q, за которой следует пара цифр m.n , где m — количество бит, используемых для целой части значения, а n — количество бит дробной части.
По умолчанию нотация описывает знаковый двоичный формат с фиксированной точкой, при этом немасштабированное целое число хранится в формате дополнения до двух , используемом в большинстве двоичных процессоров. Первый бит всегда дает знак значения (1 = отрицательное, 0 = неотрицательное), и он не учитывается в параметре m . Таким образом, общее количество w используемых бит равно 1 + m + n .
Например, спецификация Q3.12 описывает знаковое двоичное число с фиксированной точкой с w = 16 битами в общей сложности, включая бит знака, три бита для целой части и 12 бит, которые являются дробью. То есть, 16-битное знаковое (дополнительное число) целое число, которое неявно умножается на коэффициент масштабирования 2 −12
В частности, когда n равно нулю, числа являются просто целыми числами. Если m равно нулю, все биты, кроме бита знака, являются битами дроби; тогда диапазон сохраненного числа составляет от −1,0 (включительно) до +1,0 (исключительно).
M и точка могут быть опущены, в этом случае они выводятся из размера переменной или регистра, где хранится значение. Таким образом, Q12 означает знаковое целое число с любым количеством бит, которое неявно умножается на 2 −12 .
Буква U может быть добавлена к Q для обозначения беззнакового двоичного формата с фиксированной точкой. Например, UQ1.15 описывает значения, представленные как беззнаковые 16-битные целые числа с неявным масштабным коэффициентом 2 −15 , которые находятся в диапазоне от 0,0 до (2 16 −1)/2 15 = +1,999969482421875.
Вариант нотации Q использовался ARM . В этом варианте число m включает бит знака. Например, 16-битное знаковое целое число будет обозначаться Q15.0
в варианте TI, но Q16.0
в варианте ARM. [2] [3]
Разрешение (разница между последовательными значениями) формата Q m . n или UQ m . n всегда равно 2 − n . Диапазон представимых значений зависит от используемой нотации:
Обозначение | Нотация Texas Instruments | Обозначение ARM |
---|---|---|
Подписано Q м . н | −2 м до +2 м − 2 − н | −2 м −1 до +2 м −1 − 2 − n |
Беззнаковый UQ m.n | 0 до 2 м − 2 − н | 0 до 2 м − 2 − н |
Например, формат числа Q15.1 требует 15+1 = 16 бит, имеет разрешение 2 −1 = 0,5, а представляемые значения находятся в диапазоне от −2 14 = −16384,0 до +2 14 − 2 −1 = +16383,5. В шестнадцатеричном формате отрицательные значения находятся в диапазоне от 0x8000 до 0xFFFF, за которыми следуют неотрицательные значения от 0x0000 до 0x7FFF.
Числа Q представляют собой отношение двух целых чисел: числитель сохраняется в памяти, знаменатель равен 2n .
Рассмотрим следующий пример:
Если основание числа Q должно быть сохранено ( n остается постоянным), математические операции числа Q должны сохранять постоянным знаменатель . Следующие формулы показывают математические операции над общими числами Q и . (Если мы рассмотрим пример, как указано выше, равно 384 и равно 256.)
Поскольку знаменатель представляет собой степень двойки, умножение можно реализовать как арифметический сдвиг влево, а деление — как арифметический сдвиг вправо; на многих процессорах сдвиги выполняются быстрее, чем умножение и деление.
Для сохранения точности промежуточные результаты умножения и деления должны иметь двойную точность, и необходимо соблюдать осторожность при округлении промежуточного результата перед обратным преобразованием в требуемое число Q.
Используя C, операции будут следующими (обратите внимание, что здесь Q относится к числу бит дробной части):
int16_t q_add ( int16_t a , int16_t b ) { return a + b ; }
С насыщенностью
int16_t q_add_sat ( int16_t a , int16_t b ) { int16_t result ; int32_t tmp ; tmp = ( int32_t ) a + ( int32_t ) b ; если ( tmp > 0x7FFF ) tmp = 0x7FFF ; если ( tmp < -1 * 0x8000 ) tmp = -1 * 0x8000 ; результат = ( int16_t ) tmp ; вернуть результат ; }
В отличие от плавающей точки ±Inf, насыщенные результаты не являются липкими и будут ненасыщаться при добавлении отрицательного значения к положительному насыщенному значению (0x7FFF) и наоборот в этой реализации, показанной на ассемблере. В языке ассемблера флаг Signed Overflow может использоваться для избежания приведения типов, необходимых для этой реализации C.
int16_t q_sub ( int16_t a , int16_t b ) { return a - b ; }
// предварительно вычисленное значение: #define K (1 << (Q - 1)) // насыщаем до диапазона int16_t int16_t sat16 ( int32_t x ) { if ( x > 0x7FFF ) return 0x7FFF ; else if ( x < -0x8000 ) return -0x8000 ; else return ( int16_t ) x ; } int16_t q_mul ( int16_t a , int16_t b ) { int16_t result ; int32_t temp ; temp = ( int32_t ) a * ( int32_t ) b ; // тип результата - тип операнда // Округление; средние значения округляются в большую сторону temp += K ; // Корректируем, разделив на основание и насыщая результат result = sat16 ( temp >> Q ) ; вернуть результат ; }
int16_t q_div ( int16_t a , int16_t b ) { /* предварительно умножаем на основание (масштабируем до Q16, чтобы результат был в формате Q8) */ int32_t temp = ( int32_t ) a << Q ; /* Округление: средние значения округляются вверх (вниз для отрицательных значений). */ /* ИЛИ сравниваем старшие биты, т. е. if (((temp >> 31) & 1) == ((b >> 15) & 1)) */ if (( temp >= 0 && b >= 0 ) || ( temp < 0 && b < 0 )) { temp += b / 2 ; /* ИЛИ сдвигаем 1 бит, т. е. temp += (b >> 1); */ } else { temp -= b / 2 ; /* ИЛИ сдвиг на 1 бит, т.е. temp -= (b >> 1); */ } return ( int16_t )( temp / b ); }