Однажды, ко мне пришла бредовая идея приделать к Денди мышь вместо джойстика. Зачем? Для чего? Да просто так, по фану. Потому что такого еще ни у кого не видел. Формально, на данную идею меня подтолкнуло одно видео, на котором чел играл в Punisher. Конкретно с этой игрой я знаком мало, но, тем не менее, в подобного рода играх крутить прицелом с крестовины было всегда неудобно. Вкупе со спортивном интересом "а заработает ли?" и для того, чтобы "чисто поржать", решил-таки уделить немного свободного времени для спаривания обычной компьютерной мышки со старушкой Денди.

В качестве функционального клея для стыковки двух железяк выбрал Arduino Nano просто потому, что была под рукой. Плюс пятивольтовая логика всех компонентов намекала на простое подключение одного к другому безо всяких согласователей уровней и прочего обвеса. Мышь - самая дешевая с PS/2 интерфейсом, свежекупленная, специально для этого проекта. Собственно, и все. Несмотря на широкую распространенность PS/2 аксессуаров, найти рабочую библиотеку оказалось не простой задачей. Одни библиотеки были достаточно стары и не собирались под свежей ARDUINO SDK, другие собирались, но работали криво, либо не работали вообще. В конце концов, наиболее подходящая библиотека, все же, была найдена и вроде даже работала. Сознаюсь, смалодушничал. Можно было уделить чуть больше времени для изучения протокола мыши, но для одноразовой задачи делать этого совсем не хотелось. Далее осталась часть эмуляции нажатий геймпада приставки. Вот тут действительно, проще всего написать самому, тем более что ничего особенного там нет. Внутри джойстика Денди контроллера NES стоит микросхема сдвигового регистра.

Схема джойстика обыкновенного (данные на Q7 задом наперед, очепятка)
Схема джойстика обыкновенного (данные на Q7 задом наперед, очепятка)

Каждый бит входа привязан к кнопке контроллера. Проще говоря, она преобразовывает комбинацию одновременно нажатых кнопок в последовательность, которую уже читает консоль и реагирует на нее действием на экране. По сигналу 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

Комментарии (9)


  1. Javian
    07.10.2022 12:47
    +1

    Судя по движениям кисти руки на видео - долго играть не получится.


    1. HotPixel Автор
      07.10.2022 12:52
      +4

      в платформеры и экшены - однозначно нет)


  1. dlinyj
    07.10.2022 12:50
    +8

    Очень хабратортно! Прям снимаю шляпу, я бы попробовал.


  1. pehat
    07.10.2022 13:44
    +1

    Так вот чем надо было играть в Elite под NES!


  1. FGV
    07.10.2022 16:59
    +2

    По сигналу LATCH (читай Reset), микросхема сбрасывает свой счетчик и ждет тактового сигнала CLOCK для опроса каждого бита входа. При каждом такте отдается состояние каждого следующего бита микросхемы, т.е. для опроса всех 8 кнопок контроллера требуется 8 тактов. А далее по кругу - снова сброс, регистрация текущих нажатий кнопок и 8 тактов опроса состояний. Но тут вот какой нюанс. CLOCK и состояния кнопок можно назвать инверсными сигналами. То есть, активный уровень такта — это когда он равен нулю, равно как и нажатие кнопки будет с уровнем 0.

    Забавное описание. В даташите на cd4021 немного другое написано. Счетчиков никаких нет, есть цепь Д-триггеров соединенных последовательно выход данных->вход данных. По высокому уровню сигнала LATCH происходит асинхронная установка состояния внутренних триггеров-регистров в соответствии с уровнями на входах P1-P8. При низком уровне LATCH триггеры на изменение входных ног P1-P8 не реагируют.

    По фронту (переход из 0 в 1) тактового сигнала CLOCK происходит сдвиг (последовательная перезапись) состояний триггеров от 1 к 8. Ну и тактов для опроса требуется не 8 а 7, т.к. на выходе регистра после перехода LATCH в низкий уровень уже появляется состояние 8 триггера.


    1. HotPixel Автор
      08.10.2022 13:31
      +1

      технически - все верно, но на пальцах "даташитное" объяснение будет менее понятным. У меня, в свое время (лет 10 назад) были сложности с пониманием работы геймпада и вот такое сухое описание принципов работы микросхемы отбивало всякое желание копать дальше. Для людей, которые в программировании железа дальше Си не ходили, чаще всего регистры, триггеры, мультиплексоры и прочие логические термины вводят в ступор. Поэтому, если принять cd4021 как "черный ящик", то (наверно) такое упрощенное описание принципа работы будет лучше


  1. heaver
    07.10.2022 20:07

    Можно было бы аппаратный spi прикрутить, получилось бы проще и производительнее на порядки.


  1. FirstEgo
    07.10.2022 21:41
    +1

    А вообще вкусно! Можно даже развить идею, добавив запись нажатий в память и получить аппаратный летсплей - вставил этот джой и картридж в любую денди и смотри как он сам играет. Красота! А можно и прокачать, добавив диктофон и слушать как бомбил геймер)... Люблю такие проекты. Помнится, кто-то удлиннял к соседу выше этажом джойстик. А мощности передатчика китайской денди хватало на пол-двора, так что хватало просто телека.


  1. Kyaru
    10.10.2022 09:50
    +1

    Помню как то мне один пацан хвастался, что ему родители настоящий компьютер купили. Я обрадовался, ибо у меня в селе только я и еще один знакомый чел имели компы, и тут появился еще один, с кем можно дисками меняться. И тут он мне говорит, что в него вставляются дискеты, как на денди.... Я очень удивился, говорю "Пошли к тебе, покажешь", в итоге это оказался один из китайских клонов NES, исполненный в виде клавиатуры и мыши. Пробовал даже играть во что то мышью, один в один, как у автора в видео получалось, не удобно и странно)