Geschwindigkeit von shiftOut

Hallo,

ich habe in Verbindung mit einem Shiftregister zum ersten Mal den Befehl shiftOut verwendet und frage mich, ob dieser Befehl ebenso wie delay den Code blockieren kann.

Ich wundere mich darüber, wie schnell das entspr. Byte an das Shiftregister übergeben wird und vor allem, dass das Shiftregister dies immer richtig interpretiert, auch wenn ich kurz hintereinander (20ms) "Pakete" schicke.

Äh- die Frage steckt im ersten Textblock. :slight_smile:

Gruß Chris

Du kannst ja mal eine entsprechende Laufzeitmessung machen, indem du bspw. 10.000 Bytes ins Schieberegister shiftest und die Differenz der Start- und Endzeit ausgibst. :wink:
Im Datenblatt zum SR findest du auch Angaben, wie schnell das IC angesteuert werden darf, das liegt typischerweise im MHz-Bereich, also wesentlich schneller als der Arduino das über Shiftout ausgeben kann.

Echt fett!

Läuft ja somit praktisch gefühlt in Echtzeit. Bin begeistert.

Gruß Chris

Aus der "wiring_shift.c" kann man entnehmen, dass der Code so wie ist bereits am Maximum der Geschwindigkeit ist:

void shiftOut(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder, uint8_t val)
{
	uint8_t i;

	for (i = 0; i < 8; i++)  {
		if (bitOrder == LSBFIRST)
			digitalWrite(dataPin, !!(val & (1 << i)));
		else	
			digitalWrite(dataPin, !!(val & (1 << (7 - i))));
			
		digitalWrite(clockPin, HIGH);
		digitalWrite(clockPin, LOW);		
	}
}

Man kann es bestimmt noch schneller machen, wenn man auf den Port direkt zugreift und das If weglässt, da man immer gleich auf das Shiftregister schreibt. Oder gleich die for-Schleife inline macht.

Frage an die Profis: Warum zwei "!" das ist doch dann not(not(...))? Die Arduino-Entwickler haben sich da bestimmt was dabei gedacht...

Wenns schneller sein muß dann kannst Du auch die SPI-Schnittstelle verwenden.
Grüße Uwe

MGOS:
Aus der "wiring_shift.c" kann man entnehmen, dass der Code so wie ist bereits am Maximum der Geschwindigkeit ist:

Nunja......
Direkter port zugriff waere viel schneller. Sogar eine einfachere Methode, z.B. "digitalWriteFast" zu benutzen, waere auch schon maechtig schneller.

Frage an die Profis: Warum zwei "!" das ist doch dann not(not(...))? Die Arduino-Entwickler haben sich da bestimmt was dabei gedacht...

Das nimmt irgenwelche Eingaben und konvertiert die um so das nur 0 (LOW) oder 1 (HIGH) ausgegeben wird.

Zum Beispiel
!1 = 0; !0 = 1; also !!1 = 1
!5 = 0; !0 = 1; also !!5 = 1
!0 = 1; !1 = 0; also !!0 = 0

Nach "wiring_digital.c" nimmt digitalWrite einen uint8_t als val, der wiederum mit einer if-else konstruktion zu HIGH/LOW ausgewertet wird.

void digitalWrite(uint8_t pin, uint8_t val)
{
	uint8_t timer = digitalPinToTimer(pin);
	uint8_t bit = digitalPinToBitMask(pin);
	uint8_t port = digitalPinToPort(pin);
	volatile uint8_t *out;

	if (port == NOT_A_PIN) return;
	if (timer != NOT_ON_TIMER) turnOffPWM(timer);

	out = portOutputRegister(port);

	uint8_t oldSREG = SREG;
	cli();
	if (val == LOW) {   //Hier!
		*out &= ~bit;
	} else {
		*out |= bit;
	}
	SREG = oldSREG;
}

Und braucht dafür etwa 56 Taktzyklen:
http://www.billporter.info/2010/08/18/ready-set-oscillate-the-fastest-way-to-change-arduino-pins/

Der direkte Zugriff auf den Pin braucht 2 or 3 Takte, je nach Methode.

hmmmm
Hab mal probiert sowas zusammenzubasteln:

#define PORT PORTB
#define DDR  DDRB
#define BitMaskCLK 0b00000010  //pin 9 = Clock Pin
#define BitMaskDAT 0b00000001  //pin 8 = Data Pin

void shiftOutFast(uint8_t val, uint8_t bitOrder){
  DDR |= BitMaskCLK | BitMaskDAT;
  uint8_t i;
  if (bitOrder = LSBFIRST){
    for (i = 0; i < 8; i++){
      if (val & 0b1)
        PORT |= BitMaskCLK | BitMaskDAT;
      else 
        PORT = (PORT &~BitMaskDAT) | BitMaskCLK;
      val >>= 1;
      PORT &= ~BitMaskCLK;
    }
  }
  else {
    for (i = 0; i < 8; i++){
      if (val & 0b10000000)
        PORT |= BitMaskCLK | BitMaskDAT;
      else 
        PORT = (PORT &~BitMaskDAT) | BitMaskCLK;
      val <<= 1;
      PORT &= ~BitMaskCLK;
    }
  }
}

Die beiden Pins müssen auf dem gleichen Port liegen (sonst wären noch kleine Änderungen nötig), aber sonst sollte das funktionieren.

Das BV-Makro wäre da auch eine Option:
http://194.81.104.27/~brian/microprocessor/BVMacro.pdf

Dann muss man nur die Bit-Nummer definieren und nicht die ganze Maske

Und der schnellste Weg ein Bit zu toggeln (für CLK) ist eine 1 auf das Port-Eingangsregister PINx zu schreiben (Datenblatt, Seite. 77). Das ist einen Takt schneller als ein XOR. Wobei das hier vielleicht 1 oder 2 Takte spart. Ist also etwas extrem :slight_smile:

MGOS:
hmmmm
Hab mal probiert sowas zusammenzubasteln:

Da man bestimmt schon weiss ob LSB oder MSB zuerst geschickt werden sollten, waeren zwei functionen besser.
Und statt eine variable zu verschieben, und mit einer anderen zu zaehlen, wuerde ich es so machen (nicht getested):

void shiftOutFasterLsb(const byte & val)
{
    DDR |= BitMaskCLK | BitMaskDAT; // Move to setup() in actual program

    byte bit = 1;
    while(bit)
    {
        PORT |= BitMaskCLK | ((val & bit) ? BitMaskDAT : 0);
        PORT ^= BitMaskCLK;
        bit <<= 1;
    }
}

Serenifly:
Und der schnellste Weg ein Bit zu toggeln (für CLK) ist eine 1 auf das Port-Ausgangsregister PINx zu schreiben (Datenblatt, Seite. 77). Das ist einen Takt schneller als ein XOR. Wobei das hier vielleicht 1 oder 2 Takte spart. Ist also etwas extrem :slight_smile:

Hmmm, dachte PINx ist "read-only"?
Welches Datenblatt? Atmel328p Blatt geht nur bis Seite 26 :smiley:

Die 448 Seiten Version:

Es stimmt dass die PINx Register laut Seite 92 Read-Only sind, aber das scheint eine etwas verstecke Funktion zu sein.

Wobei "&= ~" auch nur 2 Takte braucht und das OR wahrscheinlich einen Takt. Da man XOR zweimal machen müsste wäre das wohl auch nicht besser. Hauptsache man hat erst mal das writeDigital() weg. Da bis auf den letzten Takt zu optimieren ist nicht unbedingt sinnvoll.

@int2str: das ist natürlich noch besser :slight_smile:
Es könnte aber ein Fehler drin sein: PORT |= BitMaskCLK | ((val & bit) ? BitMaskDAT : 0);
Der Datenpin kann mit Or nie zurückgesetzt werden.

Serenifly:
Die 448 Seiten Version:
http://www.atmel.com/Images/doc8161.pdf

Es stimmt dass die PINx Register laut Seite 92 Read-Only sind, aber das scheint eine etwas verstecke Funktion zu sein.

Wobei "&= ~" auch nur 2 Takte braucht und das OR wahrscheinlich einen Takt. Da man XOR zweimal machen müsste wäre das wohl auch nicht besser. Hauptsache man hat erst mal das writeDigital() weg. Da bis auf den letzten Takt zu optimieren ist nicht unbedingt sinnvoll.

Danke! Wieder was dazu gelernt :slight_smile:
Und ich stimme auch voll zu das man dass nicht ueber-optimieren muss - macht aber Spass :smiley:

MGOS:
@int2str: das ist natürlich noch besser :slight_smile:
Es könnte aber ein Fehler drin sein:
Der Datenpin kann mit Or nie zurückgesetzt werden.

Adleraugen! :slight_smile:
Also dann das beste beider Programm-schnippsel zusammen nehmen:

void shiftOutFasterLsb(const byte & val)
{
    DDR |= BitMaskCLK | BitMaskDAT; // Move to setup() in actual program

    byte bit = 1;
    while(bit)
    {
        if (val & bit)
            PORT |= BitMaskCLK | BitMaskDAT;
        else
            PORT = BitMaskCLK | (PIN &~BitMaskDAT);
        PIN = BitMaskCLK;
        bit <<= 1;
    }
}

Wäre es jetzt unverschämt, wenn ich darum bitten würde, dass man da im Code a bissle was neikommentiert, damit auch ein Anfänger evtl. Fragmente davon verstehen können? :stuck_out_tongue:

Kann man das zeitlich anders als in Zylen ausdrücken (ms/ns), wie viel schneller das ist, als mit der "Arduinoischen Standard-shiftOut-Methode"?

Gruß Chris

Chris72622:
Wäre es jetzt unverschämt, wenn ich darum bitten würde, dass man da im Code a bissle was neikommentiert, damit auch ein Anfänger evtl. Fragmente davon verstehen können? :stuck_out_tongue:
Kann man das zeitlich anders als in Zylen ausdrücken (ms/ns), wie viel schneller das ist, als mit der "Arduinoischen Standard-shiftOut-Methode"?

Die neue Funktion ersetzt shiftOut(8,9,LSBFIRST,val) und ist ca 11.5 mal schneller als diese (wahrscheinlich sogar noch schneller wegen der while-Schleife die ich beim Testen verwendet habe). Ein Ausführen der shiftOut()-Funktion braucht bei 16 MHz ungefähr 127.7 µs, die shiftOutFasterLsb nur 11.3 µs.

Kurze Erklärung des Codes (erstmal das hier (für die Ports) und evtl auch das (für die bitweisen Operatoren) lesen)

#define PORT PORTB     //Wir definieren den Port, auf dem Clock und Datenpin liegen (hier: Port B, dort sind die Pins 8 und 9), da kann man auch PORTC oder PORTD nehmen
#define DDR  DDRB      //Die zugehörigen DataDirectionRegister, dort steht drin ob die Pins Input oder Output sind; auch von Port B
#define BitMaskCLK 0b00000010  //pin 9 = Clock Pin; Pin 9 ist bit 1 von Port B
#define BitMaskDAT 0b00000001  //pin 8 = Data Pin; Pin 8 ist bit 0 von Port B

void shiftOutFasterLsb(const byte & val)
//statt "byte val" benutzen wir "const byte & val", also übergeben die Addresse der Variablen, die wir rausschieben wollen, damit wir keine neue Variable anlegen müssen; wir verändern die Variable ja nicht! "const" da die Addresse immer gleich ist.
{
    DDR |= BitMaskCLK | BitMaskDAT; // Move to setup() in actual program
  //Das gehört ins Setup und setzt die beim Port im DDR die Bits, die für die I/O-Richtung verantwortlich sind. 1 = Output, 0 = Input. Mit dem bitweisen Or werden die jeweiligen Bits der beiden Pins als Output gesetzt. Das ist macht alos das Gleiche wie:
  //pinMode(8,OUTPUT);
  //pinMode(9,OUTPUT);

    byte bit = 1;   //Variable die speichert, welches Bit wir gerade rausschreiben. Wir fangen beim LSB an.
    while(bit)    //Wenn wir die "1" 7 mal geschoben haben, ist sie vom LSB zum MSB gewandert. Beim 8. Mal ist das Bit dann links rausgeschoben und die Variable 0. Da 0 = false wird die Schleife abgebrochen
    {
        if (val & bit)  //ist das aktuelle Bit des herauszuschiebenden Wertes gesetzt?
            PORT |= BitMaskCLK | BitMaskDAT;   //ja: setze Clock und Datenpin HIGH
        else
            PORT = BitMaskCLK | (PORT &~BitMaskDAT);  //nein: setze nur Clock HIGH und Datenpin LOW. "PORT &~BitMaskDAT" gibt alle Pins so zurück wie sie waren, außer der der in BitMaskDat gesetzt ist wird 0.
        PORT ^= BitMaskCLK;  //Toggle den Clockpin. Da er davor HIGH war, wird er jetzt wieder LOW. 
        bit <<= 1;  //Schiebe die Merkervariable um eins nach links, jetzt ist das nächste Bit dran.
    }
}

Super!

Ganz recht herzlichen Dank!

Gruß Chris

MGOS, super Erklaerung!
Karma +1 :slight_smile:

Da ich gerade etwas mit einem Shift-Register ausprobiert habe, komme ich auf diesen Beitrag nochmal zurueck...

Den while loop in shiftOutFasterLsb() zu ent-rollen bringt nochmals eine maechtige verringerung der Laufzeit.
Auch habe ich gemerkt das mein Shift-Register es nicht mag, CLOCK und DATA gleichzeitig zu schalten. DATA muss schon gesetzt sein, before CLOCK umgeschaltet wird.

Mit dem folgenden Testprogramm habe ich das shiftOutLSB() mit dem normalen shiftOut() verglichen. shiftOut() braucht ca. 110us pro byte und die neue shiftOutLSB() nur 4.5us!! Genau 9 instructionen oder 0.5625us pro bit.

#include <util/delay.h>

#define DAT  0x01
#define CLK  0x10
#define CLR  0x04

uint8_t leds = 0x3;
uint8_t direction = 1;

void clear()
{
    PINB = CLR;
    PINB = CLR;
}

void bitOut(const byte & val)
{
    if (val)
        PORTB |= DAT;
    else
        PORTB &= ~DAT;

    PINB = CLK;
    PINB = CLK;
}

void shiftOutLSB(const byte & val)
{
    bitOut(val & 0x01);
    bitOut(val & 0x02);
    bitOut(val & 0x04);
    bitOut(val & 0x08);
    bitOut(val & 0x10);
    bitOut(val & 0x20);
    bitOut(val & 0x40);
    bitOut(val & 0x80);
}

int main()
{
    DDRB |= CLK | DAT | CLR;
    PORTB = CLR;
    
    for(;;)
    {
        clear();
        shiftOutLSB(leds);
        
        if (direction)
            leds <<= 1;
        else
            leds >>= 1;

         if (leds & 1 || leds & 0x80)
             direction = !direction;

        _delay_ms(50);
    }
}