§ Добавим переменных

В предыдущей статье я рассказал, как создать дисплей, но этого всё равно недостаточно, поскольку, чтобы создать текстовый дисплей, нам будет необходима память, где это будет храниться, а также возможность с памятью взаимодействовать.
Для начала потребуются несколько новых глобальных переменных:
int             ms_prevtime;
unsigned char   RAM[1024*1024+65536-16];
Переменная ms_prevtime будет отвечать за сохранение предыдущего значения таймера, чтобы знать, когда обновить экран в следующий раз. Естественно, RAM содержит в себе 1 мб + HIMEM. Не знаю, насколько это важно, но сделать стоит.
Видеопамять для текстового режима располагается по адресу 0xB8000 и занимает примерно 4 килобайта, или ровно 4000 байт = 80x25, что равно 2000, и стоит учесть, что видеопамять организована так, что на 1 символ приходится 2 байта: младший байт это ASCII-код, а старший это атрибут цвета. Цвет состоит из 2 частей - младшая часть отвечает за "передний цвет" (бит 1 в знакогенераторе), а старший за бит 0 в знакогенераторе (фон).

§ Создадим таймеры

В самом начале программы надо будет очистить ms_prevtime = 0; хотя это не так обязательно, на самом деле, но для порядка.
Самое главное, это добавить следующий код в программу:
// Остановка на перерисовку и ожидание
ftime(&ms_clock);

// Вычисление разности времени
int time_curr = ms_clock.millitm;
int time_diff = time_curr - ms_prevtime;
if (time_diff < 0) time_diff += 1000;

// Если прошло 20 мс, выполнить инструкции, обновить экран
if (time_diff >= 20) {

    ms_prevtime = time_curr;
    // .. исполнение нескольких инструкции ..
    SDL_Flip(sdl_screen);
}
Он располагается после блока обработки событий SDL и перед SDL_Delay(1), который ограничивает безудержное потребление процессорных ресурсов.
Теперь разберу что тут происходит. Сначала фунция ftime получает значение времени в структуру ms_clock, после чего находим time_curr - текущее время в миллисекундах от 0 до 999. Далее рассчитывается разность time_diff, и она может быть отрицательная. Если так, добавляем 1000 к этой разности, чтобы выровнять. Эта разность получается когда предыдущее значение было например 900, а стало 10, то есть таймер повернулся назад. На самом деле прошло 110 мс, но как это понять? Надо просто представить, что 10 это на самом деле 1010 и вычесть 900, получим нужную разницу. Вот и всё.
Далее, если разница 20 или более, то прошло 20 миллисекунд. Почему я выбрал 20? Потому что 20*50=1000, то есть 50 - это 50 раз обновление в секунду. На самом деле можно выбрать и 10 мс и 5, это не сильно играет роли, но мне просто так удобнее, потому что это похоже на кадровую развертку.
Если прошло 20 мс, то устанавливаем ms_prevtime равным time_curr, чтобы начать отсчет еще 20 мс, выполняем блок инструкции, например 20 тыс инструкции (там будет зависеть уже от реализации), и в конце делаем SDL_Flip, чтобы показать полученный видеокадр.
Если сделать менее 20 мс, то нагрузка из-за постоянного Flip вырастет, если же сделать более 20 мс, то скорость обновления экрана будет медленной. Так что 20 мс это вполне оптимальное число.

§ Запись в видеопамять

На самом деле мы сделаем запись не только в видеопамять
// Реальная запись в память
void wb(int address, unsigned char value) {

    RAM[address] = value;

    // Записываемый байт находится в видеопамяти
    if (address >= 0xB8000 && address < 0xB8FA0) {

        address = (address - 0xB8000) >> 1;
        int col = address % 80;
        int row = address / 80;
        address = 0xB8000 + 160*row + 2*col;
        print_char(col, row, RAM[address], RAM[address + 1]);
    }
}
Рассмотрим код. Этот код для записи в память, который просто записывает байт (от 0 до 255) в RAM по определенному адресу. Но если же адрес памяти будет [0xB8000 .. 0xB8FA0], то в этом случае запись будет происходит также и в видеопамять SDL.
Для начала нормализуем address = (address - 0xB8000) >> 1, чтобы рассчитывать запрошенный col и row. Как мы помним, col будет 0 как в ячейке 0, так и в ячейке 1, поэтому и нужен сдвиг вправо (деление на 2), далее col и row рассчитываются очень просто, через деление с остатком.
Следующим шагом будет вычисление нового видеоадреса, который будет указывать на ascii, а потом на атрибут. Дело в том, что запись в байт будет не только в ascii область, но еще и в область атрибутов, а поскольку обновлять при этом нужно и символ и атрибут, я и сделал такого рода нормализацию.
Ну и соответственно, будет вызвана процедура print_char, которая нарисует на экране символ.

§ Общие процедуры чтения и записи в память

Вообще не лишним будет добавить общую процедуру записи и чтения byte | word в память:
// Запись значения в память
void wr(int address, unsigned int value, unsigned char wsize) {

    if (wsize == 1) {
        wb(address, value);
    } if (wsize == 2) {
        wb(address,   value);
        wb(address+1, value>>8);
    }
}

// Чтение из памяти
unsigned int rd(int address, unsigned char wsize) {

    if (wsize == 1) return RAM[address];
    if (wsize == 2) return RAM[address] + 256*RAM[address+1];

    return 0;
}
В этом коде будет зависеть от выбранного wsize, если он 1, то будет записан байт, если 2, то слово (word).
Исходный код доступен по ссылке.
Следующая статья "Декодирование опкода инструкции"
27 сен, 2020
© 2007-2023 Разрешил психичный Мармок