Timing für WS2812b

Hallo liebe Community,

seit kurzem bin ich dabei mir eine kleine Klasse zu schreiben, welche die LED-Strings mit den WS2812B Mikrokontrollern steuern kann. Mir ist bewusst, dass es dafür bereits supertolle vorgefertigte Klassen, wie z.B. von Adafruit gibt, meine Intention lag jedoch darin mich mit den Timern auseinanderzusetzen und zu verstehen, wie diese funktionieren.

Bauteile:

  • Arduino Uno
  • LED-strip mit 40LEDs, welche jeweils einen WS2812B integriert haben

Meine Interpretation des Datenblattes:

Datenblatt des WS2812B:https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf.

Laut dem Datenblatt interpretiert der WS2812B ein Signal mit ...

  • ... hoher Taktflanke von 0.8us und darauffolgender niedriger Taktflanke von 0.45us als 1.
  • ... hoher Taktflanke von 0.4us und darauffolgender niedriger Taktflanke von 0.85us als 0.
    Der Einfachheit halber habe ich in meinem Code nur 0.4us und 0.85us genommen, da diese dennoch im Toleranzbereich von 150ns liegen.

Die einzige Zeit, bei der ich noch zweifle, ob ich diese richtig interpretiert habe ist die RES-time:
Ich interpretiere das so, dass sobald der x * 24Bit lange Stream in die µCs geschrieben worden ist und daraufhin eine pause von 50us auftritt die µCs sozusagen "aktiviert" werden und die Daten, welche sie davor durch den Stream bekommen haben. Das x soll hierbei die anzahl an LEDs bezeichnen.

Bitte korrigiert mich hier schon einmal, ob ich mit dieser Interpretation falsch liege.

Im folgenden nun mein Code:

/* Calculate the time for the timers
 *  
 * 0code T0H = 0.4us, T0L = 0.85us 
 * 1code T1H = 0.85us, T1L = 0.4us 
 * RESET = >50us
 * Toleranz +- 150ns
 * 
 * TIMER2 = 8Bit zaehler -> 256zyklen
 * 
 * prescale = 1
 * 
 * 0.85us: COMPCOUNT = 256 - 0.00000085 * 16000000 = 242.4
 * -> COMPCOUNT = 242 -> OCR_count = 14
 * 
 * 0.4us: COMPCOUNT = 256 - 0.0000004 * 16000000 = 249.6
 * -> COMPCOUNT = 250 -> OCR_count = 6
 * 
 * prescale 1024
 * 50us: COMPCOUNT = 256 - 0.00005 * 16000000/1024 = 255.2
 * -> COMPCOUNT = 0 -> lasse ueberlaufen
 */

#define WRITE_HIGH_0 0        //the index for writing a high flank for the 0
#define WRITE_LOW_0 1         //the index for writing a low flank for the 0
#define WRITE_HIGH_1 2        //the index for writing a high flank for the 1
#define WRITE_LOW_1 3         //the index for writing a low flank for the 1

//bistate indicates whether I want to write 1 or 0 to the chip and 
//if its the high or low flank
volatile byte bitstate = 0;

#define COMPCOUNT_HIGH_0 6    //the value for the comparecounter, if we want to send a 0 and its the high flank
#define COMPCOUNT_LOW_0 14    //the value for the comparecounter, of we want to send a 0 and its the low flank
#define COMPCOUNT_HIGH_1 14   //the value for the comparecounter, if we want to send a 1 and its the high flank
#define COMPCOUNT_LOW_1 6     //the value for the comparecounter, if we want to send a 1 and its the low flank

auto led_string_write(unsigned int, unsigned int, unsigned int) -> void;
auto create_stream(unsigned int, unsigned int, unsigned int) -> byte*;
auto led_show(byte*) -> void;

//loopvariables for waiting that the timers end up with their work
volatile bool wait_for_high_1 = false;
volatile bool wait_for_low_1 = false;
volatile bool wait_for_high_0 = false;
volatile bool wait_for_low_0 = false;
volatile bool wait_for_reset = false;

//this interrupt writes the low flank to the chips
ISR(TIMER2_COMPA_vect){
  if(bitstate == WRITE_HIGH_1){
    wait_for_high_1 = false;
    bitstate = WRITE_LOW_1;
    OCR0A = COMPCOUNT_LOW_1; //auf 0.4us stellen
    TCNT0 = 0;
    PORTD = B00000000; //pin2 fuer 0.4us auf low stellen
  } 
  else if(bitstate == WRITE_LOW_1){
    wait_for_low_1 = false;
    TCNT0 = 0;
  } 
  else if(bitstate == WRITE_HIGH_0){
    wait_for_high_0 = false;
    bitstate = WRITE_LOW_0;
    OCR0A = COMPCOUNT_LOW_0; //auf 0.85us stellen
    TCNT0 = 0;
    PORTD = B00000000; //pin 2 fuer 0.85us auf low stellen
  } 
  else if(bitstate == WRITE_LOW_0){
    wait_for_low_0 = false;
    TCNT0 = 0;
  }
}
ISR(TIMER2_OVF_vect){
  wait_for_reset = false;
}

void setup() {
  DDRD = B00000100;
  PORTB = B00000000; //use pin2 for the tests
  
  cli();
  TCNT2 = 0;
  TCCR2A = 0;
  TCCR2B = 0;
  TCCR2B |= (1 << CS20);
  TIMSK2 |= (1 << OCIE2A);  
}

void loop() {
  led_write(0, 0, 0);
}

void led_write(unsigned int red, unsigned int green, unsigned int blue){
  byte *stream = create_stream(&red, &green, &blue);
  led_show(stream);   //write the stream to the leds
  
  //wait or writing the whole stream
  cli();
  wait_for_reset = true;
  TCCR2A = 0;
  TCCR2B = 0;
  TCCR2B |= (1 << CS20);
  TCCR2B |= (1 << CS22); //prescale auf 1024 setzen, damit der overflow nach 50us ausgeloest wird
  TIMSK2 = 0;
  TIMSK2 |= (1 << TOIE2); //soll auf overflow reagieren
  TCNT0 = 0; //auf 0 setzen
  sei();

  //hier sollen auch die ganzen 50us gewartet werden, bis der overflow interrupt aufgerufen wird und fertig gelaufen ist
  while(wait_for_reset);

  cli();
  TIMSK2 = 0;
  TIMSK2 |= (1 << OCIE2A);
  TCCR2B = 0;
  TCCR2B = (1 << CS20); //prescale wieder auf 1 setzen
  
  
  delete[] stream;
}

//creates a stream for the data the leds should hold
byte* create_stream(unsigned int r, unsigned int g, unsigned int b){
  byte *stream;
  byte red, green, blue;
  const byte colour_bitlength = 8;
  byte bitmask = B00000001;
  
  stream = new byte[24]; //3 * 8 Bit, da jede LED 8Bit hat
  
  if(r > 255) r = 255;
  if(g > 255) g = 255;
  if(b > 255) b = 255;
  red = r; 
  green = g; 
  blue = b;

  //Beachte, dass das highbit zuerst gesendet wird und mit green angefangen wird
  //deshalb muss stream[0] = rechtestes bit von green sein und so weiter

  //green
  for(int i = 0; i < colour_bitlength; ++i){
    stream[i] = (green & bitmask);
    green = (green >> 1);
  }

  //red
  for(int i = 0; i < colour_bitlength; ++i){
    stream[i + colour_bitlength] = red & bitmask;
    red = (red >> 1);
  }
  
  //blue
  for(int i = 0; i < colour_bitlength; ++i){
    stream[i + (2 * colour_bitlength)] = (blue & bitmask);
    blue = (blue >> 1);
  }
  return stream;
}

void led_show(byte *stream){
  for(unsigned int i = 0; i < 24; ++i){
    if(stream[i]){
      cli();
      wait_for_high_1 = true;
      wait_for_low_1 = true;
      bitstate = WRITE_HIGH_1;
      OCR2A = COMPCOUNT_HIGH_1; //set to 0.85us
      TCNT2 = 0;
      PORTD = B00000100; //set pin2 to high
      sei();
      
      //wait until the signal was completely sent to the microchip
      while(wait_for_high_1);
      while(wait_for_low_1);
      
    } else{
      cli();
      wait_for_high_0 = true;
      wait_for_low_0 = true;
      bitstate = 0;
      OCR2A = COMPCOUNT_HIGH_0; //set to 0.4us
      TCNT2 = 0;
      PORTD = B00000100; //set pin2 to high
      sei();

      //wait until the signal was completely sent to the microchip
      while(wait_for_high_0);
      while(wait_for_low_0);
    }
  }
  cli();  
}

Auftretende Probleme:
Ich habe an meinen Arduino, wie oben bereits beschrieben, 40LEDs anhängen, von denen nur die erste Leuchten sollte. Sobald ich den Code hochlade leuchte 4.

Eigentlich sollten durch den Bytestream die entsprechenden Farben in die µC geschrieben werden.
Egal, welche Werte ich in die Funktion gebe, ich bekomme immer nur die Farbe weiß heraus.
Ich habe mir mal per Serieller Schnittstelle den Bytestream ausgeben lassen under ist immer korrekt.
Daraus folgere ich, dass irgendwas mit den Zeiten falsch sein muss, da die µCs der LEDs diese immer als 1 interpretieren.

Was die Zeiten angeht kann ich mir leider nicht sicher sein, ob diese jetzt eingehalten werden oder nicht, da ich leider kein Oszilloskop besitze und somit schlecht beurteilen kann, wie ich diese anpassen soll.

Ich bedanke mich schon einmal für jegliche Hilfe

Grüße Tyrone_Montana

Was die Zeiten angeht kann ich mir leider nicht sicher sein, ob diese jetzt eingehalten werden oder nicht, da ich leider kein Oszilloskop besitze und somit schlecht beurteilen kann, wie ich diese anpassen soll.

Naja...
Dann ist das also nur ein blindes stochern im Nebel...
(sorry)

Ein USB Oszi ist für ca 60€ zu haben.

Ein LogikAnalyser für unter 10€
Die Software Sigrok/PulseView ist frei.

Eins der beiden Instrumente wirst du brauchen.
Als Sinneserweiterung, denn mit deinen natürlichen, angewachsenen, Sinnen wirst du nicht weit kommen.

Übrigens:
Ich vermute, dass du mit Timern und ISRs sowieso keine Chance hast das Timing einzuhalten.
Es wird auf hart codierte Wartezeiten mit NOPs herauslaufen.

Bei einem anderen µC, z.B. einem aus der STM32 Reihe, könnte man evtl was mit DMA machen. Aber beim UNO? No.

Mir ist bewusst, dass es dafür bereits supertolle vorgefertigte Klassen, wie z.B. von Adafruit gibt

Na denn schau dir die doch einfach an.

Kann natürlich sein dass die eigene Kreativität ein wenig leidet, wenn man etwas Gutes sieht, auf das man selber nicht gekommen wäre :wink:

Hi

Ob Das nun 'etwas Gutes' ist, wenn man sieht, wie in der Lib die Laufzeit der einzelnen Befehle 'mitgezählt' wird, damit eben die Flanken dann kommen, wann der Chip Die auch haben will und deshalb die eigene Kreativität leidet?
Ok, man kann erkennen, daß dort - wohl von Leuten, Die wissen, was Sie machen, TAKTE gezählt werden - wenn's mit einem Timer einfacher wäre, hätten Diese wohl einen anderen Weg eingeschlagen.

Die 50ns Pause greifen IMMER, wenn nach einer Datenübertragung für 50ns Nichts mehr passiert, schalten Alle LEDs auf den eingetragenen Farbwert.
LEDs, Die keinen neuen Farbwert bekommen haben, wissen eh Nichts davon, daß die Übertragung beendet ist - für Die hat die Übertragung noch nicht Mal angefangen.

MfG