Warum ist loop langsamer wie while(1) ? max Toggle Takt

Hallo,

ich spiele gerade mal wieder mit Pin toggeln rum, um den max. Takt herauszubekommen.

Dabei fiel mir auf, dass man mit cbi/sbi oder Portzugriff "nur" 1MHz erreicht.
Wenn man das aber nochmal zusätzlich in while einpackt, werden es 4MHz.

Ich dachte loop ist eine while(1) ?

Wie macht dass die SPI Lib so nebenbei in der loop das sie ihre 4MHz Clock erreicht, wenn ich solche Klimmzüge machen muß? Schaltet die ISR ab, schickt ihre Takte rein oder raus und schaltet ISR wieder ein?

/*
 Arduino Mega 2560
*/ 

#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))   // setzt das angegebene Bit auf 1
#endif
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))  // setzt (löscht) das angegebene Bit auf 0
#endif

// add this to the top of your sketch
#define NOP __asm__ __volatile__ ("nop\n\t")

void setup()  {

  digitalWrite(13, LOW);  // Port.B.Bit.7
  digitalWrite(37, LOW);  // Port.C.Bit.0
  digitalWrite(40, LOW);  // Port.G.Bit.1
  pinMode(13, OUTPUT);       
  pinMode(37, OUTPUT);      
  pinMode(40, OUTPUT);   
  
  cli();      // Interrupt's ausschalten, derzeit ohne Wirkung
        
}   // Ende setup

void loop(void) {
  
  // 72,7kHz
  //digitalWrite(13, LOW);    // 6,5µs
  //digitalWrite(13, HIGH);   // 7,25µs
  
  
  // 960kHz 
  //cbi(PORTB,7);   // 0,125µs    Pin 13
  //sbi(PORTB,7);   // 0,9167µs   Pin 13
    
  // 960kHz 
  //cbi(PORTG,1);   // 0,125µs    Pin 40
  //sbi(PORTG,1);   // 0,9167µs   Pin 40
  
  // 1MHz
  //PORTB = 0b00000000;   // 0,0833µs    Pin 13
  //PORTB = 0b10000000;   // 0,9167µs    Pin 13
  
  // 1MHz
  //PORTC = 0b00000000;   // 0,125µs   Pin 37
  //PORTC = 0b00000001;   // 0,875µs   Pin 37

  // 4MHz
  while(1)  {
    PORTC = 0b00000000;   // 0,0833µs   Pin 37
    PORTC = 0b00000001;   // 0,1667µs   Pin 37
  } 
  
}   // Ende loop

Das beantwortet wahrscheinlich nicht deine Frage, aber wenn es darum geht einen möglichst schnellen Taktausgang zu haben:

Folgendes erzeugt 8 MHz an Pin 9:

void setup ()
{
    // set up 8 MHz timer on pin 9
    pinMode (9, OUTPUT); 

    // set up Timer 1
    TCCR1A = bit (COM1A0);  // toggle OC1A on Compare Match
    TCCR1B = bit (WGM12) | bit (CS10);   // CTC, no prescaling
    OCR1A =  0;       // output every cycle
}

void loop () { }

(siehe Fastest output possible - #7 by nickgammon - Programming Questions - Arduino Forum)

Hallo,

interessant. Das funktioniert dann aber nur mit Pin 9 beim Uno und Pin 11 beim Mega2560?
Weil man den Timer direkt nutzt?

Doc_Arduino:
Ich dachte loop ist eine while(1) ?

Statt zu denken, könntest Du Dir natürlich auch den Arduino-Quelltext der main() Funktion ansehen, dann dürfte es klarer werden. Im Normalfall ruft die main() Funktion von Arduino aus die loop() Funktion in so einer Schleife auf:

 for (;;) {
   loop();
   if (serialEventRun) serialEventRun();
 }

In dem Fall wird die loop-Funktion durch das SerialEvent Handling ausgebremst. Die if-Abfrage, ob ein SerialEvent-Handler im Sketch verwendet wird, kostet jedesmal etwas Zeit, bevor die loop erneut aufgerufen wird. Das ist ein Humbug, den man bei vernünftiger Programmierung sowieso nie braucht, den aber die Arduino-Macher standardmäßig trotzdem als Bremse in ihre main() Funktion eingebaut haben.

wenn ich solche Klimmzüge machen muß?

Musst du doch gar nicht!

Werfe setup() und loop() aus deiner ino raus.

Ersetze es durch dieses:

#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>

int main(void){
   DDRB |= _BV(PINB5);  //PB5 (Pin13) als OUTPUT 
    while(1){
       _delay_ms(1000);
       PORTB ^= _BV(PINB5);
    }
}

Der der Inahlt der Main ist ein Beispiel, hier aus dem Forum

Interessant. Das funktioniert dann aber nur mit Pin 9 beim Uno und Pin 11 beim Mega2560?
Weil man den Timer direkt nutzt?

Wenn ich das richtig verstehe (bin ja nur Dilettant) dann braucht es "den richtigen" Timer plus Compare Match. Das geht nur mit "ausgewählten Pins".

Weiter unten in dem erwähnten Beitrag zeigt Nick Gammon auch wie verschiedene C-Code-Schnippsel vom Compiler in Assembler-Code umgesetzt werden und wieviele Tacktzyklen das braucht.

Hallo,

hab gelesen was Nick so getestet hat. Hat sich viel Arbeit gemacht. Danke übrigens uxomm für den Link und Hinweis.

Wenn man serialEventRun rauswirft, funktioniert die serielle nicht mehr. Wie machst Du das dann?
Und wegen dem Quellcode lesen. In dem Unterordner finde ich mich schwer zurecht.

Das hier macht nur 1,6MHz

/*
 Arduino Mega 2560
*/ 

#define F_CPU 16000000UL
#include <avr/io.h>

int main(void){
   DDRB |= _BV(PINB7);  //PB7 (Pin13) als OUTPUT
    while(1){
      PORTB ^= _BV(PINB7);
    }
}

Hallo,

ohne den serialEventRun Aufruf komme ich auf 1,14MHz statt auf 1MHz. Ist also nicht der Bringer.

// in der loop
// 
  PORTB = 0b00000000;   // Pin 13
  PORTB = 0b10000000;

Hallo,

dann habe ich noch von PINx gelesen und 2,x MHz.

Mit while 2,6MHz und ohne while 615kHz.

Was ich aber wirklich nicht verstehe ist wie das PINB hier wirkt. Ich meine der Befehl ist doch zum lesen eines Registers da und nicht zum schalten. Trotzdem taktet der Pin 13 aber die LED leuchtet nicht.

/*
 Arduino Mega 2560
*/ 

void setup()  {
  digitalWrite(13, LOW);  // Port.B.Bit.7
  pinMode(13, OUTPUT);       
 
  cli();      // Interrupt's ausschalten, derzeit ohne Wirkung
        
}   // Ende setup

void loop(void) {

  // 2,667MHz
  while(1)  {
    PINB = bit (7);   // 0,2083/ 0,1667µs   Pin 13
  } 
  
}   // Ende loop

PINB = bit (7); // 0,2083/ 0,1667µs Pin 13

Was soll das bewirken? Hier kannste eigentlich dann auch direkt auf pinMode verzichten.

DDRB |= 1<<7; // Als Ausgang

void loop(void)
{
PORTB ^= 1<<7; // togglen
}

Hallo,

das ist ja meine Frage, wie das funktionieren soll. Es funktioniert aber, der Pin taktet ja. Ohne das die LED leuchtet. Die LED muß aber laut Schaltplan immer leuchten. Egal ob das Ding Eingang oder Ausgang ist. Das Ding hängt direkt auf dem OPV.

PORTB ^= 1<<7; // toggeln
bringt 571kHz. in while 1,6MHz

Das ist langsamer weil erst das Bit immer um 7 Stellen geschoben werden muß. Deshalb wird das direkte
PORTB = 0b00000000; // 0,0833µs Pin 13
PORTB = 0b10000000; // 0,9167µs Pin 13
schneller sein.

Edit:
das liegt nicht am Bit schieben.
PORTC ^= 1<<0; // toggeln
bringt auch nur 533kHz

Wenn Du es ganz genau wissen willst schau mal hier: Faster Counter | Blinkenlight

Das hier macht nur 1,6MHz

Das macht ziemlich genau 10 Takte, für die While und das Port toggeln....
Ich finde das schon recht gut!

Im Gegensatz zu digitalWrite, was alleine schon irgendwas um 160 Takte nimmt.

Das ist langsamer weil erst das Bit immer um 7 Stellen geschoben werden muß.

Nein!
Den konstanten Ausdruck "1<<7" wertet der Compiler aus und schreibt das Ergebnis in den Code.

Doc_Arduino:
Das ist langsamer weil erst das Bit immer um 7 Stellen geschoben werden muß.

Schau dir mal den Assembler Code dazu an. Da wird nichts geschoben. Der Compiler setzt das in völlig andere und kürzere Assembler Befehle um

Der ganze Kram mit |=(...) und &=(...) wird z.B. in sbi und cbi umgesetzt. Ein OpCode mit 2 Takten Ausführungszeit

Aus dem XOR werden dagegen 3 Assembler Befehle :frowning:
Er liest erst mal das Register ein, macht dann eine Substraktion und schreibt den Wert wieder raus

Mit Standard-Mitteln toggelt man einen Pin am schnellsten wenn man eine 1 auf das Eingangs Register PINx schreibt

Und dass Schleifen kurzen Code ausbremsen ist schon seit Ewigkeiten bekannt. Auch wenn sie effizient sind. Deshalb wurde loop unrolling erfunden.

Hallo,

naja schon, aber warum funktioniert PINx als Ausgang, wenn ich doch damit immer das Register lese?
Außerdem bringt das nur 615kHz, nur in eigener while macht das 2,6MHz. Letzteres macht aber praktisch keinen Sinn in einem Sketch wo noch andere Dinge passieren sollen.

da ist man mit dem hier schneller, 1MHz.
PORTB = 0b00000000; // 0,0833µs Pin 13
PORTB = 0b10000000; // 0,9167µs Pin 13
und das in einer eigenen while wären 4MHz.

das PINx ist demnach nicht so effektiv wie gedacht.

Das ist eine Sonder-Funktion die im Datenblatt steht (wenn auch sehr leicht zu übersehen) und genau für diese Zwecke existiert. Normalerweise würde man nie auf dieses Register schreibend zugreifen. Also konnte man das für etwas anderes missbrauchen.

Atmega328 Datenblatt Seite 77. Punkt 13.2.2

Dein Problem ist dann eben noch der riesige Overhead durch die Schleife. Da sollte man etwas loop unrolling betreiben damit die Schleife nicht ständig ausgeführt wird. Das ist genau was Udo bei "Fast Counter" gemacht hat.

Ach ja, und weil du wegen der SPI Lib gefragt hast: das wird da in Hardware erledigt. Man muss nur ein Register mit Daten füllen und der Rest geht automatisch. Es gibt auch Software SPI Implementierungen. Die sind dann entsprechend langsamer.

Hallo,

das was Udo macht verstehe ich nicht. Ist mir 2 Nummern zu hoch. Sind nur wilde Zahlen für mich. Sorry.

Okay, das mit dem PINx ist ein Trick. Aber das bringt nur 615kHz, nur in eigener while macht das 2,6MHz. Letzteres macht aber praktisch keinen Sinn in einem Sketch wo noch andere Dinge passieren sollen.

da ist man mit dem hier schneller, 1MHz.
PORTB = 0b00000000; // 0,0833µs Pin 13
PORTB = 0b10000000; // 0,9167µs Pin 13
und das in einer eigenen while wären 4MHz.

das PINx ist demnach nicht so effektiv wie gedacht.

Ich meinte "Fast Counter". Nicht "Faster Counter":

Da wird das genauer erklärt:

Now to the last and most aggressive of the optimizations – the COUNT macros. These macros are an “unrolled loop”. Instead of toggling the pins in some loop I have the macro processor output the toggle statements to the compiler.

Das kann man auch auf andere Arten erledigen je nach Anwendung.

Aber der Punkt ist dass Schleifen langsam sind. Auch ein jmp kostet Zeit. Selbst dein Sprung genauso lange dauert wie deine Anweisung verplemperst du immer noch 50% der Zeit mit Springen. Es ist also schneller die Toggle Befehle x mal hintereinander hinzuschreiben, weil man dann mehr Zeit mit Toggeln verbringt und weniger mit Springen.

Hallo,

okay mit dem weniger springen und dadurch Zeit sparen habe ich verstanden.
Was ich aber immer noch nicht gerafft habe ist, warum beim schreiben mit PINx der Pin taktet?
Ich schreibe doch immer eine 1 rein, warum taktet der dann?

@ jurs:
was ist mit dem SerialEventRun? Wenn du das rausnimmst, wie bekommst Du die serielle zum laufen?
Würde mich der Vollständigkeit interessieren.

Die ganzen Übungen mit dem maximal erzielbaren Takten bringen mich zur Erkenntnis, dass ich meinen geplanten Frequenzgenerator mit einem 16Bit DAC (TI8830) per SPI vergessen kann. Weil das alles verpufft. Wegen den 2x 16 Takten. Selbst wenn ich den mit 8MHz SPI Takt optimal ohne Verzögerungen ansteuern kann, kommen am Ende theoretisch nur 250kHz hinten raus. Für den Fall das ich ihn als Rechteckgenerator Zweck entfremde.
Wenn ich dann Sinusfrequenzen ausgeben möchte, lande ich ganz schnell im Hz Bereich.
Ich dachte ich kann mit einem 16Bit DAC besonders schöne Kurven ausgeben. Habe aber nicht an die zeitliche Verzögerung gedacht.

Das bedeutet, optimal ist immer noch ein 8Bit Register vom µC selbst und ein R2R rangebammelt.
Zwei 8Bit Register kombinieren wird nicht funktionieren.
Gibt es überhaupt µC mit herausgeführten 16Bit Ports?

Was ich aber immer noch nicht gerafft habe ist, warum beim schreiben mit PINx der Pin taktet?
Ich schreibe doch immer eine 1 rein, warum taktet der dann?

Weil das von Atmel so in der Hardware implementiert wurde. Damit man einen Pin mit einem OpCode toggeln kann ohne den Zustand vorher auszulesen.

Der Schreibzugriff auf das PINx Register war durch nichts belegt. Das hat sich also angeboten, da man nicht noch ein "Toggle Register" oder was ähnliches anlegen wollte.