Donnerstag, 16. Mai 2013

Musik - Schritt für Schritt (Teil 3): Encoder Board

Hier geht es zu Teil 1
Hier geht es zu Teil 2

Die ersten Tests meines CSQ-1 Step Sequencer Projektes zeigten, das es manchmal nicht so einfach ist, wie man es sich vorgestellt hat. Die Folge war die Entwicklung des Encoder Boards.


Wozu ein Encoder Board?

Je nach Drehgeschwindigkeit erzeugen Encoder (Inkrementalgeber) Impulse unterschiedlicher Frequenz, die man durch Verändern eines Zählerstandes in numerische Werte wandeln kann. Im Fall meines CSQ-1 Step Sequencers z.B. um die Tonhöhe eines Steps zu verändern.
Dummerweise kann es passieren, dass gerade in dem Moment, in dem man am Encoder dreht, der Mikrocontroller mit etwas anderem beschäftigt ist, als die Encoder-getriggerten Zählerstände zu verändern. Die Folge ist, dass Impulse verloren gehen und die beabsichtigte Änderung eines Parameters nicht in dem gewünschten Umfang eintritt. Anders ausgedrückt: die Zählerstandsänderung ist nicht mehr linear zur Änderung des Encoderdrehwinkels. Die Auswirkungen dieses Makels sind nicht vorhersagbar und es entsteht der Eindruck, dass das Gerät nicht richtig funktioniert.

Man könnte den Controller bei seiner bisherigen Tätigkeit über die Ansteuerung per externem Interrupt unterbrechen, so dass er sich vorrangig um das Erfassen der Encoder Impulse kümmert. Im Fall eines MIDI Step Sequencers könnte dies jedoch zu unerwünschten Verzögerungen in der Erzeugung der MIDI Daten führen.

Als brauchbare Lösung betrachte ich daher das parallele Bearbeiten dieser Aufgaben. D.h. um die Encoder muss sich ein separater Controller kümmern. Er soll stetig die Impulse der Encoder erfassen und die Zähler entsprechend verändern. Auf Anfrage des Hauptcontrollers (Master), überträgt der Subcontroller (Slave) die aktuellen Zählerstände.
Bei meinem CSQ1 soll der ATmega328P-PU diese Aufgabe erledigen. Er ist auch im Arduino UNO verbaut, wodurch sich die Programmentwicklung einfach gestalltete. Dazu muss der ATmega328 mit dem Arduino Bootloader ausgestattet werden. Das ist mit einem Programmer, z.B. mySmartUSB MK2 oder ähnlichem. Ich hatte jedoch noch Controller mit vorinstalliertem Bootloader und konnte mir diesen Schritt sparen.
Die Datenübertragung sollte per I²C-Bus (Inter-Integrated Circuit) erfolgen, der auch im Standard-Mode mit 100 kHz Taktrate schnell genug für diese Anwendung sein müsste.

 
Die Hardware:

Der ATmega328 stellt 23 Ein-/Ausgänge zur Verfügung:
Bei Nutzung des Arduino Boards sind die blauen Bezeichnungen in der Programmierung zu verwenden. Für die Hardware Entwicklung ist natürlich die Pin-Nummerierung des ICs relevant.

Die Pins werden wie folgt verwendet:

PIN UNO Verwendung
1 RESET RESET (zum Einspielen der Firmware erforderlich)
2, 3 0, 1 RXD, TXD (zum Einspielen der Firmware erforderlich)
4 - 6
11 - 19
23 - 26
2 - 13
A0 - A3
16 Input Pins für 8 Encoder
7, 8
Spannungsversorgung
9, 10
Externe Beschaltung mit 16 MHz Quarz
20 - 22
Unrelevant (extern mit +5V und GND beschaltet)
27, 28 A4, A5 SDA, SCL (I2C für Datenübertragung zum Hauptprozessor)


Die externe Beschaltung des ATmega328 beschränkt sich auf die Taktgenerierung per 16 MHz Quarz, eine paar passive Bauteile und einen Reset-Taster. Als guter Anhaltspunkt für die Schaltungsentwicklung diente mir das Referenz Design des Arduino Uno (man muss das Rad nicht 2x erfinden).
Zusätzlich wurden 2 Pull-Up Widerstände (R3, R4) für den I²C-Bus eingebaut.

Mit einem Slave-Controller können 8 Encoder angeschlossen werden, das entspricht beim CSQ-1 einem Register. Es werden also 2 Slave-Controller benötigt. 

Der Schaltplan:


Vor dem Platinendesign noch ein kleiner Test der Schaltung auf einem Steckbrett.


Der mySmartUSB MK2 Programmer diente als UART-Bridge zum Einspielen der Firmware in den ATmega und zur Stromversorgung. Der Arduino Mega wurde mit einem Polling Programm versehen, das die Zählerstände via I²C-Bus abfragt und diese dann über den seriellen Monitor sichtbar macht. Fazit: die Schaltung funktionierte und konnte "in Platinenmaterial gegossen werden".


 

Bestückungsplan:



 
Ätzvorlage:



Das fertige Encoder Board:



Einspielen der Firmware in die beiden ATmegas:

Die Board- und Programmer Einstellungen in der Arduino IDE werden für den Arduino Uno gewählt:

    -> Tools -> Board -> Arduino Uno
    -> Tools -> Programmer -> Arduino as ISP
   
Beim Upload der Firmware mit dem mySmartUSB ist nach Erscheinen der Nachricht "Binary sketch size..." im IDE Statusfenster kurz die Reset-Taste zu betätigen, da bei dieser Beschaltung kein automatisches Reset erfolgt (ähnlich der Programmierung eines Arduino Pro Mini). Nur so wird der Upload fehlerfrei beendet.

Wichtig: Für Register 2 muss in der Firmware (Zeile 53)  eine von Register 1 unterschiedliche Bus-Adresse eingetragen werden. Ich verwende für
Register 1:      int bus_adr = 2;
Register 2:      int bus_adr = 3;


Die Firmware (Register 1):


/* Firmware for CSQ1 encoder board register 1

  by christian marmann, 130514
 
  I2C address = 2

  IDE selektions using mySmartUSB mkII:
    -> Tools -> Board -> Arduino Uno
    -> Tools -> Programmer -> Arduino as ISP
  
  Upload with mySmartUSB mkII:
 
    - connection diagram:
    mySmartUSB mkII  ---- encoder board
    right interface        register 1   or   register 2
      PIN 10 = GND   ---- PIN 9  = GND ---- PIN 9  = GND
      PIN 9  = +5V   ---- PIN 10 = +5V ---- PIN 10 = +5V
      PIN 8  = RxD   ---- PIN 3  = TxD ---- PIN 7  = TxD
      PIN 7  = TxD   ---- PIN 4  = RxD ---- PIN 6  = RxD

    - starting upload:
      when message "Binäre Sketchgröße [...]" appears, then

      push  reset button
      for about half a second and wait till end of the upload
    
    - verify upload:
      start serial monitor with 9600 baud to verify encoder

      functions:
      - display:
        enc#  #[enc0]  #[enc1]  #[enc2]  #[enc3]  #[enc4]  #[enc5]  #[enc6]  #[enc7]
      
      - turning CW --> counter should increase
      - turning CCW --> counter should decrease till "0"
      - if counting is reverse, then shift encoder pin definition
        in "Encoder myEncx()" statement below or shift cabeling of the
        related encoder   
*/

#define ENCODER_DO_NOT_USE_INTERRUPTS

#include <Encoder.h>
#include <Wire.h>

Encoder myEnc0( 9, 8 );
Encoder myEnc1( 10, 7 );
Encoder myEnc2( 6, 11 );
Encoder myEnc3( 5, 12 );
Encoder myEnc4( 13, 4 );
Encoder myEnc5( A0, 3 );
Encoder myEnc6( A1, 2 );
Encoder myEnc7( A2, A3 );
long pos[] = {0, 0, 0, 0, 0, 0, 0, 0};
long newPos[] = {0, 0, 0, 0, 0, 0, 0, 0};
int encAnz = 8;
int bus_adr = 2; // i2C bus address

void setup() {
  Serial.begin(9600);
  Serial.println("Basic NoInterrupts Test:");
  Wire.begin(bus_adr);                // join i2c bus
  Wire.onRequest(requestEvent); // register event
}

void loop() {
  newPos[0] = myEnc0.read();
  newPos[1] = myEnc1.read();
  newPos[2] = myEnc2.read();
  newPos[3] = myEnc3.read();
  newPos[4] = myEnc4.read();
  newPos[5] = myEnc5.read();
  newPos[6] = myEnc6.read();
  newPos[7] = myEnc7.read();
  for (int i=0; i < encAnz; i++){
    if (newPos[i] != pos[i]) {
      Serial.print( i );
      for (int j=0; j < encAnz; j++){
        if( newPos[i] <= 0 ){
          switch (i){
            case 0: myEnc0.write(0); break;
            case 1: myEnc1.write(0); break;
            case 2: myEnc2.write(0); break;
            case 3: myEnc3.write(0); break;
            case 4: myEnc4.write(0); break;
            case 5: myEnc5.write(0); break;
            case 6: myEnc6.write(0); break;
            case 7: myEnc7.write(0); break;
          }
           newPos[i] = 0;
           pos[i] = 0;
        }
        Serial.print ("\t");
        Serial.print ( newPos[j] );
      }
      Serial.println("");
      pos[i] = newPos[i];
    }
  }
}

void requestEvent(){
  union {
    int encoder [8];
    byte buf [16];
    } encValues;
  encValues.encoder [0] = pos[0];
  encValues.encoder [1] = pos[1];
  encValues.encoder [2] = pos[2];
  encValues.encoder [3] = pos[3];
  encValues.encoder [4] = pos[4];
  encValues.encoder [5] = pos[5];
  encValues.encoder [6] = pos[6];
  encValues.encoder [7] = pos[7];
  Wire.write((byte *) &encValues, sizeof encValues);
}



Das Polling Programm:
 
/* Encoder_I2C_poller

  by christian marmann, 130425
 
  dedicated to the CSQ-1 project

  this programm polls encoder position data via I2C from another
  atmega328 which handles the encoders


*/


#include <Wire.h>
int var[] = {0,0,0,0,0,0,0,0};
int bus_adr = 2;

void setup()
{
  Wire.begin();        // join i2c bus (address optional for master)
  Serial.begin(38400);  // start serial for output
}

void loop(){
  getEncValues();
  for( int i=0; i<8; i++ ){
    Serial.print(var[i]);
    Serial.print("\t");    // print the character
  }
  Serial.println();
  delay(10);
}

void getEncValues() {
  union {
    int encoder [8];
    byte buf [16];
    } encValues;
 
  if (Wire.requestFrom( bus_adr, sizeof encValues) != sizeof encValues)
    return;  // oops!
  
  for (byte i = 0; i < sizeof encValues; i++)
    encValues.buf [i] = Wire.read ();
  
  var[0] = encValues.encoder [0];
  var[1] = encValues.encoder [1];
  var[2] = encValues.encoder [2];
  var[3] = encValues.encoder [3];
  var[4] = encValues.encoder [4];
  var[5] = encValues.encoder [5];
  var[6] = encValues.encoder [6];
  var[7] = encValues.encoder [7];
}



Der Test mit den Encodern:



Aus Fehlern lernt man:

Während Register 1 einwandfrei funktionierte, traten beim Register 2 Unregelmäßigkeiten auf. Die Zähler reagierten teils sprunghaft und entwickelten ein "Eigenleben". Auch nach vertauschen der Prozessoren und beim Test mit einem einzelnen Encoder zeigten sich diese Effekte. Der Verdacht war, das es entweder an der Platine selbst läge oder Störsignale durch einen Designfehler entstünden. Bei der Durchsicht der Platine mit starker vergrößernder Lupe konnte ich keine Brücken oder Unterbrechungen ausmachen.
Beim Debuggen mit Hilfe der Firmware und des seriellen Monitors kam zum Vorschein, dass immer wieder der setup() Block aufgerufen wurde, was in der I2C Polling Routine zu den unregelmäßigen Anzeigen führte.
Da dies beim Betätigen jedes Encoders der Fall war, vermutete ich, das es an der Masseverbindung liegen könnte. Beim Durchmessen der Masseleiterbahnen mit einem Widerstandsmessgerät konnte ich dann eine Leiterbahnunterbrechung lokalisieren, die ich zuvor mit der Lupe nicht entdeckt hatte. Evtl.hatte sich hier beim Tonertransfer ein Haar auf der Oberfläche befunden. Es kam eine Silberdraht-Brücke über die Lücke. Nicht schön, aber nun funktioniert auch Register 2. Also:
Mut zur Lücke!
[... wird fortgesetzt]