PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : Sound-Funktion



ManniMammut
18.04.2006, 14:55
Hallo!
Ich bin grad am überlegen, wie ich meinen kleinen Piezo-Speaker ansprechen kann. Das funktioniert schon ganz gut, wenn ich nen Port in Endlosschleife mit delays immer ein- und ausschalte. Jetzt würde ich das ganze nur noch gerne in eine elegantere Funktion schreiben, der ich nur noch ne Frequenz und ne Tonlänge übergebe. In etwa so:


sound( 440, 1000 ); //erzeugt ein a (440Hz), das 1000ms andauert

Der Piezo hängt bei meinem Mega32 an PortD.6 mit PWM ist also nix zu machen. Rechenzeit spielt erstmal keine Rolle, die Frequenz kann also auch mit Delays erzeugt werden. Timer o.Ä. wären natürlich eleganter.
Achja, wäre schön, wenn die Funktion so einfach wie möglich gehalten würde, damit sie ein Anfänger auch verstehen kann :oops:

Vielen Dank, Manni

SprinterSB
18.04.2006, 16:56
Am besten geht das in einem Timer.

Hier mal eine Routine zum Initialisieren, aus dem Wiki zusammengeschnitzt:

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/signal.h>

// Werte zur Berechnung der Interrupt-Rate bei AVR-Fuses,
// die auf 1MHz eingestellt sind (Werkseinstellung für internen RC-Oszillator)
#define F_CPU 1000000 // 1 MHz

// Prototypen der Funktion(en)
void timer1_init (uint16_t);

// Initialisiert Timer1, um mit einer Frequenz von freq IRQs auszulösen
void timer1_init (uint16_t freq)
{
// ATmega: Mode #4 für Timer1 und voller MCU-Takt (Prescale=1)
TCCR1A = 0;
TCCR1B = (1 << WGM12) | (1 << CS10);

// OutputCompare1A Register setzen
OCR1A = (uint16_t) ((uint32_t) F_CPU / freq -1);

// OutputCompare1A Interrupt aktivieren
TIMSK |= (1 << OCIE1A);
}

// Die Interrupt Service Routine (ISR) wird mit
// der Frequenz freq ausgeführt.
SIGNAL(SIG_OUTPUT_COMPARE1A)
{
// mach was
}

void main()
{
timer1_init (1000);
sei();

while (1);
}

Für klassische AVRs wie AT90S2313 muss die Initialisierung etwas abgeändert werden, weil sich Bit-Bezeichner unterscheiden:


// AVR Classic:
// Timer1 läuft mit vollem Takt
// CTC: Clear Timer on CompareMatch
// Timer1 ist Zähler
TCCR1A = 0;
TCCR1B = (1 << CS10) | (1 << CTC1);

ManniMammut
18.04.2006, 19:15
Vielen Dank schonmal!


#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/signal.h>


// Initialisiert Timer1, um mit einer Frequenz von freq IRQs auszulösen
void timer1_init( uint16_t freq )
{
// ATmega: Mode #4 für Timer1 und voller MCU-Takt (Prescale=1)
TCCR1A = 0;
TCCR1B = ( 1 << WGM12 ) | ( 1 << CS10 );

// OutputCompare1A Register setzen
OCR1A = ( uint16_t ) ( ( uint32_t ) 1000000 / freq -1 ); // konstante 1000000, weil F_CPU auf meinen 16MHz-Quarz gesetzt ist

// OutputCompare1A Interrupt aktivieren
TIMSK |= ( 1 << OCIE1A );
}

// Die Interrupt Service Routine (ISR) wird mit
// der Frequenz freq ausgeführt.
SIGNAL(SIG_OUTPUT_COMPARE1A)
{
// mach was
PORTD ^= ( 1 << PD6 );
}

int main( void )
{
DDRD |= ( 1 << PD6 );

timer1_init( 264 );
sei();

while (1);
}

Damit kann ich zumindest schonmal die Frequenz(en) erzeugen. Das funktioniert auch schon sehr zuverlässig (hab mehrere Töne mit unserem Klavier verglichen).
Aber wie bringe ich den AVR dann dazu, den Ton nur ne bestimmte Zeit zu spielen? Nehm ich da 'ne Zählvariable, die den Ton nur "rauslässt", wenn der Ist-Wert noch unter dem Soll-Wert liegt?

SprinterSB
18.04.2006, 19:26
Du hast duch bestimmt noch mehr Zähler. Nimm einen als Zeitbasis, der zB einen Grundtakt von 1ms oder 10ms macht.

Im Hauptprog kannst du dann die Frequenz setzen und tönen/schweigen, so lang wie es sein soll.

Die ANpassung von F_CPU machst du besser im Makro, und nicht an allen Stellen der Quelle, wo es auftaucht. Da übersieht du sonst leicht eine Stelle, wenn du mal nen anderen Quarz dranhängt.

Die Töne brauchst du übrigens nicht zur Laufzeit immer wieder auszurechnen (die Teilung F_CPU/freq), sondern man kann die OCR-Werte speichern anstatt die Frequenzen. Ausrechnen überlässt man GCC.

Könnte etwa so aussehen mit den Daten. Hier ein "Happy Birthday" :-)



#define BASE 440

#define C0 1.0
#define D0 1.122
#define E0 1.26
#define FIS0 1.414
#define G0 1.498
#define A0 1.682
#define H0 1.888
#define C1 2*1.0
#define D1 2*1.122

#define FREQ(x) (F_CPU/(2*(x)-1))
#define TON(a,b,c) { FREQ(BASE*a), DAUER(b), PAUSE(c) }
#define DAUER(x) 12*x
#define PAUSE(x) 5*x

typedef struct
{
uint16_t freq;
uint8_t dauer;
uint8_t pause;
} ton_t;

const ton_t morse_ton[] PROGMEM =
{
TON (D0, 1, 1),
TON (D0, 1, 1),
TON (E0, 2, 1),
TON (D0, 2, 1),
TON (G0, 2, 1),
TON (FIS0, 4, 2),

TON (D0, 1, 1),
TON (D0, 1, 1),
TON (E0, 2, 1),
TON (D0, 2, 1),
TON (A0, 2, 1),
TON (G0, 4, 2),

TON (D0, 1, 1),
TON (D0, 1, 1),
TON (D1, 2, 1),
TON (H0, 2, 1),
TON (G0, 1, 1),
TON (G0, 1, 1),
TON (FIS0, 2, 1),
TON (E0, 2, 2),

TON (C1, 1, 1),
TON (C1, 1, 1),
TON (H0, 2, 1),
TON (G0, 2, 1),
TON (A0, 2, 1),
TON (G0, 4, 1),
{}
};

ManniMammut
18.04.2006, 21:21
Hmmm, ich bin grad nen bisschen gefrustet, weil ich aus deinem letzten Beitrag eigentlich nix verstanden hab.
Dann habe ich das "Blinky-Tutorial" mal angeschaut - funktioniert soweit auch (auch wenn's 10mal so schnell blinkt; liegt wohl am externen Quarz. btw, wie passt man das auf die tatsächliche Taktfrequenz ändern? ändern von F_CPU hat nix gebracht) - aber was dadrin passiert und wofür die ganzen Register da sind hab ich nicht kapiert. Dann habe ich mir das AVR-GCC-Tut angeschaut und im Timer-Kapitel ebenso nix verstanden... :(

Kann mir vll jemand erklären, was bei dem Blinky überhaupt passiert? Wäre schön, wenn das jemand so ausführlich wie möglich kommentieren könnte.

Vielen Dank, Manni

lionking
18.04.2006, 21:35
nette idee musik mitm µC zu machen... ich hab vor langer zeit mal unter dos mal mit som programm musik über den pc-piepser gemacht, leider weiss ich nicht mehr wie das programm hiess, war noch auf disketten.

@sprinter: 440Hz hat das A und nicht das C... aber bei dieser anwendung ist das ja eigentlich egal.

SprinterSB
19.04.2006, 12:22
@lionking: Oje, ich wollte mir eben nicht den Wolf transponieren :oops:


In dem Blinky-Beispiel muss man F_CPU anpassen und danach neu übersetzen. Bei manchen Makefile-Generatoren wird das F_CPU schon im Makefile gesetzt bzw. als Parameter an avr-gcc übergeben.

Am einfachsten verständlich ist das Beispiel blinky-all.c, das alles in einer Quelle vereint. Die Quelle ist auch kommentiert, um zu sagen, was jeweils gemacht wird. Allerdings wird nicht das ganze AVR-Handbuch wiederholt, das wäre *etwas* viel. Hast du mal den Abschnitt zu Timer1 in deinem Handbuch gelesen? Da stehen die Erklärungen der einselnen SFR-Bits, und wozu die gut sind.

lionking
19.04.2006, 14:41
okok, das c' hat 264Hz

ManniMammut
22.04.2006, 15:35
Melde mich nun mit dem nächsten Problem zurück :-)
Timer1 habe ich jetzt glaub ich verstanden. Jetzt habe ich folgenden Code ausprobiert:


#include <avr/io.h>
#include <avr/signal.h>
#include <avr/interrupt.h>
#include <avr/delay.h>

#define INTERRUPTS_PER_SECOND 1000

int laenge, sound_on;

// Wird durch jeden IRQ eins hochgezählt
static volatile uint16_t irq_count = 0;

// Wert für das OutputCompare-Register (OCR1A)
#define OCR_VAL (F_CPU / INTERRUPTS_PER_SECOND -1)

// Initialisierung der Hardware
void ioinit( void )
{
DDRB |= ( 1 << PB3 );
DDRD |= ( 1 << PD6 );

// Initialisiert Timer1, um jede Sekunde 1000 IRQs auszulösen
// ATmega: Mode #4 für Timer1 und voller MCU-Takt (Prescale=1)
TCCR1A = 0;
TCCR1B = ( 1 << WGM12 ) | ( 1 << CS10 );

// OutputCompare1A Register setzen
OCR1A = ( uint16_t ) ( ( uint32_t ) OCR_VAL );

// OutputCompare1A Interrupt aktivieren
TIMSK |= ( 1 << OCIE1A );
}

// Die Interrupt Service Routine (ISR) wird INTERRUPTS_PER_SECOND mal
// pro Sekunde ausgeführt. irq_count zählt die Aufrufe und blinkt die LED
// wenn 1 Sekunde vergangen ist.
SIGNAL( SIG_OUTPUT_COMPARE1A )
{
uint16_t count;

// irq_count um 1 erhöhen und
count = 1+irq_count;

if( count >= laenge )
{
count = 0;

PORTB ^= ( 1 << PB3 );

sound_on = 0; //Sound soll durch die while-Schleife in sound(...) abgeschaltet werden
}

irq_count = count;
}

void sound( int length )
{
laenge = length;
sound_on = 1;

while( sound_on != 0 ) //während sound_on = 1 ist, soll irgend eine Frequenz erzeugt werden. sobald sound_on = = ist, soll die Schleife unterbrochen werden.
{
PORTD ^= ( 1 << PD6 );
_delay_ms( 100 );

sei();
}
}

// Das Hauptprogramm (Einsprungpunkt)
int main( void )
{
//Peripherie initialisieren
ioinit();

PORTB |= ( 1 << PB3 );

sound( 1000 );

while(1);
}


An PortB.3 hängt eine LED, welche ganz brav wie im Tutorial im Abstand von einer Sekunde ein- und ausgeknipst wird. Das funktioniert auch ->Der Timer funktioniert an sich. Allerdings will ich die sound(...)-Funktion auch nur eine Sekunde ausführen. Das soll nach einer Sekunde durch das Unwahr-Machen der while-Schleife passieren. Warum funktioniert das nicht? Das Programm soll einfach nur einmal nach einer Sekunde die while-Schleife (und damit auch die sound-Funktion) beenden und die LED an PortB.3 weiterblinken lassen.

Eigentlich müsste das doch von der Idee her funktionieren? Tut es aber nicht...

u.A.w.g. ;-), Manni

SprinterSB
22.04.2006, 18:28
Nö, so nicht sound_on ist einmal global, einmal lokal. Es bezieht sich also auf unterschiedliche Objekte...

Das globale sound_on ist zudem volatile (flüchtig). Die Deklaration des lokalen sound_on fliegt in die Tonne!

ManniMammut
23.04.2006, 12:07
Nö, so nicht sound_on ist einmal global, einmal lokal. Es bezieht sich also auf unterschiedliche Objekte...

sound_on ist aber doch nur einmal, nämlich global, definiert?!



Das globale sound_on ist zudem volatile (flüchtig). Die Deklaration des lokalen sound_on fliegt in die Tonne!

Wie gesagt, sound_on ist nur einmal, global, deklariert...

Ich dachte globale Variablen kann ich überall (also egal aus welcher Funktion) ändern und die Variable nimmt dann in allen Funktionen diesen Wert an?

Wenn ich jetzt Müll geredet habe, bin ich in meinen Grundfesten der C-Prorammierung erschüttert.

SprinterSB
23.04.2006, 12:26
sound_on ist aber doch nur einmal, nämlich global, definiert?!

Ooops, stimmt, da hab ich was falsch gelesen.


Ich dachte globale Variablen kann ich überall (also egal aus welcher Funktion) ändern und die Variable nimmt dann in allen Funktionen diesen Wert an?

Wenn ich jetzt Müll geredet habe, bin ich in meinen Grundfesten der C-Prorammierung erschüttert.
Jein. Wenn du zum Beispiel hinschreibst (a global)

a = 0;
if (0 == a)
{
}

Dann wird ein (optimierender) Compiler das if immer ausführen, wenn er weiß, daß er a auf 0 gesetzt hat. Damit ist für ihn die Bedingung immer erfüllt! Wenn zB zwischen der Zuweisung und dem if eine IRQ auftritt, in der a verändert wird, hat der Compiler keine Möglichkeit, das zu wissen.
Daher definiert man a als volatile.
int volatile a;
und sagt damit, daß a flüchtig ist, sich also ohne Zutun des Compilers (er weiß ja nicht wann eine IRQ zuschlagen könnte oder ob die Hardware evtl a ändert) ändern kann.

Das volatile hat die Konsequenz, daß jedes mal, wenn auf a zugegriffen wird, dieses auch wirklich geschrieben oder gelesen wird. Damit wird die 0 in a gespeichert und danach a wieder gelesen.

Und alles wird gut...

ManniMammut
24.04.2006, 08:10
Ah!
Okay, danke, wieder was dazugelernt. Werde ich dann mal daheim ausprobieren (sitze im Moment in der Schule...).

//€DIT: Sobald ich sound_on als volatile int deklariere, geht's 8)

So, dann muss ich mal schauen, wie ich weiterkomme.

SprinterSB
24.04.2006, 17:33
Naja, so lernt man zwar einprägsam, aber durch Lesen von Tutorials geht's manchmal schnaller ;-)