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

Это ж старый драйвер. Его скоро удалять будут. Новый называется GPTimer

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

Лучше потрачу время на реализацию беспроводной связи с пультом

Раз такой грандиозный замах на гексапода, может дистанционная заливка скетчей на платформу сперва?

нееее, это третично. У esp-now есть ограничения, в т.ч. устройства должны на 1 канале вафли работать. Надо будет разбираться, как им друг друга искать, если 1 из них подключена к роутеру, например. В общем куча нюансов.

Для начала мне надо убедиться, что весь необходимый мне базовый функционал работает на этих контроллерах, на платах esp32-s3-nano. Но пока что всё выглядит довольно миленько

1 лайк

Я бы попробовал тупо запустить таймерные прерывания на мин время дискретности ШИМа и в каждом прерывании тупо сравнивал бы с нужной величиной времени для каждого выхода. Подсчитывая №прерываний.
Если № > Период Сброс счетчика. //то есть прошел весь период ШИМа

Если скорости хватит то должно работать идеально по временам.

Прикинул:
Период 20мС, число ШИМ 3000, период прерываний 6.7мкС.
ХЗ вроде должно успевать.

Тут правильнее сделать, чтобы данные не менялись, пока идет период. Например, копировать подготовленные данные в конце периода в отдельные переменные именно для прерываний.

в целом понял, хочется только в ISR сначала присваивать значения регистрам, а уже потом делать какие-либо сравнения и вычисления - так будет максимальная точность импульсов. Поэтому я хотел:

  1. при старте импульсов сначала сконфигурировать таймер на прерывание в момент истечения первого импульса и присвоить глобальным переменным с масками регистров значения, которые применятся при сработке прерываний
  2. поднять все порты, на которых будут импульсы
  3. запустить таймер

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

Но повторюсь, пока что в долгий ящик. А то уже неделю почти этот ШИМ мучаю, а на глаз погрешность и сейчас не особо заметна.

Теперь возникло много вопросов про wifi и esp-now.

  1. Есть esp_wifi_get_mac(WIFI_IF_STA, baseMac) и есть WiFi.macAddress(). Правильно ли я понял, что для каждого из 4 режимов работы (WIFI_IF_STA WIFI_IF_AP WIFI_IF_NAN WIFI_IF_MAX или какие там) предусмотрен свой мак адрес? И WiFi.macAddress() вернет мак режима, в котором сейчас запущен чип, а esp_wifi_get_mac(WIFI_IF_STA, baseMac) вернет мак адрес для указанного первым параметром режима, причем неважно, запущен ли чип в этом режиме, или нет?
  2. Если отправить esp_now_send с маком FF:FF:FF:FF:FF:FF, то все esp, у которых включен esp-now получат этот запрос. Таким образом можно рассылать авто коннект. А должен ли я добавлять мак FF:FF:FF:FF:FF:FF в список пиров? Ощущение, что да, т.к. список пиров хранит не только маки, но и иные параметры, например номер канала и ключи шифрования. И перед тем, как что-то отправлять, я должен создать пира, а в esp_now_send первым параметром указывается именно номер пира - если не будет в списке, то отправка выдаст ошибку.
  3. При перезаливке скетча список пиров очищается? Можно ли быстро ушатать память чипа, если при каждом старте будет очищаться список пиров и снова добавляться пиры?
  4. С номерами каналов путаница. Во многих статьях написано, что платы должны работать на одинаковом канале, иначе по esp-now они друг друга не увидят. Но чет я видел определение канала в структуре esp_now_peer_info_t, но нигде пока что не видел установки канала wi-fi на принимающей стороне. Как правильно-то?
  5. есть ли функция для определения, доступен ли в данный момент пир? Или надо ориентироваться на call-back при попытке отправить в адрес пира сообщение-пустышку?
  6. Описание функции esp_err_t esp_now_fetch_peer(bool from_head, esp_now_peer_info_t *peer) гласит, что “Only return the peer which address is unicast, for the multicast/broadcast address, the function will ignore and try to find the next in the peer list.” Не понимаю, функция фетчит только пиров с юикаст адресом. Я честно не знаю, чем юникаст адреса от остальных отличаются, но если случайно в список пиров будет зарегистрирован не юникаст адрес - я что, никогда его там найти и удалить не смогу???

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

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

Кстати, я ведь могу сделать это в бесконечном цикле задачи: вместо for где задержки сделать while(1) и уже в нем вышеперечисленное делать, а если дошли до последнего индекса массивов, то просто вышли из цикла. Попробую может позже

На часть вопросов сам себе отвечу:

Очищается при каждом рестарте платы, даже если не перезаливали скетч. Т.е. получается что в постоянную память не пишется

по всей видимости нет

Фетчит только юникастов, да. Так как пункт 3, то ничего трагичного.

Вопрос про каналы вай-фай остается открытым…

Набросал тут саму идею, как в прерывании выдавать ШИМ по каналам, в лоб. Может не будет успевать, хз.

uint16_t Cnt;     //счетчик прерываний
uint16_t Val[18];  //величины каналов для ШИМ
uint8_t pin[] = {1,2,3,... }; // номера пинов подключим или типа того

void ISR() // прерывания быстрые
{
  if(++Cnt>3000) //разрядность ШИМ  
    {
      Cnt=0; //начало периода
      for(int i=0;i<18;i++) 
       {
        if(!Val[i]) pin[i]=0; //начало периода ШИМ, сброс нулевых значений
        else pin[i]=1;        //включаем выхода, которые не нулевые
       }
      return;
    }
  
  for(int i=0;i<18;i++) // по всем выходам
  {
    if(Val[i]<Cnt) pin[i]=0; //ШИМ закончился, сбрасываем пин в ноль
  }

}

Если в массиве pin номера пинов

не совсем ясно зачем вы эти номера чему-то присваете в прерывании.

А если речь о том, чтобы выставить на этих пинах нужный уровень - то именно этот вопрос автор и обсуждал последние несколько десятков сообщений

Да, с пинами я там мутно написал, просто показать идею, сорри.
Надеюсь, смысл понятен. Это не рабочий кусок, конечно, просто чтобы пояснить алгоритм.

Именно, выставить уровень, те установить 0 или 1.
Способов это сделать много… можно гнать единицу влево, например.

Я переделал на счетчик, но без прерывания, и оставив методику сортировки каналов. Получилось как будто бы лучше - вместо ets_delay_us(delays[i]); теперь при формировании импульсов сравнивается значение счетчика с таковым для текущего элемента массива из очереди. Если превзошло, то сразу гасятся порты, а только потом инкрементятся индексы и переприсваиваются значения задержки и регистров. Если интересно см. строки 101…126.

Импульс может быть 0…3 мс, при этом значение канала 0…120000. (для серв актуально 0,5…2,5 мс / 20000…100000), Это не есть разрешающая способность, т.к. все равно будет погрешность, особенно для импульсов, которые заканчиваются почти одновременно: после гашения портов есть операции, которые могут подвинуть по времени следующую итерацию. Но все равно точность довольно высока.

Лучше мне уже не сделать… Разве что можно каналы с минимальным различием длин импульсов объединять, и также сделать длину очереди динамической, но это уже попахивает снобизмом…

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





// структура для хранения пары значений "номер вывода GPIO" - "ширина импульса PWM в микросекундах"
struct tChannel { uint8_t pin; uint64_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) {

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

  // настройка таймера
  // таймер опирается на 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_set_counter_value (TIMER_GROUP_0, TIMER_0, 0);
  // запуск таймера
  timer_start (TIMER_GROUP_0, TIMER_0);



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

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

  // маски регистров, по которым будут подниматься каналы
  uint32_t up_port_0_mask = 0, up_port_1_mask = 0;
  // массив длин импульсов в масштабе тиков таймера
  uint64_t pulsesCounts[cahnnelsCount];
  // массивы масок регистров, по которым будут гаситься каналы
  uint32_t down_port_0_masks[cahnnelsCount];
  uint32_t down_port_1_masks[cahnnelsCount];

  for (uint8_t i = 0; i < cahnnelsCount; 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);
    // установка нулевых стартовых значений для масок и длин импульсов
    pulsesCounts[i] = 0;
    down_port_0_masks[i] = 0;
    down_port_1_masks[i] = 0;
  }

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

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

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

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

    // маски портов для гасящихся каналов на следующей итерации
    uint32_t down_port_0_mask = down_port_0_masks[0],
             down_port_1_mask = down_port_1_masks[0];
    // длина импульса в масштабе счетчика таймера
    uint64_t pulseCount = pulsesCounts[0];
    // переменная для текущего значения счетчика таймера
    uint64_t value;
    // индекс
    uint8_t n = 0;

    // сброс счетчика таймера
    timer_set_counter_value (TIMER_GROUP_0, TIMER_0, 0);

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

    // гашение портов согласно очередности
    while (1) {

      // получение текущего значения счетчика таймера
      timer_get_counter_value (TIMER_GROUP_0, TIMER_0, &value);
      // если значение с таймера превзошло длину импульса, окончание которого ожидается в данный момент
      if (value > pulseCount) {

        // переача в регистры масок, содержащих биты всех портов, которые необходимо погасить на данной итерации
        GPIO.out_w1tc      |= down_port_0_mask;
        GPIO.out1_w1tc.val |= down_port_1_mask;
        // инкремент индекса
        n++;
        // если не все элементы массивов обработаны
        if (n < cahnnelsCount) {

          // копирование масок во временные переменные
          down_port_0_mask = down_port_0_masks[n];
          down_port_1_mask = down_port_1_masks[n];
          // копирование длины импульса во временную переменную
          pulseCount = pulsesCounts[n];

        } else break; // если обработано всё - выходим из цикла

      }

    }

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

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

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

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

      // сброс признака успешного наполнения массивов
      succesfull = false;
      // формирование масок для всех каналов ШИМ, у которых длина импульса равна найденной
      for (uint8_t from = 0; from < cahnnelsCount; 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 = pulseWidthTreshold;
    
    }
  
    // если наполнения массивов прошло успешно, а самый длинный импульс не превышает установленный порог
    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 < cahnnelsCount; i++) {

        pulsesCounts[i] = tmp_pulsesCounts[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;



}



uint64_t val[18] = { 60000, 20000, 20000, 20000, 20000, 20000, 20000, 20000, 20000, 20000, 20000, 20000, 20000, 20000, 20000, 20000, 20000, 20000 };

void loop() {
 

  for (uint8_t i = 0; i < 18; i++) {
    val[i] += i * 50;
    if (val[i] > 180000) val[i] = 20000;
    cahnnels[i].value = val[i] > 100000 ? 200000 - val[i] : val[i];

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


  delay(20);

}

Идея нормальная, только для меня так и остается непонятным. чем вам внешний драйвер ШИМ не угодил…
Микроконтроллер общего назначения типа ЕСП32 - это компромисс, он умеет очень многое, но как плата за это - все делает “не очень”. Лучше использовать специализированное железо.

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

максимально дешево = недорогие сервоприводы в первую очередь, а значит микро размер, что очень сильно определяет размер всего робота:

Просто взгляните: если увеличить размер конечностей, то сервам будет тяжко, а если увеличить размер “туловища” то робот будет выглядеть несуразно.

И вот сейчас туда надо упихать две 18650, стаб на 10А, контроллер и излишки проводов серв (всё что на фото собсно). Мне очень в этой связи нравится esp32-s3-nano.

Конечно, было бы супер взять всю нужную рассыпуху и напаять ее на индивидуально изготовленную PCB, но это дороже и сложнее.

А еще к такой же esp32-s3-nano без проблем подцепился oled экран, к ее же стабу, и им одной 18650 хватает, что тоже очень порадовало. Это в пульт управления.

Я сначала все это делал на mega 2560 pro (мелкая которая) и HC-12 для радиосвязи. Так оно крупнее, требует больше пайки, в разы менее производительно, да еще и зараза дороже примерно вдвое! Решил твердо - надо отказываться, по сему приходится изучать новое и перекладывать уже реализованные идеи на новый контроллер

не уверен, что вы отвечали на мой вопрос… причем тут Мега и преимущества ЕСП32, Я разве предлагаю от них отказаться?
ну да ладно…

прямой ответ: мне некуда запихивать еще какие-либо аппаратные драйвера, плюс они усложняют проект