Изначально я планировал сделать Лазерную арфу, но пока получился промежуточный результат — устройство, которое можно использовать как лазерный проектор — рисовать лазером различные фигуры, записанные в файлах формата ILDA. Я в курсе, что многие, кто берется за сборку лазерного проектора, в качестве устройства, управляющего гальванометрами (так и не понял как лучше перевести на русский сочетание “galvo scanner"), используют дешевые слегка модифицированные звуковые платы для компьютера. Я пошел иным путем, так как в конечном счете мне нужно будет полностью автономное устройство, которое может работать без компьютера.
Посмотрим из чего состоит мой лазерный проектор. Стоимость всех деталей составила около 8000 руб, из которых больше половины — это 70mW лазерный модуль.
- Гальванометры и драйверы к ним для отклонения луча лазера по осям X/Y
- 532нм 70mW лазерный модуль с питанием от 5В Dragon Lasers SGLM70
- Texas Instruments Stellaris Launchpad
- Самодельная плата с ЦАП AD7249BRZ
- Блок питания
Железо
В моей системе используется 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 %dn", 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 libusbn");
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 devicen");
return EXIT_FAILURE;
}
else
printf("Device openedn");
if(libusb_kernel_driver_active(dev, 0) == 1) {
fprintf(stderr, "Kernel driver activen");
libusb_detach_kernel_driver(dev, 0);
}
ret = libusb_claim_interface(dev, 0);
if(ret < 0) {
fprintf(stderr, "Couldn't claim interfacen");
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 ILDAn");
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 errorn");
}
libusb_release_interface(dev, 0);
libusb_close(dev);
libusb_exit(ctx);
return 0;
}
В функции main()
с помощью nanosleep также регулируется периодичность, с которой микроконтроллеру посылаются новые данные.
Полностью исходный код прошивки контроллера можно посмотреть на GitHub.
Планы на будущее
В дальнейшем планируется-таки доделать сие до состояния, похожего на изначально задумывавшуюся лазерную арфу. Для этого достаточно одного, а не двух зеркал, так как лазерный луч двигается только вдоль одной оси. Принцип работы арфы заключается в том, что контроллер зажигает и гасит луч лазера в известные ему моменты времени, создавая лазерную «клавиатуру» в воздухе. Исполнитель, перекрывая рукой в светоотражающей перчатке яркий луч лазера, приводит в действие фоточувствительный элемент в основании «арфы». Так как микроконтроллер знает, в какой момент какую часть клавиатуры он «рисовал», то может определить какой из лучей был перекрыт. Дальше дело за формированием соответствующего MIDI-сообщения и отправке его в компьютер или подключенный аппаратный синтезатор для формирования звука.
Автор: madprogrammer