Ви не увійшли.
"56 кроків", 40% і т.д. влаштовує не дуже
Ну у вас декілька варіантів:
1. Розібратись, чому не працює зі "стандартною" Servo. Мені не вдалося відтворити ту поведінку, що ви описуєте: імпульси йдуть чітко з періодом 20 мс і очікуваною тривалістю. Значить, або ми запускаємо різний код (чи скомпільований з різними налаштуваннями), або не враховуєм якісь умови відтворення, або ви невірно описали поведінку.
Яка інформація потрібна для подальшого аналізу - вказано в попередніх повідомленнях.
2. Реалізовувати поетапно те ж саме, що робить Servo (апдейт OCR в обробнику переривання), і дивитись, на якому етапі перестає працювати. Тоді стане зрозуміло, і чому перестає, і чи можна це побороти.
3. Оптимізувати ваш варіант із відліком інтервалів по перериванню, а не по значенню TCNT. При грамотній реалізації обробник можна вмістити в декілька десятків тактів, на 8 мегагерцах де дасть роздільну здатність у порядка 200-300 кроків. Але у вас же там ще nRF24 планується, чи не так? Будуть ще переривання і затримки від SPI. Як воно із вашим кодом взаємно впливатиме - передбачити складно. Можливо, доведеться також оптимізувати і SPI.
4. Обрати іншу апаратну платформу, яка краще підходить для цієї задачі. Он пишуть, у ESP32 аж 16 апаратних jitter-free каналів PWM. Можливо, варто розглянути інший драйвер BLDC, який керується іншим способом. Якось не дуже раціонально перетворювати цифрове значення у сервоподібну PWM, яка потім драйвером перетворюється на іншу PWM вже для двигунів.
Зазвичай спочатку формулюється задача, аналізуються необхідні ресурси, вже потім обирається залізо для реалізації. А ви штучно створили собі обмеження і намагаєтесь їх героїчно подолати.
Неактивний
Ну я так розумію там відтворюється якийсь проект від Алекса Гувера. Краще тоді з мінімальними змінами. У нього, судячи з каментів в коді, теж на 8 мгц щось не так.
Та там не зрозуміло, які коментарі від Гувера, а які вже від автора топіка. Якщо оригінальний код - це ось цей, то там ніякого Servo взагалі нема.
(А тут тема перейшла на нову сторінку, то того повідомлення з кодом вже не видно навіть в режимі редагування)
Остання редакція dimich (2026-07-20 17:05:00)
Неактивний
До речі, а що взагалі не так з тою лібою servo з професійної точки зору? Всередині наче нормально, підтримується купа процесорів. Якщо робити руками - вийде таке саме.
Винахід велосипеда - формування сигналу у прериванях методом зміни регістру співпадіня.
Налаштування таймера 1:
cli(); // отключить глобальные прерывания
TCCR1A = 0; // установить регистры в 0
TCCR1B = 0;
// Задание частоты прерываний выполняется програмированием регистров OCR1A, TCCR1B CS10, TCCR1B CS11,TCCR1B CS12
// CS12=0, CS11=0, CS10=1 делитель 0
// CS12=0, CS11=1, CS10=0 делитель 8
// CS12=0, CS11=1, CS10=1 делитель 64
// CS12=1, CS11=0, CS10=0 делитель 256
// CS12=1, CS11=0, CS10=1 делитель 1024
// Для 1000 прерываний в секунду CS12=0, CS11=0, CS10=1, OCR1A = 319, на выходе импульсы с частотой почти 50 Гц
// Неприятный сюрприз - 8 МГц ардуинка 1000 прерываний не тянет :(
// Пришлось уменьшить число временых интервалов с 1000 до 500, а число "уровней" PPM сигнала до 32 :(
// От отправки даных в СОМ порт вообще пришлось отказатся :(
OCR1A = 40; // установка регистра совпадения
TCCR1B |= (1 << WGM12); // включить CTC режим
TCCR1B |= (0 << CS10); // Установить биты на коэффициент деления
TCCR1B |= (1 << CS11);
TCCR1B |= (0 << CS12);
TIMSK1 |= (1 << OCIE1A); // включить прерывание по совпадению таймера
sei(); // включить глобальные прерыванияПідпрограмма:
{
// Принцип формирования импульсов управления - импульсы формируются ПООЧЕРЕДНО 50 раз в секунду, сначала устанавливается высокий уровень на ножке управления
// сервопривода 1, выдерживается требуемый интервал времени, устанавливается низкий уровень.
if (Serv==1) { // Проверка для какого канала управления производится формирование сигнала(канал №1)
switch (Shag) {
case 1:
digitalWrite(2, HIGH); // Установка высокого уровня на выводе 2
OCR1A = Serv_1_1; // Установка значения регистра совпадения - формирование длительности сигнала,
// значение вычисляется в основном цикле
Shag=2; // Присвоение переменно Shag значения 2 - чтобы при следующем прерывании сработал 2-й блок
break; // Выход из оператора case без выполнения 2-го блока
case 2:
digitalWrite(2, LOW); // Установка низкого уровня на выводе 2
OCR1A = Serv_1_2; // Формирование паузы перед началом формирования импулься на выводе 4
// значение вычисляется в основном цикле
Shag=1; // Присвоение переменно Shag значения 1 - чтобы при следующем прерывании сработал 1-й блок
Serv=2; // Присвоение переменно Serv значения 2 - чтобы при следующем прерывании перейти к формированию
// импулься для второго сервопривода
break;
}
} // Конец формирования сигнала для канала 1
//----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
if (Serv==2) { // Все тоже самое для канала 2, меняется только номер вывода на котором формируется сигнал
switch (Shag) {
case 1:
digitalWrite(4, HIGH);
OCR1A = Serv_2_1;
Shag=2;
break;
case 2:
digitalWrite(4, LOW);
OCR1A = Serv_2_2;
Shag=1;
Serv=3;
break;
}
}
//----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
if (Serv==3) { // Все тоже самое для канала 3, меняется только номер вывода на котором формируется сигнал
switch (Shag) {
case 1:
digitalWrite(7, HIGH);
OCR1A = Serv_3_1;
Shag=2;
break;
case 2:
digitalWrite(7, LOW);
OCR1A = Serv_3_2;
Shag=1;
Serv=4;
break;
}
}
//----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
if (Serv==4) { // Все тоже самое для канала 4, меняется только номер вывода на котором формируется сигнал
switch (Shag) {
case 1:
digitalWrite(8, HIGH);
OCR1A = Serv_4_1;
Shag=2;
break;
case 2:
digitalWrite(8, LOW);
OCR1A = Serv_4_2;
Shag=1;
Serv=5;
break;
}
}
//----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
// Для пробы используется только 4-ре канала, 5-й добавлен чтобы потом не мучится с вычислением задержек
if (Serv==5) { // Все тоже самое для канала 5, меняется только номер вывода на котором формируется сигнал
switch (Shag) {
case 1:
digitalWrite(8, LOW);
//digitalWrite(8, HIGH); // К этому времени на канале 4 (вывод)уже установлен низкий уровень, чтобы не возица потом
OCR1A = Serv_5_1; // вставлена пустышка - потом одну строку закоментировать, другую разкоментировать
Shag=2; // и выбрать на каком пине будет генерация сигнала
break;
case 2:
digitalWrite(8, LOW);
OCR1A = Serv_5_2;
Shag=1;
Serv=1; // Сигнал сформирован на вссех 5 каналах - возвращаемся к каналу 1
break; // частота сигнала определяется сумой задержек сформированим програмирование регистра совпадений
} // таймера 1 и делителем частоты таймера 1
}
}Осцилограф підтвердив працездатність, прошу покритикувати, особливо в частині швидкості виконаня.
Неактивний
До речі, а що взагалі не так з тою лібою servo з професійної точки зору?
Та як і в багатьох бібліотеках, деякі параметри прибиті цвяхами - без модифікації коду самої бібліотеки їх не зміниш.
Там і так уже AVR-специфічний код, чому б не транслювати ардуінівські піни на регістри AVR при ініціалізації, а в обробнику лише писати в них.
При зміні значення одного каналу фаза наступних зміщується. Якщо сумарне значення тривалостей для всіх каналів перевищує 20 мс, період слідування імпульсів збільшується. Зрозуміло, що така специфіка імплементації, але її треба мати на увазі.
Дещо дивують конструкції типу
if(SERVO(timer,Channel[timer]).Pin.isActive == true)Макроси, що посилаються на члени класу, і т.п.
Неактивний
if (Serv==1) { // Проверка для какого канала управления производится формирование if (Serv==2) { // Все тоже самое для канала 2, меняется только номер вывода на котором ...
Не лінь оце вам стільки коду дублювати?
Я би зробив цикл і таблички.
прошу покритикувати, особливо в частині швидкості виконаня.
Це як раз той випадок, коли має сенс замість digitalWrite() писати напряму в регістри GPIO.
Остання редакція dimich (2026-07-21 21:51:54)
Неактивний
TCCR1B |= (1 << WGM12); // включить CTC режим TCCR1B |= (0 << CS10); // Установить биты на коэффициент деления TCCR1B |= (1 << CS11); TCCR1B |= (0 << CS12);
Дільник таймера завжди виставляйте однією операцією запису:
TCCR1B = (1 << WGM12) | (0 << CS10) | (1 << CS11) | (0 << CS12);Або, якщо значення інших бітів невизначені:
TCCR1B = (TCCR1B & ~((1 << CS10) | (1 << CS11) | (1 << CS12)))
| (0 << CS10) | (1 << CS11) | (0 << CS12);Або, якщо за логікою заздалегідь відомо, що таймер зупинено (в CS1[2..0] нулі):
TCCR1B |= (0 << CS10) | (1 << CS11) | (0 << CS12);Неактивний
TCCR1B |= (0 << CS10)
Мені здається що ця конструкція не має смислу. Можливо компілятор її навіть виріже.
>> Осцилограф підтвердив працездатність, прошу покритикувати, особливо в частині швидкості виконаня.
Ну раз швидкості вистачає, все інше косметика і тактоложество. З косметичної точки зору - некрасиво ![]()
Мені здається що ця конструкція не має смислу. Можливо компілятор її навіть виріже.
Не виріже, бо регістри volatile.
Насправді, в більшості випадків така побітова конфігурація на практиці не має негативного ефекту. Лише, наприклад, при виставленні дільника на 1024 можна з подивом виявити, що Output Compare спрацьовує раніше ніж очікується. Але все одно краще писати зрозуміло і правильно.
А для насправді точних таймінгів потрібно ще й Timer Prescaler скидати, який спільно з Timer0 використовується ![]()
Неактивний
хттпс://godbolt.org/z/a5v8q5xzj
#include <avr/io.h>
int main() {
TCCR1B |= (0 << CS10) | (1 << CS11) | (0 << CS12);
}дає таку конструкцію
main:
push r28
push r29
in r28, 0x3d ; 61
in r29, 0x3e ; 62
ldi r24, 0x81 ; 129
ldi r25, 0x00 ; 0
movw r30, r24
ld r18, Z
ldi r24, 0x81 ; 129
ldi r25, 0x00 ; 0
ori r18, 0x02 ; 2
movw r30, r24
st Z, r18
ldi r24, 0x00 ; 0
ldi r25, 0x00 ; 0
pop r29
pop r28
ret#include <avr/io.h>
int main() {
TCCR1B |= (1 << CS11);
}відповідно, таку
main:
push r28
push r29
in r28, 0x3d ; 61
in r29, 0x3e ; 62
ldi r24, 0x81 ; 129
ldi r25, 0x00 ; 0
movw r30, r24
ld r18, Z
ldi r24, 0x81 ; 129
ldi r25, 0x00 ; 0
ori r18, 0x02 ; 2
movw r30, r24
st Z, r18
ldi r24, 0x00 ; 0
ldi r25, 0x00 ; 0
pop r29
pop r28
retрізниця відсутня ![]()
ну якби логічно, як 0 не рухай, він залишається 0.
Якщо розписати так
#include <avr/io.h>
int main() {
TCCR1B |= (0 << CS10); // Установить биты на коэффициент деления
TCCR1B |= (1 << CS11);
TCCR1B |= (0 << CS12);
}то таки чесно виконує безсмислену операцію
// TCCR1B |= (0 << CS12);
ld r24, Z
st Z, r24Неактивний
Якщо розписати так ... то таки чесно виконує безсмислену операцію
Справа не в беззмістовній операції, а в тому, що якби було би, наприклад,
TCCR1B |= (1 << CS10);
TCCR1B |= (0 << CS11);
TCCR1B |= (1 << CS12);то по закінчені цього коду таймер би уже натікав більше ніж треба.
Для покращення читабельності можна ще, наприклад, так:
uint8_t tmp = (1 << WGM12);
tmp |= (1 << CS10);
tmp |= (0 << CS11);
tmp |= (1 << CS12);
TCCR1B |= tmp;Неактивний
Нас рятує те що біти CS* після reset скинуті
.
Правильно мабуть
TCCR1B |= (1 << CS10);
TCCR1B &= ~(1 << CS11);
TCCR1B |= (1 << CS12);Або макросів навернути
Щоб якщо треба задати 3 біти - щоб вони не тільки встановлювались а і скидались. Ну або через змінну.
Остання редакція jokeR (Вчора 14:12:37)
Неактивний
Правильно мабуть
TCCR1B |= (1 << CS10); TCCR1B &= ~(1 << CS11); TCCR1B |= (1 << CS12);
Не зовсім. Правильно, щоб значення в регістрі оновлювалось однією інструкцією. А для працюючого таймера правильніше спочатку зупинити його, сконфігурувати, потім запустити. Звісно, якщо потрібно залишити якісь біти незмінними, то прочитати поточне значення і застосувати маску.
Якщо ж виставляється чи скидається лише один біт в одному з нижніх 64 регістрів, то компілятор може заоптимізувати це в одну інструкцію sbi чи cbi.
Або макросів навернути
Або static inline функцію. Але це вже синтаксичний цукор.
Остання редакція dimich (Вчора 14:45:09)
Неактивний
Налаштуваня таймера дані виключно для розуміня чого я там накарябав, налаштування виконується ОДИН РАЗ ще до початку основного цикла і НІЯК на швидкість виконаня не вплиива...
Пращює і добре - для таких ПрофесіАналів у програмувані як я важливіше розуміти як воно працює, а не щоб було красиво.
Ось цей шматок таким залишив навмистно:
OCR1A = 40; // установка регистра совпадения
TCCR1B |= (1 << WGM12); // включить CTC режим
TCCR1B |= (0 << CS10); // Установить биты на коэффициент деления
TCCR1B |= (1 << CS11);
TCCR1B |= (0 << CS12);Це с"огодні в мене такі налаштування таймеру такі, а якщо завтра захочу встановити інші?
Так усе у одному місці і з пам"яткою "що за що відповіда"....
Мене більше турбує обробка преривання, до ц"ого часу я обходився оператором if, і на комбінацію switch case прейшов щоб не пхати у код goto .
Неактивний
Ну вам залишилось Serv_4_1, Serv_4_2, Serv_5_1 .. занести в таблицю (масив)
І робити навігацію по масиву перемиканням індексу
І буде точно як в servo.cpp ![]()
TCCR1A = 0; // normal counting mode
TCCR1B = _BV(CS11); // set prescaler of 8
TCNT1 = 0; Зверніть увагу на макрос _BV, краще виглядає ніж (1 << CS11)
А ще краще як на мене
#define PRESCALER_8 (_BV(CS11))Неактивний
Ось цей шматок таким залишив навмистно:
OCR1A = 40; // установка регистра совпадения TCCR1B |= (1 << WGM12); // включить CTC режим TCCR1B |= (0 << CS10); // Установить биты на коэффициент деления TCCR1B |= (1 << CS11); TCCR1B |= (0 << CS12);Це с"огодні в мене такі налаштування таймеру такі, а якщо завтра захочу встановити інші?
Так а в чем будет неудобство, если вы всё это запишите в одну строчку, как рекомендовали выше? В данном случае у вас идет четыре раза чтение и четыре раза запись, вместо одного чтения/записи. И в первом присваивании замените |= на просто =, тем самым обнулив остальные биты.
до ц"ого часу я обходився оператором if, і на комбінацію switch case прейшов щоб не пхати у код goto .
в вашем случае if будет выполняться быстрее, чем switch. не намного, но быстрее.
Неактивний
Можна подумати під avr дофіга компіляторів і TCCR1B з CS11 є на дофіга архітектурах
дофига не дофига, но я, например, окромя gcc пользовался еще codevision и iar. и как то кумарило, что попадались такие макросы, что свойственны конкретному компилятору. особенно бесил разный стиль ассемблерных вставок.
Неактивний
важливіше розуміти як воно працює, а не щоб було красиво.
Одне іншому не заважає, а навіть допомагає. "Красиво" - це не мета, а наслідок коректності, зрозумілості та ефективності коду.
Це с"огодні в мене такі налаштування таймеру такі, а якщо завтра захочу встановити інші?
Так усе у одному місці і з пам"яткою "що за що відповіда"....
Так в чому проблема залишити все там само і так само, тільки записувати значення в регістр один раз замість чотирьох?
Вище наведено приклади, як можна написати щоб було і зручно, і правильно.
Мене більше турбує обробка преривання, до ц"ого часу я обходився оператором if, і на комбінацію switch case прейшов щоб не пхати у код goto .
Так всі гілки однакові, відрізняються лише значення змінних. Навіщо їх дублювати?
Нумеруйте все з нуля, окрема змінна Shag стає непотрібною. Всі ваші if та case перетворюються на:
static const uint8_t pins[4] = { 2, 4, 7, 8 };
static volatile uint16_t ocr[4][2];
ISR ...
{
uint8_t idx = Serv;
Serv = (idx + 1) % 8;
uint8_t channel = idx / 2;
uint8_t state = idx % 2;
digitalWrite(pins[channel], !state);
OCR1A = ocr[channel][state];
}Тільки не забувайте, що змінні, які читаються в обробнику переривань та більші за 8 біт, з основної програми потрібно оновлювати з вимкеними перериваннями. Ви ж тільки шматок коду показали, тому я не знаю, який там у вас тип у Serv_1_1 та інших.
Остання редакція dimich (Вчора 23:50:03)
Неактивний
Зверніть увагу на макрос _BV, краще виглядає ніж (1 << CS11)
Этот макрос для конкретного компилятора, а 1 << CS11 работает везде.
У ардуіни ще свій макрос bit() є в Arduino.h. Який, доречі, правильніший за (1 << x) та _BV() з точки зору стандарту.
Неактивний