Выбор контроллера (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

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

1 лайк

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

Помимо этого, логика плавных движений для повторения траекторий, как раз заключается в том, что каждый довольно маленький промежуток времени просчитывается положение конечностей робота в пространстве, т.е. углов сервоприводов, при заданном приращении времени (а-ля дифференцирование по времени). Я решил приращение сделать статичным, равным периоду импульсов шим - 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, если стабильно заработает, то тема будет наконец-то решена))

1 лайк

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

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

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

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

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

1 лайк

Пока не начал разбираться в 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);

}

1 лайк

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

1 лайк

согласен, тут каждая новая итерация цикла, где порты гасятся, будет нести прибавку к погрешности. Я пока не знаю, как в таймеры это упихать правильно. Хотя… А в функции прерывания таймера можно же перезапустить этот же таймер с новыми параметрами аларма и переназначением функции 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);


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