Рисуем на осциллографе в режиме X-Y
20 июля 2020
Помимо обычного режима, отображающего зависимость напряжения от времени, многие осциллографы имеют режим X-Y. В этом режиме рисуется кривая на плоскости. Координаты X и Y точек, принадлежащих кривой, определяются входом с двух каналов осциллографа. Режим X-Y многим знаком по фигурам Лиссажу. Но при желании можно нарисовать и что-то поинтересней. Этим мы сегодня и займемся.
Идея не нова и описана во многих источниках. Мне кажется, что впервые я ее подсмотрел у Alan Wolke, W2AEW (видео один, два и три), но это не точно.
Итак, нам предстоит генерировать два сигнала сложной формы. Звучит, как задача для микроконтроллера и пары ЦАП. Было решено воспользоваться самодельной отладочной платой на базе микроконтроллера STM32F405. Если вы захотите повторить описанные далее шаги, то можете воспользоваться платой HydraBus, BlackIce II, или какой-то другой. Главное, чтобы микроконтроллер имел два ЦАП. Спортировать проект с STM32F405 на другой микроконтроллер STM32 проще простого.
Примечание: Вас могут заинтересовать статьи Передача изображений в SSB-сигнале с помощью Python и Микроконтроллеры STM32: использование АЦП и ЦАП, если вдруг вы их пропустили.
Рисуемую фигуру определим, как множество вершин с заданными координатами X и Y:
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_y = 0;
Здесь хранятся текущие координаты X и Y. Это то, что сейчас выдают ЦАП.
int16_t prev_idx = 0;
Индексы точек к которой и от которой мы сейчас рисуем отрезок.
int16_t dy = 0;
Инкремент curr_x
и curr_y
за одну итерацию рисования отрезка.
Код устроен так, что сначала фигура рисуется от первой точки к последней, а затем от последней точки к первой. Переменная direction хранит 1
, если в данный момент мы рисуем в прямом направлении, и -1
, если в обратном. Рисование в двух направлениях использовано для ликвидации отрезка между первой и последней точкой на конечном рисунке. Такой артефакт возникает, если зациклить отрисовку «в лоб».
Когда мы понимаем, что закончили рисовать очередной отрезок, и пора рисовать следующий, вызывается процедура 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()
:
HAL_DAC_Start(&hdac, DAC_CHANNEL_1);
HAL_DAC_Start(&hdac, DAC_CHANNEL_2);
next_line();
UART_Printf("Ready!\r\n");
HAL_Delay(1);
}
А так выглядит основной цикл программы:
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()
.
Приведенный код не претендует на неземную красоту, но он работает:
Я не придумал ничего лучше, чем вывести свой радиолюбительский позывной. Сложность картинок ограничена в основном временем, которое вы готовы инвестировать в проект. Помимо статической картинки возможно сделать анимацию, и даже небольшую игру.
Проект можно повторить на микроконтроллере, отличном от STM32, или даже на FPGA. Если у выбранного вами железа нет ЦАП, это не страшно. Можно воспользоваться внешним ЦАП или сделать ЦАП с нуля на R-2R лестнице.
Надеюсь, что вы нашли эту информацию полезной. Полную версию исходников вы найдете в этом репозитории на GitHub.
Метки: STM32, Электроника.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.