• Система автоматизации с открытым исходным кодом на базе esp8266/esp32 микроконтроллеров и приложения IoT Manager. Наша группа в Telegram

ESP8266 и аппаратный таймер hw_timer

Melandr

Member
Добрый день!
Разбираюсь с работой аппаратного таймера ESP. Сделал простую программу, которая тикает каждую секунду и осуществляет вывод в UART.
Код:
#include "user_interface.h"
#include "hw_timer.h"

bool tickOccured;

void hw_test_timer_cb() {

  tickOccured = true;

}

void ICACHE_FLASH_ATTR user_init(void) {

//  hw_timer_init(NMI_SOURCE, 1);
  hw_timer_init(FRC1_SOURCE, 1); 
  hw_timer_set_func(hw_test_timer_cb);
  hw_timer_arm(1000000);
} // End of user_init

void setup() {

  Serial.begin(115200);
  Serial.println();
  Serial.println();

  Serial.println("");
  Serial.println("--------------------------");
  Serial.println("ESP8266 Timer Test");
  Serial.println("--------------------------");
  tickOccured = false;
  user_init();

}

void loop() {

  if (tickOccured == true)
  {
    Serial.println("Tick Occurred");
    tickOccured = false;
  }

  yield();  // or delay(0);

  //system_soft_wdt_feed();
} //end loop
В принципе программа работает, правда показания вывода времени тиков плавают на единицы миллисекунд. Теперь стоит задача этим таймером организовать 2 интервала времени, что-то типа ШИМ, с возможностью изменения скважности выходного сигнала, формируемого при помощи этого таймера. Но непонятно, как правильно следует инициализировать аппаратный таймер, чтобы получить шим сигнал. В коде выше используется автоповторение.
Код:
void ICACHE_FLASH_ATTR hw_timer_init(frc1_timer_source_type source_type, uint8_t req)
{
    if (req == 1) {
        RTC_REG_WRITE(FRC1_CTRL_ADDRESS,
                      FRC1_AUTO_LOAD | DIVDED_BY_16 | FRC1_ENABLE_TIMER | TM_EDGE_INT);
    } else {
        RTC_REG_WRITE(FRC1_CTRL_ADDRESS,
                      DIVDED_BY_16 | FRC1_ENABLE_TIMER | TM_EDGE_INT);
Переменная req задает режим работы таймера, однократный или повторяющийся.
Как я понимаю, для организации режима ШИМ необходимо в секции setup() единоразово запустить таймер, а далее в обработчиках прерывания от этого таймера инициализировать таймер противоположными значениями времени и изменять состояние флага для организации ШИМ. А в основном цикле уже по флагу переключать вывод GPIO.
Также смотрел на форуме реализации диммеров и не могу понять, как рассчитывают длительность требуемой задержки включения симистора. Вот ссылка на тему https://esp8266.ru/forum/threads/up...tjazhnogo-ventijaltora.4115/page-3#post-59600
Вот инициализация таймера в прерывании по переходу через "0"
Код:
      int dimDelay = 30 * (255 - curDimmerVal) + 400;//400
      hw_timer_arm(dimDelay);
Почему умножают на 30 и прибавляют 400.
Если судить по логике работы функции hw_timer_arm в нее необходимо передать значение в микросекундах.
Следовательно, для организации диммирования необходимо в эту функцию передавать значения от 50 мкс до 10000 мкс
Также имеет ли смысл рулить выходным пином в основном цикле, или можно сделать смену состояния пина в обработчиках прерываний?
 

nikolz

Well-known member
Добрый день!
Разбираюсь с работой аппаратного таймера ESP. Сделал простую программу, которая тикает каждую секунду и осуществляет вывод в UART.
Код:
#include "user_interface.h"
#include "hw_timer.h"

bool tickOccured;

void hw_test_timer_cb() {

  tickOccured = true;

}

void ICACHE_FLASH_ATTR user_init(void) {

//  hw_timer_init(NMI_SOURCE, 1);
  hw_timer_init(FRC1_SOURCE, 1);
  hw_timer_set_func(hw_test_timer_cb);
  hw_timer_arm(1000000);
} // End of user_init

void setup() {

  Serial.begin(115200);
  Serial.println();
  Serial.println();

  Serial.println("");
  Serial.println("--------------------------");
  Serial.println("ESP8266 Timer Test");
  Serial.println("--------------------------");
  tickOccured = false;
  user_init();

}

void loop() {

  if (tickOccured == true)
  {
    Serial.println("Tick Occurred");
    tickOccured = false;
  }

  yield();  // or delay(0);

  //system_soft_wdt_feed();
} //end loop
В принципе программа работает, правда показания вывода времени тиков плавают на единицы миллисекунд. Теперь стоит задача этим таймером организовать 2 интервала времени, что-то типа ШИМ, с возможностью изменения скважности выходного сигнала, формируемого при помощи этого таймера. Но непонятно, как правильно следует инициализировать аппаратный таймер, чтобы получить шим сигнал. В коде выше используется автоповторение.
Код:
void ICACHE_FLASH_ATTR hw_timer_init(frc1_timer_source_type source_type, uint8_t req)
{
    if (req == 1) {
        RTC_REG_WRITE(FRC1_CTRL_ADDRESS,
                      FRC1_AUTO_LOAD | DIVDED_BY_16 | FRC1_ENABLE_TIMER | TM_EDGE_INT);
    } else {
        RTC_REG_WRITE(FRC1_CTRL_ADDRESS,
                      DIVDED_BY_16 | FRC1_ENABLE_TIMER | TM_EDGE_INT);
Переменная req задает режим работы таймера, однократный или повторяющийся.
Как я понимаю, для организации режима ШИМ необходимо в секции setup() единоразово запустить таймер, а далее в обработчиках прерывания от этого таймера инициализировать таймер противоположными значениями времени и изменять состояние флага для организации ШИМ. А в основном цикле уже по флагу переключать вывод GPIO.
Также смотрел на форуме реализации диммеров и не могу понять, как рассчитывают длительность требуемой задержки включения симистора. Вот ссылка на тему https://esp8266.ru/forum/threads/up...tjazhnogo-ventijaltora.4115/page-3#post-59600
Вот инициализация таймера в прерывании по переходу через "0"
Код:
      int dimDelay = 30 * (255 - curDimmerVal) + 400;//400
      hw_timer_arm(dimDelay);
Почему умножают на 30 и прибавляют 400.
Если судить по логике работы функции hw_timer_arm в нее необходимо передать значение в микросекундах.
Следовательно, для организации диммирования необходимо в эту функцию передавать значения от 50 мкс до 10000 мкс
Также имеет ли смысл рулить выходным пином в основном цикле, или можно сделать смену состояния пина в обработчиках прерываний?
про последние две формулы обращайтесь к ее автору.
Предположу следующее:
таймер формирует интервалы в мкс. Но загружаемое в него значение dimDelay рассчитывается по некоторой , произвольной переменной curDimmerVal
которая вычитается и 255 следовательно размером в байт.
Предположу что автор списал этот код с какого-то 8-битного процессора.
А коэффициенты 30 и 400 - растягивают и смещают это значение на нужный автору диапазон.
Скорее всего автор сам не понял, что списал из интернета.
---------------------
Чтобы определиться с коэффициентами, вам надо определиться с диапазоном изменения переменной управления и диапазоном формируемых интервалов.
 

pvvx

Активный участник сообщества
Если судить по логике работы функции hw_timer_arm в нее необходимо передать значение в микросекундах.
#define XS_TO_RTC_TIMER_TICKS(t, prescaler, period) \
(((t) > (0xFFFFFFFF/(APB_CLK_FREQ >> prescaler))) ? \
(((t) >> 2) * ((APB_CLK_FREQ >> prescaler)/(period>>2)) + ((t) & 0x3) * ((APB_CLK_FREQ >> prescaler)/period)) : \
(((t) * (APB_CLK_FREQ >> prescaler)) / period))

void ICACHE_FLASH_ATTR set_new_time_int_us(uint32 us)
{
// максимальный делитель у таумера 0x007fffff
// пределы 0.2..1677721.4 us при пред-делителе DIV_BY_16
#if ((APB_CLK_FREQ>>4)%1000000)
TIMER_LOAD = XS_TO_RTC_TIMER_TICKS(us, 4, 1000);
#else
TIMER_LOAD = us * (APB_CLK_FREQ>>4)/1000000; // = us * 5
#endif
}
 

nikolz

Well-known member
#define XS_TO_RTC_TIMER_TICKS(t, prescaler, period) \
(((t) > (0xFFFFFFFF/(APB_CLK_FREQ >> prescaler))) ? \
(((t) >> 2) * ((APB_CLK_FREQ >> prescaler)/(period>>2)) + ((t) & 0x3) * ((APB_CLK_FREQ >> prescaler)/period)) : \
(((t) * (APB_CLK_FREQ >> prescaler)) / period))

void ICACHE_FLASH_ATTR set_new_time_int_us(uint32 us)
{
// максимальный делитель у таумера 0x007fffff
// пределы 0.2..1677721.4 us при пред-делителе DIV_BY_16
#if ((APB_CLK_FREQ>>4)%1000000)
TIMER_LOAD = XS_TO_RTC_TIMER_TICKS(us, 4, 1000);
#else
TIMER_LOAD = us * (APB_CLK_FREQ>>4)/1000000; // = us * 5
#endif
}
Я так понимаю, это Вы в раздел для начинающих написали,
чтобы им неповадно было спрашивать.
 

pvvx

Активный участник сообщества
Я так понимаю, это Вы в раздел для начинающих написали,
чтобы им неповадно было спрашивать.
Вы читать то умеете? Или только писать?
Разбираюсь с работой аппаратного таймера ESP.
Так что если чел. хочет разобраться с работой аппаратной части таймеров, да задет ещё вопросы о коэф., то ему и подкинуто, что у таймера есть ещё и предделитель и входная частота, которая тоже может отличаться...
И шоб он не мучался дана формула расчета. Её же нет в буке о ESP8266.
 

pvvx

Активный участник сообщества
И заодно для Melandr - примеры как сделать PWM на таймере находятся в самом SDK. Входят в стандартные функции.
 

Melandr

Member
Доброй ночи!
Набросал простую программку, для проверки работы аппаратного таймера
Код:
#include "user_interface.h"
#include "hw_timer.h"

#define TIME_PULSE 20
#define MAX_DIMMING_VALUE 255
#define MIN_DIMMING_VALUE 0

#define PWM_PIN     2         //GPIO2         

byte tickOccured;
int state = 0;
int period = 10000;
int dimDelay = 0;
int timePause;
volatile byte tarBrightness = 20;   //Желаемая яркость 1-100
volatile byte curBrightness = 20;     //Текущая яркость
unsigned long lastMillis = 0;

void ICACHE_RAM_ATTR hw_test_timer_cb() {

  curBrightness = tarBrightness;

  if (state == 0) {
    digitalWrite(PWM_PIN, LOW);
    state = 1;
    hw_timer_arm(TIME_PULSE);
    tickOccured = 0;
  }
  else if (state == 1) {
    digitalWrite(PWM_PIN, HIGH);
    state = 2;
    hw_timer_arm(timePause);
    tickOccured = 1;
  }
  else if (state == 2) {
    digitalWrite(PWM_PIN, LOW);
    state = 0;
    hw_timer_arm(dimDelay);
    tickOccured = 2;
  }
}

void ICACHE_FLASH_ATTR user_init(void) {

  //  hw_timer_init(NMI_SOURCE, 1);
  hw_timer_init(FRC1_SOURCE, 1);
  hw_timer_set_func(hw_test_timer_cb);
  hw_timer_arm(dimDelay);
} // End of user_init

void setup() {

  Serial.begin(115200);
  Serial.println();
  Serial.println();

  Serial.println("");
  Serial.println("--------------------------");
  Serial.println("ESP8266 Timer Test");
  Serial.println("--------------------------");

  pinMode(PWM_PIN, OUTPUT);
  digitalWrite(PWM_PIN, LOW);

  dimDelay = 39.1 * (MAX_DIMMING_VALUE - curBrightness);
  timePause = period - (dimDelay + TIME_PULSE);

  user_init();

}

void loop() {

  dimDelay = 39.1 * (MAX_DIMMING_VALUE - curBrightness);
  timePause = period - (dimDelay + TIME_PULSE);
 
  if (millis() - lastMillis > 5000) {
    lastMillis = millis();
    Serial.print("dimDelay = ");
    Serial.println(dimDelay);
    Serial.print("timePause = ");
    Serial.println(timePause);
  }
  yield();  // or delay(0);

  system_soft_wdt_feed();
} //end loop
и возник вопрос,
В настройках Arduino IDE частота ESP8266 - 80 МГц, какой делитель частоты по умолчанию? Я его в программе не указывал. Второе посмотрел на осциллографе, у меня получается импульс высокого уровня 800 мкс.
SDS00013.png
При этом с периодом импульсов, заданном 10 мс, все нормально
SDS00014.png
Хотя в программе я указываю длительность импульса 20 мкс,
ЗЫ: хотя я завтыкал, так как источник FRC1_SOURCE, в описании SDK минимальное время указывают 100 мкс. Утром поставлю NMI_SOURCE и проверю. Но все равно не стыкуется.
 

pvvx

Активный участник сообщества

pvvx

Активный участник сообщества
digitalWrite() в Arduino исполняется несколько us и имеет нехилый джиттер. Т.е. для PWM не годится.
Минимальный шаг прерывания для NMI от таймера на ESP8266 должен быть более 7 us при 160 MHz и минимальной процедуре обработки (почти asm, но писанный на СИ). Иначе переполнение стека.
При использовании Arduino подходов и процедур это надо умножить раз в сто.
 

nikolz

Well-known member
Доброй ночи!
Набросал простую программку, для проверки работы аппаратного таймера
Хотя в программе я указываю длительность импульса 20 мкс,
ЗЫ: хотя я завтыкал, так как источник FRC1_SOURCE, в описании SDK минимальное время указывают 100 мкс. Утром поставлю NMI_SOURCE и проверю. Но все равно не стыкуется.
Для диммера нет смысла формировать импульс в 10-20 мкс в колбеке таймера.
Формируйте импульс с помощью задержки
Т е выдали 1 на пин сделали задержку на 15 мкс , выдали 0 на пин.
На вывод 1 и 0 на пин уйдет несколько мкс. В итоге будет примерно 20.
------------
таймер лучше включить в однократный режим
а в конце колбека делать рестарт таймера.
При рестарте учтите время на выполнение кода колбека если надо точно выдержать интервал.
 

pvvx

Активный участник сообщества
Если я инициализирую таймер этой функцией однократно
hw_timer_init(FRC1_SOURCE, 0);
Как сделать рестарт в коллбеке?
Загрузить что-нить в сам счетчик таймера :) Т.к. значение сравнения для вызова прерывания он уже проглатил и не возьмет новое до счета до него...
 

pvvx

Активный участник сообщества
Для диммера нет смысла формировать импульс в 10-20 мкс в колбеке таймера.
Формируйте импульс с помощью задержки
Т е выдали 1 на пин сделали задержку на 15 мкс , выдали 0 на пин.
На вывод 1 и 0 на пин уйдет несколько мкс. В итоге будет примерно 20.
Мигающая реклама, ох как-же это надоело. И так оно везде мигает... Вам глазки не жалко? Или там холодильники и вы влепили медный-закисный диод от трамвая прямо в фазу и землю чтобы они все в запитанном доме погорели?
 

pvvx

Активный участник сообщества
Melandr Для нормального диммирования вам придется анализировать и огибающую в сети. Всё остальное - это детсад и китайщина горячо любимая nikolz
 

pvvx

Активный участник сообщества
И не забудьте отключить WiFi. Иначе реконекты приведут к паузам в вашей программе на секунду или около того, а во время простого соединения рассчитывайте на паузы от 150 us и более на время отработки дровами WiFi своих нужд... Т.е. всё будет мигать и гореть.
 

Melandr

Member
То есть при активном подключении к WiFi сети программа не сможет крутиться как положено из-за задержек WiFi? Сеть я анализирую, есть детектор перехода через "0".
 

pvvx

Активный участник сообщества
То есть при активном подключении к WiFi сети программа не сможет крутиться как положено из-за задержек WiFi?
Да. И это касается не только цикла Loop() в Arduino, но и прерываний. Немного менее сказывается только на NMI прерывания...
Сеть я анализирую, есть детектор перехода через "0".
Это не о чем не говорит. Синус в сети из-за современных китай-нагрузок без корректора мощности искажен очень сильно.
Путем отсчета от начала перехода через ноль вы будете мигать нагрузкой в зависимости от работы других приборов в сети. Т.е. ваша нагрузка будет показывать когда и кто включился или отключился, или изменил режим работы :p
 

pvvx

Активный участник сообщества
Если ESP работает в режиме AP, то вам незя запрещать прерывания на более чем 1 us. Иначе будет скакать сигнал beacon-а c меткой времени в 1 us.
Так-же не желателен цикл Loop() с длительным временем не отдачи wifi ресурса единственного ядра CPU. Тогда ESP будет хамить в эфире - сбивать работу другим в сети (сбивать пакеты) и мешать работать с AP устройствам в режимах малого потребления (к примеру будет быстрее садиться телефон подключенный к той-же AP).
Но не стоит беспокоиться - всем Ардуинщикам на это наплевать - такова концепция.
 
Сверху Снизу