§ Добавим переменных
В предыдущей статье я рассказал, как создать дисплей, но этого всё равно недостаточно, поскольку, чтобы создать текстовый дисплей, нам будет необходима память, где это будет храниться, а также возможность с памятью взаимодействовать.Для начала потребуются несколько новых глобальных переменных:
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).
Исходный код доступен по ссылке.
Следующая статья "Декодирование опкода инструкции"