/* Programm für Vertikalsynchronisation und Klasse-D-Endstufe
* für ATtiny13 @ 16 MHz
* „h#s“ Henrik Haftmann, TU Chemnitz, 22. März 2016
* tabsize = 8, encoding = utf-8 (ohne Windows-Header BOM)
Hardware:
1 PB5 ADC0 Einstellung der Bildhöhe (Sollamplitude) 3,75 .. 5 V
2 PB3 PCINT3 Kombinations-Sync-Eingang, negativ, H: 15..16 kHz, V: 48..62 Hz
3 PB4 ADC2 Spulenstrommessung (Istwert)
4 GND 0 V
5 PB0 OC0A Horizontalausgang (70% Tastverhältnis, nicht springend)
6 PB1 OC0B Push-Pull-Ausgangstreiber, invertierend
7 PB2 Booster-Transistor, H = Boost
8 Ucc 5 V
Da der Reset-Eingang mit einer „hohen“ Analogspannung benutzt wird,
ist das Programmieren der Reset-Disable-Fuse nicht unbedingt notwendig.
PWM-Trägerfrequenz: 38 kHz (8-Bit-PWM) zeilensynchron
Zum Vergleich: STV9380A: Modulationsfrequenz 140 kHz asynchron;
Rückschlag 700 µs ≈ 11 Zeilen
Was noch reinpasst:
* Automatische Erkennung der Sync-Polarität
* Schnelle HSync-Anpassung nach Vsync für Signal aus VHS-Videorekorder
* Koindizenzerkennungs-Ausgang (am Bildhöhen-Poti)
* OneWire-Digitalsteuerung (an Stelle des Bildhöhen-Potis);
für's TV-übliche I²C wäre ein größerer µC angebracht.
Unklar: Regelzeitkonstante für HSync (sollte für verrauschtes TV-Signal lang sein)
Daten PAL-Fernsehsignal:
H: 1,5 µs vordere Schwarzschulter
4,7 µs Synchronimpuls
5,8 µs hintere Schwarzschulter: Σ 12 µs
V: 625 Zeilen, 575 sichtbar
5 Vortrabanten: Zeilensynchronimpulse doppelter Frequenz und halber Dauer
5 Synchronimpulse (2,5 Zeilen lang) 32 µs - 4,7 µs = 27,3 µs
— erster Impuls im Raster zum Zeilensynchronimpuls = Beginn Zeile 0,
— erster Impuls in 2. Hälfte = inmitten Zeile 312
5 Nachtrabanten
*/
#define __SFR_OFFSET 0
#include <avr/io.h>
.section .fuse,"a",@progbits // --set-section-flags=.fuse="alloc,load" ersparen
.byte 0b11111010 // kein Taktteiler, kein SPI
.byte 0b11111110 // kein Reset
.section .signature,"a",@progbits
.byte SIGNATURE_0,SIGNATURE_1,SIGNATURE_2
.text
/* H:
Quadrant[----- 00 -----][----- 01 -----][----- 10 -----][----- 11 -----][-
Timer0 0 128 256 384 512 640 768 896 1024
µs 0 8 16 24 32 40 48 56 64
___________________________________________________________
Sync(1) \___/ \_ Horizontalsynchronimpuls
____
Sync(2) \__________________________________________________________/ \_ Komb. Vertikalsynchronimpuls (Vollbildmodus)
_____________________________ _____________________________
Sync(3) \_/ \_/ \_ Vor- oder Nachtrabanten {(3)-(5) = Halbbildmodus}
____ ____
Sync(4) \__________________________/ \__________________________/ \_ Komb. Vertikalsynchronimpuls
_____________________________ ____
Sync(5) \_/ \__________________________/ \_ Halbzeilig einsetzener Vertikalsynchronimpuls
_____________
Sync(6) \____________________________________________________ Nur Vertikalsynchronimpuls (unkorreliert)
PinChg PpPpPppppp PpppPpPpppp Ppp Pp Interrupts (Großbuchstaben) und Rechenzeiten
T0Ovl O───────o O─────────o
T0CmpA Aaaaaaaaaa↓ A A──aaaaaaaaaaaaaaaaaaa↓ (O verzögern ist unkritisch, P nicht!)
OCR0A var 40 00 ↓ E0 Wert und Zeitpunkt, wirkt nach Zählerüberlauf
OCR0B var Wert und Zeitpunkt, wirkt nach Zählerüberlauf
_________________________________________________
PB0 \_____________/ \_ zum Zeilentransistor
_ _ _ _ _
PB1 / \\\\\\\\\\\\\_/ \\\\\\\\\\\\\_/ \\\\\\\\\\\\\_/ \\\\\\\\\\\\\_/ PWM-Vertikalendstufensteuerung
___________
ADC ____/fertig \Start____________________________________________ synchron laufender A/D-Wandler
12 13 0 1 2 3 4 5 6 7 8 9 10 11
½Zeile n n+1 n+2 alle 32 µs
Der Quadrant 01 wird per CTC ein wenig verkürzt,
um hinreichend streng zeilensynchron mit wenig Jitter (< 1 ‰!)
den Zeilensynchronimpuls auszugeben.
(Die 16-bit-Uhr aus TCNT0 und R9 springt dann bspw. von 509 auf 512.)
Das beeinträchtigt die PWM-Vertikalendstufensteuerung ein wenig.
Daher wird OSCCAL so groß wie möglich gehalten.
Über ungleichmäßige Hsync in der Nähe der Vertikalsynchronisation
aus Videorecordern mache ich mir hier keine Gedanken.
Ohne HSync (6) läuft die Rattermaschine einfach mit der maximalen OSCCAL-Frequenz durch.
V:
½Zeile 0 64 128 192 256 320 384 448 512 576 640
hex: 040 080 0C0 100 140 180 1C0 200 240 280
PB2 /\______________________________________________________________________________/\
PB1 __........ooooooooooooooooOOOOOOOOOOOOOOOOOOOOOOOOOOOOOººººººººººººººººººººººººº__
≈ADC +100 +80 +60 +40 +20 0 -20 -40 -60 -80 -100 negativ gehender Ablenkstrom
≈OCR1B 0 28 48 68 88 108 128 148 168 188 208 228 steigender Wert nach unten
PWM LL mehr Low ... ... 50% ... ... mehr High LL „nichtinvertierende“ PWM
Registerbelegung:
R0 temporär
R1 Null
R3:R2 A/D-Wert für Strom-Null, um 10 0000 0000 (512) herum bei 0,55 V
R5:R4 Bildhöhe = Amplitude des Sägezahnstroms, immer 01 xxxx xxxx (256..511)
R7:R6 Rechenfaktor aus Bildhöhe/Zeilenzahl, für konstante Bildhöhe bei PAL↔NTSC-Umschaltung
R8 Bresenham-Akku für jitterarme Zeilensynchronisation auf CTC-Basis (OSCCAL wäre viel zu grob)
R9 High-Teil des Zählers, Quadrantenzähler (Bit 1 und 0)
R11:R10 tic = Zeitstempel der fallenden Flanke
R13:R12 e' = vzb. integrierter Fehler (für den PI-Regler)
R15:R14 A/D-Wert, y (PI-Regler) wenn Halbzeilenzahl >= 6, Zähler für Hauptprogramm
R17:R16 temporär
R19:R18 temporär
R23:R22 Halbzeilenzahl
R25:R24 Aktuelle Halbzeile (Halbzeilen erforderlich wegen Zeilensprungverfahren)
R27:R26 Feinabgleich der Zeilenfrequenz (Soft-PLL), normal 0xFF00
R29:R28 frei
R31:R30 frei
RAM und EEPROM sind ungenutzt, ebenso der Analogvergleicher.
*/
#define Kp 100
#define Ki 10 // y=Ki*(e-e')+Kp*e; e'=e;
#define MAXLINE 640
#define MIN_CTC 0xFC00
#define MAX_CTC 0xFF00
.macro outi port,val
ldi r16,\val
out \port,r16
.endm
// Interrupttabelle mit Kode
rjmp main
reti // INT0
rjmp pinchg
rjmp ovl
reti // EEPROM
reti // Analogvergleicher
// ISR: Vergleich an OCR0A
// Damit sich die beiden ISRs nicht bekriegen,
// wird zwar Timerwert TCNT0 bei der Vorderflanke des Synchronimpulses
// phasenstarr auf Null geregelt,
// aber diese ISR verzögert aufgerufen.
// Pro Bildzeile wird diese ISR 3x aufgerufen
in r0,TCCR0B
outi TCCR0B,0x01 // CTC-Modus beenden
sbrc r0,3 // War im CTC-Modus?
9: reti // diesen Interrupt schnell beenden: Nachtrabanten erkennen; nächster Interrupt kommt in 4 µs
inc r9
outi TIFR0,0x02 // Überlauf-Interrupt löschen (ist hier stets gesetzt)
outi TIMSK0,0x06 // Überlauf-Interrupt freigeben
adiw r24,1 // Halbzeile erhöhen
sbrs r9,1
rjmp q2
// Im Quadrant 00 ist zeitkritische Arbeit; Auslösung erfolgte weit (14 µs = 224 Takte) nach Überlauf
mov r0,r27 // High-Teil der Feinjustierung (Soft-PLL)
add r8,r26 // Bresenham-Akku += Low-Teil der Feinjustierung
adc r0,r1 // Übertrag addieren
out OCR0A,r0 // (R0 ist nahe oder gleich 0xFF)
outi ADCSRB,0x04 // A/D-Wandler mit nächstem Überlauf starten lassen
// Erst ab hier (24 Takte nach ISR) darf der Timer0 einen Überlauf haben
// Da keine Pegelwechsel-Interrupts erwartet werden,
// dürfen Interrupts weiter gesperrt bleiben.
outi TCCR0B,0x09 // CTC-Modus (unklar ob das auch gepuffert wird — lt. Datenblatt nicht)
outi TCCR0A,0x23 // OC0A vom Pin abkoppeln, Pin bleibt High
ldi r16,hi8(MAXLINE)
cpi r24,lo8(MAXLINE)
cpc r25,r16
brcs 9b
rjmp vretr1 // Zwangs-Vertikalrückschlag
// Quadrant 10, Auslösung kurz (4µs = 64 Takte) nach Überlauf
q2: outi ADCSRB,0x01 // A/D-Wandler nicht mehr von diesem Interrupt starten
out OCR0A,r1 // 1 Takt nach Überlauf und niederpriorisiert: Wird von Überlauf-ISR geschluckt
movw r16,r24 // ab hier sind noch rund 450 Takte Zeit, abzüglich möglicher PinChange-ISR-Zeiten
lsr r17
ror r16 // Bit 0 ausschieben
// 5. Einlesen der Bildhöhe
cpi r16,2
cpc r17,r1
brcs 1f // wenn 1
brne 2f
in r4,ADCL // Sollbildhöhe alle 20 ms einlesen (nach 128 µs)
in r0,ADCH // High-Teil weg wegen Reset-Multiplex, Stellbereich ¾Ucc..Ucc
rcall div10
movw r6,r16
rcall mul10
movw r6,r0
clr r21 // Fehler-Akku löschen
clr r20
lsr r23
ror r22 // ab jetzt halbe doppelte Zeilenzahl
reti
1: outi ADMUX,0x42 // wieder Spulenstrom messen
reti
2:
// 6. Ende des Vertikalrückschlags mittels Spulenstrom oder Maximaldauer
in r14,ADCL
in r15,ADCH
sub r14,r2 // jetzt vzb.
sbc r15,r3
cp r14,r4
cpc r15,r5
brge 3f
cpi r16,lo8(10)
cpc r17,r1
brcs 4f
3: cbi PORTB,2
// 7. Sollstrom aus Zeilennummer berechnen
4: movw r18,r24 // lfd. Zeilennummer
sub r18,r22
sbc r19,r23 // vzb. Zeilennummer (-320..+319)
movw r16,r6 // vorberechneter Faktor aus Bildhöhe und Zeilenzahl
rcall mul10
sub r0,r14
sbc r1,r15
movw r18,r0 // e
ldi r16,lo8(Kp)
ldi r17,hi8(Kp)
rcall mul10
movw r14,r0
sub r18,r12 // e-=e'
sbc r19,r13
add r12,r18 // e'+=e
adc r13,r19
ldi r16,lo8(Ki)
ldi r17,hi8(Ki)
rcall mul10
add r14,r0
adc r15,r1
clr r1
ldi r16,0x80
add r14,r16
adc r15,r1
1: brmi 1f
cp r14,r27 // maximal erlaubter PWM-Wert
cpc r15,r1
brcs 2f
out OCR0B,r14 // der berechnete Wert
reti
1: out OCR0B,r1 // der Minimalwert (0)
reti
2: out OCR0B,r27 // der Maximalwert (zumeist 0xFF)
reti
ovl: inc r9 // im Quadrant 01 oder 11
outi TIMSK0,0x04 // Mit Compare-Interrupt abwechseln
sbrc r9,1
rjmp 1f
// Im Quadrant 11 (r9 ist jetzt xxxx xx00)
out TIFR0,r16 // Output-Compare-Interruptflag (wegen OCR0A=0) löschen
outi TCCR0A,0xE3 // inverse PWM für OC0A im Quadrant 0 zum Zeilentransistor
outi OCR0A,0xF0 // nächste Auslösung weit nach Überlauf
reti
// Im Quadrant 01 (r9 ist jetzt xxxx xx10)
1: outi OCR0A,0x40 // nächste Auslösung kurz (> 2,4 µs) nach Überlauf
reti
//Ausgerollte vzl. Multiplikation
//R17:R16 Faktor 1 (10 bit)
//R19:R18 Faktor 2 (16 bit vzb.)
//R1:R0 Produkt/1024 (10 bit vzb.)
//Takte: 6*10+6 = 66
mul10:
clr r1
clr r0
sbrc r16,0
add r0,r18
sbrc r16,0
adc r1,r19
asr r1
ror r0
sbrc r16,1
add r0,r18
sbrc r16,1
adc r1,r19
asr r1
ror r0
sbrc r16,2
add r0,r18
sbrc r16,2
adc r1,r19
asr r1
ror r0
sbrc r16,3
add r0,r18
sbrc r16,3
adc r1,r19
asr r1
ror r0
sbrc r16,4
add r0,r18
sbrc r16,4
adc r1,r19
asr r1
ror r0
sbrc r16,5
add r0,r18
sbrc r16,5
adc r1,r19
asr r1
ror r0
sbrc r16,6
add r0,r18
sbrc r16,6
adc r1,r19
asr r1
ror r0
sbrc r16,7
add r0,r18
sbrc r16,7
adc r1,r19
asr r1
ror r0
sbrc r17,0
add r0,r18
sbrc r17,0
adc r1,r19
asr r1
ror r0
sbrc r17,1
add r0,r18
sbrc r17,1
adc r1,r19
asr r1
ror r0
ret
// Ausgerollte vzl. Division
// R1:R0 = Dividend 10 bit
// R23:R22 = Divisor (Halbzeilenzahl) 10 bit; Divisor>Dividend
// R17:R16 = Quotient*1024
// R1:R0 = Rest
// Takte: max. 3+8*10+4 = 87, min. 3+6*10+4 = 67
div10: clr r16
clr r17
lsl r0
rol r1
cp r0,r22
cpc r1,r23
brcc 1f
sub r0,r22
sbc r1,r23
sbr r17,1<<1
1: lsl r0
rol r1
cp r0,r22
cpc r1,r23
brcc 1f
sub r0,r22
sbc r1,r23
sbr r17,1<<0
1: lsl r0
rol r1
cp r0,r22
cpc r1,r23
brcc 1f
sub r0,r22
sbc r1,r23
sbr r16,1<<7
1: lsl r0
rol r1
cp r0,r22
cpc r1,r23
brcc 1f
sub r0,r22
sbc r1,r23
sbr r16,1<<6
1: lsl r0
rol r1
cp r0,r22
cpc r1,r23
brcc 1f
sub r0,r22
sbc r1,r23
sbr r16,1<<5
1: lsl r0
rol r1
cp r0,r22
cpc r1,r23
brcc 1f
sub r0,r22
sbc r1,r23
sbr r16,1<<4
1: lsl r0
rol r1
cp r0,r22
cpc r1,r23
brcc 1f
sub r0,r22
sbc r1,r23
sbr r16,1<<3
1: lsl r0
rol r1
cp r0,r22
cpc r1,r23
brcc 1f
sub r0,r22
sbc r1,r23
sbr r16,1<<2
1: lsl r0
rol r1
cp r0,r22
cpc r1,r23
brcc 1f
sub r0,r22
sbc r1,r23
sbr r16,1<<1
1: lsl r0
rol r1
cp r0,r22
cpc r1,r23
brcc 1f
sub r0,r22
sbc r1,r23
sbr r16,1<<0
1: ret
pinchg: in r0,TIFR0 // Überlauf-Flag (= 9. Bit des 8-Bit-Zählers) fangen
in r16,TCNT0 // Capture Zählerwert im nächsten CPU-Takt
mov r17,r9 // High-Teil
tst r0 // Wenn Null hatte gerade Überlauf stattgefunden
breq 1f // … dann High-Teil erhöhen
sbrc r0,1 // Zählerüberlauf anhängig?
1: inc r17 // High-Teil der Zeit inkrementieren
sbic PINB,3
rjmp 9f
// Vorderflanke
sub r16,r10
sbc r17,r11 // Differenz bilden
add r10,r16
adc r11,r17 // tic speichern
// Synchronisation der Rattermaschine
// Vorderflanken sind entweder Horizontalsynchronimpulse
// oder Trabanten mit doppelter Zeilenfrequenz.
// Da der Zähler mit vierfacher Zeilenfrequenz laufen soll,
// könnte OSCCAL direkt synchronisiert werden.
// Da jedoch der Horizontalausgang nicht springen sollte,
// (würde eine daran angeschlossene Horizontalendstufe killen)
// werden Trabanten — wie im Fernseher — ignoriert
// 1. FLL: Frequenzsynchronisation
subi r16,lo8(0x380)
sbci r17,hi8(0x380) // Ergebnis sollte 0x0000 bis 0x00FF sein, optimal 0x80
brne 1f // Trabanten und Fehlimpulse aussieben
cpi r16,0x80-8
brcs 1f
cpi r16,0x80+8
brcc 1f
// 2. PLL: Phasensynchronisation bei kleiner Frequenzabweichung
movw r16,r10 // tic holen
andi r16,0x03
sbrc r16,1
ori r16,0xFC // Vorzeichenerweiterung
1: subi r16,0x80
sbci r17,0 // jetzt ±128 (PLL: ±512)
asr r17
lsr r16 // halbieren für dämpfende Wirkung, jetzt ±64 (PLL: ±256)
add r26,r16
adc r27,r17 // größerer CTC-Wert macht die ISRs langsamer
// auf Überläufe prüfen und OSCCAL verstellen
// Bei 15,625 kHz Zeilenfrequenz (PAL) = 64 µs
// ergibt sich eine Frequenz ≤ 16 MHz (statt 9,6 MHz).
// Bei 15,734.. kHz (NTSC) entsprechend ≤ 16,112 MHz
// Der Zähler startet schließlich bei Null bei jedem Zeilensynchronimpuls.
// Damit werden evtl. sichtbare Interferenzen der digitalen PWM-Ausgangsstufe
// auf das Fernsehbild wirksam unterdrückt.
// Denn die PWM-Trägerfrequenz ist mit 62,5 kHz vergleichsweise niedrig.
in r17,OSCCAL
ldi r16,hi8(MIN_CTC)
cpi r26,lo8(MIN_CTC)
cpc r27,r16
brcs 3f // grob: mehr OSCCAL
ldi r16,hi8(MAX_CTC+1)
cpi r26,lo8(MAX_CTC+1)
cpc r27,r16
brcc 4f // grob: weniger OSCCAL
reti
3: subi r27,-2 // in den gültigen Bereich hinauf
inc r17
brmi 3f // OSCCAL zu groß: Fehler!!
out OSCCAL,r17
3: reti
4: subi r27,2 // in den gültigen Bereich hinab
dec r17
cpi r17,112 // OSCCAL zu klein: Fehler!!
brcc 4f
out OSCCAL,r17
4: reti
// Rückflanke
9: sub r16,r10
sbc r17,r11 // Impulsdauer
// Vertikalsynchronimpuls detektieren
// Ohne VSync nur wenige µs.
// Bei identischen Halbbildern (keine Trabanten) fast so lang wie eine Zeile.
// Bei versetzten Halbbildern eine halbe Zeile lang.
// Bei fehlenden Horizontalimpulsen „elend“ lang — da wird das Ende erfasst.
breq 9f // < 32 µs
tst r25
breq 9f // noch weniger als 256 Halbzeilen geschrieben
ldi r16,hi8(500)
cpi r24,lo8(500) // Bei plausibler Zeilenzahl …
cpc r25,r16
brcs 1f
vretr1: movw r22,r24 // … Zeilenzahl merken
1: sbi PORTB,2 // Booster aktiveren
out OCR0A,r1 // High ausgeben
ldi r25,0
ldi r24,0 // Start von oben
out ADMUX,r1 // Bildhöhen-Poti lesen lassen
9: reti
main: clr r1 // SPL ist bereits initialisiert
ldi r22,lo8(MAXLINE)
ldi r23,hi8(MAXLINE)
outi OSCCAL,127 // auf > 16 MHz hochziehen (typ. laut Datenblatt)
outi MCUCR,0x20 // Sleep (Idle) aktivieren
outi PORTB,0x09 // Pullup am Sync-Eingang, Halbbrückenausgang 0V ohne Booster
outi DDRB, 0x26 // Ausgänge aktivieren
outi PCMSK,0x08 // Pegelwechsel-Interrupt
outi TCCR0B,0x01 // 62,5 kHz Trägerfrequenz
outi OCR0A,0xE0 // Timer-Behandlung „am Ende“
outi TIMSK0,0x08 // Interrupt alle 32 µs (Halb-Zeile)
outi ACSR,0x80 // Analogvergleicher totlegen
outi ADMUX,0x42 // Referenzspannung 1,1 V benutzen
// outi ADCSRB,0x04 // Trigger bei Timerüberlauf
outi ADCSRA,0xE6 // Synchrone A/D-Wandlung, Vorteiler 64: Zeit 52µs
outi DIDR0,0x37 // Analogeingänge ohne Digitalfunktion, Ausgänge nicht rücklesen
ldi r26,lo8(MAX_CTC)
ldi r27,hi8(MAX_CTC) // Feinabgleich-Register für Timer im CTC-Modus
ldi r16,lo8(1000)
ldi r17,hi8(1000)
movw r14,r16
1: ldi r24,0 // ISRs sollen nur synchronisieren, keinen Vertikalrückschlag auslösen
ldi r25,0 // und nicht OCR0B berechnen / verändern
sei
sleep // nur genau eine ISR kommt hier dran
cli
sub r16,r24 // line==1 im Falle dass Timer0-Überlauf aufgerufen wurde
sbc r17,r25
brne 1b // ergibt Wartezeit von 64 ms,
// das sollte den Koppelkondensator zur Vertikalablenkspule
// hinreichend entladen haben, und der Strom durch den Messshunt ist Null
in r2,ADCL // den entsprechenden A/D-Wert als Kalibrierwert einfangen
in r3,ADCH
outi OCR0B,0xFF // „unten“ anfangen: Halbbrückenausgang 0V
outi TCCR0A,0xE3 // Schnelle PWM, OC0A invers, OC0B nichtinvers
sbi PORTB,1 // Zeilentransistor ansteuern
sei
1: sleep // die Rattermaschine freilassen,
rjmp 1b // die ISRs verändern munter alle Register: egal!
Detected encoding: UTF-8 | 0
|