IR Fjernbetjening

Fra Holstebro HTX Wiki
Skift til: navigering, søgning
IR Fjernbetjening med modtager

Siden her dokumenterer en lille fjernbetjening[1] som består af et modul med en 27 knaps fjerbetjenings-enhed og en IR Modtager der kan opfange de signaler som fjernbetjeningen sender ud, og med en Arduino eller lignende microcontroller kan man fortolke de signaler der sendes.

Siden beskriver udviklingsprocessen frem mod et færdigt software modul til Arduino og en løsning til et I2C-modul.

Teori omkring IR fjernbetjeninger

Langt de fleste fjernbetjeninger i dag benytter sig af en 38 kHz moduleret IR-sending, hvor IR-lysdioden tændes og slukkes med 38 kHz, og der så tændes og slukkes for dette modulerede signal med nogle tider omkring 0,5 - 1 ms.

Ideen i dette er at man kan sende med en pæn effekt ved at tænde lysdioden i et meget kort tidsrum (ca. 1 us) og så have den slukket i ca. 26 us hvilket giver frekvensen på 38 kHz. På denne måde kan man få en fornuftig rækkevidde.

En anden ting der kan være med til at forøge rækkevidden betragteligt er at når det er fast 38 kHz man skal modtage, så kan man filtrere og forstærke ret meget, og man kan endda lave AGC på signalet så man har en god styring af hvornår man modtager 38 kHz og hvornår man ikke gør det - dette giver en meget effektiv støjundertrykkelse, så det kun er nyttesignalet der når frem.

Alt dette ligger integreret i den lille 3-bens modtager som også er beskrevet under IR Modtager.

Målinger på det modtagne signal

Til denne beskrivelse er det specifikt den indkøbte fjernbetjening fra Kina-land der arbejdes med. Andre fjernbetjeninger har helt sikkert andre måder at kommunikere på.

Modtager-modulet kobles op med +5V og GND fra en Arduino og så måles der på databenet. Ved en tilfældig tast ses følgende signal:

Modtagelse af signal fra fjernbetjening
Modtagelse af signal fra fjernbetjening

Som det kan ses på signalet, så giver modtageren høj ud i passiv tilstand, og det modtagne signal starter med en lang periode lav, og en knapt så lang periode høj. Disse perioder måles til 9,1 ms høj og 4,5 ms lav. Disse sendes for at fastlægge niveauet og trimme modtageren ind på signal-styrken, men kan også anvendes til at identificere starten af en tast, så det er det første der skal kigges efter.

Dernæst kommer der 8 lave og 8 høje signaler der begge er ca. 500 us lange. Dette tolkes som at der sendes 8 gange 0-bit, så en kort høj efter en kort lav kan forstås som et 0, og de 8 bit er altså en hel byte med værdien 0x00.

Efter dette kommer der igen 8 lave og 8 høje signaler, men denne gang er de høje signaler ca. 1700 us lange, og det tolkes som 1-bit, så disse 8 bit er en hel byte med værdien 0xFF.

Med denne tolkning af 0-bit og 1-bit kan det næste der modtages tolkes som 16 bit med et værdi-indhold, og ved at måle på flere forskellige taster kan man se, at det kun er disse værdier der er forskellige fra gang til gang.

Kigger man på det viste signal her oven over, så kan man tolke de første 8 bit af værdi-indholdet som 00110000 og de sidste 8 bit som 11001111 altså det bit-mæssige inverterede signal.

Til sidst i signalet kommer der en lav periode på 500 us, inden signalet igen lægger sig konstant høj.

Indholdet i det modtagne signal

Som en opsamling på målingerne kan der konstateres at der startes med to lange perioder hhv. lav og høj, hvorefter der kommer 4 bytes hvor 0-bit kodes som 500 us lav, 500 us høj og 1-bit kodes som 500 us lav og 1700 us høj.

De 4 bytes kan værdi-mæssigt reduceres til 1 byte med kontrol-indhold rundt omkring, hvor de to første bytes skal være hhv. 0x00 og 0xFF. Den tjedje byte er så værdi-indholdet og den fjerde er samme byte blot inverteret.

Ved at kontrollere alle disse tider og parametre inden for rimelige grænser, så vil man kunne tolke det modtagne uden at man modtager noget tilfældigt støj, som kan blive forstået som en rigtig tast.

Hardwaren til de første versioner

Til de første versioner af softwaren, der bare laves i Arduinoen, kobles det viste op, hvor der tilsluttes VCC og GND fra Arduinoen og D0 føres tilbage til digital pin 2 (det er også testet på pin 3). Helt simpelt.

Opstilling til test af IR Fjernbetjening
Opstilling til test af IR Fjernbetjening

Software til test af modtagelsen

Der skal modtages på et input og til testen skrives der ud i den serielle konsol, så der laves en initialisering som følger:

// IR-receiver
int IR = 2;
int LED = 13;

// the setup routine runs once when you press reset:
void setup() {
  // initialize serial communication at 9600 bits per second:
  Serial.begin(9600);
  // make the IR pin an input:
  pinMode(IR, INPUT);
  pinMode(LED, OUTPUT);
  digitalWrite(LED, LOW);
}

LED benet bruges som test af om der modtages.

Til at håndtere modtagelsen erklæres en række variabler som følger:

int state = 0;
int bitCnt = 0;
long lastTime;
long tid;
uint8_t by1;
uint8_t by2;
boolean pakkeSlut = false;

state er en central variabel - forklares nærmere senere.

bitCnt anvendes til at tælle sig frem gennem en bytes 8 bit.

lastTime er tidspunktet for det sidste skift i niveau.

tid er den tid i mikrosekunder der er gået siden sidste skift

by1 og by2 er to bytes der anvendes til at opsamle de modtagne bits i til 3. og 4. byte.

pakkeSlut angiver at nu er der modtaget en hel pakke fra en tast.

Anvendelsen af states i softwaren

For at kunne holde styr på hvor langt man er kommet i modtagelsen af en taste-pakke, så er der variablen state der gennemløber en række værdier, som hver symboliserer progressionen i pakken. Til første test-version er der defineret 13 forskellige states, der har følgende betydninger:

  1. Er uden for pakken og indgangen er høj - venter på den lange lave start-indikator. Når der registreres lav skiftes til state 1.
  2. I dette state modtages den lange lave. Når der registreres høj tjekkes om tiden på den lange lave er korrekt, og hvis den er skiftes til state 2, ellers tilbage til state 0.
  3. I dette state modtages den lange høje. Når der registreres lav tjekkes om tiden på den lange høje er korrekt, og hvis den er skiftes til state 3, ellers tilbage til state 0.
  4. I 3. og 4. state modtages den første byte 0x00 - der kontrolleres at tiden både høj og lav ligger på ca. 500 us.
  5. Er modtagelsen af det høje niveau. Når der er modtaget 8 bit skiftes til state 5.
  6. I 5. og 6. state modtages anden byte der skal være 0xFF - igen kontrolleres at tiderne er korrekte.
  7. Er modtagelsen af det høje niveau. Når der er modtaget 8 bit skiftes til state 7.
  8. I dette state modtages det lave niveau i de 8 bit som udgør tredje byte. Når der skiftes til høj kontrolleres at tiden ligger på ca. 500 us.
  9. I dette state modtages det høje niveau i de 8 bit som udgør tredje byte. Når der skiftes til lav kontrolleres om tiden ligger på ca. 500 us, så det er en 0-bit, eller om tiden ligger på ca. 1700us, så det er en 1-bit. Efter modtagelsen af de 8 bit skiftes til state 9.
  10. Fungerer som state 7, bare på fjerde byte.
  11. Fungerer som state 8, bare på fjerde byte.
  12. I dette state modtages den sidste lave puls i pakken. Når der skiftes til høj kontrolleres der om tiden ligger omkring 500 us, hvis det er tilfældet skiftes til state 12, ellers nulstilles til state 0 (pakken smides væk).
  13. Her ventes lidt (6 ms) og pakkes sættes som godkendt.

Softwaren der håndterer de viste states er implementeret i følgende kode der kører i loop-funktionen:

void loop() {
  tid = micros() - lastTime;
  switch (state) {
    case 0:
      if (! digitalRead(IR)) {
        state = 1;
        digitalWrite(LED, HIGH);
      } else {
        lastTime = micros();
        digitalWrite(LED, LOW);
      }
      break;
    case 1:
      if (digitalRead(IR)) {
        if ((tid > 8800) && (tid < 9300)) {
          lastTime = micros();
          state = 2;
        } else {
          Serial.print("Fejl ");
          Serial.print(state);
          Serial.print(" tid ");
          Serial.println(tid);
          state = 0;
        }
      }
      break;
    case 2:
      if (! digitalRead(IR)) {
        if ((tid > 4200) && (tid < 4800)) {
          lastTime = micros();
          state = 3;
          bitCnt = 0;
        } else if ((tid > 2000) && (tid < 2600)) {
          lastTime = micros();
          state = 13;
        } else {
          Serial.print("Fejl ");
          Serial.print(state);
          Serial.print(" tid ");
          Serial.println(tid);
          state = 0;
        }
      }
      break;
    case 3:
      if (digitalRead(IR)) {
        if ((tid > 400) && (tid < 800)) {
          lastTime = micros();
          state = 4;
        } else {
          Serial.print("Fejl ");
          Serial.print(state);
          Serial.print(" tid ");
          Serial.println(tid);
          state = 0;
        }
      }
      break;
    case 4:
      if (! digitalRead(IR)) {
        if ((tid > 400) && (tid < 800)) {
          lastTime = micros();
          state = 3;
          bitCnt++;
          if (bitCnt == 8) {
            state = 5;
            bitCnt = 0;
          }
        } else {
          Serial.print("Fejl ");
          Serial.print(state);
          Serial.print(" tid ");
          Serial.println(tid);
          state = 0;
        }
      }
      break;
    case 5:
      if (digitalRead(IR)) {
        if ((tid > 400) && (tid < 800)) {
          lastTime = micros();
          state = 6;
        } else {
          Serial.print("Fejl ");
          Serial.print(state);
          Serial.print(" tid ");
          Serial.println(tid);
          state = 0;
        }
      }
      break;
    case 6:
      if (! digitalRead(IR)) {
        if ((tid > 1300) && (tid < 2000)) {
          lastTime = micros();
          state = 5;
          bitCnt++;
          if (bitCnt == 8) {
            state = 7;
            bitCnt = 0;
            by1 = 0;
          }
        } else {
          Serial.print("Fejl ");
          Serial.print(state);
          Serial.print(" tid ");
          Serial.println(tid);
          state = 0;
        }
      }
      break;
    case 7:
      if (digitalRead(IR)) {
        if ((tid > 400) && (tid < 800)) {
          lastTime = micros();
          state = 8;
        } else {
          Serial.print("Fejl ");
          Serial.print(state);
          Serial.print(" tid ");
          Serial.println(tid);
          state = 0;
        }
      }
      break;
    case 8:
      if (! digitalRead(IR)) {
        if ((tid > 1300) && (tid < 2000)) {
          lastTime = micros();
          state = 7;
          bitCnt++;
          by1 = (by1 << 1) + 1;
          if (bitCnt == 8) {
            state = 9;
            bitCnt = 0;
          }
        } else if ((tid > 400) && (tid < 800)) {
          lastTime = micros();
          state = 7;
          bitCnt++;
          by1 = by1 << 1;
          if (bitCnt == 8) {
            state = 9;
            bitCnt = 0;
          }
        } else {
          Serial.print("Fejl ");
          Serial.print(state);
          Serial.print(" tid ");
          Serial.println(tid);
          state = 0;
        }
      }
      break;
    case 9:
      if (digitalRead(IR)) {
        if ((tid > 400) && (tid < 800)) {
          lastTime = micros();
          state = 10;
        } else {
          Serial.print("Fejl ");
          Serial.print(state);
          Serial.print(" tid ");
          Serial.println(tid);
          state = 0;
        }
      }
      break;
    case 10:
      if (! digitalRead(IR)) {
        if ((tid > 1300) && (tid < 2000)) {
          lastTime = micros();
          state = 9;
          bitCnt++;
          by2 = (by2 << 1) + 1;
          if (bitCnt == 8) {
            state = 11;
            bitCnt = 0;
          }
        } else if ((tid > 500) && (tid < 700)) {
          lastTime = micros();
          state = 9;
          bitCnt++;
          by2 = by2 << 1;
          if (bitCnt == 8) {
            state = 11;
            bitCnt = 0;
          }
        } else {
          Serial.print("Fejl ");
          Serial.print(state);
          Serial.print(" tid ");
          Serial.println(tid);
          state = 0;
        }
      }
      break;
    case 11:
      if (digitalRead(IR)) {
        if ((tid > 400) && (tid < 800)) {
          lastTime = micros();
          state = 12;
        } else {
          Serial.print("Fejl ");
          Serial.print(state);
          Serial.print(" tid ");
          Serial.println(tid);
          state = 0;
        }
      }
      break;
    case 12:
      if (tid > 6000) {
        pakkeSlut = true;
        state = 0;
      }
      break;
    case 13:
      if (tid > 6000) {
        // Afslut af en repeat
        state = 0;
      }
      break;
  }

Visning af det modtagne

Når der i loop bliver registreret at taste-pakken er afsluttet, så afvikles følgende kode, som udskriver de to værdier, der gerne skal være hinandens inverterede (bit-mæssigt).

  if (pakkeSlut) {
    digitalWrite(LED, LOW);
    Serial.print(by1);
    Serial.print(" ");
    Serial.println(by2);
    pakkeSlut = false;
  }

For at det modtagne ikke skal skrives ud igen sættes pakkeSlut til false.

Test af modtagelse

Modtagelsen kunne fungere og kunne gengive tasterne, og der kunne findes forskellige tal for alle 27 taster.

Ved de første test viste modtagelsen sig dog usikker, og derfor indførtes i koden nogle udskrifter de steder hvor modtagelsen fejlede, med en test-udskrift af tiden som f.x. den viste kode:

        } else {
          Serial.print("Fejl ");
          Serial.print(state);
          Serial.print(" tid ");
          Serial.println(tid);
          state = 0;
        }

Dette gav resultater hvor den viste fejltider fra state 2 med tider på ca. 2300 us.

Ved nærmere målinger viste det sig at når man holder en tast nede på fjernbetjeningen, så sender den nogle små pakker uden meget indhold, som indikerer at den samme tast stadig er hold nede. Det ser ud som vist her:

Modtagelse når knappen holdes
Modtagelse når knappen holdes

Ved at zoome ind på denne ekstra repeat-pakke kan man måle tiderne. Pakken ser ud som vist:

Repeat-pakke når knappen holdes
Repeat-pakke når knappen holdes

Målinger af tiderne viser at den lange lave er som før 9,1 ms, mens den lange høje er 2,3 ms som softwaren også indikerede. Repeat-pakken afsluttes med en kort lav på 500 us som de andre pakker.

For at forhindre at dette giver fejl, så indføres en ekstra test i state 2, hvor det sendes hen til state 13, hvis der modtages en lang høj på 2,3 ms. Dette state 13 afslutter så bare, således at der ikke reageres på repeat-pakkerne. Det kan udvides, hvis man på et tidspunkt ønsker en form for reaktion, hvis knapperne holdes nede.

Denne software ligger i denne ZIP-fil som fjernbetjening1.ino.

Sluttest af første version

Ved test at softwaren kunne IR-modtageren opfange fjerbetjeningens knapper med ganske pæn sikkerhed.

Udskriften viste hver gang at der blev modtaget et tal og det bit-mæssige inverterede.

Fik den lov til at stå, så kunne der enkelte gange komme en lille fejllæsning af pulser på 50 - 300 us, men det var på ingen måde noget der kunne komme i nærheden af at blive tolket som en tast.

Fjernbetjeningen må konkluderes at have en fin protokol, der kan modtages fejlsikkert med Arduinoen.

Anden version med oversættelse af knapper

For at gøre det mere brugervenligt at anvende modulet, så kan man lave en oversættelse af de enkelte tal-koder til en tekststreng, der angiver hvilken knap der er trykke på.

Men først udføres lige den sidste kontrol, nemlig om de to modtagne tal er hinandens inverterede. Dette gøres i følgende kode:

  if (pakkeSlut) {
    digitalWrite(LED, LOW);
    by2 = ~by2;  // Inverter alle bit i den anden byte
    if (by1 == by2) {  // Tjek om den er korrekt modtaget
      Serial.println(translate(by1));  // Udskriv oversættelsen
    }
    pakkeSlut = false;
  }

For at definere en nem sammenhæng mellem tal-koden og den tekststreng der skal vises, så oprettes en structure, der indeholder et tal af typen uint8_t og en String, som vist her:

struct tegn {  // Struktur til oversættelse af tal til streng
  uint8_t dat;
  String text;
};

Med denne structure kan der oprettes et array med alle tal-koderne og strengene, som vist her:

// Tabel med alle tasterne oversat fra tal til streng
const int antalTaster = 27;
tegn tabel [antalTaster] =
{
{104, "0"},
{48, "1"},
{24, "2"},
{122, "3"},
{16, "4"},
{56, "5"},
{90, "6"},
{66, "7"},
{74, "8"},
{82, "9"},
{162, "CH-"},
{98, "CH"},
{226, "CH+"},
{34, "Skip-"},
{2, "Skip+"},
{194, "Play"},
{224, "-"},
{168, "+"},
{144, "EQ"},
{152, "FOL-"},
{176, "FOL+"}
};

Endelig oprettes en funktion der kan foretage oversættelsen, ved at opsøge tal-koden i arrayet, og returnere den tilhørende tekst-streng:

String translate(uint8_t data) {
  for (int n = 0; n < antalTaster; n++) {
    if (tabel[n].dat == data) {
      return tabel[n].text;
    }
  }
  return "Ej fundet " + String(data);
}

Denne software ligger i denne ZIP-fil som fjernbetjening2.ino.

Test af anden version

Testen af den anden version af softwaren viser at knapperne på fjernbetjeningen kommer sikkert og fornuftigt igennem, og bliver skrevet ud på den serielle konsol.

Refleksioner på anden version

Denne funktion er velfungerende til at vise at fjernebetjeningen fungerer som den skal, men den vil være svær at få til at fungere sammen med anden software, da den er baseret på at der hele tiden testes på indgangen, så der kan laves en relativt præcis måling af tiden.

Det er ikke realistisk at få dette skrevet sammen med anden software, der evt. har noget tidsforbrug eller endnu værre delays.

Løsningen på dette vil være at opbygge en interrupt-drevet version. Dette vil være mest praktisk at gøre som et modul der kan inkluderes.

Tredje version som modul med interrupt

For at kunne eksistere sammen med anden software der bruger tid på andre ting, og stadig måle på skiftene inden for ca. 50 us præcision, så er det nøvendigt at kunne reagere på interrupt, og helst et skift i niveau.

På Arduinoen er dette begrænset til ben 2 og 3, så det er et af disse ben der skal anvendes som input fra IR-modtageren.

Anvendelsen af remote-modulet

Som softwaren er implementeret ligger modulet sammen med et testprogram Fjernbetjening3.ino i denne ZIP-fil.

For at initialisere module skrives følgende kode:

#include "remote.h"

Remote remote(2); 	// Signal ind på pin 2

Hvor remote bliver til et objekt, der kan kommunikere med fjernbetjeningen via digitalt input ben 2.

setup-rutinen starter bare den serialle port, så man kan se resultaterne i konsollen.

I loopet tjekkes der bare efter om der er en ny indtastning klar fra fjernebetjeningen ved at teste på remote.available, og når den er klar udskrives en oversat version af indtastningen. loop-koden ser ud som følger:

// the loop routine runs over and over again forever:
void loop() {
  if (remote.available()) {
    Serial.println(remote.translate());
  }
  delay (100);
}

remote.translate() kunne være udskiftet med remote.getByte() der i stedet ville have give det rå tal der kommer fra fjernbetjeningen.

Indholdet i interface-filen remote.h

I remote.h er der følgende kode:

#include "Arduino.h"
#include <inttypes.h>

class Remote
{
  public:
    Remote(uint8_t pin);	// Only pin 2 and 3 are available
    bool available();
    uint8_t getByte();
    String translate();
  private:
    static void serviceRemote();
};

De to includes er for at kunne arbejde med arduino-softwaren.

class Remote er definitionen af klassen, hvor de public ting er det der kan arbejdes med ude for modulet.

Remote er konstructoren det definerer det aktuelle objekt - i dette tilfælde kan der kun erklæres en instans af objektet på grund af problemer med at attache til interruptet.

available svarer på om der er modtaget noget.

getByte og translate giver værdien (hvis der er modtaget noget) hvor getByte kommer med det rå tal der er modtaget i pakken, mens translate kommer med en oversat version som en tekst-streng.

serviceRemote anvendes internt i klassen, og den skal være erklæret static for at den kan attaches til et interrupt.

Selve koden i remote.cpp

Hele remote.cpp er baseret på interrupt-rutinen serviceRemote, der kaldes hver gang inputbenet skifter niveau.

Det der samles op med interrupt-rutinen kan så tilgås med de interface-rutiner der er angivet i remote.h, uden at der kan rettes i det der samles op.

Variablerne er anvendet er følgende:

uint8_t _pin;
uint8_t state = 0;
uint8_t bitCnt = 0;
uint8_t byteCnt = 0;
long lastTime;
long tid;
uint8_t modtag;
bool _newKey;
uint8_t _key;

_pin husker hvilket ben interruptet kommer ind på.

state angiver i hvilken fase af pakken der modtages.

bitCnt tæller hvilken bit der modtages i en byte.

byteCnt tæller hvilken byte der modtages.

lastTime husker det sidste tidspunkt for et skift, så der kan beregnes en tid på den sidst modtagne periode.

tid indeholder tiden for den sidste periode.

modtag indeholder det der aktuelt er ved at blive modtaget

_newKey angiver om der er læst en hel pakke med en tast til ende.

_key indeholder den læste tast der er modtaget i pakken som et tal.

Det er kun de to sidste variabler den anvendes af interface-rutinerne, mens resten af variablerne anvendes til at holde styr på modtagelsen af pakken.

Interface-rutinen available returnerer status på _newKey, uden at der ændres på den, for at angive om der er modtaget en ny tast.

Koden ser ud som følger:

bool Remote::available() {
  return _newKey;
}

Interface-rutinen getByte henter den tast der er læst, og indikerer i _newKey at den er læst. Tastens værdi er det tal der står i pakken.

Koden ser ud som følger:

uint8_t Remote::getByte() {
  if (! _newKey) return 255;
  _newKey = false;
  return _key;
}

Interface-rutinen translate returnerer en tekstlig fortolkning af den modtagne tast. Det modtagne tal slås op i en tabel

Koden ser ud som følger:

String Remote::translate() {
  if (! _newKey) return "No key";
  _newKey = false;
  for (int n = 0; n < antalTaster; n++) {
    if (tabel[n].dat == _key) {
      return tabel[n].text;
    }
  }
  return "Not found " + String(_key);
}

Tabellen der slås op i er defineret som følger:

struct tegn {  // Struktur til oversættelse af tal til streng
  uint8_t dat;
  String text;
};

// Tabel med alle tasterne oversat fra tal til streng
const int antalTaster = 27;
tegn tabel [antalTaster] =
{
{104, "0"},
{48, "1"},
{24, "2"},
{122, "3"},
{16, "4"},
{56, "5"},
{90, "6"},
{66, "7"},
{74, "8"},
{82, "9"},
{162, "CH-"},
{98, "CH"},
{226, "CH+"},
{34, "Skip-"},
{2, "Skip+"},
{194, "Play"},
{224, "-"},
{168, "+"},
{144, "EQ"},
{152, "FOL-"},
{176, "FOL+"}
};

Konstructoren til classen tager sig af at sætte de forskellige ting op, så modulet kan fungere.

Koden ser ud som følger:

Remote::Remote(uint8_t pin) {
  _pin = pin;
  pinMode(_pin, INPUT);
  if (_pin == 2) {
    attachInterrupt(0, serviceRemote, CHANGE);
  } else if (_pin == 3) {
    attachInterrupt(1, serviceRemote, CHANGE);
  }
  _newKey = false;
  _key = 255;
}

serviceRemote bliver i konstructoren koblet til interruptet på den angivne pin, og der angives at interruptet skal ske på alle skift.

Inde i interrupt-rutinen arbejdes der igennem er række states, hvor der hele tiden kontrolleres om det modtagne er tidsmæssigt og indholdsmæssigt som det skal være.

I state 0 ventes på staten af en pakke - alle fejl-situationer sættes tilbage til state 0. Koden venter stort set på et lavt niveau (altså et skift fra højt til lavt, som er starten på enhver pakke) og når det modtages sættes state til 1.

Starten af rutinen med state 0 ser ud som følger, hvor der beregnes hvor mange mikrosekunder der er gået siden interrupt-rutinen blev kaldt sidst.

void Remote::serviceRemote () {
  tid = micros() - lastTime;
  lastTime = micros();
  switch (state) {
    case 0:  // Venter mellem pakker
      if (! digitalRead(_pin)) {
        state = 1;
      }
      break;

I state 1 ventes på at der et højt niveau, og hvis det kommer og tiden af det lave niveau var omkring 9100 us, så fortsættes ind i state 2.

Alle fejlhåndteringer bliver smidt tilbage i state 0, og hvis det næste skift giver en høj bliver den bare stående der. Hvis en fejl i state 0 kommer ind i en lav, så vil den skifte til state 1, hvor den så ikke vil blive godkendt til state 1, så den kommer direkte tilbage til state 0, fordi det ikke er en lang puls på ca. 9100 us. Strter programmet midt i en pakke, så vil den bare stå og veksle mellem state 0 og state 1, og når pakken slutter, så vil den stå klar i state 0, så den vil kunne fange den næste pakke, der begynder med 9100 us lav.

State 1 ser ud som følger:

    case 1:  // Venter i den lave marker i starten af pakken
      if (digitalRead(_pin)) {
        if ((tid > 8800) && (tid < 9300)) {
          state = 2;
        } else {
          state = 0;
        }
      }
      break;

I state 2 er den høje del af markeren modtaget. Hvis den modtages i en størrelse på ca. 4500 us, så accepteres den, og der fortsættes i state 3, samtidig med at der gøres klar til at modtage de 4 bytes der er i pakken. Hvis tiden der modtages er ca. 2300 us, så er det en pakke der indikerer at tasten holdes nede, så her gås til state 5, hvor der ventes på afslutningen af den type pakke.

    case 2:  // Venter i den høje marker i starten af pakken
      if (! digitalRead(_pin)) {
        if ((tid > 4200) && (tid < 4800)) {
          state = 3;
          bitCnt = 0;
          byteCnt = 0;
          _newKey = false;
        } else if ((tid > 2000) && (tid < 2600)) {
          state = 5;
        } else {
          state = 0;
        }
      }
      break;

I state 3 er det modtagelsen af den lave tid der læses hvis den ligger omkring 600 us, så accepteres den og der sendes videre til state 4. En lille krølle her er at afslutningen af pakken kommer ved et ekstra lavt signal. Det kan gendkendes på at byteCnt er kommet op på 4. Hvis det er tilfældet sættes _newKey til true, så der indikeres at der er modtaget en gyldig pakke, og state sættes til 0, så der er klar til at modtage en ny pakke.

    case 3:
      if (digitalRead(_pin)) {
        if ((tid > 400) && (tid < 800)) {
          state = 4;
        } else {
          state = 0;
        }
        if (byteCnt == 4) {
          _newKey = true;
          state = 0;
        }
      }
      break;

I state 4 sker selve tolkningen af om det er 0 eller 1 der læses ved at det er ca. 1700 us for 1 og 600 us for 0 dette lagres i modtag. Hvis der ikke sker en fejl, så skiftes tilbage til state 3, for at kontrollere den lave periode.

Der tælles hvor mange bit der modtages, og når der er modtaget 8 bit, så er det en hel byte. Ved modtagelsen af en hel byte gøres der klar til at modtage den næste og der tolkes så på den modtagne byte som følger:
Første byte skal være 0x00.
Anden byte skal være 0xFF.
Tredie byte er den modtagne tast - der kan ikke kontrolleres noget, den lagres bare i _key.
Fjerde byte skal være det inverterede af den modtagne tast.

Hvis der ikke modtages det forventede så forkastes pakken, og der ventes i state 0. Dette sker i følgende kode:

    case 4:
      if (! digitalRead(_pin)) {
        if ((tid > 1300) && (tid < 2000)) {
          modtag = (modtag << 1) + 1;
        } else if ((tid > 400) && (tid < 800)) {
          modtag = modtag << 1;
        } else {
          state = 0;
        }
        if (state == 4) {
          state = 3;
        }
        bitCnt++;
        if (bitCnt == 8) {
          bitCnt = 0;
          switch (byteCnt) {
            case 0:
              if (modtag != 0x00) {
                state = 0;
              }
              break;
            case 1:
              if (modtag != 0xFF) {
                state = 0;
              }
              break;
            case 2:
              _key = modtag;
              break;
            case 3:
              modtag = ~modtag;
              if (modtag != _key) {
                state = 0;
              }
              break;
          }
          bitCnt = 0;
          byteCnt++;
        }
      }
      break;

I state 5 ventes blot på afslutningen af en pakke der indikerer at tasten holdes nede. Dette ignorereres af softwaren. Man kunne lave en funktionalitet der kommer med tasten igen efter et antal repeats, så det vil virke nogenlunde som et PC-tastatur.

    case 5:
      if (digitalRead(_pin)) {
        state = 0;
      }
      break;

Som softwaren er implementeret ligger modulet sammen med et testprogram Fjernbetjening3.ino i denne ZIP-fil.

Test af tredie version

Testen af den tredie version af softwaren viser at knapperne på fjernbetjeningen kommer sikkert og fornuftigt igennem, og bliver skrevet ud på den serielle konsol. Dette fungerer uanset at der i main-loopet er en delay på 100 ms, som vil kunne symbolisere anden aktivitet.

Fjerde version som et I2C-modul - Princippet i I2C Fjernbetjening

Det monterede print med IR-modtageren på

For at flytte yderligere belastning væk fra den centrale processor, og lade anden processorkraft tage sig af håndteringen af IR-fjernbetjeningen, så kan man lægge modtagelsen ud i et I2C modul, hvor en ATTiny står for modtagelsen af signalet, og lægger det klar til at en Arduino kan hente det.

En anden årsag kan også være at man skal anvende de to kanttriggede interrupts på Arduinoen til noget andet, så da man kan have mange adresser på I2C Bussen, så er det en nem måde at fordele hardwaren på.

Strukturen i I2C Fjernbetjening

Blokdiagrammæssigt er fjernbetjeningen meget enkel, da der er en microcontroller af typen ATTiny45, der skal hente signalerne ind fra IR-modtageren og kunne kommunikere dem til Arduinoen via en I2C kommunikationsport. Ud over dette er der en lysdiode til at vise at der modtages noget, samt en power-lysdiode.

Blokdiagram over I2C Fjernbetjening
Blokdiagram over I2C Fjernbetjening

I2C Fjernbetjening hardware

I2C Fjernbetjening er lagt ud ved hjælpe af Eagle og layoutet med en PDF til at lave print efter kan hentes i denne ZIP-fil.

Det samlede diagram ser ud som følger:

Total-diagram over I2C Fjernbetjening
Total-diagram over Fjernbetjening

Det monterede print med IR-modtageren ved siden af

Printet kan monteres efter følgende layout, hvor resultatet ses på billedet til højre:

Komponent-layout over I2C Fjernbetjening
Komponent-layout over I2C Fjernbetjening

Komponentlisten til I2C Fjernbetjening er som følger:

Komponent Type Værdi
U1 MikroController ATTiny 45 (vendes rigtigt - i sokkel)
U1 IC Sokkel 8 bens IC-sokkel (vendes rigtigt)
D1 Småsignal diode 1N4148 (vendes rigtigt)
LED1 Lysdiode 5mm LED Grøn (vendes rigtigt)
LED2 Lysdiode 5mm LED Gul (vendes rigtigt)
SV1 Jumper stik 2 polet pin-række med Jumper
SV2 Header stik 3 polet header-række til IR-modtageren
PR1 Molex stik 2x3 polet pin-række til Moles fladkabel-stik
R1 Modstand 10k ohm
R2 Modstand 680 ohm
R3 Modstand 680 ohm
R4 Modstand 220k ohm
R5 Modstand 2k2 ohm
C1 Polyester kondensator 100nF
C2 Elektrolyt kondensator 10uF (vendes rigtigt)
H1 - H4 Monteringshul 10mm M3 gevindstag med M3 skrue
Kommunikationsport til I2C Fjernbetjeningen

Kommunikationsporten til I2C

Som vist på diagrammet indeholder kommunikationsporten PR1 til I2C flere ting, da portbenene anvendes til flere forskellige funktioner.

Stikket er 6-polet selvom det kun er de 4 ben der anvendes til I2C-kommunikationen.

Til I2C har porten følgende funktion:

Ben nr Signal-navn Specielle forhold
1 MISO Anvendes ikke til I2C - skal svæve ved reset
2 Vcc (+ 5V) Forsyning til I2C modulet
3 SCK Serial Clock til I2C
4 SDA Serial Data til I2C
5 Reset Mikrocontrollerens Reset - skal svæve når modulet er i funktion
6 GND Stel-forbindelse til I2C modulet

PR1 porten kan også anvendes til at programmere Mikrocontrolleren igennem, hvor porten får følgende funktion:

Ben nr Signal-navn Specielle forhold
1 MISO En del af ICSP programmeringen
2 Vcc (+ 5V) Forsyning til I2C modulet
3 SCK En del af ICSP programmeringen
4 MOSI En del af ICSP programmeringen
5 Reset Mikrocontrollerens Reset - Anvendes til at initiere ICSP
6 GND Stel-forbindelse til I2C modulet
Adresse-jumper I2C Fjernbetjening

Adresse på I2C Fjernbetjening med Jumper

Ud over dette anvendes ben 1 i PR1 til at angive om det er en lige eller en ulige adresse I2C-Fjernbetjening skal reagere på. Dette angives ved hjælp af jumperen SV1, hvor den er trukket høj af R4 når jumperen ikke er monteret og bliver trukket lav gennem R5 når man sætter jumperen på. Niveauet læses ved reset af mikrocontrolleren.

Som det kan ses på billederne til højre, så er det blot en jumper der kan sættes på for at skifte adressen. Når jumperen ikke er på, så bliver adressen (0x30) en højere (0x31), end hvis den er på.

Ideen med dette er at man kan have to ens tastaturer på samme I2C, og endda have samme program liggende i dem, men at man kan henvende sig til hvert af dem, uden det giver konflikt.

Adresse-jumperens placering kan ses på følgende billede (uden jumper på, bare med de to pins frit).

Adresse jumperen på printet
Adresse jumperen på printet

Yderligere hardware på I2C Fjernbetjening

Der er yderligere komponeter på I2C Fjernbetjening som har en mindre rolle at spille.

C1 og C2 er blot monteret for at fastholde forsyningen, hvis I2C modulet bliver forsynet gennem et længere kabel. Det er for at gøre modulet miondre støjfølsomt.

R1 og D1 er en del af reset-kredsløbet, som ellers håndteres internt i Mikrocontrolleren.

R2 og LED1 er blot en power-indikation der kan være praktisk at anvende i opstillinger hvor man kobler tingene sammen med kabler og løse ledninger. Hvis man ønsker at spare strøm i sin opstilling, så kan man spare 5 mA ved at undlade at montere disse to komponenter.

R3 og LED2 er til at lave en indikation af når der modtages et signal. Hvis man ønsker at spare strøm eller komponenter i sin opstilling, så kan man spare 5 mA ved at undlade at montere disse to komponenter.

I2C kommunikationen med modulet

For at kunne hente informationer ud af modulet via I2C kommunikationen, så er der etableret en protokol, der stiller de muligheder til rådighed man har brug for til at læse de taster der tastes på fjernbetjeningen.

Man skal angive en I2C-adresse, hvor man i dette modul kan vælge enten adresse 0x30 eller 0x31 ved at flytte på en jumper. Man kan også ændre adressen, hvis man oplever konflikt med andre moduler.

For at kunne læse og indstille modulet har man følgende kommandoer:

Kommando nr Kommando betydning Skrivning til modulet Læsning fra modulet
0 Antal i bufferen - Antallet af taster i bufferen
1 Hent fra bufferen - Den aktuelle tast fra bufferen (slettes når der læses) er 255 hvis bufferen er tom
2 Set repeat mode Repeat mode (0 / 1) -
3 Set repeat start Antal repeats før der startes repeat -
4 Clear buffer - -
5 Test variabel - To bytes til test

Når man henter en tast fra bufferen, så er det den rå byte der er sendt fra fjernbetjeningen. Man kunne selvfølgelig godt have oversat det i I2C modulet, men da tasterne har forskellige tekst-længder, så ville det gøre at protokollen ville blive mere kompliceret. Derfor er det bare det rå tal som en byte, der bliver sendt for hver tast.

Softwaren i I2C modulet

Arduino som programmer tilsluttet I2C-modulet via et fladkabel

Programmeringen af softwaren i I2C-modulet sker ved hjælp af en Arduino med et specielt programmer program inde og et Shield på som beskrevet i AVR-programmer Shield, så det er let at kable til I2C-modulet.

Softwaren i microcontrolleren til I2C modulet er opdelt i 3 moduler med følgende roller:

main.cpp der binder de to andre moduler sammen.

i2c.cpp der håndterer kommunikationen via I2C bussen. Modulet har i2c.h som interface-fil.

remote.cpp der modtager det digitale signal fra IR-modtageren, og lagrerer det til rådighed for I2C modulet. Modulet har remote.h som interface-fil.

Alle filerne ligger i denne ZIP-fil inde i mappen AVR I2C Fjernbetjening.

main.cpp

I starten skabes forbindelsen til de to andre moduler, samt til generelle filer ved hjælp af nogle includes:

// Include of general details
#include <avr/io.h>
// Include of special modules for the project
#include "i2c.h"
#include "Remote.h"

Dernæst defineres main() som er den funktion der kaldes efter reset af microcontrolleren.

int main() {
	i2c_init();		// Initialization of external modules
	Remote_init();
	while (1) {
		Remote_test();	// Tjeck for new packages from the remote control
		i2c_test();		// Tjeck for the I2C communication for every loop
	}
	return 0;	// Satisfy the compiler - the code newer comes here
}

I starten af main kaldes initialiserings-rutinerne i de to moduler.

Derefter udføres et while-loop, der aldrig slutter, så det er i princippet her alt der skal udføres sker, og det er to test-rutiner i de to moduler, der tester for aktivitet i hhv. remote-modulet og i2c-modulet.

Efter while-loopet skal compileren have en return 0, det er blot for at stille den tilfreds, fordi en standard main() i C skal returnere en værdi til styre-systemet. Dette har ingen mening her, men compileren vil gerne have det, så det skriver vi.

remote.cpp

Modtagelsen af det digitale signal sker udelukkende ved hjælp af interrupt. Der anvendes det interrupt der kan reagere på skiftet af niveau på et ben, og der anvendes timer overflow, for at kunne måle tiden præcist.

I det kanttriggede interruptet reageres på om det er højt eller lavt signal, og der skelnes mellem tiderne.

For at kunne styre modtagelsen er der etableret nogle states, der skifter som vist på følgende tids-diagram for pakken:

Angivelse af de forskellige states hen gennem målingen på en pakke
Angivelse af de forskellige states hen gennem målingen på en pakke

I hvile skulle den gerne være i state 0. Ved starten af pakken går den gennem state 1 og state 2 og kontrollerer at disse tider er OK.

Når den modtager databit skiftes mellem state 3 og 4 indtil der er modtaget 4 bytes (32 bit) hvor hver bit består af en kort lav efterfulgt af en kort eller lang høj, og repræsenterer hhv. 0 og 1.

Dette kan udtrykkes i følgende state-diagram:

Statediagram for modtagelsen af en pakke via det digitale ben
Statediagram for modtagelsen af en pakke via det digitale ben

Softwaren er implementeret som beskrevet i de følgende afsnit.

Først remote.h, der offentliggør de ting andre moduler skal anvende:

// Includefile for the I2C IR-remote module

// Functions in the Remote module
void Remote_init();
void Remote_test();

// Constants defining the remote physics
#define BUF_SIZE 16

// References for variables that are used for the remote module
extern bool repeatMode;
extern uint8_t repStart;
extern uint8_t Remote_buffer [BUF_SIZE];
extern uint8_t input_ptr;
extern uint8_t vis [4];

repeatMode er en logisk variabel der angiver om keyboradet skal fungere med repeat (som et PC-tastatur når man holder tasten nede).
repStart angiver hvor mange repeat-pakker der skal modtages før der kommer repetation på tasterne.
Remote_buffer er det array hvor de modtagne indtastninger på fjernbetjeningen lagres.
input_ptr angiver hvor i bufferen den næste skal lagres og samtidigt hvor mange der er i bufferen.
vis er blot til test.

I starten af remote.cpp includes det nødvendige, og herefter defineres både de offentlige og de interne variable der anvendes i modulet:

// Default values set for a reasonable keyboard behavior
uint8_t repStart = 10;		// Number of readings before the repeat function is initiated
bool repeatMode = true;		// If the keyboard shall repeat
uint8_t vis [4];

uint8_t Remote_buffer [BUF_SIZE];
uint8_t input_ptr = 0;
uint8_t inputVal;		// Værdien hvor de modtagne bit lagres
uint8_t inputTast;		// Variabel til den modtagne tast inden den er godkendt
uint8_t bitCnt = 0;		// Tæller til hvor langt vi er i byten
uint8_t byteCnt = 0;		// Tæller til antal af bytes i pakken
int tid;			// Tids-tæller til at registrere længden af tiderne
uint8_t overfCnt;		// Tæller til antal overflows, bruges til beregning af tiden
uint8_t state = 0;		// State-variablen - Holder styr på processen
uint8_t repCount = 0;		// Tæller til antal gentagelser af knappen

En af de filer der includes er port.h, der er tilrettet i forbindelse med dette projekt. Includefilen er lavet generelt til alle de microcontrollere vi anvender på skolen, men her vises de definitioner der kan bruges til ATTiny45:

// Macros to make the code easier to read.
#define pinB_low(pin) 		PORTB &= ~(1<<pin)
#define pinB_high(pin) 		PORTB |= (1<<pin)
#define pinB_input(pin)		DDRB &= ~(1<<pin)
#define pinB_output(pin) 	DDRB |= (1<<pin)
#define pinB_level(pin) 	(PINB & (1<<pin))

Makroerne er lavet for at lette kodeskrivningen og gøre det lettere at læse koden, som f.x.

pinB_input(PB1);
pinB_output(PB3);
if (pinB_level(PB1) > 0) {
    pinB_low(PB3);
}

I initialiserings-rutinen sættes der op så der kan interruptes og PortB pin3 sættes som input og til kanttrigget interrupt. Desuden sættes timer 0 til at tælle for hvert 8. mikrosekund (deling af 8 MHz med 64) og der sættes op at overflow af timer 0 bytes skal give interrupt

// Initializing the Remote module
void Remote_init() {
  sei();			// Set the interrupt
  pinB_output(PB4);		// LED as output
  pinB_low(PB4);		// LED off
  pinB_input(PB3);		// Digital input
  GIMSK |= 1 << PCIE;		// Enable pin change interrupt
  PCMSK |= 1 << PCINT3;		// Enable pin change interrupt from PB3
  TIMSK |= 1 << TOIE0;		// Enable Timer 0 overflow interrupt
  TCCR0A = 0;			// Timer 0 in normal mode
  TCCR0B = 0x03;		// Timer 0 divide by 64 - each count is 8 us
}

Interface-rutinen holdes til modulet som standard, men den laver ingenting, da alt i modulet foregår i interruptet.

// Routine to test the input
void Remote_test() {
	// Nothing is done here, all is implemented in the interrupt routines
}

Interruptvektoren til timer 0 overflow interrupt skal tælle en frem for hvert overflow, hvilket gøres som følger:

ISR(TIM0_OVF_vect)
{
  overfCnt++;			// Time 
}

Det kanttriggede interrupt tilknyttes interruptvektoren som følger:

ISR(PCINT0_vect)
{

For hvert interrupt beregnes tiden siden det sidste interrupt, ved at lægge timerværdien sammen med antal overflows ganget med 256. Dette giver en "tid" med enhendes 8 us, og der nulstilles så begge registre er klar til næste interrupt.

  tid = TCNT0 + 256 * overfCnt;
  TCNT0 = 0;
  overfCnt = 0;

Resten af interrupt-rutinen er baseret på state-diagrammet, og er lavet med en switch-case struktur, som startes her med case 0.

I state 0 ventes der på at der modtages et lavt niveau, og hvis der kommer det, så skiftes til state 1

  switch (state) {
    case 0:  // Venter mellem pakker
      if ((pinB_level(PB3)) == 0) {
        state = 1;
      }
      break;

I state 1 registreres om tiden for den lave start af pakken ligger omkring 9,1 ms - gør den det skiftes til state 2, ellers er det en fejl, og der skiftes tilbage til state 0, hvor der kigges efter en ny start.

    case 1:  // Venter i den lave marker i starten af pakken
      if ((pinB_level(PB3)) > 0) {
        if ((tid > 1150) && (tid < 1250)) {			// 8800 - 9300 us
          state = 2;
        } else {
          state = 0;
        }
      }
      break;

I state 2 forventes en tid på 4,5 ms. Kommer der det, så skiftes til state 3, hvor data bliver modtaget, så inden det indstilles start-parametrene for at modtage pakken.

Hvis tiden derimod er ca. 2,3 ms, så er det en repeat-pakke, som beskrevet i Test af modtagelse. I denne del håndteres at man kan få fjernbetjeningen til at repeate ved at holde den nede, så der er kommet et antal repeat-pakker

    case 2:  // Venter i den høje marker i starten af pakken
      if ((pinB_level(PB3)) == 0) {
        if ((tid > 500) && (tid < 700)) {			// 4000 - 4800 us
          pinB_high(PB4);
          state = 3;
          bitCnt = 0;
          byteCnt = 0;
	  repCount = 0;
        } else if ((tid > 250) && (tid < 350)) {	// 2000 - 2600 us
		  if (repeatMode) {
			  repCount++;
			  if (repCount >= repStart) {
				  repCount--;
				  if (input_ptr < BUF_SIZE) {
					Remote_buffer[input_ptr] = inputTast;
					input_ptr++;
				  }
			  }
		  }
          state = 5;
        } else {
          state = 0;
        }
      }
      break;

I state 3 forventes tiden at være ca. 0,5 ms for den lave del af bitten. Er den det sættes state til 4, hvor der registreres hvor lang den høje del af bitten er, og der veksles nu mellem de to states.

Når der er modtaget 4 hele bytes så lagres den modtagne tast i bufferen, hvis der ellers er plads til den - ellers vil den bare mistes.

    case 3:		// Den lave del af et databit
      if ((pinB_level(PB3)) > 0) {
        if ((tid > 60) && (tid < 98)) {		// 400 - 700 us
          state = 4;
        } else {
          state = 0;
        }
        if (byteCnt == 4) {		// Der er kommet det ekstra bit i slutningen af pakken
		  if (input_ptr < BUF_SIZE) {
			pinB_low(PB4);
            Remote_buffer[input_ptr] = inputTast;
			input_ptr++;
		  }
          state = 0;
        }
      }
      break;

I state 4 registreres først om tiden er ca. 0,5 ms eller om det er ca. 1,7 ms, for at registrere om det er en 0 eller 1 bit der er modtaget. Hvis det går godt skiftes tilbage til state 3.

    case 4:		// Den høje del af et databit
      if ((pinB_level(PB3)) == 0) {
        if ((tid > 180) && (tid < 260)) {		// 1300 - 2000 us
          inputVal = (inputVal << 1) + 1;
        } else if ((tid > 60) && (tid < 98)) {	// 400 - 700 us
          inputVal = inputVal << 1;
        } else {
          state = 0;
        }
        if (state == 4) {
          state = 3;
        }
        bitCnt++;

Inden der skiftes tilbage til state 3, så registreres lige om der er modtaget en hel byte. Hvis der er det, så reagres der på hvad der skal gøres, alt efter hvilken byte det er.

Den første byte skal have værdien 0x00 og den anden skal have værdien 0xFF. Hvis de ikke er det, så forkastes pakken som en fejl, ved at der skiftes til state 0.

Den tredje byte er tasten der modtages, så den lagres i en midlertidig variabel, da pakken ikke kan godkendes helt før den fjerde byte er verificeret.

Den fjerde byte skal indeholde det samme som den tredje, bare bit-mæssigt inverteret, så når den er modtaget kontrolleres om det er tilfældet, og pakken afvises hvis den ikke er det. Der gøres ikke yderligere, da pakken skal afsluttes med en kort lav. Dette sker i state 3, hvor tasten lagres i bufferen og input_ptr tælles en frem, så den er klar til at modtage den næste tast eller evt. en række repeat-pakker.

        if (bitCnt == 8) {
          bitCnt = 0;
          switch (byteCnt) {
            case 0:
              if (inputVal != 0x00) {
                state = 0;
              }
              break;
            case 1:
              if (inputVal != 0xFF) {
                state = 0;
              }
              break;
            case 2:
	      inputTast = inputVal;		// Save temporarely before it is stored in the buffer
              break;
            case 3:
              inputVal = ~inputVal;
              if (inputVal != inputTast) {
                state = 0;
              }
              break;
          }
          bitCnt = 0;
          byteCnt++;
        }
      }
      break;

Det sidte state er 5. Dette anvendes blot til at afslutte en repeat-pakke. Selve håndteringen af repeat-funktionen sker i state 2, her ventes blot på at inputtet bliver højt, så vi er klar til at modtage den næste pakke.

    case 5:		// Venter på at repeat-pakken slutter
      if ((pinB_level(PB3)) > 0) {
        state = 0;
      }
      break;

i2c.cpp

I2C modulet er opbygget omkring et andet modul usiTwiSlave[2], som udnytter hardwaren i Microcontrolleren til at lave two wire kommunikation (I2C) med. Det gøres ved at bruge de registre der kan modtage bytes hardwaremæssigt, og interruptet til at læse registrene med, så tingene sker hurtigt nok. Der er dog stadig krav til at softwaren skal kontrollere status rimeligt ofte.

Modulet I2C har en header-fil i2c.h som publicerer alle de ting der skal være offentlige for modulet.

// Includefile for the I2C remote module
#include "Remote.h"

// Functions in the I2C module
void i2c_test();
void i2c_init();

I starten af modulet includes alle de moduler der anvendes og der defineres de variabler der indgår i modulet.

#include <avr/io.h>
#include <avr/interrupt.h>
#include "port.h"
#include "i2c.h"
#include "Remote.h"
#include "usiTwiSlave.h"

// The first I2C address for the IR Remote control
uint8_t I2C_SLAVE_ADDR = 0x30;	// Can be changed to 0x31

Initialiserings-rutinen sætter muligheden for interrupt op, så I2C kommunikationen kan fungere.

Dernæst læses på pinB1 om adressen skal være 0x30 (jumperen på) eller 0x31 (uden jumper).

Til sidst i initialiseringen kaldes initialiseringen af usiTwiSlave modulet til den valgte adresse - så adressen kan kun bestemmes ved reset af modulet.

// Initializing the I2C module
void i2c_init() {
  sei();	// Set the interrupt
  pinB_input(PB1);		// Input for read address for odd or even
  if ((pinB_level(PB1)) > 0) {	// Tjeck for address setting
	  I2C_SLAVE_ADDR++;
  }  
  usiTwiSlaveInit(I2C_SLAVE_ADDR);	// init I2C Slave mode in the usiTwiSlave module
}

Strukturen i i2c_test() er at de læses om der er kommet en henvendelse fra masteren, og hvis der er det, så reageres der på det som angivet i tabellen ved I2C kommunikationen med modulet. Dette startes med følgende kode, hvor der på kommando 0 svares med antallet der er i bufferen (input_ptr).

// Routine that can access the remote variables to get and set remote information
void i2c_test() {
  uint8_t byteRcvd;
  uint8_t n;
  if (usiTwiDataInReceiveBuffer()){		// got I2C input!
    byteRcvd = usiTwiReceiveByte();		// get the command byte from master
	if (byteRcvd == 0) {			// Get the number of bytes in the buffer
		usiTwiTransmitByte(input_ptr);

Ved kommando 1 spørges efter en tast i bufferen. Hvis input_ptr angiver at der er en tast, så sendes den, ellers sendes 255. Når tasten er sendt, så flyttes indholdet af bufferen, så den forreste plads i bufferen altid indeholder den først tastede tast:

	} else if (byteRcvd == 1) {		// Get the first key from the buffer
		if (input_ptr > 0) {
			usiTwiTransmitByte(Remote_buffer[0]);
			n = 0;
			input_ptr--;
			while (n < input_ptr) {
				Remote_buffer[n] = Remote_buffer[n+1];
				n++;
			}
		} else {
			usiTwiTransmitByte(255);
		}

Kommandoerne 2 og 3 sætter repeatMode og repStart variablerne, så man via I2C kan tilpasse hvordan repeat-funktionen skal opføre sig.

	} else if (byteRcvd == 2) {		// Set repeat-mode to the transmitted state
		repeatMode = bool(usiTwiReceiveByte());
	} else if (byteRcvd == 3) {		// Set the number of repeat packages to receive before repeating
		repStart = usiTwiReceiveByte();

Kommando 4 nulstiller blot input_ptr, hvilket svarer til at indholdet i input-bufferen slettes

	} else if (byteRcvd == 4) {		// Clear the input buffer
		input_ptr = 0;

Kommando 5 har været anvendt til test, hvor den har været brugt til at aflæse variablen tid, for at kunne registrere hvilke tider de forskellige perioder havde.

	} else if (byteRcvd == 5) {		// 
		usiTwiTransmitByte(vis[0]);
		usiTwiTransmitByte(vis[1]);
	}

Test af softwaren i I2C modulet

Arduino programmer og test-Arduino tilsluttet I2C-modulet

Til test af modulet anvendes en anden Arduino, så man kan kommunikere med I2C til den. Det kan så lade sig gøre at have programmeren på en anden arduino tilsluttet samtidigt via det samme fladkabel, som vist her til højre.

I den første test af I2C Fjernbetjeningen viste det sig at den ikke ville genkende tiderne, selvom der var sat en fornuftig margen til begge sider (fra 8,8 ms til 9,4 ms). Beregningen for tallet der skulle sammenlignes med var:

Count = 9100 / 8 = 1137,5

Grænserne der blev sammenlignet med var 1100 og 1175, så det skulle være fornuftige afvigelser der kunne rummes (+/- 3%), men ved i første omgang at anvende PB4 med lysdioden, så kunne det konstateres at koden aldrig kom til state 2.

For at få en bedre ide om hvad der var galt, så indførtes debug-kommandoen nr. 5, så man kunne få overført en værdi til Arduinoen, og den kunne printes ud. Det blev gjort ved at indføre følgende kode i state 1, så tiden kunne aflæses for de lange pulser.

	if (tid > 1100) {
		vis[0] = tid;
		vis[1] = tid >> 8;
	}

Her viste det sig at tid-variablen endte omkring 1200, altså en afvigelse på ca. 5%. Ved at rette grænserne til de nuværende tal kunne det ses at koden nu kom videre i forløbet, men at der stadig var problemer. Ved at indføre tilsvarende tids-målinger forskellige steder i koden kunne det konstateres at det var generelt at tiderne var ca. de 5% længere end beregningerne indikerer. Da dette blev korrigeret, så kunne pakkerne fint modtages.

I2C læsning af en tast

For at udnytte kommandoerne til at læse en tast, så er man nødt til at tage højde for en række fejl, og fordi I2C modulet ikke altid svarer lige hurtigt, så skal man igennem en masse kontrol for at man kan være sikker på at kunne aflæse en tast korrekt.

Dette er udtrykt i følgende flowchart:

Flowchart der viser forløbet i læsningen af en tast
Flowchart der viser forløbet i læsningen af en tast

Dette kan implementeres i følgende Arduino software:

void getTastBuf() {
  Wire.beginTransmission(I2C_Address);
  Wire.write(0);
  error = Wire.endTransmission();

  if (error == 0)
  {
    delayMicroseconds(100);
    Wire.requestFrom(I2C_Address, 1);
    if (Wire.available() == 1) {
      error = Wire.read();
      if (error >= 1) {
        if (! doTranslate) {
          Serial.print("Num in buffer: ");
          Serial.print(error);
          Serial.print("\tKey reading: ");
        }
        Wire.beginTransmission(I2C_Address);
        Wire.write(1);
        error = Wire.endTransmission();
      
        if (error == 0) {
          delayMicroseconds(100);
          Wire.requestFrom(I2C_Address, 1);
          if (Wire.available() == 1) {
            error = Wire.read();
            if (doTranslate) {
              Serial.println(translate(error));
            } else {
              Serial.println(error);
            }
          }
        }
      }
    }
  } else {
    Serial.print(error);
    Serial.println(" Fail");
  }
}

Testkode i Arduinoen til at hente fra I2C modulet

Testen af I2C-modulet er lavet med den viste opstilling, hvor den ene Arduino anvendes til at teste med, mens den anden Arduino bruges til at programmere microcontrolleren på I2C fjernbetjeningen med.

Det færdige testprogram gennemgås ikke fuldstændigt, men ligger som I2C Fjernbetjening.ino i denne ZIP-fil.

Testprogrammet starter med følgende udskrift:

Demo program for I2C Fjernbetjening
--------------------------------
H	Displays this help-screen
M	Flips the mode where the keys are presented
G	Tries to read from the key-buffer
C	Clears the buffer
R n	Repeat set to n
U n	Number of repeats before starting repeat
N	Number in buffer
T	Toggle translate mode

Læsningen af indtastningerne fra PC'en gøres i loopet som vist starten af her:

void loop()
{
  // Reager på de forskellige indtastninger brugeren 
  // laver på PC'en
  if (Serial.available() > 0) {
    ch = Serial.read();
    if ((ch == 'h') | (ch == 'H')) {
      help();
    }
    if ((ch == 'r') | (ch == 'R')) {
      error = Serial.parseInt();
      setRepeat(error);
    }

Som et eksempel kan setRepeat(mode) se ud som følger, hvor der anvendes kommando 2 til at sætte det ønskede repeat-mode:

void setRepeat(byte mode) {
  Wire.beginTransmission(I2C_Address);
  Wire.write(2);
  Wire.write(mode);
  error = Wire.endTransmission();
  if (error == 0) {
    Serial.print("Repeat mode set to: ");
    Serial.println(mode);
  }
}

Som i anden version af Arduino testen, så kan denne test-version også oversætte taste-numrene til tekster. Dette sker i følgende funktion:

String translate(byte _key) {
  for (int n = 0; n < antalTaster; n++) {
    if (tabel[n].dat == _key) {
      return tabel[n].text;
    }
  }
  return "Not found " + String(_key);
}

Opsamling og konklusion

Det er lykkedes at konstruere forskellige versioner af software der kan aflæse IR-modtageren, når fjernbetjeningen sender til den.

De første versioner til Arduinoen var kun anvendelige som illustration af at princippet fungerer, og at det kunne lade sig gøre at tolke de signaler som IR-modtageren sender ud.

Tredje version til Arduinoen kan anvendes sammen med anden software, og er fuldt anvendelig, hvis man har ben 2 eller ben 3 til rådighed.

Den sidste version kræver lidt mere, hvor der skal laves et I2C hardware modul, og for at lette det også et Shield til at sætte på Arduinoen når man vil forbinde til det på en sikker måde. Denne version kræver så kun at man kan etablere en I2C kommunikation, og at man kan finde en ledig adresse at kommunikere på, så giver den store friheder, hvor den endda kan opsamle flere taster inden de behøves at blive aflæst.

Referencer