Робимо лазерний проектор з (майже) підручних матеріалів

    Спочатку я планував зробити Лазерну арфу , але поки вийшов проміжний результат — пристрій, який можна використовувати як лазерний проектор — малювати лазером різні фігури, записані у файлах формату ILDA. Я в курсі, що багато, хто береться за збірку лазерного проектора, як пристрій, керуючого гальванометрами (так і не зрозумів як краще перекласти на російську поєднання "galvo scanner"), використовують дешеві злегка модифіковані звукові плати для комп'ютера. Я пішов іншим шляхом, так як в кінцевому рахунку мені потрібно буде повністю автономний пристрій, який може працювати без комп'ютера.
 
  
Подивимося з чого складається мій лазерний проектор. Вартість усіх деталей склала близько 8000 руб, з яких більше половини — це 70mW лазерний модуль.
 
     
  1. Гальванометри і драйвери до них для відхилення променя лазера по осях X / Y
  2.  
  3. 532нм 70mW лазерний модуль з живленням від 5В Dragon Lasers SGLM70
  4.  
  5. Texas Instruments Stellaris Launchpad
  6.  
  7. Саморобна плата з ЦАП AD7249BRZ
  8.  
  9. Блок живлення
  10.  
 
 

Залізо

У моїй системі використовується Stellaris Launchpad в якості «мозку» (тому що він досить швидкий і має апаратну підтримку USB) і 12-бітний двоканальний ЦАП з послідовним інтерфейсом Analog Devices AD7249BRZ . Для керування відхиленням променя на вхід драйвера потрібно подавати аналоговий сигнал в діапазоні від -5 до +5 вольт. ЦАП AD7249BRZ якраз вміє працювати в такому режимі (а також від 0 до 5 вольт і від 0 до 10 вольт). Для нього я розвів в Eagle спеціальну плату, яка підключається до Stellaris Launchpad. Плата вимагає двополярного харчування, яке виходить за допомогою мікросхеми ICL7660 . Для перетворення єдиного вихідного напруги поставляється з гальванометрами блоку живлення (15В) в потрібні мені я використовував лінійний регулятор LM317, що в наслідку виявилося не найбільш оптимальним рішенням, особливо для харчування лазерного модуля — тому що LM-ка з великим радіатором (видно на відео) через хвилин 10 роботи нагрівається градусів до 70. Без радіатора вона просто дуже швидко перегрівалася і відключалася від перегріву (а разом з нею і лазерний модуль, через що я спочатку вирішив що він згорів і мало не відклав пару цеглин, т.к. при повторній подачі живлення він не включався — як уже потім з'ясувалося доти, поки не охолоне мікросхема).
 
Лазерний модуль спочатку не підтримував TTL-модуляцію, тому коли мені набридло просто водити лазером в різні сторони я задумався про те, щоб в потрібні моменти часу включати і відключати промінь. Для цього треба було допрацьовувати лазерний модуль паяльником. На щастя, майже всі китайські лазерні модулі вельми схожі один на одного, прості, і зроблені на операційному підсилювачі LM358. Підпаявши до його ніг 3 і 4 (неінвертуючий вхід і земля відповідно) емітер і колектор першого-ліпшого біполярного транзистора 2N4401, я, таким чином, отримав можливість модулювати роботу лазера, подаючи керуючий сигнал на базу транзистора:
 
 
Доопрацьований напилком лазерний модуль
Схема і плата для AD7249BRZ представлена ​​нижче. Можливо уважний читач знайде у схемі помилку, тому що в ній з невідомих мені причин здається не працює частина з операційним підсилювачем, яка покликана зробити вихідний сигнал схеми балансним для більшої захисту від перешкод. Мій примірник замість балансного сигналу видає небалансний, але, тим не менш, все працює і так.
 
Сподіваюся ви не злякалися страшної картинки плати з нальотом біля висновків мікросхеми, який утворився після протирання етиловим спиртом. До речі, з цієї причини рекомендують відмивати флюс ізопропиловим спиртом, так як він не залишає таких розлучень. До речі, кому цікаво, що це за роз'єми такі із засувкою на платі — це роз'єми Molex (22-23-2021 розетка, 22-01-3027 вилка, 08-50-0114 контакт для вилки), замовляв їх через Digikey, так як у китайців вони стоять якось непристойно дорого.
 
 

Софт

На цьому начебто все найцікавіше про залізну частина закінчується, так що переходимо до частини софтовой. Складається вона з двох частин — програмки для ПК і прошивки для Stellaris Launchpad, яка реалізує USB bulk-пристрій з власним форматом пакетів по 32 біта в кожному. Формат семпла описаний наступною структурою:
 
typedef struct
{
	unsigned x:12; // координата X
	unsigned rx:4; // флаг (вкл/выкл лазер)
	unsigned y:12; // координата Y
	unsigned ry:4; // не используется
} sample_t;

Пристрій використовує USB-буфери розміром 512 байт, в які з ПК з деяким запасом, і з такою швидкістю, щоб не викликати переповнення або спустошення буфера, записує дані. Використовувані гальванометри розраховані на відображення 20000 точок у секунду, тобто це необхідна частота семплювання. У функції обробки даних від USB швидкість обробки регулюється за допомогою банального
SysCtlDelay
. Регулюючи значення можна підлаштувати систему, так щоб тестова картинка ILDA відображалася правильно:
 
Зелений світлодіод на відео на початку посту блимає після обробки кожної пачки в 20000 семплів. Тобто, в ідеалі він повинен блимати рівно 1 раз в секунду.
 
Програмна частина для ПК заснована на
playilda.c
з пакету OpenLase , однак звідти вирізане все зайве і замість взаємодії з сервером JACK використовується libusb для відправки пакетів даних на Stellaris Launchpad.
 Вихідний код програми для ПК
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>
#include <stdio.h>
#include <string.h>
#include <libusb-1.0/libusb.h>
#include <iostream>
#include <string>
#include <vector>

#define MAGIC 0x41444C49

static inline uint16_t swapshort(uint16_t v) {
	return (v >> 8) | (v << 8);
}

float scale = 1.0;

typedef struct {
	uint32_t	magic;
	uint8_t		pad1[3];
	uint8_t		format;
	char		name[8];
	char		company[8];
	uint16_t 	count;
	uint16_t 	frameno;
	uint16_t 	framecount;
	uint8_t		scanner;
	uint8_t 	pad2;
} __attribute__((packed)) ilda_hdr;

#define BLANK 0x40
#define LAST  0x80

typedef struct {
	int16_t x;
	int16_t y;
	int16_t z;
	uint8_t state;
	uint8_t color;
} __attribute__((packed)) icoord3d;

typedef struct coord3d {
	int16_t x;
	int16_t y;
	int16_t z;
	uint8_t state;

	coord3d(int16_t x, int16_t y, int16_t z, uint8_t state) : x(x), y(y), z(z), state(state) { }
} coord3d;

typedef struct {
	std::vector<coord3d> points;
	int position;
} frame;

frame rframe;
int subpos;
int divider = 1;

int loadildahdr(FILE *ild, ilda_hdr & hdr)
{
	if (fread(&hdr, sizeof(hdr), 1, ild) != 1) {
		std::cerr << "Error while reading header" << std::endl;
		return -1;
	}

	if (hdr.magic != MAGIC) {
		std::cerr << "Invalid magic" << std::endl;
		return -1;
	}

	if (hdr.format != 0) {
		fprintf(stderr, "Unsupported section type %d\n", hdr.format);
		return -1;
	}

	hdr.count = swapshort(hdr.count);
	hdr.frameno = swapshort(hdr.frameno);
	hdr.framecount = swapshort(hdr.framecount);
}

int loadild(const std::string & file, frame & frame)
{
	int i;
	FILE *ild = fopen(file.c_str(), "rb");

	if (!ild) {
		std::cerr << "Cannot open " << file << std::endl;
		return -1;
	}

	ilda_hdr hdr;
	loadildahdr(ild, hdr);

	for (int f = 0; f < hdr.framecount; f++)
	{
		std::cout << "Frame " << hdr.frameno << " of " << hdr.framecount << " " << hdr.count << " points" << std::endl;
		icoord3d *tmp = (icoord3d*)calloc(hdr.count, sizeof(icoord3d));

		if (fread(tmp, sizeof(icoord3d), hdr.count, ild) != hdr.count) {
			std::cerr << "Error while reading frame" << std::endl;
			return -1;
		}

		for(i = 0; i < hdr.count; i++) {
			coord3d point(swapshort(tmp[i].x), swapshort(tmp[i].y), swapshort(tmp[i].z), tmp[i].state);
			frame.points.push_back(point);
		}

		free(tmp);

		loadildahdr(ild, hdr);
	}

	fclose(ild);
	return 0;
}

short outBuffer[128];

int process()
{
	frame *frame = &rframe;

	short *sx = &outBuffer[0];
	short *sy = &outBuffer[1];

	for (int frm = 0; frm < 64; frm++) {
		struct coord3d *c = &frame->points[frame->position];

		*sx = 4095 - (2047 + (2048 * c->x / 32768)) * scale;
		*sy = (2047 + (2048 * c->y / 32768)) * scale;

		if(c->state & BLANK) {
			*sx |= 1 << 15;
		} else {
			*sx &= ~(1 << 15);
		}

		sx += 2;
		sy += 2;

		subpos++;
		if (subpos == divider) {
			subpos = 0;
			if (c->state & LAST)
				frame->position = 0;
			else
				frame->position = (frame->position + 1) % frame->points.size();
		}
	}

	return 0;
}

int main(int argc, char **argv)
{
	libusb_device_handle *dev;
	libusb_context *ctx = NULL;
	int ret, actual;

	ret = libusb_init(&ctx);
	if(ret < 0) {
		fprintf(stderr,"Couldn't initialize libusb\n");
		return EXIT_FAILURE;
	}

	libusb_set_debug(ctx, 3);

	dev = libusb_open_device_with_vid_pid(ctx, 0x1cbe, 0x0003);
	if(dev == NULL) {
		fprintf(stderr, "Cannot open device\n");
		return EXIT_FAILURE;
	}
	else
		printf("Device opened\n");

	if(libusb_kernel_driver_active(dev, 0) == 1) {
		fprintf(stderr, "Kernel driver active\n");
		libusb_detach_kernel_driver(dev, 0);
	}

	ret = libusb_claim_interface(dev, 0);
	if(ret < 0) {
		fprintf(stderr, "Couldn't claim interface\n");
		return EXIT_FAILURE;
	}

	// To maintain our sample rate
	struct timespec ts;
	ts.tv_sec  = 0;
	ts.tv_nsec = 2000000;

	memset(&rframe, 0, sizeof(frame));
	if (loadild(argv[1], rframe) < 0)
	{
		fprintf(stderr, "Failed to load ILDA\n");
		return EXIT_FAILURE;
	}

	while(1)
	{
		process();

		if(nanosleep(&ts, NULL) != 0)
			fprintf(stderr, "Nanosleep failed");
		ret = libusb_bulk_transfer(dev, (1 | LIBUSB_ENDPOINT_OUT), (unsigned char*)&outBuffer, 256, &actual, 0);
		if(ret != 0 || actual != 256)
			fprintf(stderr, "Write error\n");
	}

	libusb_release_interface(dev, 0);
	libusb_close(dev);
	libusb_exit(ctx);

	return 0;
}

 
У функції
main()
за допомогою nanosleep також регулюється періодичність, з якою микроконтроллеру надсилаються нові дані.
Повністю вихідний код прошивки контролера можна подивитися на GitHub .
 
 

Плани на майбутнє

Надалі планується-таки доробити се до стану, схожого на початку замислювалися лазерну арфу. Для цього достатньо одного, а не двох дзеркал, так як лазерний промінь рухається тільки уздовж однієї осі. Принцип роботи арфи полягає в тому, що контролер запалює і гасить промінь лазера у відомі йому моменти часу, створюючи лазерну «клавіатуру» в повітрі. Виконавець, перекриваючи рукою в світловідбиваючої рукавичці яскравий промінь лазера, приводить в дію фоточуттєвий елемент у підставі «арфи». Так як мікроконтролер знає, в який момент яку частину клавіатури він «малював», то може визначити який з променів був перекритий. Далі справа за формуванням відповідного MIDI-повідомлення та відправці його в комп'ютер або підключений апаратний синтезатор для формування звуку.
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.