DevBoy: робимо генератор сигналів

Привіт, друзі!

У минулих статтях я розповідав про свій проект та про його програмну частину. У цій статті я розповім як зробити простенький генератор сигналів на 4 канали — два аналогових каналу і два PWM каналу.

Аналогові канали

Мікроконтролер STM32F415RG має у своєму складі 12-тибитный DAC (digital-to-analog) перетворювач на два незалежних канали, що дозволяє генерувати різні сигнали. Можна безпосередньо завантажувати дані в регістри перетворювача, але для генерації сигналів це не дуже підходить. Краще рішення — використовувати масив, в який генерувати одну хвилю сигналу, а потім запускати DAC з тригером від таймера і DMA. Змінюючи частоту таймера можна змінювати частоту генерованого сигналу.

Класичні” форми хвилі включають: синусоїдальна, меандр, трикутна і пилообразная хвилі.

Функція генерації даних хвиль в буфері має наступний вигляд

// *****************************************************************************
// *** GenerateWave ********************************************************
// *****************************************************************************
Result Application::GenerateWave(uint16_t* dac_data, uint32_t dac_data_cnt, uint8_t duty, WaveformType waveform)
{
 Result result;

 uint32_t max_val = (DAC_MAX_VAL * duty) / 100U;
 uint32_t shift = (DAC_MAX_VAL - max_val) / 2U;

switch(waveform)
{
 case WAVEFORM_SINE:
 for(uint32_t i = 0U; i < dac_data_cnt; i++)
{
 dac_data[i] = (uint16_t)((sin((2.0 F * i * PI) / (dac_data_cnt + 1)) + 1.0 F) * max_val) >> 1U;
 dac_data[i] += shift;
}
break;

 case WAVEFORM_TRIANGLE:
 for(uint32_t i = 0U; i < dac_data_cnt; i++)
{
 if(i <= dac_data_cnt / 2U)
{
 dac_data[i] = (max_val * i) / (dac_data_cnt / 2U);
}
else
{
 dac_data[i] = (max_val * (dac_data_cnt - i)) / (dac_data_cnt / 2U);
}
 dac_data[i] += shift;
}
break;

 case WAVEFORM_SAWTOOTH:
 for(uint32_t i = 0U; i < dac_data_cnt; i++)
{
 dac_data[i] = (max_val * i) / (dac_data_cnt - 1U);
 dac_data[i] += shift;
}
break;

 case WAVEFORM_SQUARE:
 for(uint32_t i = 0U; i < dac_data_cnt; i++)
{
 dac_data[i] = (i < dac_data_cnt / 2U) ? max_val : 0x000;
 dac_data[i] += shift;
}
break;

default:
 result = Result::ERR_BAD_PARAMETER;
break;
}

 return result;
}

Функції потрібно передати вказівник на початку масиву, розмір масиву, максимальне значення і необхідну форму хвилі. Після виклику масив буде заповнений семплами для однієї хвилі необхідної форми і можна запускати таймер для періодичного завантаження нового значення в DAC.

Читайте також  Архітектура складних чат-ботів

DAC в даному мікроконтролері має обмеження: типове settling time (час від завантаження нового значення в DAC і його появою на виході) становить 3 ms. Але не все так однозначно — даний час є максимальним, тобто зміна від мінімуму до максимуму і навпаки. При спробі вивести меандр ці завалені фронти дуже добре видно:

Якщо ж вивести синусоїдальну хвилю то завал фронтів вже не так помітний з-за форми сигналу. Проте якщо збільшувати частоту синусоїдальний сигнал перетворюється в трикутний, а при подальшому збільшенні зменшується амплітуда сигналу.

Генерація на 1 KHz (90% амплітуда):

Генерація на 10 KHz (90% амплітуда):

Генерація на 100 KHz (90% амплітуда):

Вже видно сходинки, тому що завантаження нових даних в DAC здійснюється з частотою 4 МГц.

Крім того, задній фронт пилоподібного сигналу завалений і знизу сигнал не доходить до того значення, до якого повинен. Це відбувається тому, що сигнал не встигає досягати заданого низького рівня, а ПО завантажує нові значення

Генерація на 200 KHz (90% амплітуда):

Тут вже видно як всі хвилі перетворилися в трикутник.

Цифрові канали

З цифровими каналами все набагато простіше — практично в будь-якому мікроконтролері є таймери дозволяють вивести PWM сигнал на виводи мікроконтролера. Використовувати найкраще 32-х бітний таймер — в такому разі не потрібно перераховувати преддетилель таймера, достатньо в один регістр завантажувати період, а в інший регістр завантажувати необхідну шпаруватість.

User Interface

Організувати інтерфейс було вирішено в чотири прямокутника, кожен має картинку виведеного сигналу, частоту і амплітуду/шпаруватість. Для вибраного каналу текстові дані виведені білим шрифтом, для інших — сірим.

Управління було вирішено робити на энкодерах: лівий відповідає за частоту і поточний вибраний канал (змінюється при натисканні на кнопку), правий відповідає за амплітуду/шпаруватість і форму хвилі (змінюється при натисканні на кнопку).

Читайте також  Легко додавати нові фічі в старий фреймворк? Муки вибору на прикладі розвитку SObjectizer-а

Крім того, реалізована підтримка сенсорного екрану — при натисканні на неактивний канал він стає активним, при натисканні на активний канал змінюється форма хвилі.

Звичайно ж використовується DevCore для здійснення всього цього. Код ініціалізації інтерфейсу і оновлення даних на екрані виглядає так:

Структура містить всі об’єкти UI

 // *************************************************************************
 // *** Structure for describes all visual elements for the channel *****
 // *************************************************************************
 struct ChannelDescriptionType
{
 // UI data
 UiButton box;
 Image img;
 String freq_str;
 String duty_str;
 char freq_str_data[64] = {0};
 char duty_str_data[64] = {0};
 // Data Generator
...
};
 // Visual channel descriptions
 ChannelDescriptionType ch_dsc[CHANNEL_CNT];

Код ініціалізації інтерфейсу

 // Create and show UI
 int32_t half_scr_w = display_drv.GetScreenW() / 2;
 int32_t half_scr_h = display_drv.GetScreenH() / 2;
 for(uint32_t i = 0U; i < CHANNEL_CNT; i++)
{
 // Data Generator
...
 // UI data
 int32_t start_pos_x = half_scr_w * (i%2);
 int32_t start_pos_y = half_scr_h * (i/2);
 ch_dsc[i].box.SetParams(nullptr, start_pos_x, start_pos_y, half_scr_w, half_scr_h, true);
 ch_dsc[i].box.SetCallback(&Callback, this, nullptr, i);
 ch_dsc[i].freq_str.SetParams(ch_dsc[i].freq_str_data, start_pos_x + 4, start_pos_y + 64, COLOR_LIGHTGREY, String::FONT_8x12);
 ch_dsc[i].duty_str.SetParams(ch_dsc[i].duty_str_data, start_pos_x + 4, start_pos_y + 64 + 12, COLOR_LIGHTGREY, String::FONT_8x12);
ch_dsc[i].img.SetImage(waveforms[ch_dsc[i].waveform]);
 ch_dsc[i].img.Move(start_pos_x + 4, start_pos_y + 4);
ch_dsc[i].box.Show(1);
ch_dsc[i].img.Show(2);
ch_dsc[i].freq_str.Show(3);
ch_dsc[i].duty_str.Show(3);
 }

Код оновлення даних на екрані

 for(uint32_t i = 0U; i < CHANNEL_CNT; i++)
{
ch_dsc[i].img.SetImage(waveforms[ch_dsc[i].waveform]);
 snprintf(ch_dsc[i].freq_str_data, NumberOf(ch_dsc[i].freq_str_data), "Freq: %7lu Hz", ch_dsc[i].frequency);
 if(IsAnalogChannel(i)) snprintf(ch_dsc[i].duty_str_data, NumberOf(ch_dsc[i].duty_str_data), "Ampl: %7d %%", ch_dsc[i].duty);
 else snprintf(ch_dsc[i].duty_str_data, NumberOf(ch_dsc[i].duty_str_data), "Duty: %7d %%", ch_dsc[i].duty);
 // Set gray color to all channels
ch_dsc[i].freq_str.SetColor(COLOR_LIGHTGREY);
ch_dsc[i].duty_str.SetColor(COLOR_LIGHTGREY);
}
 // Set white color to selected channel
ch_dsc[channel].freq_str.SetColor(COLOR_WHITE);
ch_dsc[channel].duty_str.SetColor(COLOR_WHITE);
 // Update display
 display_drv.UpdateDisplay();

Цікаво реалізована обробка натискання кнопки (являє собою прямокутник поверх якого малюються інші елементи). Якщо ви дивилися код, то повинні були помітити таку штуку: ch_dsc[i].box.SetCallback (&Callback, this, nullptr, i); спричинюється в циклі. Це завдання функції зворотного виклику, яка буде викликатися при натисканні на кнопку. У функцію передаються: адреса статичної функції статичної функції класу, вказівник this, і два користувальницьких параметрів, які будуть передані у функцію зворотного виклику — покажчик (не використовується в даному разі — передається nullptr) і число (передається номер каналу).

Читайте також  Нове в SObjectizer-5.5.23: виконання бажань чи скринька Пандори?

Ще з університетської лави я пам’ятаю постулат: “Статичні функції не мають доступу до не статичним членам класу“. Так ось це не відповідає дійсності. Оскільки статична функція є членом класу, то вона має доступ до всіх членів класу, якщо має посилання/вказівник на цей клас. Тепер поглянемо на функцію зворотного виклику:

// *****************************************************************************
// *** Callback for the buttons *********************************************
// *****************************************************************************
void Application::Callback(void* ptr, void* param_ptr, uint32_t param)
{
 Application& app = *((Application*)ptr);
 ChannelType channel = app.channel;
 if(channel == param)
{
 // Second click - change wave type
...
}
else
{
 app.channel = (ChannelType)param;
}
 app.update = true;
}

У першому рядку цієї функції відбувається “магія” після чого можна звертатися до будь-яких членів класу, включаючи приватні.

До речі, виклик цієї функції відбувається в іншій задачі (відтворення екрану), так що всередині цієї функції треба подбати про синхронізації. У цьому простенькому проекті “пари вечорів” я цього не зробив, тому що в даному конкретному випадку це не суттєво.

Вихідний код генератора завантажений на GitHub: https://github.com/nickshl/WaveformGenerator
DevCore тепер виділений в окремий репозиторій і включений як субмодуль.

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *