Controlling the SB-300 with B/SOS Hardware

This past year, I’ve been working on changing the gameplay of early Bally/Stern SS pinball machines by plugging an Arduino into J5 of the MPU. Recently, I coded new rules for Stern Meteor. Because my hardware was not able to address the SB-300 board, all the audio on my Meteor rewrite was passed through to a Wav Trigger board. Over the past couple of days, I’ve been working with new hardware that can also talk to the SB-300.

The original board I developed to interface the Arduino Nano to the MPU was only able to talk to the PIA chips U10 & U11. The address pins controlled were A12 & A9 (always grounded), A7 (always high), A4 (CS for U11), A3 (CS for U10), and A1 & A0 (Register select). The next version also addressed A5 in order to talk to the SB-100.
The new version (BALLY_STERN_OS_HARDWARE_REV 2) controls address pins A3, A4, A5 & A6 using a demultiplexer to control those four pins with two. That pin-savings allows the Nano to control the address lines A0-A6, and gain access to the SB-300.

The schematics and Gerber files are available on GitHub.
Once I was able to easily address the SB-300, I was able to create a detailed picture of how to use it.

SB-300 Features:
The SB-300 has three 16-bit timers on a 6840. This timer chip is selected with A5 & A7 (A9 & A12 have to be low) at addresses $00A0 through $00A7.
Timer 1 is used for square wave generation. When used, the waveform is always full volume.
Timer 2 is used for square wave generation that is then volume-regulated by a counter on U12 (16 different levels of volume that control U2 & U3).
Timer 3 controls the counter of U12. Volume can be ramped up or ramped down and the speed of that change is triggered by the output of Timer 3.

The SB-300 also has what I call an Effects Control Byte that is latched with A6 & A7 (A9 & A12 have to be low) at address $00C0.
The Effects Control Byte can ramp volume of Timer 2 up/down, or add noise to the signal (also volume controlled). The frequencies of the noise have 16 settings (bright to dark).

Setting Up the Timers:
The three timers have different features & uses on the SB-300. To access their control bytes or latch countdown values, the following addresses are used.
$00A0 – Write Control Register 1 or 3 (depends on the last latched value of Control Register 2-bit0, aka CR20)
$00A1 – Write Control Register 2
$00A2 and $00A3 – Timer 1 ($00A2 is MSB)
$00A4 and $00A5 – Timer 2 ($00A4 is MSB)
$00A6 and $00A6 – Timer 3 ($00A6 is MSB)
The latch bytes are sequential so that they can easily be set with register X commands. For example,
LDX #M0200

The Control Register bits are mostly the same for each timer.
CRX7 = Timer X (1, 2, or 3) output enable
CRX6 = Timer X interrupt enable (interrupts are not used by Stern)
CRX5,4,3 = operating mode (example 010 is continuous, 100 is single-shot)
CRX2 = 0 is 16-bit, 1 is 8-bit
CRX1 = clock source (MPU clock is 1, on-board terrible clock is 0)
CRX0 = is different for each.
For Timer 1, CR10 is a master control for all timers (0 is on, 1 is hold).
For Timer 2, CR20 controls which CR will be written to the next time we write to $00A0
For Timer 3, CR30 turned on will prescale the timer to divide by 8.

Here’s a set of commands that will setup all three timers and play the “diagnostic” beep. This plays 7 times when the machine boots.

  1. Write 0x00 to $00A1 – this disables Timer 2 and tells the 6840 that the next Control Register we write to at $00A0 will be CR3 (because CR20 is zero).
  2. Write 0x92 to $00A0 – this enables Timer 3 (CR37 is on), makes Timer 3 continuous (CR35 through CR33 are 010), and tells Timer 3 to use the MPU’s clock from pin 27 of J5.
  3. Write 0x93 to $00A1 – this enables Timer 2 (same as Timer 3), but also points $00A0 to CR1 the next time it’s written.
  4. Write 0x00 to $00A0 – this disables Timer 1. It’s not used for the test ping.
  5. Write 0x8000 to $00A6:$00A7 to initialize Timer 3 with 0x8000. Timer 3 controls volume ramps through U12, so this means that the volume will change every time Timer 3 counts down from 0x8000 twice. Q3 (the output of Timer 3) is the CLK input of U12, and Q3 will toggle every time Timer 3 resets. Therefore it has to countdown twice to clock U12. This will happen every 75ms (2 * 32768 / 866k). U12 has 16 volume levels, so the whole ramp will take 16 * 75ms or 1.21 seconds.
  6. Write 0x0200 to $00A4:$00A5 to initialize Timer 2 with 0x0200. The frequency of this square wave is calculated the same way as above. Frequency = (866kHz/0x0200)/2 = 845 Hz (about a G#5).
  7. Write a 0x02 to $00C0 – this tells the Effects Control to reset the counter at U12 to full volume and count down from there to off. The volume changes with each full cycle of Timer 3.

With that setup of the three timers, every time a 0x02 is written to $00C0, U12 will be reset to ramp from full volume back to off. In this way, the first beep takes all 7 steps, but the following beeps only require a repeat of step 7.

In the Meteor code, the setup of the diagnostic beep happens at $5C53:

        CLRA                             *5C53: 4F             'O'  
        STAA    M00A1                    *5C54: 97 A1          '..' ; Turn off timer 2 (point to CR3)  
        LDAA    #$92                     *5C56: 86 92          '..'  
        STAA    M00A0                    *5C58: 97 A0          '..' ; CR3 = (timer 3 on, continuous, E clock, no prescale)  
        INCA                             *5C5A: 4C             'L'    
        STAA    M00A1                    *5C5B: 97 A1          '..' ; CR2 = (timer 2 on, continuous, E clock, point to CR1)  
        CLR     >M00A0                   *5C5D: 7F 00 A0       '...' ; Turn off timer 1  
        LDX     #M8000                   *5C60: CE 80 00       '...' ;   
        STX     M00A6                    *5C63: DF A6          '..'  ; Put 0x8000 in Timer 3  
        LDX     #M0200                   *5C65: CE 02 00       '...'  
        STX     M00A4                    *5C68: DF A4          '..'  ; Put 0x0200 in Timer 2  
        LDAA    #$02                     *5C6A: 86 02          '..'  
        STAA    M00C0                    *5C6C: 97 C0          '..' ; Play a single "beep" (ramp down volume) 

The subsequent beeps happen at $5CE7:

        LDAA    #$02                     *5CE5: 86 02          '..' ;   
        STAA    M00C0                    *5CE7: 97 C0          '..' ; Play a single "beep" (ramp down)  

Other uses of the Effects Control Byte:
As shown above, sending a 0x02 to $00C0 play a single downward volume ramp timed by Timer 3. There are 255 other values recognized.
0x00 = ramp down and then hold
0x01 = ramp up and stop
0x02 = ramp down and stop
0x03 = ramp up and hold
0x04 = ramp down and then repeat
0x05 = ramp up and then repeat
0x06 = ramp down and then ramp up & repeat
0x07 = ramp up and then ramp down & repeat

Bit 4 of the EC byte turns on noise:
0x08 = play noise – the noise plays at the same time as anything from Timer 2 and is also volume-controlled by the same mechanism described above. With Timer 3 configured, sending 0x0F will play a continuos swell & ebb of white noise.

The upper four bits of the EC byte control the “color” of the white noise.
0x08 is the brightest, and 0xA8 is the darkest white noise the card can make. Anything higher than A for the uppper nibble just comes out as clicks.

Meteor’s Drone:
The constant drone in the background of Meteor (phased tones that ramp up during game play) are created by setting Timer 1 and Timer 2 to slightly different values. For the background sounds, the volume of Timer 2 is not altered by Timer 3 through U12. When other sound effects are played, it sounds like Timer 1 continues the drone while Timer 2, Timer 3, and the white noise generator create the other effects.

The drone sounds are initialized to 0x3010 (Timer 1) and 0x3000 (Timer 2).
Here’s the code that initializes and then ramps up the drones:

        LDX     #M3010                   *50B7: CE 30 10       '.0.' ; Put 0x3010 into X
        STX     M02F7                    *50BA: FF 02 F7       '...' ; Store 0x3010 in M02F7 (Timer 1 drone value)
        LDX     #M0000                   *50BD: CE 00 00       '...' ; Put 0x0000 into X
        STX     M006D                    *50C0: DF 6D          '.m'  ; Store 0x0000 in M006D (Sound FX current value?)
Z50C2   LDX     #M3000                   *50C2: CE 30 00       '.0.' ; Put 0x3000 into X
        STX     M02F9                    *50C5: FF 02 F9       '...' ; Store 0x3000 in M02F9 (Timer 2 drone value)
        LDAA    #$1E                     *50C8: 86 1E          '..'  
        STAA    M005D                    *50CA: 97 5D          '.]'
Z50CC   LDX     M02F7                    *50CC: FE 02 F7       '...' ; Load X with Timer 1 drone value
        STX     M00A2                    *50CF: DF A2          '..'  ; Set Timer 1 register to drone value
        LDX     M02F9                    *50D1: FE 02 F9       '...' ; Load X with Timer 2 drone value
        STX     M00A4                    *50D4: DF A4          '..'  ; Set Timer 2 register to drone value
        LDX     #M0100                   *50D6: CE 01 00       '...' ; Put CR bytes in X
        STX     M00A0                    *50D9: DF A0          '..'  ; Turn off Timer 1 & 2, point to 3
        INX                              *50DB: 08             '.'   ; Set X to #M0101
        STX     M00A0                    *50DC: DF A0          '..'  ; Turn off Timer 2 & 3, point to 1
        LDX     #M9293                   *50DE: CE 92 93       '...' ; Set CR1=0x92 and CR2=0x93
        STX     M00A0                    *50E1: DF A0          '..'  ; 92 = (timer 1 on, continuous, E clock, unpause), 93 = (timer 2 on, continuous, E clock, point to CR1)
        CLRA                             *50E3: 4F             'O'   ; Set A=0 (will put Timer 2 at full volume)  
        STAA    M00C0                    *50E4: 97 C0          '..'  ; ramp down volume & then hold at full volume  
        STAA    M004C                    *50E6: 97 4C          '.L'  ; Store current FX byte a M004C  

The code around *50FB increments the drone values in M02F7-8 and M02F9-A and rolls them if necessary.

Turning off the drone could be possible by rewriting *50DE to LDX #M1213 (and then adjusting the padding for checksum). However, the code that plays the other sound effects might leave Timer 2 on, so I’m not 100% confident that this is the only place it will need to be changed.

Leave a Reply