Как обнулить память verilog

Обновлено: 06.07.2024

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

Устройство памяти

Основу памяти составляют двоичные слова и схема их выбора. Двоичное слово еще называют ячейкой памяти. По состоянию шины адреса происходит активация той или иной ячейки памяти и в зависимости от состояния линии разрешения можно считать или записать данные в ячейку.

Самый простой модуль памяти имеет шину данных. Для выборки нужной ячейки необходима шина адреса ( A ). При помощи линии разрешения записи ( WE ) можно произвести сохранение информации в память. Для общей синхронизации всех процессов в проекте необходим вход тактовых сигналов ( CLK ).

Поведенческий стиль описания позволит сэкономить массу времени и сделать меньше ошибок. Давайте посмотрим насколько это будет просто. Для удобства сделаем модуль памяти параметризированным. В качестве параметров выделим ширину адресной шины ( ADDR_WIDTH ). Пусть в ней будут 4 линии. Также будет удобным определить ширину шины данных ( DATA_WIDTH ) . И так называемую глубину ( DEPTH ). Глубина это количество ячеек памяти. Разумеется, между количеством линий адресной шины и глубиной есть связь, но давайте для простоты и наглядности не будем городить функции. Количество ячеек 16 .

С параметрами закончили. Описываем входы и выходы. Обязателен вход для тактовых сигналов. Шина адреса. Шина данных на входе и выходе. Линия разрешения записи.

Самый важный момент во всем модуле это описание хранилища данных.

reg [DATA_WIDTH-1:0] mem [DEPTH-1:0];

В процедурном блоке по переднему фронту тактового сигнала если разрешающая линия уровня 1 , происходит запись состояния на входной шине данных в ячейку памяти, адрес которой выставлен на шине адреса.

Выход модуля памяти data_o соединен с ячейкой, адрес которой выставлен на шине адреса mem[addr] . Посмотрим как работает этот модуль. Сразу создадим тестовый стенд.

Посмотрим временные диаграммы. Все работает как нужно.

Довольно часто в проектах приходится хранить двоичные слова как некоторые константы. Для таких случаев применяют упрощенную конструкцию памяти. Поскольку запись в такую память не предусмотрена, то удаляется все что с этим связано. Это линия разрешения записи и даже сам тактовый сигнал не нужен. Остается адрес на входе и двоичное слово на выходе. При начальной установке описывается содержимое ячеек памяти.

При помощи этого содержимого можно преобразовать двоичное число в код, предназначенный для семисегментного индикатора.

Он состоит из полупроводниковых материалов, способных светиться при подаче напряжения между катодом и анодом. Строго говоря, принцип свечения может быть различным. Отдельными сегментами можно отображать арабские цифры, что гораздо удобнее для восприятия пользователем. Сегменты нумеруются сверху по часовой стрелке от A до центральной перекладины G .

Создаем проект с использованием памяти

Создадим модуль, использующий память. В нем позволим пользователю вводить двоичный код числа, а на индикаторе будет отображаться его привычный вид. Четыре бита под код bin , одна линия для активации индикатора на отладочном комплекте seg_select , одна для деактивации матрицы индикаторов matrix_select , три бита под код позиции position , где будет отображена цифра и семь линий для сегментов цифры. Сейчас не обращайте внимание на количество линий, это особенности изделия .
Вписываем в главный модуль созданный блок памяти и соединяем его с нужными шинами. Задаем позицию для числа, активируем и деактивируем компоненты на отладочном комплекте.

Осталось разобраться с одним небольшим, но очень важным моментом. Какие ресурсы ПЛИС использует модуль памяти. Смотрим отчет о синтезе и отмечаем, что задействовано 7 логических ячеек. При этом блочной памяти не задействовано нисколько. Это сейчас кажется, что не страшно и никакой проблемы в этом нет. Но это только пока проекты небольшие. А потом сложность наших проектов возрастет и будем работать на пределе ресурсов ПЛИС. Вот тогда какие ресурсы использованы будет очень важным. Стоит увеличить количество слов в памяти до нескольких килобайт, ресурсы ПЛИС растают прямо на глазах.

Использование ресурсов блочной памяти

Сейчас в описании памяти появилась новая линия cs . Она служит для ее активации. Если память не активирована, то на ее выходах появляются состояния высокого сопротивления, что будет означать физическое рассоединение с остальным проектом. Еще одна особенность. Это регистр для временного хранения содержимого ячейки памяти tmp_data .

Запись в память проходит как обычно, а вот чтение происходит во временный регистр tmp_data . Выход модуля связан не с ячейкой памяти, а с этим временным регистром, причем особым образом.

Это условное присвоение. Слева от вопросительного знака условие, которое может быть истинным или ложным, справа варианты при истинном и ложном значении условия. До двоеточия при истине, после при ложном значении. Все это означает, что на выход модуля пойдет содержимое вр+еменного регистра или на выходах появится высокоомное Z — состояние. Содержимое временного регистра отправится на выход только когда одновременно будет единица на линии активности модуля и ноль на линии записи.

Отчет о синтезе показывает, что теперь блочная память используется.

Так решил компилятор. Что же такое произошло, что данные поместились в совсем другой ресурс? Дополнительная линия активации модуля тут совершенно ни при чем. Это просто удобная штука, позволяющая многим устройствам быть подключенным к единой шине. Просто в один момент времени подключено что-то одно, а остальные устройства при этом в состоянии высокого сопротивления. Быть может это дополнительный временный регистр или разрядность шины данных? Строго говоря, при поведенческом стиле описания модулей единственной гарантией использования ресурсов блочной памяти является отчет о синтезе. Прямое управление ресурсами будет рассмотрено позже

Видео-обзор с канала YouTube

Поддержите статью лайком если понравилось и подпишитесь чтобы ничего не пропускать.

В данной статье разбор простейшей реализации RAM на языке Verilog.

Перед тем, как перейти к разбору кода, рекомендуется изучить базовый синтаксис языка Verilog.

Шаг 1: объявление модуля с соответствующими входными/выходными сигналами

  • data — данные для записи.
  • addr — адрес к участку памяти в RAM.
  • wr — статус (считывание/запись).
  • clk — clock cycle системы.
  • response — готовность RAM (1 — если RAM обработал запрос на считывание/запись, 0 — в противном случае).
  • out — данные, считанные из RAM.

Шаг 2: объявление регистров внутри модуля

Объявление массива для хранения данных:


Также нам понадобится хранить предыдущие входные параметры с целью отслеживания их изменений в always блоке:


И последние два регистра для обновления выходных сигналов после вычислений в always блоке:

Шаг 3: реализация логики always блока

Always блок срабатывает на negedje, т.е. в момент перехода clock-а с 1 на 0. Это сделано для правильной синхронизации RAM-а c кэшем. Иначе возможны случаи, когда RAM не успевает сбросить статус готовности с 1 на 0 и на следующем clock-е кэш решает, что RAM благополучно обработал его запрос, что в корне неверно.

Логика алгоритма always блока такова: если данные обновлены, сбрасываем статус готовности на 0 и записываем/считываем данные, если запись/считывание выполнены, обновляем статус готовности на 1.

В конце добавляем следующий участок кода:


Тип выходных сигналов нашего модуля — wire. Единственный способ изменения сигналов данного типа — долгосрочное присваивание, являющееся запрещённым внутри always блока. По этой причине в always блоке используются регистры, которые в дальнейшем присваиваются выходным сигналам.

Direct mapping cache

Direct mapping cache — один из наиболее простых видов кэша. В данной реализации кэш состоит из n элементов, а RAM условно делится на блоки по n, тогда i-ому элементу в кэше соответствуют все такие k-ые элементы в RAM, удовлетворяющие условию i = k % n.

На приведённом ниже рисунке изображён кэш размером 4 и RAM размером 16.


Каждый элемент кэша содержит следующую информацию:

  • бит валидности — является ли информация в кэше актуальной.
  • тэг — номер блока в RAM, где находится этот элемент.
  • данные — информация, которую мы записываем/считываем.

Шаг 1: объявление модуля с соответствующими входными/выходными сигналами

Объявление модуля кэша идентично RAM-у, за исключением нового выходного сигнала is_missrate. Этот выходной сигнал хранит информацию о том, был ли последний запрос на считывание missrate-ом.

Шаг 2: объявление регистров и RAM-а

Перед тем, как объявить регистры, определим размеры кэша и индекса:


Далее, объявляем массив, в котором и будут храниться данные, которые мы записываем и считываем:


Также нам нужно хранить биты валидности и тэги для каждого элемента в кэше:


Регистры, в которые будет разбит входной адрес:


Регистры, хранящие в себе входные значения на предыдущим clock-е(для отслеживания изменений входных данных):


Регистры для обновления выходных сигналов после вычислений в always блоке:


Выходные значения для RAM:


Объявление модуля RAM и подключение входных и выходных сигналов:

Шаг 3: реализация логики always блока

Начнём с того, что на каждый clock у нас есть два состояния — входные данные изменены, либо не изменены. Исходя из этого мы имеем следующее условие:


Блок 1. В случае, если входные данные изменены, первым делом мы сбрасываем статус готовности на 0:


Далее мы обновляем регистры, хранившие входные значения предыдущего clock-а:


Разбиваем входной адрес на тэг и индекс:


Для вычисления тэга используется побитовый сдвиг вправо, для индекса достаточно просто присвоить, т.к. лишние разряды адреса не учитываются.

Следующий шаг — выбор между записью и считыванием:


В случае записи первоначально мы изменяем данные в кэше, затем обновляем входные данные для RAM-а. В случае считывания мы проверяем наличие данного элемента в кэше и, если он есть, записываем его в out_reg, в противном случае обращаемся в RAM.

Блок 2. Если данные не были изменены с момента выполнения предыдущего clock-а, то мы имеем следующий код:


Здесь мы ждём окончания выполнения обращения в RAM(в случае если обращения не было, ram_response равен 1), обновляем данные, если была команда на считывание и устанавливаем готовность кэша на 1.

Комментарии делятся на:


- Однострочные (//);
- Блочные (/* текст комментария или часть программы */).

Однострочными комментариями можно закомментировать часть одной строки или всю строку полностью.


Из примера видно, что строка №34 закомментирована полностью. В то время как в строке №36 закомментировали часть строки «Rc3 & Rc4» и заменили на выражение «Rc3 | Rc4». А к строке №35 написали комментарий.


Блочные комментарии дают возможность закомментировать участок программы. Но следует отметить, что рассматриваемый тип комментариев не позволяет комментировать часть кода в строке.


Хотелось бы отметить, что именно комментарии дают возможность написать заголовок к листингу. То есть отразить непосредственно в коде программы автора программы, назначение блока и т.п.
В Verilog имя (идентификатор) - последовательность букв и цифр, знаков «$» и «_», причем начинаться оно обязано не с цифры. Регистр имеет значение. Если начальный символ – «\», то следом за ним может идти любая последовательность символов. Все, что до пробела, будет считаться корректным именем. Например: «Character», «cHaracter», «$Character», «\c+Ha^racter».

Правило объявления постоянных (констант) в Verilog имеет следующий вид

[размер]['система счисления] значение константы

При объявлении размера указывается число бит в слове константы, а в системе счисления указывается соответствующий код (d = десятичный, b = двоичный, o = восьмеричный, h = шестнадцатеричный). По умолчанию система счисления воспринимается как десятичная.



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

Значения сигналов - сигналы в Verilog могут принимать одно из четырех значений:

- 0 (логический 0);
- 1 (логическая 1);
- X (не задан или не определен);
- Z (состояние высокого импеданса; для применения тристабильных состояний).


Параметры - параметр в Verilog может быть любая постоянная величина. Параметры используются для унификации блоков (макросов). Например, 4-разрядный сумматор становится полезнее если его описать как n-разрядный сумматор, где n – параметр разрядности, задаваемый пользователем перед компиляцией. Ниже приведены некоторые типичные примеры применения параметров:



Память - Verilog позволяет использовать двумерные массивы, которые обычно используют как память (ОЗУ).


Из примера видно, что объявляем регистр «m», как двумерный массив, состоящий из 64-х восьми-битных слов. Вы можете получить доступ к любому из 64-х слов указав, например m [2].


Verilog 2001 поддерживает 2-уровневый операции по чтению объявленного массива, такие как m [2], [3]. Таким образом получаете доступ к отдельным битам считываемого байта.

Verilog

Verilog - язык описания цифровых схем. На первом уроке познакомимся с базовыми типами источников сигнала используемыми в языке.

Пожалуй было бы не плохо начать наше обсуждение с понятия сигнал ( signal ). Сигналы – это электрические импульсы, которые передаются по проводам ( wire ) между логическими элементами схемы. Провода переносят информацию не производя над ней никаких вычислений. В цифровой схеме сигналы важны для передачи двоичных данных.

Базовый тип источника сигнала в языке Verilog – это провод, wire . Таким образом, если у вас есть арифметическое или логическое выражение, вы можете ассоциировать результат выражения с именованным проводом и позже использовать его в других выражениях. Это немного похоже на переменные, только их (как провода в схеме) нельзя пересоединить на лету, нельзя поменять назначение. Значение провода ( wire ) – это функция того, что присоединено к нему.

Вот пример декларации однобитного провода в “программе” Verilog:

Регистры описываются так же как и провода:
reg [3:0] m;
reg [0:100] n;
Они могут использоваться так же, как и провода в правой части выражений, как операнды:
wire [1:0] p = m[2:1];
Вы можете определить массив регистров, которые обычно называют “память” ( RAM ):
reg [7:0] q [0:15]; //память из 16 слов, каждое по 8 бит
Еще один тип источника сигнала – это integer . Он похож на регистр reg , но всегда является 32х битным знаковым типом данных. Например, объявим:
integer loop_count;

Verilog позволяет группировать логику в блоки. Каждый блок логики называется “модулем” ( module ). Модули имеют входы и выходы, которые ведут себя как сигналы wire .

При описании модуля сперва перечисляют его порты (входы и выходы):

module my_module_name (port_a, port_b, w, y, z);
А затем описывают направление сигналов:

input port_a;
output [6:0] port_b;
input [0:4] w;
inout y; //двунаправленный сигнал, обычно используется

//только для внешних контактов микросхем

Числа – это числа. Они могут использоваться во всяких арифметических и логических выражениях. Например, можно прибавить 1 к вектору “ aa ”:
wire [3:0] aa;
wire [3:0] bb;
assign bb = aa + 1;
На следующих уроках будет рассказано про некоторые арифметические и логические функции.

Читайте также: