Программирование DSP VisualDSP: использование типов с фиксированной точкой Thu, March 28 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

VisualDSP: использование типов с фиксированной точкой Печать
Добавил(а) microsin   

В этом документе (перевод раздела "Using Native Fixed-Point Types" документации [1]) предоставлен обзор поддержки компилятором VisualDSP традиционных типов с фиксированной точкой (native fixed-point types) fract и accum, как это определено в Главе 4 "Extensions to support embedded processors" ISO/IEC draft document Technical Report 18037.

[Поддержка типов с фиксированной точкой]

Тип данных с фиксированной точкой это такой тип, у которого точка основания, разделяющая целую и дробную части, находится в фиксированной позиции внутри бит двоичного числа заданной разрядности. Это включает целые типы (точка находится непосредственно справа от самого младшего значащего бита числа). Однако в здесь используется термин "фиксированная точка" только к тем случаям представления дробных чисел, когда под дробную часть выделено не нулевое количество бит числа, т. е. есть в наличии биты, стоящие справа после точки. Могут быть также биты под целую часть числа, стоящие слева от точки.

У процессора Blackfin имеется встроенная аппаратная поддержка для арифметики на числах с такими типами данных, имеющих фиксированную точку. Например, процессор может выполнять сложение, вычитание и умножение на 16-битных и 32-битных дробных значениях. Однако язык C не предусматривает простого расширения семантики арифметики, которая позволит использовать поддержку нижележащей аппаратуры.

Чтобы упростить использование этой аппаратной возможности и применить расширение языка для алгоритмов DSP, которые манипулируют типами с фиксированной точкой, компилятор поддерживает набор типов native fixed-point, арифметика которых подчиняется семантике чисел с фиксированной точкой. Это упрощает написание высокопроизводительных алгоритмов для манипуляции данными с фиксированной точкой, без необходимости обращаться к встроенным функциям компилятора (compiler built-ins), или встраиваемому коду на языке ассемблера (inline assembly).

Появление стандарта для таких типов с плавающей точкой установлено в Главе 4 "Extensions to support embedded processors" ISO/IEC Technical Report 18037. VisualDSP++ предоставляет всю функциональность, обозначенную в этой главе, и эта глава является полезным справочником, объясняющим тонкости семантики библиотечных функций и арифметических операторов. Однако в следующих секциях дается обзор этих типов данных, семантики арифметики использования этих типов, и руководства о том, как писать высокопроизводительный код с использованием этих типов.

Традиционные типы с плавающей точкой (Native Fixed-Point Types). Два ключевых слова, _Fract и _Accum, используются для декларирования переменных, имеющих тип с фиксированной точкой. Каждое из этих ключевых слова может быть также использовано совместно со спецификаторами типа short и long, а также signed и unsigned. Таким образом, имеется 12 доступных типов с плавающей точкой, хотя некоторые из них являются псевдонимами для типов, имеющих такой же размер и формат.

Путем подключения заголовочного файла stdfix.h можно использовать более удобный синтаксис - fract и accum вместо _Fract и _Accum. Заголовочный файл stdfix.h также предоставляет прототипы многих полезных функций, и весьма рекомендуется подключать его в файлы исходного кода, которые используют типы с фиксированной точкой. Таким образом вся дискуссия, которая пойдет дальше, будет использовать семантику с ключевыми словами fract и accum, как это делает остальная часть документации VisualDSP++.

Форматы типов с фиксированной точкой даны в таблице 1-15. В столбце "Представление" таблицы число после точки показывает количество бит, отведенное под дробную часть, в то время как число слева от точки означает количество бит целой части числа, включая бит знака, который обозначается буквой s. Типы со знаком (signed) используют форму представления чисел с дополнением до 2 (как обычные числа со знаком). Также в этой таблице представлен диапазон значений, которые могут принимать эти числа. Обратите внимание, что нижнее значение диапазона указано включительно (обозначено квадратной скобкой), в то время как верхнее значение диапазона указано исключительно (т. е. не включая, что обозначено круглой скобкой). Таким образом, для положительного верхнего предела значений может быть представлено значение только на вес 1 бита меньше, чем указанное.

Таблица 1-15. Форматы хранения, диапазоны и размеры типов Native Fixed-Point.

Тип Представление Диапазон sizeof
short fract s1.15 [-1.0,1.0) 2
fract
long fract s1.31 4
unsigned short fract 0.16 [0.0,1.0) 2
unsigned fract
unsigned long fract 0.32 4
short accum s9.31 [-256.0,256.0) 8
accum
long accum
unsigned short accum 8.32 [0.0,256.0)
unsigned accum
unsigned long accum

Стандарт (Technical Report 18037) также определяет ключевое слово _Sat для квалификатора типа с фиксированной точкой (альтернативное, более удобное представление sat). Это предусматривает, что вся арифметика над переменной fixed-point с таким квалификатором должна использовать арифметику с насыщением, т. е. в случае переполнения результата арифметических операций число останется в виде самого максимального (или самого минимального) значения, которое может представить тип. Когда квалификатор sat не используется, стандарт говорит, что арифметические операции могут допускать переполнение, которое будет вести себя неопределенным (с точки зрения стандарта) образом, т. е. будет зависеть от платформы, на которой работает код. VisualDSP++ принимает квалификатор sat для совместимости, но будет всегда генерировать код, который будет насыщать результат операции независимо от того, был использован квалификатор sat или нет. Это дает максимальную воспроизводимость результатов и позволяет писать код, не беспокоясь о неожиданных эффектах переполнения.

Традиционные константы с фиксированной точкой (Native Fixed-Point Constants). Константы с фиксированной точкой могут быть заданы в том же формате, что и константы с плавающей точкой, включая любую десятичную или бинарную экспоненту. Для дополнительной информации об этих форматах см. врезку с описанием функции strtofxfx.

Для идентификации типа констант используются суффиксы. Заголовочный файл stdfix.h также декларирует макросы для максимальных и минимальных значений типов с фиксированной точкой. См. таблицу 1-16 для подробного описания суффиксов и максимальных и минимальных значений соответствующих типов с фиксированной точкой.

Таблица 1-16. Суффиксы и макросы для констант типа с фиксированной точкой.

Тип Суффикс Пример Min Max
short fract hr 0.5hr SFRACT_MIN SFRACT_MAX
fract r 0.5r FRACT_MIN FRACT_MAX
long fract lr 0.5lr LFRACT_MIN LFRACT_MAX
unsigned short fract uhr 0.5uhr 0.0uhr USFRACT_MAX
unsigned fract ur 0.5ur 0.0ur UFRACT_MAX
unsigned long fract ulr 0.5ulr 0.0ulr ULFRACT_MAX
short accum hk 12.4hk SACCUM_MIN SACCUM_MAX
accum k 12.4k ACCUM_MIN ACCUM_MAX
long accum lk 12.4lk LACCUM_MIN LACCUM_MAX
unsigned short accum uhk 12.4uhk 0.0uhk USACCUM_MAX
unsigned accum uk 12.4uk 0.0uk UACCUM_MAX
unsigned long accum ulk 12.4ulk 0.0ulk ULACCUM_MAX

[Мотивирующий пример]

Рассмотрим очень простой пример операции скалярного умножения (dot-product) с фиксированной точкой. Как бы Вы написали подобное с использованием традиционных типов с фиксированной точкой? Алгоритм выполняет умножение каждой пары дробных значений во входных массивах. Тип accum разработан для хранения результатов накопления, что является именно тем, что нужно. Предположим, что данные состоят из векторов 16-битных значений в диапазоне [-1.0,1.0). Тогда естественно написать следующее:

#include < stdfix.h >
 
accum dot_product(fract *a, fract *b, int n)
{
   accum sum = 0.0k;
   int i;
 
   for (i = 0; i < n; i++)
      sum += a[i] * b[i];
   return sum;
}

Этот показанный выше алгоритм выполняет попарное умножение элементов входных массивов и накапливает результат в переменной, которая получит насыщение при переполнении. Фактически это простое расширение алгоритма скрывает тонкость, связанную с семантикой арифметики, что обсуждается во врезке FX_CONTRACT, но оно показывает, как можно просто расширять алгоритмы, которые манипулируют данными с фиксированной точкой и выполняют насыщение результата при переполнении, без необходимости поиска специальных путей для расширения этих семантик через целочисленную арифметику.

[Семантика арифметики с фиксированной точкой]

Семантики арифметики с фиксированной точкой, работающие в соответствии с вышеупомянутым Technical Report, следующие:

1. Если двоичный оператор имеет один из операндов с плавающей запятой, то другой операнд преобразуется в тип с плавающей запятой, и производится операция над числами с плавающей точкой, давая результат с плавающей точкой.

2. Если в операторе имеется два операнда с фиксированной точкой, но с разным отношением к знаковости (т. е. один операнд signed, другой unsigned), то тогда операнд unsigned переводится в signed без изменения размерности (однако также см. FX_CONTRACT).

3. Выведение типа результата оператора. Результатом типа будет тип операнда с самым высоким рангом. Ранг увеличивается в следующем порядке: short fract, fract, long fract, short accum, accum, long accum (или их unsigned эквиваленты). Оператор, у которого только один операнд типа fixed-point, даст результат типа fixed-point (исключение составляет оператор сравнения, который даст результат boolean).

4. Математический результат, который получается от действия оператора над значениями операндов, преобразуется в тот тип результата, который определен на шаге 3. Другими словами, результат будет как если бы он был вычислен с бесконечной точностью перед тем, как преобразовать его в тип для конечного результата.

Преобразования между различными типами обсуждаются в секции "Преобразования типов данных и типы с фиксированной точкой".

[Преобразования типов данных и типы с фиксированной точкой]

Правила преобразования в тип с фиксированной точкой и из этого типа следующие:

1. Когда происходит преобразование в тип с фиксированной точкой, то если значение операнда может быть представлено типом с фиксированной точкой, то результат получит это значение. Если значение операнда выходит за диапазон типа с фиксированной точкой, то результат будет ближайшее значение операнда для этого типа с фиксированной точкой. Другими словами, преобразование к типу с фиксированной точкой приведет к насыщению математического значения операнда до диапазона значений типа с фиксированной точкой. Если значение операнда находится в диапазоне типа с фиксированной точкой, но не может быть представлено точно, то результат получит ближайшее значение либо к самому большому, либо самому малому значению операнда. Для дополнительной информации см. "Поведение округления".

2. Когда происходит преобразование в целый тип из числа с фиксированной точкой, то результатом будет целая часть от типа с фиксированной точкой. Дробная часть будет отброшена, как при округлении по направлению к 0; (int)(1.9k) даст 1, и (int)(-1.9k) даст -1.

3. Когда происходит преобразование типа с фиксированной точкой к типу с плавающей точкой, то результат получит ближайшее к значению операнда значение с плавающей точкой.

У этих правил есть важные последствия, которые Вы должны учитывать:

1. Преобразование целого числа в дробное полезно только если целое число равно -1, 0 или 1. Все другие целые значения получат насыщение к дробному типу. Так что оператор наподобие:

fract f = 0x4000; // попытка присвоить 0.5 дробной переменной f

вовсе не присвоит 0.5 переменной f, но вместо этого f получит значение FRACT_MAX, потому что 0x4000 это целое число, большее 1. Вместо этого используйте:

fract f = 0.5r;

или

fract f = 0x4000p-15r;

Обратите внимание, что второй рекомендуемый пример использует синтаксис двоичной экспоненты, доступный для констант с фиксированной точкой; т. е. в данном примере значение 0x4000 масштабируется на 2-15.

2. Присваивание дробного значения целому приведет к нулю, кроме случая, когда дробное значение равно -1.0. Присваивание беззнакового дробного значения целому всегда даст в результате 0.

3. Будьте очень осторожны, чтобы случайно не перепутать типы fract16 и fract32 с fract и long fract. Типы fract16 и fract32 это не подлинные дробные типы, это просто typedef-ы от целочисленных типов данных. Так, например:

#include < stdfix.h >
#include < fract.h >
 
fract16 f16;
fract f;
 
void foo(void)
{
   f16 = -0x4000; // сохранит -0.5 в переменной f16
   f = f16;       // в результате даст f = -1.0
}

По той причине, что f16 это целое число, произойдет насыщение при присвоении его к истинно дробному типу. Компилятор выдаст ошибку, когда он может детектировать, что fract16 или fract32 были сконвертированы в тип fract или long fract (или было обратное преобразование), потому что это почти всегда означает ошибку программирования. Для преобразования между целочисленными typedef-ами и традиционными типами используйте функции преобразования bitsfx и fxbitss.

Компилятор будет выдавать предупреждения, чтобы помочь диагностировать проблемы, где скорее всего преобразования дадут не ожидаемые результаты.

[Функции преобразования полей бит bitsfx и fxbitss]

Заголовочный файл stdfix.h предоставляет функции для преобразования полей бит в тип с фиксированной точкой и обратно. В частности, эти функции полезны для преобразования между традиционными дробными типами (fract, long fract) и целочисленными typedef-ами (fract16, fract32).

Для каждого типа с фиксированной точкой декларирован соответствующий целочисленный тип, который содержит достаточное количество бит, чтобы удержать в себе информацию типа с фиксированной точкой. Это типы int_fx_t, где fx заменяется на одно из сочетаний hr, r, lr, hk, k или lk, и uint_fx_t, где fx заменяется на одно из сочетаний uhr, ur, ulr, uhk, uk или ulk.

Чтобы преобразовать тип с точкой дробного числа в поле бит, используйте семейство функций bitsfx. fx может быть любым из сочетаний hr, r, lr, hk, k, lk, uhr, ur, ulr, uhk, uk или ulk. Например, с использованием прототипа

uint_ur_t bitsur(unsigned fract);

Вы можете написать:

#include < stdfix.h >
 
unsigned fract f;
uint_ur_t f_bit_pattern;
 
void foo(void)
{
   f = 0.5ur;
   f_bit_pattern = bitsur(f);    // дает 0x8000
}

Это хороший способ для преобразования fract к fract16 или long fract к fract32, где это необходимо. Например:

#include < stdfix.h >
#include < fract.h >
 
fract f;
fract16 f16;
 
void foo(void)
{
   f = 0.5r;
   f16 = bitsr(f);   // 0x4000, как и ожидалось
}

Подробнее см. описание bitsfx во врезке ниже.

Набор функций семейства bitsfx предоставляет побитное преобразование типа с фиксированной точкой в целое число.

#include < stdfix.h >
 
int_r_t bitsr(fract f);
int_k_t bitsk(accum a);
int_hr_t bitshr(short fract f);
int_hk_t bitshk(short accum a);
int_lr_t bitslr(long fract f);
int_lk_t bitslk(long accum a);
uint_ur_t bitsur(unsigned fract f);
uint_uk_t bitsuk(unsigned accum a);
uint_uhr_t bitsuhr(unsigned short fract f);
uint_uhk_t bitsuhk(unsigned short accum a);
uint_ulr_t bitsulr(unsigned long fract f);
uint_ulk_t bitsulk(unsigned long accum a);

Указанный операнд в формате с фиксированной точкой функция преобразует в значение, умноженное на 2F, где F это количество бит дробной части типа с фиксированной точкой. Это эквивалентно передачи поля бит значения с фиксированной точкой в значение, которое хранится в целочисленном типе.

[Пример]

#include < stdfix.h >
#include < fract.h >
 
int_k_t k;
uint_ulr_t ulr;
fract16 fr16;
fract32 fr32;
 
k = bitsk(-12.5k);         /* k == 0xfffffff9c0000000 */
ulr = bitsulr(0.125ulr);   /* ulr == 0x20000000       */
fr16 = bitsr (-0.75r);     /* fr16 = 0x6000           */
fr32 = bitslr (0.25lr);    /* fr32 = 0xe0000000       */

См. также fxbits.

Подобным образом, чтобы преобразовать поле бит в тип с фиксированной точкой, используйте семейство функций fxbits. Так например, чтобы преобразовать из fract32 в long fract, используйте:

#include < stdfix.h >
#include < fract.h >
 
fract32 f32;
long fract lf;
 
void foo(void)
{
   f32 = 0x40000000;    // это 0.5
   lf = lrbits(f32);    // получение 0.5lr, как и ожидалось
}

Подробнее см. описание fxbits во врезке ниже.

Набор функций семейства fxbits предоставляет побитное преобразование целого числа в тип числа с фиксированной точкой.

#include < stdfix.h >
 
fract rbits(int_r_t b);
accum kbits(int_k_t b);
short fract hrbits(int_hr_t b);
short accum hkbits(int_hk_t b);
long fract lrbits(int_lr_t b);
long accum lkbits(int_lk_t b);
unsigned short fract uhrbits(uint_uhr_t b);
unsigned short accum uhkbits(uint_uhk_t b);
unsigned fract urbits(uint_ur_t b);
unsigned accum ukbits(uint_uk_t b);
unsigned long fract ulrbits(uint_ulr_t b);
unsigned long accum ulkbits(uint_ulk_t b);

Указанный целочисленный операнд будет преобразован в целое число, поделенное на 2F, где F это количество бит, отведенное под дробную часть числа с фиксированной точкой. Это эквивалентно передаче значений поля бит целого числа, которое хранит тип числа с фиксированной точкой.

[Условия возникновения ошибки]

Семейство функций fxbits никогда не возвратит информацию о возникновении ошибки. Если входное целое число не помещается в количество бит результата с фиксированной точкой, то тогда результат получит насыщение к самому большому или самому малому числу в соответствующем представлении с фиксированной точкой.

[Пример]

#include < stdfix.h >
 
accum k;
unsigned long fract ulr;
 
k = kbits(-0x640000000ll);    /* k == -12.5k       */
ulr = ulrbits(0x20000000);    /* ulr == 0.125ulr   */

См. также bitsfx.

[Арифметические операторы для типов с фиксированной точкой]

Вы можете использовать операторы +, -, * и / для типов с фиксированной точкой, и эти операторы будут иметь тот же эффект, как и их эквиваленты для целых чисел, кроме любой семантики переполнения или округления. Как уже обсуждалось выше, операции с фиксированной точкой, которые приводят к переполнению, получат насыщение к самому большому или самому малому представимому типом fixed-point значению. Округление обсуждается в разделе "Поведение округления".

Вы можете использовать операцию << для сдвига значения с фиксированной дочкой на положительное количество позиций бит, которое должно быть меньше, чем размер в битах типа с фиксированной точкой. Это даст тот же результат, что и умножение на число, являющееся степенью двойки, с подключением семантики переполнения:

#include < stdfix.h >
 
fract f1, f2;
 
void foo1(void)
{
   f1 = 0.125r;
   f2 = f1 << 2;  // дает 0.5r
}
 
void foo2(void)
{
   f1 = -0.125r;
   f2 = f1 << 10; // дает -1.0r
}

Также Вы можете использовать операцию >> для сдвига значения с фиксированной точкой вправо на указанное количество бит в том же диапазоне. Это даст тот же результат, что и деление на число, являющееся степенью двойки, с подключением семантики округления:

#include < stdfix.h >
 
fract f1, f2;
 
void foo1(void)
{
   f1 = 0.5r;
   f2 = f1 >> 2;  // даст 0.125r
}
 
void foo2(void)
{
   f1 = 0x0003p-15r;
   f2 = f1 >> 2;  // даст 0x0000p-15r, когда режим округления отбрасыванием,
                  // и 0x0001p-15r, когда режим стандартного округления
                  // со смещением (biased) или без смещения (unbiased)
}

Любые из этих операций могут быть использованы совместно с присваиванием, например:

#include < stdfix.h >
 
fract f1, f2;
 
void foo1(void)
{
   f1 = 0.2r;
   f2 = 0.3r;
   f2 += f1;
}

Кроме того, есть некоторое количество унарных операторов, которые можно использовать с типами фиксированной точки. Вот они:

++ эквивалентно добавлению целого числа 1.
-- эквивалентно вычитанию целого числа 1.
+ унарный плюс, эквивалентно добавлению 0.0 (операция не оказывает эффекта).
- унарный минус, означает вычитанию значения из 0.0.
! 1, если значение равно 0.0, иначе 0.

[FX_CONTRACT]

В примере скалярного умножения (см. "Мотивирующий пример" выше) содержится операция накопления:

sum += a[i] * b[i];

где переменная sum была типа accum, и значения из массивов a[i], b[i] были типа fract. Учитывая ранее рассмотренные правила, какой будет результат умножения? Поскольку оба числа a[i] и b[i] типа fract, то результат умножения будет тоже типа fract - другими словами, два операнда s1.15 при умножении друг на друга дадут результат s1.15. Таким образом, правила говорят, что это должно быть эквивалентно следующему коду:

fract tmp = a[i] * b[i];
sum += tmp;

Однако это означает следующее:

• Результат умножения должен быть округлен до s1.15; будут потеряны 15 бит точности.
• Результат умножения -1.0r на -1.0r должен быть FRACT_MAX, т. е. точно не 1.0.

С этим возникают две проблемы:

• Возможно, что Вы не хотели бы округлять дополнительные биты точности прежде, чем добавить результат умножения к sum. Выполнение преждевременного округления снизит точность накопления. Кроме того, процессор Blackfin имеет эффективную, одноцикловую инструкцию умножения с накоплением (multiply-accumulate, MAC), но она не отбрасывает дополнительные биты точности в результате умножения перед накоплением.
• На процессорах Blackfin инструкция MAC не делает насыщение -1.0r * -1.0r перед добавлением к регистру аккумулятора. И это снова дает эффект повышения точности в результате накопления, но не соответствует семантике типа с фиксированной точкой для примера скалярного умножения.

Чтобы сгенерировать эффективный код без потери точности, Вы должны в действительности написать:

sum += (accum)a[i] * (accum)b[i];

Причина в том, что преобразование в более высокую точность типа accum перед умножением означает, что генерируемый код может удержать промежуточный результат умножения в формате s9.31, что означает отсутствие требований к насыщению результата или округления бит младшего порядка. Это также будет означать использование компилятором аппаратной инструкции MAC.

Для удобства, компилятор может сделать это за Вас, если будете использовать режим, известный как FX_CONTRACT. Имя FX_CONTRACT используется потому, что поведение подобно FP_CONTRACT в C99. Когда режим FX_CONTRACT включен, компилятор может удерживать промежуточные результаты в точности выше, чем это задает Technical Report. Другими словами, можно выбрать не округлять дополнительные биты точности или выполнять насыщение в промежуточном результате, если это нежелательно. Если выразиться точнее, компилятор удерживает промежуточный результат в повышенной точности при следующих обстоятельствах:

• Сохранение промежуточного результата с повышенной точностью будет более эффективным для нижележащего аппаратного обеспечения.
• Промежуточный результат не сохраняется обратно в любую именованную переменную.
• Нет явного преобразования типа для промежуточного результата.

Другими словами,

sum += a[i] * b[i];

приведет к использованию инструкции MAC. Но для этих двух примеров:

sum += (fract)(a[i] * b[i]);

или

fract tmp = a[i] * b[i];
sum += tmp;

будет происходить приведение результата умножения обратно к типу fract перед применением накопления.

Есть и другие примеры, где FX_CONTRACT может сохранять промежуточные результаты с повышенной точностью:

• Неявное преобразование типа unsigned fixed-point в больший по размеру signed fixed-point тип не будет приводить к первоначальному преобразованию в типа signed fixed-point меньшего размера.
• Умножение signed fract и unsigned fract может создать дробное умножение смешанного типа, вместо того, чтобы сначала преобразовать unsigned fract в signed fract.

По умолчанию для компилятора разрешено поведение FX_CONTRACT. Режимом FX_CONTRACT можно управлять прагмой (см. описание #pragma FX_CONTRACT {ON|OFF} в [1]), или с опций помощью командной строки (см. описание опций -fx-contract и –no-fx-contract [3]). Прагма может использоваться в области действия файла исходного кода или внутри функции. Здесь применяются те же правила области действия, что и для прагмы FX_ROUNDING_MODE [1].

[Поведение округления]

Что произойдет, когда long fract будет преобразовано в fract? 16 младших значащих бит не могут быть представлены в результате, поэтому при преобразовании они должны быть отброшены. В случае, когда значение long fract не может быть точно представлено значением типа fract, есть 2 выбора: результат может быть представлен ближайшим fract значением, которое больше исходного значения long fract, либо ближайшим значением, которое меньше чем исходное значение long fract. Это известно под термином поведение округления (rounding behavior).

На некоторые операции с фиксированной точкой также может влиять округление. Например, умножение двух дробных значений для генерации дробного результата того же размера требует отбрасывания определенного количества младших бит от полного результата. Например, умножение s1.15 * s1.15 даст полный результат s2.30. Это приведет к насыщению до s1.30 и 15 младших значащих бит должны быть отброшены, чтобы дать результат s1.15.

По умолчанию любые биты должны быть отброшены усечением. Например:

#include < stdfix.h >
 
fract f1, f2, prod;
 
void foo(void)
{
   f1 = 0x3ffp-15r;
   f2 = 0x1000p-15r;
   prod = f1 * f2;   // даст 0x007fp-15r, с отбрасыванием
                     // младших значащих бит это даст 0xe000
}

Это эквивалентно всегда округлению вниз до отрицательной бесконечности. Такая тенденция будет приводить к результатам, точность которых будет ухудшаться.

Если вычисления не дают требуемой точности, то Вы можете использовать смещенное (biased) или несмещенное округление к ближайшему значению. Компилятор поддерживает прагмы и опции командной строки для управления режимом округления. В режимах округления biased или unbiased результат будет округлен к ближайшему значению, которое можно представить типом результата, так что конечное значение будет 0x0080p-15r.

Разница между biased и unbiased округлением сказывается, когда значение для округления лежит точно на половине между двумя ближайшими значениями, которые может представить тип результата. В этом случае biased округление всегда округляет в сторону большего из этих двух значений, (с применением насыщения, если округление привело к переполнению), в то время как unbiased округление всегда округляет в сторону меньшего из значений, у которого младший значащий бит равен 0. Например:

#include < stdfix.h >
 
fract f;
long fract lf;
 
void foo1(void)
{
   lf = 0x34568000p-31lr;
   f = lf;  // даст 0x3456p-15r в режиме округления unbiased,
            // но 0x3457p-15r в режиме округления biased
}
 
void foo2(void)
{
   lf = 0x34578000p-31lr;
   f = lf;  // даст 0x3458p-15r в обоих режимах, biased
            // и unbiased
}

Обычно округление unbiased более дорогое в контексте трат тактов процессора, чем biased, но дает более точный результат, поскольку ошибки округления в половине случаев не имеют того же направления, и нее настолько сильно влияют на конечный результат.

Округление, обсуждаемое здесь, влияет только на операции, которые касаются результата с фиксированной точкой. Операции, касающиеся целочисленного результата, округляются в направлении 0. Есть также несколько исключений из этих правил округления:

• Преобразование значения с плавающей точкой в значение с фиксированной точкой приводит к округлению в сторону 0.
• Функции roundfx, strtofxfx и fxdivi всегда выполняют либо biased, либо unbiased округление, в зависимости от текущего состояния бита RND_MOD. Они не поддерживают режим округления отбрасыванием.

Подробнее об установке режима округления рассказано в разделе ниже.

[Установка режима округления]

Как упоминалось в разделе "Поведение округления", имеется 3 режима округления, поддерживаемых арифметикой с фиксированной точкой:

• Обрезка (truncation), это режим округления по умолчанию.
• Biased округление к ближайшему большему значению.
• Unbiased округление к ближайшему меньшему значению.

Чтобы установить режим округления, Вы можете использовать прагму или опции компилятора.

Следующие опции командной строки компилятора управляют поведением округления [3]:

-fx-rounding-mode-truncation
-fx-rounding-mode-biased
-fx-rounding-mode-unbiased

Указанный опцией режим округления станет режимом по умолчанию для всего исходного компилируемого кода.

Вы также можете использовать прагмы для более гибкого управления округлением. Вот они:

#pragma FX_ROUNDING_MODE TRUNCATION
#pragma FX_ROUNDING_MODE BIASED
#pragma FX_ROUNDING_MODE UNBIASED

Если одна из этих прагм применена на области действия файла, то её влияние будет распространено до конца юнита трансляции, или пока не встретится другая прагма на уровне файла, которая поменяет текущий режим округления.

Если одна из этих прагм была применена в пределах области действия составного оператора (т. е. блока кода, ограниченного фигурными скобками), то влияние прагмы будет распространяться до конца составного оператора. Режим округления будет возвращено к режиму, который действовал до входа в область действия составного оператора.

Пример того, как использовать прагмы для управления округлением, показан в листинге ниже.

#include < stdfix.h >
 
#pragma FX_ROUNDING_MODE BIASED
 
fract my_func(void)
{
   // в этом месте режим округления biased
   {
      #pragma FX_ROUNDING_MODE UNBIASED
      // в этом месте режим округления unbiased
   }
   // в этом месте режим округления biased
}
 
#pragma FX_ROUNDING_MODE TRUNCATION
fract my_func2(void)
{
   // в этом месте режим округления truncation
}

У процессора Blackfin есть специализированные инструкции для поддержки округления к ближайшему значению. Однако, какое именно будет использоваться округление - biased или unbiased - зависит от текущего состояния бита RND_MOD. Чтобы упростить генерацию эффективного кода, компилятор предполагает, что когда режим округления либо biased, либо unbiased, бит RND_MOD был установлен в тот же тип округления. Это означает, что компилятор может эффективно использовать аппаратную поддержку для этих режимов округления без необходимости установки или очистки этого бита каждый раз при использовании инструкции, зависящей от бита RND_MOD.

Таким образом, в Вашей ответственности гарантировать, что бит RND_MOD установлен корректно. Для упрощения этой задачи предоставляются встроенные функции:

int set_rnd_mod_biased(void);int set_rnd_mod_unbiased(void);

Эти функции возвращают предыдущее состояние бита RND_MOD. Имеется другая встроенная функция (void restore_rnd_mod(int)), которая сбрасывает бит RND_MOD в сохраненное состояние. Например, если Вы напишете:

#include < stdfix.h >#include < builtins.h >
fract my_func(void)
{
   #pragma FX_ROUNDING_MODE BIASED
   int saved_rnd_mod = set_rnd_mod_biased();
   // в этом месте режим округления biased
   restore_rnd_mod(saved_rnd_mod);
   // в этом месте будет тот же режим округления,
   // который был на входе в функцию
}

Если Вы используете прагмы для указания режима округления без установки бита RND_MOD, то можете получить смесь biased и unbiased поведения округления. Для дополнительной информации см. "#pragma FX_ROUNDING_MODE {TRUNCATION|BIASED|UNBIASED}" и "Changing the RND_MOD Bit" в руководстве [1].

[Арифметические библиотечные функции]

Заголовочный файл stdfix.h также декларирует некоторое количество функций, которые позволяют выполнять полезные арифметические операции на комбинациях типов чисел с фиксированной точкой и целых типов чисел. Это семейства функций divifx, idivfx, fxdivi, mulifx, absfx, roundfx, countlsfx и strtofxfx.

Функции divifx где fx заменяется на один из суффиксов r, lr, k, lk, ur, ulr, uk или ulk, позволяет выполнить деление целого числа на число с фиксированной точкой, чтобы получить в результате целое число.

#include < stdfix.h >
 
int divir(int numer, fract denom);
int divik(int numer, accum denom);
long int divilr(long int numer, long fract denom);
long int divilk(long int numer, long accum denom);
unsigned int diviur(unsigned int numer, unsigned fract denom);
unsigned int diviuk(unsigned int numer, unsigned accum denom);
unsigned long int diviulr(unsigned long int numer,unsigned long fract denom);
unsigned long int diviulk(unsigned long int numer,unsigned long accum denom);

Функция принимает целочисленное делимое numer и делитель denom в формате с фиксированной точкой, и функция вычислит частное и вернет ближайший целый результат.

У семейства функций divifx поведение не определено, когда делитель denom равен 0.

[Пример]

#include < stdfix.h >
 
int quo;
unsigned long int ulquo;
 
quo = divik(125, -12.5k);        /* quo == -10 */
ulquo = diviulr(125, 0.125ulr);  /* ulquo == 1000 */

См. также fxdivi, idivfx.

[Почему следует использовать divifx]

Если Вы напишете:

#include < stdfix.h >
 
fract f;
int i, quo;
 
void foo(void)
{
   // ПЛОХАЯ ИДЕЯ: деление int на fract даст в результате fract, а не int
   f = 0.5r;
   i = 2;
   quo = i / f;
}

то результат деления будет типа fract целая часть которого будет сохранена в переменной quo. Это означает, что значение quo будет 0, так как деление приведет к переполнению, и выдаст дробный результат, который близок к 1.

Чтобы получить желаемый результат, напишите:

#include < stdfix.h >
 
fract f;
int i, quo;
 
void foo(void)
{
   // ТАК БУДЕТ ПРАВИЛЬНО: если использовать divifx
   // для получения целого результата
   f = 0.5r;
   i = 2;
   quo = divir(i, f);
}

В результате переменная quo получит значение 4.

Функции idivfx, где fx заменяется на один из суффиксов r, lr, k, lk, ur, ulr, uk или ulk, позволяет выполнять деление числа с фиксированной точкой на число с фиксированной точкой, чтобы получить целочисленный результат.

#include < stdfix.h >
 
int idivi(fract numer, fract denom);
int idivk(accum numer, accum denom);
long int idivlr(long fract numer, long fract denom);
long int idivlk(long accum numer, long accum denom);
unsigned int idivur(unsigned fract numer, unsigned fract denom);
unsigned int idivuk(unsigned accum numer, unsigned accum denom);
unsigned long int idivulr(unsigned long fract numer,
                          unsigned long fract denom);
unsigned long int idivulk(unsigned long accum numer,
                          unsigned long accum denom);

Принимая дробные значения делимого (numer) и делителя (denom), функция вернет целое значение, которое будет самым близким к результату деления.

У семейства функций idivfx поведение не определено, когда делитель denom равен 0.

[Пример]

#include < stdfix.h >int quo;unsigned long int ulquo;
quo = idivk(125.0k, -12.5k);        /* quo == -10 */
ulquo = idivulr(0.5ulr, 0.125ulr);  /* ulquo == 4 */

См. также divifx, fxdivi.

[Почему следует использовать idivfx]

Если Вы напишете:

#include < stdfix.h >
 
fract f1, f2;
 
int quo;void foo(void)
{
   // НЕПРАВИЛЬНО: деление двух чисел fract даст в результате fract, не int
   f1 = 0.5r;
   f2 = 0.25r;
   quo = f1 / f2;
}

то результат деления будет fract, целая часть которого будет сохранена в переменной quo. Это означает, что значение quo будет 0, поскольку произойдет переполнение при делении, и это даст дробный результат, близкий к 1.

Чтобы получить желаемый результат, напишите:

#include < stdfix.h >
 
fract f1, f2;
int quo;
 
void foo(void)
{
   // ТАК ПРАВИЛЬНО: использование idivfx даст целочисленный результат
   f1 = 0.5r;
   f2 = 0.25r;
   quo = idivr(f1, f2);
}

тогда результат будет 2, и он будет записан в переменную quo.

Функции fxdivi, где fx заменяется на один из префиксов r, lr, k, lk, ur, ulr, uk или ulk, позволяют выполнить деление целого числа на целое, и в результате получить число с фиксированной точкой.

#include < stdfix.h >
 
fract rdivi(int numer, int denom);
accum kdivi(int numer, int denom);
long fract lrdivi(long int numer, long int denom);
long accum lkdivi(long int numer, long int denom);
unsigned fract urdivi(unsigned int numer, unsigned int denom);
unsigned accum ukdivi(unsigned int numer, unsigned int denom);
unsigned long fract ulrdivi(unsigned long int numer,
                            unsigned long int denom);
unsigned long accum ulkdivi(unsigned long int numer,
                            unsigned long int denom);

Функция принимает два целых числа, делит их друг на друга и возвратит тип с фиксированной точкой, по значению ближайший к результату деления.

У семейства функций fxdivi поведение не определено, когда делитель denom равен 0.

[Пример]

#include < stdfix.h >
 
accum quo;
unsigned long fract ulquo;
 
quo = kdivi(125, -10);  /* quo == -12.5k     */
ulquo = ulrdivi(1, 8);  /* ulquo == 0.125ulr */

См. также divifx, idivfx.

[Почему следует использовать fxdivi]

Если Вы напишете:

#include < stdfix.h >
 
int i1, i2;
fract quo;
 
void foo(void)
{
   // НЕПРАВИЛЬНО: деление int на int даст в результате int, не fract
   i1 = 5;
   i2 = 10;
   quo = i1 / i2;
}

то результат деления будет целым числом, которое будет преобразовано во fract и присвоено переменной quo. Это означает, что значение переменной quo будет 0, потому что результат деления будет округлен к целому 0, и затем этот 0 будет преобразован в формат fract.

Чтобы получить желаемый результат, напишите:

#include < stdfix.h >
 
int i1, i2;
fract quo;
 
void foo(void)
{
   // ТАК ПРАВИЛЬНО: использование fxdivi даст результат fract
   i1 = 5;
   i2 = 10;
   quo = rdivi(i1, i2);
}

полученное правильное значение 0.5 будет присвоено переменной quo.

Функции mulifx, где fx заменяется на один из суффиксов r, lr, k, lk, ur, ulr, uk или ulk, позволяют умножить целое число на число с фиксированной точкой, и получить в результате целое число.

#include < stdfix.h >
 
int mulir(int i, fract f);
int mulik(int i, accum a);
long int mulilr(long int i, long fract f);
long int mulilk(long int i, long accum a);
unsigned int muliur(unsigned int i, unsigned fract f);
unsigned int muliuk(unsigned int i, unsigned accum a);
unsigned long int muliulr(unsigned long int i,unsigned long fract f);
unsigned long int muliulk(unsigned long int i,unsigned long accum a);

Принимая целое число и число с в формате с фиксированной точкой, семейство функций mulifx вычисляет результат умножения и вернет ближайшее целое к полученному результату.

[Почему следует использовать mulifx]

Если Вы напишете:

#include < stdfix.h >
 
int i, prod;
fract f;
 
void foo(void)
{
   // НЕПРАВИЛЬНО: умножение int на fract даст
   // результат типа fract, не int
   i = 50;
   f = 0.5r;
   prod = i * f;
}

то результат умножения будет fract, целая часть которого сохраняется в переменной prod. Это означает, что значение prod будет 0, так как умножение приведет к переполнению, и приведет к дробному числу, близкому к 1.

Чтобы получить правильный результат, напишите:

#include < stdfix.h >
 
int i, prod;
fract f;
 
void foo(void)
{
   // ПРАВИЛЬНО: используется mulifx для получения целого результата
   i = 50;
   f = 0.5r;
   prod = mulir(i, f);
}

В этом варианте получится значение 25, которое будет присвоено переменной prod.

Функции absfx, где fx заменяется одним из суффиксов hr, r, lr, hk, k или lk, вычисляют абсолютное значение числа с плавающей точкой.

#include < stdfix.h >
 
fract absr(fract f);
accum absk(accum a);
short fract abshr(short fract f);
short accum abshk(short accum a);
long fract abslr(long fract f);
long accum abslk(long accum a);

Дополнительно к индивидуально именованным функциям Вы также можете использовать универсальный по типу макрос absfx(), где тип операнда может быть любым знаковым (signed) типом с фиксированной точкой. Этот макрос предназначен для работы в режиме C99. Макрос возвратит тот же тип, что и его операнд.

[Пример]

#include < stdfix.h >
 
accum a;
long fract f;
 
a = abshk(-12.5k);      /* a == 12.5k  */
f = abslr(0.75lr);      /* f == 0.75lr */
 
#if defined(_C99)
   a = absfx(-12.5k);   /* a == 12.5k  */
   f = absfx(0.75lr);   /* f == 0.75lr */
#endif

См. также abs, fabs, labs.

Функции roundfx, где fx заменяется одним из суффиксов hr, r, lr, hk, k, lk, uhr, ur, ulr, uhk, uk или ulk, принимает 2 аргумента. Первый это операнд с фиксированной точкой, тип которого соответствует имени вызываемой функции. Второй параметр дает количество бит дробной части. Первый операнд округляется до указанного вторым параметром количества бит дробной части. Второй операнд должен задавать значение между 0 и количеством бит дробной части в округляемом типе. Округление происходит к ближайшему значению. Однако каким именно будет округление - смещенным (biased) или не смещенным (unbiased) - зависит от состояния бита RND_MOD аппаратуры. Подробнее см. раздел "Поведение округления". Если округленный результат выходит за пределы значений типа результата, то результат получит насыщение к максимуму или минимуму значения для типа результата с фиксированной точкой.

#include < stdfix.h >
 
fract roundr(fract f, int n);
accum roundk(accum a, int n);
short fract roundhr(short fract f, int n);
short accum roundhk(short accum a, int n);
long fract roundlr(long fract f, int n);
long accum roundlk(long accum a, int n);
unsigned fract roundur(unsigned fract f, int n);
unsigned accum rounduk(unsigned accum a, int n);
unsigned short fract rounduhr(unsigned short fract f, int n);
unsigned short accum rounduhk(unsigned short accum a, int n);
unsigned long fract roundulr(unsigned long fract f, int n);
unsigned long accum roundulk(unsigned long accum a, int n);

[Пример]

#include < stdfix.h >
 
long fract lf, rnd;
 
void foo1(void)
{
   lf = 0x45608100p-31lr;
   rnd = roundlr(lf, 15);  // даст результат 0x45610000p-31lr;
}
 
void foo2(void)
{
   lf = 0x7fff9034p-31lr;
   rnd = roundlr(lf, 15);  // даст результат 0x7fffffffp-31lr;
}

Дополнительно Вы также можете использовать универсальный по отношению к типу макрос roundfx(), где тип первого операнда может быть любым из типов с фиксированной точкой. Этот макрос определен для использования в режиме C99. Макрос вернет тот же тип, что и тип операнда.

Функции countlsfx, где fx заменяется одним из суффиксов hr, r, lr, hk, k, lk, uhr, ur, ulr, uhk, uk или ulk, возвратит самое большое значение k, которое соответствует операнду, сдвинутому влево на k настолько, что не произошло переполнение. Для нулевого входного операнда результат будет количеством бит в типе операнда.

#include < stdfix.h >
 
int countlsr(fract f);
int countlsk(accum a);
int countlshr(short fract f);
int countlshk(short accum a);
int countlslr(long fract f);
int countlslk(long accum a);
int countlsur(unsigned fract f);
int countlsuk(unsigned accum a);
int countlsuhr(unsigned short fract f);
int countlsuhk(unsigned short accum a);
int countlsulr(unsigned long fract f);
int countlsulk(unsigned long accum a);

Дополнительно также можно использовать макрос countlsfx(), где тип операнда может быть любым типом с фиксированной точкой. Этот макрос используется в режиме C99.

[Пример]

#include < stdfix.h >
 
int scal1, scal2;
 
void foo(void)
{
   scal1 = countlsk(-3.0k);   // даст 6, потому что
                              // -3.0k << 6 = -192.0k
   scal2 = countlsuk(3.0uk);  // даст 6, потому что
                              // 3.0uk << 6 = 192.0uk
}
 
int n;
 
n = countlsk(-12.5k);         /* n == 4 */
n = countlsulr(0.125ulr);     /* n == 2 */
#if defined(_C99)
   n = countlsfx(-12.5k);     /* n == 4 */
   n = countlsfx(0.125ulr);   /* n == 2 */
#endif

Функции strtofxfx, где fx заменяется одним из суффиксов hr, r, lr, hk, k, lk, uhr, ur, ulr, uhk, uk или ulk, анализируют строковое представление числа с фиксированной точкой и возвращают число в формате с фиксированной точкой. Функции ведут себя подобно функции strtod, и принимают входную строку в том же формате.

#include < stdfix.h >
 
fract strtofxr(const char *nptr, char **endptr);
accum strtofxk(const char *nptr, char **endptr);
short fract strtofxhr(const char *nptr, char **endptr);
short accum strtofxhk(const char *nptr, char **endptr);
long fract strtofxlr(const char *nptr, char **endptr);
long accum strtofxlk(const char *nptr, char **endptr);
unsigned fract strtofxur(const char *nptr, char **endptr);
unsigned accum strtofxuk(const char *nptr, char **endptr);
unsigned short fract strtofxuhr(const char *nptr, char **endptr);
unsigned short accum strtofxuhk(const char *nptr, char **endptr);
unsigned long fract strtofxulr(const char *nptr, char **endptr);
unsigned long accum strtofxulk(const char *nptr, char **endptr);

Подразумевается, что nptr указывает на строку, которая представляет число как десятичное с плавающей точкой или шестнадцатеричное с плавающей точкой. Любой из форм числа может предшествовать последовательность из символов пробела (как это определено функцией isspace), которая будет игнорироваться функцией.

Принимается следующая строка числа в представлении с десятичной плавающей точкой:

[sign] [digits] [.digits] [{e|E} [sign] [digits]]

Поле sign является необязательным, и может быть либо плюсом (+), либо минусом (–). Поле digits может быть одной или большим количеством символов десятичных цифр. Последовательность цифр может содержать десятичную точку (.).

За десятичными цифрами может идти экспонента, которая состоит из вводной буквы (e или E) и опционального целого знакового (signed) числа. Если не появляется ни десятичная точка, ни часть экспоненты, то предполагается, что десятичная точка следует за последней цифрой строки.

Принимается следующая строка числа в представлении с шестнадцатеричной плавающей точкой:

[sign] [{0x}|{0X}] [hexdigs] [.hexdigs] [{p|P} [sign] [digits]]

Строка шестнадцатеричного числа с плавающей точкой может начинаться с опционального плюса (+) или минуса (–), за которым идет префикс обозначения шестнадцатеричного числа 0x или 0X. За этой последовательностью символов должен идти один или большее количество шестнадцатеричных символов, последовательность которых может содержать десятичную точку (.).

За шестнадцатеричными символами идет двоичная экспонента, которая состоит из буквы p или P, опционального знака и не пустой последовательности десятичных цифр. Эта экспонента интерпретируется как степень двойки, которая используется для масштабирования дробного числа, представленного полями [hexdigs] [.hexdigs].

Первый символ в последовательности текста, который не удовлетворяет представленным формам числа, остановит сканирование строки. Если endptr не NULL, то по завершении работы функции он укажет на символ, который останавливает сканирование, и место, на котором было остановлено сканирование, будет сохранено в этом указателе.

[Условия возникновения ошибки]

Функции strtofxfx вернут 0, если не было произведено преобразование, и в объекте, на который указывает endptr, будет сохранен указатель на недопустимую строку. Если корректное значение результата привело к переполнению, то будет возвращено максимальное положительное или отрицательное число для заданного значения с фиксированной точкой. Если корректное значение привело к недогрузке (underflow, т. е. число слишком мало, чтобы его можно было представить заданным форматом с фиксированной точкой), то будет возвращен 0. В случае переполнения в errno будет сохранено значение ERANGE.

[Пример]

#include < stdfix.h >
 
char *rem;
accum k;
unsigned long fract ulr;
 
k = strtofxk ("-2345.5E-3 abc",&rem);
      /* k = -2.3455k, rem = " abc" */
ulr = strtofxulr ("0x180p-12,123",&rem);
      /* ulr = 0x1800p-16ulr, rem = ",123" */

См. также strtod, strtol, strtoul.

[Спецификаторы формата преобразования ввода/вывода]

Семейство функций printf и scanf поддерживают спецификаторы преобразования для типов с фиксированной точкой. Они приведены в таблице 1-17. Обратите внимание, что спецификаторы преобразования для signed-типов, %r и %k, задаются в нижнем регистре, в то время как для unsigned-типов применяются %R и %K в верхнем регистре.

Таблица 1-17. Спецификаторы формата преобразования ввода/вывода для типов с фиксированной точкой.

Тип Спецификатор формата преобразования
short fract %hr
fract %r
long fract %lr
unsigned short fract %hR
unsigned fract %R
unsigned long fract %lR
short accum %hk
accum %k
long accum %lk
unsigned short accum %hK
unsigned accum %K
unsigned long accum %lK

При использовании с семейством функций scanf эти спецификаторы преобразования принимают тот же формат, который потребляют функции strtofxfx, который тот же самый какой применяется для %f. Для дополнительной информации см. врезку с описанием функций strtofxfx.

При использовании с семейством функций printf значения с фиксированной точкой печатаются следующим образом:

• По умолчанию как шестнадцатеричные значения, или когда используется опция командной строки компилятора -no-full-io. Например:

printf("fract: %r\n", 0.5r);  // дробное число напечатается в виде 4000

• Как значение с плавающей точкой, когда использовалась опция командной строки компилятора -fixed-point-io или -full-io. Например:

printf("fract: %r\n", 0.5r);  // дробное число напечатается в виде 0.500000

Принимаются опциональные спецификаторы точности, которые управляют количеством печатаемых десятичных цифр, и должна ли печататься завершающая десятичная точка. Однако они не дадут эффекта, за исключением использования опций командной строки -fixed-point-io или -full-io. Подробнее см. врезку с описанием функции fprintf.

Функция осуществляет форматированный вывод на печать в файловый поток.

#include < stdio.h >
 
int fprintf(FILE *stream, const char *format, /*аргументы*/ ...);

Функция fprintf передает свой вывод печати в именованный выходной поток stream. Строка format (так называемая строка форматирования) задает, что будет выведено, и какие произойдут преобразования печатаемых аргументов.

Строка format содержать нулевое или большее количество спецификаторов формата преобразования, каждый из которых начинается символом процента (%). Сам спецификатор формата следует за символом %, и может содержать из одного или большего количества следующих полей:

• Flag – опциональные символы, которые модифицируют поведение преобразования параметра в текст.
• Width – опциональное числовое значение (или *), которое задает минимальную ширину поля.
• Precision – опциональное числовое значение, которое задает минимальное количество появляющихся цифр (задается точность преобразования).
• Length – опциональный модификатор, который указывает размер аргумента.
• Type – символ, который задает тип применяемого преобразования.

Символы поля flag могут идти в любом порядке, и их появление не обязательно. Допустимые флаги описаны в следующей таблице.

Флаг Поле
- Левое выравнивание результата в пределах поля (по умолчанию результат выравнивается вправо).
+ Всегда начинает преобразование со знаком, где знак плюс или минус. По умолчанию только отрицательные значения указываются со знаком минус.
пробел Добавляется префикс из пробелов к результату, если если первый символ не знак минуса и также не был указан флаг +.
# Результат преобразуется в альтернативную форму в зависимости от типа преобразования:
o если значение не 0, то оно печатается с предшествием нулей.
x если значение не 0, то оно печатается с префиксом 0x.
X если значение не 0, то оно печатается с префиксом 0X.
a, A, e, E, f, F всегда генерирует появление десятичной точки.
0 (ноль) Задает альтернативу дополнению нулями. Начальные нули будут использоваться для необходимости дополнения поля до указаной ширины (спецификатором Width), лидирующие нули будут следовать за любым знаком или спецификатором базы счисления. Этот флаг игнорируется, если он появляется после флага '-', или если он используется в спецификации преобразования, которая использует точность и один из типов преобразования a, A, d, i, o, u, x или X. Флаг 0 может использоваться вместе с преобразованиями a, A, d, i, o, u, x, X, e, E, f, g и G.

Если задано поле width, то преобразованное значение дополняется пробелами до указанной ширины, если после преобразования количество символов оказалось меньше указанного значения. Обычно пробелы используются для дополнения поля слева, но будет использоваться дополнение справа, если указан флаг '-'. Флаг '0' может использоваться, чтобы заменить дополнение пробелами на дополнение символом 0 (см. описание флагов в таблице выше). Ширина width также может быть задана как '*', что показывает, что текущий аргумент в вызове fprintf это число int, которое задает значение width. Если значение отрицательное, то оно интерпретируется как флаг '-' и положительное значение поля width.

Опциональное значение precision (точность) начинается с точки (.), за которой идет либо звездочка (*), либо десятичное целое число. Звездочка (*) показывает, что точность задана целочисленным аргументом, предшествующим форматируемуму выводимому аргументу. Если указана только точка, то подразумевается нулевая точность. Значение precision дает разные эффекты, в зависимости от используемого спецификатора преобразования:

• Для A точность указывает количество цифр после десятичной точки. Если точность нулевая, и не указан флаг #, то не будет сгенерирована десятичная точка.
• Для d, i, o, u, x, X задается минимальное количество появляющихся цифр, по умолчанию 1.
• Для f, F, E, e, k, K, r, R задается количество цифр после десятичной точки, по умолчанию 6. Если спецификатор # присутствует с нулевой точностью, то не будет сгенерирована десятичная точка.
• Для g, G задается максимальное количество значащих цифр.
• Для s, указывается максимальное количество записываемых символов.

Модификатор length может опционально использоваться для указания размерности аргумента. Модификаторы длины должны предшествовать только спецификаторам преобразования d, i, o, u, x, X, k, K, r, R или n, если не детализованы другие спецификаторы преобразования.

Length Действие
h Аргумент должен быть представлен типом short int, short fract или short accum.
hh Аргумент должен интерпретироваться как тип char.
j Аргумент должен быть представлен типом intmax_t или uintmax_t.
l Аргумент должен быть представлен типом long int, long fract или long accum.
ll Аргумент должен интерпретироваться как тип long long int. 
L Аргумент должен интерпретироваться как long double. Этот модификатор длины должен предшествовать спецификаторам преобразования a, A, e, E, f, F, g или G.
t Аргумент должен интерпретироваться как ptrdiff_t.
z Аргумент должен интерпретироваться как size_t.

Обратите внимание, что спецификаторы размера hh, j, t и z, как это описано в стандарте C99 (ISO/IEC 9899:1999), доступны толкьо в случае выбора опции компилятора -full-io.

Следующая таблица содержит определения допустимых спецификаторов преобразования, которые задают один из применияемых типов преобразования:

Спецификатор Преобразование
a, A Число с плавающей точкой.
c Символ.
d, i Десятичное целое со знаком.
e, E Научная нотация (мантисса/экспонента).
f, F Десятичное число  плавающей точкой.
g, G Преобразование работает как e, E или f, F.
k Знаковый тип accum.
K Тип accum без знака.
n Указатель на число со знаком, к которому число символов, записанное до сих пор, будет сохранено без другого вывода.
o Беззнаковое восьмеричное.
p Указатель на void.
r Дробное (fract) со знаком.
R Дробное (fract) без знака.
s Строка символов.
u Целое число без знака.
x, X Число без знака в шестнадцатеричной нотации.
% Позволяет вывести на печать символ % без интерпретации его как начала спецификатора преобразования.

Спецификатор преобразования a|A преобразует число с плавающей точкой стиля [-]0xh.hhhhp±d, где до точки стоит одна из шестнадцатеричных цифр. Спецификатор преобразования a|A всегда содержит минимум одну цифру для экспоненты.

Спецификатор преобразования e|E преобразует число с плавающей точкой стиля [-]d.ddde±dd. Экспонента всегда содержит как минимум две цифры. Случай, когда e педшествует экспоненте, соответствует спецификатору преобразования.

Спецификатор преобразования f|F конвертирует десятичную строку вида [-]d.ddd.

Спецификатор преобразования g|G конвертирует как спецификаторы e|E или f|F, в зависимости от преобразуемого значения. Если экспонента значения преобразована меньше чем -4, или больше или равна precision, то используются преобразования e|E, иначе f|F.

Все спецификаторы преобразования a, A, e, E, f, F, g и G, когда аргумент представляет бесконечность, выведут inf или INF в том же регистре, в каком был задан спецификатор. Для всех спецификаторов a, A, e, E, f, F, g и G аргумент, который представляет результат NaN, выводится как nan или NAN в том же регистре, в каком был задан спецификатор.

Спецификаторы преобразования k|K and r|R преобразуют значение с фиксированной точкой в десятичную нотацию [-]d.ddd, когда Ваше приложение собрано с опцией командной строки компилятора -full-io или -fixed-point-io. Иначе спецификаторы k|K и r|R будут преобразованивать значение с фиксированной точкой как шестнадцатеричное.

Функция fprintf возвращает количество напечатанных в поток символов.

[Условия возникновения ошибки]

Если функция fprintf завершилась неудачно, то она вернет отрицательное значение.

[Пример]

#include < stdio.h >
 
void fprintf_example(void)
{
   char *str = "hello world";
   /* Выведется в stdout " +1 +1." */
   fprintf(stdout, "%+5.0f%+#5.0f\n", 1.234, 1.234);
   /* Выведется в stdout is "1.234 1.234000 1.23400000" */
   fprintf(stdout, "%.3f %f %.8f\n", 1.234, 1.234, 1.234);
   /* Выведется в stdout "выровнено:                          слева:5 справа: 5" */
   fprintf(stdout, "выровнено:\nслева:%-5dсправа:%5i\n", 5, 5);
   /* Выведется в stdout "90% тест-программ печатают hello world" */
   fprintf(stdout, "90%% тест-программ печатают %s\n", str);
   /* Выведется в stdout "0.0001 1e-05 100000 1E+06" */
   fprintf(stdout, "%g %g %G %G\n", 0.0001, 0.00001, 1e5, 1e6);
}

См. также printf, snprintf, vfprintf, vprintf, vsnprintf, vsprintf.

[Портирование кода, написанного с использованием fract16 и fract32]

Если у Вас есть код, использующий типы fract16 и fract32 вместе со встроенными функциями и вызовами библиотечных функций, то возможно Вы захотите переписать свой код для использования новых традиционных типов с фиксированной точкой (native fixed-point). В этом разделе содержится несколько советов, как это сделать проще всего.

Поскольку fract являются 16-разрядным типом, и long fract является 32-разрядным типом, то базовая стратегия состоит в замене использования переменных fract16 на fract и переменных fract32 на long fract.

Во-первых, код написанный с использованием fract16 и fract32 часто содержит константы. Если они написаны с использованием суффиксов r16 и r32, то Вы можете просто поменять суффикс для создания типа native fixed-point.

Например:

fract16 f1 = 0.5r16;
fract32 f2 = 0.75r32;

станет

fract f1 = 0.5r;long fract f2 = 0.75lr;

Если Ваш код содержит шестнадцатеричные константы, то удобно использовать синтаксис двоичной экспоненты для преобразования констант:

fract16 f1= 0x1234;
fract32 f2 = 0x12345678;

станет

fract f1 = 0x1234p-15r;long fract f2 = 0x12345678p-31lr;

Множество встроенных функций (built-ins) больше не нужны, поскольку Вы перешли к использованию традиционных типов с фиксированной точкой – вместо этого можно использовать обычную арифметику. Соответствие между встроенными функциями для fract16 и fract32 и традиционными арифметическими действиями показано в таблице 1-18.

Таблица 1-18. Соответствие между встроенными функциями fract16 и строенными функциями fract32 и обычной арифметикой чисел с фиксированной точкой (Native Fixed-Point Arithmetic).

Встроенные функции fract16 или fract32 Традиционная арифметика с фиксированной точкой
fract16 f1, f2;
fract16 f3 = add_fr1x16(f1, f2);
fract f1, f2;
fract f3 = f1 + f2;
fract16 f1, f2;
fract16 f3 = sub_fr1x16(f1, f2);
fract f1, f2;
fract f3 = f1 - f2;
fract16 f1, f2;
fract16 f3 = mult_fr1x16(f1, f2);

fract f1, f2;
fract f3 = f1 * f2; // в режиме округления
                    // с отбрасыванием
fract16 f1, f2;
fract16 f3 = multr_fr1x16(f1, f2);

fract f1, f2;
fract f3 = f1 * f2; // в режиме округления
                    // biased/unbiased
fract16 f1, f2;
fract32 f3 = mult_fr1x32(f1, f2);
fract f1, f2;
long fract f3 = (long fract)f1 * (long fract)f2;
fract16 f1;
fract16 f2 = abs_fr1x16(f1);
fract f1;
fract f2 = absr(f1);
fract16 f1;
fract16 f2 = negate_fr1x16(f1);
fract f1;
fract f2 = -f1;
fract16 f1;
int n = norm_fr1x16(f1);
fract f1;
int n = countlsr(f1);
fract32 f1, f2;
fract32 f3 = add_fr1x32(f1, f2);
long fract f1, f2;
long fract f3 = f1 + f2;
fract32 f1, f2;
fract32 f3 = sub_fr1x32(f1, f2);
long fract f1, f2;
long fract f3 = f1 - f2;
fract32 f1;
fract32 f2 = negate_fr1x32(f1);
long fract f1;
long fract f2 = -f1;
fract32 f1;
int n = norm_fr1x32(f1);
long fract f1;
int n = countlslr(f1);
fract32 f1;
fract16 = trunc_fr1x32(f1);

long fract f1;
fract f2 = f1; // в режиме округления
               // с отбрасыванием
#include < fract2float_conv.h >
fract16 f1;
fract32 f2;
float f3;
f2 = fr16_to_fr32(f1);
f1 = fr32_to_fr16(f2);
f3 = fr16_to_float(f1);
f3 = fr32_to_float(f2);
f1 = float_to_fr16(f3);
f2 = float_to_fr32(f3);

fract f1;
long fract f2;
float f3;
f2 = f1;
f1 = f2;
f3 = f1;
f3 = f2;
f1 = f3;
f2 = f3;

Для удобства встроенные функции также предоставлены для того же функционала, что дают традиционные типы с фиксированной точкой, и нужно просто заменить в имени встроенной функции "fr" на "fx".

Например, если у Вас есть такой оригинальный код:

#include < fract.h >
#include < builtins.h >
 
fract16 offset = 0.5r16;
fract16 add_offset(fract16 f)
{
   return add_fr1x16(f, offset);
}

то Вы могли бы изменить его на следующий:

#include < stdfix.h >
#include < builtins.h >
 
fract offset = 0.5r;
fract add_offset(fract f)
{
   return add_fx1x16(f, offset);
}

хотя чище было бы написать:

#include < stdfix.h >
 
fract offset = 0.5r;
fract add_offset(fract f)
{
   return f + offset;
}

Имеется некоторое количество встроенных функций (built-ins), которые не могут напрямую отобразиться на арифметику с фиксированной точкой, но аналогичная функциональность все же доступна (подробнее см. таблицу 1-19). Эти встроенные функции выполняют дробное умножение 1.31 с округлением результата. Однако результат может не быть двоично идентичным результату, который был получен умножением традиционного long fract, даже в режиме округления к ближайшему значению, поскольку округление, выполняемое традиционными типами более точны, чем округление, предоставленное встроенными функциями. Рекомендуется использовать традиционную арифметику с фиксированной точкой, за исключением случаев, когда нужно побитно точно воспроизвести результаты Вашей предыдущей реализации. В таком случае Вы можете использовать побитно точный эквивалент встроенным функциям: mult_fx1x32x32,  mult_fx1x32x32NS и multr_fx1x32x32.

Таблица 1-19. Встроенные функции fract16 и fract32 и традиционная арифметика с фиксированной точкой, имеющие подобную семантику.

Встроенные функции fract16 или fract32 Традиционная арифметика с фиксированной точкой
fract32 f1, f2;
fract32 f3 = mult_fr1x32x32(f1, f2);

long fract f1, f2;
long fract f3 = f1* f2; // режим округления
                        // biased/unbiased
fract32 f1, f2;
fract32 f3 = multr_fr1x32x32(f1, f2);

long fract f1, f2;
long fract f3 = f1* f2; // режим округления
                        // biased/unbiased
fract32 f1, f2;
fract32 f3 = mult_fr1x32x32NS(f1, f2);
long fract f1, f2;
long fract f3 = f1* f2; // режим округления
                        // biased/unbiased

Имеется множество библиотечных функций, которые используют типы fract16 и fract32. Как общее правило, Вы можете просто заменить "fr" на "fx", чтобы получить библиотечную функцию, которая примет вместо типов fract16 и fract32 и/или вместо типов fract16 и fract32 возвратит традиционные типы с фиксированной точкой. Однако нет аналогичной версии с фиксированной точкой для типа вектора fract2x16 или для комплексных дробных типов complex_fract16 и complex_fract32, поэтому специальные усилия должны быть предприняты, когда используется смесь традиционных типов с фиксированной точкой и векторных или комплексных дробных типов. Типы fract2x16, complex_fract16 и complex_fract32 могут быть использованы с традиционными типами, пока правильно осуществляется доступ к данным членов класса в функциях конструктора и аксессора, приведенных в таблице 1-20.

Таблица 1-20. Функции Constructor и Accessor для использования с традиционными типами фиксированной точки (Native Fixed-Point) и с комплексными и векторными дробными типами (Complex and Vector Fractional).

Встроенная функция Описание
complex_fract16 ccompose_fx_fr16 (fract real,     
                                  fract imag);
Создает значение типа complex_fract16 из типов fract вещественной и дробной части.
fract real_fx_fr16 (complex_fract16 c); Распаковывает вещественную часть типа fract из комплексного значения типа complex_fract16.
fract imag_fx_fr16 (complex_fract16 c); Распаковывает мнимую часть типа fract из комплексного значения типа complex_fract16.
complex_fract32 ccompose_fx_fr32(long fract real,
                                 long fract imag);
Создает значение complex_fract32 из типов long fract вещественной и реальной части комплексного числа.
long fract real_fx_fr32 (complex_fract32 c); Распаковывает вещественную часть числа типа long fract из комплексного значения типа complex_fract32.
long fract imag_fx_fr32 (complex_fract32 c); Распаковывает мнимую часть числа типа long fract из комплексного значения типа complex_fract32.
fract2x16 compose_fx_fr2x16 (fract x, fract y); Создает значение типа fract2x16 из двух величин типа fract.
fract low_of_fx_fr2x16 (fract2x16 vec); Распаковывает младшую часть типа fract из значения типа fract2x16.
fract high_of_fx_fx2x16 (fract2x16 vec); Распаковывает старшую часть типа fract из значения типа fract2x16.

Система именования библиотечных функций, которые берут смесь типов фиксированной точки и типов fract2x16, complex_fract16 или complex_fract32, добавляют префикс "fx_" перед "fr2x16", "fr16", или "fr32" в имени функции. Вы можете проверить имя, когда консультируетесь со страницей документации для библиотечной функции. Обратите внимание, что не требуется изменять имена функций, которые не используют типы fract16 или fract32.

В этой врезке рассматривается пример программы для вычисления вариантности массива 16-битных дробных чисел.

Вариантность (variance) массива значений samples[] вычисляется по формуле:

variance formula1

где n количество выборок в массиве.

Как это перекладывается на типы с фиксированной точкой? Выборки в массиве samples составляют дробные (fract) значения, так что для вычисления суммы всех значений выборок нужен тип с размерностью, который больше чем этот дробный тип. Если имеется меньше 256 выборок, то определенно сумма уложится в тип accum без появления насыщения. Тот же аргумент прикладывается к сумме квадратов выборок.

Однако выше приведенная формула также нуждается в вычислении промежуточного результата sample_length * sum(samples[i] * samples[i]). Умножение на sample_length означает, что не обязательно результат умножения уложится в диапазон типа accum.

Эквивалентная формула для вариантности будет следующая:

variance formula2

Это альтернативное определение формулы означает, что промежуточные результаты можно вычислить с помощью типа accum. Возможная реализация вычислений вариантности массива 16-битных дробных чисел показана в листинге ниже.

#include < stdfix.h >
#include < builtins.h >
 
// FX_CONTRACT ON гарантирует, что компилятор распознает
// идиомы accum += fract * fract
 
#pragma FX_CONTRACT ON
fract fract_variance(const fract *samples, int sample_length)
{
   fract variance = 0.0r;
   if (sample_length > 1)
   {
#pragma FX_ROUNDING_MODE UNBIASED
      int i, saved_rnd_mod = set_rnd_mod_unbiased();
      accum diff, sum_of_samples = 0.0k, sum_of_squares = 0.0k;
      long fract mean;
      // это гарантирует, что не будет насыщения, пока
      // sample_length < = 255
      for (i = 0; i < sample_length; i++)
      {
         sum_of_samples += samples[i];
         sum_of_squares += samples[i] * samples[i];
      }
      mean = sum_of_samples / sample_length;
      diff = sum_of_squares - (mean * sum_of_samples);
      variance = diff / (sample_length - 1);
      restore_rnd_mod(saved_rnd_mod);
   }
   return variance;
}

Сначала подключается заголовочный файл stdfix.h, чтобы можно было использовать обычный синтаксис fract и accum. Следующее, что Вы можете заметить, это явное использование #pragma FX_CONTRACT ON. Поскольку это поведение по умолчанию для режима FX_CONTRACT, то здесь не строго обязательно использовать такую прагму, но это полезно для того, чтобы четко представлять себе поведение кода в программе.

Функция вычисляет вариантность только если в массиве имеется больше одной выборки, иначе функция просто вернет 0.

Затем функция устанавливает режим округления. Здесь используется unbiased округление, чтобы достичь наибольшей точности результата. Это достигается совместным использованием прагмы FX_ROUNDING_MODE UNBIASED и встроенной функции set_rnd_mod_unbiased, как это обсуждалось ранее в разделе "Установка режима округления".

Цикл вычисляет сумму выборок и сумму квадратов. Поскольку режим FX_CONTRACT включен (ON), то точность не теряется, так как дробные числа перемножаются друг с другом и суммируются в типе accum.

После цикла сумма выборок делится на sample_length, чтобы получить среднее значение. Оно должно быть в диапазоне [-1.0,1.0). Значение сохраняется в переменную типа long fract, чтобы максимально сохранить точность.

Далее функция вычисляет разность между суммой квадратов и результатом среднего значения суммы выборок массива samples. Поскольку абсолютное значение среднего меньше или равно 1, то этот результат умещается в типе accum, и поскольку этот результат и сумма квадратов обе не отрицательные, то разность должна также уместиться в accum.

И наконец, вариантность вычисляется делением этой разницы на число, на единицу меньшее sample_length. Теоретически это значение может быть больше единицы; в нашем случае возвращаемое значение тогда получит насыщение, так что получится FRACT_MAX.

[Ссылки]

1. VisualDSP++ 5.0 C/C++ Compiler and Library Manual for Blackfin® Processors site:analog.com.
2. Встроенные функции компиляторов Blackfin VisualDSP и GCC.
3. Опции командной строки компилятора Blackfin.
4. VisualDSP: использование форматов переменных.
5. Поддержка традиционных типов с фиксированной точкой в VisualDSP++.

 

Добавить комментарий


Защитный код
Обновить

Top of Page