Поддержка традиционных типов с фиксированной точкой в VisualDSP++ Печать
Добавил(а) microsin   

Наверное Вы не раз встречали в исходном коде языка C/C++ константы и имена функций с суффиксами наподобие hr, r, lr, hk, k или lk. Это специальное расширение языка, предназначенное для упрощения доступа к арифметике с фиксированной точкой (fixed-point), операции насыщения (saturation) и т. п.

В стандарте ANSI C есть два широких класса арифметических тиров: целочисленные типы (char, int, long и т. д.) и типы с плавающей точкой (float, double). Арифметика для целочисленных (integer) типов быстрая, но эти типы представляют только целые числа с диапазоном, ограниченным разрядностью числа. Числа с плавающей точкой (floating-point) предоставляют способ расширить диапазон представляемых значений чисел, от величин очень близких к 0, до очень больших величин. Процессоры SHARC предоставляют быструю аппаратную поддержку для арифметики с плавающей точкой. Однако на процессоре Blackfin вызовы функций из библиотек реального времени используют программную эмуляцию арифметики с плавающей точкой. Это происходит прозрачно для C-программиста, однако приводит к значительным затратам процессорного времени.

Таким образом, на платформе Blackfin целые числа предоставляют большее быстродействие, чем числа с плавающей точкой, но это может обойтись дороже в плане затрат времени на разработку. Определенная проблема касается целочисленного переполнения. Если значение типа со знаком (signed) переполнится и выйдет из допустимого для типа диапазона, то результат окажется неопределенным, и Вы не можете положиться на его значение в программе. Если переполнится значение беззнакового типа (unsigned) и превысит свой допустимый диапазон, то результат по циклически поменяет свое значение (перепрыгнет через 0), так что, к примеру, инкремент очень большого положительного числа даст очень маленькое положительное число. Для приложений DSP получение такой проблемы не всегда то, что Вы хотите. Часто Вы хотели бы минимизировать потенциальную ошибку подобного рода применением насыщения в переполняемом значении, когда значение достигает самого большого допустимого (или минимально допустимого для числа со знаком) значения, которое может представить тип.

Примечание: насыщение это специальная функция, которая применяется для смягчения последствий переполнения целых чисел. Что такое насыщение, как оно работает и какие действия выполняет, см. Википедию или см. раздел "Saturation (насыщение)" в статье [2].

Как можно решить эти проблемы? Аппаратура ядра процессора Blackfin предоставляет поддержку типов чисел с фиксированной точкой (fixed-point types), которые в числе содержат дробную и возможно целую часть, но при этом отсутствует переменная экспонента, которая используется в типах чисел с плавающей точкой. Арифметика с фиксированной точкой быстрая (почти так же, как целочисленная), и предоставляет семантику для насыщения при переполнении. Использование этих типо в с фиксированной точкой - обычная практика в DSP-программировании. Как использовать на ассемблере понятно [2], но как использовать арифметику с фиксированной точкой на языке C? До некоторых пор способом получить доступ к вычислениям с фиксированной точкой на C были вызовы с помощью компилятора встроенных функций и функций библиотек времени выполнения. Однако этот подход может быть неуклюжим, и приводит к трудно портируемому коду.

Начиная с VisualDSP++ 5.0 update 9, код арифметики с фиксированной точкой может быть написан с использованием новых традиционных типов с фиксированной точкой (native fixed-point types), выраженных четко и кратко. Язык, выбранный для специальных вычислений с плавающей точкой, определен в части 4 ISO/IEC Technical Report 18037 "Extensions to support embedded processors" (расширения для поддержки встраиваемых приложений, см. [3]). С использованием этих расширений код, который Вы пишете, может быть легко портирован для использования с другими компиляторами, поддерживающими TR 18037 с подобными размерами типов с фиксированной запятой. Хотя эта статья относится прежде всего к Blackfin, подмножество поддержки типов fixed-point также доступно для SHARC. Подробности см. в документации на компилятор SHARC и руководстве пользователя для библиотек.

Давайте посмотрим, как работают типы с фиксированной точкой. Сначала нужно подключить файл заголовка stdfix.h, в котором декларируются полезные функции и макросы. После этого Вы можете использовать имена типов fract и accum вместе с квалификаторами short, long и unsigned. Обратите внимание, что только некоторые из этих комбинаций приводят к различным типам; наподобие стандарта языка C, стандарт TR 18037 имеет архитектурно-независимые определения синтаксиса и семантики, и некоторые подробности реализации будут изменяться в соответствии с низлежащей аппаратурой. Так что, к примеру, на архитектуре Blackfin типы int и long int оба представляют 32-битный целый тип, а short fract и fract оба представляют 16-битный дробный тип.

Все дробные типы со знаком (signed fract) имеют диапазон [-1, 1), в то время как дробные типы без знака (unsigned fract) получают значения в диапазоне [0, 1). Однако чем больше разрядность дробного числа, тем выше у него точность.

Для типов accum диапазон представляемых чисел составит [-256, 256) для чисел со знаком (signed accum), и [0, 256) для чисел без знака (unsigned accum). Подробнее про использование типов с фиксированной точкой см. раздел "Using Native Fixed-Point Types" в руководстве компилятора C и библиотек.

Как это можно использовать на практике? Как первый шаг в математику с фиксированной точкой, давайте напишем скалярное произведение (dot-product):

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

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

Во-вторых, взгляните на тело цикла for. Здесь умножение работает над двумя дробными типами, и делает точно то, что Вы ожидаете – перемножаются два дробных числа, и в результате получается другое дробное число. Например, 0.5 * 0.5 даст результат 0.25. Подобным образом сложение в переменной типа accum использует насыщение, если результат вышел за пределы диапазона значений accum.

Что произойдет, если мы будем использовать смешанные типы? В большинстве случаев получится то же самое, что Вы ожидаете. Например, если записать:

fract f = 0.25r;
f = 2 * f;

То вы получите результат 0.5. В сущности, Вы можете думать об арифметике с заданными точностью и диапазоном чисел, в которой будет происходить насыщение в результатах операций до значения, которое может представить тип числа. Опять-таки за получением подробностей см. руководство компилятора C и библиотек, или спецификацию TR 18037.

Все арифметические операции +, -, *, /, !, << и >> могут применяться для fixed-point типов. Дополнительно Вы можете использовать операторы отношений ==, !=, < , < =, > и >=. Это дает Вам дополнительную гибкость, чтобы более кратко выразить алгоритм. Однако стоит попробовать обратить внимание на то, что поддерживает Ваша низлежащая аппаратура. Точно так же, как и при использовании целочисленной арифметики, лучше избегать деления во внутренних циклах, потому что это обычно требует вызова функции при поддержке компилятором, и приведет к повышенному расходу ресурса выполнения процессора. Однако также стоит избегать, например, умножения типов accum (во внутренних циклах), поскольку у процессора Blackfin также нет для этого аппаратной поддержки. Использование unsigned-типов обычно приводит к немного менее эффективному коду, поскольку у процессора Blackfin есть более продвинутая поддержка дробной арифметики со знаком.

Стоит изучить возможности библиотечной поддержки операций для типов с фиксированной запятой. Technical Report определяет некоторое количество полезных функций-утилит для округления значений (round), подсчета бит знака или нуля, или для выполнения альтернативных типов деления или умножения. Библиотека также определяет спецификаторы типа, которые можно использовать в строках спецификации формата printf или scanf, чтобы осуществить ввод/вывод чисел с фиксированной точкой. Чтобы получить преимущество от такой поддержки ввода/вывода, убедитесь, что Ваша сборка использует разрешение на fixed-point I/O (с использованием опций командной строки –fixed-point-io или –full-io, см. [4]), поскольку эта поддержка не подключена в библиотеке по умолчанию, потому что увеличивает размер библиотечного кода.

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

• Для инициализации переменных, у которых тип с фиксированной точкой, всегда используйте для констант суффиксы, задающие тип fixed-point (r, k и т. п.). Не присваивайте битовые маски – например, число 0x4000, присвоенное типу fract, приведет сразу к насыщению, и даст результат 1.0, поскольку присваиваемое число выходит за диапазон [-1, 1).
• Если нужно присвоить -1 числу типа fract, используйте макрос FRACT_MIN (или LFRACT_MIN для типа long fract). Константа -1.0r, к сожалению, даст не то, что Вы ожидаете, так как в соответствии со стандартом C она означает –(1.0r), и поскольку 1.0 выходит за пределы диапазона числа fract, это будет вычислено в число несколько больше, чем -1.
• Присваивание дробного значения целому также вероятно приведет к ошибке. Например, если Вы присвоите число типа unsigned fract числу целого типа, то всегда получите 0, поскольку unsigned fract будет усечено до ближайшего целого числа.
• Смешивание в арифметических выражениях типов fixed-point и типов floating-point будет преобразовывать тип fixed-point в тип floating-point и выполнять арифметику floating-point.

Раздел 4 Technical Report 18037, который поддерживается начиная с VisualDSP++ 5.0 update 9, представляет стандарт для типов с фиксированной точкой, который Вам поможет писать на языке C/C++ чистый, удобный в сопровождении код, использующий возможности процессора Blackfin.

Константы для дробных чисел с фиксированной точкой (fixed-point) могут быть указаны в том же формате, как и константы с плавающей точкой (floating-point), включая любую десятичную или двоичную экспоненту. Для дополнительной информации см. "strtofxfx" на странице 3-330 руководства "C/C++ Compiler and Library Manual for Blackfin® Processors" (или врезку "Функция strtofxfx"). Суффиксы используются для идентификации типа констант. Заголовочный файл stdfix.h также предоставляет макросы для минимальных и максимальных значений чисел с фиксированной точкой, см. таблицу 1-16.

Таблица 1-16. Суффиксы и макросы для констант Fixed-Point.

Тип Суффикс Пример Минимальное значение Максимальное значение
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

Функция преобразует строку в число с фиксированной запятой (fixed-point).

#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);

[Описание]

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

Десятичная форма строки числа floating-point:

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

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

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

Шестнадцатеричная форма строки числа floating-point:

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

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

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

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

[Ошибки преобразования]

Функции strtofxfx возвратят 0, если не может быть произведено преобразование, и в объекте, на который указывает endptr, сохранен указатель, указывающий на недопустимую строку. Если корректное значение привело к переполнению (overflow), то будет возвращено максимальное положительное или отрицательное (как это необходимо) значение с фиксированной точкой. Если корректное значение привело к недогрузке (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.

[Ссылки]

1. Native Fixed-Point Types in VisualDSP++ 5.0 site:ez.analog.com.
2. Blackfin: система команд (ассемблер) - часть 1.
3. Programming languages - C - Extensions to support embedded processors site:open-std.org.
4. Опции командной строки компилятора Blackfin.
5. Встроенные функции компиляторов Blackfin VisualDSP и GCC.
6. VisualDSP: использование форматов переменных.
7. VisualDSP: использование типов с фиксированной точкой.