§ Создание makefile
Захотел вот написать статью про разработку на языке Си некоего эмулятора 8086 процессора, но начать я решил прямо с текстового видеоадаптера, ну и также самой заготовки под эмулятор. В общем-то, хочу научиться писать такие статьи.Для начала, необходимо создать директорию (в Линуксе), где будем делать код, и туда поместить два файла - это makefile и t8086.c - тот самый файл, где будет происходить вся логика. Писать эмулятор я собираюсь без классов, на чистом Си. Содержимое makefile будет следующим:
OPTS_ALL=-O3 -fsigned-char -std=c99 OPTS_SDL=`sdl-config --cflags --libs` t8086: t8086.c ${CC} t8086.c ${OPTS_SDL} ${OPTS_ALL} -o t8086 strip t8086 clean: rm t8086Здесь разберу подробнее. Параметр -O3 говорит о том, что будет использоваться оптимизация третьего уровня (-O3). Параметр -fsigned-char указывает на то, что если написать
char
, то будет использован именно signed char
в программе. Параметр -std=c99 показывает, что программа будет использовать соглашения о стандартах кодирования c99. И наконец в параметре OPTS_SDL указан интересный вызов ПРОГРАММЫ sdl-config, да, программы, которая вставляет в параметры что-то вроде этого:-I/usr/include/SDL -D_GNU_SOURCE=1 -D_REENTRANT -L/usr/lib/x86_64-linux-gnu -lSDLЗачем это нужно? Ну тут все просто: для того, чтобы компилятор знал, откуда брать файлы и какие библиотеки подключать нужно, чтобы можно было обратиться к функциям SDL.
§ Начинаем кодить t8086.c
Начнем с самого простого, а именно с шаблона кода:#include <time.h> #include <sys/timeb.h> #include <memory.h> #include <unistd.h> #include <fcntl.h> #include "SDL.h" SDL_Surface* sdl_screen; SDL_Event sdl_event; struct timeb ms_clock; int main() { SDL_Init(SDL_INIT_VIDEO); sdl_screen = SDL_SetVideoMode(2*640, 2*400, 32, SDL_HWSURFACE | SDL_DOUBLEBUF); SDL_EnableUNICODE(1); SDL_EnableKeyRepeat(500, 30); SDL_WM_SetCaption("Эмулятор 8086", 0); // ... Здесь будет общий цикл ... SDL_Quit(); return 0; }В этом шаблоне мы подключаем функции времени time.h и sys/timeb.h, чтобы точно знать системное время, потому что это очень важно и нужно для прерывания. Также пригодится memory.h - управление памятью, системная библиотека различных полезных констант unistd.h, управление файлами в библиотеке fcntl.h и конечно же SDL.h - сама библиотека (или фреймворк) SDL.
Глобально объявлены sdl_screen - окно, в котором будет текстовый видеобуфер (и не только текстовый), sdl_event - принимаемые события от этого окна, ms_clock - текущее время, в данном случае интересуют только миллисекунды. Это нужно чтобы точно знать, когда требуется обновить экран.
Здесь SDL_Init инициализирует возможность создания видеобуфера. Функция SDL_SetVideoMode создает окно размером 640 на 400 пикселей (удвоенного размера) с глубиной 32 пикселя (многовато, но на всякий случай), а также с опциями SDL_HWSURFACE (использовать аппаратный видеобуфер) и SDL_DOUBLEBUF (двойную буферизацию). SDL_WM_SetCaption, ясное дело, устанавливает заголовок окна. SDL_Quit завершает сессию работы с SDL.
Почему удвоенного? Потому что если сделать обычного размера, то эти пиксели потом не увидишь, будет слишком мелко.
SDL_EnableUNICODE разрешает использование юникода, а SDL_EnableKeyRepeat разрешает клавишам, при нажатии на них, совершать повтор, как в обычной нормальной клавиатуре.
§ Общий цикл обработки событий SDL
А теперь очень важная вещь. Поскольку SDL должен запустить окно, то он его запускает, но при этом сразу же выходит. То есть окно на миллисекунду появляется и сразу же исчезает. Это плохо, потому что надо его вывести и ждать от пользователя каких-то событий. Например нажатие на "выход". А иначе тогда смысла нет во всём этом.Сверху был код, и там было написано "... Здесь будет общий цикл ...", так вот, я его добавляю:
// Цикл char in_start = 1; while (in_start) { // Проверить наличие нового события while (SDL_PollEvent(& sdl_event)) { switch (sdl_event.type) { // Остановить программу case SDL_QUIT: in_start = 0; break; } } // ... код обработки ... SDL_Delay(1); }Что тут происходит? В цикле while (in_start) будет постоянно повторяется другой цикл - прием событий SDL_PollEvent, который передает по ссылке переменную sdl_event, и она заполняется, если очередное событие наступило. Если событие есть, то SDL_PollEvent вернет число, отличное от 0 и передаст управление далее, в глубину. Там как раз и поджидает switch, который будет распознавать тип события. В данный момент есть только SDL_QUIT, и ничего больше. Но и этого пока достаточно. Что делает SDL_QUIT? Если это событие появилось, то значит, что пользователь нажал на "закрыть окно": in_start становится 0 и программа завершается.
SDL_Delay(1) останавливает выполнение программы на 1 миллисекунду и это крайне важно, поскольку если никаких событий нет, то программа начинает жрать 100% ресурсов процессора. Эта конструкция не допускает такого кошмара.
§ Рендеринг шрифтов
Для начала нам потребуется ASCII таблица для C, которая находится у меня на сайте. Пусть это будет font16.h файл (файл находится в прикрепленном коде в конце статьи), туда и положим наши данные с помощью:#include "font16.h"Необходимо создать также вспомогательную функцию pset, чтобы можно было рисовать точки:
void pset(int x, int y, uint32_t color) { if (x >= 0 && y >= 0 && x < 2*640 && y < 2*400) { ( (Uint32*)sdl_screen->pixels )[ x + 2*640*y ] = color; } }И теперь весь код выглядит примерно как-то так:
#include <time.h> #include <sys/timeb.h> #include <memory.h> #include <unistd.h> #include <fcntl.h> #include "font16.h" #include "SDL.h" SDL_Surface * sdl_screen; SDL_Event sdl_event; struct timeb ms_clock; int width; int height; // Нарисовать точку на экране void pset(int x, int y, uint32_t color) { if (x >= 0 && y >= 0 && x < width && y < height) { ( (Uint32*)sdl_screen->pixels )[ x + width*y ] = color; } } int main() { // Инициализация окна SDL_Init(SDL_INIT_VIDEO); sdl_screen = SDL_SetVideoMode(width = 2*640, height = 2*400, 32, SDL_HWSURFACE | SDL_DOUBLEBUF); SDL_EnableUNICODE(1); SDL_EnableKeyRepeat(500, 30); SDL_WM_SetCaption("Эмулятор 8086", 0); // Цикл char in_start = 1; while (in_start) { // Проверить наличие нового события while (SDL_PollEvent(& sdl_event)) { switch (sdl_event.type) { case SDL_QUIT: in_start = 0; break; } } SDL_Delay(1); } SDL_Quit(); return 0; }Здесь на самом деле осталось добавить совсем немного, всего лишь вывод символа, а за это будет отвечать новая функция print_char:
// Печать символа void print_char(int col, int row, unsigned char pchar, uint8_t attr) { // Стандартная DOS-палитра 16 цветов uint32_t colors[16] = { 0x000000, 0x0000cc, 0x00cc00, 0x00cccc, 0xcc0000, 0xcc00cc, 0xcccc00, 0xcccccc, 0x888888, 0x0000ff, 0x00ff00, 0x00ffff, 0xff0000, 0xff00ff, 0xffff00, 0xffffff, }; int x = col*8, y = row*16; uint8_t fore = attr & 0x0F, // Передний цвет в битах 3:0 back = attr >> 4; // Задний цвет 7:4 // Перебрать 16 строк в 1 символе for (int i = 0; i < 16; i++) { unsigned char ch = font16[pchar][i]; // Перебрать 8 бит в 1 байте for (int j = 0; j < 8; j++) for (int k = 0; k < 4; k++) pset( 2*(x + j) + (k&1), 2*(y + i) + (k>>1), colors[ ch & (1 << (7 - j)) ? fore : back ] ); } }Теперь разберемся, что тут происходит. Задается соответствие цветам colors в стандартной 16 цветной палитре DOS. Далее высчитывается позиция (x,y) по заданному (col,row) - столбцу и строке, где будет рисоваться символ. Потом высчитывается атрибуты. Старший ниббл атрибута отвечает за задний цвет т.е. там где в знакогенераторе будет бит 0, а младший ниббл (нижние 4 бита) отвечают за передний цвет, т.е. бит 1.
После начинается цикл отрисовки символа. Перебираются 16 линии, так как высота каждого символа это 16 линии, и 8 столбцов, ибо ширина символа 8 пикселей. Выбираются данные из массива font16, определенного ранее.
Потом происходит некоторая магия. Для начала разберу это:
2*(x + j) + (k&1) 2*(y + i) + (k>>1)Так как каждый пиксель удваивается, то k, принимая значения от 0 до 3, последовательно проходит по всем 4 точкам удвоенного пикселя. Вместо рисования 1 пикселя будут нарисованы 4.
1 << (7 - j)) ? fore : backch & (Вот тут выбирается с помощью сдвига один из битов. Поскольку у меня корявый font16, биты приходится зеркально обернуть по горизонтали, и поэтому я использую 7-j. Сдвиг 1 << n дает возможность проверить каждый бит. Если запрошенный бит знакогенератора будет 1, то будет выдан цвет fore, иначе back.
Таким вот образом можно вызывать процедуру:
0, 0, 'c', 0x17); SDL_Flip(sdl_screen);print_char(И будет нарисован символ "c" в позиции 0,0. SDL_Flip обязателен - он переворачивает нарисованный экран к пользователю и мы можем увидеть то что мы нарисовали.
Вот какой результат даст программа:
for (int i = 0; i < 25; i++) for (int j = 0; j < 80; j++) print_char(j, i, i*j+1, 0x17); SDL_Flip(sdl_screen);Так выглядит скриншот программы:
А также код можно скачать по этой ссылке.
Следующая статья "Доработка текстового дисплея".