Всем привет. Эта третья статья про мой самодельный компьютер на логических микросхемах (первая часть, вторая часть). Как вы догадались из названия, речь пойдет о видеокарте. Видеокарта – это, на мой вкус, лучшая часть этого проекта. Да, процессор – это интересно и круто, но всё же в нем много компромиссных решений. В видеокарте компромиссов почти нет. И рабочая частота у нее 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
Формирование VSYNC

Кроме 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, и больше никогда. Нужно, чтобы выполнялись два условия:

  1. если hx < 800, n_h_rst = 1,

  2. если 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.

Формирование HSYNC
Формирование HSYNC

Запись в видеопамять

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

Видимая область (коричневый) и невидимая (зеленый)
Видимая область (коричневый) и невидимая (зеленый)

Невидимая область занимает 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, которые выдают гладенькие, как бабушкины пирожки, фронты, и разводил критические дорожки с учетом возможных наводок: подальше от других и не ведя их долгое время рядом с одним сигналом, а перепрыгивая в разные участки платы.

Всем спасибо за внимание! Если следующий пост будет, то он будет про АЛУ.

Программа построения графиков
Программа построения графиков