Electronics » the Z80 project » soundcard..
I wasn't that statisfied with my PIC-based sound board - it had
quite a bit of noise in its output, was cumbersome to update the
sound channels, had untidy microcontroller code and was generally
inelegant in design. Still, as a first try it could've been worse.
 Soundboard Version 1 - Hmmmmm.. |
My original idea was to emulate the C64's SID chip (there's an
interesting interview with its creator Bob Yannes
here)
but the PIC microcontrollers I was using weren't fast enough.
The Ubicom SX28 with its 75MHz capability, on the other hand, would be..
Simple waveforms like sawtooth, triangle and variable
pulse could be generated using the phase accumulating oscillator
principle (desc below) and white noise would just need a random
number generator. In addition, sampled sound could be played
using the the SX's internal SRAM buffer with a Z80 interrupt generated
every half-buffer period to get wave data updates.
A few sums revealed that 4 channels would easily be possible
at a clock speed of 64MHz and these could be mixed digitally
in the microcontroller. This would save having multiple R2R
DACs and analogue mixing on the PCB. Also, as the SX has more
ports than the PICs I was originally using, it would be easier
to update the various sound parameters from the Z80 CPU.
One disadvantage in all this would be a drop in the
"purity" of the sound output - it would look (on a scope) like
a digitized recording of a SID chip rather than the SID itself.
Additionally the digital mixing would reduce the sound
resolution unless I had greater than 8 bit output (which I
briefly considered but decided it wasn't worth the additonal external logic).
In any case I guessed such issues wouldn't be a huge deal - SID
emulators on the Amiga always sounded OK and the Soundblaster code I
wrote which emulated the Amiga's sound hardware for Giddy3 was also
quite acceptible - even at 22KHz output rate. (I'm still not looking
for anything like HiFi quality here!)
The Microcontroller Code
I designed my SX sound code to run on the RTCC interrupts at a fixed
rate (I left the register update code running "in the background").
One channel's output is calculated every interrupt and all four are
mixed every forth to produce the final output for the DAC.
The maximum interrupt frequency was arrived at roughly by measuring
the longest path through the sound routine code and dividing by 4
(leaving some cycles over for the IRQ overhead and register update code
to do its thing when needed). The simple waveforms only took a dozen or so clock cycles each, but
they then needed to be scaled to control their volume. White noise
was slower with the processing of a 24 bit linear feedback register
(desc below) to provide the numbers. The sampled sound playing code took
the most SX cycles due to it setting/caching internal pointers
and creating the external signals for the Z80 IRQ side* The worst case
(including scaling) was about 150 cycles per SX IRQ - but this still meant
the SX at 64MHz could update its output at around 100KHz - more than
double CD rate (but only 8 bit of course).
(* In my design only one channel can play interrupt-based sampled sound at a time in
order to keep the interrupt signalling simple. With some more external
logic all four channels could theoretically play IRQ-based samples - their
buffer refill signals could set four flipflops and the OR-sum of those outputs
would go to the CPU's IRQ line. The CPU would then need to poll the flipflops
to see which channel buffer needs new data. I think double buffering the
channels in such a system would be pushing things a tad though, what
with the SX's limited SRAM).
As mentioned, the way I created the simple waveforms was to use a SID-like
phase accumulator. In my case this was just a 24 bit counter to which I
add a 16 bit value (directly related to the desired output frequency)
each sample period. I then take bits 12-20 as my 8 bit output..
Osc hi: Osc mid: Osc lo:
=============================
00000000 00000000 00000000 <- Accumulator
+
Freq hi: Freq Lo:
00000000 00000000 <- Frequency control
----------------------------
0000 0000
! !
'--------'
!
'---------------------> 8 bit output
The raw output from the above would be a sawtooth shape, ie:
rising and then suddenly dropping to the lowest point again.
To make a triangle wave, I test bit 7 of the output
and - if 1 - invert the other bits and shift it left.
For a variable pulse width wave, I compare the output
to an 8 bit pulse-width value held in a register, if it's
greater than this I set the output to the channel's volume
register and if lower, the output equals zero. This way, the pulse
waves dont need any seperate scaling.
For the white noise (and samples) I update the output if
there's any carry from the Osc_mid byte of the accumulator.
The linear feedback register used for the white noise is just 3 initially
random bytes being shifted to the right with the MSB being fed the result
of a XOR of various bits in the register.
>>>>>> Rotate Right >>>>>>
MSB LSB
00000000 00000000 00000000
! ! ! !
'----------<--------+--+-+ <- XOR'd bits
Volume scaling: There's no divide or multiply instructions
on the SX of course so my scaling routine uses the "shift
and add" method (the equivalent to long multiplication by
hand, in binary) to multiply the output by the volume to
create a 16bit value, the most significant byte of the
result is then the new output.
Mixing the four channels is as simple as adding the
four 8 bit outputs together and dividing by 4 (two right shifts).
As the mixing only takes place every fourth interrupt, it
occured to me that a digital filter could be implemented
by dividing the difference between one output value and
the next by 4 and using the result as a delta to modify
the output EVERY interrupt and help smooth out the aliasing.
The effect proved negligible though, I guess because
the output was already being updated above 100KHz (and with sampled
sound, the slope wasn't between one actual sample and the next,
just output updates). Anyway, the final value from the mixer
routine is presented to an output port on the SX microntroller
which is connected directly to a DIY digital to analogue
converter - a simple "R2R" resistor ladder:
The Sound board circuit:
 R2R resistor DAC diagram |
In a R2R resistor ladder, each output bit is connected to a series of
resistors in such a way that each adds its binary weight to the total voltage at the
end. From standard logic supply voltages, this gives a range of 0 to 5 volts in
256 steps (I used 10K and 20K resistors for my DAC - its not particularly
critical). TV audio spec requires a 1v peak-to-peak signal, but its easy
to scale it lower with a potential divider. I used a 50Kohm trimmer to set the
level and buffered the output with a 7611 op-amp wired as a voltage follower.
The output of the buffer is then just connected to the TV audio-in via a 1k resistor and 1uf capacitor.
Here's a(nother badly scribbled) schematic of the circuit.
My previous soundboard used a pretty slow method
of sending data to the sound registers. This time all
I do is latch (with 574 ICs) the Z80 databus
and the high 8 bits of the address bus when I write
to the sound port using the Z80 "OUT (c),a" instruction.
(During this instruction, the Z80's B register is
presented to the high 8 bits of the address bus and
the A register to the data bus.) Once these bytes are
latched the SX can just toggle the latch chips'
output enables to get either the register address (for the SX's FSR)
or the data to place into it (mov to INDF).
 Sound board version 2 - better! |
I also make any write to the sound port set a "busy" flag on
a 74 flipflop and get the SX to clear it when the new data has
been accepted. Any Z80 sound routine reads this flag through a
245 buffer before attempting to send any more data. Another
line from the SX to the 245 buffer allows the Z80 to tell
which half of the sample buffer is being used when playing
digitized sound. The remaining bus lines of the 245 were
pulled high via resistors and sent an (old) standard Atari 2600 joystick port:)
Further reading / demo tunes etc can be found here
|