Сейчас мы более детально рассмотрим устройство и возможности такого ресурса ПЛИС, как блочная память. Пожалуй, вряд ли какое проектируемое устройство обработки данных обойдется без использования такого важного ресурса.
В прошлый раз мы рассмотрели устройство и порядок использования так называемой однопортовой памяти со случайным доступом (RAM-память). В такой памяти в один момент времени можно получить доступ только к одной ячейке. Память имеет одну шину адреса. Если она используется как хранилище данных, разделяемое между двумя потребителями, то в таком случае остается только разнести обращения во времени. Сначала один потребитель, потом второй. Это требует внесение в проект некоторого арбитра - хотя бы самого простейшего устройства, препятствующего коллизиям — нарушениям работы устройства вследствие одновременного обращения к общему ресурсу.
Общие принципы функционирования многопортовой памяти
По счастью, существуют разновидности многопортовой памяти, обладающие способностью одновременного доступа данным несколькими потребителями. Такая память существенно упрощает жизнь разработчикам. Кроме того, без разделения обращений во времени проектируемые устройства обладают существенно повышенным быстродействием. Идея создания такой памяти появилась благодаря находчивости инженеров.
Весьма распространенным способом организации множественного доступа к памяти является организация обычной памяти в так называемые банки. В самом простом случае, берется две микросхемы памяти и подключаются с расширением адресного пространства.
Младшие ячейки в одной микросхеме, старшие в другой. В шину адреса добавляется старшая линия, отвечающая за адресацию своей половины общего пространства адресов. Таким образом, любой потребитель может получить доступ к абсолютно любой ячейке. Второй потребитель также может получить доступ к памяти, но только к ячейке, находящейся в другой банке. То есть в другой микросхеме. Физически потребители обращаются к разным микросхемам. Однако, при этом они одновременно обращаются к данным, находящимся в едином адресном пространстве. Идея настолько потрясает воображение, что непременно хочется попробовать это воплотить в каком либо из проектов.
В какой-то момент появляются по настоящему честные экземпляры многопортовой памяти. В состав программируемых логических интегральных схем входят десятки и сотни полупроводниковых структур, выполняющих функции двухпортовой памяти.
Эти модули расположены столбцами среди массива логических ячеек. Главной особенностью этой памяти является наличие двух одинаковых портов, при помощи которых можно организовать независимую запись и чтение данных в одном общем адресном пространстве. Разумеется, при этом нельзя вести запись данных в одну ячейку. Ограничения безусловно есть. Компилятор либо позволит организовать нужную вам схему, либо запретит. Фатальных ошибок не случится. Для профессиональной работы рекомендуется ознакомиться с документацией, где описаны все возможные режимы этих элементов ПЛИС.
Разработка буферного устройства
Задача ставится следующим образом: двухпортовая память является буфером между двумя потребителями.
Один из них медленно записывает данные, другой быстро читает. Скорости записи и чтения друг от друга не зависят и могут быть специфическими в зависимости от технических особенностей каналов приема и передачи информации. Если запись будет быстрой, а чтение медленным, то это явно грозит переполнением буфера. В жизни скорость чтения обязательно должна быть выше, чем скорость записи в буфер. Иногда буфер может применяться для компенсации краткосрочных повышений скорости приема данных.
У устройства записи данных есть своя память для хранения двоичных слов (Память для чтения). Каждый новый такт происходит запись данных по следующему адресу в буфер. Как только все ячейки буфера заполнены, счетчик адреса автоматически начинает с нулевой ячейки. Тут все очень просто. Как данные готовы, так они и пишутся без оглядки на остальные части схемы.
Теперь поговорим о чтении данных. По условию задачи сигнальные импульсы на чтение приходят очень часто, гораздо чаще, чем данные записываются в буфер. Если нет никакого ограничителя на чтение данных, то это будет происходить даже с тех ячеек памяти, где данных еще нет. Это некорректная работа буфера. Информация не должна искажаться. Поэтому необходимо применить один из самых востребованных и простых способов решения такой задачи.
Технология банков памяти
Запись и чтение данных должно производиться по банкам. Они же нижняя и верхняя половины адресного пространства буфера. До тех пор, пока запись не будет произведена полностью в одну из банок, чтения быть не должно. Первым признаком того, что пора читать, является переход записи с одной половинки на другую. В это время запись будет идти во вторую половину, а чтение происходить из первой.
Поскольку половинки имеют вполне конкретный размер, то блок чтения знает точно сколько читать и когда нужно остановиться. Информация о том, что произошел переход записи на другую половину передается блоку чтения при помощи нехитрого устройства. Это довольно простая схема. Более сложные контролируют адреса начала и конца данных, блок записи пишет в конец, блок чтения читает с начала. Так работают кольцевые буферы. В нашем же случае все несколько проще.
Для начала нужно обзавестись двумя линиями тактовых импульсов, одна побыстрее, другая помедленнее. Поскольку в проекте есть индикатор, то для удобного восприятия нужно будет поделить высокую частоту кварцевого генератора до приемлемых значений.
Модуль счетчика-делителя
Итак, в модуле счетчика делителя тактовой частоты две выходные линии для сравнительно медленных тактовых импульсов (tick, slowtick).
Организуем один регистр (delay) для счетчика, который поделит высокую частоту кварцевого генератора и второй регистр (slow), который поделит получившуюся частоту еще в четыре раза. Обнулим начальные значения регистров. В процедурном блоке организуем деление частоты до периода в четверть секунды. Двенадцать с половиной миллионов импульсов это как раз то что нужно (частота кварцевого генератора 50 МГц). Выделим тактовый сигнал и поделим его еще в четыре раза. Единица будет появляться только когда состояние двухбитного счетчика (slow) два нуля. Это мы закончили с делителем частоты.
Память с данными
Теперь займемся памятью, где будут фиксированные двоичные слова, предназначенные для семисегментного индикатора. Память только для чтения, поэтому ограничимся шиной адреса и выходной шиной данных. Индикатором будет имитировано круговое движение горящего сегмента.
Горящий сегмент по очереди обойдет индикатор по контуру. Напомним, что загорается сегмент от уровня логического нуля. Ноль с каждым шагом смещается от старшего разряда к младшему.
Модуль записи данных
Теперь мы готовы описать устройство генерации и записи данных. На входе медленный тактовый сигнал (clk), его же направим на выход (tick). Модуль выставляет данные (data) и адрес (addr), по которому они будут записаны.
Чтобы вычислять адрес, нужен будет счетчик (counter). И в структуру модуля вставим блок памяти (LUT_ROM0) с двоичными словами. На вход памяти идет содержимое счетчика, это адрес. На выходе памяти данные. Направим медленный тактовый сигнал на выход. Он пригодится для тактирования записи данных в буфер. Начальное состояние счетчика обнуляем. На выходе модуля к шине адреса (addr) подключаем состояние счетчика (counter). В процедурном блоке по переднему фронту медленного сигнала вычисляем новый адрес для двоичного слова в буфере. Это был модуль записи.
Двухпортовая память
Разработаем двухпортовую память, служащую буфером между двумя потребителями.
На входе памяти шина данных (data), шина адреса (addrA) для записи, шина адреса для чтения (addrB), сигнал разрешения записи (we), тактовый сигнал для чтения (clkB), тактовый сигнал для записи (clkA) и сам выход данных (q). Опишем хранилище данных, указав разрядность слова и количество слов. В процедурном блоке по переднему фронту тактового сигнала производим запись. Обратите внимание, что это тактовый сигнал clkA именно на запись. Еще там проверка на разрешение записи (we).
В другом процедурном блоке по переднему фронту тактового сигнала на чтение (clkB) происходит выставление содержимого ячейки памяти во временный регистр на выходе модуля. С памятью закончили.
Модуль чтения данных
ТНа входе линия тактовых сигналов, линия прерывания, на выходе линия сброса прерывания, тактовый сигнал для чтения данных из буфера и шина адреса. Обратите внимание, что появились две линии пока непонятного назначения. Обе связаны с прерыванием.
Для счетчика адреса нужен регистр (counter). Обнулим его начальное состояние. В процедурном блоке по переднему фронту тактового сигнала вычисляем следующий адрес для чтения данных. Самое главное тут это то, что счет нового адреса будет происходить только по сигналу прерывания (interrupt). Забегая вперед, это такой сигнал, в течении которого происходит считывание. Чтобы не считать больше чем положено, этот сигнал нужно вовремя снять. Для чтения данных нужен тактовый сигнал, выводим его из модуля (clk).
Связываем все модули
Теперь самый главный, ответственный и объемный модуль. Это то, что свяжет все ранее разработанные модули в один работающий организм. Модуль buffer. У него вход для тактового сигнала (clk), выходы линий для активации индикаторов и выход для данных. Это сигналы к семисегментному индикатору.
Сигнал о прерывании будет храниться во внутреннем регистре interrupt. Остальное это провода для связи всех компонентов проекта между собой.
Как формируется шина адреса на чтение. Как мы помним, блок чтения данных работает только с одной банкой, поэтому в его шине адреса на одну линию меньше. В это же самое время, чтение должно происходить с совсем другой банки в которую происходит запись. Если в шину адреса на чтение добавить одну старшую линию с шины на запись, то задача будет решена. Конечно же линия должна быть в инвертированном состоянии.
Материала получилось запредельно много для одной статьи. Эксперимент оставим на следующую. Впереди нас ожидает моделирование, знакомство с такой проблемой, как гонка сигналов и способ ее решения.