Всем привет. Эта третья статья про мой самодельный компьютер на логических микросхемах (первая часть, вторая часть). Как вы догадались из названия, речь пойдет о видеокарте. Видеокарта – это, на мой вкус, лучшая часть этого проекта. Да, процессор – это интересно и круто, но всё же в нем много компромиссных решений. В видеокарте компромиссов почти нет. И рабочая частота у нее 25,175 МГц – это не жалкие 1,5 МГц у процессора.
В первой версии моего компьютера для вывода информации использовался обычный LCD-модуль 16x2 со знакогенератором. Конечно, это было здорово для начала, но хотелось большего. И я решил делать видеокарту.
Как и со всем компьютером, хотелось функционального, но не сильно сложного. Из-за ограничений адресного пространства (64кБ) графические режимы с высоким разрешением сразу отпадают. Остается два варианта: огромные пиксели или текст. Огромные пиксели мне кажутся ужасно некрасивыми и неудобными, а текстовые режимы, наоборот, кажутся приятными и вызывают ностальгию по временам DOS и Norton Commander.
Решено было делать цветной текстовый режим 80x30 символов, каждый 8x16 пикселей. Итого получается 640x480 – стандартный режим VGA, который будет точно поддерживаться любым монитором.
Видеосигнал
Чтобы вывести картинку на монитор, достаточно реализовать пять сигналов порта VGA: два цифровых (HSYNC и VSYNC) и три аналоговых (красный, зеленый и синий). Секрет успеха – в точности соблюсти все тайминги согласно стандартам.
Я не буду в деталях расписывать все подробности видеосигнала, об этом достаточно информации в сети. Подробно тайминги для выбранного видеорежима описаны здесь.
Итак, чтобы сформировать видеосигнал, нужно иметь два счетчика: горизонтальный и вертикальный. Горизонтальный считает на частоте 25,175 МГц, а вертикальный инкрементируется каждый раз, когда горизонтальный доходит до числа 800 (640 видимых пикселей плюс 160 невидимых).
Если бы это была программа, то формирование HSYNC и VSYNC выглядело бы так:
for (int vy = 0; vy != 525; ++vy) {
if (vy >= 480 + 10 && vy < 480 + 10 + 2) {
vsync = 0;
} else {
vsync = 1;
}
for (int hx = 0; hx != 800; ++hx) {
if (hx >= 640 + 16 && hx < 640 + 16 + 96) {
hsync = 0;
} else {
hsync = 1;
}
}
}
Рассмотрим на примере вертикального счетчика, как реализовать это аппаратно.
Так как считать нужно до 525, достаточно 10 бит или трех микросхем 74lv161a.
Сигнал вертикальной синхронизации должен быть нулём только в том случае, когда vy
равно 490 или 491.
vsync = ~(vy == 490 | vy == 491)
Запишем это в двоичных числах.
vsync = ~(vy == 01 1110 1010 | vy == 01 1110 1011)
Заметим, что два этих числа отличаются только младшим битом. Значит, значение этого бита нас не волнует.
vsync = ~(vy == 01 1110 101x)
Подумаем, в каких случаях vsync
точно будет единицей. Во-первых, если на месте какого-либо нуля будет стоять единица. Во-вторых, если на месте какой-либо единицы будет ноль.
vsync = vy[2] | vy[4] | vy[9] |
~(vy[1] & vy[3] & vy[5] & vy[6] & vy[7] & vy[8])
Здесь индекс в квадратных скобках – это индекс бита, как в Verilog.
Теперь можно вспомнить, что vy
не может быть больше 524 (10 0000 1100
в двоичном представлении). Это значит, vy
не может быть чем-нибудь вроде 11 1110 1010
(единицы на позициях с 5 по 9). То есть, если бит 9 единица, биты 5-8 точно будут 0. Благодаря этому наблюдению можно исключить vy[9]
из выражения: в случае, когда vy[9] == 1
, какой-либо из vy[5..8]
будет нулём, и последний член дизъюнкции будет единицей.
vsync = vy[2] | vy[4] |
~(vy[1] & vy[3] & vy[5] & vy[6] & vy[7] & vy[8])
Кажется, дальше упростить нельзя. Поэтому придется просто выбрать наиболее подходящие микросхемы из списка и реализовать эту формулу с их помощью.
Кроме vsync
, подобным образом нужно сформировать:
vy == 525
для сброса вертикального счетчика,hx == 800
для сброса горизонтального счетчика,hx >= 656 && hx < 752
для горизонтальной синхронизации,hx < 640 && vy < 480
– видимая область.
Разберем еще одно выражение: hx == 800
. Сигналы сброса счетчиков имеют активный низкий уровень (т.е., когда на линии ноль, происходит сброс), поэтому нам нужно, чтобы:
n_h_rst = hx != 800
В двоичном представлении:
n_h_rst = ~(hx == 11 0010 0000)
Что же, нужно проверять все десять бит? Нет! На самом деле, нам не нужно, чтобы n_h_rst
был ноль строго в том случае, когда hx == 800
, и больше никогда. Нужно, чтобы выполнялись два условия:
если
hx < 800
,n_h_rst = 1
,если
hx == 800
,n_h_rst = 0
.
Что происходит после 800, нас не интересует, потому что счетчик уже будет сброшен и таких значений никогда не возникнет. Заметим, что единственное число от 0 до 800 включительно, в котором все три бита 5, 8 и 9 единицы, это само 800 (11 0010 0000
). Поэтому для сброса будет достаточно такого простого выражения:
n_h_rst = ~(hx[9] & hx[8] & hx[5])
Формирование изображения
В ПЗУ знакогенератора закодированы изображения символов: так как размер каждого символа 8x16, а изображение формируется построчно, удобно хранить шрифт тоже построчно: одна строчка – один байт. При этом на каждый символ потребуется 16 байт, а всего на 256 символов – 4096 байт. Биты 4-11 адреса ПЗУ будут отвечать за код символа, а биты 0-3 – за строчку в нем.
Чтобы хранить символы и их цвета в видеопамяти, я выбрал такой подход: иметь два независимых сегмента для символов и цветов. Адрес внутри каждого сегмента вычисляется по следующей формуле:
offset = (row << 7) + col
То есть, старшие 5 бит (так как строк 30) – номер строки, младшие 7 бит (80 столбцов) – номер столбца.
Эти адреса легко получить из значений вертикального и горизонтального счётчиков:
Теперь можно нарисовать блок-схему:
Старшие биты счетчиков комбинируются в адрес, значение из памяти цветов сразу подается на выходной мультиплексор, а по значению из текстовой памяти и младшим битам вертикального счетчика сначала из ПЗУ знакогенератора извлекается строчка, из которой нижний мультиплексор извлекает отдельный бит, по значению которого верхним мультиплексором выбирается один из двух цветов. Получившиеся 4 бита цвета подаются на ЦАП, который преобразует их в аналоговые сигналы.
И тут возникает проблема: это не будет работать!
Загрузка значений из памяти займет около 200 нс или почти две трети ширины символа (318 нс). С такой схемой левые части всех символов будут нарисованы неправильно.
Выход есть – кэшировать значения строчки пикселей и цвета. Пока память подготавливает новые значения, мультиплексоры будут работать с предыдущими, сохраненными в регистры.
Получаются такие тайминги:
Тактовый сигнал ccol_clk
, по которому значения защёлкивается в регистры, – это просто инвертированный третий бит горизонтального счётчика. В качестве регистров используются знакомые 74lv273a.
Теперь, когда значения кэшируются, выходной сигнал получается сдвинутым на 8 пикселей вправо. Поэтому HSYNC тоже нужно сдвинуть вправо на такой же интервал. Проще всего это сделать, так же закэшировав его через однобитный регистр 74lv74a.
Запись в видеопамять
Нельзя одновременно читать из ОЗУ и писать в него, к тому же по разным адресам. Да, существуют двухпортовые ОЗУ, но я решил не использовать в этом проекте настолько сложные микросхемы. Поэтому процессору разрешается писать в видеопамять только в то время, когда "луч" сканирует невидимую область.
Невидимая область занимает 26% времени (всего 800x525 = 420 000, видимая 640x480 = 307 200, невидимая 112 800), что не так плохо. В то время, когда "луч" бежит по видимой области, на адресные входы видеопамяти должен подаваться адрес, составленный из битов вертикального и горизонтального счетчика, а если "луч" в невидимой области, то – адрес со внешней шины. Для мультиплексирования адреса между внешней и внутренней шиной используется три микросхемы 74lv157a. Буферы 74lv244a соединяет внешнюю шину данных со входами данных микросхем ОЗУ. Они активируются, если во время прохода невидимой области запрошена запись в соответствующий сегмент.
Чтобы не загромождать схему, показано только текстовое ОЗУ. Для цветового ОЗУ схема аналогична.
С помощью вспомогательной логики, декодирующей верхние биты адреса, видеопамять отображается на адресное пространство процессора: текстовый сегмент по адресу 0xE000
, цветовой сегмент – на 0xD000
.
Сигнал готовности памяти
Процессор не синхронизирован с видеокартой, и чтобы запись в видеопамять сработала корректно, нужен сигнал, сообщающий процессору о готовности периферийного устройства. Этот сигнал организован по принципу открытого стока (открытого коллектора).
По умолчанию mem_rdy
имеет высокий уровень благодаря подтягивающему резистору, но если хотя бы один из busy_1..busy_n
будет единицей, соответствующий транзистор откроется и притянет сигнал к нулю. Эти транзисторы расположены на платах периферийных устройств, один из них на видеокарте.
Сначала я пытался сделать обработку сигнала mem_rdy
на плате управления. Но логика получилась слишком сложной, и в отдельных случаях возникали ошибки, потому что я не учёл всех вариантов. Поэтому я переделал задающий генератор процессора так, чтобы он останавливался при низком уровне mem_rdy
.
Здесь на тактовый вход C (3) триггера U30A поступает исходный тактовый сигнал, частота которого в два раза выше той, на которой работает процессор. Если mem_rdy = 1
, то в триггер будет защёлкиваться инвертированное значение с его же выхода, в противном случае состояние триггера не изменится. Настоящий тактовый сигнал для процессора берется с выхода триггера.
ЦАП
Выходной ЦАП – самая простая часть этой схемы. Цвет – 4 бита IRGB, где I – интенсивность. Значения цвета на каждом канале вычисляются по формуле (I + 2 * C) / 3
, где C – R, G или B. ЦАП для каждого канала сводится к двум резисторам и одному диоду:
Диод нужен для того, чтобы три канала не мешали друг другу: сигнал интенсивности для всех общий. Резисторы надо подобрать так, чтобы получались желаемые цвета.
Результат
Вот, что получилось. В качестве памяти используются микросхемы 62256. Несмотря на больший, чем нужно, объем, эти микросхемы дешевые, поэтому я и поставил их. Так же и ПЗУ: из 32 кБ AT28C256 используется только 4, но зато у меня этих микросхем целая куча, не жалко испортить.
Скорости процессора и времени доступа к видеопамяти хватает, чтобы мгновенно очистить экран одним цветом. Можно рисовать эффект из Матрицы с приличной скоростью:
Игру "жизнь" с полем 80x60 удалось оптимизировать до одного кадра в секунду:
Здесь "пиксели" – это на самом деле символы, у которых закрашена верхняя, нижняя или обе половины.
Исправление ошибок
Когда мне пришли из Китая платы для видеокарты, я припаял все компоненты и почти сразу увидел правильную картинку винегрета из случайных символов, такую:
Но когда я стал пытаться запускать программы, которые что-то пишут в видеопамять, начали происходить странные вещи. Иногда всё работало хорошо и стабильно, иногда монитор терял сигнал и сразу вновь его находил, а иногда даже весь компьютер перезагружался. Это зависело, внезапно, от того, написана ли программа на ассемблере или скомпилирована. Я долго сидел с осциллографом, но увидел только перебои в сигналах синхронизации, из-за которых монитор и терял сигнал. Самое интересное, что осциллограф не видел никаких всплесков на линии rst
, которая сбрасывает весь компьютер. В итоге я всё-таки подумал, что глюки вызваны наводками на эту линию и оказался прав. Когда я изолировал ее, всё заработало стабильно. Так я еще раз познакомился с магией высокочастотных схем.
Одна из причин, почему на плате были наводки, – это использование микросхем серии 74ACT в первой версии модуля регистров, к которой тогда была подключена видеокарта. Тогда я еще не знал этих подводных камней, и при выборе серии руководствовался правилом "чем быстрее, тем лучше". У 74ACT очень низкие задержки. При переключении эти микросхемы долбят острыми, как катана самурая, фронтами, высокочастотные гармоники которых проскакивают на соседние дорожки. Когда я проектировал видеокарту, я уже знал о проблемах этой серии, потому что столкнулся с похожими эффектами в АЛУ, поэтому использовал в видеокарте чуть менее резкую серию 74LV-A. Во всех следующих модулях компьютера я использовал 74HC, которые выдают гладенькие, как бабушкины пирожки, фронты, и разводил критические дорожки с учетом возможных наводок: подальше от других и не ведя их долгое время рядом с одним сигналом, а перепрыгивая в разные участки платы.
Всем спасибо за внимание! Если следующий пост будет, то он будет про АЛУ.