Тетрис на базе Arduino и двухцветных светодиодных матриц

В этом году компания Atmel анонсировала линейку «младших» кортексов М0+ семейства SAM D09, SAM D10, SAM D11. Эти не сильно «навороченные» контроллеры имеют низкую цену и небольшие корпуса. Причем в линейке присутствуют камни в легкопаяемых корпусах SOIC-14 и SOIC-20. Для ознакомления с возможностями контроллера доступны очень дешевые отладки из серии Xplained mini, которые совместимы с шилдами от Arduino. Эти особенности, возможно, вызовут интерес не только среди профессиональных разработчиков, но и у радиолюбителей.

Когда отладки попали к нам в руки, захотелось вместо «серьёзной» демонстрационной задачи в честь приближающегося Нового года сделать что-нибудь забавное и креативное. Мы поскребли по сусекам и нашли старенький проектик - тетрис на MEGA168 через терминалку и решили портировать его на новый камень и представить общественности. Практического смысла в этом никакого, что называется Just for fun. Кому интересны подробности, прошу под кат.

Кратко о новых микроконтроллерах

  • SAM D09 - младший представитель семейства SAM D. Имеет 8К или 16К флеша и 4К SRAM. Варианты корпусов QFN-24 и SOIC-14. На борту DMA и Event system. 2 SERCOM - универсальных коммуникационных модулей, которые могут конфигурироваться как USART, SPI или I2C. 5-ти или 10-ти канальный 12-ти битный АЦП.
  • SAM D10 - апгрейд D09 в части добавления дополнительных таймеров, аналогового компаратора, ЦАП и контроллера сенсорных кнопок, а так же дополнительного SERCOM для некоторых модификаций. Варианты корпусов QFN-24, SOIC-14, SOIC-20.
  • SAM D11 - тот же D10, но с добавлением Full-Speed USB Device.


Внешний вид отладочной платы. Программатор на борту, подключение через разъем Micro USB.

Теперь про сам тетрис

Работа тетриса основана на нескольких базовых принципах:

  • общение с терминалкой осуществляется по протоколу VT100,
  • обновление картинки происходит по таймеру,
  • любая фигура вписывается в квадрат определенных размеров (4 на 4 символа).

Тетрис использует три команды из протокола VT100 : очистка экрана, перемещение курсора в начало и сделать курсор невидимым.
Для работы по этому протоколу можно использовать терминалку Tera term, например.
Для управления используются 5 клавиш-букв клавиатуры:

  • n – начать новую игру,
  • w или space – повернуть фигуру,
  • s – уронить фигуру,
  • d – переместить вправо,
  • a – переместить влево.

В коде можно легко переназначить клавиши управления на другие

Switch (c) { case "w": case " ": //ROTATE tetris_rotate(); break; case "s": //DOWN tetris_gravity(); break; case "d": //RIGHT tetris_move_right(); break; case "a": //LEFT tetris_move_left(); break; default: break; } if (c == "n") { c=0; //Seed random function so we do not get same start condition //for each new game. In essence we will not start a new game //exactly at the same time. srand(tick); //New Game is_running = true; terminal_cursor_off(); terminal_clear(); tetris_init(); tetris_new_block(); terminal_cursor_home(); tetris_print(); }

Скорость игры устанавливается таймером. Для более опытных игроков можно задать «тиканье» быстрее, тогда и фигуры будут падать быстрее.

Конечно же, подсчитываются очки: за каждую исчезнувшую строку добавляется 100 очков. За каждую следующую «исчезнувшую» одновременно с первой, добавляется в два раза больше очков, чем за предыдущую.

Портируем с mega на samd10

Из периферии контролера нам нужен SERCOM в режиме UART для непосредственной передачи фигурок и картинки, и таймер для отсчета времени обновления картинки.

Вместо милой сердцу любого программиста 8-битных контроллеров настройки UART битами в регистрах:

Static void board_init(void) { /*Configure IO pins: * - UART pins * - SW pin * - LED pin */ DDRD &= ~USART_RX_PIN_bm; DDRD |= USART_TX_PIN_bm; PORTD |= USART_TX_PIN_bm; PORTB |= SW_PIN_bm; DDRB &= ~SW_PIN_bm; /*Disable all modules we will not use*/ PRR = (1 << PRTWI) | (1 << PRTIM2) | (1 << PRTIM0) | (1 << PRSPI) | (1 << PRADC); }

конфигурируем sercom для работы в режиме uart, не забывая разрешить прерывания и callback по приему символа.

Конфигурация Sercom в режиме uart

Static void configure_console(void) { struct usart_config usart_conf; usart_get_config_defaults(&usart_conf); usart_conf.mux_setting = CONF_STDIO_MUX_SETTING; usart_conf.pinmux_pad0 = CONF_STDIO_PINMUX_PAD0; usart_conf.pinmux_pad1 = CONF_STDIO_PINMUX_PAD1; usart_conf.pinmux_pad2 = CONF_STDIO_PINMUX_PAD2; usart_conf.pinmux_pad3 = CONF_STDIO_PINMUX_PAD3; usart_conf.baudrate = CONF_STDIO_BAUDRATE; stdio_serial_init(&cdc_uart_module, CONF_STDIO_USART_MODULE, &usart_conf); } enum status_code usart_enable_rx_interrupt(struct usart_module *const module, uint8_t *rx_data) { // Sanity check arguments Assert(module); Assert(rx_data); // Issue internal asynchronous read // Get a pointer to the hardware module instance SercomUsart *const usart_hw = &(module->hw->USART); module->rx_buffer_ptr = rx_data; // Enable the RX Complete Interrupt usart_hw->INTENSET.reg = SERCOM_USART_INTFLAG_RXC; return STATUS_OK; } void configure_usart_callbacks(void) { usart_register_callback(&cdc_uart_module, USART_RX_callback, USART_CALLBACK_BUFFER_RECEIVED); usart_enable_callback(&cdc_uart_module, USART_CALLBACK_BUFFER_RECEIVED); }

В исходном коде для меги данные по uart принимались с помощью putc, для samd10 сделаем проще: пусть просто по прерыванию каждый полученный байт сваливается в определенную переменную. Это решение не претендует на правильность и безопасность, оно для простоты перехода и ускорения его.
Подробно про то, как победить порой слишком «умную» ASF для приема одного байта по прерываниям, мы писали в нашей статье на сайте we.easyelectronics.ru.

Перейдем к таймерам.
Код для меги:

Void init_timer(void) { /*Start timer used to iterate game and seed random function*/ TIFR1 = 1 << OCF1A; TIMSK1 = 1 << OCIE1A; OCR1A = TIMER_TOP_VALUE; TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10); } ISR(TIMER1_COMPA_vect, ISR_BLOCK) { ++tick; iterate_game = true; }

И соответствующий код для samd10

/** Configures TC function with the driver. */ static void configure_tc(void) { struct tc_config config_tc; tc_get_config_defaults(&config_tc); config_tc.counter_size = TC_COUNTER_SIZE_16BIT; config_tc.wave_generation = TC_WAVE_GENERATION_MATCH_FREQ; config_tc.counter_16_bit.compare_capture_channel = 2000; config_tc.clock_prescaler=TC_CLOCK_PRESCALER_DIV1024; tc_init(&tc_instance, CONF_TC_INSTANCE, &config_tc); tc_enable(&tc_instance); } /** Registers TC callback function with the driver. */ static void configure_tc_callbacks(void) { tc_register_callback(&tc_instance, tc_callback_to_counter, TC_CALLBACK_CC_CHANNEL0); tc_enable_callback(&tc_instance, TC_CALLBACK_CC_CHANNEL0); } static void tc_callback_to_counter(struct tc_module *const module_inst) { ++tick; iterate_game = true; }

Вот и все. Весь остальной код для обработки движения фигур и всей остальной логики остается таким же.
Полностью проект для samd 10 лежит на

Один из наборов для самостоятельной сборки jolliFactory поставляется с модулем управления двухцветной светодиодной матрицей . Данный модуль позволяет соединять последовательно большое количество модулей в соответствии с требованиями вашего проекта.

С помощью данных модулей было изготовлено несколько интересных устройств:

  • Проект Текстового дисплея с пролистыванием на базе 7 двухцветных светодиодных матриц
  • Проект Текстового дисплея с пролистыванием на базе двухцветной светодиодной матрицы с возможностью голосового управления и использованием Arduino (Bluetooth + Android)

Видеоигра тетрис была выпущена в 1984 году и имела огромный успех для портативной ручной приставки Game Boy, выпущенной в 1989 году, которая сделала ее популярной до настоящего времени.

Просто ради развлечения, мы решили создать простой тетрис с использованием двух последовательно соединенных двухцветных светодиодных модулей, которые управляются микроконтроллером Arduino. Мы просмотрели много сайтов в интернете, но не нашли ничего подобного.

Мы попытались найти подобные проекты в сети Интернет и получили некоторую информацию, которую впоследствии адаптировали для создания простой игры тетрис на базе Arduino и двухцветной светодиодной матрицы.

Для выполнения данного проекта необходимы базовые знания электроники, некоторые умения по пайке компонентов и знание микроконтроллера Arduino.

Ниже показано видео работы готового устройства.

Шаг 1: Изготовления драйвера управления двухцветным светодиодным модулем

В данном проекте используются две светодиодные матрицы, управляемые Arduino Nano. Нам понадобится два набора Bi-color (Red/Green) LED Matrix Driver Module Kits от jolliFactory. Каждый из этих модулей использует две микросхемы-драйвера MAX7219 для управления двухцветной светодиодной матрицей. Данные микросхемы идеально пригодны для нашего проекта, поскольку снимают основную работу с микроконтроллера и облегчают разводку логических элементов устройства.

Двухцветные светодиодные матрицы можно приобрести .

Данный набор имеет все необходимые компоненты. Для сборки нужны некоторые навыки пайки.

Ниже показано видео по сборке светодиодного модуля:

Шаг 2: Подключение

После сборки светодиодных матриц, их необходимо подключить к микроконтроллеру Arduino Nano согласно принципиальной схемы подключения (светодиодные матрицы не показаны для удобства отображения).

Для звукового выхода игровой приставки мы использовали 8-омный динамик мощностью 0.5 ватта, управляемый напрямую одним из цифровых выводов Arduino через резистор 100 ом. В данном проекте используется основной звуковой тон. Для портативной приставки будет вполне достаточно невысокого уровня громкости, регулируемого с помощью простых настроек.

Для навигации и вращения блоков игры тетрис понадобится SPST панель с четырьмя нажимными кнопками.

Необходимо использовать подтягивающие резисторы номиналом 10 кОм для входных выводов DATA IN, CLK и LOAD. При первой подаче питания на микроконтроллер или при его сбросе линии ввода/вывода будут «висеть» в воздухе. Микросхема MAX7219 увидит это как достоверные данные и начнет отображать «мусор», пока микроконтроллер не начнет отрабатывать управляющую программу. Чтобы этого не происходило необходимо использовать подтягивающие резисторы. Для снижения общего количества компонентов можно не использовать подтягивающие резисторы номиналом 10 кОм для входных выводов DATA IN и CLK.

За исключением двух двухцветных светодиодных модулей и четырех нажимных кнопок, мы выполнили монтажные соединения всей схемы на небольшой перфорированной плате размером около 60 мм x 60 мм.

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

Шаг 3: Программирование платы Arduino

В плату Arduino необходимо загрузить скетч для управления дисплеем.

Для этого мы использовали Arduino IDE V1.03.

Загружаемый скетч игры тетрис очень простой, без возможности выбора уровней и подсчета очков. Вы можете изменить и улучшить данный скетч.

Собирая устройства на Ардуино, мы часто сталкиваемся с необходимостью автономного вывода информации. И, как всегда, хочется чтобы решение было недорогим. И вот тут оказывается, что из недорогих устройств выбор не очень-то и богат.

Если выводимой инфы немного, удобно использовать семисегментные индикаторы. Они очень яркие и контрастные. Такие линейки из 4-х разрядов высотой в 0.36 дюйма на TM1637 продаются по 70 центов и управляются всего по 2-м пинам. Как нетрудно догадаться, заточены они, в основном, под отображение времени, хотя без труда могут отображать и, к примеру, температуру, давление и другие параметры, для которых достаточно 4-х разрядов.

Но если выводимой информации много, они не подойдут. В таких случаях чаще всего используются «народные» LCD 1602 дисплейчики, имеющие возможность вывода 2-х строк по 16 символов ценой в полтора бакса. На такой уже можно вывести информации в разы больше.

Его более продвинутый 4-х строчный собрат выведет инфы еще больше, но стоит уже заметно дороже, около 5 долларов, да и размер у него уже немаленький.

У всех этих устройств имеются свои плюсы и минусы. Из главных минусов можно отметить отсутствие поддержки русского языка, поскольку кодовая таблица зашита наглухо в микросхему, и невозможность вывода графики. Строго говоря, прошивка кириллицы в подобных устройствах бывает, но такие продаются в основном в России и по неразумным ценам.
Если эти минусы являются для применения в создаваемом устройстве решающими и разрешение в 84x48 точек в черно-белой графике вас устроит, то стоит обратить внимание на дисплейчик Nokia 5110. Когда-то на него был, но очень неполный, и местами устаревший. В частности там утверждалось о невозможности отображения кириллицы. На сегодня такой проблемы нет.

Герой нашего обзора, ценой менее пары баксов, ко мне пришел в прочной картонной коробке с защитной пленкой на экране, за что большое спасибо продавцу. Девайс имеет размеры печатной платы 45x45 мм красного текстолита и экран ЖК с разрешением 84x48 точек и размером 40x25 мм. Вес устройства 15 г. У него есть подсветка голубого цвета, которую можно отключить. У Ардуино этот дисплей откусит 5 цифровых пинов, не считая питания. На плате есть 2 ряда выводов, которые запараллелены между собой, поэтому можно использовать всего один ряд. Из них 5 – это управление, 2 питание и один на включение подсветки. Для включения подсветки нужно пин LIGHT замкнуть на землю (встречается другой вариант этого дисплея, как пишут - на плате синего цвета, где этот пин соединяется с питанием). Плата приходит нераспаянной, две гребенки прилагаются в комплекте.
Итак, подсоединяем выводы SCLK, DIN, DC, CE и RTS к пинам Ардуино, например, 3, 4, 5, 6, 7. Пин VCC к 3.3V (Именно 3.3, а не 5!), подсоединяем землю и качаем библиотеку .
Функции из этой библиотеки позволяют выводить графические примитивы (линия, круг, прямоугольник и т.д.), растровые картинки и текст. В составе библиотеки есть пример, показывающий ее возможности, советую посмотреть. А вот, чтобы текст выводился на русском, придется подшаманить фонт. Но, к счастью, добрые люди все уже сделали за нас и файл для подмены можно скачать .
Пример скетча я дам далее, а результат вывода текста на русском видим выше. Нетрудно подсчитать, что на самом маленьком размере шрифта (№ 1), можно вывести 84 символа (по 14 в 6 строк), чего вполне хватит для вывода, например, емких диагностических сообщений. Шрифт №2 вдвое крупнее.
Разрешение экрана позволяет выводить довольно неплохие растровые двухцветные картинки, которые в программе можно использовать в качестве иконок.

Создавать такие иконки очень просто. Под спойлером я покажу, как это делается.

Как быстро создать растровую картинку на примере логотипа сайта MYSKU

Для начала сделаем скрин экрана с логотипом (клавиша Print Screen).
Запустим Paint из стандартных программ и жмем Ctrl+V. Весь экран с логотипом в нашем распоряжении.


Выделяем нужный фрагмент и жмем кнопку ОБРЕЗАТЬ. Получим наш фрагмент.

Теперь нам нужно превратить этот фрагмент в двухцветный. С эти справится сам Paint. Жмем «Сохранить как» и выбираем тип «Монохромный риcунок (*.bmp)». Не обращаем внимания на предупреждение редактора и жмем Ок и видим такую картинку:

Теперь нужно превратить этот набор пикселей в массив кодов для Ардуино. Я нашел , который справляется с такой задачей.
Ему на вход нужно подать bmp файл, но обязательно 256 цветный! Поэтому мы снова жмем «Сохранить как» и выберем тип «256-цветный рисунок bmp». Теперь запомним размеры сторон получившегося файла, их будет нужно указать в скетче (смотрим или в Paint внизу в строке состояния или открыв Свойства файла - > вкладка Подробно) и перейдем в онлайн конвертер.


Выберем наш файл, поставим галочку на шестнадцатеричных числах и нажмем КОНВЕРТИРОВАТЬ.
Получим нужный нам массив:


Копируем этот массив в скетч, компилируем и смотрим, что получилось.


Теперь отключим подсветку и посмотрим, как изображения будут выглядеть без нее.


Как видим, и текст и значки читаются хорошо. Причем, чем ярче свет, тем лучше читабельность (эх, вспоминаю как было приятно пользоваться Nokia 1100 солнечным днем, в то время, как народ прятал свои трубки с цветными матрицами в тень, чтобы набрать номер). В общем, в таком режиме можно использовать дисплей, если освещения хватает или подсветка мешает или для экономии автономного питания. Если у кого изображение на экранчике будет плохо видно, поиграйтесь с контрастностью в скетче. Лучшая контрастность при подсветке и без нее получается при разных значениях, это надо учитывать.

Пример скетча для вывода текста и картинки

#include #include // pin 3 - Serial clock out (SCLK) // pin 4 - Serial data out (DIN) // pin 5 - Data/Command select (D/C) // pin 6 - LCD chip select (CS) // pin 7 - LCD reset (RST) Adafruit_PCD8544 display = Adafruit_PCD8544(3, 4, 5, 6, 7); const static unsigned char PROGMEM ku59x39 = { 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0xc0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0xe0, 0x1f, 0xff, 0xff, 0xe0, 0x0, 0x3, 0xf0, 0xfc, 0xf, 0xff, 0xfc, 0x0, 0x0, 0x0, 0x0, 0xff, 0x87, 0xff, 0xc0, 0x0, 0x0, 0x7f, 0xc0, 0xff, 0xe3, 0xff, 0x0, 0x0, 0x7f, 0xff, 0x90, 0xff, 0xf1, 0xfc, 0x0, 0x7, 0xff, 0xff, 0x30, 0xff, 0xf8, 0xf0, 0x0, 0x7f, 0xff, 0xfe, 0x30, 0xff, 0xfc, 0x40, 0x3, 0xff, 0xff, 0xfc, 0x70, 0xff, 0xfe, 0x0, 0xf, 0xff, 0xff, 0xf8, 0xf0, 0xff, 0xff, 0x0, 0x7f, 0xff, 0xff, 0xf0, 0xf0, 0xff, 0xff, 0x81, 0xff, 0xff, 0xff, 0xe1, 0xf0, 0xff, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xe3, 0xf0, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xc3, 0xf0, 0xff, 0xff, 0xe3, 0xff, 0xff, 0xff, 0x83, 0xf0, 0xff, 0xff, 0xe1, 0xff, 0xff, 0xff, 0x87, 0xf0, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0x87, 0xf0, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0x87, 0xf0, 0xff, 0xff, 0xf8, 0x7f, 0xff, 0x1f, 0x87, 0xf0, 0xff, 0xff, 0xf8, 0x7f, 0x0, 0x0, 0x7, 0xf0, 0xff, 0xff, 0xfc, 0x38, 0x0, 0x0, 0x1, 0xf0, 0xff, 0xff, 0xfc, 0x20, 0x0, 0x0, 0xbf, 0xf0, 0xff, 0xff, 0xfe, 0x0, 0x3, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xfe, 0x0, 0x3f, 0xe1, 0xff, 0xf0, 0xff, 0xff, 0xfe, 0x3, 0xf0, 0x0, 0x1, 0xf0, 0xff, 0xff, 0xfe, 0xf, 0x80, 0x0, 0x0, 0xf0, 0xff, 0xff, 0xff, 0xe, 0x0, 0x3f, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xc, 0x3, 0xff, 0xe1, 0xf0, 0xff, 0xff, 0xff, 0x0, 0x1f, 0xff, 0xc0, 0xf0, 0xff, 0xff, 0xff, 0x80, 0x7f, 0xff, 0x88, 0xf0, 0xff, 0xff, 0xff, 0x80, 0xff, 0xff, 0x9c, 0xf0, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0x38, 0xf0, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0x19, 0xf0, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xff, 0x3, 0xf0, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0x87, 0xf0, 0xff, 0xff, 0xff, 0xce, 0x7f, 0xff, 0xdf, 0xf0, 0xff, 0xff, 0xff, 0xce, 0x7f, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0x9e, 0x7f, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0x8c, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0x80, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xc1, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xf0 }; void setup() { display.begin(); display.setContrast(50); display.setTextSize(1); display.setTextColor(BLACK); // установка цвета текста } void loop() { display.clearDisplay(); display.drawBitmap(10, 5, ku59x39, 59, 39, BLACK); display.display(); delay(2000); display.clearDisplay(); display.setCursor(0,0); display.print("ЗРС способна поражать не только баллистические, но и аэродинамические цели - самолеты"); display.display(); delay(2000); }


Ну, и, поскольку Ардуино (Процессор+Озу+Загрузчик-BIOS)+накопитель (EEPROM) + система ввода-вывода (Пульт IRDA и Nokia 5110)- это, по сути, полноценный компьютер, то почему бы не написать для него полноценной игры? Конечно, игру типа ГТА наш ардуино-комп не потянет, но простую казуальную игрушку - запросто! Напишем игру всех времен и народов - Тетрис.
Для любого программиста - это как утренняя зарядка, легкое упражение для мозга, поэтому - вперед! Да и на муське вроде такого раньше еще не было. А в игре как раз раскроется потенциал сабжа.
В качестве системы ввода я решил использовать IRDA пульт от ЛЮБОГО устройства. При таком решении нам понадобится всего лишь один , ценой 4 руб за штуку. А IR пульт найдется в любой квартире. Для озвучки мы еще применим пьезопищалку от старой материнки - это будет наш бюджетный аналог мультимедиа)). Ко мне едет сейчас более крутой , но это уже удорожание нашего суперкомпа на целый доллар! Пока обойдемся. С ним будет уже .
На макетке коммутируем устройства вывода, ввода и нашу «мультмедиа». Получилось так:


Я использовал Arduino Uno, поскольку там нужные нам 3.3V уже есть, но если использовать Mini, то придется для экрана добыть из 5 вольт нужные 3.3. Самый несложный способ из инета - поставить последовательно два кремниевых диода (подобрать).
Чтобы не рисовать электрическую схему, просто укажу задействованные мной пины Ардуино.
Подсоединение дисплея Nokia 5110:
pin 3 - Serial clock out (SCLK)
pin 4 - Serial data out (DIN)
pin 5 - Data/Command select (D/C)
pin 6 - LCD chip select (CS)
pin 7 - LCD reset (RST)
Для подсветки пин LIGHT дисплея кидаем на GND Ардуино. (Только для платы красного цвета!). Плюс питания на 3.3V. Земля на GND.
Подсоединение IR приемника:
pin 8 - IR (управляющий). Питание на +5V и GND соответственно.
Подсоединение пьезопищалки:
pin 9 - speaker, Земля на GND.
После монтажа, заливаем скетч

Скетч игры Тетрис

//// © Klop 2017 #include #include #include #include #define rk 4 // ширина квадратика #define rz 5 // ширина места #define smeX 1 #define smeY 1 #define MaxX 10 // стакан кол-во мест по гориз #define speaker 9 #define RECV_PIN 8 // нога на IRDA приемник // pin 3 - Serial clock out (SCLK) // pin 4 - Serial data out (DIN) // pin 5 - Data/Command select (D/C) // pin 6 - LCD chip select (CS) // pin 7 - LCD reset (RST) Adafruit_PCD8544 display = Adafruit_PCD8544(3, 4, 5, 6, 7); IRrecv irrecv(RECV_PIN); decode_results results; byte mstacan; byte Lst,SmeH, center, NumNext; byte MaxY; // стакан кол-во мест по вертик int dxx, dyy, FigX, FigY, Victory, myspeed,tempspeed; unsigned long ok, levo, pravo, vniz, myrecord; unsigned long flfirst=1234; // метка первого запуска byte fig= { {{0,0,0,0}, {0,0,0,0}, {0,0,0,0}, {0,0,0,0}}, {{0,1,0,0}, {0,1,0,0}, {0,1,0,0}, {0,1,0,0}}, {{0,0,0,0}, {0,1,1,0}, {0,1,1,0}, {0,0,0,0}}, {{0,1,0,0}, {0,1,1,0}, {0,0,1,0}, {0,0,0,0}}, {{0,1,0,0}, {0,1,0,0}, {0,1,1,0}, {0,0,0,0}}, {{0,1,0,0}, {0,1,1,0}, {0,1,0,0}, {0,0,0,0}}, {{0,0,1,0}, {0,1,1,0}, {0,1,0,0}, {0,0,0,0}}, {{0,0,1,0}, {0,0,1,0}, {0,1,1,0}, {0,0,0,0}}, {{0,0,0,0}, //8 {0,0,0,0}, {0,0,0,0}, {0,0,0,0}} }; //============================================== void mybeep() // звук {analogWrite(speaker, 100); delay(100); analogWrite(speaker, 0); } //============================================== void figmove(byte a, byte b) { for (byte i=0;i<4;i++) for (byte j=0;j<4;j++) fig[a][i][j]=fig[b][i][j]; } //============================================== void figinit(byte a) { for (byte i=0;i<4;i++) for (byte j=0;j<4;j++) { fig[i][j]=fig[i][j]; if (fig[a][j][i]==1) // покажем след фигуру display.fillRect(i*rz+60, 20+(j)*rz, rk , rk, BLACK); else display.fillRect(i*rz+60, 20+(j)*rz, rk , rk, WHITE); } display.display(); NumNext=a; tempspeed=myspeed-(Victory/30)*50; // через каждые 30 линий увеличим скорость падения; dxx=0; for (byte i=0;i0) display.fillRect(i*rz+1, SmeH+(j-4)*rz, rk , rk, BLACK); else display.fillRect(i*rz+1, SmeH+(j-4)*rz, rk , rk, WHITE); ds(Victory,1); display.display(); } //================================================ void ds(int aa, int b) { display.fillRect(55, 10, 29, 10, WHITE); display.setCursor(55,b*10); display.println(aa); } //================================================ bool iffig(int dx, int dy) {int i,j; bool flag=true; bool pov=false; for (i=0;iMaxX-1) dx=-1;// пробуем отодвинуть от стенки справа на 1 } } } for (i=0;i<4;i++) for (j=0;j<4;j++) if (fig[j][i]==1) if (i+FigX+dx<0 || i+FigX+dx>MaxX-1 || FigY+j+dy>MaxY-1 || mstacan>0) {flag=false; break;} // проверили на новые координаты if (flag) {FigX=FigX+dx; FigY=FigY+dy;byte k=0; for (i=0;i<4;i++) for (j=0;j<4;j++) if (fig[j][i]==1) {mstacan=1; dxx=0; } } // переместили фигуру на новые координаты else { if (pov) figmove(0,8); for (i=0;i<4;i++) // восстановили фигуру for (j=0;j<4;j++) if (fig[j][i]==1) mstacan=1; } return(flag); } //================================================ void clearstacan() { for (byte i=0;imyrecord) { myrecord=Victory; EEPROM_write(16, myrecord); } display.setCursor(5,0); display.print("Рекорд"); display.setCursor(5,10); display.print(myrecord); display.display(); display.setCursor(5,20); delay(2000);irrecv.resume(); display.println("Нажмите"); tb=getbutton(" OK"); if (tb!=ok) { ok=tb; levo=getbutton("Влево"); pravo=getbutton("Вправо"); vniz=getbutton("Вниз"); EEPROM_write(0, ok); EEPROM_write(4, levo); EEPROM_write(8, pravo); EEPROM_write(12, vniz); } display.fillRect(5, 0, (MaxX-1)*rz, 40, WHITE); myspeed=800; tempspeed=myspeed; Victory=0; } //================================================ void setup() { unsigned long tr; word gg=0; randomSeed(analogRead(0)); irrecv.enableIRIn(); // Старт ресивера IRDA display.begin(); display.setContrast(50); display.setTextSize(1); display.setTextColor(BLACK); // установка цвета текста display.clearDisplay(); Lst=rz*MaxX; // ширина стакана в пикселях MaxY=display.height()/rz+4; // Высота стакана в кубиках SmeH=display.height()%rz; // смещение сверху в пикселях для отображения random(7); EEPROM_read(0, ok); EEPROM_read(4, levo); EEPROM_read(8, pravo); EEPROM_read(12, vniz); EEPROM_read(20, tr); if (tr==flfirst) EEPROM_read(16, myrecord); else { myrecord=0; EEPROM_write(16, myrecord); EEPROM_write(20, flfirst); } newgame(); } //================================================ void dvoiki() { for (byte i=0;i
И можно ирать. Игра поддерживает привязку к любому пульту. Для этого достаточно в начале игры, на вопрос «Нажмите ОК» нажать на пульте кнопку, которая будет отвечать за вращение фигуры. Если пульт игре уже знакомый, то игра сразу запустится. Если пульт новый, то код кнопки ОК не совпадет с запомненным и игра потребует последовательно нажать кнопки «Влево», «Вправо» и «Вниз». Эти кнопки будут записаны в энергонезависимую память Ардуино и впоследствии именно этот пульт будет узнаваться сразу по нажатию кнопки «ОК».


При «проваливании» на собранную строку будет воспроизводиться писк. Он реализован на особенности нескольких пинов Ардуино (в нашем случае 9) выдавать ШИМ с заданной частотой.
Игра поддерживает все атрибуты нормальной игры. Ту и подсказка следующей фигуры и текущий счет. Игра ведет учет рекордов. Это значение хранится в энергонезависимой памяти Ардуино. Чтобы сбросить рекорд, достаточно изменить в скетче значение flfirst=1234 на любое другое. В игре также идет автоувеличение скорости падения через каждые 30 списанных строчек, так что, бесконечно долго поиграть не получится). Скетч не оптимизировался и тщательно не прогонялся, а был написан на досуге в свое удовольствие. Если кто обнаружит ошибку - пишите. О ©. Скетч разрешается править для себя как угодно. Только при публикации где-либо своих вариантов ссылку на первоисточник-муську указывайте).
Для чего делал - длинные выходные + «из любви к искусству». Была бы дочка маленькой, сделал бы ей, наверное, мини игровой автомат для кукольной комнатки на 8 марта, как раз успел бы. Добавил бы несколько игр типа Змейки и Арканоида, а корпус вырезал бы из текстолита, наверное. Только дочка в этом году уже докторскую защищает, так, что мимо, но может кому еще эта идея пригодится).

Подведем итог для дисплея Nokia 5110:
Плюсы
+Возможность вывода графики;
+Отсутствие проблем с кириллицей;
+Небольшой вес;
+Отличное соотношение габариты/кол-во выводимой информации;
+Плюсы примененной технологии ЖК - малое энергопотребление и хорошая читабельность на ярком свете;
+Отключаемая подсветка;
+Цена.

Минусы
-Подсветка неравномерная;
-Изображение черно-белое (оттенков нет);
-Необходимость позаботиться о 3.3V, не на каждой Ардуино такое напряжение есть.

Вердикт: За свои деньги по совокупности характеристик уверено занимает свою нишу, недаром и является таким долгожителем среди устройств отображения для Ардуино.

В этом уроке мы создадим игру «Тетрис». Это известная многим игра «головоломка» изобретённая и написанная советским программистом Алексеем Пажитновым. Первоначальная версия игры была написана для компьютера «Электроника-60» на языке Паскаль. Игра была выпущена 6 июня 1984 г. Мировую известность игра приобрела благодаря её выпуску на портативной консоли GameBoy компании Nintendo

Правила игры «Тетрис»:

Вверху игрового поля появляются случайные геометрические фигуры, которые падают пока не достигнут низа поля или других фигур. Во время падения, фигуры можно сдвигать по горизонтали (влево / вправо), поворачивать на 90° (по часовой стрелке) и ускорять их падение. Таким образом игрок может выбирать место падения фигуры. Если из упавших фигур соберётся горизонтальный ряд без пробелов (пустот), то этот ряд исчезнет, а всё что было над ним опустится. За каждый исчезнувший ряд игрок получает очки. Скорость падения фигур увеличивается с каждым новым уровнем игры. Уровень игры увеличивается через определённое количество появившихся фигур. Игра заканчивается если новая фигура не может появиться вверху игрового поля по причине того что ей мешают уже имеющиеся там фигуры.

Справа от игрового поля выводится информация о текущем уровне, количестве набранных очков и изображение фигуры, которая появится следующей. Зная какая фигура появится следующей, можно планировать место падения текущей фигуры.

Нам понадобится:

  • Trema OLED-дисплей 128x64 х 1шт.
  • Trema-модуль Кнопка х 4шт. (в ассортименте: синяя , красная , зелёная)
  • Trema Set Shield х 1шт.
    И никаких проводов (кроме USB для загрузки скетча).

Для реализации проекта нам необходимо установить библиотеку:

  • iarduino_OLED - графическая библиотека для работы с Trema OLED дисплеями.

О том как устанавливать библиотеки, Вы можете ознакомиться на странице Wiki - Установка библиотек в Arduino IDE .

Видео:


Схема подключения:

  • Перед подключением модулей, закрепите винтами нейлоновые стойки в отверстиях секций 1, 2, 3, 5 и 6 Trema Set Shield .
  • Установите Trema Set Shield на Arduino Uno .

Остальные модули устанавливаются на Trema Set Shield следующим образом: Trema Кнопки устанавливаются в центр нижних колодок секций 1, 2, 5 и 6, а Trema OLED-дисплей 128x64 устанавливается в верхнюю колодку 3 секции, как это показано на рисунках ниже.


После чего закрепите модули вкрутив через их отверстия нейлоновые винты в установленные ранее нейлоновые стойки (нейлоновые стойки и винты входят в комплектацию Trema Set Shield).

Наличие всего двух колодок в секциях Trema Set Shield , не позволит Вам неправильно установить модули, т.к. при неправильном подключении модули будут смещены относительно разметки своей секции и Вы не сможете закрепить их винтами.

Назначение кнопок:
  • L (Left) смещение фигуры влево.
  • R (Right) смешение фигуры вправо.
  • T (Turn) поворот фигуры на 90° по часовой стрелке.
  • D (Down) сброс фигуры вниз (ускорение падения фигуры).

Код программы:

Код может показаться немного громоздким. Для понимания кода рекомендуем сначала прочитать раздел «алгоритм работы» следующий сразу за кодом. А потом ознакомиться с комментариями в строках самого кода.

#include // Подключаем библиотеку iarduino_OLED. iarduino_OLED myOLED(0x78); // Объявляем объект myOLED, указывая адрес дисплея на шине I2C: 0x78 (если учитывать бит RW=0). // #define pinBtnL A0 // Номер вывода к которому подключена кнопка L - сдвиг фигуры влево (Left). #define pinBtnR A3 // Номер вывода к которому подключена кнопка R - сдвиг фигуры вправо (Right). #define pinBtnT 11 // Номер вывода к которому подключена кнопка T - поворот фигуры по ч.с. (Turn). #define pinBtnD 6 // Номер вывода к которому подключена кнопка D - опустить фигуру вниз (Down). #define pinSeed A1 // Номер аналогового входа значения которого будут использованы для выбора случайной точки входа в последовательность псевдослучайных чисел, для корректной работы функции random(). #define GAME_OFF 0 // Одно из состояний игры (будет храниться в переменной state). #define GAME_ON 1 // Одно из состояний игры (будет храниться в переменной state). #define GAME_OVER 2 // Одно из состояний игры (будет храниться в переменной state). /* Подключаем шрифты и картинки: */ // extern uint8_t SmallFontRus; // Подключаем шрифт SmallFontRus (шрифт предустановлен в библиотеке iarduino_OLED). extern uint8_t MediumFontRus; // Подключаем шрифт MediumFontRus (шрифт предустановлен в библиотеке iarduino_OLED). extern uint8_t Img_Logo; // Подключаем изображение Img_Logo (изображение предустановлено в библиотеке iarduino_OLED). /* Определяем настраиваемые значения: */ // const uint16_t sumToNewLevel = 20; // Определяем количество фигур которое нужно набрать для перехода на новый уровень. const uint16_t startTime = 1000; // Определяем начальное время в мс с которым фигура опускается на одну клетку игрового стола (начальная скорость игры). const uint16_t changeTimeLevel = 10; // Определяем значение в мс на которое уменьшается время опускания фигуры с каждым новым уровнем (увеличение скорости). const uint32_t tmrKeyHold = 400; // Определяем время в мс после которого нажатая кнопка будет считаться удерживаемой (залипание при нажатии кнопки). const uint8_t scrBoundary = 68; // Определяем границу отделяющую левую и правую части экрана. const uint8_t newCubeSize = 6; // Определяем размер клеток в пикселях для поля отображения будущих фигур. const uint8_t tableCubeSize = 3; // Определяем размер клеток в пикселях для игрового стола. /* Объявляем массивы и переменные: */ // const uint8_t tableRows = (64-3) / (tableCubeSize+1); // Определяем количество строк на игровом столе (зависит от размера клеток игрового стола). Количество = (высота экрана - 3 линии) / (высота клеток игрового стола + 1 разделительная линия). const uint8_t tableCols = (scrBoundary-4) / (tableCubeSize+1); // Определяем количество столбцов на игровом столе (зависит от размера клеток игрового стола). Количество = (ширина до границы - 4 линии) / (ширина клеток игрового стола + 1 разделительная линия). uint32_t tableArray ; // Объявляем массив клеток игрового стола. Каждый элемент массива - это строки игрового стола, а каждый бит это клетка на игровом столе (0-пуста, 1-закрашена). uint8_t figureArray ; // Объявляем массив для хранения вида движущейся фигуры. | Каждый элемент массивов figureArray и figureArrayNew - это строки фигуры, в которых биты 4-0 являются флагами наличия кубиков в столбцах стоки (0-нет, 1-есть). uint8_t figureArrayNew ; // Объявляем массив для хранения вида будущей фигуры. | Последний элемент массивов figureArray и figureArrayNew - это флаг запрещающий поворот фигуры (0-разрешено, 1-запрещено). int8_t figurePos ; // Объявляем массив позиции движущейся фигуры на игровом столе (в клетках). : отступ слева, : отступ сверху, : начальный отступ новой фигуры слева, : начальный отступ новой фигуры сверху. uint8_t state; // Объявляем переменную для хранения текущего состояния игры: GAME_OFF, GAME_ON, GAME_OVER. uint32_t sumfig; // Объявляем счётчик количества созданных фигур: 0-4294967296. uint8_t level; // Объявляем счётчик текущего уровня игры: 1-99. uint32_t points; // Объявляем счётчик набранного количества балов: 0-99999. uint32_t tmrShift; // Объявляем переменную для хранения времени следующего сдвига фигуры вниз. bool valBtnL, valBtnR, valBtnT, valBtnD; // Объявляем переменные для хранения состояния кнопок. /* Объявляем функции: */ // void getKeys (void); // Объявляем функцию получения состояния всех кнопок в переменные valBtnL, valBtnR, valBtnT, valBtnD. void showWelcome (void); // Объявляем функцию прорисовки анимированного приветствия. void showGameScreen (void); // Объявляем функцию прорисовки игрового поля. void showFigure (bool, bool); // Объявляем функцию прорисовки или затирания фигуры (аргументы: флаг , флаг ). void showTable (void); // Объявляем функцию прорисовки всех кубиков в клетках игрового стола. bool createNewFigure (void); // Объявляем функцию создания будущей фигуры. Функция возвращает false если вставить ранее созданную будущую фигуру на игровой стол не удается (завершение игры). void turnFigure (bool, uint8_t=1); // Объявляем функцию поворота фигуры на 90° по часовой стрелке (аргументы: флаг , количество поворотов будущей фигуры на 90° по часовой стрелке). bool shiftFigure (uint8_t); // Объявляем функцию сдвига фигуры на одну клетку игрового стола. Если возвращает false если сдвиг невозможен. (аргументы: 1 - влево, 2 - вправо, 3 - вниз, иначе - без сдвига). bool checkFigure (void); // Объявляем функцию проверки корректности положения фигуры на игровом столе (перед её отрисовкой). Если положение фигуры некорректно, то функция вернёт false. bool checkTable (void); // Объявляем функцию проверки полностью заполненных строк на игровом столе (функцию требуется вызывать до создания новой фигуры на игровом столе). Если на игровом столе есть полностью заполненные строки, то функция вернёт true. uint8_t deletTableRows (void); // Объявляем функцию удаления полностью заполненных строк на игровом столе (функцию требуется вызывать до создания новой фигуры на игровом столе). Функция возвращает количество удалённых строк с игрового стола. // // Примечание: игровой стол - прямоугольная область которая заполняется перемещаемыми фигурами. // будущая фигура - фигура отображаемая справа от игрового стола. // новая фигура - фигура которая только появилась на игровом столе. void setup(){ // myOLED.begin(); // Инициируем работу с Trema OLED дисплеем. myOLED.autoUpdate(false); // Запрещаем автоматический вывод данных без обращения к функции myOLED.update(). // myOLED.setCoding(TXT_UTF8); // Указываем кодировку текста в скетче. Если на дисплее не отображается Русский алфавит, то раскомментируйте функцию setCoding и замените параметр TXT_UTF8, на TXT_CP866 или TXT_WIN1251. pinMode(pinBtnL, INPUT); // Конфигурируем вывод pinBtnL как вход. pinMode(pinBtnR, INPUT); // Конфигурируем вывод pinBtnR как вход. pinMode(pinBtnT, INPUT); // Конфигурируем вывод pinBtnT как вход. pinMode(pinBtnD, INPUT); // Конфигурируем вывод pinBtnD как вход. randomSeed(analogRead(pinSeed)); random(12345); // Выбираем случайную точку входа в последовательность псевдослучайных чисел, для корректной работы функции random(). randomSeed(analogRead(pinSeed) + random(12345)); // Выбираем случайную точку входа в последовательность псевдослучайных чисел, для корректной работы функции random(). random(12345); // Вызываем функцию random() в холостую (т.к. её первое значение после функции randomSeed(...) часто повторяется). figurePos = (int8_t(tableCols)-5) / 2; // Определяем начальный отступ новой фигуры слева (количество клеток от левого края игрового стола). showWelcome(); // Выводим анимированное приветствие. state=GAME_OFF; // Переводим состояние игры в GAME_OFF. } // // void loop(){ // getKeys(); // Получаем состояния всех кнопок в переменные valBtnL, valBtnR, valBtnT, valBtnD. // Не играем: // if(state==GAME_OFF){ // Если состояние игры равно GAME_OFF, то... if(valBtnL||valBtnR||valBtnT||valBtnD){ // Если нажата любая кнопка, то... state = GAME_ON; // Переводим состояние игры в GAME_ON (играем). sumfig = 0; // Сбрасываем счётчик количества созданных фигур. level = 1; // Сбрасываем счётчик текущего уровня. points = 0; // Сбрасываем счётчик набранного количества балов. tmrShift = 0; // Сбрасываем время следующего сдвига вниз фигуры на игровом столе. valBtnD = 0; // Обнуляем состояние кнопки Down (состояние остальных кнопок обнулится при первом вызове функции getKeys). memset(tableArray , 0, tableRows*4); // Обнуляем массив tableArray заполняя нулями все tableRows его элементов по 4 байта каждый. memset(figureArray , 0, 6); // Обнуляем массив figureArray заполняя нулями все 6 его элементов. memset(figureArrayNew, 0, 6); // Обнуляем массив figureArrayNew заполняя нулями все 6 его элементов. showGameScreen(); // Прорисовываем игровое поле. createNewFigure(); // Создаём будущую фигуру. При этом новой фигуры на игровом столе ещё нет. createNewFigure(); // Создаём будущую фигуру. Теперь ранее созданная будущая фигура стала новой фигурой на игровом столе. } // }else // // Играем: // if(state==GAME_ON){ // Если состояние игры равно GAME_ON, то... if(valBtnL){shiftFigure(1);} // Если нажата кнопка Left, то сдвигаем фигуру игрового стола влево на одну клетку. if(valBtnR){shiftFigure(2);} // Если нажата кнопка Right, то сдвигаем фигуру игрового стола вправо на одну клетку. if(valBtnT){turnFigure(1);} // Если нажата кнопка Turn, то поворачиваем фигуру игрового стола на 90° по часовой стрелке. if(valBtnD || (millis() > tmrShift)){ // Если нажата кнопка Down или наступило время очередного сдвига вниз фигуры на игровом столе, то... tmrShift = millis()+(startTime-((level-1)*changeTimeLevel)); // Обновляем время следующего сдвига вниз фигуры на игровом столе. if(!shiftFigure(3)){ // Сдвигаем фигуру игрового стола на 1 клетку вниз. Если фигура достигла низа игрового стола или другой фигуры на игровом столе, то... if(checkTable()){ points+=deletTableRows(); } // Проверяем наличие заполненных строк на игровом столе, если они есть, то удаляем заполненные строки, добавляя их количество в переменную points. valBtnD = 0; // Обнуляем состояние кнопки Down (состояние остальных кнопок обнулится при первом вызове функции getKeys). level = sumfig/sumToNewLevel+1; // Определяем текущий уровень игры (он зависит от количества уже выведенных фигур). sumfig++; // Увеличиваем счётчик созданных фигур. myOLED.setFont(SmallFontRus); // Указываем шрифт который требуется использовать для вывода цифр и текста. myOLED.print(level , scrBoundary + 8*myOLED.getFontWidth(), 7); // Выводим текущий уровень в указанную позицию на экране. myOLED.print(points , scrBoundary + 5*myOLED.getFontWidth(), 20); // Выводим текущий уровень в указанную позицию на экране. myOLED.update(); // Обновляем информацию на экране OLED дисплея. if(!createNewFigure()){state=GAME_OVER;} // Создаём будущую фигуру. Если создать фигуру невозможно, то переводим состояние игры в GAME_OVER (игра завершена). } // } // }else // // Игра завершена: // if(state==GAME_OVER){ // Если состояние игры равно GAME_OVER, то... for(uint8_t i=0; itL+tmrKeyHold){valBtnL=1;} fL=1;}else{fL=0;} // Если кнопка L нажата, то присваиваем переменной valBtnL значение 1, но только если кнопка не была нажата ранее (fL==0) или если она нажата дольше tmrKeyHold миллисекунд. valBtnR=0; if(digitalRead(pinBtnR)){ if(!fR){tR=t; valBtnR=1;}else if(t>tR+tmrKeyHold){valBtnR=1;} fR=1;}else{fR=0;} // Если кнопка R нажата, то присваиваем переменной valBtnR значение 1, но только если кнопка не была нажата ранее (fR==0) или если она нажата дольше tmrKeyHold миллисекунд. valBtnT=0; if(digitalRead(pinBtnT)){ if(!fT){valBtnT=1;} fT=1;}else{fT=0;} // Если кнопка T нажата, то присваиваем переменной valBtnT значение 1, но только если кнопка не была нажата ранее (fT==0). if(digitalRead(pinBtnD)){ valBtnD=1;} // Если кнопка D нажата, то присваиваем переменной valBtnD значение 1. Сброс переменной valBtnD осуществляется в коде цикла loop. } // // // Прорисовка приветствия: // Значения возвращаемые функцией: нет. void showWelcome(){ // Аргументы принимаемые функцией: нет. myOLED.autoUpdate(true); // Разрешаем автоматический вывод данных без обращения к функции myOLED.update(). myOLED.clrScr(); // Чистим экран OLED дисплея. myOLED.setFont(MediumFontRus); // Указываем шрифт который требуется использовать для вывода цифр и текста. myOLED.print(F("ТЕТРИС") , OLED_C, OLED_C); // Выводим текст по центру экрана. delay(500); // Ждём myOLED.drawRect(11, 0, 20, 9, true); delay(200); // 0 Выводим заставку: myOLED.drawRect(0, 54, 9, 63, false); delay(200); // 1 Кубики фигур прорисовываются в следующем порядке... myOLED.drawRect(0, 0, 9, 9, true); delay(200); // 2 myOLED.drawRect(118, 11, 127, 20, false); delay(200); // 3 myOLED.drawRect(11, 43, 20, 52, false); delay(200); // 4 ### ## 20A 6B myOLED.drawRect(0, 11, 9, 20, true); delay(200); // 5 # ## 5 73 myOLED.drawRect(96, 0, 105, 9, false); delay(200); // 6 ТЕТРИС myOLED.drawRect(107, 11, 116, 20, false); delay(200); // 7 # ## 4 myOLED.drawRect(22, 54, 31, 63, false); delay(200); // 8 ### ## 198 myOLED.drawRect(11, 54, 20, 63, false); delay(200); // 9 myOLED.drawRect(22, 0, 31, 9, true); delay(200); // A myOLED.drawRect(107, 0, 116, 9, false); delay(200); // B myOLED.drawImage(Img_Logo, OLED_R, OLED_B); delay(200); // Выводим картинку Img_Logo в правом нижнем углу экрана. myOLED.invText(true); myOLED.bgText(false); // Инвертируем цвет текста и запрещаем выводить цвет фона. myOLED.print(F("ТЕТРИС") , OLED_C, OLED_C); // Выводим текст по центру экрана. Текст будет чёрным, а белый фон не отобразится. Таким образом мы закрасим предыдущий текст. myOLED.setFont(SmallFontRus); // Указываем шрифт который требуется использовать для вывода цифр и текста. myOLED.invText(false); myOLED.bgText(true); // Возвращаем нормальный цвет текста и разрешаем выводить цвет фона. myOLED.print(F("www.iarduino.ru") , OLED_C, OLED_C); // Выводим текст по центру экрана. myOLED.autoUpdate(false); // Запрещаем автоматический вывод данных без обращения к функции myOLED.update(). } // // // Прорисовка игрового поля: // Значения возвращаемые функцией: нет. void showGameScreen(void){ // Аргументы принимаемые функцией: нет. myOLED.clrScr(); // Чистим экран OLED дисплея. myOLED.setFont(SmallFontRus); // Указываем шрифт который требуется использовать для вывода цифр и текста. myOLED.drawRect(0,0,tableCols*(tableCubeSize+1)+2,tableRows*(tableCubeSize+1)+2); // Выводим рамку игрового стола. myOLED.print(F("УРОВЕНЬ:") , scrBoundary, 7); // Выводим текст в указанную позицию на экране. myOLED.print(level); // Выводим текущий уровень сразу после текста. myOLED.print(F("СЧЁТ:") , scrBoundary, 20); // Выводим текст в указанную позицию на экране. myOLED.print(points); // Выводим текущий счёт сразу после текста. myOLED.update(); // Обновляем информацию на экране OLED дисплея. } // // // Прорисовка или затирание фигуры: // Значения возвращаемые функцией: нет. void showFigure(bool i, bool j){ // Аргументы принимаемые функцией: i - флаг (1-фигура на игровом столе, 0-будущая фигура), j - флаг (1-прорисовать фигуру, 0-затереть фигуру). int8_t x0, y0; // Объявляем переменные для хранения координат верхнего левого угла массива фигуры в пикселях. int8_t x1, y1; // Объявляем переменные для хранения координат верхнего левого угла кубика фигуры в пикселях. int8_t s = i ? tableCubeSize: newCubeSize; // Определяем размер кубиков фигуры. if(i){ // Если первый аргумент функции равен 1, то... // Прорисовываем или затираем фигуру на игровом столе: // x0 = 2+figurePos*(s+1); // Определяем координату левого угла массива фигуры в пикселях. y0 = 2+figurePos*(s+1); // Определяем координату верхнего угла массива фигуры в пикселях. for(uint8_t row=0; row<5; row++){ y1=row*(s+1); // Проходим по клеткам фигуры сверху вниз (строкам). for(uint8_t col=0; col<5; col++){ x1=col*(s+1); // Проходим по клеткам фигуры справа на лево (колонкам). if(bitRead(figureArray, 4-col)){ // Если бит 4-col элемента row массива figureArray равен 1, значит кубик есть, тогда... if(y0+y1>1){ // Если позиция кубика находится в области игрового стола, тогда... myOLED.drawRect(x0+x1, y0+y1, x0+x1+s-1, y0+y1+s-1, true, j); // Прорисовываем закрашенный кубик на игровом столе. }}}} // myOLED.update(); // Обновляем информацию на экране OLED дисплея. }else{ // Если первый аргумент функции равен 0, то... // Прорисовываем или затираем будущую фигуру: // x0 = scrBoundary; // Определяем координату левого угла массива фигуры в пикселях. y0 = 30; // Определяем координату верхнего угла массива фигуры в пикселях. if(j){ // Если второй аргумент функции равен 1, то... // Прорисовываем будущую фигуру: // for(uint8_t row=0; row<5; row++){ y1=row*(s+1); // Проходим по клеткам фигуры сверху вниз (строкам). for(uint8_t col=0; col<5; col++){ x1=col*(s+1); // Проходим по клеткам фигуры справа на лево (колонкам). if(bitRead(figureArrayNew, 4-col)){ // Если бит 4-col элемента row массива figureArrayNew равен 1, значит кубик есть, тогда... myOLED.drawRect(x0+x1, y0+y1, x0+x1+s-1, y0+y1+s-1); // Прорисовываем не закрашенный квадрат белого цвета. }}} // }else{ // Если второй аргумент функции равен 0, то... // Затираем место под будущую фигуру: // myOLED.drawRect(x0, y0, x0+5*(s+1), y0+5*(s+1), true, 0); // Прорисовываем закрашенный квадрат черного цвета покрывая всю площадь предназначенную для вывода будущих фигур. } // myOLED.update(); // Обновляем информацию на экране OLED дисплея. } // } // // // Прорисовка всех кубиков в клетках игрового стола: // Значения возвращаемые функцией: нет. void showTable(){ // Аргументы принимаемые функцией: нет. int8_t x, y; // Объявляем переменные для хранения координат верхнего левого угла клетки игрового стола в пикселях. int8_t s = tableCubeSize; // Определяем размер клеток игрового стола. for(uint8_t row=0; row= tableRows){return false;} // Если позиция кубика фигуры превышает количество строк (клеток по вертикали) игрового стола, значит положение фигуры некорректно (фигура вышла за нижнюю границу игрового стола). if(x-(4-col) >= tableCols){return false;} // Если позиция кубика фигуры превышает количество столбцов (клеток по горизонтали) игрового стола, значит положение фигуры некорректно (фигура вышла за левую границу игрового стола). if(x-(4-col) < 0){return false;} // Если позиция кубика фигуры находится в отрицательном столбце (клетке) игрового стола, значит положение фигуры некорректно (фигура вышла за правую границу игрового стола). if(bitRead(tableArray,x-(4-col))){return false;} // Если позиция кубика фигуры наложилась на кубик в клетке игрового стола, значит положение фигуры некорректно (фигура наложилась на уже имеющуюся фигуру игрового стола). } // } // } // return true; // } // // // Проверка заполненных строк на игровом столе: // Значения возвращаемые функцией: функция вернёт true если на игровом столе есть полностью заполненные строки. bool checkTable(){ // Аргументы принимаемые функцией: нет. // Добавляем фигуру игрового стола в массив клеток игрового стола: // for(uint8_t row=0; row<5; row++){ // Проходим по клеткам фигуры сверху вниз (строкам). for(uint8_t col=0; col<5; col++){ // Проходим по клеткам фигуры справа на лево (колонкам). if(bitRead(figureArray, col)){ // Если бит col элемента row массива figureArrayNew равен 1, значит кубик есть, тогда... bitSet(tableArray,tableCols-figurePos-(4-col)-1); // Устанавливаем в 1 бит массива tableArray соответствующий клетке игрового стола на которой находится кубик фигуры. } // } // } // // Проверяем игровой стол на наличие полных строк: // uint32_t fullRows = 0; // Определяем переменную, значение которой будет эталоном для заполненной строки игрового стола. for(uint8_t i=0; i0; j--){tableArray[j]=tableArray;} // Сдвигаем все строки находящиеся выше вниз, на одну строку (клетку). } // } // showTable(); // Прорисовываем игровой стол с удалёнными пустыми строками. } // return sum; // Возвращаем количество удалённых пустых строк. } //

Алгоритм работы:

  • В начале скетча (до кода setup) выполняются следующие действия:
    • Подключаем графическую библиотеку iarduino_OLED для работы с Trema OLED дисплеем.
    • Объявляем объект myOLED указывая адрес дисплея на шине I2C, он должен совпадать с адресом установленным переключателем на обратной стороне платы OLED дисплея.
    • Объявляем константы pinBtnL, pinBtnR, pinBtnT, pinBtnD, pinSeed с указанием номеров выводов Arduino, которые будут задействованы в скетче.
    • Объявляем константы GAME_OFF, GAME_ON, GAME_OVER для удобочитаемости скетча.
    • Подключаем шрифты и картинки предустановленные в библиотеке myOLED.
    • Определяем константы с настраиваемыми значениями. Меняя эти значения можно менять размеры игрового стола, размеры фигур, скорость игры и время «залипания» кнопок.
    • Объявляем массивы и переменные участвующие в работе скетча.
    • Объявляем функции используемые в скетче.
  • В коде setup выполняются следующие действия:
    • Инициируем работу с Trema OLED дисплеем и запрещаем автоматический вывод данных.
    • Указываем кодировку текста в скетче (если требуется).
    • Конфигурируем выводы к которым подключены кнопки.
    • Готовим корректную работу функции random() для генерации псевдослучайных чисел.
    • Выводим анимированное приветствие (текст «Тетрис» с появляющимися фигурами).
    • Переводим состояние игры в GAME_OFF «Не играем» (ждём нажатие любой кнопки).
  • В коде loop сначала выполняется чтение состояний кнопок, после чего выполняется 1 из 3 частей:
    • «Не играем» - эта часть кода ожидает нажатия на любую кнопку. Если любая кнопка будет нажата, будут подготовлены переменные, прорисуется игровое поле и игра перейдёт в состояние GAME_ON «Играем».
    • «Играем» - эта часть кода является основной. Здесь в 18 строках кода реализован весь алгоритм игры. Он более подробно описан ниже.
    • «Игра завершена» - эта часть кода содержит анимацию закраски и очистки игрового стола, вывод текста «КОНЕЦ ИГРЫ», вывод анимированного приветствия и перевод игры в состояние GAME_OFF «Не играем».
  • Весть алгоритм игры полностью реализован в разделе «Играем» который состоит из 4 частей:
    (каждая часть этого раздела заключена в тело оператора if).
    • Первая часть сдвигает фигуру на игровом столе влево. Код в теле оператора if выполняется только при нажатии на кнопку Left. Единственная функция shiftFigure() в теле оператора if, выполняет сдвиг фигуры игрового стола на одну клетку. Параметр функции равный 1 указывает сдвинуть фигуру влево.
    • Вторая часть сдвигает фигуру на игровом столе вправо. Код в теле оператора if выполняется только при нажатии на кнопку Right. Единственная функция shiftFigure() в теле оператора if, выполняет сдвиг фигуры игрового стола на одну клетку. Параметр функции равный 2 указывает сдвинуть фигуру вправо.
    • Третья часть выполняет поворот фигуры на игровом столе. Код в теле оператора if выполняется только при нажатии на кнопку Turn. Единственная функция turnFigure() в теле оператора if, выполняет поворот фигуры на 90°. Первый параметр функции равный 1 указывает что повернуть требуется фигуру на игровом столе.
    • Четвёртая часть выполняет сдвиг фигуры игрового стола на одну клетку вниз. Код в теле оператора if выполняется как от нажатия на кнопку Down, так и по достижении времени tmrShift. Параметр функции равный 3 указывает сдвинуть фигуру вниз. Отпускание кнопки Down не заблокирует выполнение кода в теле оператора if при следующем проходе цикла loop (Если нажать и отпустить кнопку Down, то фигура будет сдвигаться вниз пока не достигнет дна или других фигур).
      Код в теле оператора If выполняет следующие действия:
      • Обновляем время tmrShift для следующего сдвига фигуры вниз на игровом столе.
      • Сдвигаем фигуру игрового стола на 1 клетку вниз. Проверяя не достигла ли фигура дна игрового стола или другой фигуры на игровом столе. Если не достигла, то на этом выполнение данного участка кода будет закончено.
      • Если фигура достигла дна игрового стола или другой фигуры (закончила падение), то выполняются следующие действия:
        • Проверяем наличие заполненных строк игрового стола, если они есть, то они удаляются, со сдвигом всего что находится выше и добавлением бала игроку.
        • Сбрасываем флаг нажатия на кнопку Down (на случай если фигура падала принудительно)
        • Обновляем текущий уровень игры в соответствии со значением счётчика созданных фигур.
        • Увеличиваем счётчик созданных фигур.
        • Выводим номер текущего уровня игры и количество набранных баллов.
        • Создаём будущую фигуру с выводом её изображения в поле справа от игрового стола, а ту фигуру которая ранее находилась в этом поле переносим на верх игрового стола (она станет новой фигурой в игре).
        • Если новую фигуру не удалось разместить на игровом столе (ей мешают другие фигуры), значит игра закончена, переводим состояние игры в GAME_OVER «Игра завершена».
      • Действия перечисленные выше выполняются вызовом функций:
        • shiftFigure(); - выполняет сдвиг фигуры на одну клетку игрового стола и возвращает false если сдвиг невозможен.
        • turnFigure(); - поворачивает фигуру игрового стола на 90°.
        • checkTable(); - проверяет наличие заполненных строк игрового стола, возвращая true или false.
        • deletTableRows(); - удаляет все заполненные строки с игрового стола, возвращая количество удалённых строк.
        • createNewFigure(); - создаёт будущую фигуру а предыдущую будущую фигуру делает новой на игровом столе, возвращая false если не удалось вставить фигуру на игровой стол.

Все строки скетча (кода программы) прокомментированы, так что Вы можете подробнее ознакомиться с кодом прочитав комментарии строк.

  • Сергей Савенков

    какой то “куцый” обзор… как будто спешили куда то