§ Организация памяти

В прошлой статье я разобрал, как сделать простой видеоадаптер на верилоге, который показывает серый экран, а в этой хочу рассказать, как сделать видеоадаптер, который показывает текст из видеопамяти.
Первым делом, надо понять, как хранится текст в памяти и откуда что берется. Как и всегда, я возьму самое простое видеоразрешение 640x400. Буквы будут моноширинными, по 8 пикселей по ширине и 16 пикселей по высоте. Один символ (знакоместо) - это один байт.
В одной строке умещается 640/8 = 80 знакомест. Всего 25 строк (25*16 = 400). Это значит, что в первой строке будут представлены знакоместа от 0 до 79, во второй строке от 80 по 159 и т.д.
Конечно же, теперь необходимо подумать, где хранить данные для того, чтобы их выводить на экране. Место хранения таких данных называется видеобуфер, для него потребуется 80*25=2000 символов. В ПЛИС-е обычно есть специальные блоки блочной памяти размером по 1Кб каждый. Для того, чтобы выделить место в блочной памяти, нужно 2кб, или 2048 байт, при этом 48 байт останутся неиспользуемыми.
Хорошо, видеобуфер у нас есть, хранить есть где, а как же тогда с самими начертаниями символов? Знакоместа имеют размер 8x16, один пиксель равен одному биту (0 - точки нет, 1 - точка есть), то есть, для глифы на самом деле, будут состоять из всего лишь двух цветов - из цвета символа и фона. Цвет символа и фона может задаваться в отдельных областях памяти, но я не буду усложнять и сделаем для простоты черно-белые буквы.
Итак, 8 пикселей по ширине равен ровно 1 байту, а это значит, что 1 символ будет занимать в памяти 16 байт, потому что высота символа 16 пикселей, и это далее значит, что для хранения 256 глифов потребуется 256*16=4096 байт памяти, а это ровно 4Кб. Можно хранить 128 символов, что займет у знакогенератора 2Кб. Пожалуй, сделаю так.
Номер знакоместа на экране в видео области будет находиться в пределах от 0 до 127. Но, поскольку мы храним 7 бит для каждого знакоместе в видеобуфере, то куда деть свободный 8-й бит? Сделаем так, что он будет означать инверсию цветов, то есть, если значение символа равно 128, то тогда из знакогенератора будет выводится на самом деле символ 0, но вместо фона будет серый цвет.
Итого — 2Кб на видеобуфер, 2Кб на знакогенератор, выходит, в целом 4Кб видеопамяти требуется для организации вывода текста на экране.

§ Получение данных из знакогенератора

Итак, что необходимо? К предыдущему видеоадаптеру, помимо r,g,b,hs,vs выходов теперь будет нужны выходы, которые будут устанавливать адреса в видеопамять и забирать оттуда данные.
Я сделаю так, что не потребуется обращаться к двум адресам одновременно, поскольку в этом нет нужды. Поскольку всего памяти 4Кб, то используется 12 бит для ее адресации. Младшие 2Кб отведены под видеобуфер и там будут храниться знакоместа, а старшие 2Кб отведены под знакогенератор:
1module vga
2(
3  input clock,
4  output r, output g, output b,
5  output hs, output vs,
6  output reg [11:0] address,
7  input      [ 7:0] data
8);
9
10...
11endmodule
Как видно, я применил конструкцию output reg [11:0] address, потому что в верилоге можно на выход сразу назначить регистр, минуя определение его в коде. И при записи в этот регистр будет сразу же появляться на выходе. Если что, входящий регистр делать нельзя.
Итак, как сделать, чтобы знакоместа выводились на экран? Для этого надо всегда знать, в какой сейчас позиции находится луч. Из предыдущей статьи такая позиция известна, и описывается через регистры-счетчики (x,y).
Также известно, что видимая область начинается тогда, когда заканчивается задний порожек. Для горизонтальной развертки он составлял 48 пикселей от начала, а для вертикальной - 35 пикселей.
Однако, не все так просто, как может показаться. Рассмотрю вот что:
  • y=35,x=40, устанавливаем address=0, чтобы прочитать из видеобуфера очередной символ (первый символ)
  • y=35,x=41, считываем из памяти во временный регистр (например tchar) тот символ, что находится в памяти по адресу 0, здесь же устанавливаем новый адрес, который равен 2048 + 16*tchar[6:0] + (y-35)[3:0]. Обращаю внимание, что берется [6:0], только младшие 7 бит, поскольку старший бит потребуется для инверсии цвета. Также, 2048 - это адрес начала знакогенератора. Поскольку количество байт на 1 символ равно 16 байт, то умножается на 16 (это 4 сдвига влево). Что значит (y-35)[3:0] ? Это номер строки в знакогенераторе по y, от 0 до 15. Здесь y-35 вычитается для того, чтобы начинать отсчет с 0, а не с 35, что даст на экране неприятный сдвиг.
  • y=35,x=42...46, не делаем ничего
  • y=35,x=47, читаем из памяти значение из знакогенератора и записываем его в регистр (например rmask, 8 битный), а также записываем бит цвета из 8-го бита tchar, к примеру, в регистр rcolor.
Придя к x=48, у нас уже будут подготовлены rmask и rcolor, которые будут потом использоваться для вывода на экран. Получается некий конвейер — пока происходит вывод на экран заранее подготовленного значения маски из знакогенератора, в это время будет читаться следующий символ и данные для него.
Чтобы получить позицию, относительно которой будет высчитываться начало видимой области, можно вычесть задний порожек:
1wire [9:0] xv = x - 40; // Не 48, а 40, из-за конвейерного метода
2wire [8:0] yv = y - 35; // А здесь 35
Для xv я вычел 40, а не 48, что на 8 пикселей меньше, и это не случайно, поскольку подготовка данных из знакогенератора идет за 8 пикселей до того, как они будут выведены, поэтому 40.
Заведем регистры:
1reg [7:0] rmask;   // Маска из знакогенератора
2reg       tcolor;  // Здесь хранится временное значение для цвета
3reg       rcolor;  // Цвет из символа (из tchar)

§ Обработчик

И вот теперь самое главное, это обработка данных:
1always @(posedge clock) begin
2
3  case (xv[2:0])
4
5  3'h0: begin address <= xv[9:3] + yv[8:4]*80; end
6  3'h1: begin address <= {1'b1, data[6:0], yv[3:0]}; tcolor <= data[7]; end
7  3'h7: begin rmask   <= data; rcolor <= tcolor; end
8
9  endcase
10
11end
Пожалуй, придется вызывать пояснительную бригаду, чтобы понять, что тут происходит.
Первое. Объявляется блок always @(posedge clock), который значит, что все что находится в begin .. end, будет выполнено на позитивном фронте clock, который равен 25 Мгц.
Второе. case (xv[2:0]) выбирает один из 8 вариантов, по которым формируется извлечение данных из видеопамяти. То есть, в течении 8 пикселей из памяти выбираются нужные данные и в самом конце (на 7-м такте) записываются в регистры, откуда уже и будут показываться в следующих 8 пикселях. Поэтому выборка происходит заранее!
На 0-м пикселе (самый левый): в адрес памяти записывается номер символа, который формируется из (xv/8) - это номер столбца, о будет от 0 до 79, и прибавляется номер (yv/16), умноженный на 80. Тем самым образом, происходит формирование адреса в видеопамяти, который указывает на символ в видеобуфере. Номер символа (еще называют знакоместо) от 0 до 1999.
На 1-м пикселе выбирается номер из знакогенератора. Он формируется так:
  • Берется строка от 0 до 15 из Y
  • Складывается с номером символа (от 0 до 127), умноженного на 16. Поскольку тут сдвиг на 4, то это умножение на 16
  • Добавляется +2048 — здесь 1 сдвинут на 11, что дает 2048.
Итого, получается 12-битный адрес, который запрашивает из знакогенератора нужную маску.
С 2 по 6 пиксели ничего не происходит, потому что ничего более не требуется.
На 7-м пикселе записываются данные из tchar в rcolor, а также значение из data в rmask. Если эту запись сделать в любом другом месте, то в итоге данные будут выводиться еще до того, как они должны будут, и получится "деление" на две части символа, так что надо делать только так, чтобы получился правильный результат.

§ Вывод на экран

Остались последние штрихи:
1wire pix = rmask[ ~xv[2:0] ] ^ rcolor;
2assign {r,g,b} = visible & pix ? 3'b111 : 3'b000;
Провод pix получает значение из мультиплексора rmask (который равен 8 бит). Номер текущего рисуемого символа задается как ~xv[2:0], который равен тому же самому, что и 7 - xv[2:0], то значит, что будет выдаваться от 7 по 0 бит из rmask. Когда луч будет находится слева, то будет показан 7-й бит из rmask, а если справа - то 0-й бит.
Если задан rcolor, то полученный бит из маски инвертируется.
В конце assign принимает значение 3'b111 тогда, когда луч находится в видимой области и рисуется точка в данный момент, иначе будет 0.
Вот, пожалуй, и все, что я могу сказать. Осталось привести готовый код, синтезировать его на верилоге и проверить. Но не все так просто, конечно же. Синтезировать надо также и блоки памяти, которые задаются для каждого чипа по-своему. К тому же, эти блоки памяти надо чем-то инициализировать, для этого должна быть таблица знакогенератора, которая у меня обычно хранится в mif-файлах.

§ Полный код

А теперь что уж тянуть котов за хвост, вот и полный код, объединенный с видеоадаптером из прошлой статьи
1module vga
2(
3  input clock,
4  output r, output g, output b,
5  output hs, output vs,
6  output reg [11:0] address,
7  input      [ 7:0] data
8);
9
10reg [9:0] x = 10'b0;
11reg [8:0] y = 9'b0;
12
13wire xborder = x == 10'd799;
14wire yborder = y ==  9'd448;
15wire visible = x >= 48 && x < 48+640 && y >= 35 && y <= 35+400;
16
17// Горизонтальная и вертикальная синхронизация
18assign hs = x  < 48+640+16;
19assign vs = y >= 35+400+12;
20
21// Выравнивание
22wire [9:0] xv = x - 40; // Не 48, а 40, из-за конвейерного метода
23wire [8:0] yv = y - 35; // А здесь 35
24
25// Временные регистры
26reg [7:0] rmask;   // Маска из знакогенератора
27reg       tcolor;  // Здесь хранится временное значение цвета
28reg       rcolor;  // Цвет из символа (из tchar)
29
30// Итоговый пиксель
31wire      pix = rmask[ ~xv[2:0] ] ^ rcolor;
32
33// Вывод белого экрана
34assign {r, g, b} = visible & pix ? 3'b111 : 3'b000;
35
36always @(posedge clock) begin
37
38    x <= xborder ? 1'b0 : x + 1'b1;
39    y <= xborder && yborder ? 1'b0 : (xborder ? y + 1'b1 : y);
40
41    case (xv[2:0])
42
43    3'h0: begin address <= xv[9:3] + yv[8:4]*80; end
44    3'h1: begin address <= {1'b1, data[6:0], yv[3:0]}; tcolor <= data[7]; end
45    3'h7: begin rmask   <= data; rcolor <= tcolor; end
46
47    endcase
48
49end
50
51endmodule
Как подключается этот модуль из DE0:
1// Видеоадаптер
2vga VGAUnit
3(
4    .clock      (clock_25),
5    .r          (VGA_R[3]),
6    .g          (VGA_G[3]),
7    .b          (VGA_B[3]),
8    .hs         (VGA_HS),
9    .vs         (VGA_VS),
10    .address    (address),
11    .data       (data)
12);
13
14// Модуль памяти на 4Кб
15wire [11:0] address;
16wire [ 7:0] data;
17
18font UnitFont
19(
20    .clock     (CLOCK_50),
21    .address_a (address),
22    .q_a       (data),
23);
Здесь можно скачать исходные коды и вообще, проект для DE0 Cyclone 3.