Мини-аркада на ATtiny85: Snake — ручная OLED-консоль своими руками
Компоненты и материалы
| Компонент | Количество |
|---|---|
| ATtiny85 (Microchip) | 1 |
| Графический OLED-дисплей 128x64 (I2C) | 1 |
| Плата разъема micro USB (SparkFun microB USB Breakout) | 1 |
| Arduino Nano R3 (для программирования ATtiny85) | 1 |
Инструменты
- Паяльник
- 3D-принтер
Приложения и сервисы
- Autodesk Fusion 360 (моделирование корпуса)
- Arduino IDE
О проекте
История и вдохновение
В 2017 г. я делал портативную консоль Pong на Arduino Nano и OLED. Она была громоздкой. Теперь цель — крошечная консоль с игрой Snake на ATtiny85.
Выбор компонентов
- ATtiny85 — миниатюрный, достаточно I/O: 2 АЦП, I2C, GPIO. Программируется через Arduino Nano (как ISP).
- 2-осевой джойстик с кнопкой — 3 пина сигналов.
- OLED 128x64 (DFRobot) — компактный, высокое разрешение, I2C.
Пайка схемы
ATtiny85 на переходнике SOP-8 → DIP-8 на макетной плате. Два разъема: для программирования и для экрана. Джойстик и micro USB breakout подпаяны к периферии.
Проектирование корпуса
Спроектирован в Fusion 360: компоненты размещены, вокруг построен корпус в стиле аркадного автомата 80-х. Файлы STL в разделе CAD.
Программирование Snake
- Макс. длина змейки: 30 сегментов (экономия RAM). Победа при 29 съеденных точках.
- Позиции сегментов хранятся в двумерном массиве.
- При движении головы позиции каскадно сдвигаются вниз.
- Еда появляется в случайной точке.
- Проверка столкновений: совпадение координат головы с любым сегментом или выход за границы экрана (0-127 по X, 0-63 по Y).
Игровой процесс
Включаем, ждем загрузки еды и первого сегмента. Управляем джойстиком. Игра увлекательна и убивает скуку в миниатюрном формате.
Код (ATtiny85, C/C++)
Требуется библиотека U8g2lib (установите через Library Manager).
//#include <TinyWireM.h>
#include <U8g2lib.h>
U8G2_SSD1306_128X64_NONAME_1_SW_I2C u8g(U8G2_R0, /* clock=*/ 2, /* data=*/ 0, /* reset=*/ U8X8_PIN_NONE);
#define MAX_LENGTH 30
#define X 0
#define Y 1
#define JOYSTICK_X 2
#define JOYSTICK_Y 3
#define DIR_THRESH 300
uint8_t segmentPositions[MAX_LENGTH][2];
uint8_t headPosition[2] = {63, 31};
uint8_t foodPosition[2];
uint8_t tempPosition0[2], tempPosition1[2];
int segmentLength = 1;
void gameUpdate();
enum DIRECTIONS { RIGHT, DOWN, LEFT, UP } currentDirection;
void setup() {
//TinyWireM.begin();
u8g.begin();
u8g.setPowerSave(0);
pinMode(JOYSTICK_X, INPUT);
pinMode(JOYSTICK_Y, INPUT);
randomSeed(analogRead(0));
beginGame();
}
void loop() {
u8g.firstPage();
do { gameUpdate(); u8g.setColorIndex(1); } while(u8g.nextPage());
}
void beginGame() {
currentDirection = RIGHT;
spawnFood();
delay(1000);
}
bool checkCollisions() {
for(int i=1; i<segmentLength; i++) {
if(segmentPositions[i][X] == headPosition[X] && segmentPositions[i][Y] == headPosition[Y]) return 1;
}
if(headPosition[X] <= 0 || headPosition[X] >= 128) return 1;
if(headPosition[Y] <= 0 || headPosition[Y] >= 64) return 1;
return false;
}
void spawnFood() {
foodPosition[X] = random(5, 123);
foodPosition[Y] = random(5, 60);
}
void checkFoodEaten() {
if(headPosition[X] == foodPosition[X] || headPosition[Y] == foodPosition[Y]) {
segmentLength += 1;
spawnFood();
}
}
void updateDirection() {
int joy_x_val = analogRead(JOYSTICK_X);
int joy_y_val = analogRead(JOYSTICK_Y);
if(joy_x_val <= DIR_THRESH) currentDirection = LEFT;
else if(joy_x_val >= 1023-DIR_THRESH) currentDirection = RIGHT;
else if(joy_y_val <= DIR_THRESH) currentDirection = UP;
else if(joy_y_val >= 1023-DIR_THRESH) currentDirection = DOWN;
}
void displaySegments() {
for(int segment=0; segment<segmentLength; segment++)
u8g.drawPixel(segmentPositions[segment][X], segmentPositions[segment][Y]);
u8g.drawPixel(foodPosition[X], foodPosition[Y]);
}
void gameUpdate() {
updateDirection();
tempPosition0[X] = headPosition[X]; tempPosition0[Y] = headPosition[Y];
switch(currentDirection) {
case UP: headPosition[Y] -= 1; break;
case DOWN: headPosition[Y] += 1; break;
case LEFT: headPosition[X] -= 1; break;
case RIGHT: headPosition[X] += 1; break;
}
for(int segment=1; segment<segmentLength; segment+=2) {
tempPosition1[X] = segmentPositions[segment][X];
tempPosition1[Y] = segmentPositions[segment][Y];
segmentPositions[segment][X] = tempPosition0[X];
segmentPositions[segment][Y] = tempPosition0[Y];
tempPosition0[X] = segmentPositions[segment+1][X];
tempPosition0[Y] = segmentPositions[segment+1][Y];
segmentPositions[segment+1][X] = tempPosition1[X];
segmentPositions[segment+1][Y] = tempPosition1[Y];
}
displaySegments();
checkFoodEaten();
if(checkCollisions()) endGame();
if(segmentLength >= MAX_LENGTH) endGame();
delay(50);
}
void endGame() {
segmentLength = 1;
headPosition[0] = 63; headPosition[1] = 31;
beginGame();
}
3D-модели
Файлы STL доступны в репозитории проекта.
Схемы

Производственный процесс
- TinyML-детектор языка на Arduino Nano 33 BLE Sense с Edge Impulse
- Создание игры в реальном времени на Arduino Nano с гироскопом MPU-6050 и OLED-дисплеем
- Цифровые кости на Arduino: генератор случайных чисел с LCD-дисплеем и кнопкой
- Создание рулетки на 37 светодиодов с Arduino Nano
- Портативный дальномер на Arduino Nano: HC-SR04, питание 18650, 3D-корпус
- Сборка аудиореактивной настольной лампы на Arduino Nano
- Медное гальваническое покрытие: умная система на Arduino UNO
- NeoPixel Matrix Pong на Arduino Nano: неоновая игра Pong на матрице 15×10
- Создание потрясающих световых последовательностей с Arduino и сдвиговым регистром
- Умная розетка 120В на Arduino с часами реального времени