Привет, друзья!
В прошлых статьях я рассказывал про свой проект и про его программную часть. В этой статье я расскажу как простенький генератор сигналов на 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.0F * i * PI) / (dac_data_cnt + 1)) + 1.0F) * 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
Организовать пользовательский интерфейс было решено в четыре прямоугольника, каждый имеет картинку выводимого сигнала, частоту и амплитуду/скважность. Для текущего выбранного канала текстовые данные выведены белым шрифтом, для остальных — серым.
Управление было решено делать на энкодерах: левый отвечает за частоту и текущий выбранный канал(изменяется при нажатии на кнопку), правый отвечает за амплитуду/скважность и форму волны(изменяется при нажатии на кнопку).
Кроме того, реализована поддержка сенсорного экрана — при нажатии на неактивный канал он становится активным, при нажатии на активный канал меняется форма волны.
Конечно же используется DevCore для осуществления всего этого. Код инициализации пользовательского интерфейса и обновления данных на экране выглядит так:
// *************************************************************************
// *** 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 теперь выделена в отдельный репозиторий и включена как субмодуль.
Ну а зачем мне нужен генератор сигналов, будет уже в следующей(или одной из следующих) статье.
Автор: Nick_Shl