Lamps
Compared to the displays, writing to the lamps is fairly easy. Well, that's not exactly true. It's fairly easy once you understand how they're using the MC14514 chip, which actually took me a little while to figure out. My first attempts weren't working at all, and it wasn't until I was trying to explain it to my friend that I finally had a revelation. The lamps states are written four at a time on U10A:PA0-PA3, addressed to one of 16 banks by U10A:PA4-PA7, and then strobed into place by U10:CB2. Super easy! The bank addressed by leaving U10A:PA4-PA7 all high is unused so there's a place to park the address lines when you want to use the lines for other things.
Unfortunately, that's not exactly how it works. I was picturing this like four bits were being latched off to control the lights. In essence, that's the way it works, but the MC14514 is not a latch. This chip is a 4->16 memory decoder, like you might use to chip-enable one of 16 memory chips. The "address" is latched on the decoder chip when U10:CB2 is strobed, but the real latching actually occurs on the silicon-controlled rectifiers (SCRs) on the lamp board. Unlike regular transistors (which turn off when you stop biasing them), the SCRs will stay "on" until there's no more current coming in through the anode. Once a lamp is turned on, it stays on for as long as there is current being sunk into to through the lamp. But our lamps are lit by rectified AC voltage, so it drops to zero volts 120 times a second. So when we "turn on" one of the SCRs, it automatically stays on for (at most) 8.3 milliseconds.
This is how it works: we send lines U10A:PA4-PA7 out to the lamp board as the "address" and lines U10A:PA0-PA3 as data. All the address lines are shared by the four MC14514's on the lamp board and each of the data lines goes to one chip's "Inhibit" lines. When the inhibit line is high on the MC14514, all the outputs will be low.
First, an "address" is latched to the MC14514, which tells the MC14514 decoder chip which line we want to go high (0-15). Then, if the inhibit line is set low, that one line will go high and the SCR will be biased so it will turn on. If the inhibit line stays high, the lamp's SCR is never activated. The lamp then stays on for 8.3 ms, until the supply current drops to zero and it loses its latch.
Timing of this process is crucial. We want to turn on the lights when the voltage is as low as possible so that there's not a huge inrush of current into a cold bulb. However, if we try to latch the SCR too early, it won't hold its value because it needs a certain amount of current through the anode in order to stay on. Reading the datasheet for the 2N5060 (the SCR), it looks to me like it requires 20mA through the anode to turn it on and 5mA to hold it. Assuming that the switched illumination bus has 5.7V and the resistance of a cold bulb is (maximum in my small sample set) is 4.4Ω, it should take about... I don't know. I tried to calculate how much time it would take for enough voltage to appear after the zero crossing, and I came up with about 50µs. Looking at this scan from the actual running machine, it appears that the original designers wait almost a full millisecond after zero crossing before they send out the first lamp strobe.
For my own implementation, I started with no delay between the zero-crossing interrupt and the lamp writes. Of course, the lamps didn't come on (there wasn't enough voltage to the anode yet to latch the SCRs). It wasn't until I put a delay in there that I could actually get values to stick. Experimentally, this is a scope of the minimum time I could wait (below).
I was able to shorten the delay to about 700µs after the zero-crossing before I can reliably latch the lights on. This might be a bit early, given the types of bulbs used. If you installed bulbs that had a higher cold resistance, the lights might not turn on every time. I might try some different bulbs to see what happens. I'm currently using #47 bulbs. Another note - currently, to achieve this delay, I'm counting clock cycles and waiting. This is a wasteful implementation, but I haven't thought of a better wait to get the delay between the interrupt and when I need to latch the lamps. I could set a one-shot timer with a different handler, but I haven't gotten that working yet. Anyway, because I'm waiting, I have to enable interrupts during the 120Hz interrupt handler, in case the display interrupt needs to fire. This means that sometimes my lamp strobes will be delayed while the Arduino is updating the lamps. That looks like this:
You'll also notice that my lamp strobes are tighter-packed that the original M6800 code. The Arduino is running 32x the speed of the M6800, so it can fetch and prepare data much faster. Checking the timing of the MC14514, my implementation is within speed tolerances, so I didn't bother to space out my strobes any.
If you look at my implementation of the 120Hz ISR, you'll see all kinds of interesting things happening before I strobe the lamps (during that "delay" period). After finishing the lamp implementation, I started working on solenoids and switches. Solenoids want to be turned off at the zero crossing to prevent back EMF from frying the solenoid transistor (I wrote that to sound smart, did it sound smart?). I don't honestly remember where I read that, but I know for a fact that the M6800 code always turns off the solenoids near a zero-crossing and I'm sure that it's to prevent wear on the solenoid driver circuits. Anyway, I use the "delay" time to do a couple of things. I turn off solenoids if they need to be turned off, and I read the switches. I'll talk about those in detail in later sections, but I mention it here in case you're looking at the code and wondering why I do so much before I strobe the lamps. The Arduino has 700µs to kill, so it might as well be doing something. In the original M6800 implementation, the switches are read after the lamps are strobed. Their 120Hz ISR also took 3.7ms to run. The Arduino implementation gets everything done in about 1ms. That's a lot of time given that it runs every 8.3ms, but everything works.
One final note: reading about SCRs and how they're used to drive lamps, I read that many implementations will toggle the bulb on/off at 60Hz in order to get a slightly dimmer lamp. I implemented that by having two sets of bulb values - on/off state and dim state. By OR-ing the dim state into the data every other time through the interrupt, the bulbs can be dimmed. I might put yet another dimming array to get four different brightnesses at 30Hz. I'm not sure what these will do to bulb life. If the game layer chooses not to use dimness, so be it, but it's there if desired.