| Weitere Artikel aus dem Elo-Magazin |
Signalanalyse
von Piotr Platek
| Ganzen Artikel lesen... | ![]() |

Die Ping-Pong-Platine von ELO bietet nicht nur eine (Retro-)Spielplattform, sondern auch eine Entwicklungsumgebung, womit sehr unterschiedliche Anwendungsbereiche getestet werden können. Eine weitere Idee möchte ich hier gerne vorstellen. Der Atmega8-Mikrocontroller hat einen A/D-Wandler (ADC = Analog-to-Digital-Converter), der von der Firmware im Ping-Pong-Spiel zur Ablesung der Potentiometerstellungen und der entsprechenden Steuerung der Tischtennisschläger verwendet wird. Zudem eignet sich die Platine mit ihrer 12x10 LED-Matrix gut zur einfachen, grafischen Darstellung von Messwerten. Die beiden Ausprägungen der Platine -- vorhandener A/D Wandler und 120 frei ansteuerbare LEDs -- haben mich zur Idee der "Visualisierung" von Musik bzw. Audiosignalen angeregt. Jeder kennt das von Musikanlagen oder Computerprogrammen, die parallel zur Wiedergabe bunte Balken im Rhythmus der Musik anzeigen. Die Balken entsprechen dabei Stärke und Frequenz des Signals. Die Signalanalyse kann mit verschieden Mitteln durchgeführt werden. Ein Weg ist die "Diskrete Fourier-Transformation". Die komplexe, mathematische Transformation wird hier mittels eines schnellen Algorithmus "FFT" (Fast Fourier Transform / Schnelle Fourier-Transformation) durchgeführt. Die FFT liefert als Ergebnis die im abgetasteten Signal hauptsächlich vorkommenden Frequenzen mit den zugehörigen Amplituden, die als „bunte Balken" auf dem Ping-Pong-Display angezeigt werden.
An sich nutzt der FFT-Algorithmus Fließkomma-Arithmetik (zur Berechnung der trigonometrischen Funktionen), was aber für den kleinen Atmega8 Mikrocontroller eine gewisse Herausforderung darstellt. Die Berechnung muss außerdem in Echtzeit erfolgen, damit der Rhythmus der Musik verfolgt werden kann. Für den armen ATmega8 ist das definitiv zu viel. Aufgrund der verhältnismäßig einfachen grafischen Darstellung, kann auf die höchste Genauigkeit verzichtet werden und die einfachere Festkomma-Arithmetik verwendet werden.
Zudem können die trigonometrischen Werte im voraus bestimmt und in einer Tabelle als feste Werte gespeichert werden. Mit dieser Vorgehensweise läßt sich die FFT auch sehr gut in Assembler codieren. Um das Rad nicht neu zu erfinden, habe ich im Internet recherchiert und ich bin auf die Seite http://elm-chan.org/works/akilcd/report_e.html gestoßen, auf der eine fertige FFT-Lösung, freundlicherweise mit einem GPL-Lizenz-Model, vorgestellt wird. Diese Lösung hat als niedrigste Auflösung 32 Werte pro Analyselauf. Die Ping-Pong-Platine bietet aber nur 12 LED-Spalten. Deswegen muss das Ausgabeergebnis im Programm entsprechend angepasst werden. Das kann noch im Rahmen der FFT oder danach umgesetzt werden. Beide Fälle habe ich implementiert und getestet. Die Auflösung der FFT habe ich auf 16 Werte begrenzt (und dazu habe noch eine kleine Anpassung in der Datei ffft.S vorgenommen). Von den ursprünglich 32 Werten werden jeweils zwei benachbarte Frequenzwerte zusammengefasst und nur der größere der beiden angezeigt. Zudem werden sowohl die beiden niedrigsten als auch die beiden höchsten Frequenzen (plus Gleichstromkomponente) ausgelassen.
Mein Programm bietet darüber hinaus verschiedene Möglichkeiten der Darstellung. Die Balken können langsam runterfallen, schnell dem Signal folgen und nur einen maximalen Wert hinterlassen oder nur die maximalen Werte anzeigen. Zur Änderung der Darstellung nutze ich den vorhandenen Port D, Pin PD2 als Ansteuerung. Der Pin PD2 wird über einen Mikrotaster mit der Masse des Geräts verbunden. Deshalb bitte beachten, dass die Initialisierung des Ports und die Interrupt-Routine von mir leicht abgeändert wurden.
Als weitere Möglichkeit (der Darstellung) habe ich eine Art Oszilloskop implementiert. In diesem Modus kann die Zeitbasis mit dem linken Poti eingestellt werden. Allerdings sind hier Auflösung des Displays, die (niedrige) Abtastrate, sowie der Signaleingang für ein „professionelles" Oszilloskop nicht geeignet.
Auch wenn Anzeige und Genauigkeit der oben genannten Frequenzanalysedarstellung erstaunlich gut sind, sollte der Signaleingang hier noch einmal kurz besprochen werden. In der Grundversion wird das Audiosignal von der Kopfhörerbuchse direkt über einem Kondensator (100 µF) mit dem P3-Kontakt verbunden.
Wenn jemand einen „richtigen" Audiofrequenzanalysator entwickeln möchte, sollte er den Signaleingang mit einem Tiefpassfilter (low-pass filter) versehen, um eine Glättung des Signals zu erreichen. Noch besser wäre ein Bandbreitenfilter, der für den vorgesehenen Audiofrequenzbereich ausgelegt (berechnet) ist. Als Beispiel möchte ich hier einen Bandbreitenfilter vorschlagen, der mit aktiven Elementen arbeitet (Operationsverstärker mit passiven Elementen versehen), der für eine Bandbreite von 20 Hz-20 kHz ausgelegt (berechnet) ist. Die zwei invertierenden Verstärker in Reihenschaltung sichern ein sauberes Ausgangssignal und eine gute Frequenzantwort.
Download: Pong-FFT
Siehe auch: http://blogs.zobniow.net/micro
/*
* Audio spectrum analyzer for Ping-Pong Board from Conrad.de
*
* Fixed-point FFT routines for megaAVRs (C)ChaN, 2005
* http://elm-chan.org/works/akilcd/report_e.html
*
* Ready for AVR Studio 4 or WinAVR
* Modified multiplaxing display procedure based on the code from
* Laufschrift program written by Sascha Bader
*
* Ver. Date Author Comments
* ------- ---------- -------------- ------------------------------
* 1.00 25.12.2009 Piotr Platek initial
* 2.00 16.01.2009 Piotr Platek correct sampling method, adding display modes
* 2.01 17.01.2009 Piotr Platek changed display frequency to get better sampling accuracy
* 2.02 17.01.2009 Piotr Platek Mode switch moved to PD3 (PD2 is conectyed to Poti
* added oscoloscope mode with different time x-scale
*
*
* Feel free to provide me any feedback
***************************************************************************
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the GNU General Public License
** as published by the Free Software Foundation; either version 2
** of the License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software Foundation,
** Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
***************************************************************************
*/
#define F_CPU 8000000UL
#include <avr/io.h>
#include <stdlib.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include <avr/eeprom.h>
#include <avr/sleep.h>
#include <stdio.h>
#include <inttypes.h>
#include <ctype.h>
#include <util/delay.h>
#include <math.h>
#include "display.h"
#include "ffft.h"
/* defines */
#define FFT_SIZE (FFT_N/2) /* number of result to be plotted */
uint16_t leds[SWIDTH]; /* Screen memory definition */
volatile uint16_t offset = 0; /* index for data buffer colection */
#define FALLDELAY (80) /* falling dwon delay for the freguency bars */
volatile uint16_t delta1 = 0; /* delay counter */
#define PD3DELAY (25) /* delay for butten detections */
#define DEBOUNCING (1) /* debouncing button */
volatile uint16_t pd3_delay = 0; // counter for button test procedure
volatile int8_t pd3_processed= 1; // all events processed
/************************************************************************
* SIG_OVERFLOW0
*
* This routine is run evertime the timer counter TCCR0 overflows.
*
*
* Task:
* 1) decrease counter of for falling down freq. bars based on the peak value
* 2) gather data from ADAC
* 3) drive the screen
*
***********************************************************************/
SIGNAL(SIG_OVERFLOW0) {
uint16_t ledval;
uint8_t portdout;
uint8_t portcout;
static uint8_t col = 0;
cli();
if (pd3_processed) // check only if the previous key event was processed
{
if (bit_is_clear(PIND, PD3)) // check the button PD3
{
if (pd3_delay < PD3DELAY) pd3_delay++;
}
else
{
if (pd3_delay < DEBOUNCING) // filter out button bouncing
{
pd3_delay = 0; // start new process it was just a short bump
}
else
{
pd3_processed = 0; // stop checking PD3 as long as not procceed in main loop
pd3_delay = 0; // start new process it was just a short bump
}
}
}
if (delta1) delta1--; // as long as not 0 decrease the delay for falling down bars
if (col == SWIDTH) col = 0;
PORTB = 0;
portdout = PORTD & 0b00001111; // save lower first 4 bits and zero higher 4
PORTD = portdout;
portcout = PORTC & 0b11110000; // save hiher first 4 bits and zero lower 4
PORTC = portcout;
if ( col == 0 ) PORTB &= ~(1 << 4);
else PORTB |= (1 << 4);
PORTB |= (1 << 3);
PORTB &= ~(1 << 3);
PORTB |= (1 << 2);
PORTB &= ~(1 << 2);
ledval = leds[col++];
PORTC = (ledval & 0x000f) | portcout;
PORTD = (ledval & 0x00f0) | portdout;
PORTB = (ledval >> 8) & 0x0003;
sei();
}
int main(void)
{
int16_t capture[FFT_N]; /* Wave captureing buffer */
complex_t bfly_buff[FFT_N]; /* FFT buffer */
uint16_t spektrum[FFT_N/2]; /* Spectrum output buffer */
uint8_t k, m;
int16_t SpecVal;
uint16_t peaks[SWIDTH];
int8_t mode = 0;
uint8_t PotiSwitch = 0b0111;
int16_t TimeScale = 100;
int16_t TimeScaleCounter = 0;
/* initialize port data directions */
DDRC = 0x0f; // 0x0f PORTC7.6 als AD-Eingang
DDRB = 0xff; // Portb = Output
// this is important to setup PD3 as input
DDRD = 0b11110111; // Portd = Output but PD3
PORTD |= 0b00001000; // Pull Up at PD3 aktivieren
/* initialize timers */
TCCR0 |= _BV(CS01) | _BV(CS00); // clk prescale (clk/64)
TIMSK |= _BV(TOIE0); // enable timer/counter0 overflow interrupt
TCNT0 = 0; // reset the timer
/* initialize the ADC */
// ADMUX |= _BV(ADLAR); // we only want 8-bit
ADMUX |= _BV(MUX2) | _BV(MUX1) | _BV(MUX0);
// ADMUX |= _BV(REFS1) | _BV(REFS0);
ADMUX |= _BV(REFS0);
// Setup ADC sampling clock
// ADCSRA &= ~_BV(ADPS2) & ~_BV(ADPS1) & ~_BV(ADPS0); // ADC=CLK/2
// ADCSRA |= _BV(ADPS1); // ADC=CLK/4
// ADCSRA |= _BV(ADPS1) | _BV(ADPS0); // ADC=CLK/8
// ADCSRA |= _BV(ADPS2); // ADC=CLK/16
ADCSRA |= _BV(ADPS2) | _BV(ADPS0); // ADC=CLK/32
// ADCSRA |= _BV(ADPS2) | _BV(ADPS1); // ADC=CLK/64
// ADCSRA |= _BV(ADPS2) | _BV(ADPS1) | _BV(ADPS1); // ADC=CLK/128
ADCSRA |= _BV(ADEN);
ADCSRA |= _BV(ADSC); // Start the first conversion
sei(); //enable global interrupts
for (m=0; m < SWIDTH; peaks[m++]=0);
while(1)
{
if (offset >= FFT_N) // buffer of samples is full - ready to process FFT
{
// process FFT
fft_input(capture, bfly_buff);
fft_execute(bfly_buff);
fft_output(bfly_buff, spektrum);
offset = 0; // release the next buffer data collection
m = 0; // start the index for peaks and screen memory
for (k=2; k <= FFT_SIZE-1; k+=1) // the other spectrum modes
{
SpecVal = spektrum[k];
// SpecVal = spektrum[k] > spektrum[k+1] ? spektrum[k] : spektrum[k+1];
if (m <= SWIDTH-1) // Process first SWIDTH-1 results
{
/*************************************************************************
* There is no true filter on input.
* I tried to eliminate the constat part of the signal and some disruption
* and noice by introducing a software filter
**************************************************************************/
if (SpecVal <= 4 ) SpecVal = 0;
else SpecVal /= 3;
if (SpecVal > 10 ) SpecVal = 10; // if there is a peak bigger then 10, make it 10
if (SpecVal > peaks[m]) peaks[m] = SpecVal; // save a maximum value in the peak table
// Display results in one of 3 modes
switch (mode)
{
case 0: // falling down bars
leds[m] =
((FULLBAR << (10-SpecVal)) & FULLBAR) |
((FULLBAR << (10-peaks[m])) & FULLBAR);
break;
case 1: // real time bars + falling dwon peaks
leds[m] =
((FULLBAR << (10-SpecVal)) & FULLBAR) |
((0b0000000000000001 << (10-peaks[m])) & FULLBAR);
break;
case 2: // falling down peaks
leds[m] = ((0b0000000000000001 << (10-peaks[m])) & FULLBAR);
break;
case 3: // real time bars
leds[m] = ((FULLBAR << (10-SpecVal)) & FULLBAR);
break;
case 4: // Osciloscope
break;
default: // clear display
leds[m] = 0;
}
m++; // proceed to the next point
}
}
}
else
if (mode==4) // Osciloscope mode
{
if (!bit_is_set(ADCSRA, ADSC)) // check if conversion is ready
{
if ( PotiSwitch == 0b0111) // check the side ADC7 = Osci. Input
{
for (m=0; m < SWIDTH-1 ; m++) // scroll screen from right to left
{
leds[m] = leds[m+1];
}
SpecVal = ADC;
SpecVal /= 51; // depends on the input volatage
leds[SWIDTH-1] = 1 << (9-SpecVal);
// if (TimeScaleCounter > 50)
{
ADMUX &= ~_BV(MUX0);
PotiSwitch = 0b0110; // Next measurement for ADC6
TimeScaleCounter = 0; // Next ADC for Time Scale
}
// else TimeScaleCounter++;
ADCSRA |= _BV(ADSC); // Already start conversion
_delay_ms(TimeScale);
}
else // the other side -< ADC6 = Left Poti
{
TimeScale = ADC;
TimeScale /= 33;
ADMUX |= _BV(MUX0);
PotiSwitch =0b0111;
ADCSRA |= _BV(ADSC); // start the next ADC conversion
}
}
}
else
{ // Gather next set of samples
// cli(); // for highe screen refresh frequency
// it is recomended to disable interupts
// for sampling time
ADMUX |= _BV(MUX2) | _BV(MUX1) | _BV(MUX0); // Ensurethe correct input line
do
{
if (!bit_is_set(ADCSRA, ADSC)) // check if conversion is ready
{
capture[offset++] = ADC-32768; // convert to signed linear
ADCSRA |= _BV(ADSC); // start the next ADC conversion
}
} while (offset < FFT_N);
// sei(); // see cli() above remarks
if (!delta1) // the time for bar fall down has elapsed
{
cli(); // see comment above
delta1 = FALLDELAY; // setup a new delay for next fall down
sei();
for (m=0; m <= SWIDTH-1; m++)
{
if (peaks[m]) peaks[m]--; // decrease all peaks by 1
};
}
}
// check keyboard
if (!pd3_processed) // there is not yet processed key event
{
mode ++;
if (mode > 4) mode = 0;
pd3_delay = 0; // start the new check for key event
pd3_processed = 1; // --//--
}
}
}















