§ Создание makefile
Захотел вот написать статью про разработку на языке Си некоего эмулятора 8086 процессора, но начать я решил прямо с текстового видеоадаптера, ну и также самой заготовки под эмулятор. В общем-то, хочу научиться писать такие статьи.Для начала, необходимо создать директорию (в Линуксе), где будем делать код, и туда поместить два файла - это makefile и t8086.c - тот самый файл, где будет происходить вся логика. Писать эмулятор я собираюсь без классов, на чистом Си. Содержимое makefile будет следующим:
1OPTS_ALL=-O3 -fsigned-char -std=c99 2OPTS_SDL=`sdl-config --cflags --libs` 3 4t8086: t8086.c 5 ${CC} t8086.c ${OPTS_SDL} ${OPTS_ALL} -o t8086 6 strip t8086 7 8clean: 9 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
Начнем с самого простого, а именно с шаблона кода:1#include <time.h> 2#include <sys/timeb.h> 3#include <memory.h> 4#include <unistd.h> 5#include <fcntl.h> 6#include "SDL.h" 7 8SDL_Surface* sdl_screen; 9SDL_Event sdl_event; 10struct timeb ms_clock; 11 12int main() { 13 14 SDL_Init(SDL_INIT_VIDEO); 15 sdl_screen = SDL_SetVideoMode(2*640, 2*400, 32, SDL_HWSURFACE | SDL_DOUBLEBUF); 16 SDL_EnableUNICODE(1); 17 SDL_EnableKeyRepeat(500, 30); 18 SDL_WM_SetCaption("Эмулятор 8086", 0); 19 20 // ... Здесь будет общий цикл ... 21 22 SDL_Quit(); 23 return 0; 24}В этом шаблоне мы подключаем функции времени 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 должен запустить окно, то он его запускает, но при этом сразу же выходит. То есть окно на миллисекунду появляется и сразу же исчезает. Это плохо, потому что надо его вывести и ждать от пользователя каких-то событий. Например нажатие на "выход". А иначе тогда смысла нет во всём этом.Сверху был код, и там было написано "... Здесь будет общий цикл ...", так вот, я его добавляю:
1// Цикл 2char in_start = 1; 3while (in_start) { 4 5 // Проверить наличие нового события 6 while (SDL_PollEvent(& sdl_event)) { 7 8 switch (sdl_event.type) { 9 10 // Остановить программу 11 case SDL_QUIT: in_start = 0; break; 12 } 13 } 14 15 // ... код обработки ... 16 17 SDL_Delay(1); 18}Что тут происходит? В цикле 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 файл (файл находится в прикрепленном коде в конце статьи), туда и положим наши данные с помощью:1#include "font16.h"Необходимо создать также вспомогательную функцию pset, чтобы можно было рисовать точки:
1void pset(int x, int y, uint32_t color) { 2 3 if (x >= 0 && y >= 0 && x < 2*640 && y < 2*400) { 4 ( (Uint32*)sdl_screen->pixels )[ x + 2*640*y ] = color; 5 } 6}И теперь весь код выглядит примерно как-то так:
1#include <time.h> 2#include <sys/timeb.h> 3#include <memory.h> 4#include <unistd.h> 5#include <fcntl.h> 6#include "font16.h" 7#include "SDL.h" 8 9SDL_Surface * sdl_screen; 10SDL_Event sdl_event; 11struct timeb ms_clock; 12int width; 13int height; 14 15// Нарисовать точку на экране 16void pset(int x, int y, uint32_t color) { 17 18 if (x >= 0 && y >= 0 && x < width && y < height) { 19 ( (Uint32*)sdl_screen->pixels )[ x + width*y ] = color; 20 } 21} 22 23int main() { 24 25 // Инициализация окна 26 SDL_Init(SDL_INIT_VIDEO); 27 sdl_screen = SDL_SetVideoMode(width = 2*640, height = 2*400, 32, SDL_HWSURFACE | SDL_DOUBLEBUF); 28 SDL_EnableUNICODE(1); 29 SDL_EnableKeyRepeat(500, 30); 30 SDL_WM_SetCaption("Эмулятор 8086", 0); 31 32 // Цикл 33 char in_start = 1; 34 while (in_start) { 35 36 // Проверить наличие нового события 37 while (SDL_PollEvent(& sdl_event)) { 38 switch (sdl_event.type) { 39 case SDL_QUIT: in_start = 0; break; 40 } 41 } 42 43 SDL_Delay(1); 44 } 45 46 SDL_Quit(); 47 return 0; 48}Здесь на самом деле осталось добавить совсем немного, всего лишь вывод символа, а за это будет отвечать новая функция print_char:
1// Печать символа 2void print_char(int col, int row, unsigned char pchar, uint8_t attr) { 3 4 // Стандартная DOS-палитра 16 цветов 5 uint32_t colors[16] = { 6 0x000000, 0x0000cc, 0x00cc00, 0x00cccc, 7 0xcc0000, 0xcc00cc, 0xcccc00, 0xcccccc, 8 0x888888, 0x0000ff, 0x00ff00, 0x00ffff, 9 0xff0000, 0xff00ff, 0xffff00, 0xffffff, 10 }; 11 12 int x = col*8, y = row*16; 13 14 uint8_t fore = attr & 0x0F, // Передний цвет в битах 3:0 15 back = attr >> 4; // Задний цвет 7:4 16 17 // Перебрать 16 строк в 1 символе 18 for (int i = 0; i < 16; i++) { 19 20 unsigned char ch = font16[pchar][i]; 21 22 // Перебрать 8 бит в 1 байте 23 for (int j = 0; j < 8; j++) 24 for (int k = 0; k < 4; k++) 25 pset( 26 2*(x + j) + (k&1), 27 2*(y + i) + (k>>1), 28 colors[ ch & (1 << (7 - j)) ? fore : back ] 29 ); 30 } 31}Теперь разберемся, что тут происходит. Задается соответствие цветам colors в стандартной 16 цветной палитре DOS. Далее высчитывается позиция (x,y) по заданному (col,row) - столбцу и строке, где будет рисоваться символ. Потом высчитывается атрибуты. Старший ниббл атрибута отвечает за задний цвет т.е. там где в знакогенераторе будет бит 0, а младший ниббл (нижние 4 бита) отвечают за передний цвет, т.е. бит 1.
После начинается цикл отрисовки символа. Перебираются 16 линии, так как высота каждого символа это 16 линии, и 8 столбцов, ибо ширина символа 8 пикселей. Выбираются данные из массива font16, определенного ранее.
Потом происходит некоторая магия. Для начала разберу это:
12*(x + j) + (k&1) 22*(y + i) + (k>>1)Так как каждый пиксель удваивается, то k, принимая значения от 0 до 3, последовательно проходит по всем 4 точкам удвоенного пикселя. Вместо рисования 1 пикселя будут нарисованы 4.
1ch & (1 << (7 - j)) ? fore : backВот тут выбирается с помощью сдвига один из битов. Поскольку у меня корявый font16, биты приходится зеркально обернуть по горизонтали, и поэтому я использую 7-j. Сдвиг 1 << n дает возможность проверить каждый бит. Если запрошенный бит знакогенератора будет 1, то будет выдан цвет fore, иначе back.
Таким вот образом можно вызывать процедуру:
1print_char(0, 0, 'c', 0x17); 2SDL_Flip(sdl_screen);И будет нарисован символ "c" в позиции 0,0. SDL_Flip обязателен - он переворачивает нарисованный экран к пользователю и мы можем увидеть то что мы нарисовали.
Вот какой результат даст программа:
1for (int i = 0; i < 25; i++) 2for (int j = 0; j < 80; j++) 3 print_char(j, i, i*j+1, 0x17); 4 5SDL_Flip(sdl_screen);Так выглядит скриншот программы:
А также код можно скачать по этой ссылке.
Следующая статья "Доработка текстового дисплея".