← На главную

Рисуем на осциллографе в режиме X-Y

Помимо обычного режима, отображающего зависимость напряжения от времени, многие осциллографы имеют режим X-Y. В этом режиме рисуется кривая на плоскости. Координаты X и Y точек, принадлежащих кривой, определяются входом с двух каналов осциллографа. Режим X-Y многим знаком по фигурам Лиссажу. Но при желании можно нарисовать и что-то поинтересней. Этим мы сегодня и займемся.

Идея не нова и описана во многих источниках. Мне кажется, что впервые я ее подсмотрел у Alan Wolke, W2AEW (видео один, два и три), но это не точно.

Итак, нам предстоит генерировать два сигнала сложной формы. Звучит, как задача для микроконтроллера и пары ЦАП. Было решено воспользоваться самодельной отладочной платой на базе микроконтроллера STM32F405. Если вы захотите повторить описанные далее шаги, то можете воспользоваться платой HydraBus, BlackIce II, или какой-то другой. Главное, чтобы микроконтроллер имел два ЦАП. Спортировать проект с STM32F405 на другой микроконтроллер STM32 проще простого.

Примечание: Вас могут заинтересовать статьи Передача изображений в SSB-сигнале с помощью Python и Микроконтроллеры STM32: использование АЦП и ЦАП, если вдруг вы их пропустили.

Рисуемую фигуру определим, как множество вершин с заданными координатами X и Y:

uint8_t xs[] = { 2, 2, 22, 22, 2, 22, 27, 48, 48, 27, 27, 27, 48, 48, 27, 48, 52, 52, 73, 73, 52, 73, 73, 85, 77, 77, 77, 85, 91, 98, 98, 98, 91, 102, 102, 102, 123, 102, 123, }; uint8_t ys[] = { 42, 84, 84, 63, 63, 42, 42, 63, 84, 84, 63, 84, 84, 63, 42, 42, 42, 84, 84, 63, 63, 63, 42, 42, 50, 84, 50, 42, 42, 50, 84, 50, 42, 42, 84, 63, 84, 63, 42, };

Координаты принадлежат воображаемой плоскости размером 128 на 128 точек. Числа от 0 до 127 могут быть записаны в один байт, и они относительно удобны при переносе рисунка из тетрадки в код. Степень двойки позволяет использовать битовые сдвиги вместо дорогой операции деления. Это пригодится нам далее.

В коде использовано несколько глобальных переменных:

uint8_t curr_x = 0; uint8_t curr_y = 0;

Здесь хранятся текущие координаты X и Y. Это то, что сейчас выдают ЦАП.

int16_t idx = 0; int16_t prev_idx = 0;

Индексы точек к которой и от которой мы сейчас рисуем отрезок.

int16_t dx = 0; int16_t dy = 0;

Инкремент curr_x и curr_y за одну итерацию рисования отрезка.

int8_t direction = 1;

Код устроен так, что сначала фигура рисуется от первой точки к последней, а затем от последней точки к первой. Переменная direction хранит 1, если в данный момент мы рисуем в прямом направлении, и -1, если в обратном. Рисование в двух направлениях использовано для ликвидации отрезка между первой и последней точкой на конечном рисунке. Такой артефакт возникает, если зациклить отрисовку «в лоб».

Когда мы понимаем, что закончили рисовать очередной отрезок, и пора рисовать следующий, вызывается процедура next_line():

void next_line() { const uint8_t speed = 32; int8_t next_direction = direction; idx += direction; if(idx < 0) { idx = 0; next_direction = -direction; } else if(idx >= sizeof(xs)/sizeof(xs[0])) { idx = sizeof(xs)/sizeof(xs[0])-1; next_direction = -direction; } prev_idx = idx - direction; if(prev_idx < 0) { prev_idx = sizeof(xs)/sizeof(xs[0])-1; } else if(prev_idx >= sizeof(xs)/sizeof(xs[0])) { prev_idx = 0; } direction = next_direction; curr_x = xs[prev_idx]; curr_y = ys[prev_idx]; dx = (((int16_t)xs[idx]) - ((int16_t)xs[prev_idx]))/speed; dy = (((int16_t)ys[idx]) - ((int16_t)ys[prev_idx]))/speed; if(dx == 0) { if(xs[idx] > xs[prev_idx]) { dx = 1; } else if(xs[idx] < xs[prev_idx]) { dx = -1; } } if(dy == 0) { if(ys[idx] > ys[prev_idx]) { dy = 1; } else if(ys[idx] < ys[prev_idx]) { dy = -1; } } }

Здесь происходит инкремент / декремент idx и next_idx. Если мы достигли последней точки, обновляется direction. Также пересчитываются значения curr_x, curr_y, dx и dy.

Fun fact! В качестве упражнения предлагаю вам поэкспериментировать с разными значениями speed. Как меняется изображение при больших и меньших значениях константы? Объясните результат.

В начале исполнения мы инициализируем ЦАП, а также проставляем значения глобальным переменным вызовом next_line():

void init() { HAL_DAC_Start(&hdac, DAC_CHANNEL_1); HAL_DAC_Start(&hdac, DAC_CHANNEL_2); next_line(); UART_Printf("Ready!\r\n"); HAL_Delay(1); }

А так выглядит основной цикл программы:

void loop() { curr_x += dx; curr_y += dy; if(((dx > 0) && (curr_x > xs[idx])) || ((dx < 0) && (curr_x < xs[idx]))) { curr_x = xs[idx]; } if(((dy > 0) && (curr_y > ys[idx])) || ((dy < 0) && (curr_y < ys[idx]))) { curr_y = ys[idx]; } uint32_t x = ((uint32_t)curr_x)*0xFFF/128; uint32_t y = ((uint32_t)curr_y)*0xFFF/128; HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, x); HAL_DAC_SetValue(&hdac, DAC_CHANNEL_2, DAC_ALIGN_12B_R, y); if((curr_x == xs[idx]) && (curr_y == ys[idx])) { next_line(); } }

Здесь curr_x и curr_y увеличиваются на dx и dy соответственно, с поправкой на то, что мы можем промахнутся мимо целевой точки из-за ошибки округления. Затем полученные координаты масштабируются из 7 бит в 12 бит, и это отправляется на пару ЦАП. Если мы видим, что дошли до последней точки в отрезке, то вызываем next_line().

Приведенный код не претендует на неземную красоту, но он работает:

Рисуем на осциллографе в режиме X-Y при помощи микроконтроллера STM32

Я не придумал ничего лучше, чем вывести свой радиолюбительский позывной. Сложность картинок ограничена в основном временем, которое вы готовы инвестировать в проект. Помимо статической картинки возможно сделать анимацию, и даже небольшую игру.

Проект можно повторить на микроконтроллере, отличном от STM32, или даже на FPGA. Если у выбранного вами железа нет ЦАП, это не страшно. Можно воспользоваться внешним ЦАП или сделать ЦАП с нуля на R-2R лестнице.

Надеюсь, что вы нашли эту информацию полезной. Полную версию исходников вы найдете в этом репозитории на GitHub.