NES TDD

291. Захват состояния геймпада и последовательное чтение

Захват кнопок геймпада и их последовательное чтение.

Урок 291 из 356 · tests/chapter_07_controller_input/test_291_controller_capture_and_serial_read.py

Файлы, создаваемые на этом шаге

emulator/input/controller.py

Зачем нужен этот шаг

CPU NES не получает состояние геймпада как целый байт из $4016. Он получает по одному биту кнопки за раз. Прежде чем встраивать этот протокол в CpuBus, чистый объект Controller должен уметь:

Ссылки

https://www.nesdev.org/wiki/Standard_controller
https://www.nesdev.org/wiki/Controller_reading_code

1. capture the current button booleans into a stable snapshot
2. expose the captured bits in NES serial order
3. handle strobe high vs strobe low behavior

Ключевой термин: strobe Strobe — это управляющий сигнал, записываемый CPU. Для геймпада NES strobe определяет, продолжает ли геймпад захватывать текущее состояние кнопок или переходит к перебору захваченных последовательных битов.

Минимальный пример

controller.a = True
controller.write_strobe(1)
controller.write_strobe(0)
controller.read_bit()  # returns A bit

Распространённое заблуждение

"The controller should return all buttons as one byte."

Эмулятор может хранить захваченные кнопки внутри как один байт, но протокол со стороны CPU читает данные по одному биту за раз.

Пример предполагаемой реализации

def capture_buttons(self) -> None:
    value = 0

    if self.a:
        value |= BUTTON_A
    if self.b:
        value |= BUTTON_B
    if self.select:
        value |= BUTTON_SELECT
    if self.start:
        value |= BUTTON_START
    if self.up:
        value |= BUTTON_UP
    if self.down:
        value |= BUTTON_DOWN
    if self.left:
        value |= BUTTON_LEFT
    if self.right:
        value |= BUTTON_RIGHT

    self.captured_buttons = value
    self.read_index = 0


def write_strobe(self, value: int) -> None:
    self.strobe = (value & 1) == 1

    if self.strobe:
        self.capture_buttons()


def read_bit(self) -> int:
    if self.strobe:
        self.capture_buttons()

    if self.read_index >= 8:
        return 1

    bit = (self.captured_buttons >> self.read_index) & 1
    self.read_index += 1
    return bit

Вне рамок этого шага

  • маршрутизация $4016 в CpuBus
  • сопоставление клавиш pygame
  • порт геймпада 2
  • контроллеры расширения Famicom
  • поведение сбоя чтения DMC/геймпада

Запустить этот урок

uv run pytest tests/chapter_07_controller_input/test_291_controller_capture_and_serial_read.py -v