Eigene Funkprotokolle

Über diesen Beitrag

In den letzten Beiträgen habe ich vorgestellt wie man mittels geeigneter Bibliotheken Funkmodule und Funksteckdosen mit dem Arduino oder anderen Microcontrollern ansteuert. Aber geht 433 MHz Funk auch ohne Bibliothek? Ist es kompliziert, eigene (einfache) Funkprotokolle zu entwickeln? Wenn man ein paar Dinge beachtet, ist es nicht sonderlich schwierig und macht darüber hinaus auch viel Spaß. Ich stelle hier zwei Konzepte vor. Dabei benutze ich einfache 433 MHz Transmitter- und Receivermodule wie in den vorherigen Beiträgen. Bitte nicht vergessen Antennen an die Module zu basteln. Ein Anschlussschema für den Arduino muss ich wohl nicht noch einmal abbilden: GND an GND, VCC an 5 V und den Datapin des Moduls gemäß Vorschrift im jeweiligen Sketch. Für die Transmitterseite habe ich einen Arduino Nano eingesetzt, auf der Receiverseite einen UNO. 

Geeignete Module für eigene Funkprotokolle
Einfache „One-Way“ Module für 433 MHz

Schnelligkeit ist gefragt

Bevor es losgeht muss ich noch ein kleines bisschen ausholen. Funktechnik ist so schnell, dass die Geschwindigkeit, in der ein Pinstatus (HIGH oder LOW) abgefragt werden kann, durchaus relevant wird. Normalerweise verwendet man den digitalRead Befehl, hier z.B. zum Abfragen des Pin 5:

pinStatus = digitalRead(5);

Gleichbedeutend, wesentlich schneller, aber auch viel kryptischer ist die „C“ Schreibweise:

pinStatus = PIND & (1<<PD5);

Ich werde vielleicht in einem anderen Beitrag auf das Thema „C“ näher eingehen, denn es gibt auch viele andere Gelegenheiten, wo die „C“ Schreibweise sinnvoll ist. Hier werde ich nur kurz erklären, was das bedeutet. „PD5“ ist eine vordefinierte Konstante mit dem Wert 5 und steht für den Pin 5 am PORTD des ATmega 328. Dieser Pin ist zufälligerweise der Digitalpin 5 am Arduino. Der Pin „PB5″ ist beispielsweise Digitalpin 13 am Arduino. „1<<PD5“ ist eine Binäroperation und bedeutet: verschiebe die 1 um fünf Stellen nach links. Also wird aus der 1, sprich der 00000001, wird eine 00100000. 

PIND ist das Statusregister des PORTD. Sind alle Pins „LOW“ steht im PIND Register „00000000“. Ist nur PD5 „HIGH“, steht dort „00100000“. „&“ ist ein binäres „UND“. Es vergleicht bitweise beide Werte links und rechts von sich. Sind beide gleich, ist das Ergebnis „TRUE“ bzw. „1“. Sind beide ungleich, ist das Ergebnis „FALSE“ bzw. „0“. Und damit führen beide oben genannten Befehle zum selben Ergebnis. 

Wieviel schneller die „C“ Schreibweise ist, testet der folgende Sketch. Ich habe den Status des Pin 5 mit beiden Befehlen 100000 Male auslesen lassen und die dafür benötigte Zeit in Millisekunden messen lassen. Hier der Sketch und das Ergebnis:

unsigned long numberOfReads = 100000;
unsigned long startTime = 0;
unsigned long readTimeLength = 0;
bool pinStatus;

void setup() {
  Serial.begin(9600); 
  pinMode(5,INPUT); 
}

void loop() {
  startTime = millis();
  for(unsigned long i=0; i<numberOfReads; i++){
    pinStatus = digitalRead(5);
  }
  readTimeLength = millis() - startTime;
  Serial.print("digitalRead: ");
  Serial.print(readTimeLength);
  Serial.print(" ms ");
  Serial.print(" / ");
  delay(1000);
  
  startTime = millis();
  for(unsigned long i=0; i<numberOfReads; i++){
    pinStatus = (PIND & (1<<PD5));
  }
  readTimeLength = millis() - startTime;
  Serial.print("PIND Abfrage: "); 
  Serial.print(readTimeLength);
  Serial.println(" ms");
  delay(5000);
}

 

Für Funkprotokolle ist digitalRead ein wenig langsam

Demnach ist der „C“ Befehl ca. achtmal schneller. Pro Einzelabfrage ergeben sich 3.45 Mikrosekunden gegenüber 0.44 Mikrosekunden.

Viel Rauschen im Äther

Eigentlich klingt es zunächst einfach: Auf der Transmitterseite führt ein „HIGH“ am Datenpin zum Senden eines Signals. Dadurch wird an der Receiverseite ein „HIGH“ am Datenpin erzeugt und das lässt sich hinsichtlich der Länge auswerten. Wir wenden hier also keine Modulationsverfahren an. Einfach, aber funktioniert. 

Der folgende Sketch für die Receiverseite fängt Signale auf und misst deren Länge am Pin 5. Um Zeiten zu messen, verwenden die meisten wohl gerne den „millis()“ oder „micros()“ Befehl. Zum Messen sehr kurzer Zeiträume hat sich eine andere Methode bewährt. In einer Schleife wird die Bedingung für die Messung abgefragt. Solange die Abbruchbedingung nicht erfüllt ist, wird in der Schleife eine kurze Zeit (die Auflösung / Resolution) gewartet und dann ein Zähler inkrementiert. Am Ende ergibt Zähler x Auflösung die Zeit. 

const int resolution = 5;
unsigned int counter; 

void setup(){
  Serial.begin(9600);
  DDRD = 0x00; // Arduino Pins D0 - D7 als Input, "C" Schreibweise
  PORTD = 0x00; // Arduino Pins D0 - D7 sind LOW, "C" Schreibweise
}

void loop(){
  counter = 0;
  while(!(PIND & (1<<PD5))){/* Wait for High*/}
  while((PIND & (1<<PD5))){ /* Count High */
    delayMicroseconds(resolution);
    counter++;
  }
  Serial.print(counter);
  Serial.print(", "); 
}

Lässt man den Sketch laufen ohne dass man einen Sender eingeschaltet hat, ist erstaunt man wie viele Signale trotzdem detektiert werden. Wie soll man in dieser Suppe aber nun sein „echtes“ Signal finden? 

„Grundrauschen“

Das Prinzip

Man kann ein Signal als „echtes“ Signal kennzeichnen indem man es in ein eindeutig identifizierbares Start- und Endsignal einbettet. In meinem ersten Testsignal sind dies überlange „HIGH“ Dauersignale von 10000 µs. Dazwischen finden sich die „HIGH“ Informationssignale von 400, 600, 800,….., 2000 µs. Diese sind durch „LOW“ Phasen von 600 µs separiert. 

Eigene Funkprotokolle - Ein Beispiel

Der Sketch dazu sieht folgendermaßen aus und braucht glaube ich keine weitere Erklärung:

int txPin =  10; /* Transmitter Daten Pin */
const int lowLength = 600; /* konstante Low Phase */
 
void setup(){                
  pinMode(txPin, OUTPUT);   
}
 
void loop(){
  unsigned int mcs;
  startSequence(); /*Sende die Startsequenz*/
  for(int i=0; i<9; i++){ /* Sende neun Signale; High: 400, 600, .... */
    digitalWrite(txPin, HIGH);
    mcs = i*200 + 400;
    delayMicroseconds(mcs);
    digitalWrite(txPin,LOW);
    delayMicroseconds(lowLength); 
  }
  endSequence();  /* Sende die Endsequenz */
  delay(5000);
}

void startSequence(){
  digitalWrite(txPin, HIGH);
  delayMicroseconds(10000);
  digitalWrite(txPin, LOW);
  delayMicroseconds(lowLength);
}

void endSequence(){
  digitalWrite(txPin, HIGH);
  delayMicroseconds(10000);
  digitalWrite(txPin, LOW); 
}

Kommen wir zum Receiversketch. Der Receiver wartet zunächst auf ein gültiges Startsignal. Ist das gegeben, also eine bestimmte Länge überschritten, werden die Informationssignale analysiert, d.h. im Wechsel werden die Längen der „HIGH“ und „LOW“ Phasen gemessen. Diese Werte werden in einem zweidimensionalen Array „signalLength“ als „lowCounter“ und „highCounter“ gespeichert. Die Länge der Signale ist dann low- bzw. highCounter x Resolution (hier 20 µs). Wenn der „highCounter“ eine definierte Größe überschreitet, so handelt es sich um das Endesignal. Die Sequenz ist damit vollständig übermittelt und wird auf dem seriellen Monitor ausgegeben. 

int resolution = 20;

void setup(){
  Serial.begin(9600);
  DDRD = 0x00; // Arduino Pins D0 - D7 als Input
  PORTD = 0x00; // Arduino Pins D0 - D7 sind LOW
}

void loop(){
  waitForSignal();
}

void waitForSignal(){
  while(!(PIND & (1<<PD5))){/*Warte bis ein Signal kommt*/}
  if(startSequence()){      /* Ist das Signal die Startsequenz? */
    analyseSignal();        /* Wenn ja, dann analysiere das Signal */
  }
}
  
void analyseSignal(){
  int signalCounter = 0; /* Zähler für die High/Low Signalpaare */
  int lowCounter = 0;    /* Low Signallänge = lowCounter * resolution; */
  int highCounter = 0;   /* High Signallänge = highCounter * resolution; */
  int signalLength[15][2];  /* Die Signale werden in einem zweidimensionalen array gespeichert */
  bool msgCompleted = false;

  while(!msgCompleted){ /* Solange wir uns in einer Sequenz befinden */
    lowCounter = 0;
    highCounter = 0; 
    
    while(!(PIND & (1<<PD5))){ /* Warten auf High, messen des Low Signals */
      delayMicroseconds(resolution);
      lowCounter++;
    }
    while(PIND & (1<<PD5)){   /* Warten auf Low, messen des High Signals */
      delayMicroseconds(resolution);
      highCounter++;
    }
    if(highCounter < 200){    /* Ein Low/High paar wurde empfangen */ 
      signalLength[signalCounter][0] = lowCounter;
      signalLength[signalCounter][1] = highCounter;
      signalCounter++;
    }
    else if(highCounter < 5000){ /* Ende Signal empfangen, das if ist redundant */
      msgCompleted = true;
    }
  }
  /* Es folgt die Ausgabe */
  Serial.println("low / high");
  for(int i=0; i<signalCounter; i++){
    Serial.print(signalLength[i][0]);
    Serial.print(" / ");
    Serial.println(signalLength[i][1]);
  }
  Serial.println("--------");
  delay(2000);
}

bool startSequence(){
  int counter = 0;
  while(PIND & (1<<PD5)){
    delayMicroseconds(resolution);
    counter++;
  }
  if(counter >400){ /* Das Startsignal wurde empfangen */
    return true;
  }
  else{
    return false;
  }
}
Das Testsignal auf der Receiverseite

Und, siehe da, man erhält schöne unterscheidbare Signale. Da die Auflösung 20 µs beträgt und die „LOW“ Signale 600 µs lang sind müsste der lowCounter 30 betragen. Genau genommen 29, da er bei Null los zählt. Wir messen also um die 100 µs zu viel. Und das, was bei den „LOW“ Signalen zu viel ist, fehlt bei den „HIGH“-Signalen. Wahrscheinlich brauchen die Receivermodule eine gewisse Zeit um bei Signalempfang den Datenpin auf „HIGH“ zu setzen. Auf jeden Fall bekommt man hier guten Eindruck in welchen Geschwindigkeiten man Daten übertragen kann. 

Protokoll 1 – bitweise Übertragung

Die Grundidee

Da wir nun gesehen haben, dass man Informationen über die Signallänge übermitteln kann, ist es nun an der Zeit eigene Funkprotokolle zu entwickeln. Im ersten Beispiel übertragen wir Bits, die über die „HIGH“ Phasenlänge definiert werden. Eine „0“ ist ein 600 µs Signal und eine „1“ ein 1200 µs Signal. Dazwischen gibt es feste „LOW“ Signale von 600 µs. Nimmt man mal an, dass Nullen und Einsen gleich oft vorkommen, dann ergibt sich eine durchschnittliche Signallänge von 1500 µs, d.h. eine Übertragungsrate von „sagenhaften“ 0,666 kbit/s. Durch das Grundlagenexperiment weiter oben haben wir gesehen, dass auch sehr viel kürzere Signale noch eindeutig unterschieden werden können. Hier geht also noch ordentlich was in Sachen Geschwindigkeit. 

Der Transmittersketch

Die zu übermittelnde Information habe ich als String definiert. Einen String kann man zunächst sehr bequem in seine einzelnen Zeichen zerlegen: euerString[ites Zeichen]. Für die Zerlegung der Zeichen in ihre 8 Bits verwende ich eine Binäroperation in der Funktion „sendeByte“, die Ihr – wenn Ihr oben gut aufgepasst habt – verstehen solltet. In Kurzform: Das Byte wird schrittweise nach rechts verschoben. Dann wird mit dem logischen „UND“ geprüft, ob das jeweils erste Bit „1“ oder „0“ ist. Entsprechend wird dann ein kurzes oder langes „HIGH“ in der Funktion sendeSignal() gesendet. Das Ende Signal habe ich auf 5000µs gekürzt.

int txPin =  10; /* Transmitter Data Pin */

void setup() {
  pinMode(txPin, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  String msgString = "Hallo Welt, wie geht es dir heute?";
  sendeString(msgString);
  delay(5000);
}

void sendeString(String msg) {
  int len = msg.length();
  initMsg();  /* Sende das Initialisierungssignal */
  for (int i = 0; i < len; i++) { /* Byte by byte senden */
    sendeByte(byte(msg[i]));
  }
  closeMsg();
}

void sendeByte(byte msgByte) { /*Bit by bit senden */
  bool msgBit = 0;
  for (int i = 7; i >= 0; i--) { /* "Extrahieren" der bits */
    msgBit = (msgByte >> i) & 1; 
    sendeSignal(msgBit);
  }
}

void sendeSignal(bool sendBit) {
  digitalWrite(txPin, HIGH);
  if (sendBit == false) {
    delayMicroseconds(600);
  }
  else {
    delayMicroseconds(1200);
  }
  digitalWrite(txPin, LOW);
  delayMicroseconds(1000);
}

void initMsg() {  /* Initialisierung */
  digitalWrite(txPin, HIGH);
  delayMicroseconds(10000);
  digitalWrite(txPin, LOW);
  delayMicroseconds(600);
}

void closeMsg() { /*Abschlusssignal */
  digitalWrite(txPin, HIGH);
  delayMicroseconds(5000);
  digitalWrite(txPin, LOW);
  delayMicroseconds(600);
}

Der Receiversketch

Der Receiversketch ähnelt sehr dem Receiversketch für das Testsignal.  Zunächst wird auf ein gültiges Startsignal gewartet, dann werden die Signallängen ausgewertet und als Bits interpretiert. Die Bits werden über Binäroperationen zu Bytes zusammengesetzt, die Bytes in Characters umgewandelt und aus diesen der String wieder zusammen gesetzt. Da nur ganze Bytes übertragen werden, sollte am Ende kein Bit mehr übrig sein. Und wenn doch, dann ist irgendetwas schief gegangen und ein Error Flag wird gesetzt.  

int resolution = 20; /* Auflösung */
int error = 0;       /* Fehler flag */

void setup(){
  Serial.begin(9600);
  DDRD = 0x00;
  PORTD = 0x00; 
  Serial.println("Warte auf Signal...");
}

void loop(){
  listenToSignal();
}


void listenToSignal(){
  int lowPulse = 0;
  int highPulse = 0;
  byte bitNo = 0;
  byte incomingByte = 0;
  String msg = "";
  int msgLen = 0;
  bool msgCompleted = false;
  error=0;
  
  while(!(PIND & (1<<PD5))){/*Wait*/}
  if(startSequence()){
       
    while(!msgCompleted){
      lowPulse = 0;
      highPulse = 0;
      
      while(!(PIND & (1<<PD5))){
        delayMicroseconds(resolution);
        lowPulse++;
      }
      while(PIND & (1<<PD5)){
        delayMicroseconds(resolution);
        highPulse++;
      }
      if(highPulse < 45){
        incomingByte = (incomingByte << 1);
      }
      else if(highPulse < 70){
        incomingByte = (incomingByte << 1) + 1;   
      }
      else if(highPulse >= 150){
        msgCompleted = true;
        if(bitNo!=0){
          error = 1; 
        }
      }
      if(bitNo<7){
        bitNo++;
      }
      else{
        msg = msg + char(incomingByte);
        bitNo = 0;
        msgLen++;
        incomingByte = 0;
      }
    }
    if((!error)&&(msg.length()!=0)){
      Serial.println(msg);
    }
//    else{
//      Serial.println("error");
//    }
  }
}

bool startSequence(){
  int counter = 0;
  while(PIND & (1<<PD5)){
    delayMicroseconds(resolution);
    counter++;
  }
  if(counter > 300){
    return true;
  }
  else{
    return false;
  }
}

Und siehe da – es funktioniert!

Eigene Funkprotokolle - das kommt am Receiver an mit.

Protokoll 2 – Zahlen ziffernweise übertragen

Die Grundidee

Vorab: Diese Idee für ein eigenes Funkprotokoll stammt noch aus meinen ersten Tagen in der Arduino Welt. Die Übertragungsgeschwindigkeit lässt sich noch erheblich steigern – vielleicht habt Ihr ja Lust das umzuarbeiten. Mir kommt es hier auf das Prinzip an. Ich verwende diese Methode in meinem selbstgebauten Funk-Grillthermometer und die Übertragung arbeitet überaus zuverlässig. 

Also, die Idee ist simpel: Eine Zahl wird in Ziffern zerlegt und die Ziffern über die Signallänge kodiert (hier im Millisekundenbereich). Durch die sehr langen Signalzeiten braucht man keinen „C“ Code. Damit man Nullen übertragen kann, bekommt jedes Signal noch ein Offset. Anstelle der 0 wird eine 1 übertragen, anstelle der 2 eine 3, usw. Auf der Receiverseite wird das wieder herausgerechnet. 

Da die Signale extrem lang sind, heben sie deutlich sich vom Grundrauschen ab. Ein Startersignal ist entsprechend nicht erforderlich. 

Die Transmitterseite

Ich sende drei festgelegte Ziffern vorab als eine Art Sicherheitscode. Wird dieser nicht erhalten, so ist das Signal ungültig. Ansonsten dürfte der Sketch selbsterklärend sein. 

int txPin =  10; /* Transmitter Data Pin */
byte ziffer[10];
unsigned long number = 79631956; /* zu sendende Zahl */
int digits; /* Stellen: hier 10stellige Zahl */
 
void setup(){                
  Serial.begin(9600);
  pinMode(txPin, OUTPUT);  
  zerlegeZahl(number, digits); /* Zahl in Ziffernarray */
  Serial.println(digits);
  for(int i=digits; i>=0; i--){
    Serial.print(ziffer[i]);    
  }
}
 
void loop(){
  
  sendNumber();
  digitalWrite(txPin, LOW);
  delay(5000);
}
 
void sendNumber(){
  //"Sicherheitscode": 214
  sendeSignal(2);
  sendeSignal(1);
  sendeSignal(4);
  for(int i=0; i<digits; i++){
    sendeSignal(ziffer[i]);
  }
}

void sendeSignal(byte laenge){
  digitalWrite(txPin, HIGH);
  delay((laenge+1)*2); /* Die Signallänge ist (Ziffer + 1) mal 2, d.h. maximal 20ms */
  digitalWrite(txPin, LOW);
  delay(5);
}

void zerlegeZahl(unsigned long zahl, int& laenge){
  laenge = 0;
  while(zahl>0){
    ziffer[laenge]=zahl%10;
    zahl=zahl/10;
    if(zahl >= 0) laenge++;
  }
}

Der Receiversketch

Zunächst habe ich hier den Datenpin des Receivers an den Analogpin A0 gelegt. Ein Signal wird als „HIGH“ Signal interpretiert wenn am Datenpin eine Mindestspannung erreicht wird. Der Faktor für die Umrechnung der Signallänge in die Ziffern wurde empirisch ermittelt. In der großen do…while Schleife wird zunächst überprüft ob eine Abbruchbedingung vorliegt. Das könnte eine Gesamtzeitüberschreitung sein oder wenn die Zeit zwischen zwei Signalen überschritten wird, was als Ende der Signalübertragung gewertet wird. Zu kurze Signale werden als Störsignal verworfen. Am Ende wird erst der Sicherheitscode herausgerechnet und aus der Ziffernfolge die übertragene Zahl rekonstruiert. 

int rxPin = A0; /* Receiver Data Pin an Analogpin 0 */
const float calcFactor = 12.5; /* Umrechnungsfaktor Signallänge in Zahl */

void setup(){
  Serial.begin(9600);
  Serial.println("Los geht's....");
}

void loop(){
  listenForNumber();
}

void listenForNumber(){
  int maxDigit=-1; /* Gibt die Stellen der Zahl an */
  unsigned long number=0; 
  int digit[20];
  unsigned long listenBeginTime;
  unsigned long maxListenTime = 60000;
  unsigned long lastSignalTime = 0;
  const int maxBetweenSignalTime = 150;
  for(int i=0; i<3; i++) digit[i]=0;
  int threshold = 300;  // ein Analogread über 300 gilt als Wesswert
  unsigned int counter;  //Zähler für 50 Mikrosekundeneinheiten; 50 Mikrosekunden ist die Auflösung
  bool firstSignal = true;
  bool messageCompleted = false;
  bool noSignal; 
  
  listenBeginTime=millis();
  noSignal = false;
  do{
    counter = 0;
    while(analogRead(rxPin)<threshold){
      if((millis()-listenBeginTime) > maxListenTime){ /* Abbruch wg. Gesamtzeitüberschreitung */
        noSignal = true;
        break; 
      }
      if(!firstSignal && ((millis()-lastSignalTime) > maxBetweenSignalTime)){
        messageCompleted = true; /* nach 150 ms ohne Signal gilt die Übertragung als komplett */
        break;
      }
    }
    while(analogRead(rxPin)>threshold){  // Messe die Funksignallänge
      delayMicroseconds(50);
      counter++;
    }
    if(counter>9){ // nur wenn, dass Funksignal eine Mindestlänge hat, ist es kein Störsignal
      firstSignal = false; 
      lastSignalTime=millis();
      maxDigit++;
      digit[maxDigit]=((int)round(counter/calcFactor));
    }
    
  }while(!messageCompleted || noSignal);

  /* "Herausnehmen" des Sicherheitscodes */
  if((digit[0]==3&&digit[1]==2&&digit[2]==5)&&(messageCompleted==true)){
      unsigned long pot=1;
      for(int i=3; i<=(maxDigit); i++){ /* Umrechnung der relevanten Ziffern in die Zahl */ 
        number += (digit[i]-1)*pot;
        pot = pot*10;
      }
    Serial.println(number);
  }
}
Eigene Funkprotokolle: Ergebnis von Protokoll Nr. 2
Und auch diese Übertragungsmethode funktioniert

Wie gesagt, die Übertragung ist langsam, aber überaus stabil. Ich habe das Ganze noch in einer Bibliothek verpackt. Sie hat ggü. dem Code oben die Einschränkung, dass sie auf sechs Ziffern beschränkt ist. Ihr könnt die Bibliothek mit Beispielen hier (geht rechts auf das Download-Symbol) herunterladen und nach Belieben verbessern.

So, das war sehr lang. Aber ich hoffe, es hat Euch ein paar Anregungen gegeben. Viel Spaß, wenn Ihr eigene Funkprotokolle entwickelt! Wie immer freue ich mich über Rückmeldungen. 

7 thoughts on “Eigene Funkprotokolle

  1. Huhu Wolfgang,
    ich nehme an , du lernst gerne dazu …, daher mal einige Hinweise:

    – msgBit = (msgByte >> i) & 1; //wenig sinnvoll, schau dir mal die shift functions in Cores von Spence Kode an, (dort von mir eingebracht), das variable shiften kosten viel Zeit/Code,

    anstatt
    void sendeByte(byte msgByte) { /*Bit by bit senden */
    bool msgBit = 0;
    for (int i = 7; i >= 0; i–) { /* „Extrahieren“ der bits */
    msgBit = (msgByte >> i) & 1;
    sendeSignal(msgBit);
    }
    }

    //simple (less run time & programm code)
    void sendByte(uint8_t msgByte) {
    for (uint8_t i=8; i!=0; msgByte>>=1,i–) {
    sendSignal(msgByte & 1);
    }
    }

    Hinweis:
    Parameter von Funktionen sind immer lokale Kopien, die auf den Stack abgelegt sindUND können direkt genutzt/manipuliert werden. Ich sehe oft (auch hier), dass nochmals lokale Kopien benutzt werden – das ist sinnfrei.

    zerlegeZahl() soll offensichtlich binary zu packed bcd Darstellung also bin2bcd() realisieren, dazu gibt es wesentlich effizientere Methoden als die hier verwende, Goggle ist dein Freund!

    Allgemein viel Redundanz im Code Style, z.B.

    //anstatt
    if (sendBit == false) {
    delayMicroseconds(600);
    }
    else {
    delayMicroseconds(1200);
    }

    //übersichtlich
    delayMicroseconds(sendBit? 1200 : 600);

    Ein Protokoll zu definieren ohne Verwendung von Daten Strukturen (struct) ist selten übersichtlich u. effizient und ohne Zustandsdiagram sind oft/immer unberücksichtigte/unerkannte Zustände im Protokollablauf die dann für Überraschungen/Fehler sorgen. Es ist fehleranfällig State Machines mit if/else anstatt switch/case zu implementieren, da die Übersicht verloren geht.
    Gut möglich, wenn ich das Protokoll analysiere (und dazu gehören sämtliche Zeitbedingungen – auch der wd-tmr), dann finde ich unbeachte Zustände!

    Ein Protokoll State-Diagramm ist sehr hilfreich und eher Pflicht, weil ohne nur Bastelei.
    Das Protokoll beinhaltet keine Fehlererkennung und das ist ganz schlecht.
    Hier mal mein einfaches CRC-8 als Anregung, was auch für one-wire Protokolle verwendbr ist.

    //test data+crc: int8_t crc_test[] = {0x10,0x50,0xA9,0x0A,0x02,0x08,0x00,0x37};
    //*****************************************************************************
    uint8_t owi_crc(ds18xx_t *pSensor, const uint8_t num) { //X^8+X^5+X^4+X^0
    //*****************************************************************************
    uint8_t crc = 0;
    for (uint8_t j=0; j!=num; j++) {
    crc ^= pSensor->scratchpad[j];
    for (uint8_t i=0; i!=8; i++)
    crc = (crc>>1) ^ ((crc & 1)? 0x8C : 0);
    }
    return crc; //value:0 means no error if cal. based on data+crc
    }

    Eine Preamble (dein Code Wort oder sonst was) ist hier immer notwendig und hat nichts mit Rx Synchronization zu tun (womit die Tx Taktgewinnung im Rx gemeint wird), sondern mit der HW der Rx Filtertechnik. Simple ausgedrückt, damit die analogen Filter einschwingen, sonst hat die Filterantwort ein Phasendelay.
    Das gilt auch für IRed Empfänger IC, daher sollte der 38KHz Träger vorab der Datenübertragung immer aktiv geschaltet werden.

    Keep going!

    1. Vielen Dank! Ich lerne immer gerne dazu. Heute würde ich wohl insbesondere die Beiträge aus der Anfangszeit wohl auch ein bisschen anders schreiben. So umfassende Dinge bekomme ich aber nicht mehr eingepflegt. Das schaffe ich nicht in meiner One-Man-Show.

      1. Hut ab, wie du hier mit viel Motivation/Liebe die Themen presentierst, darstellst und als Person rüber kommst! Das könnte ich nicht mal ansatzweise. Ich verstehe was du schreibst und denke ähnlich!

        Das Arduino Framework ist zwar C++, ABER von den C++ Features ist da fast nichts verwendet. Der Code ist im wesentlichen eher C11 und da gibt es auch einige interessane Feature die du vielleicht noch nicht kennst wie „compound literals“ oder „sequence point innerhalb switch/while/if. Hierzu gibt es 2-4 gute Bookz, z.B. 21st Century C, Klemens oder Moderne C Programmierung, Schellig, die du hier findest http://libgen.rs/. Lese den Klemens, du wirst garantiert ein Level in C aufsteigen und überrascht sein, weil viel Neues jetzt dazu kommt.

        Ich programmiere auch in Python/Micropython seit 4 Jahren und denke erst 30% zu verstehen! Ein C Program in Python Syntax (was einfach zu erreichen ist) hat nichts mit Python zu tun. Ich denke, Python reicht für mich für Jahrzehnte und macht mir richtig viel Spass. Es gibt in der MicroPython Community 2-3 People von den kannst du lernen was Python wirklich kann/bedeutet, der eine davon ist der Autor von asyn, Peter Hinch https://github.com/peterhinch, schau dir seinen Code an und du lernst viel dazu. Ich bin immer noch am Anfang von meiner Reise!

  2. Hallo Wolle,
    danke für die Anleitung und den Code. Das war wirklich extrem informativ und hilfreich!

    Vor einigen Monaten hatte ich beim netten Chinesen aus dem Netz einen Sack voll 433Mhz Transmitter WL102-341 und Empfänger WL101-341 für einen sehr schmalen Taler erstanden. Ziel ist es eine Batteriegestützte Temperatur und Feuchteerfassung für jeden Raum zu bauen. Dabei kommt jeweils ein ATtiny85V, ein HTU21D und ein WL102-341 aufs Board.
    Empfängerseite ist ein WL101-341 mit einem ESP8266 (ESP-01) der von allen 433MHz Sendern die Werte annimmt und über WLAN und MQTT die Daten weiter schickt.

    Ich habe mich in den letzten 2 Tagen mit der 433MHz Übertragung beschäftigt und bestimmt 15 weitere graue Haare bekommen, denn es gab einige Klippen zu umschiffen.

    Der Code der RadioHead Library welchen Du in einem anderen Blogpost über 433MHz erwähnst ist zwar einfach zu nutzen, frisst aber enorm viel Speicher auf meinem kleinen Tiny85. Aus diesem Grund entschied ich mich für Deinen PWM-ähnlichen Code. Leider hatte ich immer wieder Probleme beim Empfang mit der Genauigkeit. Häufig waren Störsignale (kurze 160µs Blibs) zwischen den High-Leveln. Das konnte ich zwar noch etwas eindampfen indem ich die if … else etwas angepasst und die Pegellängen mehr eingegrenzt habe. Leider führte das immer noch zu ca 10% Ausschuss.
    Erst ein Blick in den Code von RadioHead brachte die Erleuchtung. Da steht nämlich in der RH_ASK.cpp:
    // Initialise the first 8 nibbles of the tx buffer to be the standard
    // preamble. We will append messages after that. 0x38, 0x2c is the start symbol before
    // 6-bit conversion to RH_ASK_START_SYMBOL
    uint8_t preamble[RH_ASK_PREAMBLE_LEN] = {0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x38, 0x2c};

    0x2a entspricht Binär 101010 und wird jeder Übertragung vorangestellt. Somit ergibt sich ein alternierendes Signal welches offenbar der Synchronisierung des Empfängers mit dem Sender dient. Ich habe somit Deinen Code etwas erweitert:
    void initMsg() { /* Initialisierung */
    uint8_t preamble_len = 32;
    do {
    digitalWrite(txPin, !(PIND & (1 << PIND5)));
    delayMicroseconds(500);
    } while (–preamble_len);

    digitalWrite(txPin, HIGH);
    delayMicroseconds(10000);
    digitalWrite(txPin, LOW);
    delayMicroseconds(600);
    }
    Tatsächlich werden die störenden Blibs beim Empfang auf ein Minimum reduziert. Eine Anpassung am Code des Empfängers ist nicht nötig, da der ja auf das 10ms Startsignal wartet und somit die vorangehenden Signale ignoriert.

    Ein weiterer Härtefall ist der Watchdog Timer im ESP, sowie die Interrupts. Diese sollte man in jedem Fall abschalten während das Signal analysiert wird. Ich habe dafür am Begin der listenToSignal() Routine noch folgendes:
    ESP.wdtDisable();
    hw_wdt_disable();
    noInterrupts();
    Natürlich am Ende nicht vergessen wieder einzuschalten 😉
    Dann ist mir aufgefallen, daß man die Empfangsroutine noch besser und schneller gestalten kann, indem man statt den while(PIND & (1<= 500 && highPulse <= 700) {
    incomingByte = (incomingByte << 1);

    Als Dritte und letzte Klippe bin ich mit dem Debugging der Pulslängen beim Empfang böhöse ins Straucheln gekommen. Ich hab mich immer gewundert, warum der Empfang der ersten paar Bytes problemlos klappt und dann immer wieder falsche Werte liefert. Ich hatte direkt nach dem Messen der Pulslänge ein serial.print(…) im Code. Und genau das hat mir das ganze Timing verhauen. Der ESP (wie auch der Tiny85) vertrödelt für das Senden der UART-Daten so viel Zeit, daß er die nächsten 1-2 Bits des Senders "verpasst". Also: in Zeitkritischen Routinen niemals Debugging-Code einfügen 😉

    Danke für Deinen Anstoß und die Idee der Codierung. Weiter so!

    1. Hallo David,

      danke für den reichhaltigen Kommentar! Die Erweiterung um die Initialisierung ist ein hilfreicher Hinweis. Aber auch der Hinweis zu dem ESP Watchdog – das hat mich bei anderen Projekten schon einige Zeit gekostet.

      Ich setze mittlerweile meistens HC-12 Module ein. Die sind zuverlässig und einfach anzusteuern. Fast schon langweilig wenn etwas einfach so funktioniert. Und leider sind die Teile etwas teurer.

      Viel Spaß noch bei deinen Projekten!

      VG, Wolle

  3. Tolle Idee!
    Leider habe ich Probleme mit der Umsetzung auf ESP32.
    Im letzten Abschnitt der 2. Funklösung Receive-Modul an der Stelle:
    while(!messageCompleted || noSignal);
    Kann ich noch einen Breakpunkt setzen, beim nächsten Stepp bootet der ESP32 ohne Ende.
    Hab mit Hardwaredebugging in VS Code PlatformIO keinen Erfolg.

    Gibt es irgend welche Erkenntnisse, die weiterhelfen könnten?

    1. Kann es sein, dass hier der Watchdog Timer des ESP32 zuschlägt? Jedenfalls ist das ein Problem beim ESP8266. Der startet bei solchen Leerschleifen nach 3 oder 8 Sekunden (Hardware / Software Watchdog. Wenn das der Fall ist, müsste man dem ESP32 zwischendurch etwas zu tun geben, den Watchdog Timeout verlängern oder den Watchdog abstellen, wenn möglich. Ich habe leider gerade nicht so viel Zeit, das im Detail zu recherchieren. Ich hoffe das hilft erstmal weiter. Sonst melde dich nochmal.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert