Хабрахабр

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

Привет, друзья!

В этой статье я расскажу как сделать простенький генератор сигналов на 4 канала — два аналоговых канала и два PWM канала. В прошлых статьях я рассказывал про свой проект и про его программную часть.

Аналоговые каналы

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

"Классические" формы волны включают: синусоидальная, меандр, треугольная и пилообразная волны.

image

Функция генерации данных волн в буфере имеет следующий вид

// *****************************************************************************
// *** GenerateWave ********************************************************
// *****************************************************************************
Result Application::GenerateWave(uint16_t* dac_data, uint32_t dac_data_cnt, uint8_t duty, WaveformType waveform)
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

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

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

Кроме того, реализована поддержка сенсорного экрана — при нажатии на неактивный канал он становится активным, при нажатии на активный канал меняется форма волны.

Код инициализации пользовательского интерфейса и обновления данных на экране выглядит так: Конечно же используется 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}; // Generator data ... }; // 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++) { // Generator data ... // 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) и число (передается номер канала).

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

// *****************************************************************************
// *** 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 теперь выделена в отдельный репозиторий и включена как субмодуль.

Ну а зачем мне нужен генератор сигналов, будет уже в следующей (или одной из следующих) статье.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть