Графический интерфейс генератора. Часть 1

Где-то в середине лета я начал реализовывать давно витавшую в моей голове идею о функциональном генераторе. На форуме уже есть отдельная тема про него. Пройдена куча граблей, но тем не менее, сделано достаточно много как по железу, так и по софту.
В двух статьях я подробно опишу свою реализацию графического интерфейса генератора. Первая часть охватит опрос элементов управления, формирование команд для сигнальной платы и верхний уровень интерфейса, вторая — нижний уровень интерфейса, т.к. это самая сложная часть.


Размеры передней панели корпуса GIANTA G768 127х71 мм. В них надо вместить функциональные и режимные клавиши, цифровую клавиатуру, энкодер, светодиоды и, конечно же, дисплей, на который места остается не так уж и много. Мне понравился дисплей 2.2” на ILI9341, как наиболее оптимальный и по размерам, и по стоимости. Получаем разрешение 320х240 точек, что вполне неплохо.
Как по мне, так интерфейс не должен быть слишком заумным, напротив, чем проще, тем лучше. Поэтому за образец взял интерфейс генератора АКИП-3402, с которым достаточно часто работаю. Правда там дисплей более вытянутый, но зато у меня по вертикали места больше. Так вот и родился первый эскиз в paint’e.

Интерфейс состоит из следующих элементов:
— заголовок;
— 2 поля параметров;
— 4 вкладки.
Вкладки отображают сокращенные названия параметров. Каждое поле отображает полное название, текущее значение, единицу измерения и пиктограмму. Причем если значение в активном поле числовое, то отображается черта под разрядом, значение которого будет меняться при повороте энкодера. Активные вкладка и поле параметра выделены другим цветом. Цветов использовал немного, т.к. пестрый интерфейс больше раздражает, чем способствует работе.
Структура проекта можно сказать линейная, все пользовательские функции имеют префикс в соответствии с названием файла, например: TAB_Init(void); В этой статье будут рассмотрены модули, выделенные зеленым цветом.

Модуль ILI9341 вобще не буду описывать. Автором драйвера дисплея является ВитГо, присутствующий на форуме. Оттуда я и скачал проект и оформил по своему вкусу, добавив свою функцию вывода текста.
А сейчас будет код. Много кода.
Разбор интерфейса начну с модуля hardware. В нем инициализируется вся периферия МК: ног для контроля кнопок и энкодера, управления светодиодами, пищалкой, UART для связи с сигнальной платой и UART для отладки, счетный таймер, без прерываний тикающий раз в 1 мс. С его помощью сделаны задержки без остановки остальной программы. Объявлены дефайны для удобной работы со всем этим добром, а также коды возможных событий.
Внизу статьи прикреплен архив с файлами, которые хорошо прокомментированы. Здесь буду приводить только некоторые участки кода.
Функций тут немного:

void     HW_Init(void);                                       // инициализация периферии МК

// две функции обеспечивают связь с сигнальной платой генератора
void     HW_SetSignalParam(uint8_t CONTROL_x, int32_t value); // задать параметр сигнала
int32_t  HW_GetSignalParam(uint8_t CONTROL_x);                // получить параметр сигнала

// управление светодиодами на передней панели
void     HW_LedOn (uint16_t LED_x);                           // включить светодиод
void     HW_LedOff(uint16_t LED_x);                           // выключить светодиод
uint8_t  HW_IsLedOut();                                       // возвр. состояние св.диода OUT

Поясню только связь с сигнальной платой. Установка параметра сигнальной платы осуществляется по идентификатору команды. Список идентификаторов будет во второй статье. Тип параметра всегда int32_t. В связи с этим частоты задаются в мГц, а времена в мкс.

void HW_SetSignalParam(uint8_t CONTROL_x, int32_t value)
{
    USART_SendData(USART3, CONTROL_x);
    while(USART_GetFlagStatus(USART3, USART_FLAG_TXE) == RESET);
    USART_SendData(USART3, (uint8_t) value >> 24);
    while(USART_GetFlagStatus(USART3, USART_FLAG_TXE) == RESET);
    USART_SendData(USART3, (uint8_t) value >> 16);
    while(USART_GetFlagStatus(USART3, USART_FLAG_TXE) == RESET);
    USART_SendData(USART3, (uint8_t) value >> 8);
    while(USART_GetFlagStatus(USART3, USART_FLAG_TXE) == RESET);
    USART_SendData(USART3, (uint8_t) value);
    while(USART_GetFlagStatus(USART3, USART_FLAG_TXE) == RESET);
}

Чтобы получить значение какого-либо параметра, нужно в байт идентификатора команды добавить 1 в старший разряд и отправить сигнальной плате вместе с любым значением. После чего подождать приема данных. Никаких проверок на правильность не делается, т.к. интерфейсная и сигнальная платы находятся рядом друг с другом, скорость UART не высока, поэтому вероятность ошибки мизерная. На данный момент не замечено ни одной.

int32_t HW_GetSignalParam(uint8_t CONTROL_x)
{
    CONTROL_x |= 0x80;

    paramWaitFlag = 1;
    receivedParam = 0;
    HW_SetSignalParam(CONTROL_x, 0);

    while(paramWaitFlag);

    return receivedParam;
}

По управлению светодиодами я надеюсь вопросов не будет.
Перебираемся в файл main.c. Здесь у нас вызывается инициализация всего интерфейса, включая периферию. Опрос элементов управления осуществляется в главном цикле. Так достигается максимальное быстродействие отрисовки дисплея, т.к. не мешают прерывания. Опрос идет в таком порядке:
— энкодер
— матрицы кнопок
— кнопки под энкодером.
Полученный код события обрабатывается следующим образом:

/// повтор срабатывания как на компе----------------------------------------------------------

if(currentEvent == EVENT_NULL)        // кнопка не нажата
{
    lastEvent = EVENT_NULL;
}
else if(currentEvent != lastEvent)    // кнопку только что нажали
{
    lastEvent = currentEvent;
    TIM_COUNTER = 0;
}
else                                  // кнопку держим нажатой
{
    if(TIM_COUNTER < 800)             // после первого срабатывания ждем 800 мс
    {
        currentEvent = EVENT_NULL;
    }
    else                              // а потом по 200 мс (800-600)
    {
        TIM_COUNTER = 600;
    }
}

Благодаря этому происходит фильтрация дребезга, а также формируется реакция на нажатие кнопки, как на ПК:-нажал-сработало-большаяпауза-сработало-пауза-сработало-…-отпустил-
После этого, если код события не нулевой, он отправляется в обработчик верхнего уровня графического интерфейса, объект Screen:

if(currentEvent != EVENT_NULL)
{
   SCREEN_EventHandler(currentEvent);
}

По структурной схем, приведенной выше, видно, что интерфейс базируется на трех основных объектах:
Экран(Screen)->Вкладка(Tab)->Поле(Control)
Все эти объекты статические (скрытые), доступ к ним осуществляется через перечисления (за исключением экранов): TAB_x, CONTROL_x. Естественно, самым сложным является последний. Но начнем «сверху», с простого.
Я широко использую макросы, это позволяет очень быстро создавать объекты различных конфигураций. Все объекты интерфейса помимо вспомогательных функций имеют 3 основные: инициализация, отрисовка, обработка события.

void Init(void);
void Paint(void);
void EventHandler(Event_Type event);


Объекты Screen (Экраны)

Экран представляет собой структуру

typedef struct
{
    const char *name;
    Tab_Type    tabs[4];
    uint8_t     selectedTab;

} Screen_Struct;

Каждая структура содержит заголовок, 4 идентификатора вкладки и номер выделенной вкладки. Можно использовать одни и те же вкладки в разных экранах.
Создаются экраны с помощью макроса. Кроме того, имеется указатель на текущий экран, а также экран текущей формы сигнала.

#define CREATE_SCREEN(screen,name,tab1,tab2,tab3,tab4) \
Screen_Struct screen = {(const char *) name,           \
                        (Tab_Type) tab1,               \
                        (Tab_Type) tab2,               \
                        (Tab_Type) tab3,               \
                        (Tab_Type) tab4};


CREATE_SCREEN(screenCalibration, "Calibration",      TAB_BIAS_GAIN,
                                                     TAB_FILTER,
                                                     TAB_NULL,
                                                     TAB_NULL);
static volatile Screen_Struct *currentScreen;   // указатель на текущий экран
static volatile Screen_Struct *currentWave;     // указатель на экран текущей формы сигнала

В функции Init вызываем инициализацию объектов нижнего уровня, определяем текущий экран и активную вкладку

void SCREEN_Init(void)
{
    TAB_Init();                                 // инициализируем все вкладки

    currentScreen = &screenCalibration;         // задаем текущий экран
    currentScreen->selectedTab = 0;             // указываем активную вкладку
}

Функция отрисовки экрана отвечает только за заголовок, остальное рисуется другими объектами. Тут отмечу, что шрифты для дисплея я создаю в программе MikroElektronika — GLCD Font Creator. Мне она больше всего понравилась. Для создания нового шрифта и добавления его в проект нужна пара минут. Заметьте, что перед отрисовкой текста я задаю шрифт. А сам текст можно выравнивать по центру или краям. Кроме того, текст у меня не чистит место для себя, т.к. до него на этом месте может быть более длинный текст, поэтому место все равно чистить отдельно надо.

void SCREEN_Paint(void)
{
    //заливаем фон
    ILI9341_DrawFillRect(0,0,239,319,COLOR_BACKGROUND);

    int16_t i,pos;
    pos = 0;

    //верхняя линия \__________/ выделяет зону заголовка
    for(i=12; i>0; i--)
    {
        ILI9341_DrawPixel(228-i, i, COLOR_UNSELECTED_ITEMS);
    }

    ILI9341_DrawLine(216,12,216,320-12,COLOR_UNSELECTED_ITEMS);

    for(i=0; i<12; i++)
    {
        ILI9341_DrawPixel(216+i, 320-12+i, COLOR_UNSELECTED_ITEMS);
    }

    // заголовок экрана
    ILI9341_SetFontType(FONT_COURIER_NEW_11_18);
    ILI9341_DrawText(219,160,currentScreen->name, COLOR_UNSELECTED_ITEMS, ALIGN_CENTER);

    //вкладки
    CONTROL_ClearTabSpace();
    uint8_t sel;
    for(i = 0; i < 4; i++)
    {
        //перед отрисовкой проверяем на выделение
        TAB_Paint(currentScreen->tabs[i], i, (currentScreen->selectedTab == i));
    }
}

В обработчике функции EventHandler мы проверяем код события. Благодаря разбиению текста на функциональные блоки, такая большая функция достаточно удобно читается. Сначала проверяем конкретный код EVENT_OUT, т.к. он не привязан к графической части.

// НАЖАТА КНОПКА ВЫХОДА
if(event == EVENT_OUT)
{
    if(IsLedOut())
    {
        HW_SetSignalParam(CONTROL_OUT, 0);
        LED_Off(LED_OUT);
    }
    else
    {
        HW_SetSignalParam(CONTROL_OUT, 1);
        LED_On(LED_OUT);
    }
    return;
}


Затем проверяем код, функциональная ли была кнопка. Тут важный момент, если был начат ввод с цифровой клавиатуры, значит во вкладках отображаются единицы измерения, следовательно код надо передать на следующий уровень интерфейса — вкладке. Если ввод не начат, значит переходим на другую вкладку (если код текущей не соответсвует коду), либо меняем местами активные и неактивное поля параметров.

// НАЖАТА ФУНКЦИОНАЛЬНАЯ КНОПКА
if(IS_EVENT_FUNC_BUTTON(event))
{
    // проверяем, включен ли режим ввода с цифровой клавиатуры
    if(TAB_IsInputing(currentScreen->tabs[currentScreen->selectedTab]))
    {
        TAB_EventHandler(currentScreen->tabs[currentScreen->selectedTab], event);
    }
    // если не включен, выполняем обработчик объекта Screen
    else
    {
        uint8_t numFunc;

        switch(event)
        {
            case EVENT_FUNC1: numFunc = 0; break;
            case EVENT_FUNC2: numFunc = 1; break;
            case EVENT_FUNC3: numFunc = 2; break;
            case EVENT_FUNC4: numFunc = 3; break;
        }

        if(currentScreen->selectedTab == numFunc)
        {
            TAB_ToggleControl(currentScreen->tabs[currentScreen->selectedTab]);
        }
        else if(currentScreen->tabs[numFunc] != TAB_NULL)
        {
            currentScreen->selectedTab = numFunc;
        }
        SCREEN_Paint();
    }
}

Далее проверяем код на принадлежность к форме сигнала. Включаем соответствующий светодиод и отправляем команду. Не забываем сохранить указатель на текущий экран. Кроме того, т.к. в этой секции задается форма сигнала, сохраняется указатель на экран формы сигнала. На него будет осуществлен переход при выключении режима модуляции, ГКЧ или пакета. Дойдем до этого.

// НАЖАТА КНОПКА ВЫБОРА ФОРМЫ СИГНАЛА
else if(IS_EVENT_WAVE_BUTTON(event))
{
    LED_Off(LED_WAVE_ALL);
    switch (event)
    {
        case EVENT_WAVE_SINE:
             LED_On(LED_WAVE_SINE);
             currentScreen = &screenSine;
             currentWave = &screenSine;
             HW_SetSignalParam(CONTROL_WAVEFORM, WAVEFORM_SINE);
             break;

        case EVENT_WAVE_SQUARE:
             LED_On(LED_WAVE_RECT);
             currentScreen = &screenSquare;
             currentWave = &screenSquare;
             HW_SetSignalParam(CONTROL_WAVEFORM, WAVEFORM_SQUARE);
             break;

        case EVENT_WAVE_RAW:
             LED_On(LED_WAVE_RAW);
             currentScreen = &screenRaw;
             currentWave = &screenRaw;
             HW_SetSignalParam(CONTROL_WAVEFORM, WAVEFORM_RAW);
             break;

        case EVENT_WAVE_OTHER:
             LED_On(LED_WAVE_OTHER);
             currentScreen = &screenOther;
             currentWave = &screenOther;
             HW_SetSignalParam(CONTROL_WAVEFORM, WAVEFORM_OTHER);
             break;
    }

    SCREEN_Paint();
}

В следующей секции проверям принадлежность кода события к режиму работы генератора. Соответствующим образом управляем светодиодами и командами сигнальной плате. При выключении режима переходим на экран текущей формы сигнала.

// НАЖАТА КНОПКА ВЫБОРА РЕЖИМА СИГНАЛА
else if(IS_EVENT_MODE_BUTTON(event))
{
    switch (event)
    {
        case EVENT_MODE_MOD:
            // включаем режим модуляции
            if(currentScreen != screenModulation)
            {
                HW_LedOff(LED_MODE_ALL);
                HW_LedOn(LED_MODE_MOD);
                currentScreen = screenModulation;
                HW_SetSignalParam(CONTROL_MODE, MODE_MODULATION);
            }
            // выключаем режим модуляции
            else
            {
                HW_LedOff(LED_MODE_MOD);
                currentScreen = currentWave;
                HW_SetSignalParam(CONTROL_MODE, MODE_CONTINUOUS);
            }
            break;

        case EVENT_MODE_SWEEP:
            // включаем режим ГКЧ
            if(currentScreen != screenSweep)
            {
                HW_LedOff(LED_MODE_ALL);
                HW_LedOn(LED_MODE_SWEEP);
                currentScreen = screenSweep;
                HW_SetSignalParam(CONTROL_MODE, MODE_SWEEP);
            }
            // выключаем режим ГКЧ
            else
            {
                HW_LedOff(LED_MODE_SWEEP);
                currentScreen = currentWave;
                HW_SetSignalParam(CONTROL_MODE, MODE_CONTINUOUS);
            }
            break;

        case EVENT_MODE_BURST:
            // включаем режим пакета
            if(currentScreen != screenBurst)
            {
                HW_LedOff(LED_MODE_ALL);
                HW_LedOn(LED_MODE_BURST);
                currentScreen = screenBurst;
                HW_SetSignalParam(CONTROL_MODE, MODE_BURST);
            }
            // выключаем режим пакета
            else
            {
                HW_LedOff(LED_MODE_BURST);
                currentScreen = currentWave;
                HW_SetSignalParam(CONTROL_MODE, MODE_CONTINUOUS);
            }
            break;
    }

    SCREEN_Paint();
}

Ну и, наконец, если код не соответствует указанным выше секциям, просто отправляем его на следующий уровень — вкладкам.

// ВСЕ ОСТАЛЬНЫЕ КОДЫ ОТПРАВЛЯЕМ АКТИВНОЙ ВКЛАДКЕ
else
{
    TAB_EventHandler(currentScreen->tabs[currentScreen->selectedTab], event);
}


Объекты Tab (Вкладки)

Этих объектов уже побольше, чем экранов. Главное отличие от них — это наличие идентификатора (перечисления)

typedef enum
{
    TAB_FILTER               ,
    TAB_BIAS_GAIN            ,

    TAB_FREQ_PERIOD          ,
    TAB_AMPL_OFFSET          ,
    TAB_VMAX_VMIN            ,

    TAB_RAW_SYMMETRY         ,
    TAB_DUTY_CYCLE           ,

    TAB_SWEEP_START_STOP     ,
    TAB_SWEEP_TIME_TYPE      ,

    TAB_MOD_FREQ_PERIOD      ,
    TAB_MOD_DEPTH_WAVE       ,

    TAB_BURST_COUNT_PERIOD   ,
    TAB_BURST_TIME_PAUSE     ,
    TAB_BURST_PHASE          ,

    TAB_OTHER_WAVEFORM       ,
    TAB_NULL
} Tab_Type;

Структура вкладки простая и содержит только два идентификатора поля параметра (у меня это контрол).

typedef struct
{
    Control_Type    topControl;
    Control_Type    botControl;
} Tab_Struct;

Функций тоже побольше. Добавляется проверка вкладки на начало ввода с цифровой клавиатуры, а также функция смены активного и неактивного поля.

void    TAB_Init           (void);
uint8_t TAB_IsInputing     (Tab_Type TAB_x);
void    TAB_Paint          (Tab_Type TAB_x, uint8_t tabPos, uint8_t selected);
void    TAB_EventHandler   (Tab_Type TAB_x, Event_Type event);
void    TAB_ToggleControl  (Tab_Type TAB_x);

В файле tab.c создается массив указателей на объекты Tab. Размерность массива указывается как последний элемент перечисления. Соответственно появляется правило: нельзя добавлять новый элемент последним в перечислении (после TAB_NULL).
Далее идет макрос, с помощью которого создаются объекты вкладок.

volatile Tab_Struct *tabs[TAB_NULL];

#define CREATE_TAB(tab, topControlType, botControlType) \
Tab_Struct         tab={topControlType, botControlType};


CREATE_TAB(tabFilter   , CONTROL_FILTER, CONTROL_NULL);
CREATE_TAB(tabBiasGain , CONTROL_BIAS  , CONTROL_GAIN);

Теперь надо внести в массив указатели на объекты в соответствии с идентификаторами. Это делается в функции инициализации, где предварительно вызывается инициализация нижнего уровня — полей параметров. Также частично настраиваются поля (контролы)

void TAB_Init(void)
{
    uint8_t i;

    // запускаем инициализацию контролов
    CONTROL_Init();

    // обнуляем указатели в массиве, чтобы при задании координат не было ошибок
    for(i=0; i<TAB_NULL; i++)
    {
        tabs[i] = 0;
    }

    // присваиваем указателям значения
    tabs[TAB_FILTER]             = &tabFilter;
    tabs[TAB_BIAS_GAIN]          = &tabBiasGain;

    // задаем координаты и выделение контролам
    for(i=0; i<TAB_NULL; i++)
    {
        if(tabs[i] != 0)                // проверяем на корректность указателя
        {
            // верхний контрол
            if(tabs[i]->topControl != CONTROL_NULL)
            {
                CONTROL_SetSelected(tabs[i]->topControl, 1);    // выделен
                CONTROL_SetPosition(tabs[i]->topControl, 141);  // позиция
            }

            // нижний контрол
            if(tabs[i]->botControl != CONTROL_NULL)
            {
                CONTROL_SetSelected(tabs[i]->botControl, 0);    // не выделен
                CONTROL_SetPosition(tabs[i]->botControl, 67);   // позиция
            }
        }
    }
}

Функция отрисовки выводит на экран только одну вкладку, а если она выделена, то вызываются соответствующие функции отрисовки полей параметров.

void TAB_Paint(Tab_Type TAB_x, uint8_t tabPos, uint8_t selected)
{
    uint16_t pos;
    uint16_t color;

    // проверка на нулевой идентификатор
    if(TAB_x == TAB_NULL)
    {
        color = COLOR_UNSELECTED_ITEMS;
        CONTROL_DrawTabLine(tabPos, color); // рисуем невыделенную линию
        return;                             // и выходим
    }
    else
    {
        color = (selected) ? COLOR_SELECTED_ITEMS : COLOR_UNSELECTED_ITEMS;
        CONTROL_DrawTabLine(tabPos, color); // рисуем линию заданного экраном цвета
    }

    // задаем шрифт
    ILI9341_SetFontType(FONT_COURIER_NEW_11_18);

    // определяем координаты вывода текста во вкладках
    pos = tabPos*80+40;

    // отрисовка верхнего текста, если он есть
    if(tabs[TAB_x]->topControl != CONTROL_NULL)
    {
        ILI9341_DrawText(25, pos, CONTROL_Name(tabs[TAB_x]->topControl), color, ALIGN_CENTER);
    }

    // отрисовка нижнего текста, если он есть
    if(tabs[TAB_x]->botControl != CONTROL_NULL)
    {
        ILI9341_DrawText(2, pos, CONTROL_Name(tabs[TAB_x]->botControl), color, ALIGN_CENTER);
    }

    // если вкладка выделена, вызываем функции отрисовки контролов
    if(selected)
    {
        CONTROL_Paint(tabs[TAB_x]->topControl);
        CONTROL_Paint(tabs[TAB_x]->botControl);
    }
}

Функция вывода рамки вкладки DrawTabLine находится на уровень ниже, т.к. поле параметра (Control) выводит на вкладках возможные единицы измерения при нажатии на цифровую клавиатуру. Там тоже надо рисовать рамки.
При получении кода события функция отправляет его в активный контрол, а второй обновляет. Таким образом, например при изменении частоты генератора период, отображаемый ниже, тоже изменится.

void TAB_EventHandler(Tab_Type TAB_x, Event_Type event)
{
    if(TAB_x == TAB_NULL) return;

    if(TAB_CurrentControl(TAB_x) == topControl)
    {
        CONTROL_EventHandler(topControl, event);
        CONTROL_Update(tabs[TAB_x]->botControl);
    }
    else
    {
        CONTROL_EventHandler(botControl, event);
        CONTROL_Update(tabs[TAB_x]->topControl);
    }
}

Функция смены активного поля тоже довольно простая. Меняются флаги выделения полей (контролов). Эти флаги находятся не во вкладке только из-за цифровой клавиатуры, обработкой событий от которой занимаются контролы.

Надеюсь, эта часть получилась понятной. Легко наращивается функционал, формировать экраны можно из любых вкладок, а вкладки — из любых параметров (контролов). Весь интерфейс, показанный на видео, я сделал за 10-15 минут, без учета написания и отладки функций.
Во второй части будет описание контролов и их богатого функционала.

Я не исключаю ошибок в любой части софта. Интерфейс еще будет отшлифовываться, будет добавляться функционал. Но принципы, заложенные в основу, вероятнее всего будут сохранены. Однозначно будут оптимизированы зоны отрисовки, будет меньше мерцать экран при нажатии кнопок.
  • +7
  • 26 ноября 2015, 20:22
  • sva_omsk
  • 1
Файлы в топике: emblocks.zip

Комментарии (3)

RSS свернуть / развернуть
Файлы-то не добавил вчера. Обновил.
0
Что-то про модуляцию упоминали… ;-) а в схеме про это — ничего. Очень бы хотелось узнать, какие виды модуляции будут. 20 мегасэмплов — это, конечно, круто, но вот возможности STMки по модуляции, скорее всего, закончатся где-то в районе мегагерца. На синусе, таким образом, могли бы довести максимальную частоту до 10 МГц с модуляцией сигналом до 1 МГц. Легко реализуется AM, FM, PM, DSB. С SSB сложнее, нужен комплексный модулирующий сигнал. Однако тоже решаемо. Какие-то простые формы модулирующих сигналов (синус, пила, треугольник, прямоугольник) можно генерировать внутри вторым программным DDS. Другой вопрос — с внешней модуляцией, чем эту мегагерцовую полосу затащить в процессор. Аналог — проще всего, АЦП эту полосу вытянет. С цифрой сложнее. Хотелось бы сеть или USB. Но 405й проц сеть аппаратно не поддерживает. Для USB понадобится high-speed, что, наверное, затруднительно.
Что я этим хочу сказать. Функциональный генератор сигналов произвольной формы — это одно. Генератор синусоиды с разными видами модуляции — совсем другое, это ближе к возбудителю радиопередатчика. Там основные требования — стабильность опорной частоты, низкие уровни побочных излучений. Все же, генератор сигналов с модуляцией намного более востребованная вещь, чем без нее.
0
В первой версии хочу ограничиться умножением и сложением амплитуды сигналов. Формулы уже есть:

Честно говоря очень сильно сомневаюсь, что смогу вытянуть частоту модулированного сигнала хотя бы до 1 МГц. Внешней модуляции вобще нет. Есть вход синхронизации, будет использоваться, например, для запуска пакета. Там логические уровни.
Из внешних интерфейсов пока только UART. Дело в том, что я планировал серию устройств: генератор, БП, и третье, соединяющееся с обоими как раз по UART и управляющее ими. Это третье имело бы уже стандартный интерфейс: USB или Ethernet и имело бы ПО для снятия графиков, например АХ и АЧХ. Один не потяну)
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.