§ Создание 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);
Так выглядит скриншот программы:

А также код можно скачать по этой ссылке.
Следующая статья "Доработка текстового дисплея".