Funktionswiedereintrittsproblem, verursacht durch den Datenüberlagerungsmechanismus von Keil C51

Probleme, die durch das Lesen des E/A-Pin-Pegels verursacht werden

Als ich kürzlich mit einem Projekt beschäftigt war, bei dem der 51. Single-Chip-Mikrocomputer im Unternehmen zum Einsatz kam, stieß ich auf ein seltsames Problem. Das Projekt verwendet mehrere E / A-Pins, um den Status des externen Pegels in der While-Schleife der Hauptfunktion zu erhalten, und verwendet gleichzeitig einen ADC, um das externe Analogsignal im periodischen Timer-Interrupt zu sammeln. Im tatsächlichen Betrieb wird festgestellt, wann Die E/A erhält den externen Pegel. Der falsche Pegel wird zufällig abgerufen (z. B. ist der externe Eingang auf niedrigem Pegel, der gelesene E/A-Wert wird jedoch als hoher Pegel angezeigt), und der Codeausschnitt im Zusammenhang mit dem Problem lautet ungefähr wie folgt:

/* 定时器1周期性溢出中断 */
void timer_irq(void) TIMER_VECTOR
{
    
    
    u16 adc_date = adc_get(); /* 采样 */
    buf->append(adc_date);    /* 加入缓冲 */
}

int main(void)
{
    
       
    while(1)
    {
    
    
        get_io_status(); /* 获取IO状态 */
	}
}

Eine weitere Suche ergab, dass beide Funktionen get_io_status() und adc_get() dieselbe Funktion io_get() verwenden, um den E/A-Level zu erhalten, und die Funktionsimplementierung ungefähr wie folgt ist:

bool io_get(u8 io_number)
{
    
    
    bool status;
    status = get_io_status_by_number(io_number);
    return status;
}

Führen Sie das Programm aus, nachdem Sie die Funktion adc_get () mit Anmerkungen versehen haben, und stellen Sie fest, dass get_io_status () den Pegel normal erhalten kann. Nachdem ich darüber nachgedacht habe, kann dies an der Wiedereintrittsfunktion der Funktion io_get liegen. Nachfolgende Experimente bestätigten meine Idee ebenfalls.

Die Datenüberlagerungsfunktion verursacht Probleme

Einige Leute haben möglicherweise Fragen: Globale Variablen werden in der Funktion io_get() nicht verwendet. Warum verursacht der Wiedereintritt der Funktion io_get() Probleme? Tatsächlich stellt der obige Code für stm32 und andere moderne Arm-basierte Einzelchip-Mikrocomputer grundsätzlich keine Probleme dar. Die meisten Compiler von Armkern-Einzelchip-Mikrocomputern (oder fast alle C-Sprachcompiler) werden Funktionsparameter und lokale Variablen speichern Im RAM-Stapel kann verstanden werden, dass bei jedem Aufruf einer Funktion ein Speicherplatz für diesen Aufruf zugewiesen wird, um die Parameter dieses Aufrufs und die lokalen Variablen in der Funktion zu speichern. Wenn eine Funktion also nur lokale Variablen verwendet, und die internen Die anderen aufgerufenen Funktionen sind ebenfalls wiedereintrittsfähig, sodass der Wiedereintritt in die Funktion grundsätzlich keine Probleme verursacht. Bei älteren 51-Kern-Einzelchip-Mikrocomputern ist die Situation jedoch anders: Viele 51-Kern-Einzelchip-Mikrocomputer verfügen möglicherweise nur über Einige hundert Bytes, und der vom Stapelzeiger unterstützte Adressbereich beträgt nur 256 Bytes. Wenn Sie beim Aufrufen von Funktionen immer noch Parameter und lokale Variablen auf dem Stapel speichern möchten, läuft dieser kleine Stapel bald über und basiert auf dem Stapel Der Stapel- und Popup-Vorgang des Funktionsaufrufs beim Ein- und Austritt der Funktion stellt für viele 51-Einzelchip-Mikrocomputer mit niedriger Geschwindigkeit ebenfalls einen erheblichen Mehraufwand dar. Um dieses Problem zu lösen, verwendet der Keil C51-Compiler daher eine Funktion namens Datenüberlagerung bei der Implementierung der C-Sprachfunktionsfunktion. Hier ist ein Auszug aus dem Originaltext im Handbuch:

Die meisten C-Compiler speichern Funktionsargumente und lokale Variablen in einem Teil des Hardware-Stacks, der als Stack-Frame bezeichnet wird. Ein Basiszeiger wird beim Eintritt in eine Funktion gesetzt und dient dem Zugriff auf Funktionsargumente (auf der einen Seite des Basiszeigers) und lokalen Variablen (auf der anderen Seite des Zeigers). Während diese Technik bei Geräten gut funktioniert, bei denen der Stapel beliebig groß sein kann, ist die Leistung beim 8051 schlecht. Der 8051-Hardware-Stack ist auf maximal 256 Bytes begrenzt. Daher ist die Verwendung von Stack-Frames auf dem 8051 eine große Verschwendung des begrenzten verfügbaren Speichers.

Der Keil C51 C Compiler arbeitet mit dem LX51 Linker zusammen, um Funktionsargumente und lokale Variablen an festen Speicherorten unter Verwendung klar definierter Namen zu speichern (so dass Funktionsargumente einfach übergeben und abgerufen werden können). Der Linker analysiert die Struktur des Programms und erstellt einen Aufrufbaum, mit dem er die Datensegmente überlagert, die lokale Variablen und Funktionsargumente enthalten. Diese Technik funktioniert äußerst gut und bietet die gleiche effiziente Speichernutzung wie ein herkömmlicher Stapelrahmen. Er wird manchmal als Kompilierungszeit-Stack bezeichnet, da das Stack-Layout vom Compiler und Linker festgelegt wird. Ein Vorteil des Kompilierungszeit-Stacks besteht darin, dass auf Argumente und lokale Variablen direkt über feste Speicheradressen zugegriffen wird (was beim 8051 sehr schnell ist). Typische stapelbasierte Zugriffe sind indirekt und werden auf dem 8051 langsamer ausgeführt.

Obwohl diese Technik größtenteils automatisch abläuft, gibt es einige Situationen, die besondere Aufmerksamkeit erfordern.

  • Indirekte Funktionsaufrufe (Funktionszeiger).
  • Tabellen oder Arrays von Funktionszeigern.
  • Rekursive Funktionen (Funktionen, die sich selbst aufrufen).
  • Reentrant-Funktionen (Funktionen, die von mehr als einem Ausführungsthread aufgerufen werden – zum Beispiel eine Funktion, die von main und einer Interrupt-Serviceroutine aufgerufen wird.

Jede dieser Situationen erfordert besondere Aufmerksamkeit Ihrerseits und erfordert möglicherweise die Verwendung der Linker- Anweisung OVERLAY .

Tatsächlich wird diese Funktion im Volksmund erklärt: Gemäß der Konvention sollten wir den Stapel verwenden, um die Parameterübergabe und die Speicherung lokaler Variablen beim Aufrufen von Funktionen wie bei vielen anderen C-Compilern zu implementieren, aber der Stapel von C51 ist zu klein, daher der Klassiker Die Leistung der Dinge auf dem 51-Kern-Einzelchip-Mikrocomputer ist nicht gut, verglichen mit dem schlechten RAM des 51-Kern-Einzelchip-Mikrocomputers, wenn jeder Funktionsaufruf einen Stapelraum öffnet und dann den Stapel schiebt und öffnet , dann reicht der kleine Arbeitsspeicher und das bisschen Leistung des 51-Kern-Single-Chip-Mikrocomputers nicht aus. Wie können wir also den Overhead des Stapels reduzieren? Keil C51 sagte, dass ich beim Kompilieren den Funktionsaufrufbaum Ihres Programms analysieren und Ihnen die Speicheradresse jedes Funktionsparameters und jeder lokalen Variablen im Voraus direkt im RAM zuweisen werde zu reduzieren Die Kosten für das Schieben und Herausspringen in den Stapel beim Aufrufen der Funktion und für die lokalen Variablen einiger Funktionen haben wir festgestellt, dass sie zugewiesen werden, wenn sie nicht gleichzeitig verwendet werden müssen Eine Adresse, was einem Zeitmultiplexen des Raums dieser Adresse entspricht. Dadurch wird die Verwendung von RAM reduziert, was tatsächlich der Bestimmung der Stapelstruktur während der Kompilierungsphase entspricht.

Hier ist ein Beispiel für das Zeitmultiplexen des RAM-Speicherplatzes: Angenommen, ich analysiere und stelle fest, dass das Programm Funktion A, Funktion B und Funktion C enthält und gleichzeitig Funktion B und Funktion C in Funktion A aufgerufen werden. Bei der Zuweisung von RAM-Speicherplatz müssen dann die lokalen Variablen der Funktion A separaten Adressen zugewiesen werden, die nicht mit den lokalen Variablenadressen der Funktionen B und C übereinstimmen dürfen, da sich der Lebenszyklus der Funktion A mit den Lebenszyklen der Funktionen B und C überschneidet. Die lokalen Variablen in den Funktionen B und C sind jedoch dieselbe RAM-Adresse, die für das Raum-Zeit-Multiplexen verwendet werden kann, da zwischen ihnen keine Aufrufbeziehung besteht und sich die Funktionslebenszyklen nicht überlappen. Die folgende Abbildung beschreibt die Aufrufbeziehung und den Ausführungslebenszyklus zwischen den Funktionen A, B und C:

Bild-20220625011935120

Dann könnte die Adresszuweisung ihrer lokalen Variablen im Speicher wie folgt aussehen:

Bild-20220625012734117

Es wird jedoch auch im offiziellen Dokument erwähnt: Unser Ding ist vollautomatisch und funktioniert in den meisten Szenarien gut, aber wir müssen den folgenden zwei Sonderfällen besondere Aufmerksamkeit schenken:

  1. Im Programm werden Funktionen wie Funktionszeiger oder Arrays von Funktionszeigern verwendet, um Funktionen indirekt aufzurufen. (Der Compiler weiß nicht, auf welche Funktion der Funktionszeiger zur Laufzeit letztendlich zeigen wird, und kann natürlich nicht die richtige Funktionsaufrufkette generieren, um die Speicherzuweisung zu steuern. Daher ist es am besten, bei Verwendung von Keil C51 keine Funktionszeiger zu verwenden.)

  2. Im Programm kommt es zu Funktionswiedereintritten, beispielsweise beim rekursiven Aufrufen einer Funktion oder beim gleichzeitigen Aufrufen derselben Funktion in verschiedenen Kontexten, beispielsweise wenn die Hauptfunktionsschleife Funktion A zur Hälfte aufruft, dann die Ausführung von Funktion A unterbricht und unterbricht die Verarbeitungsfunktion Funktion A wird erneut aufgerufen. (Alle lokalen Variablen in der Funktion sind vorab zugewiesene Adressen. Wenn die Funktion also rekursiv aufgerufen wird oder dieselbe Funktion gleichzeitig in verschiedenen Kontexten aufgerufen wird, werden die bei jedem Aufruf verwendeten lokalen Variablen tatsächlich an derselben Adresse gespeichert kann Probleme verursachen)

Wenn die beiden oben genannten Situationen im Programm auftreten müssen, bietet keil C51 auch eine Lösung namens OVERLAY Linker Directive an. Tatsächlich ändern wir die vom Compiler generierte Funktionsaufrufkette manuell. Hier ist ein Link: BL51-Benutzerhandbuch: OVERLAY Linker Directive (keil.com) Ohne weitere Beschreibung werde ich am Ende des Artikels einige Methoden nennen, die meiner Meinung nach machbar sind, um die beiden oben genannten Situationen zu vermeiden.

Anscheinend hat der oben erwähnte Sonderfall 2 das Problem mit dem E/A-Lesefehler in meinem Projekt verursacht.

Problemüberprüfung

Schauen wir uns noch einmal die Implementierung von io_get() an:

bool io_get(u8 io_number)
{
    
    
    bool status;
    status = get_io_status_by_number(io_number);
    return status;
}

Ich verwende diese Funktion in der Hauptschleife und im Interrupt der Hauptfunktion, aber Keil C51 weist beim Kompilieren nur einen RAM-Speicherplatz für den lokalen Variablenstatus zu, daher kann Folgendes passieren:

  1. Ich rufe io_get in der Hauptschleife der Hauptfunktion auf, um einen bestimmten Status-Pin-Level als Low-Level 0 zu erhalten, und speichere ihn in der lokalen Statusvariablen.
  2. Zu diesem Zeitpunkt wird vor der Rückkehr zum Status der Timer-Interrupt empfangen und der Sprung ausgeführt, um den Interrupt auszuführen.
  3. Im Interrupt wird io_get aufgerufen, um die ADC-Daten zu lesen, und die Statusvariable wird auf einen hohen Pegel 1 geändert.
  4. Verlassen Sie zu diesem Zeitpunkt den Interrupt und kehren Sie zur Ausführung der Hauptfunktion zurück. Die in der Hauptfunktion aufgerufene Funktion io_get gibt den Status zurück.
  5. Da sich der Status während des Interrupts geändert hat, tritt ein „magisches“ Phänomen auf, bei dem der Status-Pin offensichtlich den niedrigen Pegel 0 hat, die Funktion io_get jedoch den hohen Pegel 1 zurückgibt.

Ausweichmethode

Wenn bei Verwendung von Keil C51 eine Funktion sowohl im normalen Hauptfunktionsprozess als auch im Interrupt aufgerufen werden muss, können Sie die folgenden Maßnahmen in Betracht ziehen, um Wiedereintrittsprobleme zu vermeiden:

  1. Verwenden Sie das reentrant-Schlüsselwort von keil C51 für die Funktion, um dem Compiler mitzuteilen, dass diese Funktion reentrant sein wird. Anschließend verwendet der Compiler die herkömmliche Stapelmethode, um den Aufruf dieser Funktion zu verarbeiten. Dies kann jedoch die RAM-Auslastung und den Funktionsaufruf erhöhen Overhead (Push and Pop).
  2. Schalten Sie den Interrupt aus, wenn die Funktion ausgeführt wird, und aktivieren Sie ihn nach Abschluss der Ausführung, um einen erneuten Eintritt zu vermeiden. Dies ist jedoch nicht für Fälle geeignet, bei denen strenge Anforderungen an die Interrupt-Reaktionszeit gestellt werden.
  3. Rufen Sie diese Funktion nicht im Interrupt auf, sondern erweitern Sie den Funktionscode direkt in der Interrupt-Funktion. Dies ist für keinen Anlass geeignet und kann die Codequalität und Wartbarkeit erheblich beeinträchtigen.

Die obige Methode kann möglicherweise den dringenden Bedarf lösen, und vielleicht bleibt noch Zeit, die von keil offiziell gegebene Lösung für die OVERLAY-Linker-Richtlinie zu untersuchen. . . . . .

Supongo que te gusta

Origin blog.csdn.net/lczdk/article/details/125454711
Recomendado
Clasificación