§ Создание 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.
ch & (1 << (7 - j)) ? fore : back
Вот тут выбирается с помощью сдвига один из битов. Поскольку у меня корявый font16, биты приходится зеркально обернуть по горизонтали, и поэтому я использую 7-j. Сдвиг 1 << n дает возможность проверить каждый бит. Если запрошенный бит знакогенератора будет 1, то будет выдан цвет fore, иначе back.
Таким вот образом можно вызывать процедуру:
print_char(0, 0, 'c', 0x17);
SDL_Flip(sdl_screen);
И будет нарисован символ "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);
Так выглядит скриншот программы:

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