Он основан на "теории оптимального обнаружения", где для обнаружения сигнала известной формы (но приходящего в неизвестный момент времени) изготавливают фильтр, импульсная характеристика которого - этот самый сигнал, только инвертированный во времени. И потом, когда пропустишь "входной зашумлённый сигнал" через такой фильтр - останется лишь найти максимум на выходе, его положение и покажет время прихода сигнала, а величина максимума - его амплитуду. Прохождение сигнала через линейный фильтр - это всё равно что свёртка с его импульсной характеристикой. А свёртка сигнала со своей же инвертированной копией - это нахождение корреляции. Вот и ищем, где она максимальна!
Тут есть хитрость: размер пятен мы должны знать заранее, в отличие от нашего алгоритма, который и размер находил "заодно". Хотя нам вполне позволительно на этапе обнаружения опробовать разные размеры, пока, наконец, не обнаружим что надо.
Зато у нас как бы автоматически произойдёт "селекция целей" - слишком мелкие пятна "автоматом" отфильтруются. Вообще, можно попытаться и больше крупные отфильтровать, выбрав правильную импульсную характеристику, не просто "фильтр низкой частоты", а ближе к полосовому.
И "разделение пятен" произойдёт фактически автоматом: мы будем искать максимумы на отрезках, примерно равных ожидаемому диаметру пятен, и тут уж заведомо двух рядом не будет.
Вопрос лишь в одном: насколько реально сделать двумерный фильтр с радиусом 50..60 пикселей на ПЛИС без аппаратных умножителей, малой скоростью и памятью в 12 килобайт?
Если здесь применять классическую свёртку - ни разу не влезет, ведь нам нужно сохранять в памяти под сотню строк, а это 100 килобайт сразу (а в "лётной" матрице и вовсе АЦП 12 бит,так что 150 килобайт), да и сотни сложений на одном такте реализовать не так-то просто...
Но вообще-то, если взять старые добрые рекурсивные фильтры (БИХ), они здесь сработают вообще в полпинка!

Фильтр по строке у нас по сути уже есть, мы применяли его для устойчивой работы детектора синхроимпульсов, см. ФНЧ БИХ на ЭРИ ОП. Ему радиус надо выставлять в степенях двойки. Давайте попробуем level = 6, т.е "радиус 64". Просто пропустим сигнал через этот фильтр перед входом в видеопроцессор:

Попытался получить картинку - а она самая обычная! Тут-то я и вспомнил, что в статическую память изображение идёт вообще напрямую из АЦП, минуя ПЛИС. ПЛИС только адреса инкрементирует и даёт разрешение на запись.
Пожалуй, надо взять и перепаять эти 8 выводов :) Смогу ещё "нахаляву" один вариант осуществить: записать "тёмный кадр" в память (кадр, где выключена ИК подсветка), а потом при получении "светлого кадра" сразу же, на лету, вычитать значение из памяти, таким образом убирая блики и прочую паразитную засветку. Сказано-сделано. Всё разобрал, см "фото для привлечения внимания" в начале поста.
Слева плата оцифровки и питания, через неё же подсоединяются ЖК экранчик, SD-карточка, сюда же может втыкаться цифровая камера OmniVision, но ещё ни разу не пробовал её использовать. И ещё начал было делать самопальный приёмопередатчик МКО (потому как готовый хрен купишь как физлицо, и даже его компоненты, как-то трансформатор ТИЛ6В, микросхемки 5559ИН13У либо 5559ИН67Т), но обстоятельства поменялись, приоритетной задачей стало проверить всё остальное (а с МКО, он же Mil-Std 1553 уж как-нибудь управимся), так что эта часть ещё ждёт своего часу...
Сверху - плата статической памяти аж на 1 МБайт. Ну а в центре композиции - отладочная плата с ПЛИС 5576ХС4Т1, конфигуратором для неё аж в металлокерамике, и кучей всяких "пряников" вроде Ethernet-контроллера ENC624J600, приёмопередатчика RS485, 12-битной АЦП на 4 канала и 500 килосэмплов в секунду, преобразователя напряжения из 5 вольт в 3,3 и затем LDOшки из 3,3 в 1,8 вольта, а ещё 8 светодиодиков и 5 кнопок, которые сейчас, когда эта плата оказалась посередине "этажерки", да ещё внутри железного корпуса, стали резко не нужны.
Прежде чем перепаивать, надо было провести "инвентаризацию выводов" - вся эта конструкция соединяется сквозными цанговыми разъёмами, 4 штуки по 20 выводов, а почти все они уже заняты! По счастью, кое-какая документация у меня была, оставалось только слегка обновить её. В общем, вот просто обозначения выводов:

Те выводы, где стоит прочерк - "зарезервированы" разработчиками отладочной платы под другие цели. Например, контакты 13-20 - для подключения гнезда Ethernet (RJ-45): два светодиодика, трансформаторы передатчика и приёмника с отводом от центра. Собственно, и +3,3 вольта именно туда предназначались, но я их "стырил" :)
Контакты 61-64 - для подключения USB, но на эту отладочную плату они вообще не заходят. Контакты 79-80 - "самопальный" ЦАП в виде пассивного ФНЧ (4 подряд идущие RC-цепочки 1 кОм - 10 нФ и ферритовая бусина впридачу). Контакт 59 - VBat. Опять же, на эту отладочную плату он не подключён. Видимо, это к каким-нибудь микроконтроллерам, у которых есть отдельная фича "батарейного питания" для часов реального времени и хранения данных. Но это не наш случай.
А все универсальные I/O мы-таки забили! Больше всего (30 штук) досталось именно статической памяти (префикс SRAM), ещё 10 - на быстродействующую АЦП, которой мы оцифровываем видеосигнал. 6 штук - для подключения ЖК экранчика, 3 - для SD-карточки.
4 входа шли от селектора синхроимпульсов LM1881: Odd/Even (чёт-нечет), Burst (вспышка PAL), VSync (кадровый синхроимпульс) и Comp (композитный). Немножко наигравшись с этим селектором, я его отключил, теперь нахожу синхроимпульсы уже внутри ПЛИС, по оцифрованному сигналу. Но выпаивать пока не стал, мало ли захочется АЦП "подогнать" аккурат под диапазон от чёрного до белого, и тогда микросхемка снова станет нужна. Сейчас отпаял Odd/Even (на сигнале 720p он не нужен, ибо progressive, там нету чётных-нечётных полукадров) и на его место сунул SRAM_D0 - ну не хватало уже пинов!
И ещё 3 ножки "сами по себе": одна управляет подсветкой ЖК-экранчика через транзистор КТ608 :) Хотя сейчас он используется в полсилы, изначально там 2 пары диодов были соединены параллельно, давая ток 120 мА, а я их пересоединил последовательно, и запитал от 12 вольт, так что теперь ток лишь 60 мА.
Вторая - ИК-подсветкой камеры, причём по совместительству позволяет определить низкую освещённость в комнате, см. да будет свет.
И третья - управление экранным меню камеры (OSD), вместо "джойстика" на проводе.
В общем, как только определились, к каким ножкам теперь тянуть шину данных статической памяти (раньше они шли на те же 8 выводов, что и выход АЦП), было дело техники их перепаять.
После этого требовалось переделать проект на ПЛИС, в первую очередь модуль QuatCoreFastSRAM. Если раньше при работе с АЦП ему достаточно было переключать адреса и управлять входами разрешения, то теперь и данные должны через него проходить напрямую! Получилось как-то так:
module QuatCoreFastSRAM (input clk, input [7:0] DestAddr, input [7:0] SrcAddr, input DestStall, input SrcStall, input SrcDiscard, input [15:0] D, input ReadEnable, input WriteFromADC, input [7:0] GPU, output [18:0] RAMaddr, output RAM_CE0, output RAM_CE1, output RAM_RW, inout [7:0] RAM_data); wire IsOurDest = (~DestStall)&(~DestAddr[7])&DestAddr[6]&(~DestAddr[5]); wire IsOurSrc = (~SrcStall)&(~SrcDiscard)&(SrcAddr[7:3] == 5'b1001_1)&ReadEnable; wire LoadERL = IsOurDest & (~DestAddr[4]) & (~DestAddr[3]); wire LoadERH = IsOurDest & (~DestAddr[4]) & DestAddr[3]; wire WriteMem = IsOurDest & DestAddr[4]; wire DoWriteFromADC = WriteFromADC & (~ReadEnable); wire DoIncrement = WriteMem | IsOurSrc | DoWriteFromADC ; wire [19:0] ER; //External memory Register wire TC; //Terminal Count lpm_counter ERL (.clock (clk), .cnt_en (DoIncrement), .sload (LoadERL), .data (D), .Q (ER[15:0]), .cout (TC)); defparam ERL.lpm_direction = "UP", ERL.lpm_port_updown = "PORT_UNUSED", ERL.lpm_type = "LPM_COUNTER", ERL.lpm_width = 16; lpm_counter ERH (.clock (clk), .cnt_en (TC & DoIncrement), .sload (LoadERH), .data (D[3:0]), .Q (ER[19:16])); defparam ERH.lpm_direction = "UP", ERH.lpm_port_updown = "PORT_UNUSED", ERH.lpm_type = "LPM_COUNTER", ERH.lpm_width = 4; assign RAMaddr = ER[19:1]; assign RAM_CE0 = ~(ER[0]&(ReadEnable | WriteMem | DoWriteFromADC)); assign RAM_CE1 = ~(~ER[0]&(ReadEnable | WriteMem | DoWriteFromADC)); assign RAM_RW = ((~WriteMem) & (~DoWriteFromADC)) | clk; assign RAM_data = ReadEnable? (WriteMem? D[7:0] : 8'bzzzz_zzzz) : GPU[7:0]; //(WriteMem & ReadEnable)? D[7:0] : 8'bzzzz_zzzz; endmodule
Закомментированная строка внизу - единственное, что изменилось (актуальная версия - строкой выше). Ну и дополнительный вход GPU, раньше нужды в нём не было.
ReadEnable идёт с выхода видеопроцессора, он равен единице, когда АЦП отключён (тогда SRAM может заниматься своими делами) и нулю, когда он включается. WriteFromADC равен единице, когда началась "полезная часть видеосигнала", которая должна записываться в память с автоинкрементом.
Попробовал "с нахрапу" отсинтезировать всё это безобразие с программой ImageTransfer (позволяет отрегулировать яркость ИК подсветки, побегать по меню аналоговой камеры и получать изображение на компьютер). Сразу Critical Warning: Timing constraints not met, предельная частота 24,6 МГц. Я решил: должно заработать, всё-таки температура не предельная, напряжение питания стабильное (самый худший случай с точки зрения быстродействия - напряжение на нижней границе допустимого и максимальная температура, при которой сопротивления открытых переходов МОП-транзисторов увеличено), уж как-нибудь должно заработать!
Ну, сообщение на ЖК-экранчике появилось, и по UART было прислано "Начинаем работу". На регулирование ИК-подсветки откликался, а вот при попытке получить изображение выскочило прерывание "исчерпание заданий GPU" - раньше здесь такого не было!
Ладно, давайте хотя бы убедимся, что статическая память действительно работает, всё правильно спаяли и правильно отметили ножки. Загрузил программу HelloSRAM.asm. Сообщение "Привет лунатикам! (через СОЗУ)" появилось, но начало идти бесконечным циклом, одно за другим, как будто бы простейшая команда iLOOP перестала правильно работать!
Стоп! А ведь эти же команды используются в самом начале, чтобы передать сообщения по SPI на Ethernet-контроллер и настроить нам тактовую частоту 25 МГц. Раз мы по RS485 получаем данные, значит частота правильная! Хотя... при подаче питания сначала запускается конфигурация ПЛИС из Flash-конфигуратора, а только потом мы по JTAG прошиваем свою. И если та, старая добрая версия (ещё на старом АЛУ) настроит тактовую частоту, она никуда не сбросится, ведь при прошивке по JTAG питание не прерывается!
Прошил этот несчастный HelloSRAM в ПЗУ (конфигуратор), и оппаньки - по RS485 пошёл мусор... А выставив на компьютере скорость передачи вместо 921 600 бод 147456 бод (это 921 600 * 4 / 25), получил всё то же "Привет лунатикам! (через СОЗУ)", но опять же зацикленные, чего быть не должно было!
Ну блин, а хоть Hello, World у меня заработает?

Очень символично: HELL. Мы всё уронили, разрушили до основания. Времени уже было 21:50, пора бы домой идти, несолоно хлебавши. Но тут я увидел, что мой макет подключён к зарядке. Так-то он питается от встроенного повербанка, там 3 литий-ионных аккума последовательно дают чуть больше 12 вольт, а заряжается это дело от 5 вольт, и заряжается очень нервно - то 2 ампера начинает жрать,а потом резко гасит до нуля, а через некоторое время - снова 2 ампера. А такие резкие скачки по питанию у нас непостижимым образом сводят с ума USB Blaster, см. USB Blaster ловит наводку
И прямо при мне, когда ток скакнул на 2 ампера, увидел, как моргнула подсветка ЖК - перезагрузился!
Подумал: может хотя бы этот HELL не доработал из-за наводки, ну повезло мне как всегда. Прошил повторно - сработало нормально. И то радость, Hello, World работает...
Пока шёл к метро (велик пока не на ходу, это отдельная история), осенило, почему HelloSRAM зациклился: там прерывание срабатывает, а поскольку оно в этой программе не прописано (она гораздо раньше писалась), то по умолчанию прыжок идёт на нулевой адрес, и всё повторяется. И установка тактовой частоты в Hello, World же работала, то есть iLoop и прочие команды не виноваты, я же их наблюдал, когда испытывал новую АЛУ на аффинном алгоритме!
Нет, здесь всё дело именно в прерываниях - чего-то в них нарушилось...
Сегодня начал "локализацию проблемы". Первым делом просто отключил прерывания, хотя проще всего это было сделать, добавив параметр в модуль QuatCoreCVinterruptEncoder:
module QuatCoreCVinterruptEncoder (input clk, input OFLO, input UFLO, input WDT, output StallEn, output reg NMI=1'b0, output reg [1:0] intN = 2'b00); parameter EnableNMI = 1'b1; wire wNMI = OFLO | UFLO | WDT; reg rNMI = 1'b0; always @(posedge clk) begin rNMI <= wNMI; NMI <= (wNMI & (~rNMI)) & EnableNMI; intN <= WDT? 2'b00: OFLO? 2'b01: 2'b10; end assign StallEn = ~NMI; endmodule
Иначе пришлось бы "отрывать" провод NMI (Non-Maskable Interrupt, других у нас пока нет) и соединять на землю, а провод StallEn (разрешение остановки конвейера) - напротив, на "плюс питания". А так выставил NMIenable = 0 - и "радуешься".
Итак, запускаю - и получаю 32 одинаковых символа 0x0A. Это был последний символ в строке, Line Feed (LF). Что,память уже отвалилась???
Попытался вообще отключить от модуля памяти всю логику, завязанную на видеосигнал, оставить как было год назад. Синтезирую - та же фигня.
Приготовился уже доставать осциллограф, но тут заметил: уж больно маленький размер "проекта", менее 1000 ЛЭ. Полез во вкладку Analysis&Synthesis --- Parameter settings by entity instance - и увидел там, что "в глубине" выставлены параметры enableSRAM = 0, enableIO = 0,хотя наверху Я УСТАНАВЛИВАЛ ИХ В ЕДИНИЦУ.
ВОТ ЖЕ СВОЛОЧЬ! Есть у Quartus'а какой-то глюк, когда он не всегда параметры "распространяет" правильно сверху вниз! Не знаю до сих пор, как это "правильно" побороть, в общем, спустился уровнем ниже, там тоже выставил "ручками", отсинтезировал ещё разок, теперь размер стал существенно больше - и наконец я получил долгожданное сообщение, притом ВСЕГО ОДИН РАЗ:

(выше видны прошлые неудачные попытки, где вместо сообщения одни сплошные LineFeed)
Ну а теперь давайте снова попробуем картинку получить. Прерывания пока отключены, но если всё правильно, они и не нужны, их задача как раз локализовать ошибку. Прошиваемся, снова Critical Warning - Timing constraints not met, 24,81 МГц вместо 25. Хрен с ним...
ИК работает, жму "получить изображение" - зависает намертво.
Что ж, пора доставать осциллограф. Проверил наличие видеосигнала - на месте. Стал смотреть тактовую частоту, идущую на АЦП, она есть - и тут с ошибкой закрылся "терминал" Termite. Ага, это значит, что сейчас он попытался переварить 500 КБ символов - и не смог. То есть, КОГДА Я ТКНУЛСЯ ОСЦИЛЛОГРАФОМ - ВСЁ ЗАРАБОТАЛО! Запустил свою "программу рабочего места" - работает зараза...

Эх, значит и этот глюк никуда не ушёл. Уж сколько он мне крови попортил, а по-прежнему здесь. ТАК ЖИТЬ НЕЛЬЗЯ, пора уже в обход этого несчастного разъёма впаять провод!

Всё собрал назад, запускаю - и...

Ну, пока работает, а там видно будет.
Ладно, почти вернулись на ранее занятые позиции, если бы не две фигни:
1. Что-то "нечисто" с прерываниями - почему он перепутал "нет сигнала" с "исчерпанием заданий GPU"?
2. Опять тайминги нарушились, оно и понятно - если в QuatCore источником данных выбрать SRAM в тот момент, когда работает АЦП, то сигналу придётся пройти довольно длинный комбинаторный путь, через мультиплексор, выбирающий между шиной данных и GPU, а потом через основной мультиплексор (MEM / ALU / IMM / PC / IO / SRAM / GPU) и наконец в регистр. Видимо, надо ввести тут дополнительную защёлку, в конце концов мы можем лишний такт подождать, прежде чем записывать данные в память...
Вот прямо все грабли собрал, что были.