Вступление
При свете дня, а затем и во сне, возникла у меня идея создания собственной регламентированной тв-приставки. Собственно, тут-то открылся передо мной богатый и насыщенный мир радиотехники. Так как ранее я не имел дела с серьезной разработкой электроники, мой выбор пал на более простой вариант — Arduino и ее самая распространенная модель — Uno.
План работы
1. Разобраться с библиотекой
2. Спаять плату видео вывода
3. Написать код
4. Вырезать корпус
Финальная внешняя составляющая не особо важна в случае с подобными проектами.
Шаг 1. Разбираемся, что к чему
После нескольких десятков минут отчаянного гугления пришел к выводу, что создать приставку даже типа Денди у меня не получится. Ну, что тут делать, раз взялся, буду доводить дело до конца.
На сайте, посвященному проектам на Ардуино и вообще радиоэлектронике в целом (не реклама) нашел статью о подобной затее. Было решено использовать библиотеку TVout, так как приставка тв-шная. Для ее установки и работы пришлось немного пошаманить.
Функции установки режима
Функция begin() инициализирует вывод видеосигнала (разрешение экрана по умолчанию 128x96).
Синтаксис:
TVOut.begin(mode);
TVOut.begin(mode, x, y);
Параметры:
mode – стандарт видеосигнала:
_PAL – режим PAL;
_NTSC – режим NTSC.
Возвращаемое значение:
0 – в случае удачного соединения, 4 – в случае неудачи (недостаточно памяти для буфера вывода).
Функции задержки
Функция delay() осуществляет задержку выведенного изображения.
Синтаксис:
TVOut.delay(ms);
Параметры:
ms – задержка в мс с точностью: 20 мс для PAL и 16 мс для NTSC.
Функция delay_frame() осуществляет задержку выведенного изображения.
Синтаксис:
TVOut.delay_frame(frames);
Параметры:
frames – количество кадров для задержки…
Функция полезна для сведения к минимуму или устранения на мерцание экрана, вызванные обновлением экрана.
Функции получения параметров
Функция hres() возвращает горизонтальное разрешение экрана.
Синтаксис:
TVOut.hres();
Параметры:
нет.
Возвращаемое значение:
unsigned char – горизонтальное разрешение экрана.
Функция vres() возвращает вертикальное разрешение экрана.
Синтаксис:
TVOut.vres();
Параметры:
нет.
Возвращаемое значение:
unsigned char – вертикальное разрешение экрана.
Функция char_line() возвращает максимально возможное количество символов в одной строке при выводе текстовой информации.
Синтаксис:
TVOut. char_line();
Параметры:
нет.
Возвращаемое значение:
unsigned char – количество символов.
Основные графические функции
Функция set_pixel() устанавливает цвет пикселя экрана в точке с заданными координатами.
Синтаксис:
TVOut.set_pixel(x,y,color);
Параметры:
x,y – координаты пикселя;
color – цвет пикселя:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция get_pixel() получает цвет пикселя экрана из точки с заданными координатами.
Синтаксис:
TVOut.get_pixel(x,y);
Параметры:
x,y – координаты пикселя.
Возвращаемое значение:
color – цвет пикселя:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция fill() заполняет экран заданным цветом.
Синтаксис:
TVOut.fill(color);
Параметры:
color – цвет заполнения:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция clear_screen() очищает экран, заполняя заданным цветом.
Синтаксис:
TVOut.clear_screen(color);
Параметры:
color – цвет заполнения:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция invert() инвертирует содержимое экрана.
Синтаксис:
TVOut.invert();
Параметры:
нет.
Функция shift_direction() сдвигает содержимое экрана.
Синтаксис:
TVOut.shift_direction(distance, direction);
Параметры:
distance – расстояние для сдвига содержимого экрана.
direction – направление сдвига:
UP=0 – вверх;
DOWN=1 – вниз;
LEFT=2 – влево;
RIGHT=3 – вправо.
Функция draw_line() соединяет на экране линией две точки.
Синтаксис:
TVOut.draw_line(x0,y0,x1,y1,color);
Параметры:
x0,y0 – координаты первой точки;
x1,y1 – координаты второй точки;
color – цвет заполнения:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция draw_row() заполняет строку указанным цветом между двумя точками строки.
Синтаксис:
TVOut.draw_row(row,x0,x1,color);
Параметры:
row – вертикальная координата строки;
x1,x2 – горизонтальный координаты точек строки;
color – цвет заполнения:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция draw_column() заполняет строку указанным цветом между двумя точками столбца.
Синтаксис:
TVOut.draw_column(column,y0,y1,color);
Параметры:
column – горизонтальная координата столбца;
y1,y2 – вертикальные координаты точек столбца;
color – цвет заполнения:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция draw_rect() рисует на экране прямоугольник.
Синтаксис:
TVOut.draw_rect(x,y,w,h,color);
TVOut.draw_rect(x,y,w,h,color,fillcolor);
Параметры:
x,y – координаты левой верхней точки;
w,h – ширина и высота рисуемого прямоугольника;
color – цвет границ прямоугольника:
0 – черный;
1 – белый;
2 – инвертировать цвет.
fillcolor – цвет заполнения прямоугольника:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция draw_circle() рисует на экране круг.
Синтаксис:
TVOut.draw_ circle(x,y,r,color);
TVOut.draw_ circle(x,y,r,color,fillcolor);
Параметры:
x,y – координаты центра круга;
r – радиус круга;
color – цвет границ круга:
0 – черный;
1 – белый;
2 – инвертировать цвет.
fillcolor – цвет заполнения круга:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция bitmap() выводит на экран растровое изображение.
Синтаксис:
TVOut.bitmap(x,y,bmp,w,h);
Параметры:
x,y – координаты левого верхнего угла точки вывода;
bmp – указатель на массив памяти, где хранится картинка;
w,h – ширина, высота выводимого изображения;
Ниже рассмотрим процесс создания кода выводимых растровых изображений.
Функции вывода текстовой информации
Для применения функций вывода текстовой информации требуетя подключение файлов с включенными в библиотеку или пользовательскими шрифтами. Для подключения пользовательского набора шрифтов необходимо в скетче подключить заголовочный файл:
#include
В состав библиотеки включены следующие наборы шрифтов:
font4x6;
font6x8;
font8x8;
font8x8ext.
Функция select_font() выбирает шрифт для вывода текстовой информации.
Синтаксис:
TVOut.select_font(font);
Параметры:
font – шрифт, подключенный в скетче.
Функция print_char() выводит символ на экран.
Синтаксис:
TVOut.print_char(x,y,char);
Параметры:
x,y – позиция на экране для вывода символа;
char – символ из текущего шрифта.
Функция set_cursor() устанавливает позицию курсора для вывода текстовой информации на экран.
Синтаксис:
TVOut.set_cursor(x,y);
Параметры:
x,y – координаты для курсора.
Функция print() выводит на экран строку, символ или число.
Синтаксис:
TVOut.print(x,y,string);
TVOut.print(x,y,char,base);
TVOut.print(x,y,int,base).
Параметры:
x,y – координаты курсора.
base – формат вывода:
BYTE = 0;
DEC = 10 (default);
HEX = 16.
Функция println() выводит на экран строку, символ или число и в конце символ перевода строки:
Синтаксис:
TVOut.println(x,y,string);
TVOut.println(x,y,char,base);
TVOut.println(x,y,int,base).
Параметры:
x,y – координаты курсора.
base – формат вывода:
BYTE = 0;
DEC = 10 (default);
HEX = 16.
Функции вывода аудио
Функции вывода звука позволяют отправлять на телевизор через аудиовыход сигнал определенной частоты.
Функция tone() выдает аудиосигнал определенной частоты.
Синтаксис:
TVOut.tone(frequency,duration);
TVOut.tone(frequency).
Параметры:
frequency – частота аудиосигнала;
duration – длительность сигнала.
Функция noTone() прекращает выдачу аудиосигнала.
Синтаксис:
TVOut.noTone().
Шаг 2. Паяем видеовывод
В первую очередь нам нужно спаять некую плату для вывода видеосигнала через композитный av-выход (RCA). Паяем по следующей схеме:
Расположим два резистора номиналом 470 ом и 1к ом параллельно друг другу и припаяем к ним «плюс» от кабеля-тюльпана. Далее отведем от резистора в 470 ом провод в седьмой пин на Arduino, т.к. он отвечает за вывод видео (video), а от резистора в 1к ом отведем провод в девятый пин, так как он отвечает за синхронизацию (sync). А «минус» от кабеля-тюльпана в «землю» на Arduino. Подробнее тут (англ.)
Шаг 3. Пишем код (игру)
Я не буду объяснять, что да как подключать, ведь необходимую информацию, как всегда, можно найти в интернете. Я описываю то, что очень сложно найти или этого вообще нет.
Начинаем с экрана приветствия, куда без него. Но тут встревает важный вопрос, как назвать сие чудо? Я пораскинул мозгами и придумал — Shimo. Звучит неплохо, даже технологично, по-китайски, конечно, но это не беда.
Дальше вернемся к самой игре. И снова сложный вопрос: какую игру делать? Так как я рукожоп не очень старательный и усердный человек, а также новичок, решил написать Пинг-понг.
Начинаем. Чертим линию через середину экрана с помощью TV.draw_line(60,0,60,96,1);. Появляется шарик ровно в центре экрана. Напишем функцию его движения void ballmove(int vel, int angle). Устанавливаем с помощью TV.set_pixel(x,y,1);, переменные я так и назвал.
Далее перед манипуляциями с шариком прописываем обновление экрана, а точнее, чтобы шарик не «наследил» на экране, поэтому при переходе на следующую позицию нужно закрашивать черным предыдущую. Для этого нам нужно прописать перед всем остальным TV.set_pixel(x,y,0);. После всех изменений переменных координат нужно прописать уже установку позиции и небольшую задержку — TV.delay(50);. Примерно вот так должно получиться:
void ballmove(int vel, int angle)
{
TV.set_pixel(x,y,0);
//Манипуляции с координатами
TV.set_pixel(x,y,1);
}
Теперь о самих изменениях координат. Всего восемь направлений (1-8), переменная int angle. А там уже просто, в зависимости от поворота, отнимаем или прибавляем к переменным какую-либо часть от int velocity. Я сделал так:
if(angle == 1)
{
y -= vel;
}
if(angle == 3)
{
x += vel;
}
if(angle == 5)
{
y += vel;
}
if(angle == 7)
{
x -= vel;
}
if(angle == 2)
{
x += round(vel/2);
y -= round(vel/2);
}
if(angle == 4)
{
x += round(vel/2);
y += round(vel/2);
}
if(angle == 6)
{
x -= round(vel/2);
y += round(vel/2);
}
if(angle == 8)
{
x -= round(vel/2);
y -= round(vel/2);
}
Теперь движения ракеток. Здесь важное уточнение — я использовал только координаты по y, так как позиции ракеток по x не изменяются. Прописываем следующую функцию void racketsmove(). Далее рисуем ракетки, переменные int yb1, int yb2, TV.draw_line(10, yb1+8, 10, yb1-8, 1); и TV.draw_line(110, yb2+8, 110, yb2-8, 1);. Обновление экрана, то есть «без следа», аналогично случаю с шариком.
Управление ракетками производится с кнопок. Подключаем кнопки, пины 2 и 3 — первая ракетка, 4 и 5 — вторая ракетка. Проверяем нажатие кнопок и изменяем координаты.
Вот такая функция:
void racketsmove()
{
TV.draw_line(10, yb1+8, 10, yb1-8, 0);
TV.draw_line(110, yb2+8, 110, yb2-8, 0);
if((yb1 - 8) > 1)
{
if(digitalRead(2) == HIGH)
{ yb1 -= 2;}
}
if((yb1 + 8) < 95)
{
if(digitalRead(3) == HIGH)
{yb1 += 2;}
}
if((yb2 - 8) > 1)
{
if(digitalRead(4) == HIGH)
{yb2 -= 2; }
}
if((yb2 + 8) < 95)
{
if(digitalRead(5) == HIGH)
{yb2 += 2;}
}
TV.draw_line(10, yb1+8, 10, yb1-8, 1);
TV.draw_line(110, yb2+8, 110, yb2-8, 1);
}
Сейчас снова вернемся к ball. Теперь пропишем его коллизию и отталкивание от стен и ракеток. Функция — void ballcol(). Для этого просто проверяем его местонахождение относительно объектов, а потом и его угол. Затем этот угол изменяем на другой. С углом легко угадать.
Угол отражения равен углу падения
Можно сделать некоторые физические исключения для определенных зон ракеток.
Функция:
void ballcol()
{
if(x == 1 || x == 119 || (x == 10 && y < (yb1 + 3) && y > (yb1 - 3)) || (x == 110 && y < (yb2 + 3) && y > (yb2 - 3)))
{
if(a==1){a=5;}else if(a==2){a=8;}else if(a==3){a=7;}else if(a==4){a=6;}else if(a==5){a=1;}else if(a==6){a=4;}else if(a==7){a=3;}else if(a==8){a=2;}
}
if(x == 10 && y < (yb1 - 3) && y > (yb1 - 8))
{
a = 2;
}
if(x == 10 && y > (yb1 + 3) && y < (yb1 + 8))
{
a = 4;
}
if(x == 110 && y < (yb2 - 3) && y > (yb2 - 8))
{
a = 8;
}
if(x == 110 && y > (yb2 + 3) && y < (yb2 + 8))
{
a = 6;
}
if(y == 95 || y == 1)
{
if(a==1){a=5;}else if(a==2){a=4;}else if(a==3){a=7;}else if(a==4){a=2;}else if(a==5){a=1;}else if(a==6){a=8;}else if(a==7){a=3;}else if(a==8){a=6;}
}
}
Самое сложное позади, можете успешно вздохнуть.
На данный момент нам остается только сделать систему подсчета баллов, таймер и рестарт.
Начнем с таймера. Есть переменная секунд float ts (в ней хранится абсолютно все время), переменная int tm (количество минут, которые мы получаем из ts). Задаем значение tm операцией tm = ts/60;. И выводим значения на экран, TV.print(81,1,tm); TV.print(97,1,"."); TV.print(100,1,int(ts-(tm*60)));.
Продолжим. Функция рестарта, называем void restart(). Здесь мы возвращаем изначальные значения переменных.
Код:
void restart()
{
TV.clear_screen();
x = 60;
y = 48;
yb1 = 48;
yb2 = 48;
a = 8;
ts = 900.0;
c1 = 0;
c2 = 0;
}
Финал, система подсчета баллов, она чересчур проста. Открываем гугл и вбиваем «Правила игры в настольные теннис». Ищем, за что очки даются. Находим часть про штрафы, а дальше мы успешно находим следующее: «Очко считается выигранным, если противник не успеет отразить правильно посланный ему мяч после первого отскока». Назревает вопрос, как отсчитывать удары и прочее?.. А удары и не нужно отсчитывать, ведь наш пинг-понг с двухмерной графикой.
Мы спокойно находим выход из положения и, как всегда, просто проверяем координаты относительно боковых стенок. Если происходит столкновение, то начисляем балл игроку на противоположной стороне поля. Функция — void ballscount(). Когда выйдет таймер — мы сравниваем баллы первого игрока (переменная int c1) и второго игрока (переменная int c2), объявляем победителя, делаем задержку и вызываем рестарт.
Код:
void ballscount()
{
if(x == 1)
{
c2++;
}
if(x == 119)
{
c1++;
}
if(c1 > c2 && ts == 0)
{
TV.println(10, 45, "Player 1 won!");
delay(10000);
restart();
}
else if(c1 < c2 && ts == 0)
{
TV.println(10, 45, "Player 2 won!");
delay(10000);
restart();
}
else if(c1 == c2 && ts == 0)
{
TV.println(10, 45, "You are equal");
delay(10000);
restart();
}
Вот и все, друзья, мы полностью написали код игры. Получилось довольно забавно и можно поиграть.
Для ленивых я просто напишу весь код.
#include <TVout.h>
#include <fontALL.h>
TVout TV;
int x, y, a, c1, c2, yb1, yb2, tm, tsh, s;
float ts;
boolean paused = false;
void setup ( )
{
TV.begin(NTSC, 120, 96);
TV.clear_screen();
TV.select_font(font6x8);
TV.println( 0, 50, "Welcome to Shimo" );
TV.delay (5000);
TV.clear_screen();
x = 60;
y = 48;
yb1 = 48;
yb2 = 48;
a = 8;
ts = 900.0;
s = 2;
}
void loop ( )
{
if(!paused)
{
TV.draw_line(60,0,60,96,1);
TV.select_font(font8x8);
racketsmove();
ballscount();
TV.print(1,1,c1); TV.print(18,1,":"); TV.print(26,1,c2);
tm = ts / 60;
ts -= 0.04;
if(ts < 0)
{
ts = 0;
}
TV.draw_rect(81,1,38,10,0,0);
TV.print(81,1,tm); TV.print(97,1,"."); TV.print(100,1,int(ts-(tm*60)));
ballcol();
/*if(ts < 600)
{
s = 4;
}
if(ts < 300)
{
s = 6;
}*/
ballmove(s, a);
TV.delay(50);
if(digitalRead(6) == HIGH)
{
paused = true;
delay(1000);
}
}
else
{
TV.println(40,4,"pause");
if(digitalRead(6) == HIGH)
{
paused = false;
delay(1000);
TV.clear_screen();
}
}
}
void ballscount()
{
if(x == 1)
{
c2++;
}
if(x == 119)
{
c1++;
}
if(c1 > c2 && ts == 0)
{
TV.println(10, 45, "Player 1 won!");
delay(10000);
restart();
}
else if(c1 < c2 && ts == 0)
{
TV.println(10, 45, "Player 2 won!");
delay(10000);
restart();
}
else if(c1 == c2 && ts == 0)
{
TV.println(10, 45, "You are equal");
delay(10000);
restart();
}
}
void ballcol()
{
if(x == 1 || x == 119 || (x == 10 && y < (yb1 + 3) && y > (yb1 - 3)) || (x == 110 && y < (yb2 + 3) && y > (yb2 - 3)))
{
if(a==1){a=5;}else if(a==2){a=8;}else if(a==3){a=7;}else if(a==4){a=6;}else if(a==5){a=1;}else if(a==6){a=4;}else if(a==7){a=3;}else if(a==8){a=2;}
}
if(x == 10 && y < (yb1 - 3) && y > (yb1 - 8))
{
a = 2;
}
if(x == 10 && y > (yb1 + 3) && y < (yb1 + 8))
{
a = 4;
}
if(x == 110 && y < (yb2 - 3) && y > (yb2 - 8))
{
a = 8;
}
if(x == 110 && y > (yb2 + 3) && y < (yb2 + 8))
{
a = 6;
}
if(y == 95 || y == 1)
{
if(a==1){a=5;}else if(a==2){a=4;}else if(a==3){a=7;}else if(a==4){a=2;}else if(a==5){a=1;}else if(a==6){a=8;}else if(a==7){a=3;}else if(a==8){a=6;}
}
}
void racketsmove()
{
TV.draw_line(10, yb1+8, 10, yb1-8, 0);
TV.draw_line(110, yb2+8, 110, yb2-8, 0);
if((yb1 - 8) > 1)
{
if(digitalRead(2) == HIGH)
{
yb1 -= 2;
}
}
if((yb1 + 8) < 95)
{
if(digitalRead(3) == HIGH)
{
yb1 += 2;
}
}
if((yb2 - 8) > 1)
{
if(digitalRead(4) == HIGH)
{
yb2 -= 2;
}
}
if((yb2 + 8) < 95)
{
if(digitalRead(5) == HIGH)
{
yb2 += 2;
}
}
TV.draw_line(10, yb1+8, 10, yb1-8, 1);
TV.draw_line(110, yb2+8, 110, yb2-8, 1);
}
void ballmove(int vel, int angle)
{
TV.set_pixel(x,y,0);
if(angle == 1)
{
y -= vel;
}
if(angle == 3)
{
x += vel;
}
if(angle == 5)
{
y += vel;
}
if(angle == 7)
{
x -= vel;
}
if(angle == 2)
{
x += round(vel/2);
y -= round(vel/2);
}
if(angle == 4)
{
x += round(vel/2);
y += round(vel/2);
}
if(angle == 6)
{
x -= round(vel/2);
y += round(vel/2);
}
if(angle == 8)
{
x -= round(vel/2);
y -= round(vel/2);
}
TV.set_pixel(x,y,1);
}
void restart()
{
TV.clear_screen();
x = 60;
y = 48;
yb1 = 48;
yb2 = 48;
a = 8;
ts = 900.0;
c1 = 0;
c2 = 0;
}
Шаг 4. Вырезаем корпус
Решил вырезать корпус на лазерном резаке (или фрезеровщике, я точно не знаю) из фанеры в 4mm. Нарисовал в InkScape, немного пошаманил и перевел в формат фрезеровщика.
Для геймпадов вырезал маленькие дощечки и просверлил в них дырки под кнопки. Получилось неплохо, но, к сожалению, я потерял фотографию.
Вывод
В процессе работы была создана простая игровая телевизионная игровая приставка на Arduino со стандартной игрой Ping Pong, с двумя геймпадами, в которую мы можем поиграть и даже залипать.
Дополнительные источники
1. Информация про библиотеку
2. Информация про порты подключения
Автор: SmnTin