Это ж старый драйвер. Его скоро удалять будут. Новый называется GPTimer
Спасибо, учту. Но уже, пожалуй, на будущее. На таймере заработало, но как-то совсем нестабильно. То ли в ISR слишком долго переприсваиваются значения из массивов, то ли между прерываниями в задаче успевают переприсваиваться новые значения в глобальные переменные… в общем иногда сервы дергаются, а для перепрошивки плату приходится через кнопку в режим ожидания прошивки загонять, и разбираться в этом мне лень.
Лучше потрачу время на реализацию беспроводной связи с пультом
Раз такой грандиозный замах на гексапода, может дистанционная заливка скетчей на платформу сперва?
нееее, это третично. У esp-now есть ограничения, в т.ч. устройства должны на 1 канале вафли работать. Надо будет разбираться, как им друг друга искать, если 1 из них подключена к роутеру, например. В общем куча нюансов.
Для начала мне надо убедиться, что весь необходимый мне базовый функционал работает на этих контроллерах, на платах esp32-s3-nano. Но пока что всё выглядит довольно миленько
Я бы попробовал тупо запустить таймерные прерывания на мин время дискретности ШИМа и в каждом прерывании тупо сравнивал бы с нужной величиной времени для каждого выхода. Подсчитывая №прерываний.
Если № > Период Сброс счетчика. //то есть прошел весь период ШИМа
Если скорости хватит то должно работать идеально по временам.
Прикинул:
Период 20мС, число ШИМ 3000, период прерываний 6.7мкС.
ХЗ вроде должно успевать.
Тут правильнее сделать, чтобы данные не менялись, пока идет период. Например, копировать подготовленные данные в конце периода в отдельные переменные именно для прерываний.
в целом понял, хочется только в ISR сначала присваивать значения регистрам, а уже потом делать какие-либо сравнения и вычисления - так будет максимальная точность импульсов. Поэтому я хотел:
- при старте импульсов сначала сконфигурировать таймер на прерывание в момент истечения первого импульса и присвоить глобальным переменным с масками регистров значения, которые применятся при сработке прерываний
- поднять все порты, на которых будут импульсы
- запустить таймер
При сработке прерывания сначала пишем в регистры из глобальной переменной - порты, у которых импульс закончился упадут. Затем присваиваем в глобальные переменные новые маски регистров (порты, которые погаснут следующими) и переписываем значение аларм таймера, чтобы он сработал снова в нужный момент.
Но повторюсь, пока что в долгий ящик. А то уже неделю почти этот ШИМ мучаю, а на глаз погрешность и сейчас не особо заметна.
Теперь возникло много вопросов про wifi и esp-now.
- Есть 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) вернет мак адрес для указанного первым параметром режима, причем неважно, запущен ли чип в этом режиме, или нет?
- Если отправить esp_now_send с маком FF:FF:FF:FF:FF:FF, то все esp, у которых включен esp-now получат этот запрос. Таким образом можно рассылать авто коннект. А должен ли я добавлять мак FF:FF:FF:FF:FF:FF в список пиров? Ощущение, что да, т.к. список пиров хранит не только маки, но и иные параметры, например номер канала и ключи шифрования. И перед тем, как что-то отправлять, я должен создать пира, а в esp_now_send первым параметром указывается именно номер пира - если не будет в списке, то отправка выдаст ошибку.
- При перезаливке скетча список пиров очищается? Можно ли быстро ушатать память чипа, если при каждом старте будет очищаться список пиров и снова добавляться пиры?
- С номерами каналов путаница. Во многих статьях написано, что платы должны работать на одинаковом канале, иначе по esp-now они друг друга не увидят. Но чет я видел определение канала в структуре esp_now_peer_info_t, но нигде пока что не видел установки канала wi-fi на принимающей стороне. Как правильно-то?
- есть ли функция для определения, доступен ли в данный момент пир? Или надо ориентироваться на call-back при попытке отправить в адрес пира сообщение-пустышку?
- Описание функции 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, Я разве предлагаю от них отказаться?
ну да ладно…

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