When I set out a few weeks ago to add an audio generator to the Project:65 computer, I expected that getting it to play actual music instead of random beeps would be the difficult part of the project. That actually turned out to be pretty easy, and instead it was the interface logic between the computer and the audio generator that caused me the most trouble.
The “expansion board”, as I’ve come to call it, is the middle piece in the video up above. It took a couple tries to get this part right, so I want to go through what didn’t work and what eventually did. At the end of the video, I demonstrate the updated and expanded version of the expansion board.
What this breadboard does is pretty simple: It adds an 8-bit IO port—well, output port, anyway. The same principal should work for adding an input port, but that’s a project for another day. For the audio generator, I use the port to set 8 output lines that control the frequency of the sound. Because IO on the 6502 is memory-mapped, the way we set those data lines is by writing to a particular memory location—for example by using a POKE command in BASIC.
Usually in 6502 designs you’d use something like a 6522 VIA chip for this—and I actually do use a 6522 in the P:65 for communicating with the serial port and the disk controller. But for this project I was on a tight schedule, and I didn’t have a second 6522, so I needed to make it work using parts that I already had on hand.
I decided to build the IO port using a 74HCT574 octal D flip-flop. Basically, the 574 is an 8-bit register on a chip. It has 8 input lines, which I attached to the computer’s data bus, and 8 output lines, which hook up to the sound generator or any other output device. There’s an Output Enable pin, but I’ve just tied that to ground so the outputs are always active.
And then there’s a pin called Clock. This isn’t the same as the system clock that runs the CPU. In a D flip-flop like the 574, when the Clock input transitions from low to high, it stores whatever values are on the inputs into its internal buffer. Those values appear on the output lines until a new value is stored. So the Clock controls when we’re writing to the register.
The 574 Clock signal was the source of all my trouble, because I needed that signal to combine two things:
- Address decoding: The 574 should be written to only when we write to the particular memory location we’ve assigned to it.
- Control logic & timing: The signal should only be active when the CPU is trying to perform a write, and when the data to be written is available on the computer’s data bus.
With other chips attached to the 6502’s bus, like the 32KB static RAM I’m using, there are two control inputs involved in a write. There’s one called CE, or Chip Enable, which is activated by the address decoding circuit, and another called WE, or Write Enable, which is activated by the control logic. For a write to RAM, both CE and WE have to be enabled.
With the 574, both those signals have to be combined into the single “Clock” input. Another difference is that the RAM’s CE and WE signals are active-low, while the 574 Clock latches in new data on a rising edge. I think that change got me started off on the wrong foot: In my head I conflated the rising edge with an active-high signal, and I started to think that I needed to construct a signal for Clock that looked like the inverse of what I was doing for the active-low WE signal of the RAM.
And in fact that’s what my first design looked like:
This circuit builds the 574 Clock input out of three other signals. The first, /CE_EXP, is the “Chip Enable for Expansion Board” signal, and it comes from the P:65 computer’s address decoder. The P:65 divides the 64 KB address space into 8 KB chunks, and it was straightforward to pull out a control signal representing a chunk of address space that wasn’t being used for anything else.
The second input signal is called RWB, and it comes directly from the 6502 CPU. RWB is high when the CPU is performing a read, and low when the CPU is performing a write.
The third input is called Phi2, and it’s the computer’s 4 MHz system clock. That’s involved in the generation of this signal because it’s needed to get the timing correct. All memory accesses in a 6502 system happen during a single clock cycle, where Phi2 is low for the first half and high for the second half. When the CPU writes to memory or a device, it sets the address lines and the RWB signal during the first half of the clock cycle. However, the data that it actually wants to write doesn’t appear on the data bus until the second half. So when we want to write to the RAM chip, for example, we don’t set the WE signal active until the second half of the clock cycle begins.
In this first circuit, the three input signals are combined using a 74HC02 quad NOR gate. The 574 Clock goes high when RWB is low, and /CE_EXP is low, and Phi2 is high, so when the CPU tries to write to the 574 the data gets latched in just after Phi2 goes high at the midpoint of the clock cycle. The 574 Clock signal goes low at the end of the system clock cycle, when Phi2 goes low again (but the 574 only cares about the rising edge of its Clock input, not the falling edge).
Unfortunately, this version of the circuit didn’t quite work. It did do some things correctly: It did perform a write to the 574, and it didn’t mess up anything else in the system—those are both wins. Occasionally it would even write the correct value, but most of the time it just filled the 574 with random garbage.
To understand what was going wrong, you need to look at the timing diagram in the WDC 65c02 datasheet:
Unfortunately, this image tries to convey every possible bus interaction the 6502 can have in a single diagram, and the result is as clear as mud. Luckily I found the Visual Guide to 65xx CPU Timing by Jeff Laughton that gives some much easier-to-read images and explanation. Check it out if you want a deeper dive into 6502 timings.
In any case, after studying that diagram I found a timing bugbear that I had either forgotten or never known about. If you look closely at the “WRITE DATA” line, you can see that at the halfway point where Phi2 goes high, the data to be written still isn’t on the data bus! The timing diagram says there’s an additional gap, called “tMDS” or the “Write Data Delay Time”, between the rising edge of Phi2 and the data actually being available. And according to the datasheet, when the 6502 is running on 5 volts, tMDS is up to 25 nanoseconds.
Now, since it looks like we’re going to be counting nanoseconds for a while, it’s worth mentioning that the control circuit already has some latency in it. Each NOR gate in the 74HC02 has a typical propagation delay of about 7 ns, which means that in our circuit the 574 Clock won’t go high until about 28 ns after the rising edge of Phi2. That sounds like we should be in the clear, except that the 574 also has a timing constraint, called the “setup time” in its datasheet. It wants the data at its inputs to be stable for about 20 ns before its Clock input is activated. This circuit didn’t do that, and that made the 574 uncooperative.
There are a variety of ways to give the 574 the extra time it needs, but using what I had on hand led to this:
This time, we’ve added four more NOR gates to the end of the circuit, set up as a chain of inverters. This gives us the same output as before, just slowed down by the latency of the extra gates. With 56 ns of total delay, the 574 doesn’t try to latch in its data during the write until the data bus is good and ready.
This version of the circuit works. In fact, it’s the version I used to record the Jingle Bells demo video. It’s an entirely reasonable circuit that obeys all the timing constraints of the components it’s interfacing with. But it does feel like it’s taking a convoluted route to get there. Even though I had something that worked, I felt like I could do better.
I eventually found myself looking at the datasheet for the SRAM chip I was using. I was curious about how the beginning of the /WE signal at the halfway-point of the clock cycle interacted with the tMDS delay for the data actually becoming available. I was particularly curious because I was using a 35-nanosecond SRAM—pretty fast compared to a tMDS of 25ns. I came to the realization that the SRAM was more interested in the rising edge at the end of its /WE signal than it was with the falling edge at the beginning of it.
The end of /WE corresponds to the end of the Phi2 cycle, when Phi2 goes low again. But if you look at the timing diagram again, you’ll see that the data bus actually remains valid for a while after that—a time labeled “tDHW“, or “Write Data Hold Time”.
Well, if the SRAM is waiting for the very end of the clock cycle to latch in the new data, shouldn’t we be able to do the same thing with the 574? If so, we could reuse the existing circuitry on the main P:65 board which generates the /WE signal used by the SRAM (and also by the EEPROM when doing firmware updates in situ). I was a little worried about the length of the hold time, but several threads on 6502.org left me feeling confident that the data would remain on the bus long enough for this to work. This led to the final version of the design:
Note here that I’ve divided the design into two parts—on the left, the existing circuitry on the P:65 board that can be reused; and on the right, the dedicated expansion board logic, of which there is very little.
With so much breadboard space available, I took the opportunity to do some upgrades. This version features three 74HCT574s, for a total of 24 output lines, each of which can be set individually. In order to give each of them a unique address in the computer’s memory space, I used a 74HC138 demultiplexer/decoder. This chip takes two lines of the address bus as input, and ensures that only one of its outputs will be active. The main P:65 board also uses a 138 for address decoding; here, I’ve daisy-chained one of its outputs into the enable inputs of the expansion board’s 138. By OR-ing the expansion board 138’s outputs with the /WE signal from the mother board, we guarantee that only one of the 574s will be written to with a single write operation.
After implementing this version of the design, I found that I could hook the sound generator to any one of the 574s and make it run (after changing the address in the software, of course). I still needed to test that I could use all three ports simultaneously, so I quickly built up a breadboard with 24 LEDs, each connected to a different output line. I then wrote some test code to flash the LEDs in a variety of patterns. You can see the results at the end of the video. I’ve always loved the blinkenlights on the front panels of old mini- and micro-computers, so it was fun to finally have something like that for the P:65 computer.
I’m still a bit divided about which of the two designs is the best. I think an argument could be made that the first version is more rigorous about the timing requirements—then again, relying on logic-gate propagation times is an inexact science, and it would probably be better to replace them with a more accurate delay element.
The second version might have the opposite problem—too much latency, because it’s really pushing against that hold time value. I think the circuit could be rearranged so there was only once gate between Phi2 and the various 574_Clock values, but that would require some more gates on the expansion board, and breadboard space is precious.
That’s a problem for another day. For now, I’m simply going to declare that it works for me, therefore it’s done. That’s one of the advantages of hobby projects: No one else gets to set the requirements.