nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

QuatCore: printf - беззнаковые числа

У нас появилось "устройство вывода", а именно UART, и мы написали процедуру print, которая выдаёт строку. Теперь хочется научиться "вклинивать" в эту строку числа, для начала 16-битные целые, знаковые и беззнаковые, мы ведь в них работаем.

Один раз мы делали нечто подобное, но на верилоге. Там очень эффективным оказался метод Double Dabble ("сдвинуть и прибавить 3"). Тогда специальный модуль осуществлял преобразование 16-битного числа в 20-битное двоично-десятичное и занимал 45 ЛЭ.

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



Ситуация осложняется тем, что наш процессор почти что не умеет в логику - его АЛУ максимум умеет сдвигать влево-вправо, все остальные операции арифметические. Так что возиться по каждым 4 битам, как-то к ним прибавляя значения, регистрируя переносы где надо, и всё в таком духе - не для нас :)

Начнём с беззнаковых целых чисел...


Я нашёл 5 "магических констант", которые позволяют решить задачу на удивление легко.
  BCDtable dw 0x1, 0xA, 0x64, 0x3E8, 0x2710


[Ну очень магические!]
  BCDtable dw 1, 10, 100, 1000, 10000



Мы начинаем выдавать разряд за разрядом, начиная с самого старшего. А именно, взявшись за k-й разряд, мы вычитаем из исходного числа BCDtable[k] до тех пор, пока не получим отрицательное значение. Количество вычитаний, не приведших к смене знака мы подсчитываем - это и будет очередной десятичный разряд. Далее уменьшаем k на единичку - и всё хорошо.

Вот как это выглядит на нашем укуренном ассемблере:
	;в аккумуляторе лежит беззнаковое число, которое мы хотим выдать в UART0 в десятичном виде.
	;для начала, всегда 5 разрядов, пусть даже старшие - нули
	;мы используем регистры i, k, Y, Acc
	Bin2Bcd proc
			k		4	;номер обрабатываемого разряда
	@@start:	i		-1	;получившийся десятичный разряд
			Y		BCDtable
	@@sub:		SUB		[Y+k]
			i++		0
			JGE		@@sub
			ADD		[Y+k]
			[SP]		Acc		;сейчас аккумулятор пригодится...
			Acc		'0'
			ADD		i
			UART0		Acc
			Acc		[SP]
			kloop		@@start	
			JMP		[--SP]
	Bin2Bcd endp


Всего 14 слов, или 28 байт в памяти - не так уж плохо.

Для проверки функционирования напишем программку, которая выведет нам степени двойки.

;простейший Bin2Bcd
%include "Win1251.inc"
.rodata
	OurString  Int16 'С','т','е','п','е','н','и',' ','д','в','о','й','к','и',':'
	CRLF       Int16 13,10,-32768
	BCDtable   dw    1,10,100,1000,10000
.data
	Stack	dw	?,?
.code
	main proc
				SP		Stack
				X		OurString
				CALL 		print
				X		CRLF
				j		15
				ACC		1
		@@start:	C		Acc
				CALL		Bin2Bcd	;очередное число
				CALL		print	;перенос строки
				Acc		C
				ADD		Acc
				jLOOP		@@start
		@@endless: 	JMP 		@@endless
	main endp
	
	;X указывает на начало строки текста
	;конец обозначается числом -32768
	;пока что процедура категорически неряшливая:
	;меняет значение регистра i и Acc
	print proc
				i		0
		@@start:	ABS		[X+i]	;хитрая проверка на -32768
				JO		@@finish
				UART0	Acc
				i++		0
				JMP		@@start
		@@finish:	JMP		[--SP]
	print endp


Повторно приводить код процедуры Bin2Bcd мы не стали. При объявлении строк видим типично ассемблерный приём - строки OurString и CRLF имеют общий кусочек. То есть, когда мы начнём выводить OurString, то не остановимся на ней, и ещё выведем содержание CRLF, и только там остановимся на числе -32768.

Глянем, как оно работает на эмуляторе:


Хорошо работает! Однако нули в начале числа - как-то неаккуратно!

Можно модернизировать процедуру, чтобы их убрать. Для этого нужно хранить информацию, была ли у нас хоть одна значащая цифра? Пока её не было, мы должны пропускать нули (вместо 00001 написать просто 1), а вот нули вслед за значащей цифрой мы пропускать не должны: заменить 100 на 1 - это катастрофа. Хранить эту информацию будем в [SP+1] - не хочется засорять лишние регистры, и так мы слишком много заняли, наверняка придётся в дальнейшем их в начале процедуры сохранять в стеке, а потом восстанавливать.

Но затем нужно ещё рассмотреть "специальный случай", когда наше число - НОЛЬ. По описанной логике, программа так и возвратила бы пустую строку, а нужно - чтобы этот последний ноль всё-таки отобразился.

Все эти тонкости удлиняют нам программу на 6 строк. Любители языков высокого уровня плачут кровавыми слезами от происходящего в этом коде :)

	BetterBin2Bcd proc
			k		4	;номер обрабатываемого разряда
			[SP+1]		-1	;-1 означает, что значащих цифр ещё не было, 0-что появились
	@@start:	i		[SP+1]
			Y		BCDtable
	@@sub:		SUB		[Y+k]
			i++		0
			JGE		@@sub
			ADD		[Y+k]
			iLOOP		@@proceed	;прыжка не будет, если [SP+1]=-1 и текущий разряд нулевой
			kloop		@@start
			[SP+1]		0
	@@proceed:	[SP]		Acc		;сейчас аккумулятор пригодится...
			Acc		'0'
			ADD		i
			SUB		[SP+1]
			UART0		Acc
			[SP+1]		0
			Acc		[SP]
			kloop		@@start	
			JMP		[--SP]	
	BetterBin2Bcd endp


Раньше у нас всегда i начинал работать с -1, и по окончании цикла с вычитанием "магической константы" i показывал текущую цифру. Дальше мы прибавляли ASCII-значение "нуля" - и получали правильный символ, чтобы отправить его в UART.

Теперь i начинает работать со значения из [SP+1], либо -1, либо 0. По окончании цикла с вычитанием, у нас стоит команда
iLOOP @@proceed


Если i не равно нулю, то будет вычитаться единичка и выполняться переход на метку @@proceed. В противном случае - перехода не будет.
Так бывает в одном-единственном случае: если [SP+1]=-1, а текущая цифра - ноль. Тогда мы выполняем следующую команду:
kloop		@@start

если у нас ещё остались цифры, мы перейдём к следующей из них, "начисто проигнорировав" текущую, т.е ничего не выводя наружу. Так и надо. А вот если это уже была последняя цифра, то и этого перехода не будет совершено - мы положим в [SP+1] нолик - и выйдем всё на ту же метку @@proceed.

Там, как не сложно догадаться, мы всё-таки отображаем нашу цифру. Поскольку у нас i могла начинаться либо с -1, либо с 0, то дополнительная команда
SUB  [SP+1]

подправляет это значение. Тут ещё учитывается, что если был совершён прыжок по iLOOP @@proceed, то из i была вычтена единичка.

И наконец, команда
[SP+1] 0

указывает, что значащая цифра у нас появилась - и больше нули пропускать нельзя!


Вот новая программа для проверки всего этого безобразия (процедуры не приводим, print не изменилась, а BetterBin2Bcd мы видели только что):

%include "Win1251.inc"
.rodata
	OurString Int16 'С','т','е','п','е','н','и',' ','д','в','о','й','к','и',':',13,10,-32768
	ZeroStr Int16  'Н','о','л','и','к',':'
	CRLF Int16 13,10,-32768
	BCDtable dw 1,10,100,1000,10000
.data
	Stack	dw	?,?,?
.code
	main proc
			SP		Stack
			X		OurString
			CALL 		print
			X		CRLF
			j		15
			ACC		1
	@@start:	C		Acc
			CALL		BetterBin2Bcd	;очередное число
			CALL		print	;перенос строки
			Acc		C
			ADD		Acc
			jLOOP		@@start
			X		ZeroStr
			CALL		print
			Acc		0
			CALL		BetterBin2Bcd ;хотим проверить, "голый" ноль вообще отобразится?
	@@endless:	JMP	 	@@endless
	main endp


Вот что получается на эмуляторе:


Фурычит, однако, и нолик правильно отображает.

Ну и остаётся прошить это дело на ПЛИС. В этот раз задал скорость UART: 230 400 бод. При тактовой частоте в 4 МГц, это деление в 17 раз, погрешность 2%. Как показывает "картинка для привлечения внимания" - работает. В этот раз как-то вообще с пол-пинка зафурычило, меня это пугает...
Tags: ПЛИС, математика, программки, работа, странные девайсы
Subscribe

  • Я создал монстра!

    Вот нормальная счастливая пара разъёмов ОНЦ-БС-1-10/14-Р12-2-В и ОНЦ-БС-1-10/14-В1-2-В: У розетки кроме основного выступа, отмечающего "верх",…

  • Нахождение двух самых отдалённых точек

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

  • Слишком общительный счётчик

    Вчера я чуть поторопился отсинтезировать проект,параметры не поменял: RomWidth = 8 вместо 7, RamWidth = 9 вместо 8, и ещё EnableByteAccess=1, чтобы…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments