Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Однажды, ко мне пришла бредовая идея приделать к Денди мышь вместо джойстика. Зачем? Для чего? Да просто так, по фану. Потому что такого еще ни у кого не видел. Формально, на данную идею меня подтолкнуло одно видео, на котором чел играл в Punisher. Конкретно с этой игрой я знаком мало, но, тем не менее, в подобного рода играх крутить прицелом с крестовины было всегда неудобно. Вкупе со спортивном интересом "а заработает ли?" и для того, чтобы "чисто поржать", решил-таки уделить немного свободного времени для спаривания обычной компьютерной мышки со старушкой Денди.
В качестве функционального клея для стыковки двух железяк выбрал Arduino Nano просто потому, что была под рукой. Плюс пятивольтовая логика всех компонентов намекала на простое подключение одного к другому безо всяких согласователей уровней и прочего обвеса. Мышь - самая дешевая с PS/2 интерфейсом, свежекупленная, специально для этого проекта. Собственно, и все. Несмотря на широкую распространенность PS/2 аксессуаров, найти рабочую библиотеку оказалось не простой задачей. Одни библиотеки были достаточно стары и не собирались под свежей ARDUINO SDK, другие собирались, но работали криво, либо не работали вообще. В конце концов, наиболее подходящая библиотека, все же, была найдена и вроде даже работала. Сознаюсь, смалодушничал. Можно было уделить чуть больше времени для изучения протокола мыши, но для одноразовой задачи делать этого совсем не хотелось. Далее осталась часть эмуляции нажатий геймпада приставки. Вот тут действительно, проще всего написать самому, тем более что ничего особенного там нет. Внутри джойстика Денди контроллера NES стоит микросхема сдвигового регистра.
Каждый бит входа привязан к кнопке контроллера. Проще говоря, она преобразовывает комбинацию одновременно нажатых кнопок в последовательность, которую уже читает консоль и реагирует на нее действием на экране. По сигналу LATCH (читай Reset), микросхема сбрасывает свой счетчик и ждет тактового сигнала CLOCK для опроса каждого бита входа. При каждом такте отдается состояние каждого следующего бита микросхемы, т.е. для опроса всех 8 кнопок контроллера требуется 8 тактов. А далее по кругу - снова сброс, регистрация текущих нажатий кнопок и 8 тактов опроса состояний. Но тут вот какой нюанс. CLOCK и состояния кнопок можно назвать инверсными сигналами. То есть, активный уровень такта — это когда он равен нулю, равно как и нажатие кнопки будет с уровнем 0.
Значит, от Arduino требуется ловить по фронту сигнал LATCH, далее, не дожидаясь тактового сигнала, сразу выставлять нулевой бит регистра, а далее после изменения CLOCK отдавать уже первый, второй ... седьмой биты. Вероятно, пара строк кода будет гораздо понятнее моего объяснения.
waitLatch();
for (int i = 0; i < 8; i++) {
if (dataPad & (1 << i))
writeLo();
else
writeHi();
waitClock(HIGH);
}
writeHi();
"Вот и все", наивно подумал я. Опрашиваем мышь, интерпретируем ее кнопки/направление перемещения в команды контроллера и отправляем. Проблемы начались при первых же тестах. Контроллер явно эмулировался неправильно. Были спонтанные "нажатия" тех кнопок, которые я не нажимал, пропуски команд и прочая чертовщина. Начав разбираться в чем же дело, к своему удивлению, выяснил, что ардуинка недостаточно расторопна - в момент опроса мыши она (естественно) забивает на опрос консоли. А когда не забивает - пропускает какое-то количество сигналов LATCH, но если даже и не пропускает, то спотыкается на тактовых сигналах CLOCK. С тактовыми сигналами ей приходилось сложнее всего. Импульсы (тактами их назвать сложно) настолько коротки, что
digitalWrite(HIGH);
digitalWrite(LOW);
выполняются гораздо дольше самого импульса и 5-7 биты либо не отправляются совсем, либо отправляются с запозданием, когда уже совсем не надо. Признаться, первый раз с таким столкнулся, раньше времени реакции штатных функций всегда хватало для всего. Но это не страшно, так как вместо digitalWrite() можно напрямую оперировать состояниями портов, что в десятки (!!!) раз быстрее. digitalRead() отправляется туда же, так как она тоже не отличается быстродействием.
void Gamepad::waitClock(int state) {
if (state) {
while (PIND & (1 << PIND2)) {};
} else {
while (!(PIND & (1 << PIND2))) {};
}
}
void Gamepad::waitLatch() {
while (!(PIND & (1 << PIND3))) {};
}
void Gamepad::writeLo() {
PORTD &= ~(1 << 4);
}
void Gamepad::writeHi() {
PORTD |= (1 << 4);
}
Запускаю проект… уже гораздо лучше. Лучше, но все равно плохо. Ложные нажатия почти ушли, а вот пропуски остались. Из самого очевидного, попробовал запихать повесить LATCH на внешнее прерывание и из него отправлять буфер нажатых кнопок. Не получилось. Консоль опрашивает контроллер с частотой 50 или 60Гц (в зависимости от региона консоли), но некоторые игры могут опрашивать сразу 2 раза, с чем прерывания уже не справляются. Через некоторое время я все же победил проблему и заставил ардуину работать как надо. Ну... почти как надо. Для поставленной задачи вполне достаточно. Пришлось на время отправки посылки полностью отключать прерывания, иначе ардруина может не вовремя задуматься и испортить посылку. Уверен, что есть более элегантное решение, но уже хотелось быстрее запустить.
waitLatch(); // Ждем сигнал сброса от NES
cli(); // Отключаем прерывания, чтобы ничего не мешало правильному формированию пакета
// Передаем каждый бит при изменении сигнала Clock
for (int i = 0; i < 8; i++) {
if (dataPad & (1 << i))
writeLo();
else
writeHi();
waitClock(HIGH);
}
writeHi();
sei(); // Включаем прерывания для опроса мыши
Управление мышью было назначено так: перемещение мыши интерпретируются как нажатие кнопок крестовины направлений. Левая и правая кнопки - кнопки В и А контроллера, а Start и Select повесил на прокрутку колеса.
Исходники проекта тут https://github.com/HotPixelChannel/Mouse-To-NES
А как все это работает на деле - велкам на ютуб https://youtu.be/-WQ3YLDiz-E