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

Да, время задержки может гулять

1 лайк

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

  1. Ядро0 прочитало переменную, в ней число 5
  2. Ядро1 прочитало переменную, в ней все еще число 5
  3. Ядро0 увеличило переменную, и записала новый результат в память.
  4. Ядро1 проделало то-же самое (5+1=6), и записала в память число 6.

Таким образом потеряли одно прибавление.

1 лайк

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

Если пустой, то ничего. А если while(1){}, то может перезагрузится. Надо посмотреть, включен ли на этой конкретной задаче watchdog или выключен. По-моему, по умлочанию - выключен.

loop() вызывается вот так вот:

void loopTask(void *pvParameters) {
....
  setup();
...
  for (;;) {
#if CONFIG_FREERTOS_UNICORE
    yieldIfNecessary();
#endif
    if (loopTaskWDTEnabled) {
      esp_task_wdt_reset();      // <------- вот туточки успокаивают watchdog
    }
    loop();
....
}

Все это здорово, расстраивает только одно - нужно изучить кучу правил РТОС для того чтобы писать многозадачный код, который я отлично могу писать и без нее…
Реально же РТОС не делает ничего нового по сравнению со старым добрым “блинк_без_делей” на миллис.

Ну да.
И ценность этих знаний тоже сомнительна.

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

Вот мы сначала ехали на QNX, потом пересели на RTEMS (она, кстати, куда помощнее чем FReeRTOS).

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

это в недрах пакета, верно? если используется опция CONFIG_FREERTOS_UNICORE то он сначала отвлекается на системные задачи, потом сбрасывает пса, и только потом стартует loop.

Это то, что называется Arduino Core : клей между ESP-IDF и Ардуиной. Там, собственно, всё: digitalRead(), какой-нибудь Serial.begin() и так далее.
Это, кстати, очень хороший источник информации.

Arduino Core поставляется в исходниках, покопайся в кишках Arduino IDE, где-то оно там. Поищи файл HardwareSerial.cpp - он как раз там и лежит.

Да, loop() вызывается бесконечно, CONFIG_FREERTOS_UNICORE == 0.

Сильно прям уж в детали шедулера лезть не нужно. Только если уж из академического интереса.
“Решай проблемы по мере их поступления” (с) Жванецкий. :smiley:

1 лайк

Доброго! Мне нужно до конца закрыть для себя этот вопрос, возможно мне вообще не стоит трогать второе ядро.

CONFIG_FREERTOS_UNICORE это директива “работать на одном ядре”, т.е. yieldIfNecessary(); вызывается после каждого loop() только если плата работает в одноядерном режиме, что наводит меня на следующую мысль.

Ядро0 APP_CORE - я могу делать на нем всё что захочу, если не буду при этом вызывать блокирующие второе ядро вещи, то контролер никогда не перезагрузится по вочдог. Верно?

Ядро1 PRO_CORE - протокольные вещи. Насколько много оно на себя берет? Библиотеки для взаимодействия по беспроводным протоколам зависят от APP_CORE, или обрабатываются на PRO_CORE и лишь отдают результаты работы в APP_CORE, где я с ними работаю?

Я планирую использовать ESP-NOW для двусторонней связи двух плат. APP_CORE вполне может работать в одиночку с подготовкой, обработкой передаваемых/принимаемых данных, расчетами тригонометрии и генерации ШИМ. Но я не пойму, может ли это привести к зависанию? Например, если я в APP_CORE забиндил функцию esp_now_register_send_cb(OnDataSent); потом вызвал esp_now_send(...); если у меня в процессе выполнения одной из функций в APP_CORE сработало прерывание по таймеру и залочило APP_CORE на 3мс, у меня всё поломается?

То есть если та же esp_now_send на самом деле работает на 1м ядре, блокируя передаваемые данные по указателю (а блокируя ли? или останавливает место вызова до полного копирования данных по указателю), то вызов прерывания на 0м ядре как будто вообще никак не помешает ему, лишь бы в прерывании те самые данные не модифицировались, иначе логика проги может испортиться. А как на самом деле? Там есть общий подход, или нужно ковырять каждую библиотеку в поисках ответа?

Обычно библиотеки используют ядро какое попало. Не указывают. Деление на PRO и APP - это изначальная задумка Espressif, которая пошла не по плану.

Фактически, только Bluetooth и WiFi используют это деление.

Мой совет:

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

Если у тебя сработало прерывание и ты в нем много времени провел (сколько, я не знаю, 3мс может быть и не много) то сработает Interrupt Watchdog.

Как правило, если твое прерывание нельзя обработать быстро, то принято, вместо обработки ВНУТРИ прерывания, отдать это на откуп какой-то задаче.

Делается это так:

Обработчик прерывания, вместо фактическиъ действий делает одно быстрое: отправляет СООБЩЕНИЕ о произошедшем задаче. Которая потихонбку все обработает. Так работает WiFi, Bluetooth, UART, USB…

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

Есть куча примеров на эту тему: например, прерывание посылает сообщение в очередь сообщений задачи. А задача выгребает сообщения из очереди и обрабатывает их.

Например, так сделано в Serial (см. HWCDC.cpp в ArduinoCore).

Моя рекомендация:

  1. Не сиди долго в прерывании. Если обработка долгаяч и слождная - используй xTaskNotify или QueueSend для того, чтобы послать сообщение задаче, которая будет обрабатывать.

  2. Не думай пока о watchdog. Когда такое случится, ты увидишь сообщение об этом “Guru Meditation”

  3. Блокировки ты делаешь сам, когда считаешь нужным. А именно - когда пишешь читаешь переменные, к которым одновременно имеют доступ разные задачи. Для блокировок используются либо семафоры, либо критические секции. Разница между критическими секциями и семафорами большая, но об этом позже (сидеть внутри секции долго так же нельзя, потому что прерывания будут заблокированы, в отличии от семафора)

  4. Прерывания для ядер генерируются паралельно. Прерывание на ядре0 никак не влияет на ядро1.

PS: ты начни что-нибудь писать, чтобы мы предметно говорили. Когда начнет что-то не так работать, тогда будем дальше углубляться. Иначе эта информация вся не усвоится.

1 лайк

В ESPшках, которые Xtensa (ESP32, ESP32S2, ESP32S3), кстати, есть еще один крохотный процессор. Называется ULP (Ultra Low Power). На нем можно запускать крохотные программки. Он в основном используется, когда процессор в глубоком сне (5мка, вроде, потребление) :).

В ESPшках, которые RISC (ESP32C2,3,5,6,61 ) - что-то похожее есть, но я не углублялся.

Вангую массированное появление самоходного программного обеспечения, которое будет кочевать от одного умного дома к другому :))))

Немного о блокировках, на простом примере.

Ситуация такая: у нас есть две задачи, Задача1, и Задача2. Первая, в бесконечном цикле читает
данные из, ну, допустим, последовательного порта (UART0) в память, а вторая, в бесконечном цикле,
записывает считанное в другой порт (UART1).

У нас тут есть глобальная переменная (список, например), куда одна задача пишет, а другая - читает.
Как минимум нам нужен объект синхронихации здесь: семафор, который каждая задача будет пытаться
захватить перед обращением к нашей глобальной переменной. Кто успел - тот и съел, если Задача0
захватила семафор, то вторая задача, при попытке захватить тот же самый семафор - заблокируется.

Посмотрим на задачи наши, пусть они будут такими (тут пока нет никаких семафоров, забудем про них пока):

void task0( void *arg) {
  while(1) {

    Read_From_Uart0();

  }
}


void task0( void *arg) {
  while(1) {

    Write_To_Uart1();

  }
}

Если бы мы запускали это на какрм-нибудь Linux, то все хорошо: если данных на чтение нет,
то вызов Read_From_UART0() заблокируется. Функция не вернется до тех пор, пока данные не придут,
Задача0 будет поставлена в список ожидания и шедулер будет заниматься другими задачами.

А вот в ESP32 чтение их UART - неблокирующая операция - она сразу возвращается.

В таком случае у нас задача0 начнет съедать все ресурсы ядра. Если в операционке, в шедулере, включен
режим time slicing (это когда шедулер прерывает выполнение задач, нарезая каждой сколько-то миллисекунд)
то это худно-бедно работать будет. А если не включен, то остальные задачи на этом ядре никогда не получат
управление :frowning:

Исправить ситуацию можно так:

void task0( void *arg) {
  while(1) {

    Read_From_Uart0();
    taskYIELD(); // можно и delay(1)
  }
}

taskYIELD() - это макро из FreeRTOS, принудительная отдача управления другим задачам.

это понятно… только не уверен что применимо…

Мне нужно, судя по всему программно, стабильно генерировать восемнадцать ШИМ длиной импульса 0.5…2.5 мс с шагом не более 1 мкс и периодом 20мс. Не вешая при этом контроллер. Пока ничего лучше не придумал, как запустить прерывание каждые 20мс и какой-нибудь быстрый_таймер, делящий 20мс нацело, с достаточным разрешением в диапазоне 0.5…2.5 мс. Причем этот быстрый_таймер должен переполняться дольше 2.5мс, чтобы цикл в прерывании не “проморгал” условие выхода:

while (быстрый_таймер < значение_соответствующее_2.5_мс) {
  portB  &= ~((быстрый_таймер > значение_шим_1) << 1);
  portB  &= ~((быстрый_таймер > значение_шим_2) << 2);
  portB  &= ~((быстрый_таймер > значение_шим_3) << 3);
  portB  &= ~((быстрый_таймер > значение_шим_4) << 4);
  portB  &= ~((быстрый_таймер > значение_шим_5) << 5);
  // И так далее для 18 значений шим, свои порты и выводы мк.
  // Можно в пределах 1 порта объединить все каналы в одно присвоение, чтобы было побыстрее.
  // В любом случае я надеюсь, что на 240mhz не будет большой погрешности
  // от времени выполнения между разными строками внутри этого while
}

то есть такое прерывание должно залочить проц на 2.5мс, и ему лучше бы не мешать, иначе ШИМ “поломается”. Т.е. мне нельзя использовать delay и delaymicroseconds чтобы отдать управление на первые 0.5мс ШИМ, в которые он точно не поменяется на всех выводах, и вообще все 2.5мс лучше ничего другого на этом ядре не делать, а то длины импульсов могут внезапно сильно увеличиться.

Очень жду платы, но пока не вышли с таможни… Под рукой только esp32c3 одноядерки на 120мгц - не пойдут. Но мне в любом случае надо понять правильную концепцию, иначе потом переписывать нужно будет треть проекта примерно

кстати я бы с радостью в этом же прерывании сделал расчет тригонометрии, сразу после отправки импульсов (когда отработает while). Как раз логично обновлять координаты в такт отправки импульсов на серво, плюс всю кинематику я буду диффференцировать по постоянной величине времени dt = 20 мс. Если частоты хватит, а я полагаю ее хватит, раз mega256 то же самое делала более 100 раз в секунду. Получится эдакое логическое разделение на кинематическую часть, полностью просчитываемую каждые 20мс и управленчески-логическую часть (получение команд, менеджер задач робота), которая будет выполняться остальное процессорное время.

Разумеется, получится не ахти какое быстрое прерывание…

Если ты хочешь в течении 2.5мс эксклюзивно что-то делать, то это делается
просто:

  1. отключаешь прерывания, отключаешь шедулер (vTaskSuspendAll()),
  2. занимаешься своими делами (но учти, что шедулер выключен, т.е. теперь, наоборот нельзя отдавать управление, иначе все встанет), дергаешь пины.
  3. Включаешь шедулер (vTaskResumeAll()), разрешаешь прерывания.

Есть нюанс: пользоваться любыми блокирующими вызовами будет нельзя, при выключенном шедулере. Например, если ты вызовешь delay(), то твоя задача отдаст управление шедулеру. Который на паузе :slight_smile: и все просто остановится.

Вспомнил один тред на esp32.com. Там кто-то делал похожее, что и ты - генерировал какой-то сигнал, с жесткими таймингами.

Он натолкнулся на проблему, что его ШИМ имеет периодичные глюки, из-за того, что кто-то отбирает управление.

Проблема решилась переносом на ядро0 и отключением прерываний.

1 лайк

отличная новость для меня, спасибо! Ну, видимо уже опытным путем надо будет выяснить, будут ли такие действия негативно сказываться на работе wifi в части функций esp-now, например мешать обмениваться пакетами.

Я туплю… частота esp32s3 240mhz, то есть я как будто могу обойтись 1 таймером.

За 1 секунду таймер может насчитать 120 000 000, при минимально возможном делителе равном 2.

Чтобы таймер срабатывал каждые 20 мс, мне нужно создать прерывание по значению 120 000 000 / 50 = 2 400 000

При этом таймер же сбрасывается на ноль? то есть первые 3 миллисекунды внутри прерывания таймер будет в значениях от 0 до 120 000 000 / 1 000 * 3 = 360 000.

360 000 значений на 3 миллисекунды - более чем достаточное разрешение ШИМ для моей задачи.

Верно рассуждаю? В инете каша какая-то с таймерами на esp, с разными API и разными реализациями. И пока не встретил, как собсно получить значение счетчика.

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

я точно знаю, что тики процессора можно отсчитывать, поищи еще чуток.

а смысл? у моих mg90d мертвая зона 1 мкс, т.е. при её диапазоне управляющего импульса в 500…2500 мкс есть 2к дискретных значения, на которые серва будет реагировать. У таймера это будет 240к значений, т.е. перекрытие более чем в 100 раз. Зато есть запас на случай, если когда-нибудь будут более чувствительные сервы.

Для сравнения pca9685 имеет 12-битный ШИМ, но это на весь диапазон от нулевой длины импульсов до 100% заполнения. Если серва требует период в 20мс, то для относительно типичных 500-2500 мкс в этих 12 битах будет всего 1/10 диапазона, т.е. примерно 409 дискретных значений.

Если это сарказм, то можно конечно и поболя делитель поставить, но я так понимаю, на быстродействии это отразится вообще никак

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