Как стать автором
Обновить

Видеокарта на дискретной логике

Схемотехника *Производство и разработка электроники *DIY или Сделай сам
✏️ Технотекст 2021

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

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

Программа построения графиков
Программа построения графиков
Теги:
Хабы:
Всего голосов 139: ↑139 и ↓0 +139
Просмотры 17K
Комментарии Комментарии 26