Top.Mail.Ru

STM32 и C++. Мой вариант архитектуры, обработка прерываний.

Всех рад снова приветствовать 🤝 Думаю многие читали статьи Эдуарда в Сообществе, посвященные созданию набора библиотек для работы с микроконтроллерами STM32 на C++. Собственно, на этой почве у Эдуарда  и возник вопрос, как максимально гармонично вписать обработку прерываний, поэтому я сегодня опишу на демо-примере мою концепцию реализации.

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

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

Перенос таблицы векторов прерываний в RAM.

Рассмотрим в двух словах, как организована обработка прерываний по умолчанию. Я далее по тексту буду использовать контроллер семейства STM32F10x, для других названия файлов будут незначительно отличаться. Итак, в файле startup_stm32f103c8tx.s в g_pfnVectors определены точки входа в конкретные функции, вызывающиеся при возникновении того или иного прерывания.

Таким образом, для добавления своего кода в тот или иной обработчик прерываний, присутствующий в startup_stm32f103c8tx.s, функции переопределяются в пользовательском коде, именно так и происходит при генерации проекта через STM32CubeMx. Я же хочу дефолтные обработчики заменить своими, из чего вытекает необходимость создать свою таблицу векторов прерываний в RAM и поместить ее адрес в регистр SCB->VTOR. Так, разбираем пошагово идею:

  1. Создаем свою кастомную таблицу векторов прерываний, физически это означает определение массива. Поместим в main.cpp (о создании демо-проекта - во второй части статьи):
uint32_t ramVectorTable[constants::isrVectorTableSize] __attribute__(( aligned (constants::isrVectorTableAlingment) ));

Здесь используются константы, определенные в constants.h (полный код и проект в конце статьи):

inline constexpr uint8_t isrVectorTableSize = 67;
inline constexpr int isrVectorTableAlingment = 0x100;

Размер таблицы векторов определяется количеством обработчиков в g_pfnVectors.

  • Переместим все дефолтные обработчики из g_pfnVectors в ramVectorTable – это и будет вторым шагом. Впоследствии мы изменим те элементы таблицы, которые потребуется, остальные же спокойно будут пребывать в первозданном виде, что, в свою очередь, избавит нас от множества потенциальных проблем.
for (uint16_t i = 0; i < constants::isrVectorTableSize; i++)
{
	ramVectorTable[i] = g_pfnVectors[i];
}
  • Остается один шаг – поместить адрес ramVectorTable в регистр SCB->VTOR:
__disable_irq();
SCB->VTOR = (uint32_t)&ramVectorTable;
__DSB();
__enable_irq();

Периферия. База.

Плавно переходим к следующему этапу – непосредственной работе с периферией контроллера. И, опять же, опишу мой вариант реализации. Заключается он в следующем: для каждого типа периферийных модулей создается свой класс, инкапсулирующий в себе все взаимодействие с данным типом. Таким образом, для данного проекта я добавляю классы Gpio и Timer:

Структура проекта для STM32.

При этом все эти классы наследуют от базового PeripheralUnit, который в себе заключает общий функционал:

class PeripheralUnit
{
public:
	PeripheralUnit();
	virtual ~PeripheralUnit();
  virtual void interruptHandler();
	bool getInterruptFlag();
	void clearInterruptFlag();
	static void setIsrVectorTable(uint32_t *table);

protected:
	void init();
	void initIsr(IRQn_Type irq);

	bool isInitialized;
	bool interruptFlag;

private:
	static uint32_t *isrVectorTable;
};

Пробежимся прямо по очереди:

  • interruptHandler() – обработчик прерывания, данная функция будет переопределяться в случае необходимости в производных классах.
  • getInterruptFlag() / clearInterruptFlag() – получение/очистка флага прерываний.
  • setIsrVectorTable(uint32_t *table) – статический метод для модификации указателя на актуальную таблицу векторов прерываний.
  • init() / initIsr(IRQn_Type irq) – базовая инициализация, а также конфигурация прерывания, опять же – в случае необходимости.
  • isInitialized / interruptFlag – флаги, сигнализирующие об успешной инициализации и о срабатывании прерывания.
  • isrVectorTable – и, наконец, тот самый указатель на таблицу векторов, которая уже создана на первом этапе.

Далее реализация:

#include "PeripheralUnit.h"
#include "IrqCallback.h"



constexpr int isrVectorTableOffset = 16;

uint32_t* PeripheralUnit::isrVectorTable;
std::vector<PeripheralUnit*> IrqCallbackBase::irqPeripherals;



PeripheralUnit::PeripheralUnit() :
    isInitialized(false),
    interruptFlag(false)
{
}



PeripheralUnit::~PeripheralUnit()
{
}



void PeripheralUnit::setIsrVectorTable(uint32_t* table)
{
	PeripheralUnit::isrVectorTable = table;
}



bool PeripheralUnit::getInterruptFlag()
{
  return interruptFlag;
}



void PeripheralUnit::clearInterruptFlag()
{
  interruptFlag = false;
}



void PeripheralUnit::init()
{
  isInitialized = true;
}



void PeripheralUnit::initIsr(IRQn_Type irq)
{
  IrqCallbackBase::irqPeripherals.push_back(this);

  isrVectorTable[isrVectorTableOffset + irq] =
      (uint32_t)((SimpleCallback)(*createCallback(IrqCallbackBase::irqPeripherals.size() - 1)));
}


void PeripheralUnit::interruptHandler()
{
  interruptFlag = true;
}

Здесь все логично и, в целом, понятно уже из названия функций. Инициализация на данном этапе фактически пустая, в частности по той причине, что ее мы отдали CubeMx. Обработчик прерываний по умолчанию просто устанавливает флаг interruptFlag в true. Регистрация кастомного обработчика прерывания происходит так:

void PeripheralUnit::initIsr(IRQn_Type irq)
{
  IrqCallbackBase::irqPeripherals.push_back(this);

  isrVectorTable[isrVectorTableOffset + irq] =
      (uint32_t)((SimpleCallback)(*createCallback(IrqCallbackBase::irqPeripherals.size() - 1)));
}

К описанию этого процесса и переходим.

Обработка прерываний.

Итак, особо углубляться я не буду, если что пишите в комментарии, на форум или в группу, буду рад помочь )

Базовая ситуация такая – для того, чтобы обеспечить вызов своего обработчика прерывания, необходимо всего-то поместить его адрес на нужную позицию в ramVectorTable. Но! Указатель на функцию и указатель на метод класса это две принципиально разные вещи, поэтому изначально стандарт допускает для данных манипуляций использовать только статические методы, что кардинально противоречит моей концепции.

Допустим, есть класс Timer и два объекта tim1Instance, tim2Instance. Естественно, необходимо, чтобы каждый из объектов имел свой обработчик прерывания, поскольку физически именно так и есть:

Timer interrupt.

Для статической же функции получим:

Использование static-функции.

По существу, это ничем не лучше использования HAL’овских обработчиков, засунутых в один отдельный файл stm32f1xx_it.c. И более того, если попытаться прогуглить решение данной проблемы, то в подавляющем большинстве случаев ответом будет именно выделение функционала в статическую функцию. У нас случай нестандартный, данный вариант априори не подходит, поэтому у меня будет другое решение.

Итоговый код таков, файл IrqCallback.h:

typedef void (*SimpleCallback)(void);



class IrqCallbackBase
{
public:
  IrqCallbackBase(SimpleCallback function)
  {
    callback = function;
  }

  static void staticInvoke(uint8_t index)
  {
    irqPeripherals[index]->interruptHandler();
  }

  static std::vector<PeripheralUnit*> irqPeripherals;

  operator SimpleCallback() const
  {
    return callback;
  }

private:
  SimpleCallback callback;
};



template <uint8_t I> class IrqDynamicCallback : public IrqCallbackBase
{
public:
  IrqDynamicCallback() : IrqCallbackBase(&IrqDynamicCallback<I>::generatedStaticFunction)
  {
  }

  static void generatedStaticFunction()
  {
    return staticInvoke(I);
  }
};



template<uint8_t I> struct IrqDynamicCallbackFactory
{
  static inline std::shared_ptr<IrqCallbackBase> create(uint8_t index)
  {
    if (index == I)
    {
      return std::shared_ptr<IrqCallbackBase>(new IrqDynamicCallback<I>());
    }
    else
    {
      return IrqDynamicCallbackFactory<I + 1>::create (index);
    }
  }
};



struct Overflow
{
  static inline std::shared_ptr<IrqCallbackBase> create(uint8_t index)
  {
    return NULL;
  }
};
template<> struct IrqDynamicCallbackFactory<constants::isrVectorTableSize> : Overflow {};



std::shared_ptr<IrqCallbackBase> createCallback(uint8_t index)
{
  return IrqDynamicCallbackFactory<0>::create(index);
}

Использование в PeripheralUnit мы уже видели:

void PeripheralUnit::initIsr(IRQn_Type irq)
{
  IrqCallbackBase::irqPeripherals.push_back(this);

  isrVectorTable[isrVectorTableOffset + irq] =
      (uint32_t)((SimpleCallback)(*createCallback(IrqCallbackBase::irqPeripherals.size() - 1)));
}

Помещаем текущий периферийный модуль, для которого и реализуем обработку прерываний, в вектор IrqCallbackBase::irqPeripherals. А в таблицу векторов прерываний заносим адрес функции, что в конечном итоге приведет к вызову метода interruptHandler() текущего объекта PeripheralUnit.

Конкретная периферия.

Теперь по-быстрому добавляем классы для работы с таймерами и портами ввода-вывода, начав с первого из перечисленных:

class Timer : public PeripheralUnit
{
public:
  enum class Mode {
    Base,
    Irq,
    Dma
  };

  Timer();
  virtual ~Timer();
  void init(TIM_HandleTypeDef *timerHandle, IRQn_Type irq);
  void start(Mode);

private:
  void interruptHandler();

  TIM_HandleTypeDef *handle;
};

Собственно, три основных метода – инициализация, запуск в одном из режимов и обработчик прерывания:

void Timer::init(TIM_HandleTypeDef *timerHandle, IRQn_Type irq)
{
  handle = timerHandle;
  PeripheralUnit::initIsr(irq);
  PeripheralUnit::init();
}


void Timer::start(Mode operationMode)
{
  switch(operationMode)
  {
    case (Mode::Base):
      HAL_TIM_Base_Start(handle);
      break;

    case (Mode::Irq):
      HAL_TIM_Base_Start_IT(handle);
      break;

    case (Mode::Dma):
    default:
      break;
  }
}



void Timer::interruptHandler()
{
  if (__HAL_TIM_GET_FLAG(handle, TIM_FLAG_UPDATE) != RESET)
  {
    if (__HAL_TIM_GET_IT_SOURCE(handle, TIM_IT_UPDATE) != RESET)
    {
      __HAL_TIM_CLEAR_IT(handle, TIM_IT_UPDATE);
      interruptFlag = true;
    }
  }
}

Для этого демо-проекта я сделал только обработку прерывания по событию переполнения, в результате которого флаг interruptFlag будет выставлен в единицу.

Пару слов по поводу обработки в целом – как по мне, так удобнее всего сделать полноценную event-driven архитектуру, при которой на событие прерывания одного из модулей может приходиться N-ое количество подписчиков. В примере я сделаю максимально просто, в while(1) будем проверять соответствующие флаги, по значению которых уже выполнять те или иные действия.

Итак, движемся к GPIO. Здесь все еще более незатейливо, просто оборачиваем соответствующие вызовы HAL:

class Gpio : public PeripheralUnit
{
public:
  enum class State {
    Set,
    Reset
  };

  Gpio();
  virtual ~Gpio();
  void init(GPIO_TypeDef *gpioPort, uint16_t gpioPin);
  void write(State state);
  State read();
  void toggle();

private:
  GPIO_TypeDef *port;
  uint16_t pin;
};
void Gpio::init(GPIO_TypeDef *gpioPort, uint16_t gpioPin)
{
  port = gpioPort;
  pin = gpioPin;
  PeripheralUnit::init();
}



void Gpio::write(State state)
{
  switch(state)
  {
    case State::Set:
      HAL_GPIO_WritePin(port, pin, GPIO_PIN_SET);
      break;

    case State::Reset:
      HAL_GPIO_WritePin(port, pin, GPIO_PIN_RESET);
      break;

    default:
      break;
  }
}



Gpio::State Gpio::read()
{
  State currentState = State::Reset;

  if (HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_SET)
  {
    currentState = State::Set;
  }

  return currentState;
}



void Gpio::toggle()
{
  HAL_GPIO_TogglePin(port, pin);
}

Демо-проект.

Переходим к тестированию и для начала создаем в STM32CubeIDE новый проект. Все точно так же, как и для «обычного» C-проекта, за исключением "Targeted language" непосредственно после выбора целевого контроллера:

С++ проект в STM32CubeIDE.

В STM32CubeMx активируем необходимую периферию, у меня будут два порта ввода-вывода – один на вход (PA3), другой на выход (PC13):

GPIO.

На PA3 повесим кнопку, на PC13 светодиод, как же без него. И парочка таймеров, TIM2:

Timer configuration.

TIM3:

TIM3 settings.

Тактирование:

Настройки тактирования.

Из данной конфигурации вытекает тот факт, что таймер TIM2 будет переполняться каждые 500 мс, а TIM3 – каждые 100 мс. Также в CubeMx включаем прерывания таймеров, на примере TIM3:

Активация прерываний.

Генерируем код и осуществляем два действия:

  • переименовываем main.c в main.cpp
  • из файла stm32f1xx_it.c выкидываем с корнем обработчики TIM2_IRQHandler() и TIM3_IRQHandler(), что естественно, так как прерывания обслуживаются внутри соответствующих модулей.

Инициализацию оставляем на откуп CubeMx, при желании выкинуть HAL - просто помещаем все настройки в функцию init() конкретного класса. И да, если в процессе работы еще раз сгенерировать код через Cube, то будет создан main.c без пользовательского кода, вместо того, чтобы обновить main.cpp =\ Так что с этим разбираться нужно будет вручную, перенося либо код из main.cpp в новый main.c с последующим переименованием, либо в обратном направлении.

Для демо-проекта берем простейшую задачу: при нажатой кнопке (PA3) по переполнению TIM2  (каждые 500 мс в данном случае) изменяем состояние PC13 с целью обеспечить мигание диода. Если кнопка не нажата – бездействуем. По переполнению TIM3 просто инкрементируем счетчик extraTimerCounter.

Так будет выглядеть реализация с учетом созданного в первой части статьи, создаем соответствующие объекты:

Timer timer;
Timer extraTimer;
Gpio output;
Gpio input;

Инициализируем все и запускаем таймеры, в принципе, инициализацию можно перекинуть и в конструктор:

output.init(GPIOC, GPIO_PIN_13);
input.init(GPIOA, GPIO_PIN_3);

timer.init(&htim2, TIM2_IRQn);
timer.start(Timer::Mode::Irq);

extraTimer.init(&htim3, TIM3_IRQn);
extraTimer.start(Timer::Mode::Irq);

В while(1) проверяем прерывания и производим необходимые действия:

// Handle TIM2 interrupt
bool interruptOccured = timer.getInterruptFlag();
if (interruptOccured == true)
{
  if (input.read() == Gpio::State::Set)
  {
	output.toggle();
  }
  timer.clearInterruptFlag();
}
// Handle TIM3 interrupt
interruptOccured = extraTimer.getInterruptFlag();
if (interruptOccured == true)
{
  extraTimerCounter++;
  extraTimer.clearInterruptFlag();
}

Все, на этом процесс завершен, собираем, прошиваем, проверяем. В результате чего имеем наглядную возможность убедиться в полной работоспособности 👍 Я опустил кусок, связанный с переносом таблицы векторов прерываний, который мы уже обсудили. Он присутствует в самом начале main():

for (uint16_t i = 0; i < constants::isrVectorTableSize; i++)
{
	ramVectorTable[i] = g_pfnVectors[i];
}

__disable_irq();
SCB->VTOR = (uint32_t)&ramVectorTable;
__DSB();
__enable_irq();

PeripheralUnit::setIsrVectorTable(ramVectorTable);

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

#ifndef IRQCALLBACK_H_
#define IRQCALLBACK_H_



#include "stm32f1xx_hal.h"
#include "constants.h"
#include <vector>
#include <memory>



typedef void (*SimpleCallback)(void);



class IrqCallbackBase
{
public:
  IrqCallbackBase(SimpleCallback function)
  {
    callback = function;
  }

  static void staticInvoke(uint8_t index)
  {
    irqPeripherals[index]->interruptHandler();
  }

  static std::vector<PeripheralUnit*> irqPeripherals;

  operator SimpleCallback() const
  {
    return callback;
  }

private:
  SimpleCallback callback;
};



template <uint8_t I> class IrqDynamicCallback : public IrqCallbackBase
{
public:
  IrqDynamicCallback() : IrqCallbackBase(&IrqDynamicCallback<I>::generatedStaticFunction)
  {
  }

  static void generatedStaticFunction()
  {
    return staticInvoke(I);
  }
};



template<uint8_t I> struct IrqDynamicCallbackFactory
{
  static inline std::shared_ptr<IrqCallbackBase> create(uint8_t index)
  {
    if (index == I)
    {
      return std::shared_ptr<IrqCallbackBase>(new IrqDynamicCallback<I>());
    }
    else
    {
      return IrqDynamicCallbackFactory<I + 1>::create (index);
    }
  }
};



struct Overflow
{
  static inline std::shared_ptr<IrqCallbackBase> create(uint8_t index)
  {
    return NULL;
  }
};
template<> struct IrqDynamicCallbackFactory<constants::isrVectorTableSize> : Overflow {};



std::shared_ptr<IrqCallbackBase> createCallback(uint8_t index)
{
  return IrqDynamicCallbackFactory<0>::create(index);
}



#endif /* IRQCALLBACK_H_ */
#include "PeripheralUnit.h"
#include "IrqCallback.h"



constexpr int isrVectorTableOffset = 16;

uint32_t* PeripheralUnit::isrVectorTable;
std::vector<PeripheralUnit*> IrqCallbackBase::irqPeripherals;



PeripheralUnit::PeripheralUnit() :
    isInitialized(false),
    interruptFlag(false)
{
}



PeripheralUnit::~PeripheralUnit()
{
}



void PeripheralUnit::setIsrVectorTable(uint32_t* table)
{
	PeripheralUnit::isrVectorTable = table;
}



bool PeripheralUnit::getInterruptFlag()
{
  return interruptFlag;
}



void PeripheralUnit::clearInterruptFlag()
{
  interruptFlag = false;
}



void PeripheralUnit::init()
{
  isInitialized = true;
}



void PeripheralUnit::initIsr(IRQn_Type irq)
{
  IrqCallbackBase::irqPeripherals.push_back(this);

  isrVectorTable[isrVectorTableOffset + irq] =
      (uint32_t)((SimpleCallback)(*createCallback(IrqCallbackBase::irqPeripherals.size() - 1)));
}


void PeripheralUnit::interruptHandler()
{
  interruptFlag = true;
}
#ifndef PERIPHERALUNIT_H_
#define PERIPHERALUNIT_H_



#include "stm32f1xx_hal.h"



class PeripheralUnit
{
public:
	PeripheralUnit();
	virtual ~PeripheralUnit();
  virtual void interruptHandler();
	bool getInterruptFlag();
	void clearInterruptFlag();
	static void setIsrVectorTable(uint32_t *table);

protected:
	void init();
	void initIsr(IRQn_Type irq);

	bool isInitialized;
	bool interruptFlag;

private:
	static uint32_t *isrVectorTable;
};



#endif /* PERIPHERALUNIT_H_ */
#include "Timer.h"



Timer::Timer() :
   handle(NULL)
{
}



Timer::~Timer()
{
}



void Timer::init(TIM_HandleTypeDef *timerHandle, IRQn_Type irq)
{
  handle = timerHandle;
  PeripheralUnit::initIsr(irq);
  PeripheralUnit::init();
}


void Timer::start(Mode operationMode)
{
  switch(operationMode)
  {
    case (Mode::Base):
      HAL_TIM_Base_Start(handle);
      break;

    case (Mode::Irq):
      HAL_TIM_Base_Start_IT(handle);
      break;

    case (Mode::Dma):
    default:
      break;
  }
}



void Timer::interruptHandler()
{
  if (__HAL_TIM_GET_FLAG(handle, TIM_FLAG_UPDATE) != RESET)
  {
    if (__HAL_TIM_GET_IT_SOURCE(handle, TIM_IT_UPDATE) != RESET)
    {
      __HAL_TIM_CLEAR_IT(handle, TIM_IT_UPDATE);
      interruptFlag = true;
    }
  }
}
#ifndef TIMER_TIMER_H_
#define TIMER_TIMER_H_



#include "PeripheralUnit.h"



class Timer : public PeripheralUnit
{
public:
  enum class Mode {
    Base,
    Irq,
    Dma
  };

  Timer();
  virtual ~Timer();
  void init(TIM_HandleTypeDef *timerHandle, IRQn_Type irq);
  void start(Mode);

private:
  void interruptHandler();

  TIM_HandleTypeDef *handle;
};



#endif /* TIMER_TIMER_H_ */
#include "Gpio.h"



Gpio::Gpio() :
  port(NULL),
  pin(0)
{
}



Gpio::~Gpio()
{
}



void Gpio::init(GPIO_TypeDef *gpioPort, uint16_t gpioPin)
{
  port = gpioPort;
  pin = gpioPin;
  PeripheralUnit::init();
}



void Gpio::write(State state)
{
  switch(state)
  {
    case State::Set:
      HAL_GPIO_WritePin(port, pin, GPIO_PIN_SET);
      break;

    case State::Reset:
      HAL_GPIO_WritePin(port, pin, GPIO_PIN_RESET);
      break;

    default:
      break;
  }
}



Gpio::State Gpio::read()
{
  State currentState = State::Reset;

  if (HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_SET)
  {
    currentState = State::Set;
  }

  return currentState;
}



void Gpio::toggle()
{
  HAL_GPIO_TogglePin(port, pin);
}
#ifndef GPIO_GPIO_H_
#define GPIO_GPIO_H_



#include "PeripheralUnit.h"



class Gpio : public PeripheralUnit
{
public:
  enum class State {
    Set,
    Reset
  };

  Gpio();
  virtual ~Gpio();
  void init(GPIO_TypeDef *gpioPort, uint16_t gpioPin);
  void write(State state);
  State read();
  void toggle();

private:
  GPIO_TypeDef *port;
  uint16_t pin;
};



#endif /* GPIO_GPIO_H_ */
/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2022 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "constants.h"
#include "Timer.h"
#include "Gpio.h"

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
TIM_HandleTypeDef htim2;
TIM_HandleTypeDef htim3;

/* USER CODE BEGIN PV */
Timer timer;
Gpio output;
Gpio input;

Timer extraTimer;
uint32_t extraTimerCounter = 0;

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_TIM2_Init(void);
static void MX_TIM3_Init(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
uint32_t ramVectorTable[constants::isrVectorTableSize] __attribute__(( aligned (constants::isrVectorTableAlingment) ));
extern uint32_t g_pfnVectors[constants::isrVectorTableSize];

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
  for (uint16_t i = 0; i < constants::isrVectorTableSize; i++)
  {
    ramVectorTable[i] = g_pfnVectors[i];
  }

  __disable_irq();
    SCB->VTOR = (uint32_t)&ramVectorTable;
  __DSB();
  __enable_irq();

  PeripheralUnit::setIsrVectorTable(ramVectorTable);

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_TIM2_Init();
  MX_TIM3_Init();
  /* USER CODE BEGIN 2 */

  output.init(GPIOC, GPIO_PIN_13);
  input.init(GPIOA, GPIO_PIN_3);
  
  timer.init(&htim2, TIM2_IRQn);
  timer.start(Timer::Mode::Irq);

  extraTimer.init(&htim3, TIM3_IRQn);
  extraTimer.start(Timer::Mode::Irq);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

    // Handle TIM2 interrupt
    bool interruptOccured = timer.getInterruptFlag();
    if (interruptOccured == true)
    {
      if (input.read() == Gpio::State::Set)
      {
        output.toggle();
      }
      timer.clearInterruptFlag();
    }

    // Handle TIM3 interrupt
    interruptOccured = extraTimer.getInterruptFlag();
    if (interruptOccured == true)
    {
      extraTimerCounter++;
      extraTimer.clearInterruptFlag();
    }
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief TIM2 Initialization Function
  * @param None
  * @retval None
  */
static void MX_TIM2_Init(void)
{

  /* USER CODE BEGIN TIM2_Init 0 */

  /* USER CODE END TIM2_Init 0 */

  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};

  /* USER CODE BEGIN TIM2_Init 1 */

  /* USER CODE END TIM2_Init 1 */
  htim2.Instance = TIM2;
  htim2.Init.Prescaler = 7199;
  htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim2.Init.Period = 5000;
  htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM2_Init 2 */

  /* USER CODE END TIM2_Init 2 */

}

/**
  * @brief TIM3 Initialization Function
  * @param None
  * @retval None
  */
static void MX_TIM3_Init(void)
{

  /* USER CODE BEGIN TIM3_Init 0 */

  /* USER CODE END TIM3_Init 0 */

  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};

  /* USER CODE BEGIN TIM3_Init 1 */

  /* USER CODE END TIM3_Init 1 */
  htim3.Instance = TIM3;
  htim3.Init.Prescaler = 719;
  htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim3.Init.Period = 10000;
  htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim3) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM3_Init 2 */

  /* USER CODE END TIM3_Init 2 */

}

/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);

  /*Configure GPIO pin : PC13 */
  GPIO_InitStruct.Pin = GPIO_PIN_13;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

  /*Configure GPIO pin : PA3 */
  GPIO_InitStruct.Pin = GPIO_PIN_3;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

Ссылка на проект: MT_STM32_CppDemoProject.

Подписаться
Уведомить о
guest

63 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
Эдуард
1 год назад

Вот это да. Не успею я достичь такого уровня программирования.
Сдохну раньше.

По нормальному посмотреть не успел.
Но вопросы появились.
Вот эта часть:

void Timer::interruptHandler()
{
  if (__HAL_TIM_GET_FLAG(handle, TIM_FLAG_UPDATE) != RESET)
  {
    if (__HAL_TIM_GET_IT_SOURCE(handle, TIM_IT_UPDATE) != RESET)
    {
      __HAL_TIM_CLEAR_IT(handle, TIM_IT_UPDATE);
      interruptFlag = true;
    }
  }
}

Насколько я понимаю, это обработчик прерывания.
А кроме стандартных операций я могу здесь указать какую либо функцию, которая находится в main.c, что бы она выполнилась или нет?
Если нет, а есть возможность изменить только флаг, придётся делать автомат, что то же неплохо. Нужно же когда то это изучить.

Эдуард
Ответ на комментарий  Aveal
1 год назад

Ну всё.
Вот теперь я развернусь.

Эдуард
Ответ на комментарий  Aveal
1 год назад

Теперь всё, что я делал до этого, переписывать придётся.
И всё будет сильно зависимо от платформы.
Хотя не факт.

KIR
KIR
Ответ на комментарий  Эдуард
1 год назад

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

Эдуард
Ответ на комментарий  KIR
1 год назад

Я о таком методе читал.
Но знаний понять не хватило.
С рабочим примером проще разобраться будет.
Не теоретик я.

tonyk
tonyk
1 год назад

А как обстоят дела с наследованием? Ещё интересно, как при таком подходе обрабатывать прерывания от двух устройств, работающих в связке, например, UART+DMA как вот тут:

////////////////////////////////////////////////////////////////////////////////

void MB_RTU_UART::IRQ_Handler( IRQn_Type _irq )
{
   if( _irq == IRQn_tx )
   {
       // Обработка прерывания от потока ПДП передатчика.
.....
   }
   else if( _irq == IRQn_rx )
   {
       // Обработка прерывания от потока ПДП приёмника.
......
   }
   else if( _irq == usart_IRQn )
   {
       // Обработка прерывания от передатчика.
       //----------------------------------------------------------------------

       uint32
           usart_isr = usart -> ISR;

       if( usart_isr & USART_ISR_TC )
       {
.....
       }

       // Обработка прерывания от приёмника.
       //----------------------------------------------------------------------

       if( usart_isr & USART_ISR_RTOF )
       {
....
       }
   }
   else
   {
       // Чушь какая-то. Поймали прерывание, на которое не подписывались.
       // Занавес!
       abort();
   }
}

antonmai
1 год назад

Собрал таймер на этой основе. При срабатывании прерывания вываливается в usage_fault

antonmai
Ответ на комментарий  Aveal
1 год назад

посмотрел в таблицу векторов там вписан адрес generatedStaticFunction(). Получается она не правильный адрес отдает.

antonmai
Ответ на комментарий  Aveal
1 год назад

Ну, концепция, как раз, и понравилась.

antonmai
Ответ на комментарий  Aveal
1 год назад

ок. Если сделать один таймер, то usag fault. Если два, то hard fault.

Эдуард
Ответ на комментарий  antonmai
1 год назад

С базовыми таймерам есть пример на этой библиотеке.
Он рабочий.

antonmai
Ответ на комментарий  Эдуард
1 год назад

У меня F205. Видимо напортачил при переносе.

Эдуард
Ответ на комментарий  antonmai
1 год назад

У меня нет такого.
А тактирование заработало?
И какой чип конкретно?

antonmai
Ответ на комментарий  Эдуард
1 год назад

Да работает. Стартую таймер и через положенное время usagefault. Насколько выставлю, так и вылетает.

antonmai
Ответ на комментарий  Эдуард
1 год назад

stm32f205rct

antonmai
antonmai
Ответ на комментарий  Aveal
1 год назад

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

Эдуард
Ответ на комментарий  antonmai
1 год назад

Странно. У меня килобайт 12 накидывалось.

tonyk
tonyk
Ответ на комментарий  Эдуард
1 год назад

12К- жуть!
При моём подходе требуется (4*количество_прерываний) байт в ОЗУ и даже не знаю сколько байт в ПЗУ для функции

void CPP_caller( void )
{
    IRQn_Type
        irqn = IRQ::getActiveIRQn();

    if( CPP_handler[ irqn ] )
    {
        (( IRQ* )CPP_handler[ irqn ]) -> IRQ_Handler();
    }
    else
    {
        abort();
    }
}

Ещё заметьте, что использование класса <vector> тянет у вас вызовы new().

Кстати, попробуйте внести изменения в расположение выводов периферии у МК и запустить перестроение проекта. Вы узнаете кое-что интересное об отношении Куба к файлам с расширением "cpp", содержащих код Куба, а заодно позанимаетесь извращением. Решение есть, но тогда придётся попрощаться с кодогенератором Куба.

Вобщем, подтверждается мысль, что скрещивание бульдога с носорогом порождает монстра. Посему или угомонитесь и играйте на Си по правилам Куба, или выбрасывайте КАЛ и пишите на С++, используя регистры периферии.

tonyk
tonyk
Ответ на комментарий  Aveal
1 год назад

Ещё нюанс, который остался вами неосознан. Я сразу создаю массив с указателями на экземпляры классов, имеющих виртуальный метод-обработчик прерывания. Статически. Для чего у вас нужен <vector>? Для хранения связанного списка, скорей всего, двухсвязного, с адресами классов, имеющих обработчики прерываний. То есть в итоге вы получаете, как минимум, дополнительное удвоение потребления ОЗУ в сравнении с моим подходом. И это только то, что лежит на поверхности. Сколько и как использует <vector> ОЗУ вообще и хип в частности ваще то ещё приключение. А ведь есть ещё многозадачность, и как ваша концепция согласуется с ней, а? Получается, так себе концепция?

Эдуард
Ответ на комментарий  tonyk
1 год назад

А можно Ваш вариант полностью?
Для какой нибудь периферии.
Что бы откомпилить и посмотреть что к чему.

antonmai
antonmai
Ответ на комментарий  Эдуард
1 год назад

А тема то не отпускает!

Эдуард
Ответ на комментарий  antonmai
1 год назад

Так библиотеки доделывать нужно. А проблема до конца не решена.

antonmai
antonmai
Ответ на комментарий  tonyk
1 год назад

Подход был показан как КОНЦЕПЦИЯ! А не конечное решение. С кубом вполне можно взаимодействовать, если принять его правила. Никакой тюнинг не сделает из городского такси болид формулы 1. Концепции разные. Не нравиться куба, не используйте. Вот вы предложили, а мы подумаем. Может это ещё лучшее. Для того тут и собираемся.

Эдуард
Ответ на комментарий  Aveal
1 год назад

Я не против HAL. Я им даже пользуюсь иногда.
Просто я хочу уйти от него.
Мне так удобнее.
Сейчас я комбинирую библиотеки, что уже написаны и CMSIS, где на библиотеке не выедешь.

antonmai
antonmai
Ответ на комментарий  Эдуард
1 год назад

Так это показатель роста, когда перерастаешь стартовый набор. Ну и "Слабо без HAL?" никто не отменял. Ещё бы времени на это всё...

antonmai
antonmai
Ответ на комментарий  Aveal
1 год назад

Так это ещё и БЕСПЛАТНОЕ ТАКСИ!

antonmai
antonmai
1 год назад

В продвижение статьи. Сделал CAN мост на данном принципе с засыпанием и пробуждением, индикацией работы. На 500к не пробовал, но на сотке работает так же как и халовский, на пользоваться на порядок удобней. Если ещё в c++ углубиться вообще было бы зашибись. Сейчас винегрет получился. Единственная проблема, если подключаю халовский юсб вылетаю в дефаулт хендлер.

antonmai
antonmai
Ответ на комментарий  Aveal
1 год назад

Пока не готов. Ещё тестирую. Сначала сделал класс BasicTimer с расчетом под любой таймер на 205 камень. Таймеры работают без проблем. Для пробы включил десяток. Все работают. Но без юсб. Юсб оставил халовский как есть . Без перемещения в раму таблицы векторов юсб работает. С перемещением - вылет в дефолт. Что с таймерами , что без них. Естественно не использую таймеры с двойными прерываниями - 3 из 14.

antonmai
antonmai
Ответ на комментарий  Aveal
1 год назад

Время будет попытаюсь.

antonmai
antonmai
1 год назад

Да и не понятно, что с двойными прерываниями делать.

Эдуард
Ответ на комментарий  antonmai
1 год назад

Проверяешь, было ли установлено прерывание до тебя, если нет, сажаешь свой обработчик.
Если да, запоминаешь вектор, ставишь свой, обрабатываешь свою часть, затем передаёшь управление запомненному.
Таким образом можно построить практически бесконечные цепочки. Лишь бы памяти хватило.

antonmai
antonmai
Ответ на комментарий  Эдуард
1 год назад

Так вся прелесть метода теряется.
class SomeClass(IRQ_n irq);
SomeClass.SetCallback(void (*callback(void* callbackObj));
SomeClass.EnableIrq();

Всё изолированно.

antonmai
antonmai
Ответ на комментарий  Эдуард
1 год назад

А так придется interrupt выносить в отдельный самостоятельный класс. Не айс.

Эдуард
Ответ на комментарий  antonmai
1 год назад

Я не совсем то имел в виду.
Но с Вашим вариантом то же обдумать нужно.
Я просто мыслю ещё старыми категориями. Со времён 8086 и ассемблера.
;-(

antonmai
antonmai
Ответ на комментарий  Эдуард
1 год назад

Таки и я. Перерывчик был небольшой. Лет 30. Пытаюсь наверстать.

Эдуард
Ответ на комментарий  antonmai
1 год назад

У меня складывается ощущение, что программированием занимаются люди старой школы.
Молодёжь больше стремится к свистопеределкам на Ардуино.
Как бы эти прерывания добить до конца.
Библиотеки копятся. А прерывания так и не доделаны.

antonmai
antonmai
Ответ на комментарий  Эдуард
1 год назад

Меня , пока , устраивает как сейчас. Проект нужно добить. Осталось SPI и флешку через него.

antonmai
1 год назад

Даа. Тяжело быть деревянным. Однако побороть юсб так и не удалось. После инициализации юсб при срабатывании OTG_FS_IRQn камень уходит в дефолт хендлер. Собствено всё содранно со статьи за одним исключением. У меня есть IntrruptUnit который является наследником PeripheralUnit.
Собственно у, скажем, модуля кан , несколько InterruptUnit:
RxFifo0Interrupt* pRx0Interrupt;
RxFifo1Interrupt* pRx1Interrupt;
SceInterrupt* pSceInterrupt;
TxInterrupt* pTxInterrupt;
В них инициализация, контроль, запуск останов, регистрация и вызов калбеков.
В юсб один InterruptUnit UsbOtgFsInterrupt. Кан прекрасно работает, а юсб валиться в дефолт.
Причем, если всё вернуть к халовской реализации и просто поднять обработчики в раму, происходит то же самое.

antonmai
Ответ на комментарий  antonmai
1 год назад

на 205 можно. Последний раз голый проект с юсб. Без переноса в рам работает, после переноса default_handler. В SCB висит OTG_FS_IRQn.


antonmai
1 год назад

Flash Spi сделал без прерываний, так что протестить interrupt spi не удалось.

Алексей_Байдин
8 месяцев назад

Всем здравствуйте! Хотел бы найти ответ на свой вопрос. Есть устройство, которое должно быть надежным даже в случае отказа, возникновения ошибок в программе (зависание STM32). Мысль такая, если возникает ошибка, заходим в обработчик исключения, в нем отключаем мотор и все что нужно. На первое время, пока с моими знаниями, хотя бы так. Что дальше будет, для меня пока не важно - главное отключить мотор. Я не могу зайти в обработчики (ни в какие) исключений. Что я для этого сделал:
1) Включил глобальные прерывания: __enable_irq();
2) Разрешил их как обычные прерывания: NVIC_EnableIRQ(NonMaskableInt_IRQn);
NVIC_EnableIRQ(HardFault_IRQn);
NVIC_EnableIRQ(MemoryManagement_IRQn);
NVIC_EnableIRQ(BusFault_IRQn);
NVIC_EnableIRQ(UsageFault_IRQn);
NVIC_EnableIRQ(SVCall_IRQn);
NVIC_EnableIRQ(DebugMonitor_IRQn);
NVIC_EnableIRQ(PendSV_IRQn);
NVIC_EnableIRQ(SysTick_IRQn);
3) Переоределил сами обработчики:
extern "C" void HardFault_Handler(void)
{ GPIOB->BSRR |= GPIO_BSRR_BR_5; // выключаем мотор }
ну и так во всех, что есть:
 NonMaskableInt_IRQn     
 HardFault_IRQn       
 MemoryManagement_IRQn    
 BusFault_IRQn      
 UsageFault_IRQn  
 SVCall_IRQn         
 DebugMonitor_IRQn      
 PendSV_IRQn
 SysTick_IRQn
4) искусственно создаю ситуацию появления ошибки. Или обработчик, например Таймера, не переопределю, а он вызывается по переполнению, или такой командой: __asm("svc 0");
5) уже под отладчиком я регистр NVIC_ISER0 устанавливаю в 0xFFFFFFFF, что все прерывания разрешить системные (хотя здесь я не уверен что я правильно настраиваю)
Но при созданной ситуации "STMка зависла", я не попадаю ни в один из определённых обработчиков исключений. А попадаю в цикл всем известный Loop
  .section .text.Default_Handler,"ax",%progbits
Default_Handler:
Infinite_Loop:
b Infinite_Loop
.size Default_Handler, .-Default_Handler

Что я сделал неправильно или что недоделал или в чем не прав и ошибаюсь?

Алексей_Байдин
Ответ на комментарий  Aveal
8 месяцев назад

Спасибо! Для меня с помощью Вас и Эдуарда все разрешилось. Просьба удалить мой вопрос здесь. Я написал свой вопрос на форуме, вдруг там людям понадобятся. Что сам понял, могу ответь потом сам себе уже на форуме.

Эдуард
8 месяцев назад

https://microtechnics.ru/community/stm32/ne-mogu-popast-v-obrabotchik-isklyucheniya/
Это по вопросу Алексея Байдина. Там разбор, что случилось на самом деле.

63
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x