Helfen Sie beim Verständnis des AVR-Ausführungstimings

Ich arbeite mit einem Atmel ATMEGA32U4-Mikrocontroller - Datenblatt hier mit einem 16-MHz-Quarz für die Systemuhr.

Nach meinem Verständnis verfügt dieser Chip über eine werkseitig programmierte Sicherung „Takt durch 8 teilen“, wodurch mein Systemtakt effektiv auf 2 MHz eingestellt wird. (Seite 348 des Datenblatts. CKDIV8 (Bit 7) Standardwert ist 0, programmiert).

Ich möchte meine Systemtaktgeschwindigkeit bestätigen, also habe ich einen Code geschrieben, um einen Pin auf Low auszugeben, einen Taktzyklus zu verzögern und den Pin dann wieder auf High zu setzen. Das Messen der Zeit, in der der Pin niedrig ist, sollte einem Taktzyklus entsprechen.

Hier ist der Code, den ich verwendet habe, um dies zu erreichen:

//Set PORT E as output
DDRE = 0xFF;

asm volatile("nop\n\t"::); 
PORTE |= 1<<2;  

asm volatile("nop\n\t"::); 
PORTE &=~ (1<<2);   

asm volatile("nop\n\t"::); 
PORTE |= 1<<2;  

asm volatile("nop\n\t"::); 
PORTE &=~ (1<<2);

Ein 'nop' entspricht einem Taktzyklus, gemäß AVR-Befehlssatzhandbuch , Seite 108.

Basierend auf diesen Informationen würde ich davon ausgehen, dass eine 'nop'-Anweisung 500 Nanosekunden dauert. Ist diese Annahme richtig? (16 MHz/8 = 2 MHz. 1 2 M H z = 500ns)

Hier ist ein Scope-Plot meiner Ergebnisse:Geben Sie hier die Bildbeschreibung ein

Es scheint, dass eine 'nop'-Ausführungszeit nur 200 ns (5 MHz) beträgt. Ich würde erwarten, dass es einen zusätzlichen Overhead gibt, wenn C verwendet wird, um den Port hoch / niedrig zu setzen, also sind 500 ns tatsächlich die Mindestzeit, die ich erwarten würde, dass der Pin niedrig ist . Aber wie Sie an meinen Messcursoren 'a' und 'b' sehen können, sind es nicht einmal annähernd 500 ns.

Kann jemand erklären, was los ist?
Gibt es einen Fehler in meiner Methode?
Übersehe ich etwas 'face-palming-ly' Dummes? :p
Danke für jede Hilfe!

Wie kompilieren Sie diesen Code? Selbst mit volatilewird GCC diese Assembler-Anweisungen immer noch per gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html verschieben , wenn Sie die Optimierung aktiviert haben (alles andere als -O0).
Kompiliert mit AVR GCC, optimiert für Größe (-Os), was anscheinend die Standardeinstellung ist. Ich werde sehen, was das Ausschalten der Optimierung bewirkt.
Das Kompilieren und anschließende Zerlegen Ihres Codes in meinem Kopf führt dazu, out DDRE, 0xFF; nop; sbi PORTB, 2; nop; cbi PORTB, 2; nop; sbi PORTB, 2; nop; cbi PORTB, 2;dass, da dies alles 1-Zyklus-Anweisungen sind, die Zeit zwischen Ausgabeänderungen 2 Zyklen betragen sollte (einer für die nop, einer für sbi/ cbi). Ihr Compiler optimiert jedoch möglicherweise das nops weg oder ist nicht schlau genug, um sbiund zu verwendencbi

Antworten (3)

Das ist ein schlechter Ansatz. Idealerweise messen Sie eine sehr große Anzahl von NOP-Befehlen – sagen wir eine Million – statt nur einem. Ich denke, das ist die erste Dummheit, die ich finden kann. Ich wäre nicht sehr überrascht, wenn sich Ihre Nummer direkt nach der Implementierung dieser Änderung ändert.

Können Sie auch die Disassemblierung Ihres C-Codes anzeigen? Ich würde nicht einmal versuchen, das Timing zu beurteilen, ohne genau zu sehen, welcher Assemblercode aus meinem C-Code generiert wurde. Ich würde versuchen, von der Baugruppe rückwärts zu arbeiten, um zu berechnen, wie die Ausführungszeit sein sollte (über das von Ihnen gefundene Handbuch für den Befehlssatz). Auf diese Weise können Sie sehen, ob die Oszilloskopmessung ein wenig oder viel daneben liegt. Ein wenig könnte bedeuten, dass Sie einen Rechenfehler gemacht haben. Vieles könnte bedeuten, dass eine Ihrer Annahmen falsch ist.

Ich vermute, ich möchte eine Zahl wie eine Million, um die Taktstabilität (Teile pro Million) zu berücksichtigen? Auch ein guter Vorschlag, sich die Demontage anzusehen. Ich weiß noch nicht, wie ich das machen soll, aber ich werde es herausfinden. Dann kann ich sicher sehen, welche Anweisungen ausgeführt werden.
@dext0rb Ja, das ist ein guter Grund. Ein weiterer Grund ist, dass die Befehle zum Umschalten des externen DIO ungefähr die gleiche Zeit benötigen wie ein NOP, aber nicht annähernd die gleiche Zeit wie eine Million. Sie werden unbedeutend sein.
Vielen Dank an alle, ich wünschte, ich könnte alle Ihre Antworten akzeptieren, da sie alle sehr gut waren, aber diese hat mich dazu gebracht, nachzudenken und tiefer in die Versammlung einzutauchen. Ich bin dem Vorschlag von @noah1989 gefolgt, zu 'Bare Metal' zu gehen, und es hat sehr geholfen. Ich habe 100 NOP hintereinander gemacht und es entspricht 50uS, was für mich Sinn macht.

Ich habe vor einiger Zeit verschiedene Uhreinstellungen eines ATtiny45 mit dem folgenden C-Code überprüft. Obwohl die While-Schleife nicht perfekt ist, sind die Verzögerungsroutinen ziemlich gut mit avr-gcc-Bibliotheken optimiert. Ich nenne das Programm '1kHz.cpp'. Überprüfen Sie anhand des Datenblatts, welcher genaue Pin die Blockwelle ausgibt.

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
  DDRB = 0x10;
  PORTB = 0x00;

  while (1)
  {
    PORTB = 0x00; _delay_us(500);  // pin low, then wait
    PORTB = 0x10; _delay_us(500);  // pin high, then wait
  }
}

Der Trick bei der Uhr besteht darin, den Code mit der richtigen Uhreinstellung zu kompilieren. Unter Linux sieht es so aus, aber Windows sollte einigermaßen ähnlich sein. Wie Sie sehen, hat der ATtiny45 eine werkseitige Standarduhr von 1 MHz.

freq=8000000/8
baud=19200
src=1kHz.cpp
avr=attiny45
port=/dev/ttyUSB1

# Compile
avr-gcc -g -DF_CPU=$freq -Wall -Os -mmcu=$avr -c -o tmp.o $src
# Link
avr-gcc -g -DF_CPU=$freq -Wall -Os -mmcu=$avr -o tmp.elf tmp.o
# Convert to Intel .hex (required for my programmer)
avr-objcopy -j .text -j .data -O ihex tmp.elf tmp.hex
# Program the device
avrdude -p $avr -c stk500v1 -P $port -b $baud -v -U flash:w:tmp.hex

Übrigens kann das Experimentieren mit der Uhreinstellung Ihr Gerät blockieren, also seien Sie vorsichtig, welche Sicherungen Sie setzen/deaktivieren. Unbricking kann nur mit einem Hochspannungsprogrammierer (der mit einem Ersatz-Arduino gemacht werden kann) durchgeführt werden.

Wenn Sie Timing-Analysen auf der Ebene einzelner Anweisungen durchführen, müssen Sie sich unbedingt die Disassemblierung Ihres Programms ansehen oder es überhaupt erst in Assembler schreiben. Sie können dann die genauen Dauern aus dem Datenblatt entnehmen, indem Sie die Zyklen für jede Anweisung zählen:

Instruction_clocks

Wenn Sie jedoch nur überprüfen möchten, ob die Haupttaktfrequenz korrekt ist, gibt es eine einfachere Lösung: Erzeugen Sie einfach eine Niederfrequenzausgabe. (0,1 Hz bis einige kHz, je nachdem, was Sie zum Messen verwenden, eine blinkende LED und eine Stoppuhr könnten ausreichen.) Sie könnten dafür einen der 16-Bit-Timer verwenden oder einfach eine Schleife mit einem langen Busy- warten, z _delay_us(). Die Zeit, die für die Sprünge der Schleife aufgewendet wird, wird vernachlässigbar sein.