STM32 - энергосбережение и WatchDog





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

В статье описано использование различных режимов энергосбережения и сторожевых таймеров в микроконтроллере stm32 с библиотекой HAL.


Действия будут производится на плате Discovery F3, на ней есть разъём для подключения амперметра…






Схема питания микроконтроллера stm32F3 выглядит следующим образом…


обозначения ножек питания


VDDA domain

HSI, LSI, PLL, АЦП, ЦАП, температурный датчик, OPAMP, COMP и Reset block. Питается от входов Vssa и Vdda.

На некоторых МК есть вход Vref, это опорное напряжение для АЦП.


VDD domain

Напряжение подаваемое на входы Vdd и Vss питает различную периферию (GPIO, UART и т.д.), логику WakeUp, независимый WatchDog (IWDG), и проходя через встроенный регулятор напряжения (который понижает напряжение до 1.8V) запитывает ядро.


RTC domain

RTC, внешний кварц для часов (32.768кГц), LSE и Backup-регистры.

В обычном режиме RTC domain питается от Vdd, если же напряжение пропадает (за этим следит «детектор низкого напряжения»), то происходит переключение на резервный источник — Vbat.

Если батарейка не подключена, то рекомендуется соединять Vbat с Vdd.


На некоторых stm32 есть специальный вход — NPOR, подача сигнала на этот вход переведёт RTC domain на резервный источник. У меня такого нет, поэтому сказать нечего.



У stm32 есть супервизор подачи питания обеспечивающий стабильный запуск и работу микроконтроллера. Супервизор реализован в виде двух систем:

POR (Power On Reset – система сброса при подаче питания) — микроконтроллер будет находиться в режиме сброса пока напряжение питания на VDD не достигнет 2v. POR контролирует только напряжение питания VDD. Во время запуска микроконтроллера VDDA должен запитываться первым и быть больше или равным VDD.

PDR (Power Down Reset – система сброса при снижении питания) — при снижении напряжения питания ниже порога, микроконтроллер будет переведён в состояние сброса. PDR контролирует напряжение питания VDD и VDDA. Контроль за напряжением VDDA можно отключать через Option bytes. Так же PDR отвечает за переключение на Vbat.

Ещё есть PVD (Programmable Voltage Detector) — см. ниже.




Режимы пониженного энергопотребления


Есть три режима:

Sleep mode — спящий режим.
Stop mode — режим остановки.
Standby mode — режим ожидания.



Sleep mode — в этом режиме тактирование ядра остановлено, все цепи ввода-вывода микроконтроллера сохраняют своё состояние.

Вход в режим осуществляется командой…

HAL_PWR_EnterSLEEPMode(PWR_LOWPOWERREGULATOR_ON, PWR_SLEEPENTRY_WFI);


Первый аргумент — PWR_LOWPOWERREGULATOR_ON — не используется на stm32F1 и stm32F3, присутствует для совместимости кода с другими МК.

Для второго аргумента есть два варианта:

PWR_SLEEPENTRY_WFI(Wait for Interrupt) — выход из режима произойдёт при возникновении прерывания.

PWR_SLEEPENTRY_WFE(Wait for Event) — выход из режима произойдёт при возникновении события.



Режим Sleep удобно использовать в программе, которая ничего не делает кроме обработки прерываний. То есть, обработали какое-то прерывание и усыпили камень, возникло очередное прерывание — камень проснулся, обработал его, и опять уснул.

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

Настраиваем таймер №1 на переполнение каждые 5 сек…



И включаем прерывание…




Прописываем колбек:

/* USER CODE BEGIN 0 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance == TIM1) //check if the interrupt comes from TIM1
	{
                HAL_ResumeTick();
		HAL_GPIO_TogglePin(LD10_GPIO_Port, LD10_Pin);
	}
}

Про HAL_ResumeTick() см. ниже.


Запускаем таймер:

/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim1);



В бесконечном цикле усыпляем:

/* USER CODE BEGIN WHILE */
  while (1)
  {
          HAL_SuspendTick();
	  HAL_PWR_EnterSLEEPMode(PWR_LOWPOWERREGULATOR_ON, PWR_SLEEPENTRY_WFI);
          ...


Перед тем как войти в режим Sleep нужно вызвать функцию HAL_SuspendTick(), которая отключит прерывания от SysTick. Это таймер, отвечающий за функции задержки, типа HAL_Delay(). При выходе из режима включаем эти прерывания функцией HAL_ResumeTick() (см. выше).

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


Ещё есть дополнительное условие входа в спящий режим: если установить бит SLEEPONEXIT, то программа будет входить в этот режим только после того, как закончится обработка последнего прерывания. Если очистить бит, то вход произойдёт сразу же после вызова HAL_PWR_EnterSLEEPMode().

HAL_PWR_EnableSleepOnExit(); // установить
HAL_PWR_DisableSleepOnExit(); // очистить



После выхода из режима Sleep выполнение программы продолжается с того места где она была остановлена.




Stop mode — режим глубокой спячки ядра. Останавливаются все генераторы тактовой частоты в 1.8V domain, а так же HSI и HSE. Все цепи ввода-вывода сохраняют своё состояние.



Вход в это режим осуществляется функцией…

HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // или PWR_STOPENTRY_WFE


У первого аргумента есть два варианта:

PWR_LOWPOWERREGULATOR_ON — регулятор напряжения работает в режиме пониженного энергопотребления, но возникает дополнительная задержка при выходе из режима.

PWR_MAINREGULATOR_ON — регулятор напряжения работает в обычном режиме. Потребление энергии увеличивается, но время выхода из режима сокращается.

У второго аргумента тот же функционал, что и у спящего режима.


Выход из режима остановки возможен только с помощью прерывания или события на любой линии EXTI (таймер здесь уже не поможет ибо генераторы отключены). Прерывания и события на этой линии могут вызываться с помощью WakeUp, будильника (работает LSI RC или LSE), USART, I2C, ну и само собой внешнего прерывания.


При выходе из режима остановки используется HSI RC генератор, поэтому нужно заново сконфигурировать тактирование с помощью функции SystemClock_Config().



Настройте WakeUp


Программа будет оживать каждые пять секунд.


Активируйте прерывание…




В бесконечном цикле мигнём светиком, выведем значение счётчика и остановим программу:

/* USER CODE BEGIN WHILE */
  while (1)
  {
	  HAL_GPIO_WritePin(LD10_GPIO_Port, LD10_Pin, GPIO_PIN_SET);
	  HAL_Delay(100);
	  HAL_GPIO_WritePin(LD10_GPIO_Port, LD10_Pin, GPIO_PIN_RESET);
	  HAL_Delay(100);
	  
	  uint32_t wu = HAL_RTCEx_GetWakeUpTimer(&hrtc); // посмотреть значение счётчика WakeUp
	  snprintf(trans_str, 63, "WakeUp %lu\n", wu);
	  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
	  
	  HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // PWR_MAINREGULATOR_ON
	  // после wakeup'а программа стартует отсюда
	  SystemClock_Config(); // рестартуем системный клок
          ...



У F103 нету WakeUp'а, поэтому можно воспользоваться будильником.



Когда МК находится в режиме остановки, то прошиваться он не будет. Нужно зажать кнопочку Reset и отпустить её за мгновение до появления надписи Waiting for debugger connection...


Atolic TrueStudio




Standby mode


Это самый экономичный режим. Отключается почти что всё, кроме RTC и IWDG (независимый WatchDog. См. ниже). Информация в регистрах теряется, а цепи ввода-вывода отключаются. Состояние МК почти такое же как и без питания.


Вход в режим осуществляется функцией…

HAL_PWR_EnterSTANDBYMode();



Выходить из режима можно тремя способами: WakeUp, будильник и IWDG. Сам по себе выход похож на то, как если бы был нажат Reset, то есть МК начинает работу с самого начала.


Для проверки можно использовать код от предыдущего режима изменив функцию входа…

/* USER CODE BEGIN WHILE */
  while (1)
  {
	  HAL_GPIO_WritePin(LD10_GPIO_Port, LD10_Pin, GPIO_PIN_SET);
	  HAL_Delay(100);
	  HAL_GPIO_WritePin(LD10_GPIO_Port, LD10_Pin, GPIO_PIN_RESET);
	  HAL_Delay(100);
	  
	  uint32_t wu = HAL_RTCEx_GetWakeUpTimer(&hrtc); // посмотреть значение счётчика WakeUp
	  snprintf(trans_str, 63, "WakeUp %lu\n", wu);
	  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
	  
	  HAL_PWR_EnterSTANDBYMode();
          ...


Здесь есть один нюанс. Дело в том, что после срабатывания WakeUp'а микроконтроллер сразу же ресетится, а флаг WakeUp'а остаётся в регистрах взведённым. Соответственно после инициализации и очередного входа в режим, МК видит этот флаг и тут же опять ресетится, и так по кругу.

Побороть это очень просто, надо сбрасывать флаг до бесконечного цикла:

/* USER CODE BEGIN 2 */
  __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
/* USER CODE END 2 */


При использовании будильника нужно так же сбрасывать этот флаг.

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


IWDG выводит МК из Standby просто обресетивая его. Описание ниже.


Помимо режимов энергосбережения можно использовать другие методы снижения энергопотребления: уменьшить тактовую частоту, использовать внутренний генератор (HSI RC), понижать частоту перед переходом в спящий режим, отключать различные шины (см. файл stm32f3xx_hal_rcc.h) и т.д.




PVD (Programmable Voltage Detector) — мониторинг напряжения питания (VDD) микроконтроллера.

В программе задаётся определённый уровень напряжения…

PWR_PVDLEVEL_0 — 2.2V
PWR_PVDLEVEL_1 — 2.3V
PWR_PVDLEVEL_2 — 2.4V
PWR_PVDLEVEL_3 — 2.5V
PWR_PVDLEVEL_4 — 2.6V
PWR_PVDLEVEL_5 — 2.7V
PWR_PVDLEVEL_6 — 2.8V
PWR_PVDLEVEL_7 — 2.9V

… и если напряжение поднимется выше или опустится ниже этого уровня, то сработает прерывание.



Далее поясню всё на примере.

Активируйте прерывание…




В соседней вкладке создаём инициализацию прерывания…



В коде появится функция...

static void MX_NVIC_Init(void)
{
  /* PVD_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(PVD_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(PVD_IRQn);
}



В программе объявляем глобальный флажок:

/* USER CODE BEGIN PV */
char trans_str[64] = {0,};
volatile uint8_t flag = 0; // флажок
/* USER CODE END PV */


Создаём две функции — инициализация PVD и колбек:

/* USER CODE BEGIN 0 */
static void PVD_Config(void)
{
    PWR_PVDTypeDef sConfigPVD = {0,};
    sConfigPVD.PVDLevel = PWR_PVDLEVEL_6; // 2.8V
    sConfigPVD.Mode = PWR_PVD_MODE_IT_RISING_FALLING; // прерывание при падении и при превышении
    HAL_PWR_ConfigPVD(&sConfigPVD); // конфигурируем
    HAL_PWR_EnablePVD(); // активируем PVD
}

void HAL_PWR_PVDCallback(void)
{
    flag = 1;
}


Чтоб сконфигурировать PVD нужно передать структуру (sConfigPVD) с двумя элементами: первый — пороговое значение, а второй определяет признак, по которому будет вызываться прерывание или событие. В данном случае прерывание будет вызываться и при падении напряжения ниже порога и при превышении. Можно сделать чтоб только при понижении или наоборот.

варианты
PWR_PVD_MODE_NORMAL — Basic mode is used
PWR_PVD_MODE_IT_RISING — External Interrupt Mode with Rising edge trigger detection
PWR_PVD_MODE_IT_FALLING — External Interrupt Mode with Falling edge trigger detection
PWR_PVD_MODE_IT_RISING_FALLING — External Interrupt Mode with Rising/Falling edge trigger detection
PWR_PVD_MODE_EVENT_RISING — Event Mode with Rising edge trigger detection
PWR_PVD_MODE_EVENT_FALLING — Event Mode with Falling edge trigger detection
PWR_PVD_MODE_EVENT_RISING_FALLING — Event Mode with Rising/Falling edge trigger detection


Инициализируем PVD, а в бесконечном цикле проверяем флажок и мигаем светиком:


  /* USER CODE BEGIN 2 */
  PVD_Config();
  /* USER CODE END 2 */

  /* Infinite loop */  
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  if(flag == 1)
	  {
		  HAL_GPIO_WritePin(LD10_GPIO_Port, LD10_Pin, GPIO_PIN_SET);
		  HAL_Delay(100);
		  HAL_GPIO_WritePin(LD10_GPIO_Port, LD10_Pin, GPIO_PIN_RESET);
		  HAL_Delay(100);
		  flag = 0;

		  snprintf(trans_str, 63, "FLAG PVDO %d\n", __HAL_PWR_GET_FLAG(PWR_FLAG_PVDO)); 
		  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
	  }

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

Если нет необходимости оперативно реагировать на изменение напряжения, то можно просто (без прерываний) проверять этот флаг где-то в программе и делать какие-то действия. При инициализации укажите режим — PWR_PVD_MODE_NORMAL.


Теперь прошейте плату и уменьшайте напряжение (нужно раздобыть какой-нибудь регулятор напряжения), как только напряжение станет ниже 2.8v, то сработает прерывание и флаг PVDO будет равен единице. При повышении напряжения прерывание сработает снова, а PVDO обнулится.

Прерывание сработает только в момент перехода через порог. Если указать порог 2.4v и просто подать на плату 3v, то ничего не произойдёт.


Во избежание ложных срабатываний (если напряжение будет «дрожать» около порогового значения), у PVD предусмотрен гистерезис ~100mV.

картинка



PVD может пробуждать МК из режима Sleep, а в режиме Standby он не работает.




WatchDog


WatchDog — сторожевой таймер — предназначен для предотвращения зависания микроконтроллера. По сути это просто счётчик, который запускается во время старта, а в бесконечном цикле постоянно сбрасывается. Если по каким-то причинам бесконечный цикл зависнет и счётчик не будет сброшен, то он досчитав до конца сделает микроконтроллеру Reset.

У stm32 есть два WatchDog'а, один независимый — IWDG, тактируется от HSI RC и работает в любых режимах, а второй WWDG, тактируется той же частотой что и шина APB1. Соответственно в режимах глубокой спячки не функционирует.

Счётчики обоих WatchDog'ов считают «вниз» от установленного значения.


Настройте IWDG следующим образом…


Предделитель частоты устанавливаем — 64, а счётчик будет считать от 3125.

IWDG тактируется от LSI RC (40кГц), соответственно он досчитает до нуля через пять секунд. 40кГц / 64 = 625 тиков в секунду, 3125 / 625 = 5 сек. Если мы не сбросим счётчик в течении пяти секунд, то он обресетит МК.


Для проверки соорудим вот такой код:

/* USER CODE BEGIN 2 */
  snprintf(trans_str, 63, "START\n");
  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  static uint16_t sec = 1;

	  HAL_GPIO_WritePin(LD10_GPIO_Port, LD10_Pin, GPIO_PIN_SET);
	  HAL_Delay(500);
	  HAL_GPIO_WritePin(LD10_GPIO_Port, LD10_Pin, GPIO_PIN_RESET);
	  HAL_Delay(500);


	  snprintf(trans_str, 63, "Sec %d\n", sec++);
	  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);

	  //HAL_IWDG_Refresh(&hiwdg); // сброс WatchDog'а
          ...


Поскольку сброс WatchDog'а закомментирован, то МК ресетится каждые 5 сек.



Теперь раскомментируйте HAL_IWDG_Refresh(&hiwdg) и МК будет работать нормально. Reset произойдёт только если программа зависнет.

Пять секунд конечно много, это значение должно быть чуть больше времени работы цикла + прерывания, если они есть. Однако можно устанавливать большую задержку (до 26 сек) и вгонять МК в режим Standby. То есть использовать IWDG как WakeUp.


IWDG window value — любопытная штука, называется «окно» WatchDog'а. Здесь устанавливается значение раньше которого нельзя сбрасывать счётчик. Если сбросить раньше, то произойдёт Reset.

Например, если указать 2000 (окно от 3125 до 2000)



… то WatchDog будет постоянно ресетить МК.



Это происходит из-за того, что WatchDog сбрасывается раньше чем успевает досчитать от 3125 до 2000. Если указать 2600, то всё будет окей.

Таким образом можно контролировать не только зависание программы, но и слишком быстрое выполнение её.

зачем это надо
Допустим мы добавили в наш пример какую-то функцию, которая должна выполнятся не менее трёх секунд, тогда создаём окно от 3125 до 1300, а сброс WatchDog'а прописываем после выполнения этой функции. Теперь если что-то пойдёт не так и программа не войдёт в эту функцию, то WatchDog сделает Reset.


Чтоб «окно» не участвовало в работе, оставляйте его значение равным или больше счётчика.

На F103 «окно» есть только у WWDG. Он кстати поэтому так и называется — WWDG (Window Watchdog).


IWDG может выводить МК из любого режима энергосбережения, включая Standby.




WWDG не такой крутой как IWDG, он не умеет выводить МК из режима Standby, но зато может генерить очень полезное прерывание.

Тактируется на частоте PCLK1 (APB1) делённой на постоянный делитель 4096. Настройки предделителя, «окна» и счётчика такие же как и у IWDG. Счётчик считает в диапазоне от 127 до 64. Да, вот так вот.

Для наглядности эксперимента пришлось понизить частоту шины до 162кГц...



… а WWDG настроил так...


162кГц / 4096 / 4 = 9.89 тиков в секунду, 127 — 64 = 63, 63 / 9.89 = 6.37 сек.



Если активировать Early wakeup interrupt ⇨ Enable и включить прерывание, тогда WatchDog перед тем как сделать харакири Reset вызовет это самое прерывание. То есть если программа подвисла, то прежде чем всё обресетится, можно выполнить какие-то действия в обработчике, например сохранить данные.


Колбек выглядит так:

void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef* hwwdg)
{
    // последнее желание
}


Счётчик сбрасывается командой…

HAL_WWDG_Refresh(&hwwdg);




На этом пожалуй всё.


Всем спасибо


Чтиво

Телеграм-чат istarik

Телеграм-чат STM32


  • 0
  • 54860
Поддержать автора


Telegram-чат istarik

Задать вопрос по статье
Telegram-канал istarik

Известит Вас о новых публикациях






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

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