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

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

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

Комментарии (22)


  1. YMA
    29.11.2021 14:40
    +11

    Поэтому процессору разрешается писать в видеопамять только в то время, когда "луч" сканирует невидимую область.

    Прямо олдскулы свело, вспомнился "снег" на CGA - там наоборот, процессор имел доступ когда пожелает, а ущемлялись права генератора сигнала. Для эстетов решение было софтовое, тоже ждали vsync.


  1. 8street
    29.11.2021 18:03
    +2

    При переключении эти микросхемы долбят острыми, как катана самурая, фронтами

    Чтобы такого не было, около выхода микросхемы, в разрыв дорожки, ставится малоомный резистор, зачастую 47 Ом достаточно. Емкость линии и этот резистор образуют простой ФНЧ, который несколько сглаживает фронты сигнала. Если передача двухсторонняя, то резисторы ставятся у каждого источника/приемника сигнала.


    1. ynoxinul Автор
      29.11.2021 18:09
      +1

      Это помогает против переотражений сигнанла и "звона", а поможет ли от наводок?

      Кстати, это уже третье объяснение, почему нужно ставить резистор у передатчика :) Первые два были:

      1. Резистор увеличит сопротивление линии, что сгладит отражения, потому что они низкой мощности.

      2. Резистор согласует импеданс линии с импедансом передатчика, что уберет отражения.

      Я не особо в этой высокочастотной магии разбираюсь, но мне кажется, все три объяснения – просто взгляд с разных сторон на один и тот же процесс. Правильно?


      1. mpa4b
        29.11.2021 18:40
        +7

        1. Резистор конечно же не увеличит сопротивление линии, но если его собственное сопротивление много больше сопротивления линии, то сигнал у приёмника будет похож на то, как заряжается RC-цепочка, где R -- этот самый резистор, а C -- суммарная емкость линии. Ну разве что будет не чистая экспонента, а ступеньками с длительностью времени прохода сигнала по линии туда-сюда.

        2. Резистор у источника НЕ убирает отражения у приёмника, если там несогласовано. Он эти отражения -- поглощает (если его сопротивление, конечно, равно или близко к волновому сопротивлению линии). Со стороны приёмника (который с т.з. линии -- обрыв) это выглядит как приходящий чистый сигнал без звона, вызванного отражениями.

        3. И от наводок тоже резистор поможет, в случае, если источник на линии преимущественно видит ёмкостную нагрузку (а это случай для несогласованных относительно высокоомных линий, типа как если всё распаяно МГТФом или сделано на 2-слойной плате без полностью залитого одного слоя земли, и оканчиващихся высокоомным входом цифровой микросхемы). Дело в том, что заряд емкости резким фронтом -- это резкий и сильный импульс тока, который течёт и в обратную сторону по земле, связывающей приёмник и источник, получается такой 'виток' с током, который наводит пики напряжения во всех других 'витках', связанных с ним индуктивно (т.е. перекрывающихся с ним частично на плате).


      1. 8street
        29.11.2021 18:51
        +2

        Тут уже дали некоторые ответы. Дополню.

        От наводок хорошо поможет минимальное расстояние между прямым и обратным проводником. Обычно обратным проводником является 0V или GND, поэтому есть рекомендация заливать GND полигоном. Вообще есть разные трюки с землей и зеленным полигоном. Например, применяется не только земляной полигон, но и окружение дорожек землей. Я не знаю как у вас разведены слои, но если у вас на верхнем слое, на фото, тоже земля, то это правильно, не хватает разве только переходных отверстий с равным шагом на внутренний (или с обратной стороны) земляной полигон.

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

        Еще можно несколько понизить сопротивление линии, чтобы электромагнитным волнам наводок было сложнее навести на ней напряжение, просто соединив резистором линию передачи (в её конце) и землю, скажем 5-47 кОм (выбирается как можно более низким, но при этом с сохранением работоспособности). Но это может вызвать ещё некоторые проблемы, поскольку отдаваемый ток в линию возрастет, то возрастет и потребление, импульсное, поэтому блокировочные емкости возле микросхемы необходимы. Этот способ также описан в согласовании волновых сопротивлений и для некоторых стандартных линий передачи применяют довольно малые сопротивления, около 100 Ом.


      1. SeregaSA73
        30.11.2021 10:57

        Не всякий резистор поможет, нужен обычный в круглом корпусе, он имеет индуктивное сопротивление и гасит вч помеху :)


        1. ynoxinul Автор
          30.11.2021 11:18

          Мне 0805 помог против звона.


  1. mpa4b
    29.11.2021 18:24
    +5

    Очень странно что у вас процессор на 1.5 Мгц может писать в 55нс память только на бордюре, когда оттуда не читаются видеоданные. Казалось бы, память 55 нс, это два ваших такта 25.175 Мгц. Видео читается раз в 8 ваших тактов. Остальные 3 слота по 2 такта совершенно свободны, во время них прекрасно мог бы влезть процессор и не один.

    Таким макаром вообще можно было бы всю память расшарить между видео и процессором сразу, и более того, сделать эту память на DRAM (асинхронная, та что в симмах, 70 нс цикл) и получить сразу мегабайты оной 1 чипом.


    1. ynoxinul Автор
      29.11.2021 18:31

      Интересная идея. Когда я делал видеокарту, я такое даже не рассматривал, казалось слишком сложным.


      1. sterr
        29.11.2021 20:03
        +1

        Изучите "Ленинград" Зонова, там все просто.


    1. forthuser
      04.12.2021 13:09

      Да, вспомнился даже такой проект.
      Linux on an 8-bit micro

      image


  1. 3epg
    29.11.2021 19:03
    +1

    Прочитал все три статьи. Больше всего собственно видеокарта интересовала, года два вынашиваю мысли по постройке компьютера на базе Zilog Z180, который случайно обнаружил в каком-то телефонном оборудовании, но так как не хотел делать вывод через всякие 1602 или семисегментные индикаторы, а ПЛИС для постройки видеокарты как-то не труъ использовать.

    А ваш проект случаем не опенсорс? Можно ли где нибудь посмотреть “исходники”? Думаю многим было бы интересно.


    1. ynoxinul Автор
      29.11.2021 19:10
      +1

      Да, опенсорс: https://github.com/imihajlow/ccpu/

      Если задумаете прикрутить мою видеокарту к Z180, буду рад помочь советом.


  1. FSA
    29.11.2021 19:55
    +1

    Могу предложить идею, что ещё можно сделать. Я начинал своё знакомство с компьютерами с ZX-Spectrum. Точнее и их клонов. И если понять как работает сам компьютер я ещё мог по принципиальной схеме, то самый для меня сложный блок был именно блок формирования изображения.

    Почему стоит попробовать? Потому что это уже не дисплей двухстрочный, но ещё и не VGA монитор. Зато можно подключить к любому телевизору, хоть Рекорду В-312 :-D (интересно, есть ли до сих пор работающие экземпляры?) А ещё, мне удавалось получить изображение с видеокарты VGA на телевизоре УПИМЦТ. Пользовался RGB входом для своего ZX-Spectrum и немного поднастроил частоту строчной развёртки. Изображения было так себе и часть терялось, но играть было можно в Command & Concuer.


  1. insecto
    29.11.2021 22:04
    +2

    Это невыносимо круто, пожалуйста, не останавливайтесь.


  1. gleb_l
    30.11.2021 01:27
    +5

    Если режим - только алфавитно-цифровой, то полезно уж иметь аппаратный курсор - процессор не то, чтобы очень сильный для программного мигания, да и прерываний в нем нет ;). А это сделать поверх уже спроектированного устройства относительно несложно.

    Но главное достоинство этого видеоконтроллера - его образовательное значение. Помните книгу «телевидение - это очень просто!», или здешние посты месье @haqreu - Ваш пост по элегантности рассуждений и построений можно отнести к жемчужинам Хабра. Для большинства людей видеоконтроллер - магическая коробочка - мало кто представляет, как в принципе строятся такие устройства.


    1. ynoxinul Автор
      30.11.2021 09:58
      +2

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


  1. forthuser
    04.12.2021 12:23
    +1

    А, не думали сделать переопределяемые символы знакогенератора?
    Как пример в Jupiter ACE.

    Видеопамять была отдельной и состояла из двух банков объёмом 1 Кб. Несмотря на то, что компьютер имел только 1 видеорежим — чёрно-белый текст в 24 строки по 32 символа, он мог отображать графику за счёт возможности перепрограммирования знакогенератора. Большинство из 128 доступных ASCII символов могли быть переопределены как произвольный точечный рисунок размером 8 на 8 пикселей.

    Jupiter ACE — бытовой компьютер, производившийся в 1980-е годы британской компанией Jupiter Cantab

    P.S. Online эмулятор с играми Jupiter ACE
    Game — Dark Star
    image hosted on sendpic.org


    1. ynoxinul Автор
      04.12.2021 13:11
      +2

      Я не хотел излишне усложнять мою первую видеокарту :) К тому же, если делать полностью переопределяемую таблицу без ПЗУ, шрифт будет отъедать большой кусок от драгоценной памяти для программы.


      1. forthuser
        04.12.2021 13:22

        Почему, если используется текстовый режим в отображение на память?

        Знакогенератор это же отдельная аппаратно формируемая часть для отображения текстовых данных.

        P.S. Предварительная загрузка знакогенератора может быть, например, по I2C, SPI последовательному каналу по старту программы например из SD карты.

        Обратный поток «сырых данных» с памяти к процессору, при выборе зараннее прямоугольной области окна изображения (какого то размера) — так можно тоже улучшить ресурсы по работе с памятью для процессора. (т.е. локальный управляемый кэш уже выведенных данных в видеокарточку и их подмешивания обратно например по SPI интерфейсу)


        1. ynoxinul Автор
          04.12.2021 13:40

          Если хранить шрифт на SD-карте, тогда да. Но я сделал видеокарту до того, как была возможность подключать карту памяти.


      1. drWhy
        04.12.2021 13:41

        «мою первую видеокарту :)»
        Под стекло и на стену !)