Fun with chips #2: SN76489 sound generator IC

If there’s one sound that makes me nostalgic, it’s the brrrrr-BIP! noise of a BBC Micro being switched on. And that sound – as well as pretty much all the Beeb’s audio capabilities – can be traced to one chip – the Texas Instruments SN76489.

This chip was used in a whole host of devices, including Sega consoles and arcade games machines as well as home computers from Sord, Memotech and others.

Luckily, these chips are pretty easy to find on eBay. I couldn’t make out a date code on the one I used for this experimentation, but I assume it’s old stock of some kind.

And it’s also easy to use.

The ins and outs

The pinout is pretty simple. There’s a pin for audio output which you need to feed to an amplifier. Most schematics suggest doing this via a capacitor which acts as a noise filter. I used a variety of capacitor values and couldn’t detect that they had much of an effect.

The datasheet suggests using a capacitor between the audio output pin and an amplifier, and another between the amp and a speaker. Your mileage may vary.

The main input consists of eight data pins (D0-D7) – essentially a byte-wide input bus. You write commands and data to the chip via these pins one byte at a time. In its infinite wisdom, TI decided that D0 should be the most significant bit and D7 the least. Whatever…

There’s an active-low /OE (output enable, aka chip enable). I just tied this low, but you could put this under programmatic control if you want to.

The active-low /WE (write enable) pin is an input. Whatever you’re using to control this chip – an Arduino in my case – pulses this low once data is set up on the eight data pins, and this tells the SN76489 that a byte of data is ready to be read.

You also need a clock input. In a computer like the BBC Micro this would be the main clock signal. For messing around with the chip, I used a 4MHz oscillator can. This matches the clock speed of the Beeb. On the version of this chip I have – the SN76489AN – this clock signal is divided by 16 inside the chip to create the internal clock. The maximum input clock speed is 4MHz. An ‘N’ version of the chip exists that divides internally by 2, has a maximum input clock frequency of 500kHz but requires only four clock pulses to load data compared to the AN’s 32.

The chip has only one output pin other than the audio. The READY pin goes low while it’s doing its stuff and then is set high again when the chip is able to accept more data. Theoretically, you could use this pin for flow control, but more often than not it was ignored.

Trying it out

My test setup for playing with this chip was pretty simple. I used an Arduino Nano with eight GPIOs (D2-D9) connected to the data pins on the SN76489. D9 on the Arduino was connected to D0 on the sound chip because of the whole D0 being most significant thing.

I also connected D10 on the Arduino to the write enable (/WE) pin of the SN76489.

For a clock signal, I used a 4MHz oscillator.

The audio output pin of the SN76489 goes via a capacitor to the input of a tiny amplifier board that I had lying around. I imagine pretty much any amplifier might do for the purposes of playing around. Similarly, the speaker was a random device that was in my parts bin.

Driving the data

You could drive the eight data lines in a number of ways. On an 8-bit micro, for example, you might be tempted to feed these pins straight from the data bus. Alternatively, you could use a shift register.

On the BBC Micro (or, at least, the BBC Master I have) the lines are driven by port A of one of the 6522 Versatile Interface Adapter (VIA) chips. This port is also used for the keyboard and the MC146818 real-time clock chip. The /WE and /CE pins are commoned together and controlled via a 74LS259 addressable latch IC which itself is managed by four of the lines from port B of the same VIA chip. (The same four lines select other stuff, too.)

Writing to registers

Using the chip involves the familiar process of writing values to internal registers. The 74LS259 offers four channels – three (channels 0-2) make tones and the other (channel 3) is a noise generator.

Each of the four channels has a register that sets the tone/noise and another that sets the volume, making eight registers in all.

With the three tone channels, the tone register is 10 bits in size. This is obviously more than a byte, so we have to do some careful splitting up of this value to spread it across two bytes. We’ll get to that soon.

With the noise channel (3) the register is three bits in size.

The volume registers for all four channels are four bits, giving values in the range 0-15. Actually, ‘volume’ is a slightly misleading term as the chip defaults to maximum volume and the value you feed it is an attenuation value, so that 0 is maximum volume and 15 is silent.

Writing to the registers

The basic process with this chip is that you set up a byte on the eight data pins, then briefly pulse the /WE pin low to tell the chip that the byte is ready.

The byte you set up on the data pins is one of two types – a latch byte or a data byte. (To be pedantic, the latch byte also carries some data, but let’s keep things simple.)

You use the latch byte to tell the chip that you want to write to a specific register (volume or tone/noise) for a specific channel. Once you’ve ‘latched in’ the register, all subsequent data bytes will be applied to that register until you send another latch byte.

For example, if you send a latch byte that specifies the volume register for channel 2, all subsequent data bytes will affect the volume of that channel.

So, how does the chip know if we’re sending it a latch byte or a data byte? That’s easy – the most significant bit (bit 7) tells it. If that bit is a 1, it’s a latch byte. If it’s zero, it’s a data byte.

The meaning of the rest of the bits also depends on whether it’s a latch byte or a data byte.

Latch byte

Structure of a latch byte

In the case of a latch byte, bits 5 & 6 are used to present a two-bit value (ie, decimal values 0-3) signifying which channel we wish to latch.

Bit 4 is used to tell the chip whether we wish to talk to the volume register (1) or tone register (0) for that channel.

The remaining four bits (0-3) are data and are put into the lowest four bits of the relevant register. When you talk to a volume register (which is 4-bit), this is all the data you need. But we’ll talk in a minute about what happens if you subsequently send another data byte.

If you’re talking to the noise register (ie, the tone/noise register of channel 3), four bits is too many, as it’s a three-bit register. What happens is that the highest bit (bit 3) is simply ignored.

But what about the 10-bit tone registers for channels 0-2? The four lowest bits of the latch byte become the four lowest bits of the tone register, and this happens straight away. But that allows you to set values only in the range 0-15. If you want to set higher values – and you will – you need to follow the latch byte with a data byte.

Data byte

Structure of a data byte

As I mentioned before, a data byte starts with the most significant bit (bit 7) set to 0. Bit 6 is ignored. The remaining 6 bits are transferred to whichever register is currently latched.

  • If it’s the noise register on channel 3, bits 3-5 are discarded and bits 0-2 set the register value.
  • If it’s a volume register, bits 4 & 5 are discarded, and bits 0-3 are used to set the volume register.
  • If it’s a tone register, all six bits are used, but they are shifted left four bits, becoming bits 4-9 of the register.

Setting the tone

Let’s say you want to write the register value 0b1001110001 to the tone register of channel 2.

First you would send the value 0b11001001. Let’s break down that binary number to see what’s happening, starting from the most significant bit on the left:

1      denotes this is a latch byte
10     specifies channel 2
0      specifies a tone/noise register, not a volume register
1001   is transferred to the lowest four bits of the tone register.

At this point, you might expect the tone register to contain the value 0b0000001001, but that’s a trap for young players. In fact, the upper six bits will contain whatever was last written to that register. But’s let’s assume, for the sake of argument, that those bits are, indeed, all zeroes.

Now we send the data byte 0b00100111. Again, let’s break that down:

0        denotes this is a data byte
0        this bit it ignored, but you have to set it to something, right?
100111   six bits to place into the register's bits 4-9.

The register will now contain the desired 0b1001110001.

Out loud

What about setting the volume?

We need to start by latching the volume register for the relevant channel. Let’s go with channel 3 this time and we’ll set it to volume level 0b0111 (7). We can send 0b11110111. Once again, let’s break this down:

1      denotes a latch byte
11     indicates we watch to latch channel 3
1      says we want the volume latch
0111   is the volume we want to set

There’s no need for a subsequent data byte as the latch byte contains all the data the chip needs (the same goes for setting the noise register on channel 3).

What if we were to sent another data byte straight after the previous one? Let’s say we used 0b00101001. Here’s what would happen to the bits (left to right again):

0      denotes a data byte, so the chip knows it's getting more data
0      ignored again
10     we'll be writing to a 4-bit register, so these bits are also ignored
1001   these bits are transferred to the volume register, setting volume level 9.

You can therefore keeping changing the volume as much as you like without having to send another latch byte. Similarly, if you set the noise latch for channel 3, you can keep altering the nature of the noise using just data bytes.

With tones, it’s different. After latching in a tone register, subsequent data bytes will change only the top 6 bits – if you want to change the lowest 4 bits, you’ll need to send another latch byte.

What’s the frequency?

What do you get out? Let’s look at tones first. The 10-bit value to set defines the ‘half-period’ of the resulting tone. That doesn’t help much, does it? Fortunately, the datasheet provides a handy formula for determining the output frequency of the tone, which I’ve rewitten as:

f = Clk / ( 32 x reg)

Here, f is the tone frequency in Hertz, Clk is the frequency of your clock signal and reg is the 10-bit value you put in the register. Given that you are likely to want to know the register value for a given frequency (eg, musical note), we can re-arrange this as:

reg = Clk / (32 x f)

Making noise

The noise register on channel 3 is somewhat more complex, even though it uses only three bits. I’ll be honest and admit I don’t fully understand this, and the information here might be useful for people comfortable with terms such as ‘linear feedback shift register’.  But essentially, this is used to create white noise or various buzzes, burps and rumbles. The noise generator was heavily exploited for games (think, firing lasers or stuff blowing up).

Having fun

Part of the fun with playing with chips is simply trying stuff out and seeing what happens. That’s what I did with the noise channel.

The code below is a simple Arduino sketch that got me up and running. (It’s not worth creating a GitHub repo for this.) I’m not claiming this is optimised code – far from it. It’s just what I hacked together to help me understand the chip.

You’ll note that the main loop is empty. All this sketch does is make a beep. Actually, it makes two noises. When the SN76489 is first powered up, it outputs a buzzy noise that BBC Micro owners will recognise as the first part of its two-tone startup sound. With a 4MHz clock, the output generated here vaguely resembles that of the awakening Beeb.

[UPDATE 21/04/2021]: There were errors in the original listing I mentioned above – probably as a result of cutting & pasting. And I couldn’t find a copy of the original program. So here’s another that plays Daisy Bell. Most of you will know why I chose that. It seems to have the code for the Beeb start-up beep still in it.

/*
 * SN76489_Daisy
 * 
 * Test program for the SN76489 tone generator chip, using an Arduino Nano.
 * 
 * This page helped with frequencies: https://www.intmath.com/trigonometric-graphs/music.php
 * 
 * This one provided the music - Daisy Bell (Bicycle Built for Two): https://www.bethsnotesplus.com/2014/04/daisy-bell-bicycle-built-for-two.html
 * 
 * This is still pretty crude - playing a tune on just one channel. But it's a starting point for experimentation.
 * 
 */

#define WRITE_ENABLE 10               // pin for /WE
// #define READY 11                   // pin to read Ready line from chip - not currently using
#define CLK 4000000.0                 // 4MHz clock
#define CROTCHET 200                  // millisecond length of quarter note
#define MINIM CROTCHET * 2
#define MINIM_DOT MINIM + CROTCHET

// 'a4' needs to be a multiple of 12 (here it's 0, which works) because of the calculations
// done in noteFreq(). We'll define two octaves here.
enum notes {c4 = -9, c4s, d4, d4s, e4, f4, f4s, g4, g4s, a4, a4s, b4, c5, c5s, d5, d5s, e5, f5, f5s, g5, g5s, a5, a5s, b5};
const char* note_names[] = {"C4", "C4#", "D4", "D4#", "E4", "F4", "F4#", "G4", "G4#", "A4", "A4#", "B4", "C5", "C5#", "D5", "D5#", "E5", "F5", "F5#", "G5", "G5#", "A5", "A5#", "B5"};

enum latchType {TONE, VOLUME};
uint8_t volume = 12;

int dataPins[] = {9, 8, 7, 6, 5, 4, 3, 2};  // note reverse order

uint8_t createLatchByte (latchType type) {
  byte lByte = 0b10000000;    // setting bit 7 indicates it's a latch byte
  // Bit 4 is 0 by default, which would indicate this is a tone command.
  if(type == VOLUME) {
    lByte |= (1 << 4);        // set bit 4 to indicate volume
  }
  return lByte;
}

void muteAllChannels() {
  for(uint8_t chan = 0; chan < 4; chan++) {
    setVolume(chan, 0);
  }
}

float noteFreq(int8_t noteIdx) {
  float freq = 0;
  // noteIdx should be in range -21 to 27
  if(noteIdx > -22 && noteIdx < 28) {
     freq = 440.0 * pow(2, (float)noteIdx/12.0);
  }
  return freq;
}

void playNote(uint8_t channel, int8_t note, uint16_t dur) {
    float freq = noteFreq(note);
    char buf[24];
    sprintf(buf, "%-3s   idx: %2i   freq: ", note_names[note + 9], note);
    Serial.print(buf); Serial.println(freq);  // AVR libs can't handle floats in sprintf
    setTone(channel, (int)(CLK / (32.0 * freq)));
    delay(dur); // this is blocking, so we can only play one note at a time
}

void playPause(uint8_t channel, uint16_t dur) {
  setVolume(channel, 0);
  delay(dur);
  setVolume(channel, volume);
}

void setData(uint8_t &lByte, uint8_t &dByte, uint16_t dataVal) {
  // This function writes a 10-bit value split across two bytes. The bytes should be passed into this
  // function by reference.
  // The lowest four bits go into the latchByte, which has to be masked to protect the latch, channel
  // and tone/vol bits.
  lByte &= (lByte & 0b11110000);                      // ensure bottom four bits are cleared, others preserved
  lByte |= (dataVal & 0b0000000000001111);            // add just bottom four bits of data value
  // The other six bits go into the lower six bits of the data byte. As these bits start life as bits 4 and
  // upwards, we need to shift them right. We also AND with 0b00111111 to ensure that only the bottom six
  // bits of the data byte are set. In a data byte, bit 6 isn't used and bit 7 must be 0 to indicate
  // it's a data byte, not a latch byte.
  dByte = (uint8_t)((dataVal >> 4) & 0b00111111);   // shift bits in data & just use bottom six
}

void setChannel(uint8_t chan, byte &latchByte) {
  // The following could be done in one line, but I've split it for commenting/clarity.
  latchByte = latchByte & 0b10011111;   // clear channel data
  latchByte |= (chan << 5);             // set channel bits
}

void setTone(uint8_t channel, uint16_t toneVal) {
  // The channel should be in the range 0-2 (channel 3 is for noise). If it isn't,
  // this function does nothing.
  // Tone registers are 10-bit, which is why we need a latch byte and a data byte
  // and use setData to spread the 10-bit value across the two bytes.
  if(channel < 3) {
    byte latchByte = createLatchByte(TONE);
    byte dataByte = 0b00000000;
    setChannel(channel, latchByte);
    setData(latchByte, dataByte, toneVal);
    writeByte(latchByte);
    writeByte(dataByte);
  }
}

void setVolume(uint8_t channel, uint8_t vol) {
  byte latchByte = createLatchByte(VOLUME);
  setChannel(channel, latchByte);
  // need to check vol doesn't exceed 15
  if(vol > 15) vol = 15;
  // the value that needs to be sent to the chip is an attenuation value - ie,
  // an amount by which the volume should be reduced from maximum. So let's
  // turn this around to have a volume scale of 0 (silent) to 15 (max).
  vol = 15 - vol;
  latchByte |= (0b00001111 & vol);
  writeByte(latchByte);
}

void writeByte(byte val) {
  // Output a byte to the SN76489 chip.
  for(uint8_t pin = 0; pin < 8; pin++) {
    digitalWrite(dataPins[pin], (val & (1 << pin)) >> pin);
  }
  digitalWrite(WRITE_ENABLE, LOW);  // pulse WRITE_ENABLE low to signal to SN76489 that
  delay(1);                         // data is ready, then...
  digitalWrite(WRITE_ENABLE, HIGH); // take it high again
}


/****************************************************************************
 * SETUP                                                                    *
 ****************************************************************************/
void setup() {
  for(uint8_t pin = 0; pin < 8; pin++) {  
    pinMode(dataPins[pin], OUTPUT);       // setup data pins as outputs and
    digitalWrite(dataPins[pin], LOW);     // set all low to start with
  }
  pinMode(WRITE_ENABLE, OUTPUT);          // WRITE_ENABLE is active low, so
  digitalWrite(WRITE_ENABLE, HIGH);       // set HIGH to disable at start
//  pinMode(READY, INPUT);

  Serial.begin(115200);
  Serial.println();

//  delay(750);             // delay to enjoy default noise
//  setVolume(1, 15);
//  setTone(1, 0x90);       // vaguely BBC-like start-up beep
//  delay(200);
  muteAllChannels();        // set all 4 channels to silent

  delay(1000);
  setTone(1, 0);
  setVolume(1,volume);
 
  playNote(1, c5, MINIM_DOT);          // Dai-
  playNote(1, a4, MINIM_DOT);          // -sy
  playNote(1, f4, MINIM_DOT);          // Dai-
  playNote(1, c4, MINIM_DOT);          // -sy
  playNote(1, d4, CROTCHET);           // Give
  playNote(1, e4, CROTCHET);           // me
  playNote(1, f4, CROTCHET);           // your
  playNote(1, d4, MINIM);              // an-
  playNote(1, f4, CROTCHET);           // -swer
  playNote(1, c4, MINIM + MINIM_DOT);  // do...

  playPause(1, CROTCHET);

  playNote(1, d4, MINIM_DOT);          // I'm
  playNote(1, b4, MINIM_DOT);          // half
  playNote(1, a4, MINIM_DOT);          // cra-
  playNote(1, f4, MINIM_DOT);          // -zy
  playNote(1, d4, CROTCHET);           // All
  playNote(1, e4, CROTCHET);           // for
  playNote(1, f4, CROTCHET);           // the
  playNote(1, g4, MINIM);              // love
  playNote(1, a4, CROTCHET);           // of
  playNote(1, g4, MINIM + MINIM);      // you...

  playPause(1, CROTCHET);

  playNote(1, a4, CROTCHET);           // It
  playNote(1, b4, CROTCHET);           // won't
  playNote(1, a4, CROTCHET);           // be
  playNote(1, g4, CROTCHET);           // a
  playNote(1, c5, MINIM);              // sty- 
  playNote(1, a4, CROTCHET);           // -lish
  playNote(1, g4, CROTCHET);           // mar-
  playNote(1, f4, MINIM + MINIM);      // -riage...

  playNote(1, g4, CROTCHET);           // I
  playNote(1, a4, MINIM);              // Can't
  playNote(1, f4, CROTCHET);           // af-
  playNote(1, d4, MINIM);              // -ford
  playNote(1, f4, CROTCHET);           // a
  playNote(1, d4, CROTCHET);           // car-
  playNote(1, c4, MINIM + MINIM);      // -riage...

  playNote(1, c4, CROTCHET);           // But
  playNote(1, f4, MINIM);              // you'll
  playNote(1, a4, CROTCHET);           // look
  playNote(1, g4, CROTCHET);           // sweet

  playPause(1, CROTCHET);
  playPause(1, CROTCHET);

  playNote(1, f4, MINIM);              // on
  playNote(1, a4, CROTCHET);           // a
  playNote(1, g4, CROTCHET);           // seat

  playNote(1, a4, CROTCHET);           // of
  playNote(1, b4, CROTCHET);           // a
  playNote(1, c5, CROTCHET);           // bi-
  playNote(1, a4, CROTCHET);           // -cy-
  playNote(1, f4, CROTCHET);           // -cle
  playNote(1, g4, MINIM);              // built
  playNote(1, c4, CROTCHET);           // for
  playNote(1, f4, MINIM_DOT);          // two
  
  muteAllChannels();
}

/****************************************************************************
 * MAIN LOOP                                                                    *
 ****************************************************************************/
void loop() {
    
}

 

4 thoughts on “Fun with chips #2: SN76489 sound generator IC

  1. Bentoc

    In the “setting the tone” section, you should end up with : 0b1001111001 not 0b1001110001 ?

    Reply
  2. Anders

    Your line #23 is missing quite a lot of newlines = setData is not doing anything because everything is commented out. #36 too but that doesn’t have any practical effect.

    Reply
    1. Machina Post author

      Actually, there’s a lot more wrong with that listing. One of the joys of cutting & pasting into WordPress. I no longer have the original code, so when I get a moment I’ll replace it with something else.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.