This is 05_Dig_Ears.rtf in view mode; [Download] [Up]
Written by J. Laroche at the Center for Music Experiment at UCSD, San Diego California. December 1990.
Reading samples from the Digital Ears, and sending them to the DACs: Real time sound processing.
In this chapter, we'll see how to set-up a stream from the DSP to the DACs, receive samples from the digital ears and send them to the DACs in real time. In particular four important points are addressed:
· How to set-up the driver to receive samples from the DSP and send them to the DACs
· How to receive samples from the digital ears into the DSP.
· How to implement DMA
· How to send data to the DSP while DMA is in progress.
The C program is given below. It can also be found in Example/03_Dig_Ears/perso_b.c
// ------------------------------ Beginning of program
#import <sound/sound.h>
#import <sound/sounddriver.h>
#import <stdio.h>
#define Error(A,B) if((A)) {fprintf(stderr,"%s %s\n",B, SNDSoundError((A)));\
mach_error(B,(A)); }
#define DMASIZE 512
main (int argc, char *argv[])
{
static port_t dev_port, owner_port, cmd_port, read_port;
int i, protocol;
kern_return_t k_err;
SNDSoundStruct *dspStruct;
int low_water = 48*1024;
int high_water = 64*1024;
// Acquire ownership of SoundOut and DSP...
k_err = SNDAcquire(SND_ACCESS_OUT|SND_ACCESS_DSP,0,0,0,
NULL_NEGOTIATION_FUN,0,&dev_port,&owner_port);
Error(k_err,"SNDOUT and DSP acquisition");
// Do NOT ramp-up sounds, since this induces an extra delay in the
// DSP -> Dacs stream (ramp computation time...)
k_err = snddriver_set_ramp(dev_port,0);
// These two functions, not documented, modify the number and size of the
// sound out buffers in the driver. Make them short to lower response time
// for more info, see /usr/include/sound/snddriver_client.h
k_err = snddriver_set_sndout_bufsize(dev_port,owner_port,512);
k_err = snddriver_set_sndout_bufcount(dev_port,owner_port,4);
// Retrieve the DSP's command port...
k_err = snddriver_get_dsp_cmd_port(dev_port,owner_port,&cmd_port);
// Initialize the driver's protocol, and set-up a stream from DSP to DACS.
protocol = SNDDRIVER_DSP_PROTO_RAW;
k_err = snddriver_stream_setup(dev_port, owner_port,
SNDDRIVER_STREAM_DSP_TO_SNDOUT_44,
DMASIZE, 2,
low_water, high_water,
&protocol, &read_port);
Error(k_err,"Stream");
// Then set-up the driver with the returned modified protocol...
k_err = snddriver_dsp_protocol(dev_port, owner_port, protocol);
Error(k_err,"Protocol");
// Read the DSP code file...
k_err = SNDReadDSPfile("perso_b.lod", &dspStruct, NULL);
Error(k_err,"DSP file");
// ... and boot the dsp
k_err = SNDBootDSP(dev_port, owner_port, dspStruct);
Error(k_err,"DSP BOOT");
// Then loop while the dsp is sending the samples, passing it a volume value
while(1)
{
printf("Volume? Max = 2^^23-1 = 8388607 Min = 0\n");
scanf("%d",&i);
// To pass a value to the DSP, write it first....
k_err = snddriver_dsp_write(cmd_port,&i,1,sizeof(int),
SNDDRIVER_LOW_PRIORITY);
// Then issue a host command to tell the DSP to go fetch the value...
k_err = snddriver_dsp_host_cmd(cmd_port,20,
SNDDRIVER_LOW_PRIORITY);
}
}
// -------------------------- End of program
INCLUDE, DEFINE and VARIABLES
The include files are the same as usual except that since we will not use messages, we do not need mach.h
The DMA size is now 512.
There is a new port called cmd_port, which will contain the DSP command port.
There is also a new sound structure, dspStruct, which will contain the DSP code.
SETTING-UP the STREAM, BOOTING the DSP
As usual, the first thing we need to do is to acquire the sound out device. This time, we also need the DSP device, therefore, we pass SND_ACCESS_OUT | SND_ACCESS_DSP to the SNDAcquire() function.
SNDAcquire(SND_ACCESS_OUT|SND_ACCESS_DSP,0,0,0,
NULL_NEGOTIATION_FUN,0,&dev_port,&owner_port);
This call is followed by some additional (optional) initializations, we'll come back to them later.
We will need the DSP command port to pass host commands to the DSP (See below).
snddriver_get_dsp_cmd_port(dev_port,owner_port,&cmd_port);
To set-up a stream from the DSP to the DACs (and for any stream involving the DSP), the driver needs to know about the nature of the stream(s) to perform the proper initializations. Depending on the stream(s) set-up, the driver will allocate a number of buffers in the memory and react differently to the messages coming from the DSP. For this purpose, it uses an integer, the protocol, which is initialized and whose address is passed to each call to the snddriver_stream_setup() function. Each call modifies the protocol, and when the streams are all set-up, we'll pass the protocol to the driver for it to perform the proper initializations.
snddriver_stream_setup(dev_port, owner_port,
SNDDRIVER_STREAM_DSP_TO_SNDOUT_44,
DMASIZE, 2,
low_water, high_water,
&protocol, &read_port);
SNDDRIVER_STREAM_DSP_TO_SNDOUT_44 indicates that the stream goes from the DSP directely to the DACs with a sampling rate of 44100 Hz. It implies DMA (see below.) The size of each sample is two. The protocol is passed to the driver using the function:
snddriver_dsp_protocol(dev_port, owner_port, protocol);
The next thing to do is to read the DSP code, store it in a sound structure, and use it to boot the DSP.
SNDReadDSPfile("perso_b.lod", &dspStruct, NULL);
SNDBootDSP(dev_port, owner_port, dspStruct);
The first call SNDReadDSPfile() puts the DSP code found in the file "perso_b.lod" into the sound structure dspStruct. Sound structures are not only used to store sounds, but also to store program code for the DSP (see manual.) The .lod file has been obtained by assembling an DSP assembly file (see below).
The second call SNDBootDSP() boots the DSP with the code in dspStruct. This means that the DSP program memory is loaded with the DSP code in the sound structure, and the DSP is sent a "reset" host command, causing it to jump to the address 0 (see below.)
At that point, the DSP starts receiving samples from the SSI port (from the digital ears) and passing them through the driver to the DACs.
The rest of the C code is used to control the volume of the play-back, and will be discussed below.
SENDING THE DATA FROM THE DSP TO THE HOST: DMA
At 44100 hz stereo, the DSP needs to pass about 176000 bytes to the driver per second of sound. This is a fairly fast rate of transmission, in which it's best not to involve the host processor (the host processor is already busy with all sorts of things!) This is the reason why the driver implements a protocol referred to as Direct Memory Access (DMA.) The idea is that when the DSP is ready to send a certain amount of data, it will signal the host processor which will do the necessary initializations and let an auxiliary chip perform the actual sample reading and writing. This auxiliary chip is the DMA controller. It has a direct access to the main memory, to the DACs, and to the microphone. Therefore, it can write all the data to the memory without involving the host processor, which makes everything much faster. The precise protocol is described below. It makes use of the bit HF1 in the host status register (HSR):
· The DSP accumulates samples in a buffer in its private memory.
· When the DSP is ready to send a DMA buffer, it sends a DMA request to the host (a simple int with a predefined value), then waits for HF1 to go high.
· The host recognizes the DMA request, performs some initializations, then sets HF1.
· The DSP sends the data one by one, until the DMA buffer is empty. Then it continues sending junk data until the host signals it it has understood that DMA is completed. To do so, the host sends a "DMA done" host command to the DSP (see below), and resets HF1.
· The DSP can now accumulate more samples until it's ready to send another DMA buffer.
See the DSP program below for more precisions about the actual implementation of DMA.
GOING for REAL REAL-TIME!
The driver implements a number of internal buffers in the memory to store the samples received from the DSP before sending them to the DACs. It needs these buffer to be able to handle other processes at the same time it's sending the data to the DACs or receiving them from the DSP. If these buffers are too long, the time between the moment when the DSP received the samples from the SSI port and the moment when they're played by the DACs can be quite significant, and a delay is heard. To make the response as close as possible to real-time (the delay as short as possible), you need to make the buffers shorter. This is possible only in the 2.0 Version, and involves the two functions:
snddriver_set_sndout_bufsize(dev_port,owner_port,512);
snddriver_set_sndout_bufcount(dev_port,owner_port,4);
The first function sets the size of the buffers (8KBytes by default) to 512 bytes. The second controls the number of allocated buffers (4 by default, so this call does nothing.) A buffer size of 512 is a minimum below which the driver cannot work reliably. It gives a pretty small time-lag (almost inaudible) so that output sound and input sound appear simultaneous. Don't mess around with these functions, since they can wedge the machine.
When the driver starts playing a sound, it checks that the two first samples are zero (to avoid audible clicks in both right and left channels). If they are non-zero, the driver computes a ramp to circumvent the problem. This can be bad since it induces more delay in the stream DSP->DACs, with two results:
- A more important time lag between sound-in and sound-out
- The fourth and the following DMA buffers from the DSP are put on hold for a little time (ramp computation time), which means that the DSP accumulates samples (about 3000 samples at 44100 Hz stereo) from the SSI port without being able to transmit them, which can create problems in the DSP program. To keep the driver from computing that ramp, you can use (only in release 2.0)
snddriver_set_ramp(dev_port,0);
THE DSP PROGRAM.
The example below illustrates how to receive samples from the digital ears and how to implement DMA to the host. It can also be found in Example/03_Dig_Ears/perso_b.asm
;; --------------------------- beginning DSP program
include "ioequ.asm"
IW_Buff equ 8192 ;Start address of input buffer
Buff_size equ 4095
DMA_SIZE equ 512
DM_R_REQ equ $050001 ;message -> host to request dma
VEC_R_DONE equ $0024 ;host command indicating dma complete
DMA_DONE equ 0 ; indicates that dma is complete
;;;------------------------- Variable locations
;;;
x_sFlags equ $00fd ; dspstream flags
Stop_Flag equ $000 ; Stop DMA flag
volume equ $001 ; Volume
cra_init equ $4100 ;
crb_init equ $0a00 ;
pcc equ $1e0 ;
pcddr equ $01c ;
pcd_init equ $0010 ;
org p:$0
jmp reset
org p:VEC_R_DONE
bset #DMA_DONE,x:x_sFlags
nop
org p:$C ;SSI data received interrupt
movep x:m_rx,y:(R0)+
nop
org p:$E ;SSI data received exception interrupt
jsr except
nop
org p:40 ;Host Command interrupt
jsr getVolume
nop
org p:60 ;Host Command interrupt
jsr stop
nop
org p:62 ;Host Command interrupt
jsr go
nop
org p:100
reset
movec #6,omr ;data rom enabled, mode 2
bset #0,x:m_pbc ;host port
movep #>$000000,x:m_bcr ;no wait states on the external sram
movep #>$00BC00,x:m_ipr ;intr levels: SSI=2, SCI=1, HOST=2
bset #m_hcie,x:m_hcr ;host command interrupts
move #0,sr ;enable interrupts
;; Configure SSI port
movep #cra_init,x:m_cra
movep #crb_init,x:m_crb
movep #pcd_init,x:m_pcd
movep #pcddr,x:m_pcddr
movep #pcc,x:m_pcc
;; Configure variables
clr a
move a,x:x_sFlags ;clear flags
move #>IW_Buff,R0
move #>IW_Buff,R7
move #>Buff_size,M0
move #>Buff_size,M7
jmp main
main
move #>.9,a
move a,x:volume
bclr #0,x:Stop_Flag
bset #m_srie,x:m_crb
bset #m_sre,x:m_crb ;SSI Receive enable
_main_loop
jset #0,x:Stop_Flag,_main_loop
jclr #m_htde,x:m_hsr,_main_loop
movep #DM_R_REQ,x:m_htx ; send "DSP_dm_R_REQ" to host
_ackBegin
jclr #m_hf1,x:m_hsr,_ackBegin ; wait for HF1 to go high
move #>DMA_SIZE,b
do b,_prodDMA
_send
move R0,a
move R7,b
cmp a,b
jeq _send
move y:(R7)+,Y0
move x:volume,X0
mpyr Y0,X0,a #>$8000,X0 ; Multiply by volume, gets the rescaling factor
move a,Y0
mpyr X0,Y0,a ; Rescales the sample (shifts one byte right)
_wait
jclr #m_htde,x:m_hsr,_wait
move a,x:m_htx
_prodDMA
btst #DMA_DONE,x:x_sFlags
jcs _endDMA
jclr #m_htde,x:m_hsr,_prodDMA
movep #0,x:m_htx ;send zeros until noticed
jmp _prodDMA
_endDMA
bclr #DMA_DONE,x:x_sFlags ;reset the flag!
_ackEnd
jset #m_hf1,x:m_hsr,_ackEnd ;wait for HF1 to go low (optional!)
jmp _main_loop
except
movep x:m_sr,a
movep x:m_rx,a ; Must read the SSI status register,
rti ; then RX, in order to reset ROE.
stop
bset #0,x:Stop_Flag
rti
go
bclr #0,x:Stop_Flag
rti
getVolume
movep x:m_hrx,x:volume
rti
;; ---------------------- End DSP program
To compile this DSP program, you would type:
asm56000 -a -b -os,so -l myProgram.asm
Initializations
The first instructions in the DSP program are include and equates. We include Motorola's standard equates file ioequ.asm (this enables you to call registers by their name instead of by their address, m_hrx instead of $FFEB for example.) Then we define a number of useful constants (DMA_SIZE, Buff_size etc...) and reserve addresses for a number of variables, x_sFlags, Stop_Flag, volume. These addresses will contain respectively a flag indicating the current state of DMA, a flag used to stop sending data if neccessary, and the value of the volume.
Next are defined initialization constants used to set-up the DSP so that it can receive SSI data from the digital ears, etc... These initializations were adapted from NeXT's, in /usr/lib/dsp/smsrc/jsrlib.asm.
Next, the interrupt vectors are written: At the address zero is the interrupt called by a hardware reset. When the host has finished loading the DSP code, it issues a host command 0 causing the DSP to jump to the address 0. From there, it jumps to the routine called "reset". A number of other interrupts vectors are defined, that are discussed below.
The actual program starts at the address 100. The reset routine initializes the DSP's various registers so that it can receive host commands, etc... and initializes other useful values (pointers, volume etc...)
The DSP then jumps to the main program, performing some additional initializations, and starting receiving SSI samples:
bset #m_srie,x:m_crb
bset #m_sre,x:m_crb
The first instruction enables "data receive SSI" interrupts when samples are received on the SSI port, and the second turns the SSI port on. From that moment on, the SSI port start receiving samples and generating "data receive SSI" interrupts.
The DSP stores incoming samples in a buffer (IW_Buff) pointed to by the write pointer, R0. Simultaneously, it reads the buffer using the read pointer, R7. The DSP spends its time comparing R0 and R7. If R0 > R7, there are samples in the buffer that haven't been sent yet, and the DSP reads them and sends them to the host, using DMA, incrementing R7 until R7 = R0. R0 is incremented each time a sample is received on the SSI port.
The write pointer (R0) and read pointer (R7) are initialized to the starting address of the input buffer. Their corresponding modifier registers (M0 and M7) are set to the length of the buffer -1. This indicates that R0 and R7 should be treated as modulo M0 integers. In other words, R0 points to a ring buffer whose size is M0+1. See "address modifier types" p. 5-13 in Motorola's DSP user's manual.
RECEIVING SAMPLES ON THE SSI PORT, Interrupt vectors.
The Digital Ears (or any other Analog-Digital converter) can be plugged into the DSP via the SSI port (Serial Synchronous Port.) The incoming samples are received on that port, using interrupt vectors.
The DSP reserves the lower end of its program memory for interrupt handling. When the DSP is running a program, it can be interrupted by a number of sources: host, SSI port, CSI port etc... When the DSP is interrupted, depending on the source of the interruption, it jumps to one of the interrupt vectors addresses and executes the two words at that address and the next one. Then, if none of the addresses contain a jump instruction, it goes back to what it was doing when it got interrupted.
For example, if you set-up the DSP so that it can receive samples on the SSI port and generate corresponding interrupts, each time a sample is received on the SSI port, the DSP will get a "SSI receive data" interrupt, causing it to jump to the corresponding address $0C ($ means hexadecimal number.) See "exception processing" p. 8-1 in Motorola's DSP user's manual. If you put a specific code at the address $0C, then this code will be exectuted each time a sample is received on the SSI port.
To write at a precise location in the DSP memory, you use the org command. In our program, we put the movep instruction at the address $C:
org p:$C ;SSI data received interrupt
movep x:m_rx,y:(R0)+ ; x:m_rx is the register where the sample received from the SSI port is store.
nop
org p:$C tells the assembler to start writing the following instructions at the address $C in the program memory (see Motorola's DSP development software document, p 4-3.) Org is not a DSP instruction. It's an assembler directive.
In our example, each time a sample is received on the SSI port, the DSP jumps to the address $C, stores the received sample to the location pointed to by R0 and increments R0. Then it returns to its previous task.
The movep instruction is two words long, and the following nop instruction is not necessary. It is a good idea though, always to make sure your interrupt vectors are two words long (either one two word instruction, or two one word instruction) since the DSP will execute two instruction words at the interrupt address and interrupt address + 1.
The host can generate interrupts called host commands. The DSP reserves about 15 interrupts vectors that can be used for host commands (for example to tell the DSP to fetch a data from the host.) We'll get back to it later.
The DMA Loop
The DSP sends the host a DMA request #DM_R_REQ according to the DMA protocol, making sure it can write on the host/DSP register HRX. It waits untill HF1 goes high in HSR, then starts the DMA loop, first comparing R0 and R7.
_send
move R0,a
move R7,b
cmp a,b
jeq _send
move y:(R7)+,Y0
move x:volume,X0
mpyr Y0,X0,a #>$8000,X0
move a,Y0
mpyr X0,Y0,a
If R0 > R7, we can send samples to the host: The DSP reads the imput buffer, and multiplies the sample by the value in volume. Then it rescales the sample to make it fit in the host format:
When 2 byte samples are received from the digital ears, they are placed in the higher 2 bytes of the 3 bytes long DSP SSI register, x:m_rx (The DSP represents numbers by three byte words, and makes calculations using 6 or 7 byte registers.) When the host reads the 3 byte HRX register, it will assume the data is right justified: if it expects short ints (2 byte words) it will only read the lower two bytes of the HRX register. Therefore, if the SSI port received a sample equal to $F345 (2 bytes long), the DSP memory will store it as $F34500 (3 bytes long) and if it is passed to the host as is, the host will get the value $4500 (the 2 right bytes.) Therefore, we need to shift the bits one byte right, which is done by multiplying the value by 2^^-8 ($8000 in DSP representation.) Hence the second mpyr instruction.
The data is passed to the host, and the DSP jumps back to the beginning of the DMA loop.
When the DSP has sent enough data (DMA_SIZE), it continues sending zeros, wayting for the bit #DMA_DONE in x:x_sFlags to go high (which means the host has signaled DMA is done.) When the host understands DMA is done, it issues a host command "DMA done" causing the DSP to jump to the address VEC_R_DONE, and setting the bit #DMA_DONE in x:x_sFlags. This is how the host tells the DSP it understood DMA is done. The host also resets HF1 in HRS. The DSP is now ready to start another DMA buffer and jumps back to _main_loop.
Host Commands: controlling the volume, stopping DMA...
As we mentionned, the DSP reserves about 15 interrupt vectors for host commands. The host can generate an interrupt to any interrupt vector using the snddriver function snddriver_dsp_host_cmd(). The DSP jumps to the corresponding interrupt address to execute the next two instructions, as we saw previously. To control the volume of the play-back, we need to send its value to the DSP. To do that, we can send it a host command telling it to go wait for a value to arrive, then send the value using snddriver_dsp_write(), or we can do the contrary: send the value first and then send a host command to tell the DSP to go fetch it (this way the DSP doesn't have to check a value is present.) The C code is
snddriver_dsp_write(cmd_port,&i,1,sizeof(int),
SNDDRIVER_LOW_PRIORITY);
snddriver_dsp_host_cmd(cmd_port,20,
SNDDRIVER_LOW_PRIORITY);
The first command sends an int to the DSP, the value of the volume (actually, only the three lower bytes are sent, therefore, the values should lie in the range $FFFFFF and 0.) The second command sends a host command #20. When the DSP gets it, it jumps to the address 40 (twice the number of the host command). The address 40 on the DSP program memory contains a jsr instruction.
org p:40 ;Host Command interrupt
jsr getVolume
The DSP jumps to the address 40 executes the jsr instruction, jumping to the subroutine getVolume
getVolume
movep x:m_hrx,x:volume
rti
The value in HRX is stored in the volume, and the DSP returns to its previous task. Note that interrupt-called subroutines must terminate with a rti instruction, instead of a rts instruction for normal subroutines. We could have condensed the interrupt into
org p:40 ;Host Command interrupt
movep x:m_hrx,x:volume
That would do the job. However, note that you can only put two one-word instructions or one two-word instruction in an interrupt vector. If your interruption needs more instructions, you'll need a jsr to a subroutine terminated by rti.
Similarly, the subroutines stop and go are called by host commands #30 and #31. They set or reset the bit #0 in the stop_flag, causing DMA to stop or resume, thanks to the following instruction placed just before the DMA request.
_main_loop
jset #0,x:Stop_Flag,_main_loop
These two host commands can be used to cleanly stop DMA (that is, after flushing a complete DMA buffer), for example if you need to switch to another DSP program (see PitFalls.)
Always remember that a host command #n will trigger an interrupt at the address 2n.
Host commands are not the only way to send data to the DSP! Actually, the scheme proposed in 07_DMA_To_DSP should be preferred since it's more general and gives more control over the parameter's update. It sends the values to the DSP which receives them asynchronously using host data receive interrupts, and stores them in a private queue. At a certain point in the DSP program, the queue is examined and the parameters updated. See 07_DMA_To_DSP for details on the actual implementation.
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.