Extrazeit: Harddiskrecording mit Unterbrechung

Seit dem Erscheinen des ATARI Falcon030 ist schon viel über Harddiskrecording geschrieben worden. Es war neben der Beschreibung des Soundsubsystems eines der ersten Themen, das von den Autoren aufgegriffen worden ist. Natürlich liegt es auch daran, daß es der Aufbau des Falcon ermöglicht, mit nur wenigen Programmzeilen einen kleinen Harddiskrecorder zu programmieren. Es gibt allerdings Fälle, bei denen man mit der herkömmlichen Programmierweise schnell an Grenzen stößt. Um das zu verdeutlichen, werfen wir doch noch einmal einen Blick auf das System des „Halfbuffers“.

Um ein Lied auf Festplatte aufzunehmen, legt man sich zuerst einen genügend großen Buffer-Speicher an. Nun startet man die Aufnahme-DMA im „Endlos“-Modus und fragt in einer Schleife ständig per XBIOS die aktuelle Schreibposition ab. Überschreitet die DMA gerade die Hälfte des Buffers, speichert man die erste Hälfte ab; erreicht die Schreibposition das Buffer-Ende, sichert man die zweite Hälfte des Buffers auf Festplatte.

Mit diesen wenigen Sätzen ist das Prinzip der Aufnahme auf Festplatte schon beschrieben. Die Wiedergabe funktioniert natürlich genauso. Da die Daten per DMA transportiert werden, muß sich die CPU praktisch um nichts kümmern. Doch stellt man bei genauem Hinsehen fest, daß der Rechner die meiste Zeit mit Festplattenoperationen beschäftigt ist. Je nach Buffer-Größe und Plattengeschwindigkeit stecken bist zu 90% der Prozessorzeit im Fread- bzw. Fwrite-Befehl. Während dieser Zeit ist in TOS 4.0x das eigene Programm und sind unter MiNT (V1.0x) alle anderen Prozesse blockiert! Dieser Umstand kann schnell zum Problem werden. Stellen wir uns einfach einmal vor, wir möchten während des Abspielens per Tastatur Marken in dem Sample setzen, um es später an diesen Stellen zu „zerschneiden“.

Der Interrupt hilft

Wenn man sich die Dokumentation zum Falcon anschaut, stellt man fest, daß das Betriebssystem in der Lage ist, am Ende des DMA-Buffers einen Interrupt auszulösen. Das allein hilft natürlich noch nicht weiter, denn eine Interrupt-Routine, die nur ein Flag setzt (so wie ATARI es vorschlägt), verkompliziert nur das obige System. Schließlich fragt das Hauptprogramm nun dieses Flag anstatt der Schreib-/Leseposition ab. Man hat also im Prinzip noch gar nichts gewonnen. Außerdem hat sich die Zeit für Festplattenoperationen nicht verkürzt.

Mit einem kleinen Trick kommt man allerdings doch ans Ziel. Normalerweise haben die Buffer für Harddiskrecording eine Größenordnung von einigen 100 KBs. Der Ansatz ist nun, den Buffer in kleine Einheiten (Patterns) von ca. 1 bis 2 KB einzuteilen. Die Start- und Endadresse der Sound-DMA setzen wir auf das erste Pattem und weisen das Betriebssystem an, am Ende des Patterns einen Interrupt auszulösen. Die Interrupt-Routine hat dann folgende Aufgaben:

Auf diese Art und Weise kann man jederzeit eine Marke im Raster der Pattern-Größe setzen, denn der Interrupt wird ja auch während des Abspeicherns und Ladens von Daten ausgelöst! In der Wahl der Pattern-Größe hat man recht große Freiheit, nur zu klein sollte man sie nicht wählen, denn schließlich braucht auch die Interrupt-Auslösung und -Bearbeitung eine gewisse Zeit.

Ein Frage drängt sich geradezu auf: Wenn man genau am Ende des Patterns per Interrupt eine neue Startadresse für die DMA festlegt, kommt es doch zu kurzen, hörbaren Aussetzern in der Wiedergabe! Dieser Einwand ist richtig, und trotzdem funktioniert obiges System. Dies hat folgenden Grund: Der erste Interrupt wird nicht am Ende, sondern zu Beginn des ersten Patterns ausgelöst. D.h., während wir das erste Pattern hören, setzt der Interrupt die Adressen für das zweite Pattern. Diese neuen Positionen übernimmt die Sound-DMA aber erst am Schluß des Frames.

Ob ATARI diesen Umstand beim Design des Falcon geplant hat, möchte ich einmal dahingestellt lassen. Wichtig ist nur. daß die Methode problemlos funktioniert.

Probleme im Detail

Wie bei den meisten einfachen Problemlösungen steckt die Tücke im Detail. Das beginnt mit der ATARI-Dokumentation, die vom MFP-Interrupt „i7“ spricht, was aber nicht „MFP-Interrupt Nr.7“ bedeutet, sondern „MFP-Interrupt, der über Portbit 7 ausgelöst wird“! Wenn man das erkannt hat, sagt einem ein Blick in das Profibuch, daß es sich um den „MFP-Interrupt Nr. 15“ handelt! Die Routine SetUpInt im Beispiel-Listing (Listing 2) zeigt, wie die Initialisierung des Interrupts in Assembler aussieht.

Das nächste Problem ist, daß es zumindest unter MultiTOS nicht möglich ist, aus einem Interrupt heraus einen XBIOS-Aufruf zu starten. Nun muß aber unsere Interrupt-Routine die DMA-Start- und -Endadressen lesen und verändern, und außerdem soll sie auch noch die SHIFT-Taste abfragen. Die Betriebs-Systemaufrufe buffoper, buffptr und Kbshift sind also tabu. Wie kann man diese Funktionen ersetzen, ohne die Lauffähigkeit des Programms auf dem zukünftigen „Vogelvieh“ von ATARI zu verlieren?

Beginnen wir mit Kbshift, denn dieses Problem ist relativ einfach zu lösen. Es existiert eine Systemvariable, die genau die gewünschte Information enthält. Deren Adresse ist auch noch rechner- und betriebssystemunabhängig im ROM-Header eingetragen. Folgende Konstruktion in Pure C löst also dieses Problem:

SYSHDR **sys; 
long stack; 
long *kbshift; 
stack=Super(0L); 
sys=0x4f2;
kbshift=(*sys)->kbshift
Super(stack);

Im Interrupt:

shift=kbshift&0x3000000;

Das Problem mit den DMA-Adressen ist etwas kniffliger, denn ATARI hat (zurecht) in den normalen Entwicklerunterlagen keine Adressen von Customchips dokumentiert. Da alte Programme, die eigentlich für den DMA-Sound der STE/ TT-Computer programmiert wurden, auch auf dem Falcon laufen, liegt die Annahme nah, daß sich die Registeradressen nicht verändert haben. Ein Blick mit dem Disassembler in den XBIOS-Bereich des ROM bestätigt dies. Da ATARI wohl auch in Zukunft abwärtskompatibel sein will und sich in den STE-/TT-TOS-Versionen keinerlei DMA-Sound-Unterstützung findet, kann man davon ausgehen, daß sich an den Adressen auch in Zukunft nichts ändern wird.

So entstanden die zwei Routinen my_getpos und my_setbuffer, wobei letztere praktisch identisch mit der original TOS-Routine ist. Beide Routinen müssen im Supervisor-Mode aufgerufen werden, was aber kein Problem ist, denn wir wollen sie ja innerhalb eines Interrupts benutzen.

Das Listing

Listing 1 ist ein kleines Beispiel für die obigen Ausführungen. Das compilierte Programm spielt ein per Fileselector ausgewähltes 16-Bit-Stereo-Sample mit der Pattern-Methode vor. Während des Abspielens kann man mit der SHIFT-Taste Marken setzen, welche am Schluß ausgegeben werden. Das Programm wurde in Pure C geschrieben und benutzt nur Standardbibliotheken. Es besteht aus dem C-Quellcode (Listing 1) mit dem Hauptprogramm und dem kleinen Assembler-Quellcode (Listing 2) mit den Interrupt-Routinen. Um ein lauffähiges Programm zu erstellen, brauchen Sie noch die passende Projektdatei, welche in Listing 3 zu sehen ist.

Ausblick

Natürlich ist die hier vorgestellte Lösung nur eine Anwendungsmöglichkeit von vielen. Um Marken zu setzen, könnte man auch einen anderen, asynchronen Interrupt verwenden. Eine weitere interessante Anwendung ist die Synchronisation von Audio- und Videodaten, was für einen Multimediacomputer sehr wichtig ist. Man berechnet die Pattern-Größe (abhängig von der Sample-Frequenz) so, daß der Interrupt z.B. 12mal pro Sekunde ausgelöst wird. Der Interrupt ist dafür zuständig, daß das nächste Bild auf dem Bildschirm dargestellt wird. Bild und Ton laufen nun absolut synchron. Natürlich bleibt hierbei noch das Problem, wie man die enormen Datenmengen bewältigt - doch das ist ein anderes Thema.


/* Harddiskrecording mit Unterbrechung */
/* Listing 1 */
/* (c)1994 by MAXON-Computer */
/* Autor: Andreas Binner */

#include <tos.h>
#include <stdio.h>
#include <stdlib.h>
#include <ext.h>

/* Buffer- und Patterngröße festlegen */

#define PATTERN 2048L 
#define BUFFER PATTERN*50L 
#define HALFBUF BUFFER/2L

/* Prototypes */

long file_len(int h); 
void play(char *name);

/* Globale Variablen */

/* Zähler und Flags für Interrupt */ 
long *kbshift; 
long l;
long dmapos,loadpos,aktpos,endaddr,begaddr; 
int load_flag,int_off, shift;

/* Buffer */ 
char *sndbuf;

/* Platz für 64 Marken */ 
long marken[64]; 
int mz;

/* Externe Assemblerfunktionen */ 
extern long IntOff(); 
extern void Inton(); 
extern char IntFlag;

/* Hauptprogramm (als TTP ausgelegt) */

int main(int argc, const char *argv[])
{
    SYSHDR **sys; 
    long stack; 
    int curadder,curadc; 
    int i;

/* Adresse für 'kbshift' holen */ 
    stack=Super(0L); 
    sys=0x4f2;
    kbshift=(*sys)->kbshift;
    Super(stack);

/* Sound-Subsystem sperren und initialisieren*/ 
    locksnd(); 
    settracks(0,0); 
    setmode(1); 
    settracks(0,0);

    curadder=(int)soundcmd(ADDERIN,INQUIRE); 
    soundcmd(ADDERIN,2);

    curadc=(int)soundcmd(ADCINPUT,INQUIRE); 
    soundcmd( ADCINPUT, 0 );

    dsptristate(ENABLE,ENABLE);

/* Speicher für Buffer anfordern */ 
    sndbuf=(char *)Malloc(BUFFER);

/* Falls genug frei war -> Sample Vorspielen */ 
    if (sndbuf)
        play(argv[1]);

/* Spicher freigeben */
    Mfree(sndbuf);

/* Marken anzeigen */ 
    for (i=0;i<mz;i++)
        printf("#%d nach %1d sec\r\n",i,marken[i]/ACT_CLK33K);

/* Sound-Subsystem zurücksetzen und freigeben */ 
    dsptristate(TRISTATE,TRISTATE);
    soundcmd(ADDERIN,curadder); 
    soundcmd(ADCINPUT,curadc); 
    unlocksnd();

/* ... und raus */ 
    return(0);
}

/* Routine zum Abspielen eines Samples */

void play(char *name)
{
    int h;

/* Datei öffnen (Rohsample ohne Header!)*/ 
    h=Fopen(name,0); 
    if (h<0)
        return;

/* Buffer komplett füllen */
    Fread(h,BUFFER,sndbuf);

/* DMA initialisieren */
    devconnect(DMAPLAY,DAC,CLK_25M,CLK33K,NO_SHAKE); 
    setbuffer(0,sndbuf,&sndbuf[PATTERN]);

/* Globale Variablen setzen */ 
    begaddr=0;
    endaddr=file_len(h);
    l=PATTERN;

/* Interrupt einschalten */ 
    int_off=0;
    Inton(1);
    buffoper(PLAY_ENABLE|PLAY_REPEAT); 
    delay(10);

    mz=0; 
    shift=0; 
    load_flag=0; 
    do 
    {
/* Auf nächsten Interrupt warten */ 
        do{}
        while (!IntFlag);

        IntFlag=0;

/* Nachladen ?*/
        if (load_flag)
        {
            if (load_flag==1)
                Fread(h,HALFBUF,sndbuf); 
            else
                Fread(h,HALFBUF,&sndbuf[HALFBUF]); 
            load_flag=0;
        }
    } while (!int_off);

/* Datei schließen, DMA und Interrupt aus */
    Fclose(h); 
    setinterrupt(1,0);
    Supexec(IntOff); 
    buffoper(0);
}

/* Dateilänge feststellen */

long file_len(int h)
{
    long len;

    len=Fseek(0,h,2);
    Fseek(0,h,0); 
    return(len);
}

/* Interruptroutine */

void sound_end(void)
{
    long apos; 
    long tcode;

/* Neuer Pattern */
    my_setbuffer(0,Ssndbuf[1],&sndbuf[1+PATTERN]);

/* Aktuelle Position berechnen */ 
    apos=(dmapos%PATTERN)+loadpos;

/* Ende erreicht ?*/
    if (apos+PATTERN>=endaddr)
    {
        setinterrupt(1,0);
        int_off=1;
        return;
    }

/* Shift-Taste abfragen */ 
    tcode=*kbshift; 
    if (tcode&0x3000000)
    {
        if (!shift)
        {
            shift=1; 
            if (mz<64)
                marken[mz++]=apos/4L;
        }
    }
    else
        shift=0;

/* Load-Flag setzen */
    dmapos=my_getpos()-(long)sndbuf; 
    l+=PATTERN;
    loadpos+=PATTERN; 
    if (l==HALFBUF+PATTERN) 
        load_flag=1; 
    if (l==BUFFER)
        l = 0L; 
    if (l==PATTERN) 
        load_flag=2;
}

/* Harddiskrecording mit Unterbrechung */ 
/* Listing 2 */
/* (c)1994 by MAXON-Computer */
/* Autor: Andreas Binner */

    GLOBL IntOn 
    GLOBL IntOff 
    GLOBL IntFlag 
    GLOBL setinterrupt 
    GLOBL Supexec 
    GLOBL sound_end 
    GLOBL my_setbuffer 
    GLOBL my_getpos

/* MFP Register */

imra    EQU $fffa13
iera    EQU $fffa07
ipra    EQU $fffa0b
isra    EQU $fffa0f
aer     EQU $fffa03
mfpints EQU $100

/* Variablen */

IntFlag:    ds.b 1
function:   ds.w 1

    ALIGN

/* Interrupt aus */

IntOff:

    bclr    #7,iera 
    bclr    #7,imra 
    bclr    #7,ipra 
    bclr    #7,isra 
    rts

/* Interrupt einschalten */

IntOn:
    move    d0,function 
    move.l  #SetInt,a0 
    jsr     Supexec 
    rts

SetInt:
    clr.b   IntFlag 
    bsr     IntOff 
    move.l          #(15*4),d0
    add.l           #mfpints,d0
    move.l  d0,a0
    move.l  #my_int,(a0)
    tst.w   function
    beq     .record
    move.w  #1,d0
    move.w  #1,d1
    jsr     setinterrupt

    bra     .cont
.record:
    move.w  #1,d0 
    move.w  #2,d1 
    jsr     setinterrupt
.cont:
    bset    #7,aer 
    bclr    #7,ipra 
    bclr    #7,isra 
    bset    #7,imra 
    bset    #7,iera

    clr.l   d0 
    rts

/* Interrupt Routine */

my_int:
    st      IntFlag

/* Register sichern und C-Funktion aufrufen */ 
    movem.l a0-a6/d0-d7,-(sp) 
    jsr     sound_end 
    movem.l (sp)+,a0-a6/d0-d7

/* Nächsten Interrupt ermöglichen */ 
    bclr    #7,isra 
    bset    #7,imra 
    rte

/* DMA Position ermitteln */
my_getpos: 
    clr.l   d0 
    clr.l   d1
    move.w  $ff8908,d0 
    rol.l   #8,d0 
    rol.l   #8,d0 
    move.w  $ff890a,d1 
    rol.l   #8,d1 
    add.l   d1,d0 
    move.w  $ff890c,d1 
    add.l   d1,d0 
    rts

/* Neue DMA Position setzen */

my_setbuffer: 
    tst.w   d0 
    beq     .1
    bset.b  #$0007,$ff8901 
    bra     .2
.1:
    bclr.b  #$0007,$ff8901
.2:
    move.l  a0,d0 
    move.w  d0,$ff8906 
    ror.l   #8,d0 
    move.w  d0,$ff8904 
    ror.l   #8,d0 
    move.w  d0,$ff89O2

    move.l  a1,d0 
    move.w  d0,$ff8912 
    ror.l   #8,d0 
    move.w  d0,$ff8910 
    ror.l   #8,d0 
    move.w  d0,$ff890E 
    rts

; Harddiskrecording mit Unterbrechung 
; Projekt-Datei

*.TTP           ; Beispielprogramm als TTP
.C [ -Y ]
.L [ -L -Y ]
.S [ -Y ]

PCSTART.O
SND_INT.S ; Interruptroutinen

*

PCSTDLIB.LIB 
PCEXTLIB.LIB 
PCTOSLIB.LIB

Andreas Binner
Aus: ST-Computer 09 / 1994, Seite 93

Links

Copyright-Bestimmungen: siehe Über diese Seite