Собрав свою отладочную плату с FPGA, мне хотелось создать на ее базе что-то довольно простое и полезное. Я имел необходимость в неком информационном табло для вывода отладочной информации.
Самым популярным интерфейсом до сих пор является последовательный типа UART или SPI, его мы и будем использовать. Тогда куда выводить информацию? Конечно же VGA! Что может быть проще для ПЛИС?
Так как это именно текстовый терминал, его главная характеристика — количество символов на экране. Кроме того, он должен обладать функцией автопрокрутки, очистки и минимальной ASCII таблицей символов.
Контроллер VGA
Я использовал готовый модуль с сайта Marsohod, да и изобретать его нет смысла, лишь модифицировать. Версия на языке Verilog:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
module hvsync ( // inputs: input wire char_clock, // outputs: output reg [11:0]char_count_, //post reg output reg [11:0]line_count_, //post reg output reg hsync, output reg vsync, output reg frame ); //VGA Standart `define h_visible_area 640 `define h_front_porch 16 `define h_sync_pulse 96 `define h_back_porch 48 `define v_visible_area 400 `define v_front_porch 12 `define v_sync_pulse 2 `define v_back_porch 35 //variables reg [11:0]char_count; reg [1:0]pixel_state; reg [11:0]line_count; reg [1:0]line_state; reg end_of_line; reg end_of_frame; //permanent comb computations: always @* begin //horizontal processing if(char_count < `h_visible_area) pixel_state = 0; //active video else if(char_count < `h_visible_area + `h_front_porch) pixel_state = 1; //front porch else if(char_count < `h_visible_area + `h_front_porch + `h_sync_pulse) pixel_state = 2; //hsync impuls else pixel_state = 3; //back porch if(char_count < `h_visible_area + `h_front_porch + `h_sync_pulse + `h_back_porch) end_of_line = 0; else end_of_line = 1; //vert processing if(line_count < `v_visible_area) line_state = 0; //active video lines else if(line_count < `v_visible_area + `v_front_porch) line_state = 1; //front porch else if(line_count < `v_visible_area + `v_front_porch + `v_sync_pulse) line_state = 2; //vsync impuls else line_state = 3; //front porch if(line_count < `v_visible_area + `v_front_porch + `v_sync_pulse + `v_back_porch) end_of_frame = 0; else end_of_frame = 1; end //synchronous process always @(posedge char_clock) begin hsync <= (pixel_state==2'b10); vsync <= (line_state!=2'b10); frame <= (pixel_state==2'b0 && line_state==2'b0); //char_count_ <= char_count; line_count_ <= line_count; if(end_of_line) begin char_count <= 0; char_count_ <= 0; if(end_of_frame) line_count <= 0; else line_count <= line_count + 1'b1; end else begin char_count <= char_count + 1'b1; if (pixel_state==2'b0) char_count_ <= char_count_ + 1'b1; end end endmodule |
Все, что от него требуется: синхроимпульсы и счетчик пикселей и строк, причем счетчик пикселей должен считать только видимую часть. Формат изображения задан следующий: 640×400@70Гц. Вы можете сделать больше, но на Cyclone I может не хватить памяти для символов.
Знакогенератор
Это самый важный модуль терминала, к нему также будет необходимо ОЗУ с символами и ПЗУ со шрифтом. Я предпочел стандартный размер шрифта 8×16 пикселей, в таком случае нам будет доступно 80×25 символов на экран.
Фактически, это самая обыкновенная тайловая графическая система. Создавать некую функцию для прорисовки отдельного тайла мы не сможем, мы вынуждены рисовать вместе со сканирующем лучом монитора, непосредственно при выводе.
Aсимвол = (Xэкран div 8) + (Yэкран div 16) * 80
Xшрифт = Xэкран % 8
Yшрифт = Yэкран % 16
Для удобства лучше представить массив со шрифтом в виде одного столба, вместо матрицы. Естественно, каждый пиксель будет занимать 1 бит.
Остается прибавить к координатам шрифта смещение на = 8*16*[символ] и записать в линейном виде. Для скроллинга используем дополнительную переменную, которая будет суммироваться с адресом (Aсимвол) символа в памяти.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
`define h_symbols 80 `define v_symbols 25 module chars ( input wire [11:0] char_count, input wire [11:0] line_count, input wire [8:0] char, //Symbol code input wire [10:0]scroll_line, //Scrolling output wire [14:0] font_addr, //Address of font bitmap output wire [10:0] addr, //Address of symbol output wire redink //Ink ); assign addr = ((char_count)>>3) + (((line_count>>4)+scroll_line)*`h_symbols); assign font_addr = ((char_count%8) + (line_count%16)*8) + ((char[7:0])<<7); assign redink = ~char[8]; //Red/White ink endmodule |
Можете заметить, что знакогенератор полностью асинхронный. Я не вижу весомых причин делать его регистровым, частоты там все равно очень низкие. Кстати, я использовал специальный атрибут в коде символа — еще один цвет для разнообразия (8 + 1 бит).
Печатный станок
Кто должен помещать символы в память, переводить строку, прокручивать? — Машина состояний.
Станок всегда ожидает команд от приемника, как только приходят данные, он декодирует их (если это спец.символ) и проводит соответствующие операции с памятью символов.
Пояснения лишь требует один странный пункт «Регенерация» — это заполнение пустотой всей следующей строки при вводе символа, чтобы при прокрутке старые данные не появлялись снизу.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
`define h_symbols 80 `define v_symbols 25 module machine ( input wire clock, input wire [7:0]in, input wire get, output reg [10:0]address, output reg wr, output reg [8:0]data, output reg [10:0]scroll_line ); parameter idle = 0; parameter decode = 1; parameter changeink = 2; parameter backspace = 3; parameter write = 4; parameter nextline = 5; parameter flush = 6; parameter inc = 7; parameter nop = 8; parameter refresh = 9; //variables reg [7:0]buffer; reg redink = 0; reg fullscreen = 0; reg [19:0]addrchar; //FixME reg [7:0]count; reg [3:0]state; reg ext; //synchronous process always @(negedge clock) begin case (state) idle: if (get) state <= decode; decode: case(buffer) 0: //Null state <= idle; 26: //Pause/Break state <= changeink; 4: //Clear state <= flush; 127: //Backspace state <= backspace; 13: //Enter state <= nextline; default: state <= write; endcase refresh: if (count > 79) state <= idle; changeink: state <= idle; write: state <= inc; inc: state <= refresh; nop: state <= idle; nextline: if (!(addrchar%`h_symbols) && ext) state <= refresh; backspace: state <= nop; flush: if (addrchar == 0 && ext) state <= nop; default: state <= idle; endcase end always @(posedge clock) begin case(state) idle: begin wr <= 0; buffer <= in; ext <= 0; if (count > 0) begin count <= 0; addrchar <= addrchar - `h_symbols; end end changeink: redink <= ~redink; write: begin data[7:0] <= buffer; data[8] <= redink; wr <= 1; end inc: begin addrchar <= addrchar + 1; if (addrchar>(`h_symbols*`v_symbols)-3) fullscreen <= 1; if (fullscreen && !((addrchar+1)%`h_symbols)) scroll_line <= scroll_line + 1; wr <= 0; end nextline: begin data <= 0; if (wr) begin addrchar <= addrchar + 1; ext <= 1; end if (addrchar>(`h_symbols*`v_symbols)-3) fullscreen <= 1; if (fullscreen && !((addrchar+1)%`h_symbols)) scroll_line <= scroll_line + 1; wr <= 1; end backspace: begin data <= 0; addrchar <= addrchar - 1; if (!(addrchar%`h_symbols) && fullscreen) scroll_line <= scroll_line - 1; wr <= 1; end refresh: begin data <= 0; addrchar <= addrchar + 1; count <= count + 1; wr <= 1; end flush: begin data <= 0; if (wr) begin addrchar <= addrchar - 1; ext <= 1; scroll_line <= 0; fullscreen <= 0; end else begin addrchar <= 11'b11111111111; end wr <= 1; end endcase end always @(posedge clock) begin address <= addrchar; end endmodule |
Машина работает на половинной частоте синхрогенератора.
Приемник
В данном случае — это UART, вы можете использовать SPI или что-угодно другое. Его код я взял отсюда. Можете заметить, что FIFO здесь никакого нету. Частота передачи — 115200 бод, поэтому буферы здесь ни к чему.
Демонстрация
Текст вводится с компьютера через преобразователь.
Некоторые спец.команды для терминала:
- 0x00 — пустой символ
- 0x13 — перевод строки
- 0x127 — стереть предыдущий символ
- 0x04 — очистка экрана
- 0x26 — смена чернил
Проект в Altera Quartus./
PS: В проекте есть небольшая ошибка: терминал может кончиться Все дело в переменной addrchar, она должна циклически обнуляться. В скором времени я это исправлю.