...

суббота, 6 октября 2018 г.

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)
{
  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 для осуществления всего этого. Код инициализации пользовательского интерфейса и обновления данных на экране выглядит так:

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

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

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий