Идея сделать лазерную пушку, которая наводит два луча в одну точку, появилась у меня после игры Fallout: New Vegas. Прототипом моего лазерного пистолета послужило уникальное оружие «Алгоритм Эвклида», которое наносит удар с орбитальной станции. Параллели между лазерной пушкой с двумя лучами и орбитальной станцией простые: у меня завалялось два лазерных модуля, а картинка прицела «Алгоритма Эвклида» подала мне идею сфокусировать два лазерных луча в одну точку, чтобы получить большую мощностью.
Немного пафоса
Делать «просто пушку» с фокусировкой мне не хотелось, и я решил разнообразить ее дополнительными ~~пафосными~~ опциями, как, например, озвучка стрельбы, отображение треугольника, как у «Алгоритма Эвклида» из игры, и чтобы он менялся в зависимости от дальности фокусировки двух лазеров, а также всякие мелочи вроде мониторинга напряжения питания и т.д. В итоге получилось то, что можно увидеть на фото и видео:
Основная часть
Главное в лазерной пушке,
Под лазерными модулями располагался ультразвуковой измеритель расстояния с максимальной дальностью до 4 метров. Конечно, это небольшое расстояние, впрочем пушка все равно не претендовала на что-то серьезнее обычной игрушки.
В моей схеме работа с датчиком была организована на микроконтроллере AVR с помощью внешнего прерывания.
Внимание! Весь код был написан на языке С в среде CVAVR 3.
// код был написан на языке С в среде CVAVR 3 для mega128
#define HS PORTB.6 // ножка МК для управления ультразвковым датчиком
// состояния флагов
#define _TRUE 1
#define _ FALSE 0
unsigned int distance; // сырые данные расстояния
unsigned char takt = 0; // этап измерения
unsigned char isErrorHsFlag = _FALSE; // флаг ошибки измерения (переполнение таймера)
unsigned char isFlag = _FALSE; // флаг состояния измерения (измерено или еще нет)
// нужно настроить таймер 1 так, чтобы число 0xFFFF в его счетном регистре
// набиралось больше, чем за 38 мс.
// в данном примере для МК использовался кварц 16 МГц.
// прерывание таймера 1 по переполнению, нужно для обнаружения ошибки
interrupt [TIM1_OVF] void timer1_ovf_isr(void) {
isErrorHsFlag = _TRUE;
}
// внешнее прерывание
interrupt [EXT_INT4] void ext_int4_isr(void) {
takt++;
switch (takt) {
case 1:
// обнуляем таймер
TCNT1 = 0;
isErrorHsFlag = _FALSE;
break;
case 2:
// получаем дистанцию
distance = TCNT1;
takt = 0;
isFlag = _TRUE;
break;
};
if (isErrorHsFlag == _TRUE) {
// в случае переполнения таймера была ошибка
distance = 0xFFFF;
isErrorHsFlag = _FALSE;
}
}
// функция возвращает расстояние
unsigned int getDistance(void) {
unsigned char whileTakt = 0; // для количества циклов (лимит ожидания)
// устанавливаем ножку МК в лог. 0
HS = 0;
// обнуляем флаги и регистр состояния
takt = 0;
isFlag = _FALSE;
isErrorHsFlag = _FALSE;
// запускаем измерение расстояния
HS=1;
delay_us(10);
HS=0;
whileTakt=0;
// ждем измерения
while (isFlag == _FALSE) {
whileTakt++;
delay_ms(1);
// если лимит времени превышен (измерение длится больше 38 мс)
if (whileTakt > 38) {
isFlag = _TRUE; // выходим из цикла
};
};
return distance;
};
Чтобы расстояние «не шумело», я решил применить к данным ультразвукового датчика медианный фильтр (код, приведенный ниже, был найден на просторах интернета). Дальше оставалось лишь получить нужный угол поворота сервопривода, который рассчитывался по алгоритму приведенному ниже под спойлером (код отличается от исходного, так как в оригинале еще есть учет настраиваемых «добавок» к длинам катетов треугольника).
// для медианного фильтра
#define NULL 0
#define STOPPER 0 /* Smaller than any datum */
#define MEDIAN_FILTER_SIZE 5
// медианный фильтр
typedef struct pair{
struct pair *point; /* Pointers forming list linked in sorted order */
unsigned int value; /* Values to sort */
} PAIR_T;
static PAIR_T small = {NULL, STOPPER};
static PAIR_T buffer[MEDIAN_FILTER_SIZE] = {0};
/* Pointer into circular buffer of data */
static PAIR_T *datpoint = buffer;
/* Chain stopper */
/* Pointer to head (largest) of linked list.*/
static PAIR_T big = {&small, 0};
/* Pointer to successor of replaced data item */
struct pair *successor;
/* Pointer used to scan down the sorted list */
struct pair *scan;
/* Previous value of scan */
struct pair *scanold;
/* Pointer to median */
struct pair *median;
unsigned int i;
unsigned int MedianFilter(unsigned int datum)
{
if (datum == STOPPER){
datum = STOPPER + 1; /* No stoppers allowed. */
}
if ( (++datpoint - buffer) >= MEDIAN_FILTER_SIZE){
datpoint = buffer; /* Increment and wrap data in pointer.*/
}
datpoint->value = datum; /* Copy in new datum */
successor = datpoint->point; /* Save pointer to old value's successor */
median = &big; /* Median initially to first in chain */
scanold = NULL; /* Scanold initially null. */
scan = &big; /* Points to pointer to first (largest) datum in chain */
/* Handle chain-out of first item in chain as special case */
if (scan->point == datpoint){
scan->point = successor;
}
scanold = scan; /* Save this pointer and */
scan = scan->point ; /* step down chain */
/* Loop through the chain, normal loop exit via break. */
for (i = 0 ; i < MEDIAN_FILTER_SIZE; ++i){
/* Handle odd-numbered item in chain */
if (scan->point == datpoint){
scan->point = successor; /* Chain out the old datum.*/
}
if (scan->value < datum){ /* If datum is larger than scanned value,*/
datpoint->point = scanold->point; /* Chain it in here. */
scanold->point = datpoint; /* Mark it chained in. */
datum = STOPPER;
};
/* Step median pointer down chain after doing odd-numbered element */
median = median->point; /* Step median pointer. */
if (scan == &small){
break; /* Break at end of chain */
}
scanold = scan; /* Save this pointer and */
scan = scan->point; /* step down chain */
/* Handle even-numbered item in chain. */
if (scan->point == datpoint){
scan->point = successor;
}
if (scan->value < datum){
datpoint->point = scanold->point;
scanold->point = datpoint;
datum = STOPPER;
}
if (scan == &small){
break;
}
scanold = scan;
scan = scan->point;
}
return median->value;
}
#define CONST_RAD 5092.95817 //константа множителя для углов
unsigned int ac,bc,rad; // катеты ac bc и угол поворота
float bck; // коэффициент для катета bc
// катет AC это расстояние до цели
ac = MedianFilter(getDistance());
if (ac > 50000) ac = 50000; // ограничение дальности
// чтобы уменьшить погрешность, считаем сразу без перевода в сантиметры
// катет bc - это расстояние между лазерами
rad = atan(ac / (bc * bck));
// устанавливаем угол поворота сервопривода
pwmServo(CONST_RAD*rad);
Звук «пиу-пиу»
Конечно, реальный лазер не звучит, однако смотреться пушка будет куда эффектнее, если добавить звук и еще озвучку некоторых функций, поэтому я решил реализовать WAV-плеер внутри МК, подключив к нему flash-карту памяти на 4 Гб. Вывод звука осуществлялся через ШИМ, при этом сигнал ШИМ'а управлял транзистором, которой уже управлял током через динамик. Сами звуки я сделал в программе Fruity Loops 9 для создания музыки.
// код был написан на языке С в среде CVAVR 3 для mega128
// для работы с SD картой и файловой системой FAT необходимы библиотеки:
// #include <sdcard.h>
// #include <ff.h>
//для работы с SD картой
static FRESULT f_err_code; // FRESULT для функций модуля
static FATFS FATFS_Obj; // структура - логический раздел
unsigned int ByteRead = 255; //количество релаьно считанных байт основного файла
FIL fil_obj; //структура файла, с которым работаем
char var[127]; //буфер, сюда мы поместим то, что считаем из основного файла.
// функция открывает трек по его номеру
// для каждого номера прописывается название файла.
// Для работы функции нужно иметь два таймера, таймер 1 и таймер 2.
// таймер 2 нужен для вывода ШИМ сигнала, частота ШИМ - максимальная. Настройка - "быстрый ШИМ"
// в данном примере для МК использовался кварц 16 МГц.
void openSnd(unsigned char nSnd) {
switch (nSnd) {
case 0:
f_err_code = f_open(&fil_obj, "but_1.wav", FA_READ); //пытаемся открыть файл "but_1.wav"
break;
case 1:
f_err_code = f_open(&fil_obj, "but_2.wav", FA_READ);
break;
case 2:
f_err_code = f_open(&fil_obj, "but_no.wav", FA_READ);
break;
case 3:
f_err_code = f_open(&fil_obj, "but_ok.wav", FA_READ);
break;
case 4:
f_err_code = f_open(&fil_obj, "but_sa.wav", FA_READ);
break;
case 5:
f_err_code = f_open(&fil_obj, "but.wav", FA_READ);
break;
case 6:
f_err_code = f_open(&fil_obj, "warning.wav", FA_READ);
break;
case 7:
f_err_code = f_open(&fil_obj, "on.wav", FA_READ); /
break;
case 8:
f_err_code = f_open(&fil_obj, "laz_sys.wav", FA_READ);
break;
case 9:
f_err_code = f_open(&fil_obj, "laz_act.wav", FA_READ);
break;
case 10:
f_err_code = f_open(&fil_obj, "laz_actk.wav", FA_READ);
break;
case 11:
f_err_code = f_open(&fil_obj, "voice_s.wav", FA_READ);
break;
case 12:
f_err_code = f_open(&fil_obj, "bat_full.wav", FA_READ);
break;
case 13:
f_err_code = f_open(&fil_obj, "bat_at.wav", FA_READ);
break;
case 14:
f_err_code = f_open(&fil_obj, "Bat_a.wav", FA_READ);
break;
case 15:
f_err_code = f_open(&fil_obj, "zel_no.wav", FA_READ);
break;
case 16:
f_err_code = f_open(&fil_obj, "new_1.wav", FA_READ);
break;
case 17:
f_err_code = f_open(&fil_obj, "new_2.wav", FA_READ);
break;
case 18:
f_err_code = f_open(&fil_obj, "new_3.wav", FA_READ);
break;
case 19:
f_err_code = f_open(&fil_obj, "new_4.wav", FA_READ);
break;
case 20:
f_err_code = f_open(&fil_obj, "new_5.wav", FA_READ);
break;
};
// сообщаем об ошибках
if (f_err_code & FR_OK) puts("FR_OK\r\n");
else
if (f_err_code & FR_NO_FILE ) puts("FR_NO_FILE\r\n");
else
if (f_err_code & FR_NO_PATH ) puts("FR_NO_PATH\r\n");
else
if (f_err_code & FR_INVALID_NAME ) puts("FR_INVALID_NAME\r\n");
else
if (f_err_code & FR_INVALID_DRIVE ) puts("FR_INVALID_DRIVE\r\n");
else
if (f_err_code & FR_EXIST ) puts("FR_EXIST\r\n");
else
if (f_err_code & FR_DENIED ) puts("FR_DENIED\r\n");
else
if (f_err_code & FR_NOT_READY ) puts("FR_NOT_READY\r\n");
else
if (f_err_code & FR_WRITE_PROTECTED ) puts("FR_WRITE_PROTECTED\r\n");
else
if (f_err_code & FR_DISK_ERR ) puts("FR_DISK_ERR\r\n");
else
if (f_err_code & FR_INT_ERR ) puts("FR_INT_ERR\r\n");
else
if (f_err_code & FR_NOT_ENABLED ) puts("FR_NOT_ENABLED\r\n");
else
if (f_err_code & FR_NO_FILESYSTEM ) puts("FR_NO_FILESYSTEM\r\n");
// если ошибок нет, начинаем воспроизведение
if (f_err_code == 0) {
//пытаемся читать 1 байт с начала файла в переменную var
f_err_code = f_read(&fil_obj,var,44,&ByteRead);
//надо настроить таймер 1 на частоту дискретизации
// Timer/Counter 1 initialization
// Clock source: System Clock
// Clock value: 16000,000 kHz
TCCR1A = (0<<COM1A1) | (0<<COM1A0) | (0<<COM1B1) | (0<<COM1B0) | (0<<COM1C1) | (0<<COM1C0) | (0<<WGM11) | (0<<WGM10);
TCCR1B = (0<<ICNC1) | (0<<ICES1) | (0<<WGM13) | (0<<WGM12) | (0<<CS12) | (0<<CS11) | (1<<CS10);
DDRB.7 = 1; // настройка 7 ножки порта B на выход
// тут начинается обработка сэмпла
while(_TRUE) {
TCNT1 = 0; // обнумяем счетный регистр
f_err_code = f_read(&fil_obj,var,1,&ByteRead);
OCR2 = var[0]; // загружаем в шим
if (ByteRead == 0) break; // если конец файла, выходим
while (TCNT1 < 1000); // задержка, которая зависит от частоты дискретизации.
}
}
DDRB.7=0; // отключаем звук. Иначе ШИМ будет греть динамик.
f_err_code = f_close(&fil_obj); // закрываем файл
// настраиваем таймер 1 для работы с датчиком расстояния
// Timer/Counter 1 initialization
// Clock source: System Clock
// Clock value: 2000,000 kHz
TCCR1A=(0<<COM1A1) | (0<<COM1A0) | (0<<COM1B1) | (0<<COM1B0) | (0<<COM1C1) | (0<<COM1C0) | (0<<WGM11) | (0<<WGM10);
TCCR1B=(0<<ICNC1) | (0<<ICES1) | (0<<WGM13) | (0<<WGM12) | (0<<CS12) | (1<<CS11) | (0<<CS10);
};
LCD Экран
Так как когда-то друг мне подарил LCD-экран LPH8731-3C, то именно его я и решил использовать. Вообще, это был мой первый опыт использования в своих проектах LCD-экрана. Вся информация по данному экрану и библиотека для работы с ним были найдены тут.
Питание
Так как сервопривод и микроконтроллер требовали питания 5 вольт, нужен был повышающий DC-DC, так как запитывать пушку я планировал от одного Li-Pol аккумулятора. DC-DC у меня уже были готовы (на микросхеме LM2621), когда-то зачем-то я их сделал в виде модулей, залитых в эпоксидную смолу:
В качестве зарядника аккумулятора была использована схема на линейных стабилизаторах, найденная на просторах интернета.
Схема
Когда уже было ясно, что с чем паять, я решил в итоге собрать всю схему в виде многослойного торта:
Этот «тортик» умел проигрывать WAV, выводить данные на LCD, опрашивать ультразвуковой датчик расстояния, опрашивать напряжение на Li-Pol аккумуляторе, опрашивать кнопки, опрашивать потенциометр, управлять работой лазеров и управлять сервоприводом.
Корпус
Корпус был сделан из металлического короба от БП компьютера и листа нержавейки. Рукоятка была сделана из пенопласта и покрашена черной краской. Чтобы крышку корпуса можно было прикрутить, пришлось сажать гайки на клей, так как к ним никак не добраться руками внутри корпуса.
«Кишки пушки»
В итоге
Все заработало: пушка стреляла, фокусировалась в точку при хорошей настройке параметров. Также я в нее добавил двоичный пароль при включении, настройки для всех параметров фокусировки и настройки режимов, которые сохраняются в ПЗУ микроконтроллера, а также график разряда батареи (на котором кстати было видно, как батарея проседает во время включения лазеров). Для лазеров я сделал также дополнительные два режима работы, когда работает один лазер, и когда работают два лазера с сохранением определенного расстояния между ними
Немного истории, связанной с пушкой
У меня был один заказчик, которому я в итоге отказал в разработке его заказа. Он еще долго пытался мне объяснить, что я потерял очень могое, отказавшись от его предложения работать
Комментарии (24)
Kasatich
15.08.2016 10:23А мне сначала подумалось что тут будет такая вундервафля — слабый лазер на пушке для наведения и более мощный лазер на станине поворотной, которая бы выпускала заряд в ту же точку, что и младший брат) Размещаешь мощный лазер у себя на плече и вперед — ты уже инквизитор!)
Jamato
15.08.2016 10:40+1Сколиоз гарантирован.
Kasatich
15.08.2016 10:53Есесно экзоскелет потребуется!) Зато какой простор для разработки! Высший класс имхо наводить плечевую турель прямо взглядом, через какой-нибудь гуглоглас.
alex4321
15.08.2016 12:00+1Kasatich
15.08.2016 12:08Экак( А у этого парня разве постоянно пушка трекает положение глаз? Я вообще отталкивался от снаряжение инквизитора из Вархаммера — у них там пси-пушка постоянно следует за взглядом носителя и потому дает 100% вероятность поражения любой хрени в зоне видимости XD
alex4321
15.08.2016 12:45Опа, что за пушечка?
Valdei
16.08.2016 16:17http://warhammer40k.wikia.com/wiki/Psycannon
alex4321
16.08.2016 18:03Да, некорректно задал вопрос. Я скорее о комплекте в целом, а не самой по себе психипушке.
Впрочем — http://warhammer40k.wikia.com/wiki/Mind_Impulse_UnitValdei
16.08.2016 18:56Вот ведь хотел сначала ответить «обычная пушка, только спуск на нейроинтерфейс завязан», но подумал, что не все в теме вахи. :)
По идее реализовать что-то подобное и сейчас можно — сочетанием отслеживания зрачков и собственно использованием нейоинтерфейса для спуска.
nafikovr
15.08.2016 21:35+1главное при турели на левом плече не выстрелить посмотрев направо, иначе будут не самые приятные последствия.
SnowLoKu
15.08.2016 10:59Пришлось бы разрабатывать экоскелет для пушки, а там уже и до робота рукой подать…
MaxKitsch
17.08.2016 14:09На втором плече для симметрии можно разместить блок батарей. Или второй лазер, чтобы покрывать образованную головой мёртвую зону для первого.
Fullmoon
15.08.2016 11:36+2Безотносительно мощности лазера — вот это я понимаю, вот так и должно выглядеть оружие постапокалиптического будущего.
Electrohedgehog
15.08.2016 11:51Серьёзная работа. Уровня, скорее, хабра.
Сколько времени у вас заняло всё это? Если можно вычислить, то по частям — механика с корпусом, электроника, программирование.
Если есть другие подобные игрушки — пишите, обязательно, ещё.
Capacitor10n
15.08.2016 12:00Прикольная штука, жаль что «без урона».
А если объединить с этим: https://geektimes.ru/post/258416/
можно не только игрушку получить но и полезный инструмент)
«ПИУ», «Цель на расстоянии 11 метров поражена»
engine9
15.08.2016 12:15+1Школа и родители вдалбливают большинству людей, что нужно «делом заниматься, а не тем что хочется». Только вот реально прорывные и захватывающие биографии у тех, кто следует за своей мечтой и делает то от чего сам прётся.
Если сделать лазеры послабее и безопаснее будет улётная детская (да и взрослая) игрушка.
Abiboss