Привет, Хабр! Так получилось, что одним из моих увлечений являются интеллектуальные игры. Это «Что? Где? Когда?», «Своя игра», «Эрудит-квартет», «Брейн-ринг» и прочие. И вот, однажды мне захотелось сделать своими руками систему для этой игры. Если вам интересен процесс создания с нуля такого устройства — приглашаю под кат.
Предыстория
Коротко о правилах «Своей игры»:
Группа игроков (как правило, до 4-х человек) садится за игровые места. Объявляется тема, состоящая из пяти вопросов. Вопросы идут по возрастанию сложности и соответственно оцениваются — от 10 до 50 баллов. Ведущий объявляет номинал вопроса и начинает его зачитывать. В любой момент чтения вопроса игрок подает сигнал, после которого ведущий прекращает читать вопрос и начинает отсчет времени — подавшему сигнал игроку дается 3-5 секунд на ответ. В случае правильного и своевременного ответа, игрок получает количество баллов, равное номиналу вопроса. В ином случае у игрока отнимается это количество баллов (Итоговый счет может быть глубоко отрицательным).
Сигналом в этой игре может быть все что угодно — от хлопка в ладоши в простейшем случае, до нажатия кнопки специализированной системы для «Своей игры». Подавляющее большинство таких систем являются самодельными (не настолько интеллектуальные игры популярны, чтобы ставить это дело на поток). Кроме того, эти системы зачастую обладают различными недостатками.
Перечислю некоторые из недостатков в системах, с которыми мне приходилось иметь дело:
- большие габариты;
- работа только от сети 220В;
- работа только в связке с компьютером (взаимодействие со специальным Windows-приложением);
- некачественные, залипающие кнопки (например, от дверных звонков);
В нашем городе тренировки по интеллектуальным играм проходят каждую неделю, по воскресеньям. Причем, в зависимости от времени года и погоды, мы можем собраться в местном парке — это намного приятнее, чем коптиться в душном помещении. Из-за своих недостатков, имеющиеся в нашем распоряжении готовые системы было просто лень приносить из дому. Поэтому, зачастую приходилось играть «на хлопки». Точность определения первого, успевшего подать сигнал, оставляла желать лучшего. Часто бывало, что мнения присутствующих по этому поводу разделялись — казалось, что первым хлопнул тот, кто стоит к тебе ближе. Все эти факторы натолкнули меня на мысль о создании своей реализации игровой системы. Да и вообще, было интересно получить опыт создания устройства с нуля — от проекта до готового изделия.
Проект
Для начала, я решил определиться с основной концепцией устройства. Для себя отметил принципиальные пункты, которые должны быть реализованы в системе:
- Удобные пульты с надежными кнопками и светодиодной индикацией (подтверждение игроку, что он первый нажал на кнопку);
- Разъемное соединение пультов и устройства. Решено было сделать разъемы как на пультах, так и на самой системе. Был выбран разъем 4P4C (RJ11, обычно применяется в телефонных аппаратах). Такой выбор был сделан из-за небольших размеров и простоты ремонта кабеля — при наличии обжимного инструмента и коннекторов проблема решается в считанные минуты;
- Два возможных источника питания — внешний и внутренний. Забегая вперед — именно в реализации схемы питания я совершил наибольшее число ошибок;
- Система должна иметь разъем внутрисхемного программирования (ISP), чтобы можно было без лишних хлопот менять прошивку микроконтроллера;
- Не должно быть привязки к любым другим устройствам (компьютер и т.п.);
- Небольшие габариты.
В качестве «мозгов» устройства я выбрал чрезвычайно популярный у радиолюбителей микроконтроллер от Atmel — Atmega8. Хотя для функционала системы с головой хватило бы какого-нибудь Attiny, у него был фатальный недостаток — он не лежал у меня в столе. В отличие от вышеупомянутой Atmega.
Затем я на скорую руку набросал приблизительную принципиальную схему:
В качестве внешнего источника питания был выбран валявшийся без дела блок питания от старого модема — на выходе он имеет 12 В переменного напряжения. В роли внутреннего источника я выбрал батарейку типа «Крона» — ошибка №1. Для выработки необходимых для микроконтроллера 5 В я поставил линейный стабилизатор LM7805 — ошибка №2. О том, что это ошибки, я на практике узнал уже после сборки и тестирования устройства. Оказалось, что у «Кроны» весьма низкая емкость, да еще и почти половина ее уходила в никуда — на подогрев линейного стабилизатора. Однако, об этом позже.
Кнопки и разъемы, которые я выбрал для устройства, пришлось покупать на ebay — уж слишком высокие цены запрашивали местные торговцы. Правда, 10-миллиметровые светодиоды таки пришлось приобрести на радиорынке — ни на ebay, ни на aliexpress я не нашел индикаторных разноцветных светодиодов нужного диаметра.
Одним из важнейших и определяющих дальнейшее действие шагов был выбор корпуса. В своем городе ничего подходящего мне по качеству найти не удалось — пришлось воспользоваться торговыми площадками в Интернете. Устраивающий меня вариант нашелся в Киеве, там и были заказаны 5 корпусов — для основного блока и для пультов.
И вот, имея на руках все необходимые элементы, я стал продумывать дизайн устройства.
Печатные платы
Имея на руках корпуса, я приступил к разработке печатных плат. Вот, что у меня вышло:
Печатная плата основного блока
Печатная плата блока светодиодов
Печатная плата кнопки
Макеты печатных платы создавались в программе Sprint Layout, в конце будут ссылки на исходники. Используйте, модифицируйте — в общем, делайте с ними все что хотите.
Сами печатные платы я делал методом ЛУТ (лазерно-утюжная технология) — по моему скромному мнению, это наиболее простой и доступный простому смертному способ сделать печатную плату приемлемого качества.
Код
Тут, собственно, листинг программы микроконтроллера. Возможно, код не самый изящный, но он работает и свою функцию выполняет на ура.
Код программы на языке C
#define F_CPU 4000000UL
#define f 349
#define a 440
#define cH 523
#include <avr/io.h> // Ввод/вывод, все стандартно
#include <util/delay.h> // Нам будут нужны временные задержки
#include <avr/interrupt.h> // Без прерываний тоже никуда
// Гасим засветившиеся светодиоды
void clear_led(void)
{
PORTB = 0b00110000;
PORTC = 0xFF;
}
int blink_led(int num)
{
clear_led();
_delay_ms(100);
PORTB |= (1<<num);
_delay_ms(100);
clear_led();
_delay_ms(100);
PORTB |= (1<<num);
_delay_ms(100);
clear_led();
_delay_ms(100);
PORTB |= (1<<num);
_delay_ms(100);
clear_led();
_delay_ms(100);
PORTB |= (1<<num);
_delay_ms(100);
clear_led();
_delay_ms(100);
PORTB |= (1<<num);
_delay_ms(100);
return 0;
}
// Голос нашей программы, аргументы функции - длительность и тон соответственно
void song(void)
{
_delay_ms(500);
int T = 1000000/440;
int k = 500000/T;
int i = 0;
int tempo = 1;
while(i<k)
{
PORTD = 0b11111111; // Здесь мы делаем меандр: ставим высокий уровень на ножке
_delay_us(T/2); // Ждем некоторое время
PORTD = 0b01111111; // Ставим низкий уровень
_delay_us(T/2); // Снова ждем. Вот и готова последовательность прямоугольных импульсов
i++;
}
PORTB |= (1<<0);
PORTB |= (1<<1);
PORTB |= (1<<2);
PORTB |= (1<<3);
PORTD = 0b01111111;
_delay_ms(500/tempo); // Завершить все же лучше тишиной :)
T = 1000000/440;
k = 500000/T;
i = 0;
while(i<k)
{
PORTD = 0b11111111; // Здесь мы делаем меандр: ставим высокий уровень на ножке
_delay_us(T/2); // Ждем некоторое время
PORTD = 0b01111111; // Ставим низкий уровень
_delay_us(T/2); // Снова ждем. Вот и готова последовательность прямоугольных импульсов
i++;
}
clear_led();
_delay_ms(500/tempo); // Завершить все же лучше тишиной :)
T = 1000000/440;
k = 500000/T;
i = 0;
while(i<k)
{
PORTD = 0b11111111; // Здесь мы делаем меандр: ставим высокий уровень на ножке
_delay_us(T/2); // Ждем некоторое время
PORTD = 0b01111111; // Ставим низкий уровень
_delay_us(T/2); // Снова ждем. Вот и готова последовательность прямоугольных импульсов
i++;
}
PORTB |= (1<<0);
PORTB |= (1<<1);
PORTB |= (1<<2);
PORTB |= (1<<3);
PORTD = 0b01111111;
_delay_ms(500/tempo); // Завершить все же лучше тишиной :)
T = 1000000/349;
k = 350000/T;
i = 0;
while(i<k)
{
PORTD = 0b11111111; // Здесь мы делаем меандр: ставим высокий уровень на ножке
_delay_us(T/2); // Ждем некоторое время
PORTD = 0b01111111; // Ставим низкий уровень
_delay_us(T/2); // Снова ждем. Вот и готова последовательность прямоугольных импульсов
i++;
}
PORTD = 0b01111111;
_delay_ms(350/tempo); // Завершить все же лучше тишиной :)
T = 1000000/523;
k = 150000/T;
i = 0;
while(i<k)
{
PORTD = 0b11111111; // Здесь мы делаем меандр: ставим высокий уровень на ножке
_delay_us(T/2); // Ждем некоторое время
PORTD = 0b01111111; // Ставим низкий уровень
_delay_us(T/2); // Снова ждем. Вот и готова последовательность прямоугольных импульсов
i++;
}
PORTD = 0b01111111;
_delay_ms(150/tempo); // Завершить все же лучше тишиной :)
T = 1000000/440;
k = 500000/T;
i = 0;
while(i<k)
{
PORTD = 0b11111111; // Здесь мы делаем меандр: ставим высокий уровень на ножке
_delay_us(T/2); // Ждем некоторое время
PORTD = 0b01111111; // Ставим низкий уровень
_delay_us(T/2); // Снова ждем. Вот и готова последовательность прямоугольных импульсов
i++;
}
PORTD = 0b01111111;
_delay_ms(500/tempo); // Завершить все же лучше тишиной :)
T = 1000000/349;
k = 350000/T;
i = 0;
while(i<k)
{
PORTD = 0b11111111; // Здесь мы делаем меандр: ставим высокий уровень на ножке
_delay_us(T/2); // Ждем некоторое время
PORTD = 0b01111111; // Ставим низкий уровень
_delay_us(T/2); // Снова ждем. Вот и готова последовательность прямоугольных импульсов
i++;
}
PORTD = 0b01111111;
_delay_ms(350/tempo); // Завершить все же лучше тишиной :)
T = 1000000/523;
k = 150000/T;
i = 0;
while(i<k)
{
PORTD = 0b11111111; // Здесь мы делаем меандр: ставим высокий уровень на ножке
_delay_us(T/2); // Ждем некоторое время
PORTD = 0b01111111; // Ставим низкий уровень
_delay_us(T/2); // Снова ждем. Вот и готова последовательность прямоугольных импульсов
i++;
}
PORTD = 0b01111111;
_delay_ms(150/tempo); // Завершить все же лучше тишиной :)
T = 1000000/440;
k = 1000000/T;
i = 0;
while(i<k)
{
PORTD = 0b11111111; // Здесь мы делаем меандр: ставим высокий уровень на ножке
_delay_us(T/2); // Ждем некоторое время
PORTD = 0b01111111; // Ставим низкий уровень
_delay_us(T/2); // Снова ждем. Вот и готова последовательность прямоугольных импульсов
i++;
}
PORTB |= (1<<0);
PORTB |= (1<<1);
PORTB |= (1<<2);
PORTB |= (1<<3);
PORTD = 0b01111111;
_delay_ms(1000); // Завершить все же лучше тишиной :)
clear_led();
}
int beep(int k, int j) //функция бипа
{
int i = 0;
while(i<k)
{
PORTD = 0b11111111; // Здесь мы делаем меандр: ставим высокий уровень на ножке
_delay_us(j); // Ждем некоторое время
PORTD = 0b01111111; // Ставим низкий уровень
_delay_us(j); // Снова ждем. Вот и готова последовательность прямоугольных импульсов
i++;
}
PORTD = 0b01111111; // Завершить все же лучше тишиной :)
return 0;
}
void reset_wait(void)
{
short t = 0;
while(t==0)
{
if(!(PIND & (1<<PIND4)))
{
_delay_ms(5);
if(!(PIND & (1<<PIND4)))
{
t = 1;
clear_scr();
clear_led();
}
}
}
}
int bond007(void) // Функция-шпион для отслеживания нажатия клавиш
{
short i = 0;
short p = 0;
int t = 300;
int tone = 100;
while(i==0) // Цикл отслеживания нажатия кнопок
{ // Открывающие скобки цикла отслеживания нажатий кнопок
if(!(PIND & (1<<PIND0)) || !(PIND & (1<<PIND1)) || !(PIND & (1<<PIND2)) || !(PIND & (1<<PIND3)) || !(PIND & (1<<PIND6)))
//Если хотя бы одна из кнопок игроков нажата
{ // Открывающие скобки "Если хотя бы одна из кнопок игроков нажата"
_delay_ms(5); // Защита от дребезга
if(!(PIND & (1<<PIND0))) // Проверка нажатия кнопки игрока 1
{ // Открывающая скобка "Проверка нажатия кнопки игрока 1"
p = beep(t, tone); // Пищим
i=1; // Флаг завершения цикла отслеживания нажатия кнопки
clear_led();
p = blink_led(0);
reset_wait();
i=0;
} // Закрывающая скобка "Проверка нажатия кнопки игрока 1"
else
{
if(!(PIND & (1<<PIND1))) //Проверка нажатия кнопки игрока 2, остальное все аналогично
{
p = beep(t, tone);
i=1;
clear_led();
p = blink_led(1);
reset_wait();
i=0;
}
else
{
if(!(PIND & (1<<PIND2))) //Проверка нажатия кнопки игрока 3
{
p = beep(t, tone);
i=1;
clear_led();
p = blink_led(2);
reset_wait();
i=0;
}
else
{
if(!(PIND & (1<<PIND3))) //Проверка нажатия кнопки игрока 4
{
p = beep(t, tone);
i=1;
clear_led();
p = blink_led(3);
reset_wait();
i=0;
}
}
}
}
} // Закрывающие скобки "Если хотя бы одна из кнопок игроков нажата"
} // Закрывающие скобки цикла отслеживания нажатий кнопок
return i;
}
int main(void) // Главная функция
{ // Открывающие скобки главной функции
DDRC = 0xFF; // Порт С на выход
PORTC = 0xFF; // Подтягивающие резисторы вкл. на порт С
DDRD = 0b10000000; // порт Д на вход, кроме 8й ножки, она на выход у нас, пищалка там
PORTD = 0b01111111; // подтягивающие резисторы на весь порт Д, окромя 8го бита, ибо на нем динамик у нас.
DDRB = 0xFF; // Порт Б на выход
PORTB = 0b00110000; // Подтягивающие резисторы на 5 и 6 биты порта Б
song();
short i = 0;
while(1) // Вечный цикл памяти погибших на Клендату
{ // Открывающие скобки вечного цикла
i = bond007();
} // Закрывающие скобки вечного цикла
return 0;
}
Если вдруг случится, что кто-то откроет кат и посмотрит на код, у этого человека наверняка возникнет вопрос — что за ерунда занимает большую его часть. Дело в том, что мне захотелось внести в устройство какую-нибудь изюминку, и в качестве этой изюминки я выбрал приветствие при включении. Сразу после того, как первые электроны побегут по цепи, эта черная коробочка с разноцветными светодиодами и красной кнопкой начинает весело играть отрывок из всем известного «Имперского марша».
Вот как это выглядит:
Кроме самой мелодии, на этом коротком видео можно лицезреть пример работы системы: по нажатию кнопки загорается соответствующий светодиод на основном устройстве и индикатор. В таком состоянии система блокируется до нажатия ведущим красной кнопки сброса системы. Вот, собственно, и вся логика работы.
Ошибки в проекте
Как я уже писал выше, основные ошибки на этапе проектирования касались системы питания устройства. Ужасный КПД линейного стабилизатора, который буквально превращает в тепло «лишние» 4В при питании от «Кроны» — апофеоз энергонеэффективности. Да и сама «Крона» — далеко не лучший выбор. Этот элемент питания обладает малой емкостью (около 600 мА*ч), и его хватает очень ненадолго. С такой схемой питания система во время первого тестирования в боевых условиях проработала не больше часа. Меня это абсолютно не устраивало, поэтому пришлось переделывать эту часть схемы.
Незадолго до этого проекта я познакомился с литий-ионными аккумуляторами форм-фактора 18650. Они зарекомендовали себя с наилучшей стороны. Как минимум, даже у дешевых китайских аккумуляторов емкость в пересчете на единицу объема была значительно выше, чем у «Кроны».
Однако, с выбором этих элементов питания сразу появлялась другая проблема. Номинальное напряжение на таком аккумуляторе — 3.7 В. А этого недостаточно, чтобы запитать Atmega8. На помощь снова пришли ушлые китайцы — чтобы получить заветные 5 В я взял лежавший без дела повышающий преобразователь на LM2577. Вырвав с корнем злополучную LM7805 (оставшиеся в плате ножки можно будет увидеть на фото, размещенные дальше), я внедрил в систему питания аккуратную схемку, созданную трудолюбивыми жителями КНР.
Кроме того, на время тестирования этого варианта схемы я решил отказаться от возможности подключения внешнего источника питания. Полевые тесты прошли на ура — после многих часов эксплуатации не было никаких признаков разряда аккумулятора или просадки напряжения («Крона» просадки давала — видимо, не могла отдавать необходимый схеме ток). Я решил продолжать тесты до тех пор, пока мой доблестный noname 18650 не откажется запускать схему.
Кстати, в описании лота при покупке была заявлена емкость около 3700 мА*ч — очень самоуверенно даже для китайцев, учитывая стоимость одной банки в районе 3$. Но вышло так, что за несколько месяцев работы (что говорит еще и о низком саморазряде аккумуляторов) батарейка так и не села. Потому я сдался раньше и зарядил аккумулятор для безотказной работы устройства на одном важном мероприятии, о котором будет упомянуто позже.
Фотографии
Здесь привожу фото получившегося аппарата, в том числе и вид изнутри. Это для того, чтобы было видно, что все сделано по-честному — никакой неонки внутре там нет.
Итог
Система получилась работоспособная, выполняющая свою функцию на 100%. Помимо тренировок, она была протестирована на ЧУСИ-2013 (Чемпионат Украины по «Своей игре» 2013-го года), на котором ваш покорный слуга был одним из ведущих.
Это был замечательный опыт, который, как известно, сын ошибок трудных. В итоговой реализации устройство потеряло часть функций, которые задумывались изначально. Например, я отказался от возможности подключения внешнего блока питания — даже при активном использовании системы, посредственного аккумулятора хватает на многие месяцы.
Ниже прилагаю ссылки, по которым можно скачать макеты печатных плат (в формате .lay) и готовую прошивку для микроконтроллера. Буду рад, если кому-нибудь пригодятся мои наработки.
Печатная плата основной части;
Печатная плата блока светодиодов;
Печатная плата кнопки;
Прошивка.