Imax9
NEWS   ARTICLES   MINIMIG   FILES   ABOUT

Нужно ехать быстро.

Предлагаю вам перевод цикла Writing a new SDRAM controller Part 3 автора Alastair M. Robinson.

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

Задачи, которые еще предстоит решить, заключаются в том, чтобы контроллер своевременно реагировал на входящие запросы по нескольким портам и работал достаточно быстро, чтобы тайминги удовлетворяли требуемой скорости. Для ядра PC Engine желаемая тактовая частота составляет 128 МГц.

Типичные контроллеры SDRAM, используемые в ретрокомпьютерных проектах, используют конечный автомат, который циклически проходит через несколько заранее определенных состояний. Обычно одно из них запускает команду активации, другое посылает чтение или запись, а третие фиксирует входящие данные. Контроллер обычно принимает порт-ориентированный вид входящих транзакций, приоритетный шифратор различных портов генерирует сигналы, проверяя, что подходящий банк не используется, и обслуживает порт с наивысшим приоритетом, банк которого свободен.

Я хотел сделать все немного по-другому.

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

У этого есть два преимущества:

Конвейер имеет несколько этапов, которые я называю RAS, CAS, Mask и Latch. Некоторые из них имеют длину более 1 цикла, но поскольку мы запускаем транзакции только в альтернативных циклах (поскольку мы должны подчиняться минимальному времени чипа SDRAM между командами Active), нам нужен только один набор регистров хранения на каждом этапе.

В ядре PC Engine ни один из портов не имеет адресного пространства больше 8 мегабайт, а это значит, что все они помещаются в одном банке. Это несколько облегчает жизнь – это означает, что мы можем выделить по одному банку для двух областей VRAM, другой - для ROM и WRAM, а четвертый - для ARAM. (Это примерно в том порядке, насколько срочно мы должны обслуживать каждый порт.)

Критические пути по времени в типичном контроллере SDRAM ретро ядер, как правило, находятся между входящими сигналами запроса и адресными выводами SDRAM. Причины этого двояки: во-первых, возникают сложности с шифратором приоритета выбора порта, который будет обслуживаться, как правило, в конечном итоге выбирает один из нескольких входов мультиплексоров в адресных линиях. Что еще хуже, это относится только к циклам RAS (Active), а для циклов CAS (Read/Write) адресная шина должна управляться совершенно другим сигналом. Да, и еще должна быть последовательность инициализации, которая также должна записывать еще другие значения в адресную шину. Все это, как правило, означает, что часто существует путаница комбинационной логики и мультиплексоров, предшествующих выходным регистрам, управляющим адресными выводами SDRAM. Упрощение этого очень важно, если мы собираемся приблизиться к 128 МГц.

Как правило, цель состоит в том, чтобы начать транзакции как можно скорее, поэтому возникает соблазн попытаться избежать регистрации входящих адресов и сигналов запроса, и в этом случае любые задержки на них будут способствовать критическим путям. Чтобы избежать этого, я действительно регистрирую эти сигналы. Каждый банк имеет свой собственный шифратор приоритетов (работающий в блоке “always @(posedge clk)”, который, конечно, может быть преобразован в комбинационную логику, если я захочу, просто изменив его на “always @(*)” ). Таким образом, сколько бы портов мы ни обслуживали, у нас есть максимум четыре запроса на обслуживание главного контроллера, и сигналы запросов для каждого банка - это хорошие чистые, свежие регистры без временного багажа.

Адрес, передаваемый адресным выводам SDRAM на этапе RAS, должен быть выбран и записан как можно быстрее, но это не относится ни к фазам инициализации, ни к фазам CAS – в обоих этих случаях мы заранее знаем за несколько циклов, какое значение должно быть записано, и это помогает упростить синхронизацию; мы можем просто записать значение за цикл вперед в удерживающий регистр и записать содержимое этого регистра в качестве значения по умолчанию в адресные выводы в любое время, когда мы не находимся на этапе RAS.

Другими словами, вместо того, чтобы делать то, что равносильно:

case(state)
ras: sdram_addr <= <complicated port-oriented priority encoding>
cas: sdram_addr <= casaddr;
init_precharge_all: sdram_addr <= <bit 10 high>;
init_set_mode: sdram_addr <= <mode value>;
endcase

... или еще хуже

if(init)
sdram_addr <= <whatever the init logic currently wants to write>
else
case(state)
ras: sdram_addr <= <complicated port-oriented priority encoding>
cas: sdram_addr <= casaddr;
endcase

Мы сделаем так :

sdram_addr <= next_a;
if(ras)
sdram_addr <= <somewhat simpler bank-oriented priority encoding>;

if(cas_coming_up_soon)
next_a <= casaddr;
else if(init_set_mode_coming_soon)
next_a <= <mode value>;
else
next_a <= <bit 10 high>

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

Говоря о критических регистрах, чипы серии Cyclone предлагают “Быстрые регистры ввода-вывода”, которые полезны при отправке сигналов вне чипа. В основном это означает, что регистры физически очень близки к выводам FPGA, поэтому любая внутренняя задержка маршрутизации сводится к минимуму. Однако есть ограничения – между регистром и выводом не может быть никакой комбинационной логики, поэтому для выходов мы не можем сделать ничего подобного:

assign sdram_addr = (state == ras) ? ras_addr : cas_addr;

поскольку это вставит мультиплексоры между сигналами ras_addr и cas_addr и выводами sdram_addr.

Точно так же для входных данных заманчиво сделать что-то подобное с входящими данными:

always @(posedge clk) begin
case(latch_port)
PORT_ROM: rom_q <= SDRAM_DATA;
PORT_VRAM: vram_q <= SDRAM_DATA;
...
endcase
end

Опять же, это вставит мультиплексоры между входящими линиями данных и регистрами, принадлежащими каждому порту.

В обоих случаях нет ничего “незаконного”, как такового, в этом, но это обнуляет использование быстрых регистров ввода – вывода, поэтому мы внесем дополнительные задержки с ножек ввода-вывода, что может как вызвать, так и не вызвать проблему.

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

always @(posedge clk) begin
sdram_data_reg <= SDRAM_DATA;

case(latch_port)
PORT_ROM: rom_q_reg <= sdram_data_reg;
PORT_VRAM: vram_q_reg <= sdram_data_reg;
...
endcase
end

assign rom_q = (latch_port == PORT_ROM) ? sdram_data_reg : rom_q_reg;
assign vram_q = (latch_port == PORT_VRAM) ? sdram_data_reg : vram_q_reg;
...

Есть еще одна часть головоломки, о которой я еще не говорил, и это регенерация – я расскажу об этом в следующий раз.


Адрес для контактов : imax9@narod.ru

Если вам понравились мои работы и вы желаете поддержать сайт - сделайте дотацию.

При копировании статьи – обязательна ссылка на авторство и источник. Без разрешения автора копирование запрещено.

© Максим Ильин 2021г.

Яндекс.Метрика