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

В предыдущей статье я рассказал, как создать дисплей, но этого всё равно недостаточно, поскольку, чтобы создать текстовый дисплей, нам будет необходима память, где это будет храниться, а также возможность с памятью взаимодействовать.
Для начала потребуются несколько новых глобальных переменных:
1int             ms_prevtime;
2unsigned 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; хотя это не так обязательно, на самом деле, но для порядка.
Самое главное, это добавить следующий код в программу:
1// Остановка на перерисовку и ожидание
2ftime(&ms_clock);
3
4// Вычисление разности времени
5int time_curr = ms_clock.millitm;
6int time_diff = time_curr - ms_prevtime;
7if (time_diff < 0) time_diff += 1000;
8
9// Если прошло 20 мс, выполнить инструкции, обновить экран
10if (time_diff >= 20) {
11
12    ms_prevtime = time_curr;
13    // .. исполнение нескольких инструкции ..
14    SDL_Flip(sdl_screen);
15}
Он располагается после блока обработки событий 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 мс это вполне оптимальное число.

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

На самом деле мы сделаем запись не только в видеопамять
1// Реальная запись в память
2void wb(int address, unsigned char value) {
3
4    RAM[address] = value;
5
6    // Записываемый байт находится в видеопамяти
7    if (address >= 0xB8000 && address < 0xB8FA0) {
8
9        address = (address - 0xB8000) >> 1;
10        int col = address % 80;
11        int row = address / 80;
12        address = 0xB8000 + 160*row + 2*col;
13        print_char(col, row, RAM[address], RAM[address + 1]);
14    }
15}
Рассмотрим код. Этот код для записи в память, который просто записывает байт (от 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 в память:
1// Запись значения в память
2void wr(int address, unsigned int value, unsigned char wsize) {
3
4    if (wsize == 1) {
5        wb(address, value);
6    } if (wsize == 2) {
7        wb(address,   value);
8        wb(address+1, value>>8);
9    }
10}
11
12// Чтение из памяти
13unsigned int rd(int address, unsigned char wsize) {
14
15    if (wsize == 1) return RAM[address];
16    if (wsize == 2) return RAM[address] + 256*RAM[address+1];
17
18    return 0;
19}
В этом коде будет зависеть от выбранного wsize, если он 1, то будет записан байт, если 2, то слово (word).
Исходный код доступен по ссылке.
Следующая статья "Декодирование опкода инструкции"