• Уважаемые посетители сайта esp8266.ru!
    Мы отказались от размещения рекламы на страницах форума для большего комфорта пользователей.
    Вы можете оказать посильную поддержку администрации форума. Данные средства пойдут на оплату услуг облачных провайдеров для сайта esp8266.ru
  • Система автоматизации с открытым исходным кодом на базе 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).
Но не стоит беспокоиться - всем Ардуинщикам на это наплевать - такова концепция.
 
Сверху Снизу