Реализация текстового терминала на системе на кристалле MIPSfpga.
Все таки пришло время штурмовать MIPSfpga. С чего начать? Ну традиционно с Hello World’а. По сложившемуся мнению, для FPGA и МК такую роль выполняют проекты, мигающие светодиодами, что не очень эпично. Поэтому научимся выводить строчку Hello MIPSfpga на экран монитора c VGA-интерфейсом. Начнем.
Что такое MIPSfpga?
MIPSfpga – это проект, который содержит исходный код процессорного ядра на Verilog’е. Исходный код полностью открыт для модификации, поэтому ядро можно переписывать на свое усмотрение и добавлять новые модули. Делать все это мы будем на отладочной плате Terasic DE0-CV. А что собственно будем делать? Сделаем простенький текстовый GPU, упрощённая схема которого приведена на рисунке:
Общение с внешними модулями периферии у 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. Где хранятся и откуда извлекаются символы.
Модуль для управления пишем сами ручками. Логика его работы должна быть следующей:
Входной выходной интерфейс модуля будет иметь следующие сигналы:
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;
}
В итоге получаем надпись на экране монитора: