0,0
рейтинг
7 июня 2014 в 00:56

Собираем лазерный проектор из доступных деталей

UPD: Добавлены файлы платы с ЦАП на GitHub

Изначально я планировал сделать Лазерную арфу, но пока получился промежуточный результат — устройство, которое можно использовать как лазерный проектор — рисовать лазером различные фигуры, записанные в файлах формата ILDA. Я в курсе, что многие, кто берется за сборку лазерного проектора, в качестве устройства, управляющего гальванометрами (так и не понял как лучше перевести на русский сочетание “galvo scanner"), используют дешевые слегка модифицированные звуковые платы для компьютера. Я пошел иным путем, так как в конечном счете мне нужно будет полностью автономное устройство, которое может работать без компьютера.



Посмотрим из чего состоит мой лазерный проектор. Стоимость всех деталей составила около 8000 руб, из которых больше половины — это 70mW лазерный модуль.
  1. Гальванометры и драйверы к ним для отклонения луча лазера по осям X/Y
  2. 532нм 70mW лазерный модуль с питанием от 5В Dragon Lasers SGLM70
  3. Texas Instruments Stellaris Launchpad
  4. Самодельная плата с ЦАП AD7249BRZ
  5. Блок питания


Железо


В моей системе используется 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-сообщения и отправке его в компьютер или подключенный аппаратный синтезатор для формирования звука.
Ануфриенко Сергей @madprogrammer
карма
41,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое

Комментарии (10)

  • +4
    «Делаем лазерный проектор из (почти) подручных материалов» — я, если честно, ожидал самодельные гальванометры, типа как тут:
    image
    А на деле — конструктор из готовых деталей :( meh…
    • +1
      Исправил заголовок на более подходящий вариант.
  • 0
    Хм, 70 мв лазер — 50 долларов + доставка, лазер модуль из старого лазерника — можно достать бесплатно в ремонте компов или среди знакомых, внутри него 4 гранное алюминиевое зеркало на оси бесколлекторника на которое мы светим лазером и при должной снхронизации получаем такую арфу, надо попробовать
  • 0
    У вас на плате на месте c2a ничего нет или я с мобильного не разгялжу?
    • 0
      Есть, керамический конденсатор 220 пФ
  • 0
    Подойдет ли такая система для синего лазера 445нм 2 ватта?
    • 0
      Я думаю подойдет, только нужно проработать вопрос питания и охлаждения лазера, а не так как у меня сделано «на коленке» — в случае если у вас лазерный модуль, а не готовая головка с радиатором, активным охлаждением и собственным блоком питания.
  • 0
    А если вместо сложной отклоняющей системы, поворачивающей зеркала под нужным углом, сделать классическую, по принципу телевизионной развертки, зеркала просто вращаются с расчетной, но на порядок разной скоростью, таким образом, чтобы луч проходил по всей области экрана? Например 30rps — вертикальный, и 10k rps — горизонтальный дадут картинку в 333 линии при 30fps, а если область в виде узкой полоски с малым количеством линий, то это еще меньше уменьшает требования к скорости вращения горизонтального отклоняющего зеркала. И ведь горизонтальных отклоняющих зеркал может быть несколько!

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

    Зато изображение будет растровое, и при наличии разноцветных лазеров — еще и цветное. И шум тише и монотонный, вместо зависящего от изображения стрекота.
    • 0
      Отдельная история сделать эти самые 10k rps. Ну, можно использовать призму от принтера шестигранную и его же родной двигатель…
      А еще один момент — это модуляция лазера с частотой в несколько мегагерц. Дело в том, что лазерные диоды очччень не любят коротких «иголок» на фронтах: быстродействия лазеру хватает отработать ее и выдать в излучение, и если его мощность в импульсе превысит допустимую (хоть на наносекунды), горят зеркала резонатора в лазерном диоде. Не зря в приличных лазерных модулях делают плавный пуск.
  • 0
    акустооптический дефлектор/модулятор в качестве альтернативы гальванометрам рассматривали?

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