Make Magazin 3/2016
S. 86
Anleitung
Aufmacherbild

Audioeffekt mit ARM

Kaum ein hipper Musikstil kommt noch ohne Vintage-, Retro- oder Low-Fidelity-Qualitäten aus. Den individuellen Anstrich bekommt man am besten mit selbstgebauter Effekttechnik, zum Beispiel mit einem knackigen Bitcrusher. Unser Modell kann subtil mit zehn Bit bis zu substanziell mit nur einem Bit auflösen.

Low-Fidelity-Effekte sind in Zeiten aalglatter digitaler High-Tech-Audio-Systeme nicht nur zu einem anerkannten Stilmittel, sondern sogar zum letzten Schrei geworden. In dieser Anleitung basteln wir mit einem kleinen ARM-Prozessor so einen Effekt: einen Bitcrusher, mit dem man die Auflösung eines Audiosignals von maximal zehn Bit bis auf ein einziges Bit runterschrauben kann. Die Elektronik kann man zum Beispiel in einer sogenannten Tretmine für E-Gitarren einsetzen.

Das wichtigste Bauteil des Bitcrushers ist ein LPC810r-ARM-Mikrocontroller des Herstellers NXP, auf dem eine kleine Software läuft, die ein Audiosignal manipuliert. Im Artikel ARM-Training in diesem Heft wird die ARM-Architektur genauer vorgestellt. Der LPC810 enthält einen Kern des Typs ARM Cortex-M0+.

Um Audiosignale in den LPC810 hineinzubekommen, muss man einen externen Analog-Digital-Wandler benutzen, weil der Chip selbst keinen Wandler enthält. Wir verwenden dafür den MCP3204, den man über eine SPI-Schnittstelle ansteuert und ausliest. Der LPC810 kann zwar nicht mit allzu viel Peripherie glänzen, ist aber mit gleich zwei SPI-Schnittstellen ausgestattet. Die Ausgabe der manipulierten Audiosignale erfolgt mit Hilfe eines einfachen Verfahrens zur Digital-Analog-Wandlung: der PWM (Pulsweitenmodulation).

Entwicklungsumgebung

Vor dem Aufbau der Schaltung richten wir die Software ein, mit der man den Code kompiliert und in den LPC810 lädt. NXP bietet für seine LPC-Mikrocontroller die auf Eclipse basierende Entwicklungsumgebung LPCXpresso an, die jedoch keine Code-Downloads in den Mikrocontroller per USB-Seriell-Wandler unterstützt. Das stellt aber keine Hürde dar, denn diese Funktion kann das frei verfügbare Programm Flash Magic übernehmen. Zu den Download-Links gelangt man über unsere Internetadresse am Ende des Artikels.

Schaltung aufbauen

Schaltung auf ein Brotbrett gebaut

Der LPC810 muss nun noch mit dem Analog-Digital-Wandler verbunden werden. Als Stromversorgung kann der USB-Seriell-Wandler benutzt werden, vier Batterien oder ein Netzteil mit fünf bis zwölf Volt. Mit dieser Schaltung kann der Bitcrusher schon verwendet werden, zum Beispiel mit dem Ausgangssignal eines Computers oder Smartphones und als Ausgabe kann man einen Kopfhörer anschließen.

Der Spannungsregler LF33 sorgt dafür, dass die Schaltung mit 3,3 Volt versorgt wird. Er ist wie im Datenblatt angegeben an seinem Eingang mit einem 100 nF gegen Masse und am Ausgang mit einem 2,2 µF ebenfalls gegen Masse beschaltet. Der PWM-Audio-Ausgang des LPC810 ist mit 10 kΩ geschützt, so dass man einen Kopfhörer anschließen kann und trotzdem nicht zu viel Strom fließt. Außerdem wird das PWM-Signal mit einem 2,2-µF-Kondensator geglättet.

Die Datenleitungen und Clock-Signale der SPI-Schnittstelle sind in Blau dargestellt. Der Spannungspin für den analogen Teil des MCP3204 ist mit einem 10-µF-Kondensator gegen Masse beschaltet, um eine möglichst rauschfreie Aufnahme des Audiosignals zu ermöglichen. Das ist der Kondensator, der direkt neben den blauen Leitungen liegt.

Audiosignal auf die virtuelle Masse gezogen

Der Audioeingang ist mit einem 10-µF-Kondensator entkoppelt. Zwei 10-kΩ-Widerstände bilden einen Spannungsteiler zwischen 3,3 Volt und Masse, womit 1,65 Volt erzeugt werden, die den Audioeingang des MCP3204 auf diese Spannung ziehen. 1,65 Volt ist eine sogenannte virtuelle Masse.

Verbesserte Schaltung

Verbesserte Schaltung mit Eingangs- und Ausgangsverstärker

Man kann die Schaltung noch verbessern, dann ist sie aber für ein Breadboard nicht mehr unbedingt geeignet. Hier ist ein TS1872 Operationsverstärker für den Eingang (links) und Ausgang (rechts) verwendet worden, alternativ geht auch der MCP6002. Die Verstärkung kann eingestellt werden, indem der Widerstand in der Rückkopplung verändert wird (R2 am Eingang und R9 am Ausgang). Sind beide Widerstände gleich, bleibt der Signalpegel gleich. Je größer der Widerstand gewählt wird, desto größer ist die Verstärkung. Am Ausgang ist in der Rückkopplung ebenfalls noch ein Kondensator enthalten, der das PWM-Signal etwas glättet.

Code: main

Die #include-Anweisungen am Anfang des Codes binden die CMSIS-Bibliothek für die LPC800er-Serie ein und die Treiber für die SPI-Schnittstelle. Die #define-Anweisungen funktionieren ähnlich einer Variablen, aber der Text, der dem Namen hinter dem #define folgt, wird noch vor dem Kompilieren im Code ersetzt. Danach stehen die Definitionen einer Interruptroutine (Zeile 11) und einer Funktion (Zeile 39), die später noch genauer beschrieben werden.

Das Programm beginnt mit der Routine main() in Zeile 47. Zuerst wird hier die Clock eingestellt, die den Takt für den Prozessorkern erzeugt. Der LPC810 hat eine für unsere Zwecke ausreichend genaue Clock, die mit einer festen Frequenz von 12 MHz läuft. Für Anwendungen, bei denen diese Frequenz ausreicht, genügt es, die Routine SystemCoreClockUpdate() für die Initialisierung der Clock zu verwenden. Mit 30 MHz erreicht man aber eine deutlich bessere Rechenleistung und für den Bitcrusher hat das den Vorteil, dass damit die PWM für die Erzeugung des Audio-Ausgang-Signals schneller laufen kann. Damit aus der internen Clock ein 30-MHz-Clock-Signal erzeugt werden kann, benutzt man das PLL-Modul (Phase Locked Loop beziehungsweise Phasenregelschleife), das einen Frequenzteiler und -multiplizierer enthält. In den Zeilen 49 bis 58 wird die PLL initialisiert und entsprechende Teiler- und Multipliziererwerte eingestellt, so dass der LPC810 danach auf 30 MHz läuft.

Nun wird die Routine configurePins() aufgerufen. Der Code darin sieht ein bisschen kryptisch aus, man muss ihn aber nicht schreiben, sondern kann ihn sich mit dem LPC-Initialisation-Tool zusammenklicken, so wie im Screenshot. Das Tool kann im Browser ausgeführt werden oder heruntergeladen werden, um es offline auszuführen. Wenn man seine Einstellungen abgeschlossen hat, kann man sich im unteren Reiter aus der Datei swm.c den erzeugten Code holen. Man sollte jedoch im oberen Bereich des Fensters die passende Bibliothek wählen. Für unseren Code ist das CMSIS. Die Internetadresse am Ende des Artikels führt zu einem Link zum LPC-Initialisation-Tool.

Peripherie konfigurieren

Das LPC-Initialisation-Tool zum Einstellen der Pin-Funktionen

Danach folgt die Konfiguration der PWM. Beim LPC810 ist das etwas komplizierter als bei anderen Mikrocontrollern, denn er enthält kein einfaches Timer- beziehungsweise PWM-Modul. Dafür hat er einen State Configurable Timer (SCT), der enorm leistungsfähig ist und mit dem man weit mehr als nur eine einfache PWM realisieren kann. Der Code wird dadurch aber etwas länger. Der SCT funktioniert nach einem einfachen Grundprinzip: er fängt bei 0 an und zählt die Prozessortakte, bis er wieder auf 0 gesetzt wird. In den Zeilen 62 bis 75 wird der SCT initialisiert. Es gibt sogenannte Matches, das sind Werte, bei denen ein Ereignis (Event) ausgelöst werden kann, wenn der Zähler des SCT sie erreicht. Das Match 0 soll den SCT auf 0 setzen, und zwar genau dann, wenn der Zähler 1024 erreicht. Wir benutzen insgesamt zwei Matches und zwei Events. Match 0 löst außer dem Setzen des Zählers auf 0 Event 0 aus. Event 0 schaltet den Ausgang der PWM auf 0, und Match 1, das Event 1 auslöst, setzt den Ausgang der PWM auf 1. Bevor der SCT gestartet wird, gibt es noch eine Zeile Code, die das Auslösen eines Interrupts von Event 0 festlegt.

Die Konfiguration der SPI-Schnittstelle (Zeile 77) ist dank CMSIS sehr einfach in nur einer Zeile geleistet. Mit der Hexadezimalzahl 0x40 wird der Teiler der Clock festgelegt, mit der die SPI läuft. Sie muss für den MCP3204 wesentlich langsamer als 30 MHz sein. Wir sind experimentell bis an die äußerste Grenze gegangen, was aber die Audioqualität verbessert. In Zeile 79 wird schließlich der Interrupt für die SCT aktiviert. Der weitere Code ist eine leere while-Schleife. Daten empfangen, gesendet und bearbeitet werden nur in der Interruptroutine.

Interruptroutine

  1 #include "LPC8xx.h"
  2 #include "lpc8xx_spi.h"
  3 
  4 #define SPI_STAT_RXRDY (0x1)
  5 #define SPI_STAT_TXRDY (0x2)
  6 #define SPI_TXDATCTL_SSEL_N(s) ((s) << 16)
  7 #define SPI_TXDATCTL_EOT (1 << 20)
  8 #define SPI_TXDATCTL_FLEN(l) ((l) << 24)
  9 #define ADC_CH_D0 0b001000000
 10 
 11 void SCT_IRQHandler() {
 12   uint16_t rcv_data[2];
 13   uint16_t signal = 0;
 14   static uint8_t channel = 0;
 15   static uint16_t bitcrush = 0;
 16 
 17   channel ^= ADC_CH_D0; // Kanal abwechseln
 18   // 0b11000 senden, um die Konversion auf Kanal 1 zu starten
 19   while(~LPC_SPI0->STAT & SPI_STAT_TXRDY); // warten bis der SPI zum senden bereit ist und senden
 20   LPC_SPI0->TXDATCTL = SPI_TXDATCTL_FLEN(15) | SPI_TXDATCTL_SSEL_N(0xe) | 0b0000011000000000 | channel;
 21   while(~LPC_SPI0->STAT & SPI_STAT_RXRDY); // warten bis der SPI die Daten fertig empfangen hat
 22   rcv_data[0] = LPC_SPI0->RXDAT; // empfangene Daten in rcv_data[0] speichern
 23   while(~LPC_SPI0->STAT & SPI_STAT_TXRDY); // warten bis der SPI zum senden bereit ist und senden
 24   LPC_SPI0->TXDATCTL = SPI_TXDATCTL_FLEN(7) | SPI_TXDATCTL_EOT | SPI_TXDATCTL_SSEL_N(0xe) | 0;
 25   while(~LPC_SPI0->STAT & SPI_STAT_RXRDY); // warten bis der SPI die Daten fertig empfangen hat
 26   rcv_data[1] = LPC_SPI0->RXDAT; // empfangene Daten in rcv_data[1] speichern
 27   rcv_data[0] &= 0x0f; // ungültige Bits 0 setzen
 28 
 29   if(channel != ADC_CH_D0) {
 30     signal = (rcv_data[0] << 8) | rcv_data[1];
 31     signal = (signal >> 2) >> bitcrush;
 32     LPC_SCT->MATCHREL_L[1] = signal << bitcrush;
 33   } else {
 34     bitcrush = rcv_data[0]; // 4 bits => 0 bis 15, gebraucht wird aber 0 bis 9
 35     bitcrush = (bitcrush * 5) >> 3; // (15 * 5) >> 3 = 75 >> 3 = 9
 36   }
 37 }
 38 
 39 void configurePins() {
 40   LPC_SYSCON->SYSAHBCLKCTRL |= (1 << 7); // SWM Clock anschalten
 41   LPC_SWM->PINASSIGN3 = 0x05ffffffUL; // SPI0_SCK
 42   LPC_SWM->PINASSIGN4 = 0xff020403UL; // SPI0_MOSI, SPI0_MISO, SPI0_SSEL
 43   LPC_SWM->PINASSIGN6 = 0x00ffffffUL; // CTOUT_0
 44   LPC_SWM->PINENABLE0 = 0xffffffffUL;
 45 }
 46 
 47 int main() {
 48   //SystemCoreClockUpdate(); // => statt 12 MHz will ich 30 MHz:
 49   LPC_SYSCON->PDRUNCFG &= ~(1 << 7); // PLL anschalten
 50   LPC_SYSCON->SYSPLLCLKSEL = 0; // interner RC als Mainclock
 51   LPC_SYSCON->SYSPLLCLKUEN = 0;
 52   LPC_SYSCON->SYSPLLCLKUEN = 1; // Clock Update
 53   LPC_SYSCON->SYSPLLCTRL = 4 | (1 << 5); // M = 4+1, P = 2 => PLLout = 12*5/2 = 30 MHz
 54   while(~LPC_SYSCON->SYSPLLSTAT & 1); // warten auf PLL Lock
 55   LPC_SYSCON->MAINCLKSEL = 0x3; // PLL als Clock verwenden
 56   LPC_SYSCON->MAINCLKUEN = 0;
 57   LPC_SYSCON->MAINCLKUEN = 1; // Clock Update
 58   while(!(LPC_SYSCON->MAINCLKUEN & 0x01)); // warten bis Clock Update fertig
 59 
 60   configurePins();
 61 
 62   LPC_SYSCON->SYSAHBCLKCTRL |= (1 << 8); // SCT Clock anschalten
 63   LPC_SYSCON->PRESETCTRL |= (1 << 8); // SCT aus dem Reset holen
 64   LPC_SCT->CONFIG |= (1 << 17); // AUTOLIMIT_L (Match 0 löst Counter Clear aus)
 65   LPC_SCT->CONFIG &= ~((1 << 1) | (1 << 2)); // Bus Clock verwenden
 66   LPC_SCT->MATCHREL_L[0] = 1024; // SCT Limit = 1024
 67   LPC_SCT->MATCHREL_L[1] = 511; // Pulsweite 50/50 setzen
 68   LPC_SCT->EVENT[0].STATE = 0xFFFFFFFF; // Event 0 tritt in allen States auf
 69   LPC_SCT->EVENT[0].CTRL = (1 << 12);  // Event 0 ist mit Match 0 verknüpft
 70   LPC_SCT->EVENT[1].STATE = 0xFFFFFFFF; // Event 1 tritt in allen States auf
 71   LPC_SCT->EVENT[1].CTRL = (1 << 0) | (1 << 12); // Event 1 ist mit Match 1 verknüpft
 72   LPC_SCT->OUT[0].SET = (1 << 0); // Event 0 setzt SCTx_OUT0 high
 73   LPC_SCT->OUT[0].CLR = (1 << 1); // Event 1 setzt SCTx_OUT0 low
 74   LPC_SCT->EVEN = (1 << 0); // Event 0 löst Interrupt aus
 75   LPC_SCT->CTRL_L &= ~(1 << 2); // Timer starten
 76 
 77   SPI_Init(LPC_SPI0,0x40,CFG_ MASTER,DLY_PREDELAY(0)|DLY_POSTDELAY(0)|DLY_FRAMEDELAY(0)|      |DLY_INTERDELAY(0));
 78 
 79   NVIC_EnableIRQ(SCT_IRQn);
 80   while(1) {
 81   }
 82 }

In der Interruptroutine SCT_IRQHandler() werden insgesamt 24 Bit per SPI an den MCP3204 gesendet und empfangen. Das passiert in zwei Schritten: Zuerst werden 16 Bit gesendet und empfangen und dann noch mal 8 Bit, da die SPI des LPC810 nur 16 Bit am Stück verarbeiten kann. Laut Datenblatt des MCP3204 muss ein Startbit und ein Bit für den Modus sowie 3 weitere Bits für den Kanal gesendet werden, damit die Analog-Digital-Wandlung gestartet wird. Alle weiteren Bits werden ignoriert. Hat der MCP3204 die Daten erhalten, beginnt er mit der Wandlung und zwei Taktzyklen später mit der Übertragung der digitalen Daten an den LPC810. Es wird zunächst gewartet, bis der SPI bereit ist (while-Schleifen) und dann jeweils in der folgenden Zeile gesendet beziehungsweise empfangen. Da die empfangen Daten ebenfalls auf zwei Pakete verteilt sind, müssen sie wieder korrekt zusammengesetzt werden (Zeile 30).

In der Variable channel wird mit Hilfe eines exklusiven Oders bei jedem Aufruf der Interruptroutine ein Bit gekippt, so dass der Kanal bei jedem Aufruf gewechselt wird. In Zeile 30 und 34 werden die Daten je nach Kanal in einer Variablen zwischengespeichert. Die Variable bitcrush in Zeile 34 benutzt nur einen Teil der Daten aus der Analog-Digital-Wandlung, nämlich die vier hochwertigsten Bits. Das Potentiometer wird also in recht großen Schritten abgetastet, was aber genügt, denn es werden nur neun Schritte benötigt.

DSP

Die digitale Signalverarbeitung (DSP), also das Herzstück des Bitcrushers, besteht aus zwei Shift-Operationen. Das Audiosignal vom MCP3204 ist zwölf Bit breit und der Ausgang der PWM nur zehn Bit. Zuerst werden die zwei überflüssigen Bits mit einem Shift nach rechts entfernt. Danach werden weitere Bits, abhängig von der Potentiometerstellung per Shift entfernt. Damit von der Lautstärke nichts verloren geht, wird das Signal anschließend mit einem Shift nach links wieder auf seinen ursprünglichen Pegel gebracht. Die maximal mögliche Weite des Shifts beträgt neun Schritte, da das Signal zehn Bit breit ist, bleibt dann noch ein letztes Bit übrig. Um den Wertebereich des Potentiometerkanals von 0 bis 15 auf 0 bis 9 zu beschränken könnte man eine Division durch 1,6666 durchführen, aber sparsamer und für spezielle Menschen leichter lesbar ist eine Multiplikation mit 5 und ein Shift nach rechts um 3.

Ausblick

Auf den ersten Blick sieht das Ganze vielleicht ein bisschen viel aus, aber davon sollte man sich nicht abschrecken lassen, denn unser Code ist dank der hervorragenden Dokumentation von NXP mit nur wenigen Stunden Aufwand entstanden. Auf der Internetplattform LPCWare gibt es ein reichhaltiges Angebot an gut dokumentiertem Beispielcode und digitalen Handbüchern. Ebenfalls empfehlenswert ist das SCTimer/PWM cookbook für alle, die den SCT voll ausschöpfen wollen. Einen Link zur LPC800-Seite von LPCWare und zum SCTimer/PWM cookbook findet man unter der Internetadresse am Ende des Artikels. Viele Beispiele gibt es auch in LPCXpresso, die man mit dem Button „Import projects“ erreichen kann. Man wählt dazu das Code-Archiv NXP_LPC8xx_SampleCodeBunlde.zip aus (Import projects / Browse), das im Ordner Examples / Legacy / NXP / LCP800 abgelegt ist, und wählt dann am besten alle Projekte aus.

Das Mini-Effektgerät kann natürlich noch zu anderen Zwecken als nur dem Bitcrushing verwendet werden. Ein sehr kurzes Echo, ein Phaser- oder Flangereffekt oder dank der Rechenpower des ARM-Kerns im LPC810 ist gar ein digitaler Filter machbar. Im Grunde müssen dazu nur die Zeilen 31 bis 35 verändert werden. Da der MCP3204 noch zwei weitere Kanäle hat, kann man bis zu drei Potentiometer anschließen. Um die Audioqualität nicht leiden zu lassen, führt man in jedem zweiten Aufruf der Interruptroutine eine Wandlung des Audiokanals durch und wechselt dann die Potentiometer der Reihe nach ab. pff