Timer und Counter: Projekt-1

WTFPL-2
Hinweis: Wenn du diese Seite bearbeitest, stimmst du zu, dass deine Bearbeitungen zusätzlich unter den Bedingungen der WTF Public License veröffentlicht werden.
WTFPL-2


Die Angaben in diesem Kapitel orientieren sich am Funktionsumfang eines AtMega328p Microcontrollers. Angaben zu Seitenzahl und Sektion beziehen sich auf das zugehörige Datenblatt [M328p].

Die Timer/Counter eins AVR Microcontrolers können für die Klangerzeugung missbraucht werden. Um einen Ton zu erzeugen kann an einem der Ausgabepins ein Rechtecksignal erzeugt werden. Das Rechtecksignal kann dann mit einem Piezo-Lautsprecher ausgegeben werden.

Die gewünschte Tonhöhe kann dabei über die Länge des Zählintervalls variiert werden. Wird für die Ausgabe des Rechtecksignals einer der beiden Ausgabepins eines Timers verwendet und der Compare Output Mode des Pins entsprechend konfiguriert, so kann das Umschalten zwischen high und low der Hardware überlassen werden.

Konfiguration Timer

Bearbeiten

Um bei der Wahl der Frequenz des Timers einen großen Spielraum zu haben, wird der 16-bit Zähler TC1 verwendet.

Damit die Länge des Zählintervalls vorgegeben werden kann, muss ein Modus gewählt werden, bei dem der maximale Zählerstand im Register OCR1A vorgegeben werden kann. In diesem Projekt werden wir den Timer im Fast PWM Mode betreiben. Es muss also der Wave Genration Mode 15 gewählt werden.

Als Ausgabepin soll der Pin OCA1 verwendet werden. Damit er zur richtigen Zeit high und low geschaltet wird, muss ein geeigneter Output Compare Mode vorgegeben und die Datenrichtung das Pins auf Ausgabe gesetzt werden. Für den Output Compare Mode werden wir den Wert 1 vorgeben. In diesem Modus wird der Zustand des Pins bei jedem Erreichen des maximalen Zählerstands invertiert. In zwei aufeinander folgenden Durchläufen des Zählintervalls steht der Ausgabepin so einmal low und einmal high und erzeugt so das gewünschte Rechtecksignal.

Dem Umstand, dass für die Erzeugung einer Periode des Rechtecksignals zwei Zählerdurchläufe nötig sind, müssen wir später bei der Berechnung der Frequenz des erzeugten Signals Rechnung tragen.

Mit den Hilfsfunktionen des vorangegangenen Kapitels kann die Initialisierung des Timers wie folgt vorgenommen werden:

void setup() {
  t1_power(1);
  t1_wgm(15);

  t1_a_out();
  t1_coma(1);
}

Tonhöhen

Bearbeiten

Um eine Folge von Tönen abspielen zu können, muss die Folge der gewünschten Tonhöhen in irgendeiner Form codiert werden. Bei der Wahl dieser Codierung sind wir frei. Für dieses Projekt wurde entschieden, die Tonhöhe mit der zugehörigen MIDI Tonnummer codieren.

Die Stufen der MIDI Tonnummern entsprechen den Tonstufen der gleichstufigen Stimmung. Die Tonnummer 69 entspricht dem Kammerton A (440 Hz). Die Frequenz der MIDI Tonummer m kann somit wie folgt berechnet werden:

 

Da der Microcontroller über keine Maschinenbefehle für Berechnungen mit Fließkommazahlen verfügt, muss jede Berechnung mit float Werten in eine Serie von Maschinenbefehlen übersetzt werden. Es ist deshalb ratsam die Zahl benötigter float Berechnungen so gering wie möglich zu halten. Für die Berechnung im C Quellcode kann der Exponent   durch Division mit Rest in   zerlegt werden. Einsetzten liefert.

 

  mit   für  

Mit den vorberechneten Werten   kann folgender Code Schnipsel verwendet werden:

float F_i = {
   8.175798,  8.661957,  9.177023,  9.722718, 10.300861, 10.913382,
  11.562325, 12.249857, 12.978271, 13.749999, 14.567617, 15.433853
};

float f_m(uint8_t m) {
   float value = (2 << (m/12)) * F_i[m%12];
   return value;
}

Frequenz

Bearbeiten

Die Frequenz, mit der Timer das Zählintervall durchläuft bestimmt die Tonhöhe. Für die Erzeugung einer Periode des Rechtecksignals muss das Zählintervall zwei Mal durchlaufen werden. Die damit erzeugt Frequenz hängt sowohl vom Wert des OCR1A Registers ab, als auch von der Taktrate, mit der der Zählerstand voranschreitet. Für die Berechnung der erzeugten Frequenz gilt:

 

Sowohl den Wert von OCR1A, als auch den Prescaler Wert können wir in gewissen Grenzen vorgeben. Der maximale Wert, der für OCR1A (16-bit) gewählt werden kann beträgt 65535. Der Prescaler Wert kann durch Wahl der gewünschten Taktquelle nur in den Stufen 1 / 8 / 64 / 256 / 1024 vorgegeben werden. Umstellen der obigen Formel liefert:

 

Je geringer der Prescaler Wert gewählt ist, umso höher ist die Auflösung, die wir durch Vorgabe von OCR1A erreichen können. Wir machen bei der Wahl eines Paars OCR1A, prescaler zu gegebener Frequenz also sicher keinen Fehler, wenn wir im Rahmen der Möglichkeiten den Wert von OCR1A so groß wie möglich, d.h. den Wert von prescaler so klein wie möglich wählen.

Im C Quellcode kann die Wahl von Taktquelle cs und OCRA Wert ocra in Abhängigkeit von der gewünschten Frequenz f wie folgt implementiert werden:

uint32_t ocra  = (F_CPU / 2) / (uint32_t) f;

uint16_t prescaler[] = { 0, 1, 8, 8, 4, 4 };
uint8_t cs     = 1;

while (1) {
   if (ocra < 65536 || cs == 5)
      break;
   ++cs;
   ocra /= prescaler[cs];
   }
ocra -= 1;

Mit den Code Schipseln der vorangegangenen Abschnitte haben wir bereits alle Voraussetzungen geschaffen, um für eine gegebene Tonnummer den zugehörigen Ton auszugeben. Die Aufgabe den Ausgabepin zu den richtigen Zeitpunkten high und low zu setzen haben wir dabei auf die Hardware der Timer Einheit abgewälzt. Die einzige Aufgabe, die wir zum abspielen einer ganzen Melodie in Software durchführen müssen, besteht darin die gewünschten Tonhöhen im richtigen Rhythmus vorzugeben.

Die Folge der gewünschter Tonnummern können wir zu diesem Zweck in einem Array vorgeben. Im Programm werden wir sie diesem Array in einem festen Rhythmus entnehmen und abspielen. Um mit dieser einfachen Herangehensweise auch Töne unterschiedlicher Länge abspielen zu können, müssen wir die zugehörige Tonnummer entsprechend der gewünschten Länge mehrfach hintereinander in das Array legen. Eine Möglichkeit Pausen zu spielen haben wir nicht.

Das Repertoire, das wir mit diesem Ansatz abspielen können ist sehr beschränkt. Zu jedem Zeitpunkt kann immer nur ein einziger Ton gespielt werden. Mehrstimmige Melodien können wir also nicht abspielen.

Ein Versuch die ersten Takte des Präludium in C-Dur von Johann Sebastian Bach an die Einschränkungen des Programms anzupassen, findet sich in folgendem Code Schnipsel. Um konventionellen Speicherplatz zu sparen, kann das Array in den Programmspeicher gelegt werden. Die einzelnen Werte können dann im Programm mit der Funktion pgm_read_byte() eingelesen werden.

const uint8_t song[] PROGMEM = {
   60, 64, 67, 72, 76, 67, 72, 76,    60, 64, 67, 72, 76, 67, 72, 76,
   60, 62, 69, 74, 77, 65, 74, 77,    60, 62, 69, 74, 77, 65, 74, 77,
   59, 62, 67, 74, 77, 67, 74, 77,    59, 62, 67, 74, 77, 67, 74, 77,
   60, 64, 67, 72, 76, 67, 72, 76,    60, 64, 67, 72, 76, 67, 72, 76,
   60, 64, 69, 76, 81, 69, 76, 81,    60, 62, 66, 69, 74, 66, 69, 74,
   59, 62, 67, 74, 79, 67, 74, 79,    59, 62, 67, 74, 79, 67, 74, 79,
   59, 60, 64, 69, 72, 64, 69, 72,    57, 60, 64, 69, 72, 64, 69, 72,
   50, 57, 62, 66, 72, 62, 66, 72,    55, 59, 62, 67, 71, 62, 67, 71,
};

Quellcode

Bearbeiten

Hier abschließend der vollständige Quellcode des Projekts. Um ein lauffähiges Programm daraus zu machen nicht vergessen ihn mit den Timerfunktionen aus dem vorangegangenen Kapitel zu linken.

#include <avr/io.h>
#include <util/delay.h>
#include <stdio.h>

#include <avr/interrupt.h>
#include <avr/pgmspace.h>

#include "log.h"
#include "timer.h"

// Forward Declarations
void setup(void);
void loop(void);

float f_m(uint8_t m);

#define sbi(port, bit) (port) |= (uint8_t)  (1 << (bit))
#define cbi(port, bit) (port) &= (uint8_t) ~(1 << (bit))

const uint8_t song[] PROGMEM = {
  60, 64, 67, 72, 76, 67, 72, 76,    60, 64, 67, 72, 76, 67, 72, 76,
  60, 62, 69, 74, 77, 65, 74, 77,    60, 62, 69, 74, 77, 65, 74, 77,
  59, 62, 67, 74, 77, 67, 74, 77,    59, 62, 67, 74, 77, 67, 74, 77,
  60, 64, 67, 72, 76, 67, 72, 76,    60, 64, 67, 72, 76, 67, 72, 76,
  60, 64, 69, 76, 81, 69, 76, 81,    60, 62, 66, 69, 74, 66, 69, 74,
  59, 62, 67, 74, 79, 67, 74, 79,    59, 62, 67, 74, 79, 67, 74, 79,
  59, 60, 64, 69, 72, 64, 69, 72,    57, 60, 64, 69, 72, 64, 69, 72,
  50, 57, 62, 66, 72, 62, 66, 72,    55, 59, 62, 67, 71, 62, 67, 71,
};

float F_i[] = {
   8.175798,  8.661957,  9.177023,  9.722718, 10.300861, 10.913382,
  11.562325, 12.249857, 12.978271, 13.749999, 14.567617, 15.433853
};

float f_m(uint8_t m) {
  float value = (2 << (m/12)) * F_i[m%12] + 0.5;
  return value;
}

void setup() {
  t1_power(1);  // disable power reduction
  t1_wgm(15);   // wave generation mode 15: fast pwm max=OCR1A

  t1_a_out();   // 
  t1_coma(1);   // output compare mode 1: toggle
}

void loop() {
  unsigned int i;

  for (i=0; i< sizeof(song); ++i) {

    uint8_t m = pgm_read_byte(song + i);  // MIDI number
    float f = f_m(m);                     // frequency
    
    // choose: cs, ocra
    uint32_t ocra = (F_CPU / 2) / (uint32_t) f;
    uint16_t prescaler[] = { 0, 1, 8, 64, 256, 1024 };
    uint8_t cs = 1;
    while (1) {
      if (ocra < 65536 || cs == 5)
	break;
      ++cs;
      ocra /= prescaler[cs];
    }
    ocra += 1;
    
    // set: cs, ocra
    t1_cs(cs);
    t1_ocra((uint16_t)ocra);
    
    _delay_ms(300);
  }
}

#ifndef ARDUINO
int main(void) {
  setup();
  while(1)
    loop();
}
#endif

Ergebnis

Bearbeiten
 
Versuchsaufbau
Ergebnis

Fußnoten

Bearbeiten



 
Du hast das Recht unter den Bedingungen der WTF Public License mit diesem Dokument anzustellen was zum Teufel auch immer Du willst.