1 LCD-Counter mit einem ATtiny2313

Dieser Frequenzzähler wurde vor einigen Jahren für einen Kurzwellenempfänger gebaut. Das war damals meine erste zarte Berührung mit AVR-Controllern und BASCOM. Die Vorlage von DL1DMW für den Black Forest Counter [2] wurde an die Platinengröße des 16x2-LCD angepasst. Er zeigt eigentlich die Frequenz des Superhet-Oszillators (LO) an, subtrahiert aber vor der Anzeige die Zwischenfrequenz, so dass sich die Empfangsfrequenz ergibt. Wahlweise kann er die Zwischenfrequenz auch addieren, falls die LO-Frequenz unterhalb der Empfangsfrequenz liegt. Ein etwas komfortablerer Frequenzzähler mit einem ATmega8 ist unten in Abschnitt 3 beschrieben.

1.1 Schaltung

LCD counter circuit

Abb. 1.1: RX-Frequenzanzeige.

Die Messfrequenz wird zunächst mit T1 und T2 verstärkt, damit sie der Schmitt-Trigger IC2 zu einem sauberen Rechteck umformen kann. R5 und R6 sorgen dafür, dass der Schmitt-Trigger die Zählfrequenz symmetrisch um die halbe Versorgungsspannung am Eingangspin 9 vorgesetzt bekommt. Die Eingangsempfindlichkeit ist besser als 4 mVpp im gesamten Kurzwellenbereich (Abb. 1.2). Der 8 fach-Teiler IC3 bietet dem AVR, IC4, die Messfrequenz mundgerecht zur Zählung an. Infolge der Frequenzteilung durch 8 und mit der Quarzfrequenz von 16 MHz reicht der Messbereich bis knapp unter 64 MHz, in Abb. 1.2 gemessen bis 62 MHz. Die Anzeige erfolgt mit einem 16x2-Standard LCD. Falls der Zähler auch mit offenem HF-Eingang verwendet wird, hilft ein Widerstand von ca. 10k zwischen Eingang und Masse, Einstreuungen zu unterdrücken.

50 MHz Counter sensitivity

Abb. 1.2: Eingangsempfindlichkeit von 1 bis 62 MHz.

Die Stufungen in Abb. 1.2 sind auf die Schrittweite von 1 dB des verwendeten Stufenabschwächers zurückzuführen.

Die Einstellung der Fuse-Bits ist im Quellcode (unten im Download) angegeben. Insbesondere muss das beim ATtiny2313 ab Werk eingestellte CKDIV8 (Clock-Teiler) deaktiviert werden.

Die Korrektur mit der Zwischenfrequenz kann mit den Jumpern a bis c eingestellt werden. Es sind zwei gängige Zwischenfrequenzen programmiert: 9 MHz und 10,7 MHz.

Über T3 kann noch die jeweilige BFO-Einstellung (LSB = lower sideband bzw. USB = upper sideband) zur Anzeige gebracht werden.

Da die Platine genau passend auf die LCD-Platine gesteckt werden sollte, wurden damals, der SMD-Technik noch nicht so ganz zugetan, ATtiny2313 und 74HC-IC in DIP-Bauform verwendet, Restbestückung gemischt. Nachteil: die 2kByte Programmgröße reicht soeben mit einigen Klimmzügen. Vorteil: Zum Anpassen des Programms und zum Brennen genügt die Trial-Version von BASCOM. Für eine manuelle Eingabe der Frequenzablage war kein Platz mehr. Siehe Abschnitt 3.

1.2 Frequenzplan

Die zu messende Frequenz wird dem 16-Bit-Timer/Counter1 PD5 (T1) zugeführt und während der mit dem 8-Bit-Timer0 festgelegten Zeit gemessen. Beide Timer beziehen ihrem Takt vom Quarzoszillator des AVR.

Da die RS232-Schnittstelle nicht gebraucht wird, ist die Wahl der Quarzfrequenz für den Prozessortakt zunächst völlig frei. Soll aber speicherhungrige Real-Arithmetik zur Frequenzberechnung vermieden werden, wird die Auswahl stark eingeschränkt. Neben anderen Quarzfrequenzen ist z.B. 16 MHz ein guter Aspirant.

LCD counter timing

Tab. 1.1: Timereinstellungen.

Mit der Vorteilung der 16 MHz um 1024 wird der Timer0 mit 15.625 Hz getaktet. Neben dem groben Raster dieser Teilerfaktoren - 8, 64, 256, 1024 – gibt es zum Glück noch eine Feineinstellung mit einem Prescale. Der 8-Bit-Timer0 läuft damit statt 256 Takte von 0 bis 255 von einem bestimmten Prescale an, hier von 56 bis 255, also nur 200 Takte. Ein Timer0-Overflow bei 255 erfolgt damit mit einer Häufigkeit von 78,125 Hz bzw. alle 0.0128 sec. Wenn wir nun 50 solcher Timer0-Overflows warten, haben wir eine Torzeit von 0,64 sec. In dieser Zeit laufen bei 10 MHz Zählfrequenz 800.000 Zählimpulse auf. Geteilt durch 8 ergibt 100.000. Die Teilung durch 8 geht elegant und speichersparend mit einem Bit-Shift um 3 Stellen (8 = 23) nach rechts. Entsprechend formatiert werden dann 10.000,0 kHz (=10 MHz) angezeigt. Aufgelöst wird also bis zur 100 Hz-Stelle.

Bei 5 statt 50 Timer0-Zyklen würden mit 0,064 sec. Torzeit nur 10.000 angezeigt, also Auflösung in kHz.

Die Kalibrierung auf eine genau bekannte Frequenz erfolgt durch Ziehen der Quarzfrequenz mit dem Trimmer C14 (Abb. 1).

1.3 Timerprogrammierung

Die Grundidee stammt von Roland Walter, DL7UNO [1]. Eigentlich macht man es genauso nicht, alle Rechen- und Anzeigefunktionen in eine Timer-Interrupt-Routine zu packen. Hier geht es aber, da der AVR nichts anderes zu tun hat als Frequenzen zu zählen. Hier die wichtigsten Programmausschnitte.

Config Timer0 = Timer , Prescale = 1024
Config Timer1 = Counter , Edge = Rising , Noise_Cancel = 1
Const Presettimer0 = 56                           'Initialize Timer0
Const bytTimer0_MaxRuns = 50                      'Max. timer0 runs

On Timer0 Timer0_isr                              'On Overflow go to Timer0_isr
On Timer1 Timer1_isr                              'On Overflow go to Timer1_isr

Dim dwdCount As DWord                             'Frequency counts
Dim wrdCountLo As Word At dwdCount Overlay        'Counts low word
Dim wrdCountHi As Word At dwdCount + 2 Overlay    'Counts high word
Dim strDis As String * 16                         'Display string
Dim bytDis(16) As Byte At strDis Overlay
Dim strFreq As String * 6
Dim bytFreq(6) As Byte At strFreq Overlay
Dim bytTimer0_runs As Byte                        'Timer0 overflow counter
Dim dwdTmp0 As DWord                              'Temp. variable

'intermediate frequency (IF) in Hz for frequency offset
Const lngZF1 = 9000000                            'IF Offset 1 (Hz)
Const lngZF2 = 10700000                           'IF Offset 2 (Hz)

Der erste Block ist aus Tab. 1 schon bekannt. Im zweiten Block "On Timer0..." werden die Sprünge zu den Overflow-Interrupt-Routinen definiert. Im dritten Block sind drei Overlays angelegt:

  1. wrdCountHi und wrdCountLo belegen den gleichen Speicherplatz wie dwdCount. Damit bilden die unabhängig erzeugten Werte des High- bzw. Low-Words ohne weiteres Zutun den Wert von dwdCount als Zählergebnis.
  2. Die beiden String-/Byte-Overlays mit strDis und strFreq erlauben das wahlweise Hantieren der gesamten Strings bzw. der Einzel-Bytes für die Display-Textaufbereitung.

Vor der leeren Do...Loop erfolgen die Initialisierungen für die Timersteuerung:

bytTimer0_runs = 0
Timer0 = Presettimer0
Enable Timer0
SREG.7 = 1                                        'Enable all interrupts
Start Timer0

Do

Loop

Timer0 (8-Bit-Gate) wird mit dem PresetTimer0 (=56) vorbesetzt und mit dem Enable aktiviert und gestartet. Timer1 (Counter) läuft noch nicht.

Mit dem ersten und allen folgenden Timer0-Interrupts wird die Timer0-Interrupt-Serviceroutine angesprungen. Hier spielt die eigentliche Musik.

Timer0_isr:
'Interrupt Service Routine for Timer0 (Counter gate)
'Set Timer0 preset time
'Increment timer0 runs until > max runs
'Stop Timer1 if max Timer0 runs are expired
'Get number of counts in gate Timer/Counter1 (wrdCountLo)
'wrdCountLo is the low word of overlayed dwdCount
'wrdCountHi,the high word of overlayed dwdCount, was incemented in Timer1_isr
'Divide dwdCount by 8 (right bit shift by 3, 2^3 = 8) = frequency, units 100 Hz
'Add or substract IF frequency if jumper c is in
'display frequeny (row 1), USB/LSB and IF offset (row 2)
'Clear wrdCountHi

   Timer0 = Presettimer0                          'Reset Timer0 start counts
   If bytTimer0_runs = 0 Then
      Enable Timer1
      Start TIMER1
      bytTimer0_runs = 1
   ElseIf bytTimer0_runs < bytTimer0_MaxRuns Then
      Incr bytTimer0_runs
   Else
      Stop Timer1
      wrdCountLo = Timer1                         'get low word of dwdCount
      bytTimer0_runs = 0
      Timer1 = 0
      Shift dwdCount , Right , 3                  'Divide by 8 (2^3)=frequency     

      'Add / substract IF frequency... (not shown)
      'Prepare LCD strings and display them... (not shown) 

      wrdCountHi = 0                              'Clear Timer1 overflow counts    

   End If
Return

Zu allererst wird Timer0 wieder auf den PresetTimer0 zurückgesetzt.

Je nach Zählstand von bytTimer0_runs erfolgt eine Verzweigung mit If...Then...Else:

  1. "bytTimer0_runs = 0" ist die Startbedingung für den Counter Timer1: Enable und Start Timer1. Ab nun wird gezählt.
  2. Mit jedem Überlauf des Timer0 wird die Routine angesprungen. bytTimer0_runs zählt mit, solange die Maximalzahl bytTimer0_MaxRuns (hier 50, vgl. Tab. 1.1) noch nicht erreicht ist.
  3. Die Maximalzahl ist erreicht, die Torzeit ist abgelaufen. Timer1 wird gestoppt, sein Inhalt wird auf wrdCountLo (Low word von dwdCount) gespeichert. bytTimer0_runs wird für den nächsten Durchlauf genullt, ebenso Timer1, nachdem sein Wert auf wrdCountLo gesichert wurde. Das High word von dwdCount, wrdCountHi, steht aus den Timer1-Interrupts, s.u. schon zur Verfügung und damit dwdCount als Zählergebnis.
    Mit "Shift dwdCount , Right , 3" wird durch Verschieben um 3 Bits nach rechts im Endeffekt durch 2^3 dividiert. Ist das Gleiche wie "dwdCount / 8", nur sparsamer. Der ATtiny zwingt zum Sparen.

Damit steht die gemessene Frequenz zur Verfügung. Je nach Stellung der Jumper a bis c erfolgen die Korrektur mit einer Zwischenfrequenz und schließlich die Anzeige. Bei der Stringmanipulation der Anzeigetexte ist die Overlay-Konstruktion hilfreich.

Roland löscht in seinen Listings [1] mit TIFR.TOVn die Overflow-Flags und begründet dies ausführlich. Notwendig ist das dennoch nicht. Overflow-Flags werden mit Sprung in die Overflow-Routine automatisch gelöscht. Das Timer0-Overflow-Flag ist also bereits verschwunden. Ein Timer1-Overflow-Flag steht gar nicht an, da Timer1 ja gestoppt wurde (obige Ziffer 3 der Aufzählung).

Die Interruptroutine für den Timer/Counter1 ist regelrecht langweilig. Hier wird mit jedem 16 Bit-Überlauf lediglich wrdCountHi hochgezählt. Jedes Hochzählen bedeutet eine Addition von 2^16=65536 Zählereignissen.

Timer1_isr:
'Interrupt Service Routine for Timer1 (Counter)
'Increment counter high word. i.e. add 2^16 counts
   Incr wrdCountHi                                'Counter High Word
Return