Частный случай конвертера IEEE754 в строку.

Как это реализовано? Достаточно просто — float конвертируется в Int, методом денормализации. Отбрасываемые в ходе денормализации биты не теряются, а накапливаются в качестве дробной части числа. Для получения четырёх (а зачем больше?) знаков после запятой, дробная часть умножается на 10000.
Оба получившихся целых числа конвертируются в строки, строки объединяются и форматируются. Всё.
Подлежащее конверсии число передаётся в процедуру внутри первого аккумулятора.
Целочисленная математическая библиотека использует два аргумента — аргумент var1 хранится в регистрах r2~r9 и имеет размер до 64х бит. Аргумент var2 хранится в регистрах r10~r13 и имеет размер до 32х бит.
Эта асимметрия имеет естественную природу, поскольку умножение 32-битных чисел даёт в итоге 64 бита, и размеры аргументов позволяют провести операцию вида var1=var1*var2, для восьми-, шестнадцати-. двадцатичетырёх-, и 32-битных чисел, и обеспечить место для результата.
Два аргумента с плавающей точкой хранятся там же, только в некотором роде «наоборот». Первый аккумулятор занимает var2, а второй — var1, сейчас поясню почему именно так. Каждый из аккумуляторов имеет размер 32 бита, и соответственно занимает 4 регистра; число, хранящееся в этих регистрах имеет стандартный формат ieee754. Когда с числом производятся какие-либо действия, оно распаковывается — младшие 24 бита занимает нормализованная мантисса, старшие 8 бит занимает экспонента в дополнительном коде, а знак временно хранится в регистре T. Псевдонимы регистров, хранящих мантиссу — mant1l, mant1m, mant1h — младший, средний и старший байты соответственно. в exp1 находится экспонента. После математических действий число опять упаковывается в формат ieee или теряется, если не требуется.
И если хранить первый аккумулятор там же, где хранится первый аргумент для целочисленных процедур — то станет невозможным использование для работы с float уже готовых процедур целочисленного умножения — расширяясь в ходе умножения, первый аргумент затрёт содержимое экспоненты первого float. А второй float и так типа жертвенный, после операции он не потребуется, пусть его ломает, не жалко. Вот как-то так.
Источником вдохновения послужил распостранённый в сети текст, содержащий строку
“;**** FP Math Routines: ****
; ** Very similar to _ecvt() **”
Именно оттуда была взята заготовка для конвертера float=>int, опилена напильником, дополнена сохранением дробной части и выводилками.
Как всегда, текст можно целиком скопировать в новый проект студии, и погонять под отладчиком, глядя на результат.
Обратите внимание — при формировании строки она создаётся с хвостом из ведомых нулей, которые потом отбрасываются вставлением AZ терминатора. При этом символы нулей физически остаются в памяти, но никому не мешают…
Ещё одна особенность — ошибка в один отсчёт на экстремально малых значениях, например вместо 0.001 выводится 0.0009, но это связано взаимодействием двух факторов — во первых, сам по себе float дыряв как дуршлаг на голове пастафарианца, и физически не содержит числа 0,001. Вместо него используется число 0.0010000000475, и когда при формировании дробной части отсекаются биты, это число превращается в 0.000999994575977, затем выводилка отбрасывает пятый и последующие знаки после десятичной точки — и мы имеем что имеем.
Поэтому добрый дядя Питер Нортон в своём руководстве по программированию и предупреждал нас —
; Первый аргумент
.def var10=r2
.def var11=r3
.def var12=r4
.def var13=r5
.def var14=r6
.def var15=r7
.def var16=r8
.def var17=r9
; второй аргумент
.def var20=r10
.def var21=r11
.def var22=r12
.def var23=r13
.def var24=r14
.def var25=r15
.def temp=r16
; Первый аккумулятор соответствует VAR2, чтобы сохранить компактность и избежать повреждения порядка при умножении
.def mant1l = r10
.def mant1m = r11
.def mant1h = r12
.def exp1 = r13
; второй аккумулятор соответствует VAR1
.def mant2l = r2
.def mant2m = r3
.def mant2h = r4
.def exp_2 = r5
.dseg
strbuf: .byte 8
.cseg
; стек
ldi r16, low(RAMEND)
out spl, r16
ldi r16, high(RAMEND)
out sph, r16
; указатель на буфер для вывода строк
ldi xh, high(strbuf)
ldi xl, low (strbuf)
; указатель на массив тестовых чисел
ldi zh, high(TestData<<1)
ldi zl, low (TestData<<1)
; девять раз будем повторять для 9ти разных чисел.
ldi r17, 9
Convertloop:
rcall Fldz1 ; загрузили float
rcall F2str ; выгрузили ASCIIZ string (смотри дамп памяти)
dec r17
brne Convertloop
rjmp pc ; Halt. Поставьте сюда брейкпоинт при отладке
TestData: .dd 0xc9cccccc ; -1677721.5
.dd 0x4b7fffff ; 16777215
.dd 0x449a4005 ; 1234.0006
.dd 0x47f12040 ; 123456.5
.dd 0x4641cd80 ; 12403.375
.dd 0x43b92000 ; 370.25
.dd 0x3c000000 ; 0.0078125
.dd 0x3a83126f ; 0.001
.dd 0x399d4952 ; 0.0003
;****************************************************************************
; Конверсия Float из первого аккумулятора в строку.
; 360-911 тактов
; На входе - iee754 single в первом аккумуляторе.
; X pointer - указатель на буфер для текстовой строки.
; Результат работы - ASCIIZ строка типа 123.45 в буфере.
; 4 знака после точки, нули в хвосте отброшены.
; слишком малые числа представляет как "0.0" слишком большие как "tolrg"
; Портит первый и второй аккумуляторы, то есть var20~var25 и var10~var17
; также портит r16 и перемещает указатель X на конец строки
;****************************************************************************
F2str: bst exp1, 7 ; знак отправляем в T
brtc pc+3
ldi temp, '-' ; если знак отрицательный
st x+, temp ; начинаем строку с минуса
;---------------------------------------
ldi temp,0x80
lsl mant1h ; старший бит из мантиссы
rol exp1 ; переносится в экспоненту
eor exp1, temp
sec ; единицу
ror mant1h ; задвигаем в старший бит мантиссы
cp exp1, temp ; проверка на ноль
brne pc+2
rjmp ftstrzres ; Ответ: 0,0
; Проверка на результат больше 16777215
ldi temp, 22
sub temp, exp1
brpl pc+2
rjmp ftstrmaxres ; Ответ: tolrg
; Проверка на слишком малое число
cpi temp, 36
brlo pc+2
rjmp ftstrzres ; Ответ: 0,0
clr var24
clr var25
; Быстрый сдвиг на 8 порядков.
cpi temp,8 ; use fast byte-move, if possible
brlo pc+8
mov var24, var25 ; выдвигаем дробную часть в var24:25
mov var25, mant1l
mov mant1l, mant1m
mov mant1m, mant1h
clr mant1h
subi temp,8
rjmp pc-8
; После быстрых сдвигов порядок не нулевой ?
tst temp
breq pc+8 ; не делаем сдвигов, если не надо
; Делаем сдвиги по одному порядку за раз
lsr mant1h
ror mant1m
ror mant1l
ror var25
ror var24
dec temp
brne pc-6
; и вот тут в mant1 находится целая часть
; а в var24:25 дробная.
mov var10, mant1l
mov var11, mant1m
mov var12, mant1h
rcall Bin2str24a
ldi temp, '.' ; десятичная точка
st x+, r16
; дробную часть множим на 10k
; кастомная процедура var10 = (var24*10000)/65536 временные var12 r16
ldi temp, low (10000)
mul var25, temp ; bx
mov var12, r0
mov var10, r1
mul var24, temp ; by
add var12, r1
adc var10, var16
adc var11, var16
ldi temp, high(10000)
mul var24, temp ; ay
add var12, r0
adc var10, r1
adc var11, var16
mul var25, temp ; ax
add var10, r0
adc var11, r1
; конец хитрого умножения на 10000
; Теперь в var10:11 у нас число 0~9999, отражающее дробную часть
; Преобразуем его в строку. Алгоритм, формирующий обратную строку тут будет полезен
; возможностью быстро дополнить строку ведущими нулями
rcall Bin2str16b
ldi temp, 0x04
; Дополнение строки ведущими нулями
mov var10, temp
ldi temp, 0x30 ; "0"
cp var17, var10 ; == strlen ?
brcc pc+4
st x+, temp ; укладываем "ноль" в строку
inc var17 ; до достижения строкой нужной длины
rjmp pc-4
; Переворот обратной строки - загрузка
ld var10, -x
ld var11, -x
ld var12, -x
ld var13, -x
; и сохранение в другом порядке
st x+, var10
st x+, var11
st x+, var12
st x+, var13
; Удаление завершающих нулей (если есть)
ld r16, -x ; извлекаем последний символ из строки
cpi r16, 0x30 ; "ноль" ?
brne pc+3
dec var17 ; повторяем только до ограничителя,
brne pc-4 ; чтобы в любом случае оставить один ноль после точки
adiw xl, 1
st x+, var16 ; ZS terminator
ret
; Результат: tolrg
ftstrmaxres: ldi r16, 't'
st x+, r16
ldi r16, 'o'
st x+, r16
ldi r16, 'l'
st x+, r16
ldi r16, 'r'
st x+, r16
ldi r16, 'g'
st x+, r16
ldi r16, 0
st x+, r16
ret
; Результат: 0.0
ftstrzres: ldi r16, 0x30
st x+, r16
ldi r16, 0x2e
st x+, r16
ldi r16, 0x30
st x+, r16
ldi r16, 0
st x+, r16
ret
;****************************************************************************
; Загрузка первого аккумулятора из памяти программ по указателю Z
;****************************************************************************
Fldz1: lpm mant1l, z+
lpm mant1m, z+
lpm mant1h, z+
lpm exp1, z+
ret
; Дальши пошли процедуры конвертеров число-строка.
; Отдельно для 24х бит, отдельно для 16-ти.
; впрочем, можно использовать только одну - немного упадёт быстродействие, и надо будет изменить процедуру
; добавления ведущих нулей и удаления ведомых
;****************************************************************************
; Преобразование 16бит числа в строку, используя быстрое деление на 10
; авторство кода http://forum.easyelectronics.ru/viewtopic.php?p=240893&sid=8d8b529647d46d09ac70820dbbf982e8#p240893
; назначение регистров var10:11 - число, на выходе строка
; Временные регистры: var13 - temporary lsb; var14:15 = var10/0x0A var16 = zero
; На выходе - "обратная строка" по указателю в X, lsb впереди, msb сзади.
; var17 = счётчик глубины строки
; 48 тактов на числе 5 (наилучшее время)
; 208 тактов на числе 12345 (наихудшее время)
;****************************************************************************
Bin2str16b: clr var17 ; strlen count init
; начало алгоритма быстрого деления на 10
clr var16 ; zero
bin16tobcdloop:
ldi r16, 0xcd
mul r16, var10
mov var13, r1
ldi r16, 0xcc
mul r16, var11
mov var14, r0
mov var15, r1
mul r16, var10
add var13, r0
adc var14, r1
adc var15, var16 ; +zero
ldi r16, 0xcd
mul r16, var11
add var13, r0
adc var14, r1
adc var15, var16 ; +zero
// quotient >>= 3
lsr var15
ror var14
lsr var15
ror var14
lsr var15
ror var14
; Конец алгоритма быстрого деления на 10. до этого момента - 28 тактов.
// multiple quotient back (*10)
ldi r16, 10
mul var14, r16 ; Нас интересует только младший разряд, остальные не участвуют в формировании остатка
sub var10, r0
; В этот момент - в var14:15 число, делённое на 10, в var10 - остаток
ldi r16, 0x30
add r16, var10 ; bcd 2 ascii
st x+, r16 ; store it
inc var17 ; count++
; результат деления опять помещаем в делимое и повторяем при необходимости
mov var10, var14
mov var11, var15
or var15, var14 ; zero?
brne bin16tobcdloop
ret
;****************************************************************************
; Преобразование 24 бит беззнакового числа в строку
; по следам публикаций http://we.easyelectronics.ru/Soft/preobrazuem-v-stroku-chast-2-chisla-s-fiksirovannoy-i-plavayuschey-tochkoy.html
; и http://forum.easyelectronics.ru/viewtopic.php?p=240893&sid=8d8b529647d46d09ac70820dbbf982e8#p240893
; низкооптимизированная версия, заточенная на минимизацию использованных регистров и естественное представление строки
; назначение регистров var10:11:12 - число, var10 lsb
; На выходе строка по указателю в X, естественное представление, готова для вывода
; портит r17, r16, var1x
;****************************************************************************
Bin2str24a: clr var17 ; str count clear
clr var16 ; 0
; Цикл - последовательное деление на 10
bin24tostrloop:
; Начало алгоритма быстрого деления 24 бит на константу 10
clr var15
; var11~15 = var1x * 0xcccccd (abc* xyz)
ldi r16, 0xCC
mul r16, var12 ; a*x | a*y
mov var13, r0
mov var14, r0
add var14, r1
adc var15, r1
inc r16
mul r16, var12 ; a*z
add var12, r0
adc var13, r1
adc var14, var16 ; +zero
adc var15, var16 ; +zero
dec r16
mul r16, var11 ; b*x | b*y
mov var12, r0
add var13, r0
adc var14, r1
adc var15, var16
add var13, r1
adc var14, var16 ; +zero
adc var15, var16 ; +zero
inc r16
mul r16, var11 ; b*z
mov var11, r0
add var12, r1
adc var13, var16 ; +zero
adc var14, var16 ; +zero
adc var15, var16 ; +zero
dec r16
mul r16, var10 ; c*x | c*y
add var12, r0
adc var13, r1
adc var14, var16 ; +zero
adc var15, var16 ; +zero
add var11, r0
adc var12, r1
adc var13, var16 ; +zero
adc var14, var16 ; +zero
adc var15, var16 ; +zero
inc r16
mul r16, var10 ; c*z
add var11, r1
adc var12, var16 ; +zero
adc var13, var16 ; +zero
adc var14, var16 ; +zero
adc var15, var16 ; +zero
; сдвиг результата вправо на 3 бита
lsr var15
ror var14
ror var13
;-----------------------------
lsr var15
ror var14
ror var13
;-----------------------------
lsr var15
ror var14
ror var13
; Конец алгоритма быстрого деления на 10
;-----------------------------
; multiple quotient back (*10)
; математика в этой вселенной устроена таким образом, что если разделить число на 10, округлить и умножить на 10 снова
; то все разряды, кроме последнего не будут отличаться! Вы, может быть удивитесь, но это так.
; И при поразрядном умножении, только младший разряд множителя участвуют в формировании младшего разряда результата. Я проверял, это всегда так!
; Поэтому, чтобы найти остаток деления, не обязательно множить и вычитать всё число. Достаточно проделать этот трюк только с последним разрядом.
; можете меня не благодарить, ваш КЭП.
ldi r16, 10
mul var13, r16 ; Нас интересует только младший разряд, остальные не участвуют в формировании остатка
sub var10, r0 ; вычитаем из оригинала делённое- умноженное - округлённое.
ldi r16, 0x30
add r16, var10 ; bcd 2 ascii
push r16
inc var17 ; count++
; результат деления опять помещаем в делимое и повторяем процесс при необходимости
mov var10, var13
mov var11, var14
mov var12, var15
or var13, var15 ; zero?
or var13, var14
breq pc+2
rjmp bin24tostrloop
; извлечение строки из стека и укладка в буфер
pop r16
st x+, r16
dec var17
brne pc-3
ret
- +5
- 23 ноября 2020, 13:43
- Gornist
- 1
Файлы в топике:
fl2str.zip
Комментарии (0)
RSS свернуть / развернуть