Skocz do zawartości

[C] Sterowanie kilkoma serwami za pomocą jednego PWM + układu 4028


greebqmaster

Pomocna odpowiedź

Cześć,

W tym temacie marek1707 dał mi pomysł na podział sprzętowego PWM na kilka kanałów za pomocą np. dekodera BCD->DEC lub innego sprzętu, dzięki czemu mógłbym za pomocą jednego PWM sterować kilkoma serwami. Trochę poszperałem w temacie, ale nie znalazłem nic, co mogłoby mi bardziej pomóc. Ściągnąłem sobie jednak datasheeta rzeczonego układu, rzuciłem na niego okiem i postanowiłem jednak coś zdziałać. Po kupieniu scalaka zabrałem się do pracy.

To, na co wpadłem do tej pory wygląda mniej więcej tak:

1. PWM zasilam układ 4028 (dekoder BCD -> DEC).

2. (Założenie - OCnx przy Compare Match = 0) Kiedy następuje Compare Match wykonuję przerwanie, w którym:

2.1. Zeruję rejestr TCNTn.

2.2. Zmieniam szerokość impulsu PWM.

2.3. Zmieniam kombinację bitów DCBA, aby wysterować następne serwo.

Jeżeli powyższe ma sens, to mam problem z przerwaniem przy Compare Match - trochę się pogubiłem w datasheecie i nie wiem czy, po pierwsze, w ogóle da się zrobić takie przerwanie w trybie Fast PWM i po drugie, jeżeli się da, to jaki bit za to odpowiada.

W mojej teorii wygląda to tak, że przerwanie przy Compare Match z trybu CTC występuje też w trybie PWM, z czego wyniknął poniższy kod:

#include <avr/io.h>
#include <avr/interrupt.h>

int main()
{
  // Fast PWM z TOP = ICR1, OC1A zerowany przy Compare Match i ->
  // -> ustawiany przy TCNT1 = BOTTOM; Prescaler = 64
  TCCR1A |= (1<<COM1A1) | (1<<WGM11);        
  TCCR1B |= (1<<WGM13) | (1<<WGM12) | (1<<CS11) | (1<<CS10);

  // Zezwolenie na przerwania przy Compare Match A
  TIMSK1 |= (1 << OCIE1A); 

  // Częstotliwość PWM = 50Hz, Okres = 20ms
  ICR1=4999;  

  // Konfiguracja portów
  DDRB |= (1 << 1); // PWM przy PB1 na wyjście
  DDRD |= (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3); // Bity DCBA w układzie 4028 = Piny 0-3 w Port D
  PORTD &= ~((1 << 0) | (1 << 1) | (1 << 2) | (1 << 3)); // Wyzerowanie czterech najmłodszych bitów w Port D

  while(1)
  {
  }  

}

ISR(TIMER1_COMPA_vect)
{
 static int i;

 TCNT1 = 0;

 switch(i)
 {
   // Ustawienie pierwszego serwa
   case 0:
   OCR1A = 530;
   PORTD = 0;
   break;

   // Ustawienie drugiego serwa
   case 1:
   OCR1A = 160;
   PORTD = (1 << 0);
   break;

   // Wypełnienie pozostałego czasu do uzyskania interwału 20ms ->
   // -> i przełączenie na nieużywany output w 4028
   case 2:
   OCR1A = 4999 - (530 + 160);
   PORTD = (1 << 1);
   break;  
 }

 i++;

 if(i > 2) i = 0;
}

Niestety, nie działa 😃 :D 😃 Ustawiałem w mainie na sztywno OCR1A na kilka wartości i serwo reagowało tylko na to. Stąd mniemam, że przerwanie chyba się jednak nie wykonuje... 😃

Schemat układu:

Info: Pozwoliłem sobie na nie oznaczanie na schemacie minimalnego podłączenia pinów - procek jest w Arduino, stąd chyba w tym się nie pomyliłem. Zaznaczyłem tylko wspólne masy. Dodatkowo, jeszcze nie ogarniam jak to zrobić w Eagle, ale DCBA podłączone są pod PD0-PD3 w tej samej kolejności.

Pomożecie?

Pozdrawiam,
greebqmaster

Link do komentarza
Share on other sites

Wiesz, nie bardzo mam czas na rozkminianie Twojego kodu, przepraszam, ale pomysł z dekoderem sprowadzał się do podłączenia go w ten sposób, by to wyjście timera OC1x wprost generowało impulsy a program ustawiał tylko numer wyjścia dekodera. Dzięki temu PPM generowany jest z absolutną dokładnością timera bez żadnych opóźnień związanych z tym kiedy program zapisze coś do portu. Czyli:

1. Podłączasz trzy wejścia ABC dekodera do jakichś trzech wyjść procka. Nazwijmy je "adresowe".

2. Podłączasz wejście D do np. OC1A i korzystasz wyłącznie z wyjść 0..7 dekodera. Zauważ, że dopóki wejście D=1, żadne wyjście z zakresu 0..7 nie jest aktywne (ma stan 0).

3. Zerujesz timer 1 i puszczasz go w trybie podstawowym tak, by chodził np. z zegarem 1MHz a wyjścia adresowe procka ustawiasz na 000.

4. Programujesz rejestry timera 1 tak, by najbliższe przerwanie od komparacji z OCR1A sprzętowo wyzerowało wyjście OC1A.

5. Wpisujesz do OCR1A cokolwiek, np. 1000 i odblokowujesz przerwania od komparacji z OCR1A i oczywiście te globalne. Najbliższe przerwanie przyjdzie za jakieś 1000us bo timer właśnie wystartował od zera.

6. Zapominasz o całym interesie i wykonujesz program główny 🙂

W kodzie musi się znaleźć globalna tablica zawierająca czasy tj. długości impulsów PPM w poszczególnych kanałach dekodera liczonych w us (1000-2000 będzie dawało 1-2ms) np. "czasy". Musisz też mieć licznik wyjść dekodera, np. "licznik".

W obsłudze przerwania:

1. Sprawdzasz co to przerwanie zrobiło (bo właśnie zrobiło) z wyjściem OC1A przez odczyt odpowiedniego kontrolnego rejestru timera 1.

1a. Jeżeli wyzerowało, to znaczy, że właśnie automatycznie zaczął się impuls PPM na wskazanym wyjściu dekodera. Następne przerwanie powinno przyjść za czas określony tablicą czasy[licznik], więc robisz:

OCR1A += czasy[licznik];

Nie ruszasz timera, tylko zwiększasz OCR1A co wyznacza chwilę następnego przerwania bez naruszania liczenia czasu rzeczywistego w timerze. Oczywiście przeprogramowujesz rejestry tak, by następnym razem komparacja spowodowała ustawienie wyjścia OC1A.

1b. Jeżeli przerwanie ustawiło wyjście OC1A to znaczy, że impuls własnie się zakończył. Mając 8 wyjść i chcąc generować impulsy PPM co 20ms na każdym wyjściu, musisz ich produkować 50*8=400 na sekundę a to oznacza, że każdy następny impuls powinien się zaczynać dokładnie co 2.5ms. Ponieważ na ten impuls "zużyłeś już" czasy[licznik] µs, to następny powinien zacząć się za 2500-czasy[licznik] µs, prawda? Wtedy suma długości impulsu i przerwy po nim następującej będzie zawsze równa 2500 okresów timera czyli 2.5ms. Odpowiednio więc zwiększasz OCR1A:

OCR1A += 2500-czasy[licznik];

i przestawiasz rejestry timera tak, by następna komparacja wyzerowała wyjście OCR1A i "sprzętowo" rozpoczęła następny impuls. Ponieważ powinien on pokazać się na kolejnym wyjściu, właśnie teraz zwiększasz licznik:

licznik++;

i sprawdzasz, czy nie przekręcił się przez 8:

licznik &= 0x07;

a jego nową wartość wysyłasz na 3 wyjścia adresowe do dekodera. Teraz jest na to dobry moment, bo żadne wyjście dekodera nie jest aktywne z powodu OC1A=1, a timer zaczął odliczać czas przerwy między impulsami.

Koniec. 8 impulsów robi się samo z dokładnością do pojedynczych nanosekund zegara procesora i z rozdzielczością Timera1 czyli 1us wg tablicy długości, którą możesz dowolnie zapisywać. Tablica musi być typu uint16_t no i mieć atrybut volatile.

Mam nadzieję, że niczego nie pokręciłem 🙂 Jeśli czegoś nie rozumiesz, narysuj sobie sekwencję zdarzeń w czasie: kiedy przychodzą przerwania, jak zmieniają OC1A, co masz w OCR1A i co na wyjściach adresowych itd. To proste. A w razie trudności pytaj. Powyższa metoda zajmuje poniżej 1% czasu procesora a rozbudowując system do dwóch wyjść OC1x pracujących na raz i dwóch dekoderów, możesz rozszerzyć system do 16 wyjść PPM generowanych praktycznie w ogóle bez obciążania procesora.

EDIT: Przeczytałem to na spokojnie, poprawiłem kilka zawiłości i jeden błąd rzeczowy.

  • Lubię! 1
Link do komentarza
Share on other sites

Poddaję się. Nie jestem w stanie wysterować nawet pojedynczego serwa bez 4028 z innego niż PWM trybu Timera. Poniżej przedstawiam kod dla połączenia, gdzie wyjście PB1 podłączam bezpośrednio pod sygnał serwa:

#include <avr/io.h>
#include <avr/interrupt.h>

#define F_CPU 16000000

int main()
{
 TCCR1A |= (1 << COM1A1); // Zeruj OC1A przy najbliższej komparacji
 TCCR1B |= (1 << WGM12) | (1 << CS12); // CTC OCR1A | prescaler 256
 TIMSK1 = (1 << OCIE1A); // Włączenie przerwania od Compare Match A
 OCR1A = 2000; // Najbliższe przerwanie za 2000 impulsów

 DDRB |= (1 << 1); //Ustawienie OC1A na wyjście

 while(1)
 {
 }
}

ISR (TIMER1_COMPA_vect)
{
 // Jeśli ostatnio OC1A zostało wyzerowane, to:
 // 1. Następna komparacja ustawi OC1A
 // 2. Stan "0" trwać będzie 1175 taktów (przy fcpu = 16Mhz / 256 = 62500 Hz jest to 18.8ms)

 if (~PORTB & (1 << 1))
 {
   TCCR1A |= (1 << COM1A1) | (1 << COM1A0);
   OCR1A = 1174;
 }

 // Jeśli ostatnio OC1A zostało ustawione, to:
 // 1. Następna komparacja wyzeruje OC1A
 // 2. Stan "1" trwać będzie 75 taktów (1.2ms)

 else if (PORTB & (1 << 1))
 {
   TCCR1A |= (1 << COM1A1);
   OCR1A = 74;
 }

 //Łącznie otrzymuję sygnał trwający 1.2ms z okresem 20ms
}


Błagam, czy ktoś może mi wyjaśnić, co tu jest źle?

Link do komentarza
Share on other sites

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.