Документация Engee

Арифметика с фиксированной точкой (Fixed-Point) в Engee

Страница в процессе разработки.

В мире численных вычислений подавляющее большинство задач решается с использованием чисел с плавающей точкой (Float32, Float64). Однако в реальных системах — таких, как микроконтроллеры, DSP, ПЛИС или ASIC — использование Float-типов может быть нежелательным или невозможным. Например, в микроконтроллерах семейства STM32 базовые модели не имеют аппаратной поддержки float, и операции с плавающей точкой в них выполняются значительно медленнее, чем целочисленные. В таких случаях применяется арифметика с фиксированной точкой (Fixed-Point), при которой дробные значения кодируются в целочисленном формате с заранее заданной длиной дробной части.

Числа с фиксированной точкой (Fixed-Point) — это способ представления дробных значений с помощью обычных целых чисел и заранее заданного масштаба (количества бит под дробную часть). Вместо ресурсоемкой арифметики с плавающей точкой (Float32, Float64), здесь используется простой целочисленный формат, где «двоичная запятая» сдвигается на заданное число бит.

Например, если длина дробной части , то число хранится как целое значение , потому что по формуле пересчета внутреннего значения (stored_integer) в реальное (real_value) получим . По формуле видно, что внутреннее значение сохраняется и равно , а при вычислениях оно интерпретируется как .

Такая арифметика имеет следующие преимущества:

  • Меньшее потребление ресурсов (актуально для микроконтроллеров, ПЛИС и ASIC);

  • Контролируемая точность и диапазон значений;

  • Предсказуемое поведение при округлениях[1] и переполнениях[2];

  • Поддержка генератора кода на Verilog (HDL) и Cи.

Работа с фиксированной точкой в Engee

Для работы с фиксированными точками в Engee используется собственный пакет EngeeFixedPoint.jl, который заменяет стандартный для Julia пакет FixedPointNumbers.jl. В отличие от классического пакета, EngeeFixedPoint.jl предоставляет расширенные возможности и точный контроль над представлением и поведением чисел с фиксированной точкой — что особенно важно в системах с ограниченными ресурсами, при переносе вычислений в HDL, а также в задачах строгой точности.

Пакет EngeeFixedPoint.jl является стандартным пакетом Engee и входит в пользовательское окружение по умолчанию, поэтому не требует явного вызова (через import/using) в коде.

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

Fixed{S, W, f, T} <: FixedPoint

где:

  • S — знак (1 — знаковый[3], 0 — беззнаковый[4]);

  • W — длина слова (количество бит, выделенных на число);

  • f — длина дробной части (количество бит на дробную часть, масштаб);

  • T — тип целочисленного представления числа с фиксированной точкой (Int32, UInt64 и т.д.).

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


Для удобства EngeeFixedPoint.jl предлагает несколько способов задать тип фиксированной точки — от полного ручного задания до автоматического вывода.

S, W, f, T = 1, 25, 10, Int32

dt1 = Fixed{S, W, f, T}
dt2 = fixdt(S, W, f)
dt3 = fixdt(Fixed{S, W, f})
dt4 = fixdt(dt2)

println(dt1 == dt2 == dt3 == dt4)  # true

где:

  • dt1 = Fixed{S, W, f, T} — полное описание вручную;

  • dt2 = fixdt(S, W, f) — упрощенное создание, тип выбирается автоматически;

  • dt3 = fixdt(Fixed{S, W, f}) — получение типа на основе уже существующего описания;

  • dt4 = fixdt(dt2) — повторное использование, создает копию из существующего типа.

Все эти варианты создают один и тот же тип Fixed{1, 25, 10, Int32}, и могут использоваться в зависимости от задачи:

  • Полное описание (dt1) удобно, когда нужен контроль над всеми параметрами;

  • Упрощенный способ (dt2) подходит для типичных случаев и сокращает код;

  • Получение типа из типа (dt3) полезно при генерации кода или типизации данных;

  • Повторное использование (dt4) помогает работать с параметризированными структурами без повторного ввода параметров.

Конструкторы типа Fixed

Далее рассмотрим конкретные сценарии работы с фиксированными точками в Engee.

Так, можно напрямую задать тип и передать значение:

x = Fixed{1, 15, 2}(25)

Вывод:

fi(6.25, 1, 15, 2)

Это означает, что — целочисленное представление (stored_integer), а реальное значение (real_value) будет равно в соответствии с формулой .

Fixed{S, W, f}(i::T)

Конструктор создания фиксированной точки по целочисленному представлению Fixed{S, W, f}(i::T) принимает:

  • Параметры формата: S (знаковость), W (ширина в битах), f (дробная часть);

  • Целочисленное значение i типа T (внутреннее представление).

S, W, f = 1, 15, 2  # знаковый, 15 бит, 2 бита дробной части
i = 25
x = Fixed{S, W, f}(i) # создание из целого числа

Вывод:

fi(6.25, 1, 15, 2)  # эквивалентное представление

Fixed{S, W, f, T1}(i::T2)

Аналогичный с предыдущим конструктор, с возможностью явного указания типа хранения. Тип будет автоматически подобран в соответствии с параметрами S, W, f, независимо от указанного T1.

T = Int128
x = Fixed{S, W, f, T}(i) # с указанием типа хранения

Вывод:

fi(6.25, 1, 15, 2)  # результат идентичен

Конструкторы из FixedPointNumbers.jl

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

Поддерживаются:

  • Fixed{T, f}(i::Integer, _) — конструктор по целочисленному представлению. Принимает тип T и параметр f;

  • Fixed{T, f}(value) — конструктор по реальному значению (float).

Пример:

T = Int32
x1 = Fixed{T, f}(i, nothing) # из целого числа
x2 = Fixed{T, f}(i)          # из вещественного числа

Вывод:

6.25    # результат первого конструктора
25.0    # результат второго конструктора

Вспомогательные методы fi

Основной удобный способ создания чисел с фиксированной точкой это вспомогательные методы fi. В отличие от конструкторов, они автоматически определяет параметры представления.

x1 = fi(3.37, 0, 63, 4)        # Полный формат с явным указанием параметров
x2 = fi(3.37, fixdt(0, 63, 4)) # Через тип данных
x3 = fi(3.37, 0, 63)           # С автоматическим определением дробной части
x4 = fi(100, 1, 8, 5)          # Демонстрация обработки переполнения

Вывод:

3.375     # значение с учетом округления
true      # x1 и x2 идентичны
3.37      # с автоматическим подбором
3.96875   # результат насыщения при переполнении

Комплексные числа

Полная поддержка комплексных чисел с фиксированной точкой с теми же методами создания через fi:

s, w, f = 1, 62, 7;
v = 2.5 - 3.21im
x1 = fi(v, s, w, f)
x2 = fi(v, fixdt(s, w, f))
x3 = fi(v, s, w)
println(x1)
println(x1 == x2)
println(x3)
println()

Вывод:

fi(2.5, 1, 62, 7) - fi(3.2109375, 1, 62, 7)*im
true
fi(2.5, 1, 62, 59) - fi(3.21, 1, 62, 59)*im

Работа с массивами и матрицами

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

Векторы

Создание и работа с одномерными массивами. Параметры фиксированной точки применяются ко всем элементам:

s, w, f = 1, 62, 7  # знаковый тип, 62 бита, 7 бит дробной части
v = [1, 2, 3]       # исходный вектор

# Разные способы создания:
x1 = fi(v, s, w, f)        # с явным указанием параметров
x2 = fi(v, fixdt(s, w, f)) # через тип данных
x3 = fi(v, s, w)           # с автоматическим определением дробной части

println(x1)
println(x1 == x2)
println(x3)

Вывод:

Fixed{1, 62, 7}[1.0, 2.0, 3.0]
true
Fixed{1, 62, 59}[1.0, 2.0, 3.0]

Комплексные матрицы

Полноценная поддержка комплексных чисел в многомерных массивах:

s, w, f = 1, 62, 7
m = [im 2.5; -1.2im 25-im]

# Рабочие способы создания:
x1 = fi(m, s, w, f)        # с явным указанием параметров
x2 = fi(m, fixdt(s, w, f)) # через тип данных

println(x1)
println(x1 == x2)

Вывод:

Complex{Fixed{1, 62, 7, Int64}}[fi(0.0, 1, 62, 7) + fi(1.0, 1, 62, 7)*im fi(2.5, 1, 62, 7) + fi(0.0, 1, 62, 7)*im; fi(0.0, 1, 62, 7) - fi(1.203125, 1, 62, 7)*im fi(25.0, 1, 62, 7) - fi(1.0, 1, 62, 7)*im]
true

Базовые операции и методы

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

Граничные значения

Методы typemax и typemin позволяют определить максимальное и минимальное возможные значения для конкретного типа фиксированной точки.

dt = fixdt(0, 25, -2)  # беззнаковый тип с 25 битами и дробной частью -2
x = fi(1.5, dt)        # создаем число фиксированной точки
println(typemax(x))    # 1.34217724e8 - максимальное представимое значение
println(typemin(x))    # 0.0 - минимальное значение для беззнакового типа

Математические операции

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

x1 = fi(1.5, 0, 15, 3)
x2 = fi(1.5, 1, 25, 14)
y1 = x1+x2
y2 = x1-x2
y3 = x1*x2
y4 = x1/x2
println(y1)
println(y2)
println(y3)
println(y4)
println(typeof(y1))
println(typeof(y2))
println(typeof(y3))
println(typeof(y4))

println(x1 == x2)
println(x1 <= x2)
println(x1 > x2)

Вывод:

3.0
0.0
2.25
0.0
Fixed{1, 28, 14, Int32}
Fixed{1, 28, 14, Int32}
Fixed{1, 40, 17, Int64}
Fixed{1, 25, -11, Int32}
true
true
false

Округление

Различные стратегии округления позволяют контролировать точность вычислений. По умолчанию используется округление RoundNearestTiesUp.

x = fi(1.5, 1, 14, 3)  # знаковый, 14 бит, 3 бита дробной части
println(round(x))      # 2.0 - округление к ближайшему целому (1.5 → 2)
println(trunc(x))      # 1.0 - отбрасывание дробной части
println(ceil(x))       # 2.0 - округление вверх к большему целому
println(floor(x))      # 1.0 - округление вниз к меньшему целому

где:

  • round — банковское округление (к ближайшему четному при 0.5);

  • trunc — отбрасывание дробной части;

  • ceil — всегда в большую сторону;

  • floor — всегда в меньшую сторону.

Преобразование типов (конвертация)

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

x = fi(1.5, 1, 12, 4)
y1 = Int64(x)
y2 = UInt8(x)
y3 = Float64(x)
y4 = convert(fixdt(0, 5, 2), x)
println(y1)
println(y2)
println(y3)
println(y4)
println(typeof(y1))
println(typeof(y2))
println(typeof(y3))
println(typeof(y4))

Вывод:

1
1
1.5
1.5
Int64
UInt8
Float64
Fixed{0, 5, 2, UInt8}

Вывод

Таким образом, пакета EngeeFixedPoint.jl предоставляет следующие преимущества:

  • Расширенная система типов:

    • Полная поддержка как знаковых, так и беззнаковых чисел;

    • Произвольная разрядность (любые битовые размеры, не только 8/16/32/64/128);

    • Гибкая настройка дробной части (включая отрицательные значения и случаи, когда , длина дробной части, больше длины слова ).

  • Улучшенные правила вывода типов:

  • Платформо-зависимая кодогенерация:

    • Разные правила наследования типов для целевых платформ (Си или Verilog);

    • Предсказуемое поведение при переполнении 128-битной границы (в отличие от аналогов).

  • Расширенный функционал:


1. Округление — это процесс приведения значения к допустимому виду с учетом ограниченной дробной части (f). Поскольку числа с фиксированной точкой не могут точно представить все возможные дробные значения, при операциях результат округляется согласно заданной стратегии (например, до ближайшего значения или с усечением).
2. Переполнение — это процесс, который происходит если результат вычислений выходит за пределы, допустимые для заданного размера слова (W) и знаковости (S). В таких случаях применяется стратегия обработки переполнения: насыщение (saturation), при котором значение ограничивается максимальным/минимальным допустимым, либо усечение или вывод ошибки.
3. Знаковые — могут хранить как положительные, так и отрицательные значения. Пример: Int8, Int16, Fixed{1, 16, 4}.
4. Беззнаковые — могут хранить только положительные значения и ноль. Пример: UInt8, UInt16, Fixed{0, 16, 4}.