Помимо обычного режима, отображающего зависимость напряжения от времени, многие осциллографы имеют режим 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 .