Ich versuche, eine softwareimplementierte PWM zur Steuerung von 4 ESCs mit einem Atmega 16-Mikrocontroller zu implementieren.
Um dies zu erreichen, erzeuge ich sequentiell die Impulse für jeden ESC nacheinander in jeder Periode des Signals.
Hier ist der Code -
#define TOTAL_ESC 4
int ESC_pulse[TOTAL_ESC];
int ESC_pins[TOTAL_ESC];
int currentESC;
int main(void)
{
initESCs();
initUSART();
sei();
while (1)
{
}
}
void initESCs()
{
ESC_pulse[0] = ESC_pulse[1] = ESC_pulse[2] = ESC_pulse[3] = 2000;
ESC_pins[0] = PIND4;
ESC_pins[1] = PIND5;
ESC_pins[2] = PIND6;
ESC_pins[3] = PIND7;
currentESC = TOTAL_ESC - 1;
ICR1 = 39999; //50Hz signal @ 16MHz clock
OCR1A = 1000;
DDRD |= 1<<ESC_pins[0] | 1<<ESC_pins[1] | 1<<ESC_pins[2] | 1<<ESC_pins[3];
PORTD &= ~(1<<ESC_pins[0] | 1<<ESC_pins[1] | 1<<ESC_pins[2] | 1<<ESC_pins[3]);
TIMSK |= 1<<OCIE1A;
TCCR1A |= 1<<WGM11;
TCCR1B |= 1<<WGM13 | 1<<WGM12 | 1<<CS11;
}
ISR(TIMER1_COMPA_vect)
{
PORTD &= ~(1<<ESC_pins[currentESC]); //End prev ESC pulse
if(currentESC == TOTAL_ESC - 1 && OCR1A != 1000)
{
OCR1A = 1000;
return;
}
currentESC = (currentESC + 1) % TOTAL_ESC;
PORTD |= 1<<ESC_pins[currentESC]; //Start next ESC pulse
OCR1A += ESC_pulse[currentESC];
}
Ich versuche also, 50-Hz-Signale mit Impulsen von 1 ms bis 2 ms zu erzeugen. Mein CPU-Takt beträgt 16 MHz und der Timer-Takt ist auf 2 MHz vorskaliert.
Ich habe ein Array ESC_pulse
zum Speichern der Breite der Impulse für jeden ESC. Die darin enthaltenen Werte reichen von 2000 bis 4000 für die 1-ms- bis 2-ms-Impulse, die die ESCs benötigen. Die Logik, die ich anwende, ist, dass ich jedes Mal, wenn der Timer-Vergleichs-Interrupt auftritt, den Ausgangspin des letzten ESC lösche und den nächsten setze und den OCR1A
Wert mit dem aktuellen OCR1A
Wert + der Impulsbreite des aktuellen ESC aktualisiere, wie in gespeichert ESC_pulse
.
Mit der von der Hardware erzeugten PWM mit dem Timer ist mein ESC in der Lage, den bürstenlosen Gleichstrommotor zu betreiben. Die softwaregenerierte Technik funktioniert jedoch nicht. Ich habe keine Ausrüstung, um tatsächlich zu sehen, was die erzeugten PWM-Signale tatsächlich sind. Alles, was passiert, ist, dass der ESC Pieptöne erzeugt, die kein Signal anzeigen.
Ich bin mir nicht sicher, was ich hier falsch mache.
Ihr Programm schlägt fehl, weil OCR1A in allen PWM-Timer-Modi doppelt gepuffert ist und Sie einen solchen Modus verwenden (Fast PWM, TOP = ICR1). Wenn Sie einen neuen Wert in OCR1A schreiben, ändern Sie nicht wirklich den Wert, der von der Timer-Hardware verwendet wird. Stattdessen wird der in OCR1A gespeicherte Wert erst dann in das separate "Schattenregister" kopiert, das tatsächlich vom Timer verwendet wird, wenn der Zählerwert TOP erreicht und wieder bei Null beginnt. Dies ist sehr nützlich, um störungsfreie Hardware-PWM zu erzeugen, verhindert jedoch das, was Sie versuchen (mehrere OCR1A-Aktualisierungen pro Timer-Zyklus).
Da dieses OCR1A-Update nur einmal pro Timer-Zyklus (bei 50 Hz) erfolgt und Ihr Interrupt-Code 4 Verzögerungen von 1000 μs - 2000 μs + eine lange Verzögerung erzeugen soll, erhalten Sie am Ende eine PWM-Periode von 100 ms (5 Timer-Zyklen) und eine hohe Zeit von ~20 ms.
Die Lösung besteht darin, den Timer auf einen Nicht-PWM-Modus zu konfigurieren. Der für Ihr Programm am besten geeignete Modus ist Clear Timer on Compare Match (CTC, TOP = ICR1), der nahezu identisch funktioniert, aber OCR1A und OCR1B nicht doppelt puffert. Die WGM-Bits in TCCR1A und TCCR1B sollten auf 1100 gesetzt werden, um dies zu erreichen (siehe Datenblatt für Details).
Wenn Sie in einer Interrupt-Service-Routine auf eine Variable zugreifen, müssen Sie die Variable deklarieren volatile
. Das Weglassen des Schlüsselworts (unter anderem) ermöglicht es dem Compiler, Optimierungen vorzunehmen, die davon ausgehen, dass nur der Hauptprogrammfluss den Zustand ändern kann.
PIND4
, PIND5
, PIND6
und PIND7
wie von Ihrem Code verwendet werden nicht in den von AVR-GCC bereitgestellten Registerdefinitionen gefunden. Die allgemeineren Makros PINx
, PORTx
und DDRx
werden in "portpins.h" deklariert.
Die Ablauflogik könnte klarer geschrieben werden. Ihr aktueller Code hat 4 explizite Zustände in der Sequenz (einen für jeden Ausgangspin), von denen der letzte für zwei Vergleichsübereinstimmungen verwendet wird: Der erste, um den Ausgang von Kanal 4 niedrig zu setzen, und der zweite, um zu warten, bis die Sequenz neu gestartet werden soll. Dies geschieht seltsamerweise, indem ein bestimmter OCR1A-Wert (1000) als Flag verwendet wird. Das hat mich eine Weile am Kopf kratzen lassen.
Dies mag meinungsbasiert sein, aber ich würde eher uint8_t
, int16_t
und Freunde verwenden als zB unsigned char
oder short
in eingebetteter Software. Auf diese Weise wissen Sie und andere genau, wie groß Ihre Variablen sind, und sie sind auch weniger ausführlich.
Ihr Codebeispiel sollte <avr/io.h>
and enthalten <avr/interrupt.h>
und eine Funktionsdeklaration für bereitstellen void initESCs()
. initUSART()
ist überflüssig.
Wenn ich Ihren Code richtig verstehe, haben Sie Timer1 im schnellen PWM-Modus mit OCR1A, um die PWM-Einschaltdauer und ICR1 für den Zeitraum zu messen. Wenn OCR1A mit dem aktuellen Timerwert übereinstimmt, löst es einen Interrupt aus. Sie laden es dann mit einer längeren Zeit neu, die Idee ist, dass es den Timer 1 ~ 2 ms später für den nächsten Servoimpuls abgleicht.
Das Problem bei dieser Technik besteht darin, dass das Ausgangsvergleichsregister in PWM-Modi doppelt gepuffert und mit der PWM-Periode synchronisiert ist, sodass das Schreiben darauf während des aktuellen PWM-Zyklus nur im nächsten Zyklus Auswirkungen hat . Dies ist im Datenblatt auf Seite 98 beschrieben:-
Das OCR1x-Register wird doppelt gepuffert, wenn einer der zwölf Pulsweitenmodulationsmodi (PWM) verwendet wird... Die doppelte Pufferung synchronisiert die Aktualisierung des OCR1x-Vergleichsregisters entweder mit OBEN oder mit UNTEN der Zählsequenz. Die Synchronisation verhindert das Auftreten von unsymmetrischen PWM-Pulsen mit ungerader Länge
Anstatt also 1~2 ms nach dem Schreiben in OCR1A einen Interrupt zu erhalten, erhalten Sie ihn 20 ms + 1~2 ms später.
Ich bin mir nicht sicher, ob es möglich ist, dies mit einem Nicht-PWM-Timer-Modus auf Ihre Weise zu tun, aber es könnte einfacher sein, einfach einen einfachen Timer zu verwenden, um jeden Impuls separat zu timen, dann alle Zeiten zu addieren und von 20 ms abzuziehen, um zu erhalten die letzte Pausenzeit.
Viele moderne ESCs können mit Frequenzen von 250 Hz oder höher umgehen, sodass Sie vielleicht sogar damit durchkommen, die Impulse so schnell wie möglich nacheinander herauszudrücken.
TCCR1A |= 1<<WGM11;
Was ich in Ihrem Code NICHT sehe, ist eine Warteschlange jeglicher Art. Und du brauchst einen, denke ich.
So würde ich dies als Softwaredesignlösung angehen:
Dass es. Der ganze Prozess. Die Delta-Warteschlange ist so eingerichtet, dass sie einen „Tick“-Wert hält, der die Zählung in Mikrosekunden für das nächste Ereignis ist, das auftritt. Wenn Sie einen Eintrag in die Warteschlange einfügen, subtrahieren Sie vor dem Einfügen alle "Ticks" (Delta-Wert) in vorherigen Einträgen. Lassen Sie mich dies an einem Beispiel demonstrieren, um dies zu verdeutlichen.
Angenommen, der Zeitwert für sind: 1573, 2000, 1206 und 1573. Dann würde die Warteschlange so aussehen (ohne den zugehörigen Pin, der auch hier benötigt wird):
Das ist in der Reihenfolge der Warteschlange aufgeführt. Ich würde jedoch ein Array mit fünf Elementen mit Indizes von 0 bis 4 erstellen, wobei [0] als Warteschlange zugewiesen wird Zeiger. Die obige Warteschlange möchte also Folgendes in Array-Reihenfolge:
Wenn macht sich daran, alle vier in die Warteschlange einzufügen (die leer ist, wenn beginnt, da alle zu diesem Zeitpunkt alle abgelaufen sind), das ist die resultierende Warteschlange, wenn beendet und kehrt von seinem Interrupt-Ereignis zurück. Aber kurz vor dem Ausstieg, wie erwähnt, lädt den ersten OCR1B-Wert in die Warteschlange (hier 1206) und platziert ihn in OCR1B.
Wenn das OCR1B-Ereignis ausgelöst wird, teilt der erste Eintrag in der Warteschlange dem Code mit, welcher Pin auf LOW getrieben werden soll. Dann wird der Eintrag aus der Warteschlange entfernt. Für das erste OCR1B-Ereignis entfernt dies den 1206-Eintrag, wodurch der Delta-Wert von 367 verbleibt, der nicht null ist. Daher wird jetzt der OCR1B-Wert von 1573 in OCR1B geladen.
Wenn das nächste Ereignis eintritt, wird das OCR1B-Ereignis nun auch diesen zugeordneten Pin auf LOW treiben und diesen Eintrag aus der Warteschlange entfernen. Jetzt hat der nächste Eintrag einen Delta-Wert von 0 drin. Aus diesem Grund MUSS es bedeuten, dass es einen anderen Pin gibt, um LOW zu fahren, sodass das OCR1B-Ereignis fortgesetzt wird und diesen Pin ebenfalls auf LOW treibt und diesen Eintrag aus der Warteschlange entfernt. An diesem Punkt gibt es nur einen verbleibenden Eintrag in der Warteschlange, der den Delta-Wert 427 hat, der ebenfalls nicht Null ist. Der Code lädt nun den OCR1B-Wert von 2000 in das Vergleichsregister OCR1B und wird beendet.
Das letzte OCR1B-Ereignis wird nun ausgelöst und der Code treibt diesen Pin ebenfalls auf LOW und entfernt den Eintrag. Es sind keine Einträge mehr vorhanden. Der Code wird also einfach beendet. Es ist alles erledigt und es bleibt nur noch zu warten, bis das OCR1A-Ereignis erneut auftritt.
Das ist der Prozess zu folgen, denke ich.
Hier ist ein Beispiel dafür, wie ich die Warteschlangen dafür einrichten könnte, insbesondere das Veranschaulichen, wie in die Delta-Warteschlange eingefügt wird. Die Funktion qinsert() erledigt diese Aufgabe. ich hatte Rufen Sie qinsert() für jede der vier PWMs als Teil ihrer Aufgabe auf.
uint8_t qp[5]; /* prior in queue reference */
uint8_t qn[5]; /* next in queue reference */
uint16_t qk[5]; /* delta value */
uint16_t qv[5]; /* OCR1B value */
uint8_t qpin[5]; /* pin position 0..7 */
void qinit( void ) {
qn[0]= qp[0]= 0;
qk[0]= 0xFFFF;
return;
}
uint8_t qinsert( uint8_t node, uint16_t key ) {
uint8_t prv= 0, nxt;
uint16_t nxtkey;
qv[node]= key; /* optional, depending on overall design */
for ( nxt= qn[prv]; (nxtkey= qk[nxt]) < key; prv= nxt, nxt= qn[nxt] )
key -= nxtkey;
if ( nxt != 0 )
qk[nxt]= nxtkey - key;
qk[node]= key;
qp[node]= prv;
qn[node]= nxt;
qp[nxt]= qn[prv]= node;
return node;
}
uint8_t qunlink( uint8_t node ) {
uint8_t prv= qp[node], nxt= qn[node];
qn[prv]= nxt;
qp[nxt]= prv;
return node;
}
Chris Stratton
JimmyB
Chris Stratton