Синглтон размещающий объекты в ROM и статические переменные(С++ на примере микроконтроллера Cortex M4)

в 11:59, , рубрики: c++, cortex-m, cortex-m4, IAR, singleton, микроконтроллеры stm, Программирование, программирование микроконтроллеров, шаблоны c++

image

В предыдущей статье Где хранятся ваши константы на микроконтроллере CortexM (на примере С++ IAR компилятора), был разобран вопрос о том, как расположить константные объекты в ROM. Теперь же я хочу рассказать, как можно использовать порождающий шаблон одиночка для создания объектов в ROM.

Введение

Очень много было уже написано про Singleton(далее по тексту Синглтон) его положительные и отрицательные стороны. Но несмотря на его недостатки, у него есть очень много полезные свойства особенно в контексте встроенного ПО для микроконтроллеров.

Начнем с того, что для надежного ПО микроконтроллеров, объекты не рекомендуется создавать динамически, а потому и нет необходимости удалять их. Зачастую объекты создаются один раз и живут от момента запуска устройства, до его выключения. Таким объектом может быть даже ножка порта, она создается единожды, и точно никуда не денется во время работы приложения и она очевидно может быть Синглтоном. Кто-то должен создавать такие объекты и это может быть Синглтон.

Так же Синглтон даст вам гарантию, что та же ножка не будет создана дважды, если вдруг она используется в нескольких объектах.

Еще одним, на мой взгляд, замечательным свойством Синглтона является удобство его использования. Например, как в случае с обработчиком прерываний, пример с которым есть в конце статьи. Но а пока разберемся с самим Синглтоном.

Синглтон создающий объекты в RAM

Вообще, про них написано уже довольно много статей, Singleton (Одиночка) или статический класс?, или вот Три возраста паттерна Singleton. Поэтому заострять внимание на что такое Синглтон и описывать всё множество вариантов его реализации я не будут. Вместо этого я остановлюсь на двух вариантах, которые можно использовать во встроенном ПО.
Для начала внесу ясность, в чем же отличие встроенного ПО для микроконтроллера от обычного и почему одни реализации Синглтона для этого ПО «лучше» чем другие. Некоторые критерии исходят из требований ко встроенному ПО, а некоторые просто из моего опыта:

  • Во встроенном ПО не рекомендуется создавать объекты динамически
  • Зачастую во встроенном ПО объект создается статически и никогда не уничтожается
  • Хорошо, если расположение объекта известно на этапе компиляции

Исходя из этих предпосылок рассмотрим два варианта Синглтона со статически создаваемыми объектами, и наверное самый известный и распространенный — это Singleton Мэйерса, кстати, хотя он и должен быть потокобезопасный по стандарту С++, компиляторы для встроенного ПО делают его таким (например IAR), только при включении специальной опции:

template <typename T>
class Singleton {
  public:
    static T & GetInstance()    {
      static T instance ;
      return instance ;
    }
    Singleton() = delete ;
    Singleton(const Singleton<T> &) = delete ;
    const Singleton<T> & operator=(const Singleton<T> &) = delete ;
} ; 

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

int main() {
  //инициализируется объект типа Timer1  и возвращается ссылка на него
  auto& objRef = Singleton<Timer1>::GetInstance(); 
  //объект не инициализируется, возвращается ссылка на проинициализированный объект 
  auto& objRef1 = Singleton<Timer1>::GetInstance(); 
return 0;
}

И Синглтон без отложенной инициализации:

template <typename T>
class Singleton {
  public:
    static constexpr T & GetInstance()    {      
      return instance ;
    }
    Singleton() = delete ;
    Singleton(const Singleton<T> &) = delete ;
    const Singleton<T> & operator=(const Singleton<T> &) = delete ;
  private:
   inline static T instance ;  //инициализация происходит сразу после запуска программы
} ; 

Оба Синглтона создают объекты в RAM, отличие в том, что для второго инициализация происходит сразу после запуска программы, а первый инициализируется при первом вызове.

Как можно их использовать в реальной жизни. По старой традиции, попытаюсь показать это на примере светодиода. Итак, предположим нам нужно создать объект класса Led1, который на самом деле просто псевдоним класса Pin<PortA, 5>:

using PortA = Port<GpioaBaseAddr> ;
using Led1 = Pin<PortA, 5> ;
using GreenLed = Pin<PortA, 5> ;

Led1 myLed ; // Я могу создать этот объект глобально в RAM
constexpr GreenLed greenLed ; // Я могу создать этот объект глобально в ROM
int main() {
  static GreenLed myGreenLed ; // Могу еще раз в RAM
  Led1 led1; // Могу локально в стеке 
  myGreenLed.Toggle();
  led1.Toggle() ;
}

На всякий случай, классы Port и Pin выглядят как-то так

constexpr std::uint32_t OdrAddrShift = 20U;
template <std::uint32_t addr>
struct Port {  
    __forceinline inline static void Toggle(const std::uint8_t bit)  {    
    *reinterpret_cast<std::uint32_t*>(addr ) ^= (1 << bit) ;
  }
};

template <typename T, std::uint8_t pinNum>
class Pin {
  //класс Singleton должен быть другом, чтобы видеть приватный конструктор
  friend class Singleton<Pin> ;
public:
  __forceinline inline void Toggle() const  {
    T::Toggle(pinNum) ;
  }
  //удаляем оператор =  
  const Pin & operator=(const Pin &) = delete ; 
private:
  //скрываем конструктор, чтобы нельзя было создать объект
  constexpr Pin() {} ;  
  //скрываем конструктор копирования, чтобы нельзя было создать копию
  //можно было бы удалить, но он нам пригодиться потом
  constexpr Pin(const Pin &) = default ; 
} ; 

В примере я создал аж 4 разных объекта одного и того же типа в RAM и ROM, которые на самом деле работают с одним и тем же выходом порта А. Что здесь не очень хорошо:
Ну первое, это то, что я видимо забыл, что GreenLed и Led1 это один и тот же тип и создал несколько одинаковых объектов, занимающих место по разным адресам. На самом деле, я забыл даже, что уже создал глобально объекты классов Led1 и GreenLed, и создал их еще и локально.

Второе, вообще объявление глобальных объектов не приветствуются,

Рекомендация по технике программирования для обеспечения лучшей оптимизации компилятором

Module-local variables—variables that are declared static—are preferred over
global variables (non-static). Also avoid taking the address of frequently accessed static variables.

а локальные объекты доступны только в области видимости функции main().

Поэтому перепишем этот пример с использованием Синглтона:

using PortA = Port<GpioaBaseAddr> ;
using Led1 = Pin<PortA, 5> ;
using GreenLed = Pin<PortA, 5> ;

int main() {
  //если это синглтон Мэйерса тут проинициализируется объект класса GreenLed 
  //и вернется ссылка
  GreenLed& myGreenLed  = Singleton<GreenLed>::GetInstance(); 
  //А теперь просто вернется ссылка на тот же объект что и выше 
  Led1& led1 = Singleton<Led1>::GetInstance(); 
  myGreenLed.Toggle() ;
  led1.Toggle() ; //можно вообще так, Singleton<Led1>::GetInstance().Toggle()
}

В этом случае, не зависимо от того, что я забыл, мои ссылки всегда будут указывать на один и тот же объект. И получать эту ссылку я могу в любом месте программы, в любом методе, в том числе и, например, в статическом методе обработчика прерывания, но об этом позже. Справедливости ради, надо сказать, что код не делает ничего, и ошибка логики программы никуда не исчезла. Ну да ладно, давайте разберемся, где и как вообще у нас разместился этот статический объект, созданный Синглтоном и как он проинициализировался?

Статический объект

Перед тем как это выяснить, хорошо бы понять, что такое статический объект.

Если вы объявляете члены класса ключевым словом static, то это означает, что члены класса просто не привязаны к экземплярам класса, они являются независимыми переменными и обращаться к таким полями вы можете не создавая объекта класса. Их жизни ничего не угрожает с момента рождения и до выхода программы.

При использовании же в объявлении объекта спецификатор static определяет только время существования объекта. Грубо говоря, память для такого объекта выделяется при запуске программы и освобождается при завершении программы, при запуске же происходит и их инициализация. Исключения составляют только локальные статические объекты, которые хотя и «умирают» только при завершении программы, по сути «рождаются», а точнее инициализируются при первом прохождении через их объявление.

Динамическая инициализация локальной переменной со статическим хранилищем выполняется первый раз в момент первого прохождения через её объявление; такая переменная считается инициализированным по завершении её инициализации. Если один поток проходит через объявление переменной в момент её инициализации другим потоком, то он должен дождаться завершения инициализации.

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

Такие сложности приводят к тому, что использование локальных статических переменных и объектов во встроенном ПО будет приводить к дополнительным накладным расходам. Проверить это можно на простом примере:

struct Test1{
  Test1(int value): j(value) {}  
  int j;
} ;

Test1 &foo() {
  static Test1 test(10)  ;
  return test; 
} 

int main() {
  for (int i = 0; i < 10; ++i) {
    foo().j ++;
  }
  return 0;
}

Здесь, при первом вызове функции foo(), компилятор должен проверить, что локальный статический объект test1 еще не проинициализирован и вызвать конструктор объекта Test1(10), а при втором и последующих проходах должен убедиться, что объект уже проинициализирован, и пропустить этот шаг, перейдя сразу на return test.

Для этого компилятор просто добавляет дополнительный защитный флажок foo()::static guard for test 0x00100004 0x1 Data Lc main.o и вставляет проверочный код. При первом объявлении статической переменной, этот защитный флажок не установлен и поэтому объект должен быть проинициализирован вызовом конструктора, при следующих прохождениях, этот флажок уже установлен, поэтому надобности в инициализации больше нет и вызов конструктора пропускается. Причем эта проверка будет выполняться постоянно в цикле for.

Синглтон размещающий объекты в ROM и статические переменные(С++ на примере микроконтроллера Cortex M4) - 2

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

image

Таким образом, цена использования статической переменной или объекта во встроенном ПО возрастает как по размеру ОЗУ, так и по размеру кода. И этот факт хорошо бы держать в голове и учитывать при разработке.

Другим минусом является еще и тот факт, что защитный флажок рождается вместе со статической переменной, время его жизни равно времени жизни статического объекта, он создается самим компилятором и вы не имеете доступа к нему во время разработки. Т.е. если вдруг по какой-то причине

см. случайный сбой

Причинами случайных ошибок являются: (1) альфа-частицы, образовавшиеся в результате процесса распада, (2) нейтроны, (3) внешний источник электромагнитного излучения и (4) внутренние перекрестные помехи.

флажок из 1 перейдет в 0, то вызовется снова инициализация начальным значением. Это нехорошо, и тоже надо иметь ввиду. Подведем итог по статическим переменным:

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

Локальные статические переменные инициализируются во время первого прохода через объявление переменной.

Статические атрибуты класса, а равно как и статические глобальные переменны инициализируются сразу после запуска приложения. Причем этот порядок не определен

Теперь вернемся к Синглтону.

Синглтон размещающий объект в ROM

Из всего выше сказанного можно сделать вывод, что для нас Синглтон Мэейрса может обладать следующими недостатками: дополнительные затраты RAM и ROM, неконтролируемый защитный флажок и невозможность разместить объект в ROM, по причине динамической инициализации.

Зато у него есть один прекрасный плюс: вы контролируете время инициализации объекта. Только сам разработчик вызывает GetInstance() первый раз в тот момент когда это ему необходимо.

Чтобы избавиться от первых трех недостатков достаточно использовать

Синглтон без отложенной инициализации

template<typename T, class Enable = void>
class Singleton {
public:
  Singleton(const Singleton&) = delete ;
  Singleton& operator = (const Singleton&) = delete ;
  Singleton() =  delete ;  
    
  static T& GetInstance()   {    
    return instance;
  } 
private:  
  static T instance ;
} ;
template<typename T, class Enable>
T Singleton<T,Enable>::instance ;
 

Здесь конечно другая проблема, мы не можем управлять временем инициализации объекта instance, и должны каким-то образом обеспечить очень прозрачную инициализацию. Но это отдельная проблема, на ней сейчас останавливаться не будем.

Этот Синглтон можно переделать так, чтобы инициализация объекта была полностью статической на этапе компиляции и экземпляр объекта T создавался в ROM, используя static constexpr T instance вместо static T instance:

template <typename T>
class Singleton {
  public:
    static constexpr T & GetInstance()    {      
      return instance ;
    }
    Singleton() = delete ;
    Singleton(const Singleton<T> &) = delete ;
    const Singleton<T> & operator=(const Singleton<T> &) = delete ;
  private:
    //Инициализация constexpr атрибут constexpr конструктором копирования
    // вот тут как раз нам и нужен конструктор копирования класса T
    static constexpr T instance{T()};
} ; 
template<typename T> 
constexpr T Singleton<T>::instance ;

Здесь создание и инициализация объекта будет выполнена компилятором на этапе компиляции и объект попадет в сегмент .readonly. Правда при этом, сам класс должен удовлетворять следующим правилам:

  • Инициализация объекта такого класса должна быть статической. (Конструктор должен быть constexpr)
  • Класс должен иметь constexpr конструктор копирования
  • Методы класса объекта класса не должны менять данных объекта класса (все методы const)

Например, вполне возможен вот такой вариант:

class A {
    friend class  Singleton<A>;
public:     
  const A & operator=(const A &) = delete ;

  int Get() const  {
      return test2.Get();
  }
    
  void Set(int v) const {
      test.SetB(v);
  }
private:
  B& test;    //ссылка на объект в RAM
  const C& test2; //ссылка на объект в ROM
 //скрываем конструктор копирования и обычный конструтор
  constexpr  A(const A &) = default ;
  //инициализируем ссылками на объекты в RAM и ROM, используя Singleton
  constexpr A() :  test(Singleton<B>::GetInstance()),
                   test2(Singleton<C>::GetInstance())  {  }
};

int main() {
 //ссылка на объект класса А в ROM
  auto& myObject =  Singleton<A>::GetInstance() ; 
 //устанавливаем в объекте класса В значение полученное у объекта класса С
  myObject.Set(myObject.Get()) ; 
  cout<<"Singleton<A> - address:  "<< &myObject <<std::endl;
}

Отлично, можно использовать Синглтон для создания объектов в ROM, но как быть если некоторые объекты должны быть в RAM? Очевидно, что нужно как-то держать две специализации для Синглтона, одну для объектов RAM, другую для объектов в ROM. Сделать это можно введя, например, для всех объектов, которые должны быть размещены в ROM базовый класс:

Специализация для Синглтонов создающих объекты в ROM и RAM

//базовый класс для всех объектов, которые надо расположить в ROM
class RomObject{};
//Синглтон для ROM объектов
template<typename T> 
class Singleton<T, typename std::enable_if_t<std::is_base_of<RomObject, T>::value>> {
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator = (const Singleton&) = delete;
    Singleton() = delete;

    static constexpr const T& GetInstance()   {
        return instance;
    }   
private:   
  static constexpr T instance{T()};
};
template<typename T> 
constexpr T Singleton<T, 
     typename std::enable_if_t<std::is_base_of<RomObject, T>::value>>::instance ;

//Синглтон для RAM объектов
template<typename T, class Enable = void>
class Singleton {
public:
  Singleton(const Singleton&) = delete;
  Singleton& operator = (const Singleton&) = delete;
  Singleton() =  delete;  
    
  constexpr static T& GetInstance()   {    
    return instance;
  } 
private:  
  static T instance ;
};
template<typename T, class Enable>
T Singleton<T,Enable>::instance ;

В таком случае можно будет использовать их так:

//Объект этого класса будет расположен в RAM, его метод SetB() меняет данные класса (j)
class B {
    friend class  Singleton<A>;
public:   
  const B & operator=(const B &) = delete ;

  void SetB(int value) {
    j = value ;
  }
private:
//Прячем конструкторы, на самом деле можно конструктор копирования удалить
  B(const B &) = default ;
  B() = default;
  int j = 0;
}

//Объект это класса должен быть в ROM
class A: public RomObject{
    friend class  Singleton<A>;
public:   
  const A & operator=(const A &) = delete ;

  int Get() const  {
      return test2.Get();
  }
  //Метод меняет данные объекта класса B, но не свои  
  void Set(int v) const {
      test.SetB(v); 
  }
private:
  B& test;    //ссылка на объект в RAM
  const C& test2; //ссылка на объект в ROM
  //Конструктор копирования должен быть и надо его спрятать
  A(const A &) = default ;
  //инициализируем ссылками на объекты в RAM и ROM, используя Singleton
  constexpr A() :  test(Singleton<B>::GetInstance()),
                   test2(Singleton<C>::GetInstance())  {  }
};

int main() {
//ссылка на объект класса А в ROM
  auto& romObject =  Singleton<A>::GetInstance() ; 
//ссылка на объект класса B в RAM
  auto& ramObject =  Singleton<B>::GetInstance() ; 

//устанавливаем в объекте класса В значение полученное у объекта класса С
  ramObject.SetB(romObject.Get()) ; 
  cout<<"Singleton<A> - address:  "<< &romObject <<std::endl;
  cout<<"Singleton<B> - address:  "<< &ramObject <<std::endl;
}

Как же можно использовать такой Синглтон в реальной жизни.

Пример использования Синглтона

Я попытаюсь показать это на примере работы Таймера и Светодиода. Задача простая, моргнуть светодиодом по срабатыванию таймера. Период срабатывания таймером можно задавать.

Принцип работы будет заключаться в следующем, при вызове прерывания, будет вызываться метод OnInterrupt() таймера, который в свою очередь будет через интерфейс подписчика вызывать метод переключения светодиода.

Очевидно, что объект светодиод должен находиться в ROM, так как нет смысла его создавать в RAM, в нем даже данных нет. В принципе я его уже описал выше, поэтому просто добавим в него наследование от RomObject, сделаем constexpr конструктор и еще унаследуем интерфейс для обработки события от таймера.

Объект светодиод

//Интерфейс для обработки события по таймеру
class  ITimerSubscriber {
public:
  virtual void OnTimeOut() const = 0;
} ;

template <typename T, std::uint8_t pinNum>
class Pin: public RomOject, public ITimerSubscriber {
  //класс Singleton должен быть другом, чтобы видеть приватный конструктор
  friend class Singleton<Pin> ;
public:
  __forceinline inline void Toggle()  const {
    T::Toggle(pinNum) ;
  }
  //При срабатывании таймера мы хотим моргнуть светодиодом
  __forceinline inline void OnTimeOut() const override {
    Toggle() ;
  }
  //удаляем оператор =  
  const Pin & operator=(const Pin &) = delete ; 
private:
  //скрываем конструкторы, чтобы нельзя было создать объект
  constexpr Pin() = default ;  
 Pin(const Pin &) = default ; 
} ;

А вот Таймер я сделаю специально в RAM с небольшими накладными, буду хранить ссылку на структуру TIM_TypeDef, период и ссылку на подписчика, а в конструкторе буду производить настройку таймера (Хотя можно было бы сделать так, чтобы и Таймер тоже ложился в ROM):

Класс Таймер

 
class Timer {
public:
  const Timer & operator=(const Timer &) = delete ;  
  void SetPeriod(const std::uint16_t value)   {
    period = value ;
    timer.PSC = TimerClockSpeed / 1000U - 1U ;
    timer.ARR = value ;  
  }
 //Метод будет вызываться в обработчике прерывания
  __forceinline inline void OnInterrupt()   {
     if ((timer.SR & TIM_SR_UIF) && (timer.DIER & TIM_DIER_UIE))  {
     //тут вызываем метод подписчика, который вызывается при событии OnTimeOut
    // собственно у нас это вызов метода Toggle()
      subscriber->OnTimeOut() ; 
      timer.SR &=~ TIM_SR_UIF ;
    }
  }  
  
//Можно подписать на событие TimeOut хоть кого, кто наследует ITimerSubscriber, но одного
  __forceinline inline void Subscribe(const ITimerSubscriber& obj)  {
    subscriber = &obj ;
  }
  
  inline void Start()   {
    timer.CR1 |= TIM_CR1_URS ;
    timer.DIER |= TIM_DIER_UIE ; 
    SetPeriod(period) ; 
    timer.CR1 &=~TIM_CR1_OPM ;
    timer.EGR |= TIM_EGR_UG ; 
    timer.CR1 |= TIM_CR1_CEN ;
  }
    
protected:
  //спрячем конструктор, чтобы никто кроме наследника не смог им воспольваться
  explicit Timer(TIM_TypeDef& tim): timer{tim} {};
  const ITimerSubscriber * subscriber = nullptr ;  
  TIM_TypeDef& timer ;
  std::uint16_t period = 1000;
} ;

//Собственно таймер для моргания должен быть Синглтоном
class BlinkTimer: public Timer {
  friend class Singleton<BlinkTimer> ;  
public:     
  const BlinkTimer & operator=(const BlinkTimer &) = delete ;  

private:
  BlinkTimer(const BlinkTimer &) = default ;  
  inline BlinkTimer(): Timer{*TIM2}    {  }
} ;

int main() {
  BlinkTimer & blinker =  Singleton<BlinkTimer>::GetInstance() ;   
  using Led1 = Pin<PortA, 5> ;
  //подписываем Led1, находящийся в ROM, на событие от Таймера моргания
  blinker.Subscribe(Singleton<Led1>::GetInstance()) ; 
  blinker.Start() ;
}

В данном примере, объект класса BlinkTimer был расположен в RAM, а объект класса Led1 был расположен ROM. Никаких лишних глобальных объектов в коде. В месте, где нужен экземпляр класса, мы просто вызываем GetInstance() для этого класса

Осталось добавить обработчик прерываний в таблицу векторов прерываний. И вот тут, очень удобно использовать Синглтон. В статическом методе класса отвечающего за обработку прерываний можно вызвать метод объекта обернутый в Синглтон.

extern "C" void __iar_program_start(void) ;

class InterruptHandler {
  public:
    static void DummyHandler() { for(;;) {} } 
    static void Timer2Handler() {
      //вызываем обработчик прерывания BlinkTimer
      Singleton<BlinkTimer>::GetInstance().OnInterrupt();
    }
};

using tIntFunct = void(*)();
using tIntVectItem = union {tIntFunct __fun; void * __ptr;};
#pragma segment = "CSTACK"
#pragma location = ".intvec"
const tIntVectItem __vector_table[] =
{
  { .__ptr = __sfe( "CSTACK" ) }, //инициализация указателя на стек
  __iar_program_start, //адрес функции точки входа в программу

  InterruptHandler::DummyHandler,
  InterruptHandler::DummyHandler,
  InterruptHandler::DummyHandler,
  InterruptHandler::DummyHandler,
  InterruptHandler::DummyHandler,
  0,
  0,
  0,
  0,
  InterruptHandler::DummyHandler,
  InterruptHandler::DummyHandler,
  0,
  InterruptHandler::DummyHandler,
  InterruptHandler::DummyHandler,
  //External Interrupts
  InterruptHandler::DummyHandler,         //Window Watchdog
  InterruptHandler::DummyHandler,        //PVD through EXTI Line detect/EXTI16
  ....
  InterruptHandler::Timer2Handler,    //А вот и наш обработчик прерывания BlinkTimer
  InterruptHandler::DummyHandler,         //TIM3
  ...

  InterruptHandler::DummyHandler,        //SPI 5 global interrupt
};

extern "C" void __cmain(void) ;
extern "C" __weak void __iar_init_core(void) ;
extern "C" __weak void __iar_init_vfp(void) ;

#pragma required = __vector_table
void __iar_program_start(void) {
  __iar_init_core() ;
  __iar_init_vfp() ;
  __cmain() ;
}

Немного про саму таблицу, как это все работает:

Сразу после включения питания или после сброса, происходит прерывание по сбросу с номером -8, в таблице это нулевой элемент, по сигналу сброса программа переходит на вектор нулевого элемента, где первым делом инициализируется указатель на адрес верхушки стека. Этот адрес берется с расположения сегмента STACK, который вы настраивали в настройках линкера. Сразу после инициализации указателя, переходим на точку входа программы, в данном случае по адресу функции __iar_program_start. Дальше исполняется код инициализирующий ваши глобальные и статические переменные, инициализация сопроцессора с плавающей точкой, если таковой был включен в настройках и так далее. При возникновении прерывания, контроллер прерываний по номеру прерывания в таблице переходит на адрес обработчика прерывания. В нашем случае, это InterruptHandler::Timer2Handler, который собственно через Синглтон вызывает метод OnInterrupt() нашего таймера моргания, а тот в свою очередь метод OnTimeOut() ножки порта.

Собственно и все, можно запускать программу. Рабочий пример для IAR 8.20 лежит тут.
Более подробный пример использования Синглтона, для объектов в ROM и в RAM можно посмотреть здесь.

Ссылки на документацию:

P.S. На картинке вначале статьи все таки Синглтон не РОМ, а ВИСКИ.

Автор: lamerok

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js