Der DSP wird hauptsächlich für die Echtzeitbearbeitung im Audiobereich eingesetzt (z.B. Filter, Fast-Fourier-Transformation ...). Dieser Beitrag soll zeigen, daß man den DSP auch für Aufgaben im Videobereich benutzen kann.
Der DSP soll so programmiert werden, daß er die Funktion eines Video-Controllers übernehmen kann. Ein (einfaches) Videosystem benötigt folgende Baugruppen:
Die wichtigste Kenngröße eines Videosystems ist der Pixel-Takt. Je höher dieser Takt ist, um so mehr Pixel lassen sich innerhalb einer Zeile darstellen. Es ist also sinnlos, einen Shifter softwaremäßig zu realisieren, da sonst die Auflösung zu gering wird. Tja, der DSP hat aber gar keinen Shifter? Hat er doch, nur unter einem anderen Namen: SSI (Synchronous Serial Interface)!
Das SSI wird für die schnelle serielle Kommunikation mit anderen Systemen benutzt (z.B. mit dem CODEC). Die maximale Übertragungsrate beträgt 4 Millionen Bits pro Sekunde. Da der DSP im Falcon mit 32 MHz getaktet wird, können bis zu 8 Millionen Bits pro Sekunde übertragen werden. Dies entspricht einem Pixel-Takt von 8 MHz. 8 MHz Pixel-Takt sind für ein Videosystem nicht besonders hoch, aber wenn man das normale TV-Zeitverhalten benutzt, sind 320 Pixel pro Zeile möglich (der ST arbeitet bei der niedrigen Auflösung auch mit 8 MHz Pixelclock).
Somit ergeben sich folgende Daten für den DSP-Screen:
Zeilenfrequenz: 15625 Hz
Bildfrequenz: 50 Hz
Auflösung: 320 x 200 Pixel
Farben: 2
Ein Bild mit 320 x 200 Pixeln braucht einen Bildspeicher von 8000 Bytes. Die Word-Länge des SSI wird auf 16 Bit programmiert. Da ein DSP-Word 3 Bytes hat, ergibt sich eine etwas ungewöhnliche Bildspeicherorganisation: Von den DSP-Words wird das oberste und mittlere Byte benutzt. Das unterste Byte wird nicht verwendet. Der Bildspeicher hat also eine Größe von 4000 DSP-Words.
Die Routinen für den Bildaufbau sollen im Interrupt ablaufen, damit das Hauptprogramm während der horizontalen und vertikalen Austastlücken weiter arbeiten kann. Zeitbasis für den Interrupt ist die Zeilenfrequenz. Dafür wird der Timer des SCI auf eine Frequenz von 15625 Hz programmiert und der Timerinterrupt eingeschaltet. In diesen Interrupt-Routinen werden die Synchronimpulse erzeugt und die Daten vom Bildspeicher in das SSI geladen.
... können leider nicht einfach auf eine als parallel-I/O definierte Leitung des Port C ausgegeben werden, sie wären dann nicht exakt mit den Videodaten synchronisiert, was zu einem unruhigen Bild führt. In den Synchron-Modes des SSI gibt es aber Flags, die so gepuffert sind, daß sie genau mit den TX/RX-Daten umschalten. Hier wird das Flag OFO verwendet. Somit stehen die Synchronimpulse an dem SCO-Anschluß des DSP-Connectors zur Verfügung.
Das SSI darf nur dann Daten ausgeben, wenn das Bild dargestellt werden soll. Sonst muß immer Low-Pegel ausgegeben werden (Dunkeltastung). Man darf aber nicht das TE (Transmit Enable)-Flag zurücksetzen. da sonst die Synchronimpulsausgabe über das OFO-Flag nicht mehr funktioniert. Deshalb wird zum Schluß einer Zeile immer 0 in das TX-Register geladen. Nachdem diese 0 ausgegeben worden ist, gibt es einen Transmitter-Underrun-Error, da keine neue Daten in das TX-Register gelangt sind. Das ist aber nicht weiter schlimm, das SSI gibt einfach die alten Daten (die 0!) erneut aus. Somit bleibt der Transmitter-Ausgang solange auf Low, bis neue Daten zur Verfügung stehen.
Es gibt 5 verschiedene Timer-Interrupt-Routinen, wobei eine nur einmal zur Initialisierung benötigt wird und die anderen vier immer wieder aufgerufen werden.
timer_init:
Diese Interrupt-Routine wird nur einmal zur Initialisierung des SSI ausgeführt. Wenn das SSI nicht während des Timer-Interrupts initialisiert wird, kann es evtl. zur einer falschen Synchronisation kommen.
timer 1:
Diese Routine erzeugt den vertikalen Synchronimpuls. Er sollte nach TV-Norm 2.5 mal Zeilendauer (64 ps) = 160 ps lang sein, aber mit 128 ps (2 Zeilen) gibt es keine Probleme, und das Programm wird einfacher. Das Register r6 wird mit der Anfangsadresse des Bildspeichers geladen.
timer2:
Diese Routine legt die Größe des oberen Borders fest. Es werden nur die Horizontalsynchronimpulse erzeugt. Dieser Synchronimpuls hat eine Dauer von 4 ps. Nach dem Einschalten wird einfach solange gewartet, bis das SSI zwei Words ausgegeben hat. Da ein Word in 2 ps (1 durch 8 MHz mal 16) ausgegeben wird, werden genau 4ps erreicht.
timer3:
Nach der Ausgabe des Horizontalimpulses werden nach einer Wartezeit Bilddaten ausgegeben. Als Zeiger auf den Bildspeicher wird das Register r6 benutzt. Für 320 Pixel werden 20 (320/16) Words gebraucht. Zum Schluß wird noch eine 0 ausgegeben, um die Dunkeltastung zu aktivieren.
timer4:
Erzeugt die Horizontalimpulse für den unteren Border.
Insgesamt ergibt sich folgender Ablauf:
Int.-Nr | Funktion | Anzahl der Zeilen |
---|---|---|
1 | Vertikal-Sync | 2 |
2 | Top Border | 55 |
3 | Bild darstellen | 200 |
4 | Low Border | 56 |
- | Summe | 313 |
Ein Bild besteht also aus 313 Zeilen, damit ergibt sich die Bildfrequenz von 15625 / 313 = ~50 Hz.
Da die Register des DSP nicht so allgemeingültig wie z.B. die 68000er Register sind, können logische und arithmetische Operationen nur mit den Akkumulatoren (A oder B) ausgeführt werden. Eine Interrupt-Routine soll möglichst wenig Register verwenden, um unnötiges Register retten zu vermeiden. Als Zeilenzähler wird deshalb das Register r7 verwendet. Um ein Adreßregister um 1 zu erhöhen, benutzt man den Befehl „move (rx)+“ (Address Register Update). Um die End wertabfrage zu vereinfachen, wird folgender Trick angewendet: das Register wird immer mit dem Wert 256-n geladen. Beim n-ten lnkrementieren wird dann das Bit 8 gesetzt, welches dann einfach über ein JSET #8,rx,adr ausgewertet werden kann.
„DSP_SCR.PRG“ lädt das DSP-Programm, ,DSP_SCR. A5 6“ und kopiert dann den 8x8-Zeichensatz in den DSP. Das Hauptprogramm ist eine einfache Bildschirmroutine. Folgende Sonderzeichen werden unterstützt:
$07: Bildschirm löschen
$0d: Zeilenschaltung; wenn die letzte Zeile erreicht ist, scrollt das Bild um 1 Zeile nach oben. Das Zeichen wird im oberen Byte des Hostinterfaces erwartet.
Um ein Zeichen auszugeben, muß folgendes Programmstück in Supervisormodus ausgeführt werden:
Zeichen ist in d0
not_empty:
btst #1,ffffa202.w ;Host empty ?
beq.s not_erapty ;nein
move.b d0,$ffffa205.w ;TXH, Zeichen ausgeben
clr.b $ffffa206.w ;TXM
clr.b $ffffa207.w ;TXL
Zur Demonstration habe ich ein Programm geschrieben, das die Trap#1-Aufrufe auf den DSP-Screen protokolliert (TRAPVIEW.PRG). Dieses Programm wird aus Platzgründen nicht abgedruckt, befindet sich aber auf der Monatsdiskette.
Jetzt fehlt nur noch die Hardware (siehe Bild). Man braucht ein spezielles Adapterkabel, was man sich leicht selbst anfertigen kann. Die Widerstände können natürlich bei TTL-Monitoren entfallen.
Die Bildschirmroutine ist nur eine Minimalversion. Es sind natürlich auch grafische Funktionen möglich. Auch die Auflösung läßt sich noch weiter erhöhen (z.B. 384 x 240). Man kann noch viele andere Funktionen einbauen, z.B. Hardwarescrolling, farbige Darstellung usw., da der DSP-Screen ein frei programmierbares Videosystem ist.
Literatur:
Motorola DSP56000/DSP56001 - Users-Manual
/* DSP-Screen-Hauptprogramm */
/* (c)1993 by MAXON-Computer */
/* Autor: Steffen Scharfe */
/* Dateiname: DSP_SCR.C */
#include <stdio.h>
#include <tos.h>
extern int load_dsp ( void );
extern void write_host( long d );
extern void CopyFont( void );
void out_str( char *s );
int main( void )
{
chax info[] = "DSP-Screen \ by Steffen Scharfe\x0d";
if ( ! load_dsp() ) { /* DSP-Programm laden */
printf("kann DSP-Programm nicht laden !" );
return( 1 );
}
CopyFont();
out_str( info );
return( 0 );
}
void out_str( char *s )
{
while( *s )
write_host( (long) *s++ << 16 );
}
/* DSP-Screen-Ladeprogramm */
/* (c)1993 by MAXON-Computer */
/* Autor: Steffen Scharfe */
/* Dateiname: DSP_LOAD.C */
#include <stdio.h>
#include <tos.h>
#include <ext.h>
#include <stdlib.h>
/* Prototypen */
void write_host( long d );
int load_dsp( void )
{
char buffer[3*512];
char mem;
long adr, dat;
int i;
FILE *fp;
fp = fopen( "LOADER.DSP", "r" );
if ( fp == NULL )
return{ 0 );
i = 0;
while ( fscanf( fp, "%c %1X %1X\n", &mem, &adr, &dat ) != EOF )
{
buffer[i++] = (char) ( dat >>16 );
buffer[i++] = (char) ( dat >> 8 );
buffer[i++] = (char) dat;
if ( i > ( 3 * 512 ) )
return( 0 );
}
fclose( fp );
Dsp_ExecBoot( buffer, i / 3, 1 );
write_host( 0 ); /* kein Debugger */
fp = fopen( "DSP_SCR.DSP", "r" );
if ( fp == NULL )
return( 0 );
while ( fscanf( fp, "%c %1X %1X\n", &mem, &adr, &dat ) != EOF )
{
adr |= (long) mem << 16;
write_host( adr );
write.host( dat );
}
write_host( 0xff0000L ); /* fertig */
fclose( fp );
return( 1 );
}
void write_host( long d )
{
char *host_status = (char *)0xffffa202L;
char *host_tx = (char *)0xffffa205L;
long old_super_stack;
old_super_stack = Super( 0L );
while ( ! ( *host_status & 0x02 ) );
*host_tx++ = (char) ( d >> 16 );
*host_tx++ = (char) ( d >> 8 );
*host_tx = (char) d;
Super((void *) old_super_stack );
}
; +++++++++++++++++++++++++++++++++++
; + DSP-Screen, DSP-Assembler-Teil +
; + (c)1993 by MAXON-Computer +
; + Autor: Steffen Scharfe +
; + Dateiname: DSP_SCR.A56 +
; +++++++++++++++++++++++++++++++++++
PBC equ $ffe0
CRA equ $ffee
CRB equ $ffed
PCC equ $ffe1
PCDDR equ $ffe3
PCD equ $ffe5
TX equ $ffef
SCR equ $fff0
SCCR equ $fff2
SSISR equ $ffee
BCR equ $fffe
IPR equ $ffff
HSR equ $ffe9
HRX equ $ffeb
HTX equ $ffeb
screen equ $1000
font equ $2000
org x:$0
cursor_x dc 0
cursor_y dc 0
org p:0
jmp $40
org p:$1c
t_vector jsr timer_init
org p:$40
movep #0,x:BCR ;no Waitstates
movep #$c000,x:IPR ;Interrupt-Prioritaet
;Host initialisieren
movep #1,x:PBC
;Init Timer fuer Zeilenfrequenz
movep #$2000,x:SCR ;Timer Int an
movep #$0020,x:SCCR ;15.625 kHz
andi #$fc,mr ;Int an
jsr get_font ;Font laden
jsr cls
loop jclr #0,x:HSR,* ;auf Daten warten
movep x:HRX,a ;laden
jsr out_char
jmp loop
; Interrupt-Routinen fuer den Bildaufbau
; werden alle 64 us aufgerufen
; verwendet folgende Register
; (duerfen von dem Hauptprogramm nicht benutzt
; werden!!)
; r6: Zeiger auf den Bildspeicher
; r7: Zeilenzaehler
; n7: Hilfregister
; Init SSI Transmit, 8 MHz Pixelclock
; um die SSI Ausgabe mit dem Timerinterrupt
; zu synchronisieren
timer_init movep #$4000,x:CRA ;16 Bit, 8 MHz
movep #$123c,x:CRB ;Frame Sync intern
movep #$178,x:PCC ;Write als SCI
movep #0,x:TX
jclr #6,x:SSISR,*
move #t_vector+1,r7
move #timer1,n7
movem n7,p:(r7)
move #256-2,r7
rti
; Vertikal-Sync, 2 Zeilen ( 128 us )
timer1 bclr #0,x:CRB ;V-Sync an
movep #0,x:TX
move (r7)+
jclr #8,r7,timer1_end
move #t_vector+1,r7
move #timer2,n7
movem n7,p:(r7)
move #256-55,r7
move #screen,r6 ;Adr des Video-RAM
timer1_end rti
; Top Border, 55 Zeilen
timer2 bclr #0,x:CRB ;H-Sync an
movep #0,x:TX ;4 us
jclr #6,x:SSISR,*
movep #0,x:TX
jclr #6,x:SSISR,*
bset #0,x:CRB ;H~Sync aus
movep #0,x:TX
move (r7)+
jclr #8,r7,timer2_end
move #t_vector+1,r7
move #timer3,n7
movem n7,p:(r7)
move #256-200,r7
timer2_end rti
; Screen, 200 Zeilen
timer3 bclr #0,x:CRB ;H-Sync an
movep #0,x:TX ;4 us
jclr #6,x:SSISR,*
movep #0,x:TX
jclr #6,x:SSISR,*
bset #0,x:CRB ;H-Sync aus
movep #0,x:TX
do #6,LeftBorder ;12 us warten
jclr #6,x:SSISR,*
movep #$000000,x:TX
LeftBorder
do #20,Disp
jclr #6,x:SSISR,*
movep x:(r6)+,x:TX ;40 us fuer 1 Pixel-Zeile
Disp
jclr #6,x:SSISR,*
movep #$000000,x:TX
move (r7)+
jclr #8,r7,timer3_end
move #t_vector+1,r7
move #timer4,n7
movem n7,p:(r7)
move #256-56,r7
timer3_end rti
; Low Border, 56 Zeilen
timer4 bclr #0,x:CRB ;H-Sync an
movep #0,x:TX ;4 us
jclr #6,x:SSISR,*
movep #0,x:TX
jclr #6,x:SSISR,*
bset #0,x:CRB ;H-Sync aus
movep #0,x:TX
move (r7)+
jclr #8,r7,timer4_end
move #t_vector+1,r7
move #timer1,n7
movem n7,p:(r7)
move #256-2,r7
timer4_end rti
;--- Bildschirm loeschen
cls move #0,x0
move #screen,r0
do #20*200,cls1
move x0,x:(r0)+
cls1
move x0,x:cursor_x
move x0,x:cursor_y
rts
;--- scrollt um 8 Pixel-Zeilen nach oben
scroll move #screen,r0 ;Ziel
move #screen+8*20,r1 ;Quelle
do #192*20,scroll1
move x:(r1)+,x0
move x0,x:(r0)+
scroll1 clr a
do #8*20,scroll2 ;letzte Zeile loeschen
move a,x:(r0)+
scroll2 rts
;--- 1 Zeile nach unten, wenn letzte
; Zeile erreicht: scrollen
cr clr a
move a,x:cursor_x ;Zeilenanfang
move x:cursor_y,a
move #>24,x0 ;letzte Zeile ?
cmp x0,a #>1,x0
jeq scroll ;ja
add x0,a ;naechste Zeile
move a,x:cursor_y
rts
;--- gibt ein Zeichen aus
;--- Zeichen muss im hoeherwertigsten
; Byte von A1 stehen
out_char move #$0d,x0
cmp x0,a #$07,x0 ;CR ?
jeq cr ;ja
cmp x0,a #$400,x0 ;CLS ?
jeq cls ;ja, Bild loeschen
move a1,x1
mpy x1,x0,a #>$0007f8,x0
; 13 Bits nach rechts verschieben
and x0,a #>font,x0
add x0,a
move a1,r0 ;r0 zeigt auf Font-Daten
move x:cursor_x,a
move a,r1
nop
move (r1)+ ;naechste x-Position
nop
move r1,x:cursor_x
asr a ;/2
move a1,x0
move x:cursor_y,a
asl a ;*2
move a,x1
asl a
asl a ;*8
add x1,a ;*10
asl a
asl a
asl a
asl a ;*160
move a,x1
move #>screen,a
add x0,a
add x1,a
move a,r1 ;r1 = Screenadr
move #>$ff0000,x0 ;Maske High-Byte
btst #0,x:cursor_x
jcs mask_ok ;cursor_x zeigt
; schon auf das naechste Zeichen
move #>$00ff00,x0 ;Maske Low-Byte
mask_ok move #20,n1 ;1 Zeile = 20 Words
do #8,out_char1
move x:(r0)+,a1 ;Font-Daten
and x0,a ;maskieren
move a1,x1
move x:(r1),a1 ;Bilddaten
or x1,a ;ein-odern
move a1,x:(r1)+n1 ;schreiben
out_char1
rts
;-- Font laden
get_font move #>font,r0
do #256*8,get_font1
jclr #0,x:HSR,* ;auf Daten warten
movep x:HRX,x:(r0)+ ;laden
get_font1 rts
; DSP-Screen Font-Lader
; (c)1993 by MAXON-Computer
; Autor: Steffen Scharfe
; Dateiname: FONT.S
host equ $ffffa200
export CopyFont
CopyFont: movem.l d3-d7/a2-a6,-(sp)
pea super
move.w #$26,-(sp)
trap #14
addq.l #6,sp
movem.l (sp)+,d3-d7/a2-a6
rts
super: aline #0
move.l 4(a1),a0 ;8*8 Font
move.l 76(a0),a0 ;Adr Zeichensatz
moveq #0,d0 ;Zeichen
lea host,a1
CopyFont1: lea (a0,d0.w),a2 ;Adresse Zeichen-Font
moveq #8-1,d2
CopyChar: btst #1,2(a1) ;Host empty ?
beq.s CopyChar ;nein
move.b (a2),5(a1) ;TXH
move.b (a2),6(a1) ;TXM
clr.b 7(a1) ;TXL
add.w #256,a2
dbra d2,CopyChar
addq.w #1,d0
cmp.w #256,d0
bne.s CopyFont1
rts
;Projekt-File
;Dateiname: DSP_SCR.PRJ
dsp_scr.prg
.C [ -Y ]
.L [ -L -Y ]
.S [ -Y ]
= ; list of modules follows ...
PCSTART.O ; startup code
dsp_load.o
font.o
dsp_scr.c
PCSTDLIB.LIB ; standard library
PCEXTLIB.LIB ; extended library
PCTOSLIB.LIB ; TOS library