Wie der Titel schon sagt, brauche ich genaue Verzögerungen, nicht zu lang, idealerweise im Bereich von 0 - 350 CPU-Takten, aber wenn irgendetwas im engeren Bereich funktionieren würde, liegt der absolute Mindestbereich bei 20 - 127 CPU-Takten. Diese liegen also unter oder knapp über einzelnen Mikrosekundenverzögerungen (50 MHz CPU-Takt), relativ kurze mehrere Takte bis mehrere zehn Takte. Das Problem beim Abfragen eines Timers besteht darin, dass die Genauigkeit je nach Implementierung zu einem Schritt von maximal 7 Takten führt, zum Beispiel:
Wie geht man diese Art von Problem an? Irgendwelche Ideen sind mehr als willkommen?
Übrigens führe ich es auf einem C8051F38x-Mikrocontroller von Silicon Labs aus und verwende C51 und Keil zum Codieren und Kompilieren, wenn das wichtig ist.
Der Code, der als Teillösung entstanden ist, scheint dem gleichen Timing zu folgen, während die Schleife in C dies tut, und die "djnz" -Anweisung benötigt 5-6 CPU-Zyklen anstelle der im Datenblatt angegebenen 2/4.
ACC_save= ACC;
ACC = counter102;
P0b3 = 1; // Start the Pulse
#pragma ASM // Precice DELAY using assembler
clr C // ; 1 Clear Carry
rrc A // ; 1 C = 1 if odd
jnc even // ; 2 or 4 extra 2 cycles if branch taken (spoils cache)
nop // ; 1
nop // ; 1
clr C // ; 1
even:
subb A,#4 // ; 1
mov R7,A // ; 1
loop:
djnz R7, loop // ; supposed to be 2, but practically takes 5 to 6 cycles!
#pragma ENDASM`
P0b3 = 0; // Stop the Pulse
BEARBEITEN
Vielen Dank an alle für den tollen Input, ich hätte mir nicht vorstellen können, dass der Ideenfluss so positiv und vor allem produktiv sein könnte. Daher meine tiefe Wertschätzung an alle, die dazu beigetragen haben und in Zukunft beitragen werden. Nach euren wertvollen Beiträgen und großartigen Ideen habe ich mir also etwas einfallen lassen, das bis zu einem gewissen Grad für mich funktioniert. Der Code ist unten:
void delay(unsigned char delay_time) {
switch (delay_time)
{case 8: goto Q08;
case 9: goto Q09;
case 10: goto Q10;
case 11: goto Q11;
case 12: goto Q12;
case 13: goto Q13;
case 14: goto Q14;
case 15: goto Q15;
case 16: goto Q16;
case 17: goto Q17;
case 18: goto Q18;
case 19: goto Q19;
case 20: goto Q20;
default : goto Q00; }
Q19: PORT_ACTIVE(1); // 2clk
Q17: PORT_ACTIVE(1); // 2clk
Q15: PORT_ACTIVE(1); // 2clk
Q13: PORT_ACTIVE(1); // 2clk
Q11: PORT_ACTIVE(1); // 2clk
Q09: PORT_ACTIVE(1); // 2clk
_nop_(); // 1clk
goto EXIT1; // Skip the Even delay part
Q20: PORT_ACTIVE(1); // 2clk
Q18: PORT_ACTIVE(1); // 2clk
Q16: PORT_ACTIVE(1); // 2clk
Q14: PORT_ACTIVE(1); // 2clk
Q12: PORT_ACTIVE(1); // 2clk
Q10: PORT_ACTIVE(1); // 2clk
Q08: PORT_ACTIVE(1); // 2clk
Q00: // 0clk
EXIT1:
return; // Exit from the function takes 7 clocks
} // END of function delay
// Continued execution after the delay function
PORT_ACTIVE(0); // 2clk
PORT_ACTIVE(x) ist also eine #define-Funktion, die den pulsierenden Port aktiviert. Da ich alle Zeit habe, die ich brauche, bevor ich mit dem Impuls beginne, konnte ich den größten Teil des Overheads im Zusammenhang mit Entscheidungen vor der eigentlichen Aktivierung des Ports hineinquetschen. Dann dauert der Rücksprungbefehl immer gleich lange, sodass ich jetzt einen Impuls mit einer Breite von mindestens 8 Taktzyklen und bis zu 20 Zyklen erzeugen kann. Ich erweitere sie jetzt auf bis zu 100 Takte, natürlich auf Kosten des verfügbaren Speicherplatzes. Und so ist diese Lösung tatsächlich dank der Idee von JimmyB, die Impulsaktivierung in die Funktion fallen zu lassen und nicht davor, und natürlich dank der großartigen Ideen von TCROSLEY, wie man die ungeraden und geraden Verzögerungen handhabt, es ist gerecht dass der Wechsel zur Assemblierung für die Debugging-Erfahrung nicht wirklich freundlich ist,
Eine weitere Anmerkung ist, dass ich, sobald ich mit dem Feiern einer funktionierenden Lösung fertig war, auf das nächste Problem stoße.
ZWEITES PROBLEM
Ich muss einen zweiten Impuls Rücken an Rücken zum ersten mit unabhängiger Breite ausführen. Also kein Overhead für den zweiten Impuls, da er sonst mit unterschiedlicher Breite endet. Es bringt mich ziemlich genau an die Stelle zurück, an der ich vorher war, da der zweite Impuls wieder auf den 6-Zyklus-Engpass der While-Schleife beschränkt ist, es sei denn, es gibt eine Möglichkeit, den Verzweigungs-Overhead für den zweiten Impuls vor den ersten Impuls zu legen. Irgendwelche Ideen dazu?
Wie andere bereits erwähnt haben, geschieht dies am besten in der Montage. Hier ist mein ursprünglicher Versuch, dies zu codieren, als ich dachte, die Sprunganweisungen dauerten entweder 2 oder 4 Zyklen (siehe Bearbeiten unten für die überarbeitete Version).
void delay_sub(unsigned char i)
{
// convert 20, 21, 22 etc to count in R7 of 1, 2, 3 (extra cycle added if i is odd)
; cycles
rrc A ; 1 c = 1 if odd
jnc even ; 2 or 4 extra 2 cycles if branch taken (spoils cache)
nop ; 1 delete if using lcall's instead of acall's
nop ; 1 same
clc ; 1 in either case carry is clear prior to subb
even:
subb A,#9 ; 1
mov R7,A ; 1 R7 now = (i / 2) - 9
//while (i--);
loop:
djnz R7, loop ; 2 loop address should be in cache, so no extra cycles needed
ret ; 6
}
timing calculation (assuming acall's)
if i even:
5+7+R7*2+6 = minimum of 20 22 24 ... => R7 = 1, 2, 3 ...
if i odd:
5+8+R7*2+6 = minimum of 21 23 25 ... => R7 = 1, 2, 3 ...
Es wird davon ausgegangen, dass ein Aufruf wie ACALL(nn) erfolgt, wobei nn eine Konstante oder eine Variable in einer Byte-Variablen ist, sodass der Parameter beispielsweise unter Verwendung eines MOV A,#n-Befehls mit einem Zyklus übergeben werden kann. Das Mindesttiming, das Sie tun können, beträgt 20 Takte, wie Sie es gefordert haben.
mov A,#n ; 1
acall delay_sub ; 4
Es wird nicht geprüft, ob der Parameter größer oder gleich 20 ist, alle Werte kleiner als 20 ergeben ein falsches Timing.
Der mov-Befehl und ein Aufruf dauern 5 Zyklen. Zunächst wird der Zählwert (i) durch zwei geteilt, um zu berücksichtigen, dass der DJNZ-Befehl zwei Zyklen benötigt. Dann wird die Zählung angepasst, um einen Zyklus hinzuzufügen, wenn i ungerade ist. Schließlich wird ein fester Wert subtrahiert, sodass der Wert im zu dekrementierenden Register (R7) im Bereich 1, 2, 3 liegt ... R7 wird dann in einer engen Schleife (zwei Zyklen pro Zählung) dekrementiert. Für die Rückkehr gibt es eine feste Zyklenzahl von 6.
Wenn Sie einen LCALL anstelle eines ACALL verwenden müssen, beträgt das minimale Timing, das Sie ausführen können, 21 Takte anstelle von 20, und Sie müssen die beiden Nops nach der jnc-Anweisung löschen. Sie müssen entweder alle ACALLs oder LCALLs verwenden, Sie können sie nicht mischen.
Ich würde vermeiden, C zum Aufrufen der Funktion zu verwenden, es sei denn, Sie können garantieren, dass der Compiler keinen zusätzlichen Overhead hinzufügt. Außerdem verwende ich R7 als Scratch-Register; In Ihrem Compiler-Handbuch erfahren Sie, welche Register in einer Assembler-Funktion verwendet werden können, ohne sie speichern zu müssen (falls vorhanden).
Dies berücksichtigt auch nicht das Deaktivieren und erneute Aktivieren von Interrupts, falls erforderlich, um sicherzustellen, dass die Zeitgebungsroutine nicht unterbrochen wird.
Das Verhalten der Sprungbefehle basiert auf dem Datenblatt für den C8051F38x, wie ich es verstehe (in Bezug darauf, wann der Befehlscache beschädigt ist oder nicht). Dies kann bei anderen Versionen des 8051 anders sein.
Schließlich habe ich nicht die Syntax für den Sprung in die Inline-Assemblierung und wieder heraus gezeigt. Das Unterprogramm könnte auch in eine separate Datei gestellt und zusammengesetzt werden.
Seit ich den ursprünglichen Code geschrieben habe, hat mir das OP mitgeteilt, dass die Anzahl der Taktzyklen für einen Sprung in seinem 8051 5 oder 6 beträgt, nicht die 2 oder 4, die in dem von mir gelesenen Datenblatt angegeben sind. Also habe ich die Routine umgeschrieben, um dies zu berücksichtigen. Leider erhöht dies die minimale Zyklenzahl, die getimt werden kann, auf 32 statt 20. Wenn also Zählungen zwischen 20 und 31 unbedingt verarbeitet werden müssen, muss ein spezieller Code speziell für diesen Fall geschrieben werden (siehe unten).
void delay_sub(unsigned char i)
{
// minimum value of i is 32
; cycles
clr C ; 1
subb A,#32 ; 1 adjust for overhead of call and this routine
; a branch could be added here in case the result is negative
mov B,#6 ; 1
div AB ; 4 quotient in A, remainder in B
mov DPTR,#adjustcycles ; 1
mov R7,B ; 1
mov B,A ; 1 save quotient in B as temp
mov A,#6 ; 1
clr C ; 1
subb A,R7 ; 1 A now has 5 - B (remainder)
mov R7,#0 ; 1
jmp @A+DPTR ; 6 jump into table to add clocks based on remainder
adjustcycles: ; execute additional cycles based on remainder
inc R7 ; 1 for remainder of 5
inc R7 ; 1 for remainder of 4
inc R7 ; 1 for remainder of 3
inc R7 ; 1 for remainder of 2
inc R7 ; 1 for remainder of 1
nop ; 1 for remainder of 0
mov A,B ; 1 now has i / 6, have already adjusted for remainder
loop:
djnz loop ; 6
ret ; 6
timing in clock cycles is: 5 (call) + 21 (fixed overhead) + 6*(i/6) + (i%6) + 6 (ret)
if i = 0, 5 + 21 + 6 = 32 therefore that is the minimum count
Anstatt den Parameter i wie im vorherigen Beispiel durch 2 zu teilen, muss ich ihn jetzt durch 6 teilen, da ich davon ausgehe, dass der DJNZ-Befehl 6 Zyklen dauert. Also müssen wir i / 6 Mal durchlaufen und auch 0 bis 5 Zyklen für den Rest hinzufügen (i % 6).
Der Rest meiner obigen Kommentare trifft ziemlich gut auf dieses Beispiel zu. Ich belasse den ursprünglichen Code, falls jemand tatsächlich einen 8051 mit einer DJNZ-Anweisung mit zwei Zyklen hat.
Für Zählwerte von 20 bis 31 könnten Sie eine Subroutine mit nur einem Nop erstellen, die 12 Zyklen einschließlich Aufruf und Rückgabe benötigt:
void delay12(void)
{
nop
}
Für 20-23 Zählungen würden Sie es einmal aufrufen und nach dem Aufruf 8 bis 11 Nops hinzufügen (oder einen Dummy-Sprung zur nächsten Anweisung, der 6 Zyklen plus 2 bis 5 Nops verbrauchen würde - das Verzögern von 20 Zyklen würde also nur vier kosten Anweisungen plus das Unterprogramm, von dem angenommen wird, dass es mehr als einmal verwendet wird.). Für Zählungen von 24 bis 31 würden Sie delay12 zweimal aufrufen und je nach Bedarf 0 bis 5 Nops und/oder eine Sprunganweisung hinzufügen.
Um also 20 Zyklen zu verzögern:
acall delayl12
jump next
next:
nop
nop
djnz R7, loop
Ich habe versucht, das durch zwei Befehle zu ersetzen : dec A
; und dann JNZ loop
, aber das gleiche Ergebnis ... ich mache etwas falsch und ich bin nicht gut genug im Zusammenbauen , um herauszufinden , was es ist ...Sie könnten (ich glaube, ich erinnere mich nicht so gut an die 8051-Architektur) einen kalkulierten Sprung in ein 'Meer' von Nops machen. Kombinieren Sie es vielleicht mit einer Schleife, um die erforderliche Anzahl von Nops zu reduzieren (oder es gibt möglicherweise einen ausgefeilteren Weg, dies zu tun..)
Ich habe das einmal in der früheren, aber verwandten MCS-48-Architektur gemacht, um mit variabler Latenz oder ähnlichem umzugehen.
Sie sollten in der Montage für Einzelzykluspräzision arbeiten . Keil unterstützt einige Methoden zur Verwendung von Assembly in Kombination mit C, und wahrscheinlich würde die einfachere Inline-Assembly für Sie funktionieren.
Was auch immer Sie tun, es wird etwas Overhead geben, also ist das Beste, was Sie tun können, eine Verzögerung von n + 1 bis n + 255 Zyklen. n <= 20 Zyklen sollten machbar sein.
Ja, wie Spehro betonte, dass Sie in der Montage arbeiten sollten, und Sie können mit der Montage eine Genauigkeit von bis zu einem Maschinenzyklus erreichen, aber keinen Taktzyklus.
Zwei Möglichkeiten mit Assemblersprache
Verwenden Sie nop-Befehle und -Schleifen so, dass die Ausführungszeit von Befehlen und nops Ihrer erforderlichen Zeitverzögerung entspricht. Auf diese Weise müssen Sie wissen, wie viele Maschinenzyklen jeder Befehl in Ihrem Code benötigt.
Verwenden Sie Timer.
Die Compiler werden Ihren C-Code modifizieren, um den Code zu optimieren, was sicherlich Ihre Verzögerung ändern würde, also müssen wir aus Gründen der Präzision in Richtung Assembler gehen.
Da ich vor einem Jahr die Assemblersprache 8051 studiert habe. Es hat nicht viele Befehle und wenn Sie studieren, werden Sie sicherlich viel über Mikrocontroller und ihre grundlegende Architektur lernen. Obwohl Sie die Montage eines Mikrocontrollers nicht lernen müssen, wäre es sicherlich hilfreich zu wissen, wie die Assemblersprache für mindestens einen Mikrocontroller funktioniert.
trosley
David Tweed
Cezar
Cezar
Jaßer
JimmyB
JimmyB
trosley
Cezar