PIC16 Timer0 Kuriosität

Mit einem PIC16F886 versuche ich, alle 100 Millisekunden Interrupts mit TMR0 zu erzeugen, die vom internen Oszillator getaktet werden, aber ich bekomme ein wirklich seltsames Verhalten.

Dies ist eine batteriebetriebene Schaltung, daher verwende ich den internen 125-kHz-Oszillator, ausgewählt über:

OSCCON = 0b00010000; // select 125 KHz Int. Osc. = IRCF<2:0>=001

Den Prescaler weise ich dann TMR0 zu und setze einen Prescaler-Wert von 1:2:

T0CS = 0;   // TMR0 Clock Source: Internal instruction cycle clock (FOSC/4)
PSA = 0;    // Prescaler is assigned to TMR0
PS2 = 0;
PS1 = 0;    // > TMR0 Rate: 1:2;
PS0 = 0; 

Nach meinen Berechnungen sollte also jeder "Tick" ((1/125 000) / 4) / 2 = 1.0 × 10^-6Sekunden dauern. Wenn ich den Timer mit 155 vorlade, dauert es 100 "Ticks", bis er überläuft, und alle 100 uS einen Interrupt erzeugen.

Meine Interrupt-Service-Routine besteht aus:

if(T0IE)
{
    ticks++;
    if (ticks >= 999){
        ticks = 0;

        PORTB = ~PORTB;
    }

    TMR0 = 155; 
    return;     
}

Und es funktioniert, aber das Timing stimmt nicht ganz.

Wenn ich es mit der MPLAB SIM simuliere, dauert es etwa 85 ms und auf echter Hardware scheint es länger als 100 ms zu dauern.

Die vollständige Codeliste finden Sie hier: http://pastebin.ca/1928766

Es ist durchaus möglich, dass ich etwas falsch berechnet habe, daher wären Hinweise/Korrekturen sehr willkommen.

Antworten (5)

Ich glaube, Ihre Berechnungen mischen Häufigkeit und Zeiträume.

1 / 125000 Hz entspricht einer Periode von 8 us. FOSC/4 teilt die Frequenz weiter nach unten (oder multipliziert die Periode). Sie können eine Frequenz nicht ohne spezielle Schaltungen multiplizieren. Der Prescaler teilt auch die Frequenz oder multipliziert die Periode. Anstatt also die 8 us durch 4*2 zu teilen, müssen Sie sie mit 8 multiplizieren: 8 * 4 * 2 = eine Periode von 64 us oder das 64-fache dessen, was Sie dachten.

Leider lässt sich 1000 nicht gleichmäßig durch 64 teilen, sodass Sie keinen genauen 1-ms-Interrupt erhalten können. Stattdessen sollten Sie wahrscheinlich eine Taktfrequenz von 1 MHz (OSCCON = 0b01000000) für einen Zeitraum von 1 us verwenden. Dann entspricht 8 * 1 us 8 us, und Sie können eine Taktvoreinstellung von 256-125 = 134 oder 0x86 verwenden und alle 1 ms einen Interrupt erhalten. Zählen Sie alle 100 davon für Ihr 100-ms-Timing.

Wenn der Stromverbrauch ein Problem darstellt, können Sie alternativ die Taktfrequenz auf 250 KHz einstellen (nur das Doppelte dessen, was Sie verwendet haben) und alle 4 ms einen Interrupt erhalten. Zählen Sie dann 25 davon für 100-ms-Timing.

(Ich habe ganz vergessen, dass ich das hier gepostet habe, sorry!) Danke für deine Antwort, kurz nachdem ich die Frage gestellt habe, habe ich den dummen Fehler bemerkt :)

Sie haben recht, jeder T0-Tick ist 64 us. In 100 ms gibt es 1.562,5 T0-Ticks. Jeder T0-Überlauf dauert 256 Ticks, also gibt es 6 Überläufe. Dann entspricht T0-Tick Nummer 26,5 Ihrer 100. Millisekunde.

if (T0IE&&T0IF)
{
    // clear interrupt flag
    T0IF=0;
    ticks++;
    if (ticks == 6)
    {
        // Prepare fractional overflow of 26 ticks
        TMR0 = (256 - 26);
    }
    else if (ticks == 7)
    {   
        ticks = 0;
        PORTB = ~PORTB;
    }   

    // don't need return
}

Ich bekomme dasselbe mit der Mathematik wie Tcrosley - Sie können die Periode für einen Taktteiler nicht durch 4 teilen. Sie multiplizieren die Periode (geteilte Frequenz == multiplizierte Periode). Müssen Sie auch das Interrupt-Flag in der Serviceroutine löschen? Ich sehe nicht, dass du es tust, wenn du es tust.

Ich habe gerade ein weiteres Problem bemerkt, das mir vorher nicht aufgefallen war: Sie überprüfen oder löschen TMR0IF in Ihrer Interrupt-Routine weder. So wie der PIC arbeitet, wird GIE jedes Mal, wenn er im Begriff ist, einen Befehl auszuführen, gesetzt, und jedes periphere Interrupt-Flag wird zusammen mit seiner entsprechenden Freigabe (z. B. TMR0IF und TMR0IE) gesetzt, er löscht GIE und ruft die Interrupt-Routine auf. Durch das Löschen von GIE können die Anweisungen innerhalb der Interrupt-Routine ausgeführt werden (andernfalls würde es jedes Mal, wenn das System im Begriff war, die zweite Anweisung der Interrupt-Routine auszuführen, einen weiteren Aufruf der ersten Anweisung generieren). Die Rückkehr von der Unterbrechung setzt GIE zurück. Wenn zu diesem Zeitpunkt immer noch ein peripheres Interrupt-Flag zusammen mit seiner entsprechenden Freigabe gesetzt ist, löscht das System erneut GIE und erzeugt einen Aufruf an die Interrupt-Routine.

Es gibt zwei Strategien, mit denen eine Interrupt-Routine dieses Problem lösen kann:

  1. Wenn mehr als ein Interrupt aktiviert werden kann, sollte die Interrupt-Routine das Flag auf einen von ihnen überprüfen und, wenn das Flag gesetzt ist, dieses Flag löschen und die Bedingung behandeln, die es darstellt. Nachdem festgestellt wurde, dass das erste Flag gelöscht ist oder sein Zustand behandelt wurde, prüfen Sie das Flag für den zweiten Interrupt und löschen Sie, falls gesetzt, dieses Flag und behandeln Sie seinen Zustand. Wiederholen Sie dies für alle Interrupts, die Sie verwenden. Wenn ein unerwarteter Interrupt aktiviert wird, wird der Hauptleitungscode niemals ausgeführt, aber Interrupts verhalten sich normal (immer wenn keine erwartete Interrupt-Bedingung vorhanden ist, prüft die Interrupt-Routine kontinuierlich alle Interrupt-Bedingungen, kehrt zurück und startet neu, bis eine erwartete Interrupt-Bedingung auftritt ).
  2. Wenn es nur einen Interrupt gibt, der jemals aktiviert wird, kann man die Bedingungsprüfung überspringen und das Flag einfach bedingungslos löschen, wenn die Bedingung eintritt. Beachten Sie, dass, wenn ein anderer Interrupt irgendwie aktiviert wird, der Code die Interrupt-Routine fälschlicherweise "so schnell wie möglich" ausführen kann, ohne Rücksicht darauf, wie oft der Code tatsächlich ausgeführt werden sollte. Wenn eine solche fehlerhafte Ausführung ein Sicherheitsrisiko darstellen würde, muss man sich dagegen wehren. In vielen Fällen ist jedoch die Tatsache, dass der Hauptcode nicht ausgeführt werden kann, ein so großes Problem (was hoffentlich zu einem eventuellen Watchdog-Reset führt), dass es nicht wirklich wichtig ist, was die Interrupt-Routine tut.

Versuchen Sie, die erste Zeile Ihres Interrupt-Codes in "if (T0IF && T0IE)" zu ändern, und fügen Sie der Bedingungsklausel "T0IE=0" hinzu. Das sollte dazu führen, dass Ihre Interrupt-Frequenz näher an die Korrektheit herankommt. Es wird jedoch nicht ganz richtig sein, es sei denn, Sie verwenden so etwas wie den Code in meiner anderen Antwort.

Um mit Timer 0 ein genaues Timing zu erhalten, muss das Teilungsverhältnis entweder kleiner als etwa 259 sein oder andernfalls eine Potenz von zwei sein. Aufgrund der unglücklichen Entscheidung von Microchip, das Schreiben von TMR0 nicht zuzulassen, ohne den Preskalar zu löschen, ist es unmöglich, genaue Timings zu erhalten, wenn TMR0 mit aktiviertem Preskalar eingestellt wird.

Um genaue Zeiten mit "kurzen" Timer-Intervallen zu erreichen, muss man nur hinzufügen

  TMR0 += (259-Intervall);

Vorausgesetzt, der Code generiert eine "ADDWF _TMR0,f"-Anweisung (wie es bei jedem typischen Compiler der Fall sein sollte), sollte er eine genaue Zeitgeberrate von "Intervall"-Taktzyklen ergeben.

Wenn eine Interrupt-Aktion mit einer Rate langsamer als einmal alle 259 Zyklen ausgeführt werden muss, ist dies keine Zweierpotenz, und wenn die Interrupt-Aktion mehr als 256 Zyklen dauern könnte, fügen Sie vor dem Standard-Interrupt-Handler ein wenig Assembler-Sprache hinzu kann „genau das Richtige“ sein. Für die beste Effizienz erfordert dieser Ansatz eine Nicht-Bank-Variable, um den Ausführungszähler zu halten. Fügen Sie vor dem Interrupt-Handler Folgendes ein:

; Das Beispiel geht davon aus, dass der Code alle tausend Zyklen einmal ausgeführt werden soll
  bcf _INTCON,2 ; TMR0IF
  decfsz _intCounter,f ; Muss im unbanked RAM sein!
   refie
  ... Als normaler Interrupt-Handler erwiesen. Dann im C-Code:
  TMR0 += (3+24); /* Drei Zyklen für RTCC 'Verlust'; 24 Zyklen vorrücken;
                     also führen wir alle 1000 Zyklen einmal den Hauptinterrupt aus
                     statt einmal alle 1024 */
  GIE = ​​1; /* Interrupts sind jetzt okay */
  ... Führen Sie den Hauptunterbrechungscode aus; am Ende des Interrupts:
  intZähler += 4;
  /* Beachten Sie, dass, wenn der Timer-Tick-Interrupt mehr als dreimal ausgelöst wird, während
     Durch Ausführen der Hauptunterbrechungsroutine wird intCounter Null oder größer sein
     als vier. Dieser Code ignoriert diese Möglichkeit. */

Beachten Sie, dass dieser Code verschachtelte Interrupts zulässt, aber die "schnellen" Interrupts stören weder die Flags noch stören sie andere Register als _intCounter. Folglich besteht keine Notwendigkeit, Register zu sichern/wiederherstellen, und es gibt keine Wiedereintrittsprobleme, wenn die "schnellen" Timer-Tick-Interrupts auftreten, während die Haupt-Timer-Tick-Routine läuft. Probleme treten nur auf, wenn vier oder mehr Timer-Tick-Interrupts auftreten, während der Haupt-Timer-Tick-Code ausgeführt wird.