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

Напомните какова частота тактирования таймера на ESP и какова разрядность? Может свою идейку подкину.

источник тактов 80 гц, предделитель не меньше 2, т.о. максимальная частота 40 гц. Разрядность счетчика 64 бита, предделителя - 16 бит

код для инициализации, если нужен:

  // таймер опирается на 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);
  uint64_t value = 1;
  uint64_t alarm_value = 800000;
  // начальное значение счетчика uint64_t value: 0..4294967295
  timer_set_counter_value (TIMER_GROUP_0, TIMER_0, value);
  // значение, при достижении которого сработает прерывание uint64_t alarm_value: 0..4294967295
  timer_set_alarm_value (TIMER_GROUP_0, TIMER_0, alarm_value);
  // разрешение прерывания
  timer_enable_intr (TIMER_GROUP_0, TIMER_0);
  // привязка функции обработки перрывания
  timer_isr_register (TIMER_GROUP_0, TIMER_0, timer0_ISR, NULL, 0, NULL);
  // запуск таймера
  timer_start (TIMER_GROUP_0, TIMER_0);

некуда запихивать - могу понять, а вот “усложняют” - это “все вы врете”

Глядя как вы мучаетесь, рожая софтверный ШИМ на 18 каналов - однозначно было бы в разы проще выделить аппаратный таймер на каждый канал и забыть об этом

1 лайк

@Resin , я имелл ввиду максимальную частоту тактирования таймера. Там десятки МГц. Я однажды делал на АВР на 16 МГц, счётчик 16 битный(до 65535 считает), на нём удалось добиться 5-6 сервоприводов(это с запасом на “перекур” и послеобеденный сон😀). Сейчас сложно описать работу, позже можно будет. Суть в разбиении таймера на временнЫе кадры. Счётчик считает от 0 до 65535 за 50мс, в процессе которых в прерываниях сравнения происходит сброс и установка нужных каналов сервоприводов.
Если максимальный период ШИМ сервопривода принять за 3 мс, получаем 16 штук, следующих друг за другом. Это ещё и на любых пинах любых портов, что очень удобно.

Как это «друг за другом»? То есть макс заполнение (скважность) для каждого будет 1/16 периода?.. хмм.

1 лайк

в рамках “сделать 1 раз и забыть” однозначно вы правы. А вот с точки зрения повторяемости, каждый, кто захочет сделать такого же робота будет вынужден паять эти драйвера тоже. Альтернатива - любая приемлемая софтверная реализация, хорошо закомментированная и описанная. По сути у меня уже 4 рабочих варианта, проведя немного тестов в цифрах, убедился, что вариант с таймером практически никак не отличается от варианта с ets_delay_us(delays[i]); и остановился на последнем, как более наглядном.

Уже не мучаюсь, а так, развлекаюсь)) мне все равно интересны различные способы реализации, предлагаемые людьми.

я правильно понял, что на каждый канал отводится фрейм времени, он подымается, потом когда ему уже пора - опускается, а когда макс. длина импульса проходит, то обрабатывается следующий канал? При 18 каналах такой номер не прокатит, даже скажем максимум 2550 мкс импульс, умножаем на 18 получаем 45,9 мс. При периоде шим 20 мс, т.е. импульсы будут в два с пол раза реже, чем требуется.

точно, я в 6 утра писал, наврал с единицами измерения: источник тактов 80 МГц, предделитель не меньше 2, т.о. максимальная частота 40 МГц. Разрядность счетчика 64 бита, предделителя - 16 бит

…адрес FF:FF:FF:FF:FF:FF она проигнорирует, т.к. это броадкаст.

Если не сработает с добавлением броадкаст адреса в список пиров, посмотри, как сделано в Maradeur (это wifi-tool такой, на ESP32, с исходным кодом). Там они конструируют пакет вручную, вместе со всеми заголовками и адресами.

Функция отсылки такого “raw”-фрейма находится в ROM ESP32-S3. Можешь ее использовать для отсылки своих auto-connect сообщений.

благодарю, я уже с этими вопросами на нужном мне уровне разобрался, но инфу прикопал - потом поразбираюсь углубленно.

Сейчас мне непонятно, как быть с каналами. Я не вижу в примерах из статей, как согласовываются каналы на двух платах. Я добавляю бродкаст в пиры с указанием канала, его же примут только те, у кого wi-fi запущен на таком же канале, верно? А на каком он вообще по умолчанию запускается, если я не подключаюсь ни к какой сети?

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

  esp_now_peer_info_t peer;
  while (esp_now_fetch_peer(true, &peer) == ESP_OK) esp_now_del_peer(peer.peer_addr);

и добавлять отозвавшегося. Это если мне плевать, на каком канале будет wi-fi запущен.

Самое интересное, если я уже таки подключен к wi-fi сети и не могу на постоянку канал менять - я должен для каждого канала раскидать бродкасты с просьбой переключиться на канал сети, к которой я подключен. Потом переключиться обратно на канал wi-fi сети, и уже кидать запрос на коннект. Если никто не отозвался в течение некоторого времени - снова раскидать бродкасты по всем каналам…

Дело в том, что допустимый период сервомашинки от 20 до 60 Гц. Но это не точно. Может и больше, надо испытания проводить.
Возможно что вполне можно будет запихнуть 20 каналов в 50мс или больше.
А частота тактирования чем больше, тем более точно можно задавать значения.

это да, но чем больше период - тем меньшее кол-во изменений можно передать за секунду. И тем сильнее будут выражены рывки в кинематике, т.к. вместо относительно линейного движения, сглаженного инертностью, сервы будут двигаться короткими рывками.
Еще я так понимаю, что при deathband 1us у сервы, надо будет прерывание вызывать каждую микросекунду - это очень часто, а если такое делать весь период ШИМ, то есть в принципе непрерывно, то нагрузит проц на столько процентов, сколько вычисления в ISR занимают относительно 1 мкс…

Не знаю, сервомашинка тоже время тратит на обсчёт. Не думаю что она может это делать быстрей 20мс.

@Resin неа. Постараюсь на своём примере показать.
Счётчик 16 бит спокойно себе тикает до 40000(это 20мс).
ТС-счетчик
OCRA, OCRB- регистры сравнения.
PWM[1,2,3,4] значения ШИМ соотв.каналов.
По OCRA происходит отключение, по OCRB включение каналов. В первый вносим следующий кадр, во втором выключаем текущий канал.
Допустим, один канал занимает 10000 тиков, т.о. 1 канал 0-10000, 2 10000-20000 и .т.д.
Значения ШИМ [500, 300, 990, 800]

Общий алгоритм:

  1. Прерывание OCA: первый канал отработал, выключаем, в OCRA вносим 300+10000(время выкл.2го канала), OCRB=10000(начало след.кадра)
  2. Преывание OCB: включаем канал 2.
  3. Прерывание OCA: второй канал отработал, выключаем, в OCRA вносим 990+30000(время выключения 3-го канала)
  4. Прерывание OCB: включаем канал 3.
    Ну и далее по кругу.
    То есть в прерывании мы быстро выключаем канал, настраиваем регистры на следующий и выходим. Могу скинуть код, либо графически отобразить.
    Никаких конфликтов прерываний не будет, т.к. импульс минимум 0.5мс для серво.

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

Спасибо, мысль-то примерно понял. Но на серве 2550 всетаки максимальный импульс. А цифровые его обсчитывают быстро, даже где-то в даташите на mg90d видел, что 50гц для нее не предел. Получается, суммарно не лезет в период.

Еще момент опасный, когда по сути таймер все время срабатывает, невозможно блокировать данные. Я правда запамятовал, какая минимальная адресация SRAM, но если там меньше uint16 то могут быть проблемы…

Если можете поделиться кодом - с радостью изучу. Не обещаю что применю, но хоть буду знать, что “так тоже можно”

Сейчас интересно. Я хочу сделать, чтобы при обрыве связи робот отправлял свой обычный запрос на команды, но широковещательный. А там какой пульт подхватит и отзовется первым - тот с ним и законнектится. В мануале Espressif про esp_now_send() написано:

If peer_addr is NULL, send data to all of the peers that are added to the peer list

При этом никаких исключений… Однако добавляю одного бродкаста и одного юникаста, вызываю esp_now_send(NULL, …) и в колл-бэке отправки прописываю вывод мака в сериал. Выводит только один мак - юникастовый. Если вместо NULL отдаю бродкаст адрес - вижу только его.

В целом, для меня это идеальное поведение, т.к. алгоритм строю по принципу: в пирах всегда только два адреса: бродик и юник. Соответственно, когда стабильно приходят ответы я шлю с NULL, если счетчик неполуений ответов превзошел трешольд, то шлю на broadcast:

esp_now_send((rc_silence > rc_silence_lost) ? broadcastMAC : NULL, (uint8_t *) &data, sizeof(data));

Всё бы ничего, но в отличие от esp_now_fetch_peer() в описании esp_now_send() нет ограничений на тип адреса, от чего у меня непроходящее ощущение, что я где-то допустил ошибку. Можете подтвердить, или опровергнуть?

P.s. добавил два юникаста и похоже, работает как мне хочется:

esp_now_send((millis() / 2000) % 2 == 0 ? NULL : broadcastMAC, (uint8_t *) &data, sizeof(data));

И еще вопрос, возможно даже последний.

У меня обмен всегда двусторонний, где пульт отвечает на инициативные запросы робота. Инициативный запрос робота всегда фиксированной длины (структура). Ответ пульта - другая структура, но тоже фиксированной длины.
Но в принципе же прийти может всё что угодно. Наверное, протокол esp-now позаботится о целостности, но не о содержании уж точно. Как мне определить, что принятые данные именно моего формата? сравнивать длину с sizeof моих структур, или есть более надежный метод?

Приведу притянутый за уши пример: робот включен и спамит запросами в ожидании “свободного” пульта. Пульт выключен, но я ковыряю другую плату, которая при старте спамит бродкасты с вообще левыми данными, но на том же канале. Робот его без проблем схавает и даже по полям структуры разложит, если memcpy сможет (а мне чет подсказывает, что она полюбому сможет). Но в данных же будет белиберда, а робот на серьезных щщах посчитает, что вот такие интересные команды ему прислали…

servo_pwm.h
#ifndef SERVO_PWM
#define SERVO_PWM 1

#include "Ship_main.h"

//количество сервоприводов
#define SERVO_NUM 4
//диапазон PPM,в милисекундах,подбираем по конкретному сервоприводу
/*#define SERVO_MIN_MS 0.5
#define SERVO_MAX_MS 2.5*/
//***********************
//отладка, заужаем диапазон
#define SERVO_MIN_MS 1
#define SERVO_MAX_MS 2
//***********************
#define TIME_ONE_MS 3.0  //цикл одного сервопривода,мс
#define TIME_ALL_MS 20.0 //цикл таймера,мс,обычно 20

//
//
//пересчитываем милисекунды в тики таймера
#define SERVO_MIN (16934400U*SERVO_MIN_MS/8.0/1000.0)
#define SERVO_MAX (16934400U*SERVO_MAX_MS/8.0/1000.0)
//диапазон сервомашинки,помноженый на 65536
#define SERVO_DELTA (uint32_t)((SERVO_MAX-SERVO_MIN)*1024)
//масштабируем к стандарту на входе 0..1024
#define SERVO_MAP(x) (uint16_t)(((x*(SERVO_DELTA/1024))/1024)+(uint16_t)SERVO_MIN)
//полный цикл таймера,в тиках таймера
#define TIME_ALL (uint16_t)(16934400U*TIME_ALL_MS/8U/1000U)
//максимальный цикл одного сервопривода в тиках таймера
#define TIME_ONE (uint16_t)(16934400U*TIME_ONE_MS/8U/1000U)
//для отладки,начальные значения
#define PWM_INIT_0 100
#define PWM_INIT_1 500
#define PWM_INIT_2 1000
#define PWM_INIT_3 500
#define VAL_MAX 8 //1024 это (>>8)

typedef struct
{
	volatile uint8_t *port;
	uint8_t pin;
	uint16_t pwm_on;
	uint16_t pwm_off;
}servo_t;

//настраиваем пины для сервоприводов,указатели на главный массив и его размер
void servo_init(volatile uint8_t*,uint8_t);
//совпадение OCR1A,для установки "1" на выходе
ISR(TIMER1_COMPA_vect);
//совпадение OCR1B,для установки "0" на выходе
ISR(TIMER1_COMPB_vect);

#endif

servo_pwm.c
#include "servo_pwm.h"
//создаем массив типа структура сервоприводов
servo_t servo[SERVO_NUM];
static uint8_t n=0;//текущий номер сервопривода
//указатель на массив делаем двухбайтовый,для удобства доступа к массиву
volatile uint16_t* buf_servo;

void servo_init(volatile uint8_t* buf,uint8_t num)
{
	n=0;
	//назначаем порты,пины и прочее
	servo[0].port=&PORTA;
	servo[0].pin=(1<<6);
	servo[1].port=&PORTA;
	servo[1].pin=(1<<7);
	servo[2].port=&PORTG;
	servo[2].pin=(1<<2);
	servo[3].port=&PORTC;
	servo[3].pin=(1<<7);
	//настраиваем все выводы на выход
	for(uint8_t i=0;i<SERVO_NUM;i++)
	{
		*(servo[i].port-1)|=servo[i].pin;//пишем в DDRx
	}

	buf_servo=(uint16_t*)(buf+BEGIN_SERVO_NUM);//нацеливаем на главный массив данных
	//Отладка
	#ifdef DEBUG
	*(buf_servo+0)=PWM_INIT_0;
	*(buf_servo+1)=PWM_INIT_1;
	*(buf_servo+2)=PWM_INIT_2;
	*(buf_servo+3)=PWM_INIT_3;
	#endif
	ICR1=TIME_ALL;
	//инициализация начальных значений
	OCR1A=100;
	OCR1B=SERVO_MAP(*(buf_servo+n));

	TCCR1B|=	_BV(WGM12)|_BV(WGM13);//режим CTC,модуль ICR,обновление регистров сравнения немедленное
	TIMSK|=		_BV(OCIE1A)|_BV(OCIE1B);//разрешаем прерывания сравнения и достижения ICR
	TCCR1B|=	_BV(CS11);//запуск счетчика,clk 1/8
//	TCCR1B|=_BV(CS12);// 1/256
}

ISR(TIMER1_COMPA_vect)
{
	//включаем текущий канал сервопривода
	*(servo[n].port)|=servo[n].pin;
} 

ISR(TIMER1_COMPB_vect)
{
	//выключаем текущий канал сервопривода
	*(servo[n].port)&=~servo[n].pin;
	//если достигли последнего привода,переключаем на первый,иначе следующий
	if(n<(SERVO_NUM-1)) {n++;} else {n=0;}
	//масштабируем входные данные привода к требуемым
	//т.к. в массиве значения от 0 до 1023,а на таймер необходимо порядка 4000 тиков,то...
	//...просто значение умножаем на TIME_ONE и делим на 1024 (>>8)
	//момент фронта след. привода
	OCR1A=TIME_ONE*n+1;
	//настраиваем момент спада
	OCR1B=SERVO_MAP(*(buf_servo+n))+TIME_ONE*n;
}
По сути там всё в последних обработчиках прерываний.
1 лайк

один вопрос, чтоб потом разобраться было проще. Для какого МК этот код?

mega128, F_CPU 16.9344 МГц, Таймер с делителем 8, в режиме PWM, а так же forceOC( это режим ШИМ, при котором регистры сравнения можно писать в любой момент, буферизация не действует)
Потом может быть руки дойдут на STM перепишу.

1 лайк

Пометь пакеты данныъ уникальной сигнатурой.

Обычно добавляют несколько байт, по которым точно понятно, что пакет твой. Например, можно добавить контрольную сумму (1 байт) и версию (1 байт), итого - два байта.

1 лайк

так и подумал, если честно)) чем длиннее уникальная фраза, идентифицирующая протокол - тем меньше вероятность ошибки.

Кстати, есть смысл счетчик пакетов вводить? Может при проблемах связи какой-то пакет приняться позже, чем отправленный следом за ним?

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

1 лайк