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

20 июля 2020

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

Метки: , .


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