nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Программа захвата "во всей своей красе"

Пока ещё не испытанная, но по крайней мере в теории она может заработать (все части алгоритма уже написаны, очевидных "прорех нет") и компилируется в 82 слова (164 байта).

В отличие от "Аффинного алгоритма", который хоть и громоздкий, но состоял из ряда последовательных этапов (найти самую отдалённую точку, отсортировать остальные против часовой стрелки, найти матрицу аффинного преобразования, найти крен, найти масштаб, найти вектор), здесь отдельные части очень сильно переплетены, такого макаронного кода давненько у меня не было!



А ведь там ещё и 2 процедуры вызываются. Чувствую, что если сейчас не расскажу, как всё это безобразие работает (пусть даже никто это не прочитает) - сам позабуду напрочь, есть у ассемблера такая неприятное свойство Write-Only.


Начинается всё очень мирно.
j  1

Присваиваем j=1, чтобы можно было обращаться к [SP]..[SP+3] и [X]..[X+6], [Z]..[Z+6] и т.д.

ACQ		VSync

Посылаем видеопроцессору первое задание на обработку, с флажком "кадрового синхроимпульса". Это значит, что видеопроцессор будет ждать начала нового кадра, и только потом что-то попытается измерить. Далеко не факт, что выполнение нашей программы здесь застопорится. Так будет только если мы забьём входной буфер (FIFO) видеопроцессора до отказа. Так что мы пока пойдём дальше.

		k		10	
		Z		D1												
@@topRows:	ACQ		HSync
		kLOOP		@@topRows

"Пропускаем" несколько верхних строк, тех, что выходят за полезную область экрана. И сюда же "вклинилась" строка
Z  D1

то есть, регистр Z указывает на переменную D1 - расчётный диаметр (в пикселях) наших мишеней. Я изначально хотел его по величине экспозиции оценить, а экспозицию так подобрать, чтобы точки (те, что видны на "светлом" кадре и пропадают на "тёмном"), были довольно яркими, но не входили в насыщение. Тогда экспозиция будет функцией от дальности до мишеней, и можно будет плюс-минус лапоть понять, какой нам нужен диаметр. Но ещё подумаем. Может, и вовсе окажется, что D1=6, а дальше наш алгоритм сам диаметры оценит неплохо.

C		[Z+k]

Расчётный диаметр мишеней занесли в регистр C. Мы разнесли две этих команды (Z D1 и C [Z+k]) чтобы избежать Hazard'а. Ещё и по окончании цикла заведомо k=0, что тоже было необходимо, и пригодится на всём протяжении дальше.

[SP+2j]	ImgHeight


Тут нужно договориться о том, какие у нас локальные переменные. После долгих мучений пришёл к следующей схеме:
[SP] - совсем-совсем локальная, т.к затирается при вызове процедур (на её место приходит адрес возврата). Используется в одном месте, для сохранения расстояния между точками,
[SP+1] - X-координата ОБНАРУЖЕННОЙ ТОЧКИ
[SP+2j] = [SP+2] - Y-координата ОБНАРУЖЕННОЙ ТОЧКИ, или, что то же самое, ТЕКУЩАЯ СТРОКА.
[SP+2j+1]=[SP+3] - яркость ОБНАРУЖЕННОЙ ТОЧКИ.

Здесь, как видно, мы готовимся к циклу по всем строкам, и объявляем [SP+2] = ImgHeight = 720 для китайской камеры наблюдения, хотя есть основания его несколько поднять, об этом позже...

Ну и нумеровать решили сверху вниз, как принято в математике, и как всегда это сделано, поскольку привыкли для окончания цикла смотреть на знак. На самом деле, здесь с тем же успехом могли бы и от нуля пойти до 720, какая разница, SUB 0 или SUB 720? Раньше была, теперь нет :)

JMP		@@FinalRange


Циклы по строкам и внутри строки у нас довольно интересные - мы входим в них далеко не сверху, и выходим посерёдке.

В цикле внутри строки мы просматриваем строку слева направо, причём мы сначала ЗАПРАШИВАЕМ у видеопроцессора, что же у него получилось на каждом из отрезков, и только потом РАЗДАЁМ ЕМУ ЗАКАЗЫ. В противоположном порядке оно как-то "не клеилось". Но тогда начать надо "с конца" - перепрыгнуть на тот участок, где остались "необработанные пиксели", и заказать обработку их всех. Вот это и делает данная команда.

@@newRow:		X		ActivePoints


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

А идти мы начинаем по списку обнаруженных ярких точек, которые пока "активны", то бишь имеют шанс появиться на данной строке. (потихоньку они уходят из этого списка в список AllPoints, и рано или поздно все там окажутся)

Регистр базового адреса X подходит как нельзя лучше для прохода по этому списку :) Сейчас, по завершении команды, по адресам [X]..[X+5] будет лежать фиктивная левая точка (в данном случае "левая" - не синоним "фиктивной" :)), причём по адресу [X] лежит указатель на следующую точку, первоначально - на фиктивную правую точку. А в ней указатель уже пустой - правее неё ничего быть не может!

@@ActPointsStart:	[SP+2j+1]	GPU	
			[SP+1]		GPU	
			Acc		Threshold
			SUB		[SP+2j+1]
			JGE		@@MidCycle


И вот мы получаем от видеопроцессора пару значений, первое: макс. яркость на отрезке, второе: X-координата самой яркой точки на отрезке. Их мы сразу же помещаем в соответствующие локальные переменные. А в аккумуляторе мы сравниваем яркость с пороговой (из порога вычитаем яркость). И если яркость оказалась ниже - мы сразу же выходим в середину цикла по строке. Когда мы описывали совмещённый модуль для видеопроцессора, там выходила некая хитрость с максимальной яркостью - она выходила инвертированной. Сейчас мы пока про это забыли, но если что, JGE поменять на JL дело несложное. И порог нужно грамотно поставить, с учётом наших выкрутасов

	Z	X
	i	1

Это мы серьёзно готовимся к циклу аж на 2 итерации (может оказаться и меньше), в котором проверяется, не слишком ли близко обнаруженная сейчас точка к ранее обнаруженным? Регистр Z будет указывать на проверяемую точку, изначально (после строки Z X) - на левую. Ну и один-единственный свободный регистр i используем для организации цикла. Два других мы решили "зафиксировать": j всегда равно единице, k всегда равно нулю во всей этой процедуре.
	@@checkCycle:	Acc		[SP+1]
			SUB		[Z+2j+k]
			ABS		Acc
			[SP]		Acc
			DIV2S		C
			DIV2S		[Z+1]						
			JL		@@DoMerge
			Z		[X+k]
			iLOOP		@@checkCycle

Вот он, этот цикл. Загружаем в аккумулятор координату X только что обнаруженной точки. Вычитаем координату X проверяемой точки (на первой итерации это левая, на второй: правая). Затем берём модуль (абсолютное значение), то бишь откидываем знак. Это позволяет единообразно проверить левую и правую точки. Этот самый модуль разности сохраняем в [SP], пригодится, если решим эти точки "слить".

А после этого из расстояния между точками вычитаем половину диаметра одной (пока что РАСЧЁТНОГО ДИАМЕТРА) и половину диаметра другой (а это значение может оказаться повыше, если уже прошло сколько-то слияний). Вот теперь, если результат оказался отрицательным, значит точки слишком близко и мы должны их считать одной точкой - переходим на метку DoMerge. А в противном случае перемещаемся по списку, то есть от левой точки к правой - и повторяем цикл.
	Y		Heap									
	CALL		ListOp
	X		Z
	[Z+1]		C
	CALL		AcqQueue

Если мы дошли до этих строк (не прыгнули в DoMerge), значит, только что обнаруженная точка довольно далеко от ранее обнаруженных - и её надо добавить в список ActivePoints, сразу вслед за точкой слева от неё. Процедуру ListOp мы рассматривали ранее, она с тех пор не сильно изменилась. Мы там перемешали проверку на пустой указатель с основным кодом, чтобы убрать один Hazard, из-за чего может пройти обращение по нулевому адресу, но это не страшно, у нас там сейчас лежит последовательность для инициализации Ethernet-контроллера, которая к этому моменту уже не нужна :)

Итак, процедура ListOp перецепляет первый элемент списка, на который указывает Y (это Heap, список всех свободных элементов) в список, на который указывает X. Тем самым посередине списка активных точек вставляется новый элемент, пока что неинициализированный (там может лежать что угодно, ещё с прошлых кадров). После вызова Z указывает на этот элемент - присваиваем X=Z, чтобы случайно не начать обрабатывать эту точку повторно. И затем нужно занести 4 значения. Одно из них, лежащее в [X+1] = [Z+1] - это "диаметр" точки, по умолчанию поместим туда наш D1, который мы заблаговременно сунули в регистр C. Остальные 3 значения мы заносим внутри процедуры AcqQueue, вот она:
	AcqQueue proc	
			i		2
			JL		@@queue
	@@updCycle:	[X+2j+i]	[SP+i]
			iLOOP		@@updCycle
	@@queue:	Acc		[X+2j+k]
			DIV2S		[X+1]	
			ACQ		Acc	
			ADD		[X+1]	
			ACQ		Acc
			JMP		[--SP]
	AcqQueue endp	


Значения, лежащие последовательно в [SP+1], [SP+2], [SP+3] (X,Y-координаты и яркость) переносятся соответственно в [X+2],[X+3] и [X+4]. Только ремарка: внутри процедуры AcqQueue, поскольку SP сместился на единичку, эти [SP+1]..[SP+3] превратились в [SP]..[SP+2].
Мы заранее знаем, что при вызове из этого места JL @@queue не сработает, т.к последнее вычитание происходило при проверке на пустой указатель в ListOp, и раз мы не отправились обрабатывать ошибку "нехватка памяти", а вернулись сюда, значит флаг знака показывает положительное значение. Зачем здесь этот прыжок - объясним позже.

А после того, как мы заполнили параметры новой точки, мы посылаем в видеопроцессор два запроса. Первый - до значения (X-D/2), второй - до значения (X+D/2). Первый запрос - по отрезку, где мы "ищем новую точку", второй - по отрезку, где "уточняем координаты имеющейся".

Возвращаемся в основную процедуру.
	JMP		@@MidCycle

Итак, мы вставили новую точку, послали соответствующие запросы - и тоже переходим по метке @@MidCycle, как и в случае, когда сколько-нибудь яркой точки не находили.

Дальше идёт метка, куда мы перемещаемся, если нужно осуществить слияние точек:
@@DoMerge:	[Z+1]	[SP]

Весьма незамысловато. Решили только нарастить диаметр этой точки, поместив в соответствующее поле ранее найденное расстояние между точками. По идее можно было бы и её координаты сместить ближе к новому отрезочку, и яркость обновить, но я подозреваю, это всё излишне: на следующей строке мы прошерстим по большему отрезку, и там уже повторно получим самую яркую точку, скорее всего ближе к центру композиции. Будем посмотреть...

@@MidCycle:		Y	X
			X	[X+k]								
			Acc	0
			SUB	[X+k]
			JGE	@@FinalRange

Этот кусок мы уже обсуждали. Мы готовы наконец-то рассмотреть яркую точку, обнаруженную заранее, но должны убедиться, что она не "фиктивная правая" точка, в противном случае выйти из цикла по ярким точкам и добить оставшиеся необработанными пиксели на строке. Предыдущую точку мы сохраняем в регистре Y, если придётся удалить текущую точку.

	[SP+2j+1]	GPU
	[SP+1]		GPU
	NOP		0
	Acc		[SP+2j+1]
	SUB		[X+2j+1]
	JL		@@NotSoBright

Очень похожий кусок кода был выше. Мы получаем от видеопроцессора информацию по отрезку, где мы сразу ожидали найти точку, поскольку строкой выше она уже была. Сразу же получаем два значения: яркость и X-координату. Мы обязаны всегда извлекать оба значения, иначе выходной буфер "собьётся". Но если в прошлый раз мы сравнивали яркость с неким порогом Threshold, то теперь сравниваем с яркостью нашей точки, когда её обнаружили на строках выше. Если яркость пошла на спад, мы не хотим обновлять параметры это точки - и переходим по метке @@NotSoBright.
@@QueueAnyway:	CALL		AcqQueue						
		JMP		@@ActPointsStart	;завершили цикл					

Сюда мы приходим "естественным путём", если на текущей строке точка ярче, чем была на предыдущих. Вызываем AcqQueue, и прыжок JL внутри неё опять не срабатывает, благодаря чему выполняется вся процедура целиком - обновление параметров точки в списке (новая X-координата, новая Y-координата и новая яркость, всё из локальных переменных) и запросы видеопроцессору для новой строки. Как только процедура завершается - мы начинаем следующую итерацию цикла по ярким точкам, т.е прыгаем в начало цикла.
@@NotSoBright:	Acc		[X+2j+1]
		SUB		[SP+2j]	
		DIV2S		[X+1]	
		JL		@@QueueAnyway

Сюда мы переходим, если новая точка нас не впечатлила по яркости. Обновлять параметры точки (координату центра и яркость) мы не хотим, а если Y-координата осталась где-то "вверху", то задаёмся вопросом - может пора её вообще выкинуть из списка активных точек? Чтобы ответить на этот вопрос, загружаем в аккумулятор старую Y-координату (из списка, она БОЛЬШЕ, т.к Y у нас УМЕНЬШАЕТСЯ от строки к строке), вычитаем текущую, вычитаем половину диаметра точки, и если результат вышел отрицательным, то перепрыгиваем на @@QueueAnyway, т.е на вызов процедуры AcqQueue. И вот тут-то сработает прыжок JL внутри AcqQueue - он перескочит обновление точки, ведь её обновлять не нужно! И сделает только запросы видеопроцессору на следующую строку!
	X		AllPoints
	CALL		ListOp
	X		Y
	JMP		@@ActPointsStart

Если мы не перешли на @@QueueAnyway, значит точку пора переместить из списка ActivePoints в список AllPoints, и именно это мы делаем. Как и раньше, X должен указывать на список, куда мы вставим точку, Y - откуда извлекаем (причём Y указывает на предыдущий элемент). Но Y мы уже занесли сильно заранее, остаётся X настроить, а потом присвоить обратно, т.к этот регистр у нас должен гулять по ActivePoints, от начала до конца. Если точку "удалили", то запросов и не делаем пока, поскольку три отрезка сливаются в один, а где он оканчивается, мы пока не знаем - узнаем чуть позже...

Вот она радость - мы целиком рассмотрели цикл по активным точкам! Как видно, из него есть ровно одна "точка выхода", по достижению "фиктивной правой точки". Оттуда мы прыгаем по метке @@FinalRange. По этой же метке ИЗНАЧАЛЬНО заходим во все эти циклы, на самом верху кадра. Осталось рассмотреть этот код:

@@FinalRange:	ACQ		WholeRow
		ACQ		HSync
		Acc		[SP+2j]
		SUB		1
		JGE		@@newRow


Мы посылаем запрос на обработку ДО КОНЦА СТРОКИ, а потом ещё и запрос на строчный синхроимпульс, после чего вычитаем единичку из Y-координаты, и если результатом не стал -1, начинаем новую итерацию по строке. В противном случае наша работа завершена, кадр обработан!

Довольно громоздкая штуковина, но и это после того, как мы условились самую малость добавить "мозгов" видеопроцессору. А именно, его счётчик по X-координате должен стать полноценным 10-битным (ранее я размышлял - не сбрасываться ли ему после каждого запроса), что позволяет обойтись без переменной PixelsProcessed или типа того (которая ведёт счёт, где мы сейчас располагаемся по координате X). И что ещё важнее, он должен корректно отрабатывать, если мы запросили ему точку меньше нуля или больше 1023. В первом случае он должен положить в результат нулевую яркость и произвольную координату, во втором - дойти честно до своих 1023 и выдать ответ. Ведь здесь, внутри кода, мы не проверяем границ, и точка, обнаруженная на самом краю может привести к выходу за диапазон.

Что хуже: если какие-то точки оказались в самом низу кадра, то они могут так и остаться в списке ActivePoints и не быть перенесены в AllPoints. То ли нам нужно будет дополнительно "сцепить" эти списки в один длинный, то ли, опять же, видеопроцессору надо слегка поумнеть. А именно, если ему начать заказывать строки, хотя он отчётливо понимает, что кадр УЖЕ ЗАКОНЧИЛСЯ, он будет без малейших раздумий отвечать нулевыми яркостями, что неминуемо приведёт к тому, что все оставшиеся точки будут перекинуты из ActivePoints в AllPoints по ветви @@NotSoBright, притом это произойдёт относительно быстро, поскольку мы не должны будем выдерживать тайминги строк.

Здесь, как и раньше, получается компромисс между памятью и логическими элементами. Что из этого более дефицитно на моей ПЛИС - пока не соображу... Но по счастью, образовалось уже много мест, где можно обменивать одно на другое, так что думаю, как-нибудь оно всё вмонстрячится.


Самое обидное, что у меня сейчас нет "железа", чтобы отладить всё это дело! Видеопроцессор в каком-то состоянии есть, но ещё нужен входной FIFO, которому можно задать количество ячеек, и более сложный выходной FIFO, в который будет поступать по 2 значения - яркость и координата (при "захвате"), либо сумма и другая сумма (при сопровождении), плюс логика опустошения выходного FIFO по приходу синхроимпульсов, чтобы ошибка на одной строке не могла безнаказанно сохраняться по всему кадру. Вот за них-то сейчас и возьмусь...
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

  • Формулы приведения, что б их... (и atan на ТРЁХ умножениях)

    Формулу арктангенса на 4 умножениях ещё немножко оптимизировал с помощью алгоритма Ремеза: Ошибка уменьшилась с 4,9 до 4,65 угловой секунды, и…

  • Алгоритм Ремеза в экселе

    Вот и до него руки дошли, причина станет ясна в следующем посте. Изучать чужие библиотеки было лениво (в том же BOOSTе сам чёрт ногу сломит), писать…

  • atan на ЧЕТЫРЁХ умножениях

    Мишка такой человек — ему обязательно надо, чтоб от всего была польза. Когда у него бывают лишние деньги, он идёт в магазин и покупает какую-нибудь…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 4 comments