Реализация текстового терминала на системе на кристалле MIPSfpga.

Все таки пришло время штурмовать MIPSfpga. С чего начать? Ну традиционно с Hello World’а. По сложившемуся мнению, для FPGA и МК такую роль выполняют проекты, мигающие светодиодами, что не очень эпично. Поэтому научимся выводить строчку Hello MIPSfpga на экран монитора c VGA-интерфейсом.  Начнем.

Что такое MIPSfpga?

MIPSfpga – это проект, который содержит исходный код процессорного ядра на Verilog’е.  Исходный код полностью открыт для модификации, поэтому ядро можно переписывать на свое усмотрение и добавлять новые модули. Делать все это мы будем на отладочной плате Terasic DE0-CV. А что собственно будем делать? Сделаем простенький текстовый GPU, упрощённая схема которого приведена на рисунке:

mipsfpga3

Общение с внешними модулями периферии у MIPS процессора осуществляется через шину AHB-Lite. Шина имеет следующие сигналы:

HCLK – тактовый сигнал;

HWRITE – сигнал разрешения записи;

HADDR – адрес для чтения/записи;

HWDATA – шина данных для записи;

Традиционно периферия, которая висит на шине AHB, для процессора представляется набором регистров с некоторым адресом. Записывая или читая из этих регистров, мы осуществляем работу с периферией.

Адреса регистров периферии прописываются в файле mfp_ahb_lite_matrix_config.vh.

Добавляем туда следующие значения:

`define MFP_SYMBOL_ADDR                                    32’h1f800014 – регистр для записи ASCII — кода отображаемого символа

`define MFP_POSITION_ADDR                                 32’h1f800018 – позиция для записи символа на экране

`define MFP_WE_ADDR                                              32’h1f80001C – регистр по положительному значению в котором производится запись позиции и кода символа.

`define MFP_SYMBOL_IONUM                                4’h5

`define MFP_POSITION_IONUM                             4’h6

`define MFP_WE_IONUM                                         4’h7

Добавили.

Сам проект MIPSfpga состоит из множества файлов. Но нам нужен только один mfp_ahb_gpio_slave.v. До него нам нужно «протянуть» от файла de0_cv.h нужные нам выходные ножки такие как:

output                            [11:0] VGA_RGB

output                             VGA_HS

output                             VGA_VS

input                               CLK_VGA

Помимо всего прочего, в этот файл нам предстоит добавить разрешающие сигналы на запись:

case (write_ionum)

`MFP_RED_LEDS_IONUM                     : IO_RedLEDs   <= HWDATA [17:0];

`MFP_GREEN_LEDS_IONUM               : IO_GreenLEDs <= HWDATA [ 8:0];

`MFP_SYMBOL_IONUM                          : IO_SYMBOL   <= HWDATA [15:0];

`MFP_POSITION_IONUM                       : IO_POSITION  <= HWDATA [13:0];

`MFP_WE_IONUM                                    : IO_WE              <= HWDATA [0];

endcase

 

Ну и напоследок добавляем непосредственно два модуля работы с периферией:

mfp_ahb_vga– модуль, который и осуществляет отрисовку на экране монитора.

mfp_ahb_memory_font – модуль, который будет буферизировать изображение на экране и хранить в ПЗУ графическое отображение символов

Модуль для VGA развертки взят из предыдущей статьи. И немного доработан, в соответствии с замечаниями, за которые я очень благодарен, очень надеюсь правильно. Разрешение выбрано 600*800.

Работа модуля простая, на вход ему подается синхрочистота и тот цвет пикселя. На выходе получаем импульсы, идущие на монитор(VGA_HS, VGA_VS, VGA_RGB) и три сигнала, которые идут к модулю mfp_ahb_memory_font. Они дают понять, какой пиксель в данный момент рисуется.

Второй блок занимается хранением шрифта и буферизует в памяти то, что сейчас рисуется на экране.

Для создания буферной памяти, мы используем стандартные ip-блоки ОЗУ(RAM), ПЗУ(ROM).

RAM – ОЗУ из ячеек, которым соответствуют позиции символов на экране, хранящих значения символа. Посчитаем количество позиций на экране, разрешение экрана у нас будет 600*800, размеры символа 16*8. После не хитрых математических расчетов получаем 37*100 символов на экране.

ROM – ПЗУ, конфигурируется из файла vgafont.mif. Где хранятся и откуда извлекаются символы.

Модуль для управления пишем сами ручками. Логика его работы должна быть следующей:

mipsfpga2

Входной выходной интерфейс модуля будет иметь следующие сигналы:

pixel_clk – тактовый сигнал

visible – сигнал, показывающий, находимся ли мы в видимой части   экрана

w_line_count_out – номер строки, которая рисуется на экране

w_pixel_count_out – номер пикселя, который сейчас рисуется на экране

rgb – цвет

adr_ram – адрес, который мы хотим прочитать в ОЗУ

q_ram – данные, которые мы читаем из ОЗУ

adr_rom – адрес, который мы читаем из ПЗУ, где хранятся символы

q_rom – данные, которые мы читаем из ПЗУ

 

 

Место в памяти, где хранится символ, соответствующий положению луча на экране, будем вычислять вот так:

adr_ram <= 100*((w_line_count_out-(3)*16)/16)+(w_pixel_count_out — 217)/8;

Получив код символа из ОЗУ(q_ram), лезем в ПЗУ искать нужный нам символ, смотрим, как он рисуется на экране:

Адрес в ПЗУ:

adr_rom <= 16*(q_ram) + w_line_count_out%16;

От  ПЗУ получаем 8 бит, показывающих, как нам рисовать на экране. Из 8 бит мы выбираем один нужный нам в этот момент времени, смотрим его значение. Если «1», то пиксель закрашивается черным, если «0», то белым. Выходная шина rgb принимает значение 12’b000000000000, либо 12’b111111111111.

Компилируем проект; Вроде бы все прошло хорошо. Приступаем к прошивке. В системе на кристалле уже встроен аппаратный bootloader, который позволяет прошивать процессор через uart.

Делать мы это будем через обычный дешевый переходник uart<-> usb, который необходимо подключить следующим образом (TX линию переходника вешаем на GPIO_1[31] на отладочной плате и объединяем земли):

(фото)

Дальше нам нужно написать текст программы, откомпилировать его, и залить через переходник на процессор.

Добавим название регистров и их адреса, через которые будем осуществлять управление периферией в файл mfp_memory_mapped_registers.h.

#define MFP_SYMBOL_ADDR                  0xBF800014 // регистр записи кода символа

#define MFP_POSITION_ADDR                0xBF800018 // регистр, в котором записана позиция, в которую вписывается символ

#define MFP_WE_ADDR                            0xBF80001C // регистр при положительном значении, в котором разрешается запись значений в память из регистра MFP_SYMBOL_ADDR и MFP_POSITION_ADDR.

#define MFP_SYMBOL                               (* (volatile unsigned *) MFP_SYMBOL_ADDR   )

#define MFP_POSITION                            (* (volatile unsigned*) MFP_POSITION_ADDR)

#define MFP_WE                                         (* (volatile unsigned *) MFP_WE_ADDR   )

Ну и собственно код программы, по старой традиции которую мы чуть-чуть нарушим, выведем на экран монитора фразу Hello MIPSfpga!:

 

#include «mfp_memory_mapped_registers.h»

int main ()

{

int start_Y = 0;

int start_X = 0;

long counter;

int hello_mipsfpga[18] = {0x48, 0x65, 0x6C, 0x6C, 0x6F, 0xFF, 0x4D, 0x69, 0x70, 0x73, 0x46, 0x50, 0x47, 0x41, 0xFF, 0x21, 0x21, 0x21};

for (;;)

{

for (counter=0; counter<=17; counter++){

MFP_POSITION = counter+1;

MFP_SYMBOL = hello_mipsfpga[counter];

MFP_WE = 1;

for(n=0; n<=10; n++);

MFP_WE = 0;

MFP_POSITION = 0;

MFP_SYMBOL = 0;

for(n=0; n<=10; n++);

}        }

return 0;

}

В итоге получаем надпись на экране монитора:

mipsfpga