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