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

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

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

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


  1. 0o0
    06.10.2018 22:30
    +1

    Что-то типа того, только всегда с собой ;)
    play.google.com/store/apps/details?id=com.keuwl.functiongenerator&hl=en


    1. Nick_Shl Автор
      06.10.2018 23:07

      Именно «типа». Какая частота дискретизации при воспроизведении на смартфоне? 44.1 кГц? 48 кГц? 192 кГц? Т.е. генератор на основе смартфона физически не сможет вывести сигнал частотой 100 кГц.
      Кроме того, на смартфоне невозможно генерировать PWM сигналы с частотой в мегагерцы.
      А ещё в смартфоне на выходе стоит разделительный конденсатор и сигнал там колеблется относительно земли. В моей же «поделке» сигнал колеблется относительно половины напряжения питания и его смело можно подавать на на входы микросхем которые не толерантный к напряжениям вне диапазона 0-3.3 вольта.


      1. mickvav
        07.10.2018 08:25

        Ну, последняя-то как раз проблема решается делителем из пары резисторов + контролем амплитуды, а вот с частотным диапазоном у мобильника и правда беда-беда.


  1. u-235
    06.10.2018 23:13
    +1

    Если в функцию Application::Callback передаётся указатель на объект, то не проще ли не делать эту функцию статической?


    1. Nick_Shl Автор
      07.10.2018 00:14
      -1

      А как вы возьмёте указатель на не статическую функцию? В C++ вы не можете этого сделать. Можно получить указатель только на статические функции класса, но т.к. при вызове статических функций не передается указатель на объект, они могут работать только со статическими членами класса(об этом рассказывают в университете). А что бы работать с остальными им нужна ссылка или указатель на объект(а вот о такой возможности не говорят, да и в книжках прямого указания на это я не встречал).
      Есть ещё вариант с callback классом имеющим pure virtual функцию, но там свои нюансы. Например, невозможность использовать несколько функций обратного вызова в одном классе.


      1. u-235
        07.10.2018 00:58
        +2

        class ICallback {
        public:
                void Callback();
        };
        
        class Application: public ICallback {
        public:
        };
        
        void Application::ICallback::Callback()
        {
                std::cout << "Class Application Callback()\n";
        }
        
        class Invoker {
                ICallback *_callback;
        public:
                void SetCallback(ICallback *callback)
                {
                        _callback = callback;
                }
                void invoke()
                {
                        if (_callback != NULL) {
                                _callback->Callback();
                        }
                }
        };
        
        int main(int s, char** o)
        {
                Application app = Application();
                Invoker inv = Invoker();
        
                inv.SetCallback(&app);
                inv.invoke();
        
                return 0;
        }


        Можно сделать как то так.


        1. Nick_Shl Автор
          07.10.2018 05:44

          Попробовал. Ожидаемо не "взлетает": Error C2509 'Callback': member function not declared in 'Application'.

          Работает если сделать так
          class ICallback
          {
            public:
          	virtual void Callback() = 0;
          };
          
          class Application : public ICallback
          {
            public:
          	virtual void Callback();
          };
          
          void Application::Callback()
          {
            printf("Class Application Callback()\n");
          }
          
          class Invoker
          {
            ICallback *_callback;
          public:
            void SetCallback(ICallback *callback)
            {
              _callback = callback;
            }
            void invoke()
            {
              if (_callback != nullptr)
              {
                _callback->Callback();
              }
            }
          };
          
          int main(int s, char** o)
          {
            Application app = Application();
            Invoker inv = Invoker();
          
            inv.SetCallback(&app);
            inv.invoke();
          
            return 0;
          }


          1. u-235
            07.10.2018 11:55
            +1

            Попробовал. Ожидаемо не «взлетает»

            void Application::Callback()
            {
                    std::cout << "Class Application Callback()\n";
            }
            

            Так действительно не работает, но у меня в примере этот фрагмент выглядит по другому. Если нужно несколько одинаковых callback, то возможен такой вариант.
            Скрытый текст
            #include <iostream>
            
            class ICallback {
            public:
                    virtual void callback(){}
            };
            
            class Invoker {
                    ICallback *_callback;
            public:
                    void set_callback(ICallback *icallback)
                    {
                            _callback = icallback;
                    }
            
                    void invoke()
                    {
                            if (_callback != NULL) {
                                    _callback->callback();
                            }
                    }
            };
            
            class Application {
            private:
                    class AlarmHandler: public ICallback {
                            void callback()
                            {
                                    std::cout << "In AlarmHandler callback\n";
                            }
                    };
            
                    class KeyHandler: public ICallback {
                            void callback()
                            {
                                    std::cout << "In KeyHandler callback\n";
                            }
                    };
            
            public:
                    ICallback* get_alarm_handler()
                    {
                            static AlarmHandler alarm_handler = AlarmHandler();
                            return &alarm_handler;
                    }
            
                    ICallback* get_key_handler()
                    {
                            static KeyHandler key_handler = KeyHandler();
                            return &key_handler;
                    }
            };
            
            int main(int s, char** o)
            {
                    Application app = Application();
                    Invoker inv_alarm = Invoker();
                    Invoker inv_key = Invoker();
            
                    inv_alarm.set_callback(app.get_alarm_handler());
                    inv_key.set_callback(app.get_key_handler());
                    inv_alarm.invoke();
                    inv_key.invoke();
                    return 0;
            }


            1. Nick_Shl Автор
              07.10.2018 18:14

              А разве внутренние классы AlarmHandler и KeyHandler имеют доступ ко всем членам класса Application? В таком случае в Application придется еще публичный интерфейс делать для вызова из этих внутренних классов.


              1. u-235
                07.10.2018 18:25

                К приватным не имеют доступа. Это вроде бы лечится объявлением классов дружественными. Но это не точно.


      1. Wilk
        07.10.2018 03:42
        +2

        А как вы возьмёте указатель на не статическую функцию? В C++ вы не можете этого сделать.


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

        А что бы работать с остальными им нужна ссылка или указатель на объект (а вот о такой возможности не говорят, да и в книжках прямого указания на это я не встречал).


        С одной стороны, да, явно про это не пишут. Но на мой взгляд ничего необычного и секретного, как Вы это пытаетесь преподнести, нет. Строго говоря, нестатические функции-члены осуществляют доступ к нестатическим переменным- и функциям-членам объекта класса не при помощи какой-то магии, а посредством указателя this, указание которого можно опустить при доступе. Статические функции-члены класса отличаются исключительно тем, что в них данный указатель не передаётся. Мне кажется, не трудно сопоставить факты, чтобы понять, что статической функции нужна ссылка (в общем смысле) на объект, к нестатическим членам которого необходимо произвести доступ.

        Я извиняюсь за такой уровень моего занудства, просто я искренне не понимаю, почему Вы преподносите обыденную вещь как магию. Я не помню, когда решение подобной задачи вызывало у меня сложности, при том, что опыта у меня не так уж и много.


        1. Nick_Shl Автор
          07.10.2018 05:57

          Строго говоря, можно. Другой вопрос, что указатель на функцию-член без указателя на объект, для которого нужно её вызвать, большого смысла не имеет.
          Как? Можно код для вывода на экран адреса функции-члена класса?
          Простейший код:
          printf("Ponter to SetCallback(): 0x%08X", &SetCallback);
          не компилируется: Error C2276 '&': illegal operation on bound member function expression
          Мне кажется, не трудно сопоставить факты, чтобы понять, что статической функции нужна ссылка (в общем смысле) на объект, к нестатическим членам которого необходимо произвести доступ.
          Это вам кажется теперь, когда эти факты за вас сопоставлены, сделан вывод, причем все это не противоречит вашим собственным знаниям и опыту. Есть такая штука как инерция мышления — если в университете говорили нельзя, значит нельзя и даже не пытаемся это сделать.
          Изначально была идея взять указатель на функцию-член класса, преобразовать её к функции вида void (*callback)(void* ptr, void* param_ptr, uint32_t param); и вызывать передавая в качестве первого указателя указатель на объект. Это не сработало, потому что указатель на функцию-член класса, даже если и можно взять, то нельзя преобразовать ни к чему другому. И вот только после этого до меня дошло попробовать со статической функцией.


          1. kITerE
            07.10.2018 11:03

            В C++ есть указатели на не статические функции классов/структур: https://ideone.com/XiIW9b


            Но они не эквивалентны простому адресу начала метода (в примере это видно хотя бы по sizeof). Например так: https://itanium-cxx-abi.github.io/cxx-abi/abi.html#member-pointers


            Изначально была идея взять указатель на функцию-член класса, преобразовать её к функции вида void (callback)(void ptr, void* param_ptr, uint32_t param); и вызывать передавая в качестве первого указателя указатель на объект.

            У вызовов методов может быть отдельное соглашение о передаче параметров — https://en.wikipedia.org/wiki/X86_calling_conventions#thiscall


          1. Wilk
            07.10.2018 13:47

            Как? Можно код для вывода на экран адреса функции-члена класса?

            Во-первых, я привёл ссылку на авторитетный источник — FAQ на сайте isocpp.org, где показано, как взять адрес функции-члена.

            Во-вторых:
            std::printf ("Ponter to SetCallback(): 0x%p", &UiButton::SetCallback);
            

            Чтобы не возникло вопросов: да, я проверил такой синтаксис. Единственное что, проверял на своём коде, т.к. скачивать и собирать что-то под STM нет никакого желания. Если функция-член SetCallback принадлежит другому классу — используйте исправьте приведённый выше код.

            Это вам кажется теперь, когда эти факты за вас сопоставлены, сделан вывод, причем все это не противоречит вашим собственным знаниям и опыту.

            Кто эти факты за меня сопоставлял? Кто делал какие-то выводы? Или Вы считаете, что я увидел использование указателя на объект класса в статической функции-члене того же класса в Вашем коде впервые, и решил, что это очевидно? А ничего, что я использовал точно такой же подход в своём коде задолго до того, как Вы решили написать данную статью?

            Есть такая штука как инерция мышления — если в университете говорили нельзя, значит нельзя и даже не пытаемся это сделать.

            Я не понимаю, почему Вы так фиксируетесь на том том, что говорили в университете, причём строго на некорректной формулировке. У меня своя голова на плечах есть, чтобы пробовать что-то за пределами того, что давали на парах в университете. Я уж не буду говорить, что на пары по C++ я не ходил, т.к. ничего нового для меня там не рассказывали.


            1. Nick_Shl Автор
              07.10.2018 18:18

              Во-вторых:
              std::printf ("Ponter to SetCallback(): 0x%p", &UiButton::SetCallback);
              Чтобы не возникло вопросов: да, я проверил такой синтаксис.
              Убедили. Похоже можно взять указатель на функцию член и положить её в переменный аргумент-лист(кладется она туда как указатель на функцию член), а потом вынуть как что угодно. Но вот сделать вот так:
              std::printf ("Ponter to SetCallback(): 0x%p", (void*)&UiButton::SetCallback);
              не получится. Да и не нужно — все решается теми методами, которые тут уже обсудили.


      1. lorc
        07.10.2018 21:29

        С помощью std::bind. 2018 год на дворе, как никак.


  1. Wilk
    07.10.2018 00:02
    +1

    Здравствуйте!

    В приведённом коде функции Application::Callback я вижу сразу несколько проблем:

    1. Использование void* для передачи указателей на объекты. Помимо нарушения идеи безопасности на уровне типов, это также усложняет понимание сигнатуры функции. EDIT: Видимо, это вынужденная мера, т.к. UiButton хочет такую сигнатуру функции обратного вызова. Моего бессмысленного недовольства это не уменьшает, но позволяет понять, почему так.
    2. Отсутствие проверки указателей на nullptr. Если данные проверки были опущены для простоты понимания в рамках статьи — стоит указать на это в тексте статьи, чтобы не возникало непонимания. Если же проверок нет, то их разумно добавить, как минимум в виде assert'ов.
    3. Отсутствие проверки типов аргументов. Раз уж передаются произвольные указатели, то необходимо проверять, что это указатели на объекты правильных типов. Простейшим, хотя и не самым эффективным, является использование dunamic_cast(). Более эффективных методов много, требуют они написания различного объёма вспомогательного кода. Но лучшим решением является, разумеется, использование правильных типов параметров функции.

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

    Как бы Вам сказать… Это не совсем корректно сформулированный постулат. Идея, заложенная в постулате, заключается в том, что нельзя в статических функциях-членах класса использовать нестатические переменные-члены и функции-члены без объекта данного класса. Поэтому, Вы уж извините, никакой магии Вы не используете.


    1. Dmitri-D
      07.10.2018 06:16
      +1

      Если реализуется взаимодействие между С и C++, то такие callbacks с void* opaque pointer являются наистандартнейшим решением. Строго говоря, я не припомню иных вариантов, разве только совсем без void*, но тогда это довольно неприятная картина и начинаются танцы со всякими offsetof вычислениями по другому известному указателю.

      Проблемы с callback реализуемыми в с++:
      1) с++ в callback должен сам ловить все свои Exceptions, иначе вызывающий С код может работать предельно некорректно — например оставлять занятые мутексы, не освобожденные ресурсы и пр.
      2) в общем случае с++ должен сам убедиться, что в указателе именно экземпляр нужного класса. dynamic_cast для этого годится плохо, т.к. dynamic_cast лишь предоставляет «навигацию» по дереву наследования полиморфных классов, а тут на входе вовсе не класс, а указатель на нечто (void*) с которым dynamic_cast работать не должен (допускаю, что возможно какой-то компилятор по ошибке позволяет, но стандарт чётко это запрещает).
      С другой стороны, с++ часть может «полагаться» на правильную работу С части и ожидать в ответе именно тот указатель, который был ей передан ранее. Тогда нужно гарантировать что lifetime объекта будет достаточен, до всех вызовов callback с этим классом.
      Есть еще вариант — передать указатель на память, в котором разместить признак (просто ID объекта) и указатель на объект. Эта память может быть частью самого объкта, и в деструкторе, если вдруг объект досрочно уничтожается, можно этот ID менять на другой — легче ловить dangling pointers. Правда это вовсе не гарантирует что следующий размещенный объект не будет создан в том же месте кучи.


      1. Wilk
        07.10.2018 14:09

        Здравствуйте!

        Если реализуется взаимодействие между С и C++, то такие callbacks с void* opaque pointer являются наистандартнейшим решением.

        С этим я полностью согласен. В рамках же рассматриваемого в статье кода взаимодействия между C и C++, насколько я могу судить, не наблюдается, т.к. Application::Callback используется в связке с классом UiButton, т.е. это не C. Насколько я могу судить, весь код написан на C++, и в моём представлении в этом случае лучше использовать механизмы C++.

        в общем случае с++ должен сам убедиться, что в указателе именно экземпляр нужного класса. dynamic_cast для этого годится плохо, т.к. dynamic_cast лишь предоставляет «навигацию» по дереву наследования полиморфных классов, а тут на входе вовсе не класс, а указатель на нечто (void*) с которым dynamic_cast работать не должен (допускаю, что возможно какой-то компилятор по ошибке позволяет, но стандарт чётко это запрещает).

        Признаю, был неправ. Для проверки типа объекта, размещённого по адресу, хранимому в void*, использовать dynamic_cast нельзя. Произвёл небольшие опыты, которые показали, что dynamic_cast либо не будет работать правильно, либо приложение аварийно завершит работу. С dynamic_cast всё ещё можно что-то сделать, но это потребует передачи указателя на некоторый базовый тип, что, мягко говоря, является сомнительной идеей в общем случае, особенно при взаимодействии между C и C++.

        Относительно же времени жизни и т.п.: ох, как же у меня всегда болит голова, когда я вижу передаваемые куда-либо указатели на объекты. Я знаю, что эти объекты будут жить, сколько надо, но как-то неспокойно мне на фоне страшных рассказов про несоответствие времени жизни и времени использования объекта.


        1. Dmitri-D
          07.10.2018 18:00

          В рамках же рассматриваемого в статье кода взаимодействия между C и C++, насколько я могу судить, не наблюдается, т.к. Application::Callback используется в связке с классом UiButton


          Да, я видел это, но не смотрел что лежит под UiButton. Если UiButton лишь wrapper вокруг С библиотеки, то могу допустить что интерфейс callback напрямую вытащен наружу для упрощения.
          С другой стороны, вы правы — сам UiButton уже С++ и мог бы взять на себя функции взаимодействия с С++ приложением более подобающим для С++ способом. Для этого нужны или шаблоны или полиморфные классы. Т.е. библиотека должна знать API потребителя (приложения) — т.е. вызывать его методы — или совсем не зная его классов, работая через шаблоны, или должна знать лишь базовые классы, которые в ней же в библиотеке реалиованы и которые потребитель отнаследует и реализует соответственно.

          Я знаю, что эти объекты будут жить, сколько надо, но как-то неспокойно мне на фоне страшных рассказов

          Всякое бывает. Если время жизни не получается легко контролировать, то используется shared_ptr. Решение не идеальное, конечно, но в ряде случаев ничего другого не остается.


    1. Nick_Shl Автор
      07.10.2018 06:21

      1) А как передать указатели на разные объекты в UiButton для последующей передачи в callback функцию? Я знаю только один такой способ — привести к единому типу.

      2) Замечание верное, в данном случае — это просто упрощение и допущение: тут всего 4 кнопки, создаются они вначале и им тут же всем задаются callback функции с передачей указателя this, т.е. nullptr в данном случае там в теории не должен появится никогда.

      3) "Правильных" — это каких? Из тех что я знаю — использование callback класса с виртуальной функцией. Пример я приводил выше. Минус — в классе Application будет всего одна callback функция для всех объектов UiButton. Впрочем, она и сейчас одна, так что, возможно, переделаю именно на такой способ.
      А еще мне не очень нравится в таком способе, что если понадобится еще какой-нибудь объект(UiScroll например), то уже нужно будет множественное наследование в классе Application. Ну и дополнительно еще где-то таблицы виртуальных функций будут место занимать. Зато снимутся все вопросы с типами и безопасностью.

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


      1. Wilk
        07.10.2018 15:00

        1) А как передать указатели на разные объекты в UiButton для последующей передачи в callback функцию? Я знаю только один такой способ — привести к единому типу.


        Как вариант, использовать объект специального класса для вызова функции обратного вызова:
        class ICallback {
        public:
        // Можно использовать NVI при желании/по необходимости.
            void operator()(// Параметры функции) = 0;
        };
        
        
        class ApplicationCallbackWrapper : public ICallbackWrapper {
        public:
            ApplicationCallbackWrapper (Application* i_application)
            : application_ {i_application} {
                /* 
                    При необходимости - проверка указателя на nullptr. Если считается,
                    что вызов может быть NOOP, то можно разрешить использование нулевого
                    указателя.
                */
            }
        
            void operator()(// Параметры функции) override {
                if (!application_) {
                    /*
                        Можно добавить обработку по умолчанию. Например, вывод 
                        предупреждения в режиме отладки.
                    */
        
                    return;
                }
        
                // Необходимая обработка аргументов функции.
        
                /*
                    Будем считать, что существует одна нестатическая функция-член, 
                    которую необходимо вызывать в функции обратного вызова. В случае, 
                    если требуется вызывать различные функции, можно написать несколько
                    подобных классов-обёрток - по одному на каждую функцию. Если выбор
                    функции должен осуществляться на основе аргументов функции - 
                    необходимо добавить соответствующую обработку и
                */
                application_->Callback(// Аргументы для вызова основной функции);
            }
        
        
        private:
            /*
                Можно использовать std::weak_ptr, что позволит узнавать об удалении
                объекта. Использование данного подхода требует, чтобы время жизни
                объекта (*application_) контроллировалось std::shared_ptr, что 
                ограничивает возможность создания объектов ApplicationCallbackWrapper
                из функций-членов класса Application. Данное ограничение можно обойти 
                через использование интрузивного подсчёта ссылок.
            */
            Application* application_;
        };
        
        
        /*
            При желании можно использовать владеющий (std::unique_ptr, 
            std::shared_ptr если требуется использовать один объект обратного вызова 
            из нескольких объектов) либо не владеющий (std::weak_ptr) указатель.
            В примене простой указатель для описания общей идеи. Если объект UiButton 
            владеет функтором обратного вызова, и используется обычный указатель, то 
            следует удалить объект при задании нового функтора обратного вызова и в 
            деструкторе.
        */
        UiButton::SetCallback (ICallback* i_callback) {
            if (!i_callback) {
                // Необходимая обработка.
            }
        
            // callback_ - переменая-член класса UiButton для хранения функтора обратного вызова
            callback_ = i_callback;
        }
        
        
        UiButton::SomeFunction (...) {
            // ...
        
            if (callback_) {
                (*callback_)(// Аргументы);
            }
        
            // ...
        }
        
        
        int main (...) {
            // Используются простый указатели для краткости
            Application* application = new Application{};
        
            UiButton* button = new UiButton{};
        
            button->SetCallback (new ApplicationCallbackWrapper{application});
        }
        


        Альтернативно, можно использовать std::function:
        class ApplicationCallbackWrapper {
            /*
                Такой же класс, как выше, но без наследования.
            */
        };
        
        
        class UiButton {
            using Callback = std::function <void()>;
            // ...
        }
        
        
        UiButton::SetCallback (UiButton::Callback i_callback) {
            if (!i_callback) {
                // Необходимая обработка.
            }
        
            // callback_ - переменая-член класса UiButton для хранения функтора обратного вызова
            callback_ = i_callback;
        }
        
        
        UiButton::SomeFunction (...) {
            // ...
        
            if (callback_) {
                callback (// Аргументы);
            }
        
            // ...
        }
        
        
        int main (...) {
            // Используются простый указатели для краткости
            Application* application = new Application{};
        
            UiButton* button = new UiButton{};
        
            button->SetCallback (ApplicationCallbackWrapper{application});
        
            /*
                Если можжно гарантировать, что время жизни application больше времени 
                жизни button, то можно использовать лямбда-функцию.
            */
            button->SetCallback ([application](// Параметры функции){
                // Использование application.
            });
        
        
            // Так тоже можно
            button->SetCallback (SomeFreeFunction);
        }
        


        1. Wilk
          07.10.2018 16:36

          Извиняюсь, в коде ошибки везде, где я закомментировал параметры функции. Должны быть комментарии в стиле /**/.


  1. MikeVC
    07.10.2018 06:59

    Слабенький генератор получается. Звуковых частот да и то…
    Может лучше на DDS?
    На той же AD9850


  1. bugdesigner
    07.10.2018 10:31
    +1

    Старенький "ламповый" осциллограф! Когда-то у меня подобный Phillips был...


    1. tormozedison
      07.10.2018 12:00

      Побуду буквоедом, но PhiLLips — это отвёртка, а PhiLips — производитель техники.


  1. SergeyMax
    07.10.2018 10:56
    +1

    settling time (...) составляет 3 ms

    Микросекунд наверное, всё-таки.
    И что это за странные всплески у вас на всех осциллограммах?


  1. Alex_ME
    07.10.2018 16:06

    Я что-то не понял, зачем какие-то заморочки со статическими методами, какими-то кастами и прочим или java-style передача интерфейсов. Чем этот метод плох? Также можно и темплейт использовать. Пруф, можете проверить, что работает


    Код
    #include <iostream>
    using namespace std;
    
    class BaseFoo
    {
    public:
        virtual void bar()=0;
    };
    
    class Foo1: public BaseFoo
    {
    public:
        virtual void bar() override
        {
            cout << "Foo1::bar()\n";
        }
    };
    
    class Foo2: public BaseFoo
    {
    public:
        virtual void bar() override
        {
            cout << "Foo2::bar()\n";
        }
    };
    
    void func(BaseFoo* ptr, void (BaseFoo::*func)())
    {
        (ptr->*func)();
    }
    
    int main()
    {
        Foo1 foo1;
        Foo2 foo2;
        func(&foo1, &BaseFoo::bar);
        func(&foo2, &BaseFoo::bar);
        return 0;
    }


    1. u-235
      07.10.2018 16:24

      А чем это отличается от Java-style? И зачем func второй параметр?


      1. Alex_ME
        07.10.2018 17:29

        Выше был пример, когда делали интерфейс нужного коллбека, передавали и вызывали. Как зачем? Это, собственно, та функция-член другого класса, которую мы хотим вызвать. Внутри метода func мы можем даже не знать, что это за метод. Мы можем вместо BaseFoo* использовать шаблон и даже не знать, что за класс. Такой подход применяется, к примеру, в std::thread. Я не стал писать пример с шаблоном, чтобы показать, что с полиморфизмом тоже работает.


        Код
        #include <iostream>
        using namespace std;
        
        class Foo
        {
        public:
            void bar()
            {
                cout << "Foo::bar()\n";
            }
        };
        
        template<class T>
        void func(T* ptr, void (T::*func)())
        {
            (ptr->*func)();
        }
        
        int main()
        {
            Foo foo;
            func(&foo, &Foo::bar);
            return 0;
        }


        1. u-235
          07.10.2018 17:39

          Но ведь ваш класс Foo — это и есть тот же самый интерфейс. В чем принципиальная разница?


          1. Alex_ME
            07.10.2018 18:02

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


            Также у нас может быть несколько функций-членов с подходящей сигнатурой, мы можем передать любую из них. Например, у нас есть класс Window, который содержит два объекта Button. Мы хотим, чтобы нажатие на них обрабатывались независимыми обработчиками.


            Interface-way
            class MyWindow: Window, OnClickListener
            {
            public:
                MyWindow()
                {
                    btn1.setOnClickListener(this);
                    btn2.setOnClickListener(this);
                }
            
                // Implement IClickListener (View - какой-нибудь базовый класс Button)
                virtual onClick(View& sender) override
                {
                    // Кто вызвал метод? Надо проверить sender
                }
            }


            1. Nick_Shl Автор
              07.10.2018 18:28

              Ну первый пример, может быть, и не очень наглядный. Но если шаблон использовать, мы передать указатель на фунцию-член, в то время как принимающая функция ничего не знает о нашем классе/базовом классе.
              Как это "не знает"? Очень даже знает! Потому что шаблонный класс есть только в исходниках, в скомпилированном коде будет много почти одинаковых классов с разными типами.
              Да, можно в классе UiButton объявить указатель на callback шаблонным типом, и функция SetCallback() может принимать шаблонный тип. Но тогда для каждого типа callback'ов будет создаваться отдельный класс UiButton со свои кодом. Если callback один(как в этой статье) — проблем нет, будет все равно один экземпляр UiButton. А вот если программа посложнее, с множеством классов использующих UiButton, то объем исполняемого кода в бинарнике начнет расти очень даже значительно.


              1. Alex_ME
                07.10.2018 18:31

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


                P.S. Собственно, я совершенно не претендую на то, что мой вариант самый лучший и/или единственно правильный. Просто это тот метод, который первый приходит мне в голову при упоминании ссылки на функцию-член.


    1. Wilk
      07.10.2018 16:28

      Здравствуйте!

      Помимо уже заданных вопросов у меня есть ещё один: при чём тут Java?

      Также можно и темплейт использовать.

      Для чего Вы предлагаете использовать шаблоны?


      1. Alex_ME
        07.10.2018 17:35

        Помимо уже заданных вопросов у меня есть ещё один: при чём тут Java?

        Ну, я часто встречал подход, когда класс реализует некий интерфейс слушателя события, в котором требуемый метод обратного вызова, и передают его куда надо. В Android такое постоянно.


        Для чего Вы предлагаете использовать шаблоны?

        Чтобы можно было передать указатель на функцию-член произвольного класса. Например, так реализовано в std::thread: https://stackoverflow.com/a/10673671


        1. Wilk
          07.10.2018 17:52

          Ну, я часто встречал подход, когда класс реализует некий интерфейс слушателя события, в котором требуемый метод обратного вызова, и передают его куда надо. В Android такое постоянно.

          Паттерн проектирования наблюдатель (если я конечно не ошибаюсь в данном случае) является универсальным и его применимость не зависит от языка программирования.

          Распространённость варианта с интерфейсами в Java можно объяснить тем, что свободных функций нет (насколько я знаю), а сам язык позиционирует себя как объектно-ориентированный, что приводит к созданию классов на каждый чих.

          В C++ единой универсальной парадигмы нет, да и язык несколько старше, с определённым багажом, в результате чего варианты с интерфейсами типа IObservable и IObserver (которые, разумеется, не интерфейса, а абстрактные либо чисто абстрактные классы) существуют на ровне с сигналами-слотами, функциями обратного вызова, событиями и другими вариациями на тему реализации того же самого паттерна.

          Если я заврался на почве собственного невежества — покорнейше прошу поправить меня.

          Чтобы можно было передать указатель на функцию-член произвольного класса. Например, так реализовано в std::thread: stackoverflow.com/a/10673671


          Могу я попросить Вас привести короткий пример кода, а то никак не могу сообразить, где и как надо использовать шаблоны в данном случае. Самым гибким, на мой взгляд, является вариант с использованием std::function, который я описал выше.


          1. Alex_ME
            07.10.2018 18:29

            Распространённость варианта с интерфейсами в Java можно объяснить тем, что свободных функций нет (насколько я знаю), а сам язык позиционирует себя как объектно-ориентированный, что приводит к созданию классов на каждый чих.

            Именно поэтому я и назвал Java-style. Я не могу утверждать, что именно в Java это наиболее распространено, исключительно по скромному опыту Android разработки. В C# тоже нет свободных функций, но там есть делегаты (что-то вроде типизированного указателя на функцию с ссылки на объект) и события (фактически observer на уровня языка). А в Java нет и идут On***Listener на каждый чих.


            Могу я попросить Вас привести короткий пример кода, а то никак не могу сообразить, где и как надо использовать шаблоны в данном случае.

            Пример кода

            Как-то так. Здесь DataLoader ничего не знает о классах, которые его используют, и чьи коллбеки он вызывает.


            #include <iostream>
            using namespace std;
            
            struct SomeData{};
            
            template<class T>
            class  DataLoader
            {
            public:
                DataLoader(T* ptr, void (T::*callback)(SomeData))
                    : _callback(callback), _ptr(ptr)
                {}
            
               void beginLoading()
                { 
                    //...
                    cout << "DataLoader::beginLoading()\n";
                    SomeData some_data;
                    (_ptr->*_callback)(some_data); 
                }
            private:
                T* _ptr;
                void (T::*_callback)(SomeData);    
            };
            
            class Foo
            {
            public:
                void foo()
                {
                    cout << "Foo::foo()\n";
                    // do something        
                    DataLoader<Foo> loader(this, &Foo::callback);
                    loader.beginLoading();
                }
            
                void callback(SomeData data)
                {
                    cout << "Foo::callback()\n";
                }
            };
            
            class Bar
            {
            public:
                void bar()
                {
                    cout << "Bar::bar()\n";
                    // do something        
                    DataLoader<Bar> loader(this, &Bar::data_loaded);
                    loader.beginLoading();
                }
            
                void data_loaded(SomeData data)
                {
                    cout << "Bar::data_loaded()\n";
                }
            };
            
            int main()
            {
                Foo foo;
                Bar bar;
            
                foo.foo();
                bar.bar();
                return 0;
            }


            1. Wilk
              07.10.2018 18:40

              Да, оверхед есть. Я не измерял, но думаю, что порядка 10-20 нс на современных процессорах (сужу по оверхеду boost::type_erasure). Создавать проблемы это будет только при очень высокой частоте вызовов и при высокой загрузке системы (load average 1.0 и выше). По крайней мере, так мне говорят голоса в моей голове.

              По коду: про такой вариант не подумал, потому что Вами предложено обобщённое решение проблемы, а я пытался придумать, как применить шаблоны в рамках представленного в статье кода без его сильного изменения. Единственным недостатком Вашего варианта является, собственно, статический полиморфизм и сопутствующая невозможность использования прямолинейного использования динамического полиморфизма для объектов типа DataLoader. Решить это можно через стирание типов, но это приведёт к тому же, что происходит в std::function или boost::type_erasure, только самодельному.

              Кстати, можно решить всю задачу целиком с использованием boost::type_erasure. Тогда уже можно передавать более сложные обработчики обратного вызова (если можно это так назвать), со сложным интерфейсом и т.п., без использования механизмов наследования напрямую. Наследование при этом магическим образом перенесётся в код, сгенерированный boost::type_erasure.