Выбор контроллера (18+ ШИМ, беспроводная связь, 3.3 логика, встроенный BEC)

Сложно гадать на кофейной гуще.

Обложи вызовы и циклы cpu_ticks(). Будет понятно, кто сколько тиков процессора съедает.

Можно эту же функцию приспособить, вместо чтения таймера. Переполняется она, правда, секунда за 17, что-ли.

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

Выглядит странновато. timer_get_counter_value() как будто вообще не требует затрат времени. Хотя может особенности ее реализации. Я ее код так и не нашел, как будто концы уводят в libdriver.a, но хз чем открывать, чтобы вся кодировка прочлась

То что между итерациями цикла много времени - это норма. У меня вывод в сериал из loop(), причем 10 раз в секунду - иначе у платы начинаются проблемы с прошиванием, а иногда останавливается в loop пока порт не отключишь. Забавно причем - ШИМ по прерываниям продолжает идти, но значения в loop не меняются до тех пор, пока открыт монитор порта))))

ее использовать для паузы между импульсами, или для задержки на продолжительность импульсов?

Я долго думал над тем, как реализовать этот многоканальный шим… Генерация импульсов - задача довольно ответственная, если их фактическая длительность будет разбегаться с требуемой, это обязательно отразится на кинематике в виде неточности позиционирования конечностей, либо побочных паразитных явлениях. Соответственно, отдавать контроль другим задачам, пока подается импульс, может стать чревато…

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

Я думал над альтернативной реализацией:

  1. Перед генерацией импульсов отсортировать каналы по текущей длине импульса по возрастанию (например, построить массив указателей на пары значение-номер порта)
  2. Вычислить дельты времени от одновременного начала импульсов, до окончания первого из них d0, между окончанием первого и второго импульса d1, между окончанием второго и третьего импульсов d2 и так далее.
  3. Поднять все порты, ждать d0
  4. Погасить порт по первому указателю, ждать d1
  5. Погасить порт по второму указателю, ждать d2
  6. … и так для всех портов

Возможно я и этот вариант попробую. Нужно будет разобраться, насколько точна функция задержки delaymicroseconds() или предложенная тобой ets_delay_us() на ESP32S3, не передаст ли она управление шедулеру. Ещё очень интересно, как будет работать delaymicroseconds(0) или ets_delay_us(0), когда 2 или более каналов будут иметь одинаковые значения.

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

Хорошая.

Напоминает классический timeout() из BSD. Там тоже, безо всяких прерываний, сортировались все таймауты, отжидалась дельта до первого, совершалось действие, ждали следующую дельту…

Поэксперементируй.

Насколько я понимаю, все же генерация идет не всегда, а только в те моменты, когда сервопривод должен двигаться, правильно?

Стало быть, можно и задачу отдельную под это использовать:

пока нужно - задача генерит шим, (используя задержку-цикл ets_delay_us(delta), не delay()!). Задержка-цикл не отдает управление.

Когда движение закончено - явно отдаем управление, taskYIELD()
.
Задачу эту запусти на другом ядре: loop() у тебя выполняется на первом ядре, значит, запусти на нулевом. Чтобы она не влияла на основное приложение. Если заморочишся и сделаешь, а оно будет перезагружаться из-за вотчдога - пиши сюда :). Исправим.

Эта функция зашита в ROM, вот она:

  ets_delay_us:
entry	a1, 32
call8	xthal_get_ccount       // чтение регистра-счетчика тактов процессора
l32r	a8, [g_ticks_per_us_pro]  // глобальная переменная (количество тиков на микросекунду)
mov	a3, a10
l32i	a8, a8, 0
mull	a2, a2, a8
call8	xthal_get_ccount // опять читаем счетчик тактов процессора
sub	a10, a10, a3
bltu	a10, a2, 0x40008544 // ... и повторяем, пока не прождем сколько надо
retw

Она ничего не знает ни про многозадачность, ни про шедулер.

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

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

вполне

уже))) сейчас на работе, писал вслепую, но скоро домой, там и проверю


struct cChannel { uint16_t value; uint8_t port; };

cChannel cahnnels[18] = { { 0, 14 },
                          { 0, 13 },
                          { 0, 12 },
                          { 0, 11 },
                          { 0,  4 },
                          { 0,  3 },
                          { 0,  2 },
                          { 0,  1 },
                          { 0, 48 },
                          { 0,  5 },
                          { 0,  6 },
                          { 0,  7 },
                          { 0,  8 },
                          { 0,  9 },
                          { 0, 10 },
                          { 0, 17 },
                          { 0, 18 },
                          { 0, 21 } };



// поднимает порт с указанным номером
void portHIGH (uint8_t port) {
  
  if (port > 31) GPIO.out1_w1ts.val |= 1 << (port - 32);
  else                GPIO.out_w1ts |= 1 << port;
  
}
// гасит порт с указанным номером
void portLOW (uint8_t port) {
  
  if (port > 31) GPIO.out1_w1tc.val |= 1 << (port - 32);
  else                GPIO.out_w1tc |= 1 << port;
  
}

// генерирует импульсы нужной длины на все каналы
void altPWM (void *param) {
  
  while(1) {
    
    // время старта задачи (для шедулера)
    TickType_t lastRunAt = xTaskGetTickCount();
  
    // список номеров портов
    uint8_t ports[18];
    // список значений дельт времени в микросекундах
    uint16_t dt[18];
    // минимум из предыдущей итерации
    uint16_t minP = 0;
    // минимум на текущей итерации
    uint16_t minC = 65535;
    // индекс массива
    uint8_t i = 0;
    // признак того, что индекс массива менялся каждую итерацию
    bool ok = true;
    
    // построение списка портов и дельт времени, упорядоченного по длине импульса
    while ((i < 18) && ok) {
      
      // если индекс i поменяется в этой итерации, то ok снова станет true,
      // иначе алгоритм закончит свою работу (см пояснения ниже)
      ok = false;
      
      // поиск минимального значения, для которого еще не вычислялась дельта времени
      // может не найтись, только если все cahnnels[] == 65535
      for (uint8_t j = 0; j < 18; j++)
        if (cahnnels[j].value < minC && cahnnels[j].value > minP) minC = cahnnels[j].value;
      
      // вычисление дельты времени для текущего minC
      // на первой итерации будет == minC (длительность самого короткого импульса)
      uint16_t dtc = minC - minP;
      // перезапись минимума из предыдущей итерации
      minP = minC;
      
      // копирование номеров портов и установка значений дельты времени
      // для всех портов, у которых длина импульса равна minC
      for (uint8_t j = 0; j < 18; j++) if (cahnnels[j].value == minC) {
        
        // установка дельты времени
        dt[i] = dtc;
        // ВАЖНО!!! если будет более одного порта с такой длиной импульса
        // то задержка требуется однократно, после чего все порты должны погаснуть одновременно
        dtc = 0;
        // копирование номера порта
        ports[i] = cahnnels[j].port;
        // инкримент индекса
        i++;
        // если на любой итерации while() не дошли до этой команды,
        // значит в cahnnels[j] по каким-то внешним причинам произошли изменения,
        // и значения, которые были равны minC, изменились, либо не нашлось minC,
        // while() рискует зависнуть, а корректность работы алгоритма под сомнением
        // поэтому в начале цикла было установлено ok = false;
        ok = true;
        
      }
    
    }
  
    // если сортировка прошла успешно
    if (ok) {
      
      // поднимаем все порты
      for (uint8_t j = 0; j < 18; j++) portHIGH(ports[j]);
      
      // согласно очередности осуществляем соответствующую задержку и гасим порт
      for (uint8_t j = 0; j < 18; j++) {
        ets_delay_us(dt[j]);
        portLOW(ports[j]);
      }
      
    }
    
    // в любом случае пересчитываем тригонометрию с дельтой времени 20 мс
    
    // ...
    
    // отдаем управление шедулеру
    vTaskDelayUntil(&lastRunAt, pdMS_TO_TICKS(20));
    
  }
  
  // аварийная остановка задачи (не должна сработать никогда)
  vTaskDelete(NULL);
  
}




  TaskHandle_t task_pwm; // точно ли нужен?

 xTaskCreatePinnedToCore(
                    altPWM, // Функция
                    "PWM", // имя
                    4096, // размер стека функции (пока не нашел способа правильно вычислить)
                    NULL, // параметры (хз что)
                    (2 | portPRIVILEGE_BIT), // приоритет (пока хз как правильно)
                    &task_pwm, // дескриптор (укажем NULL?)
                    xPortGetCoreID()); // ядро


насчет этого… уже выше упоминали, но я не совсем допер все-же. У меня основной проект не шибко тяжелый для этой мощности проца. Будет:

генерация шим (блокирующая на 3мс каждые 20мс, т.е. не более 15% времени)
немного тригонометрических и алгебраических расчетов (каждые 20мс, сразу после генерации импульсов ШИМ)
связь с пультом в обе стороны с помощью esp-now, не более 50 передач в секунду, даже наверное не более 20
очень простенький action-manager, который будет определять, какие действия можно делать по командам с пульта

мне надо бы понимать, на каком ядре крутятся протоколы и esp-now, чоб вот их не трогать. А то ядро, которое я своим кодом займу пусть хоть подзавязку зашьется, я разберусь

Ну, должны, по задумке на нулевом (0=PROtocol, 1=APPlication). А там как оно на самом деле - не ясно. Инженеры из Espressif говорили (на esp32.com), что они отошли от этого деления, а названия у ядер (PRO и APP) остались по инерции.

Да, это

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"


//               NANO
//  GPIO48 PWM09      PWM18 GPIO21
//   GPIO1 PWM08      PWM17 GPIO18
//   GPIO2 PWM07      PWM16 GPIO17
//   GPIO3 PWM06      PWM15 GPIO10
//   GPIO4 PWM05      PWM14 GPIO9
//  GPIO11 PWM04      PWM13 GPIO8
//  GPIO12 PWM03      PWM12 GPIO7
//  GPIO13 PWM02      PWM11 GPIO6
//  GPIO14 PWM01      PWM10 GPIO5



struct cChannel { uint16_t value; uint8_t port; };

cChannel cahnnels[18] = { { 1, 14 },
                          { 1, 13 },
                          { 1, 12 },
                          { 1, 11 },
                          { 1,  4 },
                          { 1,  3 },
                          { 1,  2 },
                          { 1,  1 },
                          { 1, 48 },
                          { 1,  5 },
                          { 1,  6 },
                          { 1,  7 },
                          { 1,  8 },
                          { 1,  9 },
                          { 1, 10 },
                          { 1, 17 },
                          { 1, 18 },
                          { 1, 21 } };



// поднимает порт с указанным номером
void portHIGH (uint8_t port) {
  
  if (port > 31) GPIO.out1_w1ts.val |= 1 << (port - 32);
  else                GPIO.out_w1ts |= 1 << port;
  
}
// гасит порт с указанным номером
void portLOW (uint8_t port) {
  
  if (port > 31) GPIO.out1_w1tc.val |= 1 << (port - 32);
  else                GPIO.out_w1tc |= 1 << port;
  
}

uint32_t ERR_COUNT = 0;

// генерирует импульсы нужной длины на все каналы
void altPWM (void *param) {
  
  while(1) {
    
    // время старта задачи (для шедулера)
    TickType_t lastRunAt = xTaskGetTickCount();
  
    // список номеров портов
    uint8_t ports[18];
    // список значений дельт времени в микросекундах
    uint16_t dt[18];
    // минимум из предыдущей итерации
    uint16_t minP = 0;
    // минимум на текущей итерации
    uint16_t minC = 65535;
    // индекс массива
    uint8_t i = 0;
    // признак того, что индекс массива менялся каждую итерацию
    bool ok = true;
    
    // построение списка портов и дельт времени, упорядоченного по длине импульса
    while ((i < 18) && ok) {

      // если индекс i поменяется в этой итерации, то ok снова станет true,
      // иначе алгоритм закончит свою работу (см пояснения ниже)
      ok = false;
      
      // поиск минимального значения, для которого еще не вычислялась дельта времени
      // может не найтись, только если все cahnnels[] == 65535
      minC = 65535;
      for (uint8_t j = 0; j < 18; j++)
        if (cahnnels[j].value < minC && cahnnels[j].value > minP) minC = cahnnels[j].value;
      
      // вычисление дельты времени для текущего minC
      // на первой итерации будет == minC (длительность самого короткого импульса)
      uint16_t dtc = minC - minP;
      // перезапись минимума из предыдущей итерации
      minP = minC;
      
      // копирование номеров портов и установка значений дельты времени
      // для всех портов, у которых длина импульса равна minC
      for (uint8_t j = 0; j < 18; j++) if (cahnnels[j].value == minC) {
        
        // установка дельты времени
        dt[i] = dtc;
        // ВАЖНО!!! если будет более одного порта с такой длиной импульса
        // то задержка требуется однократно, после чего все порты должны погаснуть одновременно
        dtc = 0;
        // копирование номера порта
        ports[i] = cahnnels[j].port;
        // инкримент индекса
        i++;
        // если на любой итерации while() не дошли до этой команды,
        // значит в cahnnels[j] по каким-то внешним причинам произошли изменения,
        // и значения, которые были равны minC, изменились, либо не нашлось minC,
        // while() рискует зависнуть, а корректность работы алгоритма под сомнением
        // поэтому в начале цикла было установлено ok = false;
        ok = true;
        
      }
    
    }
  
    // если сортировка прошла успешно
    if (ok) {
      
      // поднимаем все порты
      for (uint8_t j = 0; j < 18; j++) portHIGH(ports[j]);

      // согласно очередности осуществляем соответствующую задержку и гасим порт
      for (uint8_t j = 0; j < 18; j++) {
        ets_delay_us(dt[j]);
        portLOW(ports[j]);
      }
      
    } else ERR_COUNT++;
    
    // в любом случае пересчитываем тригонометрию с дельтой времени 20 мс
    
    // ...
    
    // отдаем управление шедулеру
    vTaskDelayUntil(&lastRunAt, pdMS_TO_TICKS(20));
    
  }
  
  // аварийная остановка задачи (не должна сработать никогда)
  vTaskDelete(NULL);
  
}






TaskHandle_t task_pwm;



void setup () {

  Serial.begin (9600);
  Serial.println ("Hi!");

  pinMode(A7, OUTPUT);
  pinMode(A6, OUTPUT);
  pinMode(A5, OUTPUT);
  pinMode(A4, OUTPUT);
  pinMode(A3, OUTPUT);
  pinMode(A2, OUTPUT);
  pinMode(A1, OUTPUT);
  pinMode(A0, OUTPUT);
  pinMode(D13, OUTPUT);
  pinMode(D2, OUTPUT);
  pinMode(D3, OUTPUT);
  pinMode(D4, OUTPUT);
  pinMode(D5, OUTPUT);
  pinMode(D6, OUTPUT);
  pinMode(D7, OUTPUT);
  pinMode(D8, OUTPUT);
  pinMode(D9, OUTPUT);
  pinMode(D10, OUTPUT);

  xTaskCreatePinnedToCore (
    altPWM, // Функция
    "PWM", // имя
    4096, // размер стека функции (пока не нашел способа правильно вычислить)
    NULL, // параметры (хз что)
    (2 | portPRIVILEGE_BIT), // приоритет (пока хз как правильно)
    &task_pwm, // дескриптор (укажем NULL?)
    xPortGetCoreID() ); // ядро

}



float val = 500;

void loop() {

  val += 10;
  if (val > 4500) val = 500;
  uint16_t pwm = val > 2500 ? 5000 - val : val;
  for (uint8_t j = 0; j < 3; j++) cahnnels[j].value = pwm;
  for (uint8_t j = 4; j < 18; j++) cahnnels[j].value = 3000 - pwm;
  cahnnels[3].value = 1800;

  delay(20);

  Serial.println("ERR: " + String(ERR_COUNT));

}

готово, почти с пол пинка заработало (забыл minC сбрасывать - теперь в строке 88).

Из ограничений: пока хотя бы на одном канале значение ШИМ равно “0”, сортировака будет возвращать ошибку, потому что условие “if (cahnnels[j].value < minC && cahnnels[j].value > minP)” не выполнится для таких каналов. В целом - некритично. Даже при значении “0” на выходе был бы короткий импульс из-за особенностей реализации, а у серв обычно крайнее положение на 500 мкс импульсе

для теста в loop() запустил порты 0-3 в одну сторону, 5-17 в противоположную, а 4-й неподвижно. Таким образом сортировка постоянно меняется, но серва на 4 порту не колышется и ни звука не издает, а значит сигнал достаточно стабилен даже при смене порядка следования портов. То есть вроде бы ок.

Теперь попробую накрутить сверху связь по esp-now, если стабильно заработает, то тема будет наконец-то решена))

Размер стека - на глаз. 4-5кб нормально. Если вылетит "Stack Canary … " исключение - значит надо увеличить.

Параметры - это то, что получит твоя задача в качестве аргумента:

void task(void *arg /* <---- этот */) {
}

Так каждой отдельной задаче можно передавать её аргументы.

С приоритетами - если работает, ничо не меняй :)). Я обычно ставлю idle.

Пока не начал разбираться в ESP-NOW, но значительно усовершенствовал генерацию ШИМ. Теперь алгоритм не сортирует значения, а строит маски регистров, по которым будут подниматься и гаситься порты. Причем если на канале ШИМ длина импульса ноль, то бит не попадет в маску и импульса действительно не будет. Также, каналы с одинаковым значением ширины импульса теперь поднимаются/гасятся одновременно, между ними не будет вызываться функция ets_delay_us(0); (все такие вызовы, если вообще будут, то пойдут в последних итерациях цикла строки 67, уже после гашения всех портов).

Можно менять кол-во элементов в cahnnels[], код отработает как надо. Время выполнения практически не поменяется, но с добавлением количества каналов точность может немного падать из-за увеличения количества операций в цикле генерации импульсов (строка 67).

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"





// структура для хранения пары значений "номер вывода GPIO" - "ширина импульса PWM в микросекундах"
struct tChannel { uint8_t pin; uint16_t value; };

// массив каналов PWM
tChannel cahnnels[] = { { 18, 0 }, { 48, 0 }, {  1, 0 },
                        {  2, 0 }, {  3, 0 }, {  4, 0 },
                        { 12, 0 }, { 13, 0 }, { 14, 0 },
                        {  5, 0 }, {  6, 0 }, {  7, 0 },
                        {  9, 0 }, { 10, 0 }, { 17, 0 },
                        { 21, 0 }, { 38, 0 }, { 47, 0 } };





// задача для генерации импульсов PWM на все каналы каждые 20 миллисекунд
void task_pwm (void *param) {

  // ИНИЦИАЛИЗАЦИЯ

  const uint8_t count = sizeof(cahnnels) / sizeof(cahnnels[0]);

  // ограничение максимальной длины импульса
  const uint16_t pulseWidthTreshold = 3000;

  // маски регистров, по которым будут подниматься каналы
  uint32_t up_port_0_mask = 0, up_port_1_mask = 0;
  // массив последовательности временных задержек
  uint16_t delays[count];
  // массивы масок регистров, по которым будут гаситься каналы
  uint32_t down_port_0_masks[count];
  uint32_t down_port_1_masks[count];

  for (uint8_t i = 0; i < count; i++) {
    // конфигурация выводов
    gpio_reset_pin((gpio_num_t)cahnnels[i].pin);
    gpio_set_direction((gpio_num_t)cahnnels[i].pin, GPIO_MODE_OUTPUT);
    gpio_set_pull_mode((gpio_num_t)cahnnels[i].pin, GPIO_FLOATING);
    // установка нулевых стартовых значений для масок и задержек
    delays[i] = 0;
    down_port_0_masks[i] = 0;
    down_port_1_masks[i] = 0;
  }

  // запуск бесконечного цикла задачи
  for (;;) {

    // блокировка шедулера - гарантия, что формирование импульсов выполнится без остановки на другие задачи
    vTaskSuspendAll();

    // время начала генерации импульсов (для определения момента следующего старта)
    TickType_t last_PWM_at = xTaskGetTickCount();

    // ГЕНЕРАЦИЯ ИМПУЛЬСОВ

    // переача в регистры масок, содержащих биты всех портов с не нулевой длиной импульса
    GPIO.out_w1ts      |= up_port_0_mask;
    GPIO.out1_w1ts.val |= up_port_1_mask;

    // гашение портов согласно очередности с соответствующими задержками
    for (uint8_t i = 0; i < count; i++) {
      // задержка
      ets_delay_us(delays[i]);
      // переача в регистры масок, содержащих биты всех портов, которые необходимо погасить на данной итерации
      GPIO.out_w1tc      |= down_port_0_masks[i];
      GPIO.out1_w1tc.val |= down_port_1_masks[i];
    }

    // ВЫЗОВ ПЕРЕРАССЧЕТА КИНЕМАТИКИ

    // с использованием дельты времени 20 мс в качестве инкрементов всех процессов
    // ...

    // ПОДГОТОВКА ДАННЫХ ДЛЯ СЛЕДУЮЩЕЙ ИТЕРАЦИИ

    // маски регистров, по которым будут подниматься порты (временные переменные)
    uint32_t tmp_up_port_0_mask = 0, tmp_up_port_1_mask = 0;
    // массив последовательности временных задержек (временные переменные)
    uint16_t tmp_delays[count];
    // массивы масок регистров, по которым будут гаситься порты (временные переменные)
    uint32_t tmp_down_port_0_masks[count];
    uint32_t tmp_down_port_1_masks[count];
    // установка нулевых значений задержек и масок
    for (uint8_t i = 0; i < count; i++) {
      tmp_delays[i] = 0;
      tmp_down_port_0_masks[i] = 0;
      tmp_down_port_1_masks[i] = 0;
    }
    // минимальная длина импульса, определенная на предыдущей итерации цикла наполнения массивов
    uint16_t minPreviouse = 0;
    // минимальная длина импульса, определенная на текущей итерации цикла наполнения массивов
    uint16_t minCurrent = cahnnels[0].value;
    // счетчик обработанных каналов
    uint8_t current = 0;
    // индекс текущего элемента во временных массивах
    uint8_t to = 0;
    // признак того, что наполнение массивов прошло успешно
    bool succesfull = true;
    
    // цикл наполнения массивов
    while ((current < count) && succesfull) {
      
      // поиск минимального значения длины импульса, превосходящего предыдущее значение (найденное в предыдущей итерации)
      for (uint8_t i = 0; i < count; i++) if (cahnnels[i].value < minCurrent && cahnnels[i].value > minPreviouse)
        minCurrent = cahnnels[i].value;
      
      // запись значения дельты времени в массив задержек
      tmp_delays[to] = minCurrent - minPreviouse;
      // перезапись предыдущего минимального значения
      minPreviouse = minCurrent;

      // сброс признака успешного наполнения массивов
      succesfull = false;
      // формирование масок для всех каналов ШИМ, у которых длина импульса равна найденной
      for (uint8_t from = 0; from < count; from++) if (cahnnels[from].value == minCurrent) {

        // если cahnnels[] не изменялся извне и наполнение проходит успешно, то данная команда выполнится хотя бы раз на каждой итерации цикл наполнения
        succesfull = true;

        // если длина импульса на канале ненулевая, то в порты добавляется бит соответствующего пина
        if (minCurrent > 0) {

          // определение группы регистров
          if (cahnnels[from].pin > 32) {

            // смещение в регистрах группы 1
            uint32_t add_1 = 1 << (cahnnels[from].pin - 32);
            // добавление в маску регистра поднимающихся портов
            tmp_up_port_1_mask |= add_1;
            // добавление в маску регистра гасящихся портов
            tmp_down_port_1_masks[to] |= add_1;

          } else {

            // смещение в регистрах группы 0
            uint32_t add_0 = 1 << cahnnels[from].pin;
            // добавление в маску регистра поднимающихся портов
            tmp_up_port_0_mask |= add_0;
            // добавление в маску регистра гасящихся портов
            tmp_down_port_0_masks[to] |= add_0;

          }

        }

        // инкриментация счетчика обработанных каналов
        current++;

      }

      // инкриментация индекса текущего элемента в наполняемых массивах
      to++;

      // сброс минимума для успешного поиска на следующей итерации
      minCurrent = 65535;
    
    }
  
    // если наполнения массивов прошло успешно, а самый длинный импульс не превышает установленный порог
    if (succesfull && (minPreviouse < pulseWidthTreshold)) {
      
      // копирование масок поднятия портов
      up_port_0_mask = tmp_up_port_0_mask;
      up_port_1_mask = tmp_up_port_1_mask;

      // копирование значениq из временных массивов в массивы для генерации импульсов
      for (uint8_t i = 0; i < count; i++) {

        delays[i] = tmp_delays[i];
        down_port_0_masks[i] = tmp_down_port_0_masks[i];
        down_port_1_masks[i] = tmp_down_port_1_masks[i];

      }
      
    } // else при желании можно зафиксировать ошибку
    
    // разблокировка шедулера
    xTaskResumeAll();

    // отдаем управление шедулеру
    vTaskDelayUntil(&last_PWM_at, pdMS_TO_TICKS(20));
    
  }
  
  // аварийная остановка задачи (не должна сработать никогда)
  vTaskDelete(NULL);
  
}





void setup () {

  Serial.begin (9600);

  // добавление задачи в шедулер (функция, имя, размер_стека, параметры_NULL, приоритет, хэндлер_NULL, текущее_ядро)
  xTaskCreatePinnedToCore (task_pwm, "PWM", 4096, NULL, (2 | portPRIVILEGE_BIT), NULL, xPortGetCoreID()); //TaskHandle_t taskHandler_pwm;

}





uint16_t val[18] = { 1500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500 };

void loop() {

  for (uint8_t i = 0; i < 18; i++) {
    val[i] += i;
    if (val[i] > 4500) val[i] = 500;
    cahnnels[i].value = val[i] > 2500 ? 5000 - val[i] : val[i];

    Serial.print(" " + String(cahnnels[i].value));
  }
  Serial.println();

  delay(20);

}

Выглядит правильным, совсем идеально было бы выдавать это таймерных прерываниях, равных шагу ШИМа. Делеи тут чутка нестрогие будут.

согласен, тут каждая новая итерация цикла, где порты гасятся, будет нести прибавку к погрешности. Я пока не знаю, как в таймеры это упихать правильно. Хотя… А в функции прерывания таймера можно же перезапустить этот же таймер с новыми параметрами аларма и переназначением функции ISR?

Точно, можно без переназначения даже. В задаче поднимаем порты и сразу сбрасываем счетчик таймера в 0, а также сохраняем в глобальных переменных первую бит-маску для обоих регистров из массивов, в которых маски на гашение портов хранятся. Вместо дельт времени в массиве задержек храним просто длины импульса. При сработке прерывания обновляем регистры из глобальных переменных, переустанавливаем аларм на новое значение задержки, обновляем глобальные переменные.

…лень вникать в нюансы, но по сути да, мы тупо проверяем: надо выключить каждый канал? Это в общем быстрая операция. Надо - выключили. Ну, практически как у вас в коде.
А сам таймер дрючить не надо, чем тупее софт тем надежнее ))

не, это уже в №125 сделал, не очень было

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

Servo.h переизобретаете?

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

да. Потому что на esp32s3 она больше 16 серво не дает подключить

может кто-нибудь подсказать, как правильнее всего инициализировать и использовать таймер?

Запускать надо из кода задачи, сбрасывая значение счетчика и обновляя значение аларма.

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

Попробовал так:


static void IRAM_ATTR timer_pwm_isr (void *arg) {

  // переача в регистры масок, содержащих биты всех портов, которые необходимо погасить на данной итерации
  GPIO.out_w1tc      |= down_port_0_mask;
  GPIO.out1_w1tc.val |= down_port_1_mask;

  // очистка флагов прерываний
	timer_group_clr_intr_status_in_isr (TIMER_GROUP_0, TIMER_0);
  // перезапуск прерывания Alarm
	timer_group_enable_alarm_in_isr (TIMER_GROUP_0, TIMER_0);

  current_isr_index++;

  if (current_isr_index < count && delays[current_isr_index] > 0) {

    down_port_0_mask = down_port_0_masks[current_isr_index];
    down_port_1_mask = down_port_1_masks[current_isr_index];
    timer_set_alarm_value (TIMER_GROUP_0, TIMER_0, delays[current_isr_index]);

  } else timer_pause (TIMER_GROUP_0, TIMER_0);

} 



  // ИНИЦИАЛИЗАЦИЯ

  // настройка таймера
  // таймер опирается на 80mHz осцилятор!
  timer_config_t config = {
    .alarm_en = TIMER_ALARM_EN, // включение аларма таймера enum: 0 TIMER_ALARM_DIS | 1 TIMER_ALARM_EN
    .counter_en = TIMER_PAUSE, // cостояние после инициализации enum: 0 TIMER_PAUSE | 1 TIMER_START
    .intr_type = TIMER_INTR_LEVEL, // прерывание по уровню enum: 0 TIMER_INTR_LEVEL
    .counter_dir = TIMER_COUNT_UP, // направление счетчика enum: 0 TIMER_COUNT_DOWN | 1 TIMER_COUNT_UP
    .auto_reload = TIMER_AUTORELOAD_EN, // настройка автоматического рестарта прерывания enum: 0 TIMER_AUTORELOAD_DIS | 1 TIMER_AUTORELOAD_EN
    .divider = 2 // предделитель uint16_t: 2..65535
	};
  // инициализация таймера
  timer_init(TIMER_GROUP_0, TIMER_0, &config);
  // разрешение прерывания
  timer_enable_intr (TIMER_GROUP_0, TIMER_0);
  // привязка функции обработки перрывания
  timer_isr_register (TIMER_GROUP_0, TIMER_0, timer_pwm_isr, NULL, 0, NULL);



// ГЕНЕРАЦИЯ ИМПУЛЬСОВ

    // сброс счетчика таймера
    timer_set_counter_value (TIMER_GROUP_0, TIMER_0, 0);
    // значение, при достижении которого сработает прерывание
    timer_set_alarm_value (TIMER_GROUP_0, TIMER_0, delays[0]);
    // копирование первой маски из массива в переменную для прерывания
    down_port_0_mask = down_port_0_masks[0];
    down_port_1_mask = down_port_1_masks[0];
    // сброс индекса для прерываний
    current_isr_index = 0;
    // поднятие портов: переача в регистры масок, содержащих биты всех портов с не нулевой длиной импульса
    GPIO.out_w1ts      |= up_port_0_mask;
    GPIO.out1_w1ts.val |= up_port_1_mask;
    // запуск таймера гашения портов
    timer_start (TIMER_GROUP_0, TIMER_0);


видимо, что-то упустил, потому что МК вешается