In den bisherigen Teilen dieser Serie wurde (1) die Idee vorgestellt (ein Radio auf Basis des Arduino, dass die zum Ort passende UKW-Senderliste von einem RasPi per IP-Geolokation bekommt), (2) das Skript zum Beschaffen der UKW-Senderliste mit Hilfe von Screen Scraping vorgestellt und (3) die Programmierung der Kommunikation zwischen RasPi und Arduino per Seriellem Port und das Speichern in einem EEPROM des Arduino gezeigt.

In diesem Teil wird der Aufbau des eigentlichen Radios auf Basis des TEA5767 erläutert. Dieser Chip ist ein hoch integrierter Mikrocontroller, der ein komplettes UKW-Radio enthält und per I2C angesteuert wird. Es gibt ihn für 1€ – 3€ in vielen Webshops bereits auf einem Breakout-aufgelötet. Ich habe dem Board noch Steckbrett-geeignete Pins verpasst, um damit besser experimentieren zu können.

TEA5767-schema

Die Anschlüsse sind in der Abbildung oben dargestellt und neben dem I2C-Anschlüssen und einer Stromversorgung braucht es nur noch eine Antenne und einen kleinen Verstärker. Ich habe diese Anschlüsse direkt an einen portablen Lautsprecher – so einen für’s Smartphone  – angeschlossen.

Für die gesamte Schaltung braucht Ihr:

Die komplette Schaltung sieht dann so aus:

Die I2C Anschluss des TEA5767 und des LIC/I2C-Adapter werden direkt an die SDA/SCL-Anschlüsse des Arduino angeschlossen (weiß und violett im Bild oben). Die Stromversorgung (GND und VCC) erklären sich selbst. Antenne und Lautsprecher werden mit PIN 10 und PIN 8/7 verbunden.

Der Anschluss des Rotary Encoder (Drehwahlschalter) ist etwas erklärungsbedürftig: Wenn ein Rotary Encoder gedreht wird, schaltet er die beiden PINs CLK und DT etwas verzögert zueinander an oder aus. Man misst also je nach Drehrichtung, welcher PIN vor dem jeweils anderen umschaltet und kann so feststellen, ob gedreht wurde und in welche Richtung. Der Anschluss SW ist ein einfacher Taster der schließt, wenn man auf den Drehschalter drückt.

Der KY-040 verfügt über 5 Pins (CLK, DT, SW, + und GND). Die Schaltimpulse bei einer Drehbewegung kommen von CLK und DT. Diese Signale werden mit einem digitalen Port des Ardunio verbunden, um die Drehrichtung und -winkel per Interrupt zu verarbeiten. In der obigen Schaltung wurde daher wie folgt an den Arduino angeschlossen:

Rotator
  • CLK and Pin 2
  • DT an Pin 3
  • SW an Pin 5
  • + an VCC
  • GND an GND

Die Kommunikation des Arduino mit dem RasPi zum Datenaustausch der Senderliste über eine serielle Verbindung wurde in Teil 3 bereits beschrieben. Der folgende Code enthält das vollständige Programm zum Empfang der Senderliste über die serielle Verbindung (UART), das Abspeichern der Liste in den EEPROM, die Ansteuerung des LCD Displays und die Senderauswahl per Drehknopf.

 

Ich habe das Arduino-Programm auf Github hinterlegt. Ihr braucht die LiquidCrystal-Bibliothek, von der es leider mehrere Varianten gibt, die zudem gleich heißen. Wenn die Bibliothek aus dem Arduino-IDE nicht gleich funktioniert, hilft Google weiter.

Nach dem Laden der Bibliotheken in Zeile 1 – 3 werden zwei Konstanten definiert. Die erste ist die i2c Adresse des LCD Displays. Danach wird MAX_ROT_VALUE festgelegt, nach wie vielen Schritten der Drehwahlschalter wieder bei 1 beginnen soll: Der Senderspeicher soll nur die besten 16 Sender speichern.  Die Konstanten in den Zeilen 11 -13 legen fest, mit welchen Pins des Arduino der Drehwahlschalter verbunden ist.

In Zeile 18 wird das LCD initialisiert. Diese Zeile hängt von der Bibliothek ab, ich habe nach einigen Herumprobieren die LiquidCrystal_I2C Bibliothek von Libor Gabaj genommen. Der Initialisierungsroutine wird die I2C Adresse, die Anzahl der Zeichen pro Zeile und die Anzahl der Zeilen übergeben. Da das Display eine Art Schieberegler anzeigen soll, um die Stelle im Speicher zu symbolisieren, habe ich zwei neue Zeichen für das Display erstellt. Ein Zeichen für das Display ist im Wesentlichen ein Array mit 8 Byte, die auf eine 5 x 8 Matrix gemappt wird. Die folgende Abbildung zeigt, wie man sich das vostellen muss:

Die Zeichen werden in Zeile 74 – 76 ins Display geladen. In Zeile 26 – 34 wird die erste Funktion erstellt. Die Funktion isr() ist die Interrupt Routine, die Dank dem Aufruf von attachInterrupt() in Zeile 69 immer aufgerufen wird, wenn ein Spannungsveränderung an Pin 2 festgestellt. Das Programm unterbricht umgehend den aktuellen Schritt und springt in die Interrupt-Routine, daher der Name. Die Routine isr() setzt up auf 1 wenn PinCLK und PinDT gleich sind (es wurde in Uhrzeigerrichtung gedreht) oder auf 0 wenn sie unterschiedlich sind (es wurde entgegen der Uhrzeigerrichtung gedreht. Anschließend wird das Flag TurnDetect gesetzt.

Die Funktionen init_eeprom(), show_eeprom() und get_station() kümmern sich um den EEPROM. Die Details wurden in Teil 3 bereits erläutert. Das Einstellen eines Sender in der Funktion set_station(frequency) folgt dem Datenblatt (TEA5767 Datenblatt) auf Seite 22:

TEA5767 Datasheet

Für unseren TEA5767 heißt das:

PLL = 4*(frequency*1000+225)/32768/1000

wobei frequency in MHz angegeben wird, z.B. 91.5. Im Code habe ich die 1000 in die Klammer gezogen, daher multipliziere ich in der Klammer mit 1.000.000 und 225.000.

Der Rest des Programms erklärt sich auf dieser Basis dann von selbst. Über die serielle Schnittstelle kann der EEPROM rudimentär verwaltet werden: Ein knock leitet eine Senderübertragung ein und wird mit OK bestätigt. Ein r löscht die Senderliste im EEPROM und wird mit RESET bestätigt. Ein s wird mit der im EEPROM gespeicherten Senderliste beantwortet.

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>

#define LCD_ADDRESS 0x27
#define MAX_ROT_VALUE 16

volatile boolean TurnDetected;
volatile boolean up;

const int PinCLK=2; // Das CLK signal erzeugt den Interrupt auf Pin 2
const int PinDT=3; // Pin 3 liest das DT Signal
const int PinSW=5; // Pin 5 kann den Tastendruck erkennen

boolean knock = false;

// Einstellen der LCD I2C Adresse
LiquidCrystal_I2C lcd(0x27, 16, 2);

// 2 neue Zeichen erstellen
const uint8_t charBitmap[][8] = {
 { 0x04,0x04,0x0E,0x0E,0x1F,0x1F,0x1F,0x00 },
 { 0x00,0x00,0x00,0x00,0x00,0x1F,0x00,0x00 }
};

void isr () {
// Die Interrupt Routine wird ausgeführt, wenn
// eine Veränderung von HIGH nach LOW oder umgekehrt
// an CLK erkannt wird
  up = (digitalRead(PinCLK) == digitalRead(PinDT));
  TurnDetected = true;
}

struct radio_station {
  char freq[6];
  char rds[17];
};

void init_eeprom(boolean reset=false) {
  EEPROM.update(0,'I');
  if(reset) EEPROM.update(1,0);
}

void show_eeprom() {
  radio_station station;
  int size_of_station = sizeof(station);
  int count = EEPROM.read(1);
  Serial.print(count);Serial.print(":");
  for(int i=0;i<EEPROM.read(1);i++) {
    EEPROM.get(2+i*size_of_station, station);
    Serial.print(station.freq);
    Serial.print(';');
    Serial.println(station.rds);
  }
} 

void get_station(byte nr, struct radio_station &station) {
  byte size_of_station = sizeof(station);
  byte count = EEPROM.read(1);
  if (nr>count) nr = count;
  if (nr<1) nr = 1; EEPROM.get(2+(nr-1)*size_of_station, station);
} 

void set_station(float frequency) {
  uint16_t frequencyB=4*(frequency*1000000+225000)/32768;
  uint8_t frequencyH=frequencyB>>8;
  uint8_t frequencyL=frequencyB&0xFF;
  delay(100);
  Wire.beginTransmission(0x60);
  Wire.write(frequencyH);
  Wire.write(frequencyL);
  Wire.write(0xB0);
  Wire.write(0x10);
  Wire.write(0x00);
  Wire.endTransmission();
}

void setup() {
  int charBitmapSize = (sizeof(charBitmap ) / sizeof (charBitmap[0]));
  pinMode(PinCLK,INPUT);
  pinMode(PinDT,INPUT);
  pinMode(PinSW,INPUT);
  attachInterrupt (digitalPinToInterrupt(2),isr,CHANGE);

  init_eeprom();
  lcd.init();
  lcd.backlight();
  for ( int i = 0; i<charBitmapSize; i++ ) {
    lcd.createChar ( i, (uint8_t *)charBitmap[i] );
  }
  lcd.home();
  lcd.print(" Radio FMX");
  lcd.setCursor(0,1);
  lcd.print(char(0));
  for (int i=0;i<15;i++) {
    lcd.print(char(1));
  }
  Serial.begin(38400);
  Serial.println("RadioXZ");
}

void loop() {
  static int virtualPosition=0;
  radio_station station;

  if (TurnDetected) {
    lcd.setCursor(virtualPosition, 1);
    lcd.print(char(1));
    if (up) {
      virtualPosition = (virtualPosition + 1) % MAX_ROT_VALUE;
    } else {
      virtualPosition--;
      if (virtualPosition<0) virtualPosition = MAX_ROT_VALUE - 1;
    }
    TurnDetected = false;
    lcd.setCursor(virtualPosition, 1);
    lcd.print(char(0));
    get_station(virtualPosition+1, station);
    lcd.home();
    lcd.print(virtualPosition+1);
    lcd.print(" ");
    lcd.print(station.rds);
    lcd.print(" ");
    lcd.print(station.freq);
    lcd.print(" ");
    Serial.println(station.rds);
    String frequency = String(station.freq);
    set_station(frequency.toFloat());
  }
  if(Serial.available()>0) {
    String line = String(Serial.readString());
    switch(line.charAt(0)) {
      case 'k': Serial.println("OK");
        knock = true;
        break;
      case 'r': Serial.println("RESET");
        init_eeprom(true);
        knock = false;
        break;
      case 's': show_eeprom();
        break;
      default:
        if(knock==true) {
          int idx = line.indexOf(';');
          if(idx != -1) {
            String f = String(line.substring(0,idx));
            f.toCharArray(station.freq,6);
            station.freq[f.length()] = 0;
            String r = String(line.substring(idx+1));
            r.toCharArray(station.rds, 16);
            station.rds[r.length()] = 0;
            byte counter = EEPROM.read(1);
            EEPROM.put(2+(counter*sizeof(station)), station);
            delay(500);
            counter = (counter % 16) + 1;
            EEPROM.write(1,counter);
            Serial.println("OK");
          }
        }
        break;
    }
  }
}