Решив использовать последние дни отпуска для приведения имеющихся у меня запасов электронных компонентов к некоторому подобию порядка я наткнулся на неизвестную михросхему SOT-23-6 с еле читаемой маркировкой.
Микроскоп и гугл помогли идентифицировать микосхему как Attiny10 с 1024 байтами FLASH и 32 байтами SRAM.
Увидев такие весьма ограниченные характеристики я уже хотел было убрать Attiny10 обратно в коробку но почувствовал настальгию. Дело в том, что свой путь в микроэлектронике я начинал именно с микроконтроллеров серии AVR. Мне захотелось сделать на базе микроконтроллера какой нибудь полноценный девайс. В наличии у меня так же оказался OLED LCD Display размером 128x64.
Таким образом идея тетриса и сформировалась окончательно.
Электронная часть:
Я решил подключить LCD дисплей по протоколу 3-SPI. Сам протокол отличается от обычного SPI тем, что значение D/C отвечающее за выбор передачи данных или комманд для дисплея, передается дополнительным нулевым битом в SPI транзакции. Тем самым позволяя не подключать D/C вывод.
Схема подключения дисплея получилась следующая:
После подключения дисплея у микроконтроллера остается всего один свободный вывод PB3, на который требуется подключить как минимум четыре кнопки. Решением в даной ситуации является использование PB3 как вход ADC для следующей схемы:
Наличие делителя обусловлено тем, что PB3 так же является сигналом reset и напряжение на нем при штатной работе не должно падать ниже 0.9V. Конечно, можно было отключить внешний ресет с помощью фьюза RSTDISBL, но в таком случае я бы потерял возможность перепрошивать микроконтроллер, который у меня к слову был всего один.
В итоге получилась следующая общая схема:
Питание осуществляется от крошечной lipo батарейки 3.7V, 180mAh подключаемой к разъему P1. Батарейка оказался единственным элементом который мне пришлось докупить для этого проекта и по заверениям продавца имеет встроенный контроль за over discharge. По этой причине на схеме отсутствует какой либо контроль за разрядкой источника питания. Так же на схеме нет отдельного разъема для программирование, не считая PLS штырька для RESET. Часть сигналов программирования (PB0 — TPIDATA, PB1 — TPICLK) подключается через разъем дисплея P2. Сам дисплей съемный и стыкуется через PLS.
Вот так выглядит плата после разводки:
Плата без проблем была перенесена на односторонний текстолит и на мой взгляд подход максимально упростить схему полностью себя оправдал.
После монтажа всех компонентов получилось следующее:
Программная часть
Учитывая ограниченный объем имеющихся ресурсов Attiny10 вся программная часть реализована на ассемблере без использования сторонних библиотек.
Первое что я сделал это получил значения ADC для нажатия кнопок. Для этого написал отдельную программу суть которой постоянно считывать значения ADC и выводитьэти значения на gpio в формате 8 битной посылки. Дабы не тратить время на прием данных я просто подключился осциллографом к нужным gpio и таким образом считал нужные значения ADC.
Для примера вот так выглядит посылка при нажатии кнопки UP:
Получив значения для нажатия кнопок я перешел к написанию основной логики тетриса, в которой можно выделить следующие ключевые моменты.
Работа с дисплеем
Все объекты отображаемые на дисплее хранятся в SRAM микроконтроллера и учитывая объем в 32 байта тут не обойтись без масштабирования. Поэтому минимальной единицей отображения на дисплее будет квадрат 8х8 пикселей в результате игровое поле получилось размером 8х16 квадратов. Поскольку дисплей монохромный то для хранения значения квадрата хватит одного бита. В итоге, все объекты отображаемые на дисплее укладываются в массив из 16 байт. Чтобы было понятней приведу пример:
0 — 00010000
1 — 00111000
2 — 00 000 000
……………...
15 — 00 000 000
uint8_t t_field[] = {0x10, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
Нулевой байт массива соответствует первой строчке игрового поля, 15 — последней. Вот как выглядит это на дисплее:
Сам дисплей работает в horizontal addressing mode, что позволяет легко обновлять изображение просто последовательно передавая данные, инкрементация страниц происходит в самом дисплее.
Отрисовка элементов
Чтобы максимально упростить задачу отрисовки каждый элемент я задаю 8 битным паттерном. Например для элемента
паттерны будут следующие:
Каждый внутренний квадрат содержит номер бита в паттерне. В центре находится точка вращения. Структура описывающая текущий элемент содержит следующие поля:
brick_x координата x
brick_y координата y
brick_rot положение относительно точки вращения
brick_type тип элемента
Плюс такого подхода в том что существенно сокращается объем кода так как все элементы обрабатываются одинаково.
Минус же заключается в том, что невозможно задать элемент составляющий прямую линию из 4 квадратов. Как показало тестирование это довольно существенно, возможно в будущем я добавлю этот элемент с помощью отдельной отрисовки, благо в микроконтроллере осталось место.
Псевдо рандом и некоторые детали реализации
Рандом я сделал на основе комбинировании событий от нажатия кнопок и free run счетчика изменяющегося по таймеру. Получилось не идеально но вполне играбельно.
После включения питания на дисплее отображается заставка с надписью PUSH, разница во времени от появления заставки до нажатия произвольной клавиши обеспечивает случайность в выборе начальной фигуры. Предусмотрен счетчик для подсчета результатов. Каждый раз когда происходит удаление заполненной линии, счетчик инкрементирует на единицу. Изначально я хотел выводить результат в последней строке в виде двоичного кода, но в итоге от этого отказался, выглядело странно и сбивало игрока с толку. Счетчику нашлось другое применение: пауза между смещениями элемента вниз изначально равна 0.7 секунд, по мере набора очков пауза меняется следующим образом:
счетчик: 0..15 пауза 0.7
счетчик:16..31 пауза 0.6
счетчик:32..63 пауза 0.5
счетчик: > 63 пауза 0.4
В результате по мере прогресса в игре скорость падения элементов ускоряется внося дополнительную сложность.
Исходники собирались в Atmel Studio 6. Вся прошивка занимает 898 байт.
Корпус и сборка
Для полноценного девайса нужен корпус и этот корпус стал наверное самой сложной частью проекта. Я перепробовал несколько вариантов, истратил метры филламента и в итоге удалось создать приемлемый вариант:
Для создания 3д модели корпуса я использовал Fusion 360, печатал на Flying Bear Ghost 6, материал PLA.
Процесс сборки
Все исходники и материалы к проекту лежат по ссылке
Посмотреть как все выглядит в процессе работы можно на видео:
UPD: Добавил элемент "прямая линия из 4х квадратов". Репозиторий обновил. Всем спасибо за отзывы.