[Projekt] Schnelle Digital Eingabe/Ausgabe

Hi!

Nachdem ich letztens in Timing Probleme getaumelt bin, habe ich mal versucht schnelle Ein/Ausgaben zu basteln. digitalRead() und digitalWrite() waren deutlich zu langsam.

Getestet auf einem Standard 16MHz UNO.

Versuch 1 zeigte: 54 KHZ Rechteck maximal

void loop() 
{
  for(;;) 
  {
    digitalWrite(13,!digitalRead(13));
  }
}

Versuch 2 zeigte: 100 KHZ Rechteck maximal

void loop() 
{
  for(;;) 
  {
    digitalWrite(13,1);
    digitalWrite(13,0);
  }
}

Das ist nicht die Welt!

Mit meinen neuen Pin Funktionen, 2 MHz Rechteck

void loop() 
{
  for(;;) 
  {
    togglePin(LED);
  }
}

Eine Steigerung um Faktor 20.

Die Pinbezeichnung orientiert sich hier nicht an der Arduino Pin Benennung, sondern an den AVR Datenblattangaben. Die jeweiligen Funktionen sind inline deklariert, und daran hält sich der Compiler. Auch wenn die Funktionen, mit ihren Parametern, fett aussehen, wird doch nur ein einziges Assembler Statement in den Code kompiliert. (Ausnahme: setInputPullup() wird zu 2 Statements)

Beispiel, Blink.ino:

#include "Pin.h"


// Standard LED auf dem UNO Board
#define LED PINDEF(B,5) //Pin13, PORTB Bit 5 

 

void setup() 
{
  setOutput(LED);
}

void loop() 
{
  togglePin(LED);
  delay(1000);
}

Zum Abschluss natürlich noch die Pin.h, mit ihrer ganzen "Magie":

#pragma once


#ifndef __AVR__
 #error This FastPin Lib workls only with Atmel AVR 
#endif

#ifndef ARDUINO
  #include <avr/io.h>
#endif  

#define PARAMETERLIST                            \
volatile __attribute__((unused)) uint8_t * DDR , \
volatile __attribute__((unused)) uint8_t * PORT, \
volatile __attribute__((unused)) uint8_t * PIN , \
                                 uint8_t   pin

/*
  * Benutzung: Pin für die spätere Verwendung deklarieren
  * #define BuildInLED PINDEF(B,5) // die LED auf einem Arduino UNO 
*/
#define PINDEF(PORTNR,PINNR)  &DDR##PORTNR,&PORT##PORTNR,&PIN##PORTNR,PINNR



static inline void setPin(PARAMETERLIST)
{
 *PORT |= (1<<pin);   
}

static inline bool getPin(PARAMETERLIST)
{
 return (bool) *PIN & (1<<pin);   
}

static inline void unsetPin(PARAMETERLIST)
{
  *PORT &= ~(1<<pin);   
}

static inline void togglePin(PARAMETERLIST)
{
  *PIN = (1<<pin);   
}

static inline void setOutput(PARAMETERLIST)
{
  *DDR |= (1<<pin);   
}

static inline void setInput(PARAMETERLIST)
{
  *DDR  &= ~(1<<pin);   
}

static inline void setInputPullup(PARAMETERLIST)
{
  *DDR  &= ~(1<<pin); 
  *PORT |=  (1<<pin);  
}

viel Spaß damit

Ich stelle mich jetzt mal auf die Schulter von Giganten, wie es im Englischen so schön heißt, und ergänze millis() und Mega2560:

#include "Pin.h"

// Standard LED auf dem UNO, Nano, ProMini Board
// #define LED PINDEF(B,5) //Pin13, PORTB Bit 5
// Standard LED auf dem Mega2560 Board
#define LED PINDEF(B,7) //Pin13, PORTB Bit 7

unsigned long aktMillis, altMillis;
int zeit = 1000;

void setup()
{
  setOutput(LED);
}

void loop()
{
  aktMillis = millis();
  if (aktMillis - altMillis >= zeit) {
    togglePin(LED);
    altMillis = aktMillis;
  }
}

Eine Sekunde an, eine aus, schneller Blinken tut das jetzt aber auch nicht :grin:

Ich freue mich auf PinPlus, wo dann auch togglePinPlus(13); funktioniert.

Bis dahin gehört Pin.h zu meinen Standardbibliotheken.

Danke, für den Test auf dem Mega2560!

Eine Sekunde an, eine aus, schneller Blinken tut das jetzt aber auch nicht :grin:

Ja! (wäre ja auch schlimm)
Aber es bleibt mehr Zeit in der loop() für anderes Gedöns.
Für die ganzen endlichen Automaten. :wink:

Ich freue mich auf PinPlus, wo dann auch togglePinPlus(13); funktioniert.

Ich habe mir vorher etliche Libs angesehen, welche das gleiche leisten wollen.
Die Umfomung von Arduino Pin zu PORT+Bit ist nicht so ganz trivial.
Nein: Das ist gruselig! (Dutzende von Extrawürsten wollen gebacken werden)

Da kann man besser irgendwas anderes nehmen:
Z.B.: https://github.com/watterott/Arduino-Libs/blob/master/digitalWriteFast/digitalWriteFast.h^

Bis dahin gehört Pin.h zu meinen Standardbibliotheken.

Schön, dass es dir gefällt!

Es geht, nicht überraschend, auch mit Nano und ProMini, habe ich ergänzt.

combie:
Schön, dass es dir gefällt!

Tut es, weil es den Anfänger an die Ports heranführt, sollte denn "Blinken" im 2 MHz Takt gebraucht werden.

Es geht, nicht überraschend, auch mit Nano und ProMini,..

Ja, das habe ich auch wohl so erwartet...

Der Micro wäre noch interessant.
Da weiß ich auch noch nicht ob der togglePin() funktioniert.
Naja, das Datenblatt wirds mir wohl sagen...

..... habe ich ergänzt.

Schön..
Ich habe oben im Quelltext auch noch was ergänzt.

Ein netter Mensch hat mich darauf hingewiesen, dass der Code einen Eimer voll Warnings wirft, wenn man denn die Anzeige einschaltet. Von ungenutzten Funktionsparametern/Variablen spricht er. Da hat er natürlich recht, der Kollege, und auch der Compiler. Ein großer Teil der Funktionsparameter ist wirklich ungenutzt. Lässt sich bei der/meiner Strategie auch leider nicht vermeiden. Jetzt sind diese unbenutzten Variablen auch als solches gekennzeichnet. So bleibt der Compiler ruhig.

Feiner GNU Compiler!
8) Tut genau, was man von ihm will, man muss es ihm nur klar machen. 8)

Bitte noch den Schreibfehler korrigieren:

static inline bool getPin(volatile UNUSEDVAR uint8_t * DDR, volatile UNUSEDVAR uint8_t * PORT, volatile uint8_t * PIN,uint8_t pin)
{
 return (bool) *PIN & (1<<pin);   // nicht |
}

Wenn das Ergebnis 0/1 sein soll, dann eher
return (bool) (*PIN >> pin) & 1;

*PIN & (1<<pin); // nicht |

Danke!
geändert
(Mist.., der ist mir durch die Lappen gegangen....)

Wenn das Ergebnis 0/1 sein soll, dann eher
return (bool) (*PIN >> pin) & 1;

Der cast macht das schon.
Den (bool) Cast könnte man auch weglassen, wird sowieso durchgeführt.
Habe ich nur hingeschrieben um klar zu machen was passiert. Kostet keinen Code.

Auch müsste (*PIN >> pin) hier zur Laufzeit geschoben werden. Und schieben ist auf einem AVR teuer.
Dieses (1<<pin) schiebt der Compiler zur Compilezeit.

Kannst ja mal hiermit gegentesten.

Und mal hier #6 lesen. Das Toggeln müsste in einem einzigen Takt funktionieren = 8 MHz Rechteck??

Gruß, H.

Helmuth:
Kannst ja mal hiermit gegentesten.

Aber gerne doch!

#include <fastpin.h>


Pin LED(13);

void setup() 
{
  LED.setOutput();
}

void loop() 
{
  for(;;)
  {
    LED.toggle();
  }  
}

Mehr, als ein 800KHz Rechteck ist nicht drin.
Und der Speicherverbrauch ist erheblich größer.
Sowohl Flash, als auch Ram.
OK, Ram ist kein Wunder, denn meins braucht keins.

Helmuth:
Und mal hier #6 lesen. Das Toggeln müsste in einem einzigen Takt funktionieren = 8 MHz Rechteck??

Das wäre schön.. (Takte korrigiert)
1 Takt fürs Toggeln
2 Takte für den Sprung in der For Schleife
Macht 3 Takte für eine Halbwelle
6 Takte für eine Vollwelle

16MHz / 6 Takte = 2,66667MHz
Mehr/Schneller geht einfach nicht.

Danke.

Nur so als Spaß zwischen Frühstück und duschen:

void setup() 
{
  pinMode( 13, OUTPUT );
}
void loop() 
{ 
  // Auf UNO R3 mit 16 MHz ergibt diese
  // Schleife ein Rechteck von 2,65 MHz
  asm volatile(
  "0:      sbi   %[pob],    5     \n\t"
  "        cbi   %[pob],    5     \n\t"
  "        rjmp  0b               \n\t"
  :
  : [pob]  "I"   (_SFR_IO_ADDR(PORTB))
  :
  );  
}

:wink:

Helmuth:
Danke.

Gerne!

Habe auch schon Experimente gemacht, das irgendwie mit OOP und Templates so schlank zu bekommen. Ist mir bisher noch nicht gelungen. Und von den ganzen OOP getriebenen Pin Manipulatoren, die mir bisher unter gekommen sind, ist das Fastled Gedöns noch eins der besten.

Nur so als Spaß zwischen Frühstück und duschen:

Das ist dann aber kein symmetrischer Rechteck mehr!
Exakt gleiches Ergebnis, wie mein Code:

#include "Pin.h"

// Standard LED auf dem UNO Board
#define LED PINDEF(B,5) //Pin13, PORTB Bit 5 

void setup() 
{
  setOutput(LED);
}

void loop() 
{
  for(;;) 
   {
    togglePin(LED); 
    togglePin(LED); 
   } 

}

Auch 2,667MHz und auch kein symmetrischer Rechteck mehr.

Assembler bringt hier keine Vorteile.
Aber als Gegenprobe, eine feine Sache!
Danke.

In dieser Variante symmetisch und 2MHz

void loop() 
{ 
  // Auf UNO R3 mit 16 MHz ergibt diese
  // Schleife ein Rechteck von 2 MHz
  asm volatile(
  "0:      sbi   %[pob],    5     \n\t"
  "        rjmp  0b               \n\t"
  :
  : [pob]  "I"   (_SFR_IO_ADDR(PINB))
  :
  );  
}

Hallo,

was störte dich an digitalWriteFast? Nicht perfekt genug? Immer noch zu langsam. Kommt man da nicht besser ohne extra Lib direkt den Pin zu schalten. Im klassischen C direkt das Bit im Port zu schalten.

So:

PORTB |= (1 << PB5);

? ? ?

Da ist mir doch:

setPin(LED);

lieber.
Ist besser lesbar, finde ich.

Der Compiler macht sowieso aus beidem das selbe.


was störte dich an digitalWriteFast? Nicht perfekt genug?

:wink: vielleicht ist es das :wink:

void loop() 
{ 
  for(;;) // auch 2MHz
  {
    fastDigitalToggle(13); 
  }
}

Danke für die arbeit. Wenn man schnellen PinChange braucht perfekt.
Damit könnte man dann perfekt ein Software PWM machen nur in der Loop.
Karma +

Gruß
DerDani

Ich habs interessehalber mal ausprobiert, und bin mit einem Burst (for Schleife 8 mal innerhalb loop) auf eine Zykluszeit von ca. 0,5µs gekommen, etwa 2MHz oder 8 Takte :slight_smile:
Dabei wird der Port zweimal gelesen und geändert. Wenn man ihn vorher ausliest, und dann nur die Änderungen reinschreibt, könnte man noch um 2 Takte schneller werden.

Jedenfalls deutlich schneller als die 12µs mit digitalWrite. Wobei es zeitlich ziemlich egal ist, ob man damit den Pin in loop() einmal oder zweimal ändert, die meiste Zeit wird anscheinend nicht im Aufruf von loop() verdödelt, sondern in digitalWrite().

Dabei wird der Port zweimal gelesen und geändert. Wenn man ihn vorher ausliest, und dann nur die Änderungen reinschreibt, könnte man noch um 2 Takte schneller werden.

Da bin ich mir nicht sicher....
Die Änderungen wollen ja auch berechnet werden.....
Oder wenigstens aus dem Speicher gefischt.

Allerdings hast du recht, wenn man nicht ausliest, sondern stumpf das ganze Register beschreibt, wirds etwas schneller. In der Praxis wird das leider wenig Bedeutung erlangen...

void loop()  // 2,667 MHz symmetrischer Rechteck
{ 

    for(;;) PINB = 0xFF;
}

agmue:
Ich freue mich auf PinPlus, wo dann auch togglePinPlus(13); funktioniert.

Ein Stück des Weges, ist gegangen....

Blink.ino:

#include "PinDefinition.h"

using PinDefinition::OutputPin;

OutputPin<13> led;

void setup(void) 
{
   led.init();
}

void loop(void) 
{
    led.toggle();
    delay(1000);
}

Ein weiteres Beispiel: Beim Tastendruck erleuchtet die LED an Pin 13

#include "PinDefinition.h"

using namespace PinDefinition;

TasterGND<2>  taster; // Taster zwischen Pin und GND(invertierend)
OutputPin<13> led;

void setup(void) 
{
  taster.initPullup();
  led.init();
}

void loop(void) 
{
    led = taster;
}

Das ganze ist noch nicht ganz ausgereift, glaube ich mal...
Bisher existieren an Spezialisierungen nur der TasterGND und RelaisINV.
Den Taster haben wir schon gesehen...

Das RelaisINV dient zum schalten der typischen (inversen) China Relais.

#include "PinDefinition.h"

using namespace PinDefinition;

RelaisINV<13> relais;

void setup(void) 
{
  relais.init();// ohne kurze Low Phase
}

void loop(void) 
{
   relais.ein();
   delay(1000);
   relais.aus();
   delay(1000);
}

Die Klassen/Objekte beanspruchen kein Ram.
Der generiert Code beschränkt sich auf das notwendigste.
z.B. relais.aus(); wird übersetzt, wie ein: " PORTB |= 1<<PB5; "
Knapper geht es nicht.

Über Anregungen, Kritik usw. würde ich mich sehr freuen.

PinDefinition.h (7.88 KB)

Hallo,

habe mir das angeschaut. Schön das sich jemand dazu Gedanken gemacht hat. Dazu folgende Kritik, natürlich positive. :slight_smile:

Wäre es nicht von Vorteil das Bit setzen und löschen Lib intern mit dem klassischen sbi und cbi zu machen?
Bei den "Definitionen" noch ein 4. Array dazu um den echten Pinnamen alias PA4 zum Bsp. dann direkt ansprechen zu können. Dann muss die Lib nicht nochmal extra mit Bitmasken hantieren. Desweiteren wäre ich dafür schon zum bestehenden halbwegs kompatibel zu bleiben. Sprich Funktionsnamen statt digitalWrite() vielleicht setDigital oder digitalSet(). Dann müßte man im Sketch nur digitalWrite gegen den neuen Funktionsnamen austauschen und alles läuft schneller. So stellte ich mir das zumindestens vor, obs wirklich so einfach geht weiß ich nicht.

Arduino Mega2560

#include <CombiePinDefinition.h>

#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

// Servopin Definitionen, OUT
#define P12_OUT  sbi (DDRB,6)  // PB6 Ausgang
#define P12_ON   sbi (PORTB,6) // PB6 einschalten
#define P12_OFF  cbi (PORTB,6) // PB6 ausschalten

using namespace Combie::PinDefinition;

RelaisINV<12> relais;

void setup(void) 
{
  relais.init();// ohne kurze Low Phase
}

void loop(void)
{
      // 1,6MHz
      relais.ein();
      relais.aus();

      >> oder <<
      
      // 2MHz
      P12_ON;
      P12_OFF;
}

Hallo,

ich hatte eigentlich an sowas gedacht, scheint aber sinnlos zu sein, sind nur 125kHz drin, nicht besser wie digitalWrite.
Das hätte ich dann auf setDigital(Pin, High/Low) umbauen wollen. Dann würde es noch langsamer.
Hat nicht sein sollen. :wink:

>> fastSwitch.h <<

#pragma once
        

#include <avr/io.h>
 
    // Der Index ist jeweils die Arduino Pinnummer 
      
class fastSwitch               
{
  private:                  
    // *** Arduino Mega2560 Pinnummer 0 bis 13
    volatile uint8_t const Pin;      
    volatile uint8_t const *portList[15] = {&PORTE,&PORTE,&PORTE,&PORTE,&PORTG,&PORTE,&PORTH,&PORTH,&PORTH,&PORTH,&PORTB,&PORTB,&PORTB,&PORTB};
    volatile uint8_t const  numList[15]  = {PE0,PE1,PE4,PE5,PG5,PE3,PH3,PH4,PH5,PH6,PB4,PB5,PB6,PB7};
            
    
  public:                     
    fastSwitch (const uint8_t);
    void setDigitalON(void);
    void setDigitalOFF(void);    
}; 


// -------------------------------------------------------------------- //
>> fastSwitch.cpp <<

#include "fastSwitch.h"


#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
      
      
fastSwitch::fastSwitch (volatile uint8_t _Pin):
  Pin(_Pin)
{ }    


void fastSwitch::setDigitalON(void)
{
  sbi(*portList[Pin], numList[Pin]);
}


void fastSwitch::setDigitalOFF(void)
{
  cbi(*portList[Pin], numList[Pin]);
}
        

// -------------------------------------------------------------------- //
>> fastSwitch.ino <<

// *** Arduino Mega2560 only *** //

#include "fastSwitch.h"

fastSwitch LED (13);

void setup() {
  pinMode(13, OUTPUT);
 
}

void loop() {
  
  // mickrige 125kHz
  LED.setDigitalON();
  LED.setDigitalOFF();
}