Часть 0 - покупаем, паяем, ставим драйвера и софт Часть 1 - что это вообще за зверь? Часть 2 - наша первая схема! Часть 3 - кнопочки и лампочки Часть 4 - делитель частоты Часть 5 - подавление дребезга кнопки Часть 6 - заканчиваем кнопочки и лампочки Часть 7 - счетчики и жаба Часть 8 - передатчик UARTЧасть 9 - Hello, wolf!
Часть 'hA - приёмник UART Часть 'hB - UART и жаба Часть 'hC - полудуплексный UART. Часть 'hD - МКО (МКИО, Mil-Std 1553) для бедных, введение.Часть 'hE - приёмопередатчик МКО "из подручных материалов" (в процессе)
Часть 'hF - модуль передатчика МКО Часть 'h10 - передатчик сообщений МКО Часть 'h20 - работа с АЦП ADC124s051 Часть 'h21 - преобразование двоичного кода в двоично-десятичный (BCD) Часть 'h22 - Bin2Bcd с последовательной выдачей данных Часть 'h23 - перемножитель беззнаковых чисел с округлением Часть 'h24 - перемножитель беззнаковых чисел, реализация Часть 'h25 - передаём показания АЦП на компьютер Часть 'h26 - работа над ошибками (быстрый UART) Часть 'h27 - PNG и коды коррекции ошибок CRC32 Часть 'h28 - передатчик изображения PNG Часть 'h29 - принимаем с ПЛИС изображение PNG Часть 'h2A - ZLIB и коды коррекции ошибок Adler32 Часть 'h2B - ускоряем Adler32 Часть 'h2C - формирователь потока Zlib Часть 'h2D - передаём сгенерированное PNG-изображение Часть 'h2E - делим отрезок на равные части Часть 'h2F - знаковые умножители, тысячи их! Часть 'h30 - вычислитель множества Мандельброта Часть 'h31 - ускоренные сумматоры Часть 'h32 - ускоренные счётчики (делаем часы) Часть 'h33 - ускоряем ВСЁ Часть 'h34 - ускоренные перемножители Часть 'h35 - умножители совсем просто Часть 'h36 - уравновешенный четверичный умножитель Передавать один байт по UART мы научились, теперь наконец-то передадим строку. Но для начала разберёмся, как получить доступ к блокам внутренней памяти (Embedded Array Block, EAB) нашей ПЛИС. Всё на удивление просто...
Каждый блок внутренней памяти имеет ёмкость 4 кбит, т.е 512 байт. В 5576ХС4Т таких блоков - 24 штуки, что даёт суммарно 96 кбит или 12 килобайт (12288 байт) памяти. Возможны конфигурации 256×16 (8 бит адреса, 16 бит шина данных), 512×8, 1024×4, 2048×2.
Впрочем, детали внутренней реализации скрываются от программиста (разработчика, короче, ПЛИСовода) - при добавлении модуля можно ввести произвольную ширину как адреса, так и данных. Если указать больше, чем 512 байт, автоматически будет задействовано несколько блоков.
Всю память можно инициализировать, то есть начальное её содержание будет "перекачиваться" из конфигуратора. В том числе, можно определить "ROM" - блок, то есть разумеется это оперативная память, которая будет сброшена при выключении питания, но возможности изменить содержание этой памяти в процессе работы у нас не будет.
Именно такой блок мы сейчас и применим. Самый простой способ добавить блоки памяти в Quartus - зайти в меню tools - MegaWizard Plug-in Manager...
Выбираем пункт Create a new custom megafunction variation. В дереве компонентов мы выбираем Installed Plug-ins - Memory compiler - ROM: 1-port.
Придумываем имя модуля, его тип - Verilog HDL.
Далее, задаём параметры.
How wide should 'q' be: 8
How many 8-bit words of memory: 256
Слева изображена условная схема блока памяти, и она слегка напрягает! По ней видно, что и на входе адреса, и на выходе данных стоят "защёлки", а это значит: на первом такте мы формируем адрес ячейки, которую хотим получить. Только по окончании первого такта, по положительному фронту, этот адрес "защёлкнется", поступит в блок памяти, где сразу же "комбинаторно" начнёт выдаваться на выход. Но там стоит вторая "защёлка", поэтому лишь к третьему такту этот адрес поступит в нашу схему. Получается неизбежная задержка в 2 такта.
Согласно техническому описанию ПЛИС, существует возможность использовать память хоть даже вообще "комбинаторно", без защёлок:
Мы видим, что есть пути "в обход" регистров, с мультиплексором, выбирающим, каким из путей воспользоваться.
Нажимаем Next и переходим в следующее окно:
Поначалу автор неверно истолковывал значение фразы вверху: Which ports should be registered? Казалось, что registered - это "задействованы", "указаны на схеме" и всё в этом духе. И получалось, что ну разумеется, нам нужен и адрес, и данные, должны стоять обе галочки, и ни как иначе!
Оказалось, что в данном случае registered и означает наличие защёлки, то есть регистра. Снимаем галочки - и вуаля - получаем чисто комбинаторный блок ROM! На рисунке мы видим именно такой вариант. Какая бестактность!
Разумеется, память совсем без защёлок будет работать медленно, в смысле что задержки по системе глобальных соединений в одну сторону, плюс задержка по выборке памяти, плюс задержка данных по системе глобальных соединений в другую сторону получится такой, что на 80 МГц оно уж наверняка не заработает! Собственно, для повышения доступной тактовой частоты длинные комбинаторные цепи и разбивают на отдельные части, разделённые защёлками - это и есть конвейер, без которого нынче ни один процессор обойтись не сможет. Но если мы никуда не торопимся и готовы работать в несколько раз медленнее, то комбинаторный ROM - то, что доктор прописал!
В следующем окне нас просят задать имя файла инициализации. Пока мы не обязаны иметь этот файл в наличии, просто придумаем какое-нибудь имя и постараемся его запомнить. К примеру, пусть это будет HelloWorldROM.hex.
Нажимаем несколько раз next и finish - и блок памяти добавлен в наш проект.
Теперь надо создать файл инициализации. Для начала создаём самый простой текстовый документ и пишем:
ПрЮвет ВОЛКУ!
(в начале - пробел, его мы потом заменим на нулевое значение).
Quartus использует так называемый
Intel hex file. Для преобразования обычного текста в него можно применить программку SRecord (
https://sourceforge.net/projects/srecord/). Запускаем из командной строки srec_cat.exe и пишем нечто такое:
(если у нас не прописаны удобные пути к файлам проекта, то можно соответствующие файлы из проводника "кидать" внутрь командной строки, и туда вставляется полный путь к файлу)
То есть, из бинарного формата ("как есть") мы преобразуем в Intel HEX.
Затем открываем этот файл в Quartus'e, говорим ему, что работаем с 8-битными значениями и получаем табличку:
Как видно (плохо видно, если честно - как подружить Quartus с кириллицей, не разбирался ещё), всё получилось как надо, восклицательный знак и пробел, по крайней мере, на своих местах! На первой позиции пробел заменяем нулевым значением. Бывает очень полезно, когда по нулевому адресу памяти лежит нулевой байт. Нам это надо из-за особенностей компактного UART передатчика, который при включении умудряется выдать импульс Ready. Если у нас по умолчанию стоит нулевой адрес, то по приходу Ready мы скажем - "всё в порядке, мы уже дошли до конца строки!".
Сохраняем этот hex файл под тем именем, которое указали при создании блока памяти. Если мы уже позабыли, то можно заглянуть в файл на verilog'е, где описывается этот блок - там ищем строку
lpm_rom_component.lpm_file = "HelloWorldROM.hex"
Ура! А теперь осталось сделать модуль, который заставит SimpleUARTtransmitter передать строку, байт за байтом, пока не дойдём до нулевого символа, означающего конец строки. В этот раз пойдём немножко с конца и посмотрим сначала на общую схему UART-передатчика строки:
Сейчас эта схема обвешана лишними выходами для отладки, впоследствии мы их уберём, и останется 3 входа (тактовая частота, запуск передачи и адрес строки) и один-единственный выход txd. (можно ещё добавить выход окончания передачи, но пока вроде не требуется)
Мы подаём адрес начала строки и единичный импульс запуска. Модуль UARTstringControl запросит (через OutAddr) этот адрес в памяти, получит (через Char) символ, лежащий по этому адресу и, если он не равен нулю, выдаст единичный импульс на UARTst, который запустит передатчик, он начнёт отправлять тот самый байт, вытащенный из памяти.
В этот момент UARTstringControl замирает до тех пор, пока модуль передатчика не выдаст сигнал Ready. В ответ на это к выходному адресу прибавится единичка, мы проверим новое значение на ноль, и если всё в порядке - запустим передачу нового байта. Так будет продолжаться, пока мы не выйдем на нулевое значение. По нему импульс запуска уже не будет выдан, и система остановится вплоть до нового запускающего импульса.
Теперь, когда понятен принцип работы, приведём код модуля UARTstringControl. Он писался для памяти с двумя защёлками, но с комбинаторной памятью тоже справится.
module UARTstringControl (input clk,
input st,
input [AddrWidth - 1 : 0] Addr,
input [7:0] char,
input TxReady,
output reg [AddrWidth - 1 : 0] OutAddr,
output UARTst);
parameter AddrWidth = 8;
reg [1:0] clkCount = 2'b00;
wire charNotZero = (char != 1'b0);
assign UARTst = (clkCount == 3) & charNotZero;
always @(posedge clk) begin
OutAddr <= (clkCount == 0)?
(st? Addr: (TxReady&charNotZero)? OutAddr + 1'b1 : OutAddr) : OutAddr;
clkCount <= (clkCount == 0)&(~st)&(~TxReady)? clkCount : clkCount + 1'b1;
end
endmodule
Смотрим, что происходит. До тех пор, пока на входах st или TxReady ничего нет, а clkCount инициализирован нулём, ничего не происходит. ClkCount так и остаётся нулём, и OutAddr также держит свой адрес, при включении он нулевой. UARTst будет оставаться нулевым, поскольку выражение clkCount == 3 не выполняется.
Теперь подадим единичный импульс на st. По положительному фронту тактового импульса clkCount увеличится на единичку, в то время как выданный адрес "защёлкнется" в OutAddr.
Со следующего такта эта схема теряет "чувствительность" к входам st или TxRead, просто прибавляется счётчик. На вход Char потихоньку приползает символ, лежащий по адресу OutAddr. Если символ не нулевой, то на проводе charNotZero формируется логическая единичка.
Наконец, когда clkCount досчитает до трёх, то при условии ненулевого символа на выход UARTst пойдёт импульс для запуска передатчика UART. Счетчик вернётся в ноль, и наступит ожидание передачи первого символа.
Когда символ будет передан, придёт импульс TxReady. Он снова запустит счетчик. Также, если текущий символ не нулевой, мы к текущему адресу прибавим единицу, то есть перейдём к следующему символу. А дальше всё аналогично - при ненулевом символе запускается передача, и мы снова дожидаемся её окончания, и так, пока не дойдём до нулевого символа.
Попробуем синтезировать данную схему. Вроде бы всё проходит как надо, но под конец мы получаем сообщение
Critical Warning: Timing requirements for slow timing model timing analysis were not met. See Report window for details.
Если теперь зайти в Compiler Report - Timing Analysis - Summary, увидим такую табличку:
Именно пути, проходящие через память, с выхода счетчика UARTStringControl на вход счетчика UARTtransmitter, оказываются слишком медленными, чтобы работать на частоте 80 МГц.
Warning - на то и warning, что компиляция всё-таки завершается, и мы можем запустить моделирование схемы, а можем и прошить её в ПЛИС. Но Critical означает - дело серьёзное, в таком режиме схема уже не обязана работать так, как мы задумывали.
Но поскольку мы несколько тактов не интересуемся состоянием выхода памяти, то всё будет работать правильно, только вот Quartus об этом не догадывается.
Тем не менее, не будем жадничать и добавим защёлку на адрес. Именно адрес считается более "уязвимым", особенно если используется режим записи. Так вот начнём записывать в одну ячейку, а потом вдруг передумаем посреди такта и начнём записывать в другую - кому же это понравится!
Достаточно два раза щелкнуть по модулю памяти на схеме, чтобы поменять его параметры. Новый вход inclock подсоединяем к нашему clk:
Теперь после синтеза получим результат, Met timing requirements: yes. Максимально допустимая тактовая частота стала 97 МГц - это хорошо, есть запас.
UPD. В этом примере не очень ясна разница между памятью с защёлками и без них: и так, и эдак приходится ждать больше такта, прежде чем на выходе появятся верные данные! Но если у нас стоят защёлки, то сразу же после того, как мы зададим адрес, мы можем уже на следующем такте запросить новый адрес, зная, что не испортим выдачу того, исходного адреса. И на "приемной" стороне мы можем получать корректный адрес каждый новый такт - тот адрес, что был запрошен тактом ранее. Это и есть конвейер - если проследить, сколько времени уходит на производство каждого автомобиля - это часы и дни. Но как только мы выходим на режим, как каждый новый автомобиль сходит каждые 6 секунд.
Наконец, промоделируем нашу схему:
Всё работает как задумано. Первый шальной импульс TxReady не творит нам бед за счет того, что по умолчанию UARTStringControl сидит с нулевым адресом, в который занесён нулевой символ.
Осталось только прошить это дело в ПЛИС. Не забываем поменять скорость передачи BaudRate с "отладочных" 8 МГц (это позволяет не переутруждать симулятор) к штатным 9600 Гц.
Превратим эту схему в отдельный схемотехнический элемент. Для этого надо её открыть и выбрать меню file - Create/update - Create symbol files for current file. Сохраняем как-нибудь, после чего добавляем этот символ в схему верхнего уровня:
Константу 42 hex переправляем, чтобы на выход подавалась всего лишь единичка - адрес нашей единственной строки. Можно прошиваться.
Всё работает - при включении ПЛИС никаких сообщений в UART не идёт, а по каждому нажатию кнопки - посылается строка.