Всем привет! В этой публикации я расскажу про свой опыт создания небольшой вычислительной системы, проблемах связанных с этим и способах их решения, но обо всем по порядку. Меня зовут Евгений, я студент 4 курса МатМеха УрГУ(урфу). Примерно на первом курсе я понял, что меня завораживают старые компьютеры и старые технологии. Примерно тогда же я купил себе советский клон ZX Spectrum`а - Урал-8/64K. ZX Spectrum, для тех, кто в танке, это совершенно легендарный компьютер, его знают и любят все, кто даже далек от этой всей компьютерной темы. Об этом, так же, говорит и наличие современной разработки компьютерных игр для него. В общем, много чего можно рассказать о нем, но не будем сильно отвлекаться. Так вот, купил я себе такой, поигрался, попрограммировал, мне понравилась его простота, какое понимание он дает о своем устройстве тем, кто впервые с ним работает. Немного раньше  я делал небольшие проекты на Arduino и думал над каким то достаточно большим проектом, но сильно не хотел делать еще одну "умную поилку для собаки", которых в интернете наплодили достаточно, чтобы потерять всякий интерес к этому, не то, чтобы он до этого был большой. Я подумал, мне нравится Spectrum, так почему бы не сделать его эмулятор, софтверных эмуляторов полно, да и физических тоже хватает, но я решил посмотреть что есть и сделать свое. Я погуглил и нашел статью, которая и толкнула меня к первым шагам. Что мне не понравилось в уже готовых аналогичных проектах, чтобы просто их реализовать: Абсолютно нечитаемый код на 2000+ строк кода в одном файле с кучей комментариев - иногда не только пояснительных но и кода... Мощные платы - в моей задумке было использовать простенькую Arduino Nano, а точно не Mega! Много плат - опять же хотелось использовать всего одну простую плату и вместить на нее все необходимое. Необходимость обновления прошивки при появлении новых программ для устройства - убивает внутреннюю flash память устройства, нужно что то другое. Посмотрев на различные проекты и доступное оборудование, я решил сделать симулятор не ZX Spectrum, а полностью придуманного вычислителя - со своей системой инструкций, способом коммуникации с оборудованием, и языком ассемблера, соответственно! Приступим к описанию того, как строилась система. Но прежде рекомендую ознакомиться с инструкциями процессора - это основной репозиторий проекта, дальше информация оттуда будет упоминаться. Одну из проблем, которые я пытался решить - это износ внутренней flash памяти, но оперативной памяти мало для хранения программ написанных для нашего компьютера - её всего 2 кб! Так что я решил использовать SD карту. Изначально я думал хранить на карточке файловую систему и в ней файлы и открывать нужную программу, выгружая её в оперативку. Однако для такой схемы нужна библиотека SD, использование которой не оставит от этой самой оперативки ничего. Я подумал - если понадобится ФС, напишем ее поддержку уже на нашем коде, а не будем занимать память Arduino для этого. Вполне хорошей альтернативой для работы с картой послужила библиотека sd_raw, которая предоставляет доступ к любому байту на карте на чтение и запись туда сырых данных, т.е. просто набора байт. В качестве основного устройства вывода текста я взял черно-белый дисплей Nokia 5110 - точное попадание в ретро стиль, как мне кажется. В качестве клавиатуры, понятно, возьмем PS/2. Остались свободные пины - заткнем пьезо-пищалкой. Мой хороший знакомый нарисовал и напечатал мне корпус, как многие заметили по картинке выше, похожий на корпус Macintosh. Ок, поговорили как он выглядит, что внутри, теперь углубимся в систему команд... Она строилась таким образом, чтобы пользователь не писал кучу лишнего кода, чтобы проинициализировать какое то устройство, как делал я, чтобы завести мышку в x86, используя только запись в порты. Т.е. на манипуляции с устройствами выделены некоторые инструкции, такие как: gkey - получает сканкод нажатой клавиши, если ее не было, то 0. line x1, y1, x2, y2 - рисует линию по указанным координатам на экране. play label - играет музыку по смещению label в памяти. ... Как же будут устроены обработчики этих инструкций и их опкоды? Я решил этот вопрос очень просто - создал массив функций, теперь в основном цикле работы процессора байт, на который указывает указатель инструкции (IP) - будет восприниматься как номер функции в этом массиве. Если же у инструкции есть какие то аргументы, например операнды инструкции сложения add r0, r0, то соответствующая функция сама об этом знает и дочитывает их. Кстати говоря, поскольку регистров 16, то эту пару из инструкции выше можно упаковать в 1 байт, чем я и воспользовался, чтобы сократить код. С ISA разобрались, немного о регистрах, их 2 набора по 16 штук - для целых чисел и чисел с плавающей запятой, все 32-битные. Есть инструкции кастинга из одного типа в другой, при этом сами регистры при программировании ничем не отличаются, инструкция сама понимает из какого именно регистра читать/писать (хотя можно сделать псевдонимы, чтобы не путаться). Кстати о плавающих числах, тут нам можно работать с плавающими числами как с целыми в плане использования их в инструкциях, не нужно использовать регистровый стек и обращаться по ссылке в память для получения константы, как это есть в x87. Однако вводя эти числа в свою систему, я не знал как безболезненно преобразовывать их, т.е. без битового разбора числа в формате IEEE754. Оказывается такой способ есть и он очень прост, нам не потребуются никакие логические операции в огромном количестве, а только 2 строчки: Тут мы просто записали в некоторую область памяти целое число (4 байта), изначально смотрим на них именно так, потом после взятия указателя на эту область (целое число), производим каст его к указателю на вещественное число и разыменовываем. Вся основная работа происходит во второй строчке и читать ее нужно справа налево. В итоге в переменной y окажется число, байтовое представление которого в формате IEEE754 равно числу в переменной x. Дисплей - с ним особых проблем не возникало, открыл datasheet, написал библиотеку и все работает, возникла только одна проблема с тем, что необходимо сохранять содержимое буфера экрана (508 байт) в какой-то памяти для изменения отдельных пикселей. Хорошо бы можно было в оперативке, но места мало - примерно столько же ест массив инструкций и столько же библиотека sd_raw, в совокупности с прочими расходами на регистры и другие переменные и массивы. Это оставляет около 150-200 байт на локальные переменные, чего может оказаться мало. Но быстро стало ясно, что инструкции никто не будет менять (хотя идея интересная - налету подменять инструкции процессора), поэтому было решено перенести их во внутреннюю flash память, используя ключевое слово PROGMEM в Arduino IDE, которое позволяет сохранять константы любых типов в неизменяемую память, освобождая оперативную. Таким образом решилась проблема нехватки памяти для экрана. Клавиатура - тут интереснее то, что происходило в программном эмуляторе нашего девайса, но об этом позже. В остальном, я пока что с реальной клавиатурой разбираюсь - там нужно выбрать хорошую (для наших целей) таблицу сканкодов (их 3 в PS/2) и понять как легко транслировать их в символы ASCII, или хотя бы как это делает DOS. Карта памяти - в какой то момент мне пришло осознание, что даже, если у нас регистры 32-битные и карта не меньше 4 Гб, но мы все равно не можем адресовать больше 2 Гб - это связано с тем, какую плату картридера я установил в наш вычислитель - она не поддерживает карты большего объема. Это налагает ряд проблем - теперь мы можем использовать только карты на которых указан объем 2Гб, они всегда на самом деле меньше и даже так - они различаются по объему. Но так как раньше и IP и SP - специальные регистры были по умолчанию установлены в 0, то теперь для адекватного использования стека (который растет вниз) нужно знать верхнюю границу памяти. И очень кстати в библиотеке имелась функция для чтения заводской информации, а помимо производителя там было поле capacity (емкость). Вот именно в это значение мы и устанавливаем теперь SP перед началом основного цикла процессора. Пьезо-пищалка - здесь было совершено большое открытие для меня - что delay в Arduino не такой уж блокирующий, как нам все говорят. Остановимся поподробнее. Начну с того, какая задача стояла. У нас есть набор пар частот и задержек, мы хотим перебирать их и проигрывать на пищалке функцией tone, которая как раз принимает пин, частоту и задержку, пищит с нужными параметрами и отключается по прохождении задержки. Перебирать нужно не абы как, а когда исполнится инструкция play, которая раньше упоминалась. Она укажет, где лежат эти ноты и запустит проигрывание, но стандартная функция tone не умеет по циклу ходить и изымать частоты с задержками. Значит нужно как-то детектировать то, что текущая нота должна уже закончиться и пора бы включить следующую. Самым простым решением будет в основном цикле процессора следить за этим с помощью millis, которая возвращает время прошедшее с запуска контроллера в миллисекундах. Но самое простое - не самое эффективное, у нас инструкции не имеют фиксированного времени исполнения, та же delay может занимать достаточно длительное время, не давая переключить ноту. Дальнейшим решением для меня было - покопаться в исходниках функции tone и создать аналог, который принимает обработчик завершения тона, обычно это была функция, которая отключает таймер, но теперь мы ее подменяем и вместо отключения мы включаем новый тон. После того, как проигрывание завершается нужно позвать noTone, чтобы вызвать правильный обработчик и подменить его обратно. Вроде все хорошо, все работает! Однако не совсем... Я загрузил следующий код:  Оказалось, что проиграв одну ноту, он выключал проигрывание. Почему же... Обратите внимание на delay, если зайти в её исходники, то мы замечаем функцию, которая вызывается перед циклом задержки - yield, я мало чего про нее нашел, но, как я понял, это макрос, в который мы оборачиваем код и как-то можем параллельно его исполнять с основным кодом, если кто знает точно - поделитесь. Я сам попробовал решить эту проблему и у меня получилось! Покажу на примере. У нас есть такие строчки в функции noTone -  Первая строка тела записывает логический 0 в указанный выход, а вторая подменяет обратно обработчик. Что будет если их поменять? Все сломается, конечно. Вспомним, что у нас delay не совсем блокирует, а именно, код будет исполняться до "digitalWrite", перед чем заблокируется. Если строчки будут в другом порядке, то вот эта замена приведет к тому что у нас старый обработчик вернется и будет отключать таймер, на котором работает пищалка. Таким образом, экспериментально проверено, что delay блокирует код, если он изменяет состояние пинов, иначе он исполняется. Каким образом это происходит - на это у меня ответов пока что нет. Вот такие были пироги при работе с периферией нашего компьютера. Помимо его самого, для него я сделал транслятор ассемблера в машинные коды, чтобы писать начальные программы было удобнее - работает на Python. На нем начал реализовывать оболочку, которая умеет разбирать FAT32, ходить по ее директориям и исполнять файлы. Так же для удобства отладки был написан эмулятор девайса - он использует общий код с самим компом. Рассмотрим подробнее оболочку и эмулятор. Компьютер начинает исполнение инструкций с 0 байта. На карте памяти было решено создать файловую систему FAT32 и использовать ее резервные секторы для нашего загрузчика. Как правило, начало самого первого сектора содержит служебную информацию, но самые первые байты не критичны к изменению, а именно нам надо изменить первые 5 байт на инструкцию перехода к первому байту загрузчика. Ок, мы попали в загрузочную секцию, разобрали все байты, рассчитали константы в удобном виде. Сохранили это все, теперь, чтобы не пересчитывать это при каждом старте и не перезаписывать подменим еще раз инструкцию перехода на блок непосредственной загрузки. Можно видеть это в этом коде: Про FAT ничего не буду рассказывать, можно посмотреть какие там есть поля, как строятся файлы и про терминологию в этом видео: У нас в программе есть поле "текущая директория", она обозначает номер первого кластера директории. Таким образом если мы в какой то директории начали работу при выключении компьютера, то при включении мы в ней и начнем работу. Выбирать файл в директории можно клавишами стрелками вверх-вниз, исполнять по Enter, при этом если это директория, то она становится в качестве текущей. Кстати говоря, под исполнение нужно выделить место. Поскольку доступно максимум 2 Гб, то 1 нижний гигабайт мы отдадим файловой системе, а старшую область под исполнение. Поскольку экран у нас может вмещать 6 строк текста по 14 символов, то мы можем отображать имя и тип файла (файл или директория) в каждой строке, а между строками перемещаться, используя скользящее окно. Наконец, немного про эмулятор. Чтобы все работало так же как на реальном компе, понятно, что нужно реализовать соответствующим образом библиотеки работы с устройствами. Карта памяти эмулируется, как не сложно догадаться - файлом. Экран изначально был приложением на SFML, но это влияло на работу с клавиатурой, создавая две проблемы. Во-первых, нужно понимать, что SFML это библиотека для игр, поэтому там не получить никаких нормальных сканкодов, значит надо их получать откуда-то еще, но если фокус на окне SFML, то оно получает все события клавиатуры, поэтому приходится переключать фокус на консоль, но так чтобы и окно было видно. Во-вторых, SFML это графическая библиотека, т.е. в текстовой консоли (одной из тех, что доступны по ALT-CTRL-Fx) ее нельзя использовать для рисования чего-то на экране. Хотя хотелось бы перейти в текстовую консоль, поскольку есть способ получения совсем хороших сканкодов - прямо таких, какие приходят от клавиатуры. Это показывает утилита showkey, но работает она только в текстовой консоли, так как графика в Linux для обработки хоткеев всегда читает /dev/console. Суть этого метода - перейти в неканонический вид консоли, где мы будем получать сырые сканкоды. Я просто нашел исходники утилиты showkey и подправил их для своих нужд, заодно научился компоновать программу на С++ с функциями на С - это нужно поскольку showkey написана на С, а весь проект мы пишем на плюсах. Ну а для отрисовки экрана мы воспользуемся популярной библиотекой для консольной графики - ncurses. Чтобы все отображалось хорошо, нужно настроить консоль так, чтобы размер шрифта вместо 8x16 был 8x8 - красиво, как пиксели на экране. Вот, вроде бы все основное рассказал. Вообще говоря, там еще много всего можно рассказать, с чем сталкивался по пути, но это, скорее, просто занимательные мелочи, чем что то важное. Столько удачных костылей было вставлено - о некоторых рассказал, о других молчу...  Сейчас делаю сетевой интерфейс для передачи по встроенному UART - нашел способ без дополнительных проводов, только UART и диоды, собрать общую шину из таких компьютеров и передавать между ними данные. Так же думаю как сделать транслятор из, скажем, wav в описанный формат музыкальных файлов. Еще поглядываю в сторону нормального компилятора на основе LLVM, но пока только мысленно, потому что итак есть чем заняться с этим проектом. Спасибо за внимание, присоединяйтесь к проекту. Оставлю ссылки. Основной репозиторий, программная оболочка, транслятор ассемблера.