Нужно ехать быстро.
Предлагаю вам перевод цикла Writing a new SDRAM controller Part 3 автора Alastair M. Robinson.
В предыдущих частях я говорил об улучшении пропускной способности за счет чередования транзакций чтения и записи в разные банки, а также о шаблонах доступа, которых мне нужно избегать, и о некоторых деликатных проблемах с таймингами, которые необходимо учитывать.
Задачи, которые еще предстоит решить, заключаются в том, чтобы контроллер своевременно реагировал на входящие запросы по нескольким портам и работал достаточно быстро, чтобы тайминги удовлетворяли требуемой скорости. Для ядра PC Engine желаемая тактовая частота составляет 128 МГц.
Типичные контроллеры SDRAM, используемые в ретрокомпьютерных проектах, используют конечный автомат, который циклически проходит через несколько заранее определенных состояний. Обычно одно из них запускает команду активации, другое посылает чтение или запись, а третие фиксирует входящие данные. Контроллер обычно принимает порт-ориентированный вид входящих транзакций, приоритетный шифратор различных портов генерирует сигналы, проверяя, что подходящий банк не используется, и обслуживает порт с наивысшим приоритетом, банк которого свободен.
Я хотел сделать все немного по-другому.
Вместо конечного автомата я хотел использовать более простую модель конвейера, похожую на процессор, используя что-то аналогичное hazards and bubbles. Вместо представления, ориентированного на порт, я беру банк-ориентированное представление входящих запросов, в результате чего на каждом альтернативном цикле я могу начать транзакцию с любым банком, который в данный момент не используется.
У этого есть два преимущества:
- Во-первых, это помогает максимизировать пропускную способность, так как банк не остается ждать появления слота конечного автомата (некоторые контроллеры смягчают это, используя “короткие пути” через состояния, хотя это усложняет отслеживание того, какие запросы находятся в выполнении и готовы к фиксации. В частности, я обнаружил, что очень трудно добавить задержки, чтобы избежать проблемы чтения с последующей записью, о которой я говорил ранее).
- Во-вторых, он отодвигает часть шифратора приоритов от наиболее критической по времени точки, что, как мы увидим позже, очень важно.
Конвейер имеет несколько этапов, которые я называю RAS, CAS, Mask и Latch. Некоторые из них имеют длину более 1 цикла, но поскольку мы запускаем транзакции только в альтернативных циклах (поскольку мы должны подчиняться минимальному времени чипа SDRAM между командами Active), нам нужен только один набор регистров хранения на каждом этапе.
- На этапе RAS мы выбираем банк и записываем порт, адрес столбца и какие-то данные для записи в регистры, а затем устанавливаем тайм-аут, который блокирует этот банк до возможной дальнейшей работы с ним.
- Последний цикл этапа RAS передает данные на этап CAS.
- Этап CAS посылает на чип команду чтения или записи. Адрес больше не нужен, но остальные данные транзакции передаются на этап Mask.
- Если это была транзакция чтения, то последний цикл этапа Mask переводит порт на этап Latch; в противном случае цикл завершен.
В ядре 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г.