C2 Устройства и ввод-вывод
Вы уже реализовали TRM в NEMU и NPC. Следующий шаг, разумеется, — добавить поддержку ввода и вывода. Однако перед этим давайте сначала получим практический опыт разработки GUI-приложений.
Программы с поддержкой GUI-ввода и вывода
Ранее вы смогли заставить процессор minirv запустить программу, отображающую изображение в Logisim. Может ли эта программа также работать в minirvEMU? Кроме того, разработанные вами ранее программы на C могли выполнять ввод и вывод только через терминал. Как же разрабатываются привычные нам GUI-программы? Чтобы получить базовое понимание, давайте сначала попробуем запустить игры NES.
Запустите игры NES
Прочитайте начало раздела «Прежде чем начать увлекательное путешествие по PA -> Что такое NEMU?» (на английском "Before starting the pleasant PA journey -> What is NEMU?") из руководства PA1, следуя инструкциям руководства, запустите игры NES и выполните проверки отображения, клавиатурного ввода и звука.
Вам, должно быть, интересно, как на самом деле работают игры NES. На самом деле сама игра — это тоже последовательность инструкций, но эти инструкции принадлежат набору команд 6502. Этот набор команд появился в 1980-х годах и сейчас практически не используется, соответствующие процессоры тоже встречаются редко. Но только что вы запускали игру не на настоящем процессоре 6502. Так как же игра запустилась? На самом деле сначала была запущена программа-эмулятор набора команд 6502 под названием fceux. Эта программа обладает особым поведением: она способна программно эмулировать процесс выполнения инструкций 6502, тем самым эмулируя выполнение всей игры!
Знакомство с GUI-вводом и выводом
Пока что, однако, рассмотрим сначала содержимое, связанное с GUI-программами, а к эмуляторам вернёмся позже. Мы вводим среду выполнения под названием abstract machine, сокращённо AM. Она предоставляет ряд API (Application Programming Interfaces, программных интерфейсов) в виде библиотечных функций, используемых для реализации функций ввода-вывода, обычно применяемых в GUI. Упомянутая выше программа fceux реализует управление игрой и отображение, вызывая эти API. Но поведение fceux всё ещё слишком сложно для всех, поэтому сначала мы используем несколько простых примеров программ, чтобы познакомиться с функциями ввода-вывода, которые использует fceux.
Познакомьтесь с функцией часов
Запустите тестовую программу часов следующими командами:
cd am-kernels/tests/am-tests
make ARCH=native mainargs=t run
Вы обнаружите, что программа выводит одну строку в терминал каждую секунду. Кроме того, программа откроет новое полностью чёрное окно, но в текущей программе это окно не выполняет никаких функций, так что пока можно на него не обращать внимания.
Опробовав эту функцию, попробуйте прочитать am-kernels/tests/am-tests/src/tests/rtc.c, чтобы понять, как она реализована. Код io_read(AM_TIMER_UPTIME).us получает время, прошедшее с момента запуска программы, в микросекундах (us).
Познакомьтесь с функцией клавиатуры
Запустите тестовую программу клавиатуры следующими командами:
cd am-kernels/tests/am-tests
make ARCH=native mainargs=k run
Вы обнаружите, что программа откроет новое полностью чёрное окно. Нажимайте клавиши в этом новом окне, и вы увидите, что программа выводит в терминал соответствующую информацию о клавише, включая название клавиши, её код и состояние.
Опробовав эту функцию, попробуйте прочитать am-kernels/tests/am-tests/src/tests/keyboard.c, чтобы понять, как она реализована. Пока можно игнорировать код, связанный с uart. Код io_read(AM_INPUT_KEYBRD) получает событие клавиши ev; ev.keycode указывает код клавиши, а ev.keydown — нажата клавиша или отпущена. Значения кодов клавиш можно найти в abstract-machine/am/include/amdev.h. Все они используют префикс AM_KEY_, например AM_KEY_A для клавиши A. В частности, AM_KEY_NONE означает отсутствие события в клавиатуре.
Познакомьтесь с функцией отображения
Запустите тестовую программу отображения следующими командами:
cd am-kernels/tests/am-tests
make ARCH=native mainargs=v run
Вы обнаружите, что программа откроет новое окно и проиграет анимацию.
Опробовав эту функцию, попробуйте прочитать am-kernels/tests/am-tests/src/tests/video.c, чтобы понять, как она реализована. Код io_write(AM_GPU_FBDRAW, x * w, y * h, color_buf, w, h, false) означает отрисовку прямоугольного изображения размером w*h в экранных координатах (x * w, y * h). Пиксели изображения хранятся в color_buf построчно (row-major), и каждый пиксель описывается 32-битным числом в формате 00RRGGBB.
Разработка простого скринсейвера
Разобравшись, как использовать GUI-ввод и вывод в коде, далее мы реализуем простой скринсейвер, чтобы научиться разрабатывать простую GUI-программу на AM.
Сначала попробуйте сделать так, чтобы открывшееся окно отображало какой-нибудь цвет. Мы предоставляем следующий каркас кода:
#include <am.h>
#include <klib-macros.h>
void draw(uint32_t color) {
// добавьте код здесь
}
int main() {
ioe_init(); // инициализация для GUI
while (1) {
draw(0x000000ff);
}
return 0;
}
Чтобы использовать функциональность, предоставляемую AM, нам нужно написать Makefile, соответствующий спецификации AM, например:
NAME = screensaver
SRCS = screensaver.c
include $(AM_HOME)/Makefile
Если имя файла вашего C-кода не screensaver.c, измените Makefile выше в соответствии с фактическим именем файла. Затем скомпилируйте и запустите с помощью:
make ARCH=native run
Вы должны увидеть, как программа открывает новое полностью чёрное окно. Это потому, что вы ещё не реализовали корректно функцию void draw(uint32_t color). Более подробно про Makefile выше и про AM мы расскажем на этапе D. Пока вам достаточно знать, как использовать функциональность, предоставляемую AM, для разработки, компиляции и запуска программ.
Реализуйте отображение одного цвета
Реализуйте функцию draw() выше. Она должна заполнять открывшееся окно цветом, указанным аргументом color. Разрешение окна — 400x300.
После реализации снова скомпилируйте и запустите. Если реализация верна, вы должны увидеть, что открывшееся окно залито синим цветом.
Эффект, который нам нужно реализовать для скринсейвера, — это плавный переход экрана между несколькими цветами. Алгоритм градиента должен линейно интерполировать несколько цветов между текущим цветом и целевым и отображать их по порядку, тем самым достигая эффекта плавного перехода. Пусть текущий цвет c0 градиентом за n шагов переходит к целевому цвету c1, а значение каждой цветовой составляющей лежит в диапазоне [0, 255]. Тогда цвет, отображаемый на шаге i, можно вычислить, линейно интерполируя каждую из составляющих R, G и B между c0 и c1.
По завершении одного раунда градиента выбирается следующий целевой цвет для следующего раунда. Целевой цвет можно случайно выбирать из следующих цветов:
0x000000, 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff, 0x00ffff, 0xffffff
Для реализации эффекта градиента нам нужно лишь использовать код, связанный с часами, чтобы управлять интервалом между обновлениями цвета экрана.
Реализуйте эффект цветового градиента
Реализуйте эффект цветового градиента согласно описанию выше. Вы можете сами определить, сколько длится каждый раунд градиента и сколько шагов он содержит.
Наконец, используем функциональность клавиатуры. А именно, мы хотим реализовать следующее поведение клавиш:
- Если нажата клавиша
ESC(код клавишиAM_KEY_ESCAPE), выйти из программы. - Если нажата любая другая клавиша, ускорить текущий раунд градиента; после отпускания клавиши восстановить исходную длительность градиента.
Добавьте эффекты клавиатуры
Добавьте эффекты клавиатуры к скринсейверу согласно описанию выше.
Как скринсейвер защищает экран?
Раньше люди обычно использовали ЭЛТ-дисплеи. Принцип их работы заключается в том, что высокоскоростной электронный луч, испускаемый электронной пушкой, бомбардирует люминофорный слой экрана, заставляя его светиться и формируя изображение. Но если отображаемое изображение не меняется, электронный луч долгое время бомбардирует одно и то же место люминофорного слоя, что вызывает ускоренное старение этой части кинескопа или даже его пробой, повреждая экран. Скринсейвер непрерывно меняет изображение на экране, тем самым постоянно меняя положение и интенсивность бомбардировки электронным лучом и предотвращая повреждение экрана.
Однако сегодня люди чаще используют ЖК-дисплеи, принцип работы которых отличается от ЭЛТ-дисплеев. Старые скринсейверы больше не обеспечивают защиты. Требуемое выше упражнение - это просто простое упражнение, не учитывающее защитную функцию. На самом деле, если изображение на экране не меняется, это обычно означает, что пользователь долгое время не работал за компьютером. В этом случае лучшая практика - выключить дисплей.
После реализации скринсейвера у вас должно сложиться базовое понимание разработки GUI-программ. Однако мы ещё не объяснили подробно, как именно io_read() и io_write() реализуют ввод и вывод. Почему вызов io_read(AM_INPUT_KEYBRD) позволяет получить информацию о клавише? Почему вызов io_write(AM_GPU_FBDRAW, x, y, pixels, w, h, true) позволяет отрисовать пиксели на экране? В реальной компьютерной системе для этого требуется взаимодействие модулей на нескольких уровнях абстракции, включая среду выполнения, набор команд, процессор, шину, контроллер устройства и устройство ввода-вывода. По мере продвижения в обучении вы поймёте каждую деталь этого процесса на этапе B.
Разработка собственной мини-игры
Если вам интересно, вы также можете использовать API выше для разработки собственной мини-игры! Так называемая мини-игра по сути — это просто бесконечный цикл:
while (1) {
wait_for_a_new_frame();
process_user_keys();
update_game_logic();
draw_new_screen();
}
Разработанный вами скринсейвер по сути и есть такой бесконечный цикл, только его игровая логика очень проста и включает лишь обработку алгоритма градиента. Кроме того, am-kernels содержит несколько примеров мини-игр:
am-kernels/kernels/typing-game— игра на скорость печатиam-kernels/kernels/snake— «Змейка»
Разработайте собственную мини-игру
Попробуйте придумать и спроектировать мини-игру. С одной стороны, эта мини-игра в будущем сможет работать на процессоре, который вы сами разработаете. С другой стороны, мы давно собираем интересные и увлекательные мини-игры. Если разработанная вами игра окажется достаточно хороша, она может быть включена в коллекцию игр, которую мы поддерживаем для других студентов, и, возможно, даже будет продемонстрирована на реальных чипах.
Некоторые полезные ресурсы:
- Отображение символов:
am-kernels/kernels/typing-game/font.cпредоставляет битмапы символов. Пожалуйста, прочитайте соответствующий код, чтобы понять, как его использовать. - Отображение изображений: вы можете использовать команду
convert abc.jpg abc.txt, чтобы преобразовать изображение в текстовый файл, перечисляющий пиксели. Файл содержит по порядку цветовой код каждого пикселя. Этот текстовый файл можно далее обработать скриптом и преобразовать в массив на C.
Если вы планируете отправить разработанную вами игру нам, свяжитесь с ассистентом преподавателя (TA). Чтобы ваша игра могла работать в окружении других студентов, дополнительно убедитесь в следующем:
- Переносимость: не используйте числа с плавающей точкой в коде. Если требуются вычисления, связанные с дробными числами, масштабируйте их до целых чисел перед обработкой — например, для числа с 3 знаками после запятой умножьте его на 1000 перед обработкой. Если требуется вычисление элементарных функций, таких как
sin, используйте приближение через разложение в степенной ряд (power-series). Если требуется 64-битный целочисленный тип, используйтеuint64_tилиint64_t. Если требуется целочисленный тип для хранения указателей, используйтеuintptr_tилиintptr_t. - Играбельность: после входа в основную часть игры, если игрок ничего не делает в течение некоторого времени, игра должна завершаться поражением.
- Поддержка выхода: после нажатия
ESCкод должен вызыватьhalt(0)для выхода.
Реализуйте ввод-вывод в NEMU
Следуя конспекту лекций PA, завершите Фазу 3 PA2, пока не увидите следующее уведомление:
Friendly Reminder
На этом PA2 завершается.
Ввод-вывод в NPC
Для архитектуры RISC-V ввод-вывод реализуется через MMIO. Благодаря функциям чтения и записи памяти на основе DPI-C мы теперь можем реализовать ввод-вывод для NPC без изменения RTL-кода! Нам нужно лишь выполнить в этих двух функциях простую проверку диапазона адресов, чтобы перенаправлять запросы доступа к памяти от NPC к нужным устройствам. Аппаратно MMIO реализуется через шину, и позже мы реализуем настоящий MMIO.
Что касается устройств, здесь мы не используем напрямую модель устройств NEMU. Вместо этого мы реализуем для NPC в среде симуляции модель устройств, напоминающую будущую SoC для изготовления чипа. Как только мы реализуем шину, мы затем реализуем RTL-версии устройств на основе этой шины. Именно здесь становится очевидна роль абстракции IOE в abstract machine: адреса устройств и модели устройств у NEMU и NPC различаются, но после абстрагирования они оба могут запускать один и тот же исходный код эмулятора NES. Более того, всем программам на AM не требуется писаться по-разному для разных сред выполнения.
Запуск игры NES в NPC
Вы уже завершили NEMU с поддержкой RV32IM и периферии и успешно запустили игру NES в NEMU. Аналогично мы можем запустить игру NES в NPC с RV32E, но для этого сначала требуется реализовать последовательный порт (serial port) и часы.
Мы знаем, что процессоры RISC-V обращаются к периферии через MMIO. Например, в NEMU последовательный порт отображён на адрес 0xa00003f8. Аналогично мы можем реализовать простой последовательный порт и часы в среде симуляции NPC. Как упоминалось в предыдущем разделе, мы используем DPI-C, чтобы NPC вызывал функции чтения и записи pmem_read() и pmem_write() для доступа к памяти. Аналогично NEMU, мы можем добавить в эти две функции несколько проверок для реализации функциональности MMIO. Псевдокод показан ниже:
extern "C" int pmem_read(int raddr) {
// всегда читает 4 байта по адресу `raddr & ~0x3u` и возвращает их
if (raddr == clock_address) { return current_time; }
...
}
extern "C" void pmem_write(int waddr, int wdata, char wmask) {
// всегда записывает `wdata` в 4 байта, выровненных по адресу `waddr & ~0x3u`, в соответствии с маской записи `wmask`
// каждый бит в `wmask` действует как побайтовая маска для одного байта в `wdata`,
// например, `wmask = 0x3` означает, что записываются только два младших байта, а остальные байты в памяти остаются без изменений
if (waddr == uart_address) { putchar(...) }
...
}
Добавьте в NPC последовательный порт и часы
Реализуйте функциональность вывода через последовательный порт в среде симуляции NPC и запустите программу hello. Чтобы адрес последовательного порта совпадал с будущей SoC, вы можете задать адрес последовательного порта NPC равным 0x10000000.
Добавьте в NPC часы
Реализуйте часы в среде симуляции NPC и запустите тест часов реального времени из am-tests. Вы можете реализовать функциональность часов на основе системного времени. Какая библиотека C связана с системным временем и как получить системное время — оставляем вам на самостоятельный поиск (STFW).
Запустите символьную версию игры NES
Попробуйте запустить символьную версию игры NES на NPC. Обратите внимание, что среда симуляции NPC на данный момент не поддерживает клавиатурный ввод.
yyz
