Implementing “Tennis for Two” using op amps

“Tennis for Two” is the name given to an early video game implemented on an analog computer. See https://en.wikipedia.org/wiki/Tennis_for_Two and https://en.wikipedia.org/wiki/Early_history_of_video_games for more information on the game itself. This post documents the construction of a “Tennis for Two”-like game using op amps.

For the impatient 1: jump to the video section

For the impatient 2: jump to the schematics

Some references

Mubarik Mohamoud “The Bouncing ball analog computer” (PDF)

https://www.analogmuseum.org/english/examples/bouncing_ball/ (Great site)

Heathkit EC-1 manual (PDF) (this manual itself lists a lot of references to books from the 1950s, most of which seem to be available on archive.org! If you’re into that kind of thing.) It looks like this manual already contains a bouncing ball circuit; a lot of people seem to be under the impression that Telefunken were the first to have such a circuit in their manual.

Look Mum No Computer – Bouncing Balls With DIY Electric Analog Circuits (YouTube)

Introduction

Analog computers were a thing, a long time ago. What do they do? They basically consist of a power supply, a breadboard-like area, a bunch of potentiometers to set input values, and a bunch of op amps in various configurations. The breadboard-like area would (for example) be color-coded and the user would attach jumper cables between different color-coded areas. However, back in the day, op amps weren’t tiny ICs that could be had for the price of a paper airplane folded out of old newspaper. They were discrete, and made of vacuum tubes (when they were invented in 1941), or, after transistors became available, discrete transistors.

Op amp made from discrete transistors, from 1961, according to https://en.wikipedia.org/wiki/Operational_amplifier#Historical_timeline (https://upload.wikimedia.org/wikipedia/commons/8/85/Discrete_opamp.png)
Vacuum tube-based Heathkit “Educational Electronic Analog Computer” at the “マイコン博物館” computer museum in Oume, Tokyo

Using analog computers (and op amps), you could, for example, sum voltages, invert voltages, and integrate and differentiate voltages over time. Let’s try and remember some calculus from high school or university and recall what differentiating and integrating means.

If you don’t have any basic knowledge of op amps yet, EEVBlog has a good video explaining the basics at https://www.youtube.com/watch?v=7FYHt5XviKc. It isn’t absolutely necessary to understand the basic concepts of op amps, you can just use them as “black box” integrators.

Some math

If you don’t remember much with regards to differentiation and integration, skip to the next paragraph. If you remember a bit more, here’s the ultra-condensed version of the below explanation:
Differentiating a curve gets us a straight line, differentiating a straight line gets us a constant. Integration is the opposite of differentiation, so disregarding some lossy bits, integrating the constant gets us the straight line again. Integrating the straight line gets us the curve again. We can easily set a constant voltage using a potentiometer, and we can easily integrate using op amps.

Now, the less condensed version: here is a random list of facts that you may or may not remember:

  1. Integrating is the opposite of differentiation (that’s the easy one)
  2. Differentiation means getting the slope of a curve
  3. Integration, being the opposite of differentiation, means that we get the curve from the slope (a slightly lossy operation, as there is more than one curve with the same slope)
  4. You can differentiate things more than once. For example, from a quadratic curve (e.g., f(x)=x²) the first derivative yields a straight line (f'(x)=2x), from the straight line you get a constant (f”(x)=2). Integrating can be done more than once too, and as mentioned before, is the other way round, so from a constant you get a straight line, from the straight line you get a quadratic curve.

Now let’s say we have some acceleration, like… for example 9.8 m/s².
(There is a planet called Earth in the Milky Way, named after the fact that some of its crust consists of earth. The same type of earth as you’d find in a potted plant. When you drop objects from a small height onto this planet, they accelerate, and the rate of acceleration is 9.8 m/s². 9.8 is very close to π² and this is not a coincidence. Also the people who live there are made of meat, but that’s a topic for another day.)
So after 1 second the dropped object has a speed of 9.8 m/s, and after two seconds it’s 19.6 m/s, etc. Dropped objects get faster and faster. If you plot the speed of the object, you get a linear (i.e. straight) graph. However, if you plot the distance of the object from the point it was dropped, you get a curve, i.e., it gets steeper and steeper.

Let’s say, all we have at first is a constant, 9.8. In math terms, we could say that the function always producing this constant is f”(x)=9.8. (I added two apostrophes to “foreshadow” that we will be integrating this twice.) To get a linear function from the constant, we can integrate. The mathematical result of the integration is f'(x)=9.8x (+ some constant, which we will ignore here and below). Don’t remember enough about this to believe me? Try https://www.wolframalpha.com/input?i=integrate+9.8

This would draw a line with a rather steep gradient of 9.8. Implemented using op amps, the integrating op amp will have 9.8V on the input side, and a voltage linearly going up on the output. (Note: the op amp will invert the input, so in reality the voltage will go down, but that is not a major problem.)

To get a function that would plot as a curve from this linear function, we integrate again using a second op amp, and the result will be the function f(x)=4.9x² (ask Wolfram Alpha if you don’t remember how to do this (“integrate 9.8x”)). Now check out the formula to get the distance fallen after a time of t seconds at https://en.wikipedia.org/wiki/Equations_for_a_falling_body#Equations:

{\displaystyle \ d={\frac {1}{2}}gt^{2}}

Hey, that’s exactly the same formula! (g is 9.8, 1/2*g is 4.9, and t is just a rename of x.)

(Done with the math, time to talk about some electronics)

On an oscilloscope, the Y axis is voltage and the X axis is just “time”. Oscilloscopes commonly offer another mode, the X-Y mode. In this mode, one channel’s voltage will be plotted on the X axis, and another channel’s voltage on the Y-axis. Using this mode, we can simulate the effects of gravity on the Y axis and the horizontal movement of a tennis ball on the X axis. (Of course, the X-Y mode won’t exactly make it easy to debug things, so for now we’ll mostly work in the regular mode.)

Just a few more notes and we’re ready to implement stuff on breadboards and start looking at the resulting signals on an oscilloscope!

  • The power supply needs to generate a negative voltage, 0V, and a positive voltage. I’m using an old ATX power supply that I bought at a thrift store and have -5V/5V as well as -12V/12V. I use -5/5V below, this makes it much easier to interface with other components. If you don’t have anything fancy, you can just use two 5V wall warts back-to-back, or a couple AA batteries in series and tapped in the middle, or whatever you want!
  • Constants are just arbitrary voltages input using potentiometers. We don’t have to use 9.8V, we’ll just use something that is convenient and looks okay.
  • To integrate an input signal using an op amp, all we need to do is put a capacitor between the input and the output of the op amp. We’ll also need a resistor to make the integration happen non-instantaneously. (Check https://en.wikipedia.org/wiki/Op_amp_integrator or similar to learn more)
  • The op amp’s output will be inverted
  • To make the integration slow enough, we need capacitors with a high capacitance. But preferably we don’t want to have to take capacitor polarization into account, so we won’t use electrolytic capacitors. I’ll be using a 1 μF ceramic capacitor, which is the highest non-polarized capacitor value I have and makes our calculations very easy. We’ll also need resistors with a high resistance in the MΩ range, as the RC delay when using a 1 MΩ resistor with a 1 μF capacitor is 1 second (1000000 x 0.000001 = 1).

To get started, here’s the setup with an op amp integrating our constant fed in using a potentiometer:

The wire on the left side goes to -5V (or -12V; currently I’m using -5V and 5V rails because that’s more compatible with the components I have on hand). The breadboard voltage rails are 5V (red) and GND (black).

In this circuit, I’m using an LM324N quad op amp. The pinout looks like this:

(Note that the IC’s rotation isn’t the same in the above two pictures)

And here’s what we get on our oscilloscope:

Wow such straight lines!

Let’s integrate that straight line again. Here’s our setup:

And here’s what we see on the oscilloscope (probing the previous output stage and this output stage at the same time):

Is that all there is to the Y axis? Well, no. First of all, we are currently falling “up”. That’s easily fixed; we could just invert the signal again using another op amp. Or maybe we could simply input a negative voltage as our gravity, then we wouldn’t need to use an extra op amp circuit? Let’s not worry about that for now, we have some other things to take care of. Remember, this game/simulation is supposed to be tennis-like.

Tennis balls should bounce off the floor. Oh right, we don’t even have a “floor” yet, per se. We see that both signals reach “a floor” or “ceiling”, but they’re actually just reaching the op amp’s voltage rails (-5V and 5V) or the voltage set by the potentiometer, at which point they can’t go any further.

To make the ball bounce, we can “override” the gravity input and quickly feed in a negative voltage instead. (Of course, the gravity input is still there, just with a much higher resistance.) Conveniently, our floor is a negative voltage. (In the above pic, we’re still inverted of course. If we add another op amp, we can invert this signal.) We just need to “copy” this voltage to the – input of the very first op amp and we’ll produce a beautiful bounce. (“Cool so we just write some code and copy some variables, right.” “Wrong, sit down!”)

One immensely popular way to copy a voltage from one place to another is to use a wire to connect the two places! But in our case, we want a smart wire. This wire shouldn’t be active until we reach the floor (which is just a certain negative voltage of our choosing). Well, it may not be immediately obvious, but one of the most famous components in the world of electronics happens to have this property! We have surely heard of diodes before, right? A single standard diode doesn’t become active until we reach 0.7 V. If we use two diodes, it’s 1.4V, three diodes, 2.1V, etc. If we have negative voltages, we just flip the diode’s terminals around. Cool! But inconvenient. Here’s another type of diode though: the zener diode. Zener diodes are designed to be reverse-biased and become conductive (generally, very suddenly) at a certain voltage. So instead of (e.g.) stacking four regular diodes, we could use a single zener diode with a breakdown voltage of approximately 2.8V (and flip its poles because it’s reverse-biased).

So we just connect the output of the third op amp back to the input of the first op amp, via a diode!

Here’s a simulation of this circuit. (Note 1: this circuit uses wildly different resistor/capacitor values from the ones we were using before; note 2: please press reset first) The third scope is the y output of this circuit. Feel free to experiment by taking out the zener diode at the right and plugging in the single diode (which is a hypothetical diode with a forward voltage of 10V or so) or series of (normal) diodes instead. Note that directly connecting a negative voltage to a place that already has a positive voltage will produce a large current. This current is visible in the fourth scope in the simulation.

Well, things don’t look too bad in this simulation! Currently, the ball just bounces back to its previous level, but we could easily add a little something to dampen the voltage, which I may or may not explain later:

Look at the third scope, that looks quite like what we are after, does it not?

Well, things don’t always play out the same in the real world. Here’s the output of the circuit without any damping applied. As we can see, there is some damping anyway. Furthermore, the floor voltage level moves up over time, which is something I hadn’t expected.

So instead of using a zener diode to set the floor, we might want to explore some other options.

  1. Use an ideal diode or ideal zener diode.
  2. Use some other type of diode.
  3. Use a comparator to compare against a configurable floor voltage level.

Oh yeah, an ideal diode! Silly me, messing around with them non-ideal diodes. Let’s pay a couple cents more and buy some ideal ones, right?!

Enter the precision rectifier

Above, we described diodes and zener diodes as “smart wires” that become active at a certain voltage level. But that’s an “ELI5” explanation. Diodes are actually kind of devious; they do become active at a certain voltage, but when they do, they substract that voltage from the input voltage!

We could use a precision rectifier (https://en.wikipedia.org/wiki/Precision_rectifier (below circuit diagrams are taken from this page)) instead.

This sort of behaves like an ideal diode!
And this one is even more ideal because I hear this one can be used with negative voltages.

These circuits behave like diodes that don’t have a voltage drop. な、何!? Op amps, is there any thing they can’t do? There are versions that do full wave rectification too.

By the way, we find something close to the basic version of the precision rectifier in Mubarik Mohamoud’s circuit (page 7 of the PDF):

To be honest, using these things won’t help us much on our quest to keep things simple. We don’t need a perfect circuit; let’s use some artistic license and check out some other diodes first!

Other diodes

There are other diodes out there that have a high forward voltage. For example, how about… a white LED?!

Using a white LED between the output of op amp 3 and input of op amp 1, we get automatic damping… and actually this looks pretty damn good, you’re hired!
The LED blinks every time we reach its forward voltage. Kinda cool IMO! (Don’t stare too hard at the breadboard, it’s not in a clean state right now. Or the dangling jumper wire, which looks pretty close to shorting something to GND ^_^)

Of course, if we wanted to be exact and stuff, we should maybe look into using comparators instead! We’ll definitely need comparators at some point so why not check them out now.

Enter the comparator

We could also use a comparator, and yes, you guessed it, our good old omnipotent friend Op Amp can be used as a comparator, because of course he (or she) can! We’ll make an op amp compare our Y coordinate voltage with a low voltage set using a potentiometer. That’s our floor.

Op amps used as comparators always output their low voltage rail or their high voltage rail, depending on whether the input voltage is lower or higher than the reference voltage. Our above op amp had 5V and -5V rails, so the comparator would output 5V or -5V. -5V is not convenient for what we’ll do later, so we’ll convert -5V to 0V by feeding the output to a diode. The negative voltage won’t make it through, only the positive 5V. Because the negative voltage won’t make it through, it will be like the input is floating. We will add a pull-down resistor.

To make our comparators actually do stuff, we can use relays. (We could use many other things, the most simple/convenient of which might be a CD4016 or similar, but the original Tennis for Two used relays, so we will too!)

How do we connect our relay to an op amp output? Well, op amps don’t like to drive heavy loads, so we’ll use a transistor (commonly, S8050 or 2N2222), like in the following circuit. (I’m not sure if the op amps of old were able to drive relays.)

This first diode keeps away the negative voltage. I also added a pull-down to ground, because otherwise the input would be floating. E.g., on my multimeter it reads as 2V. No 0V! No, 3V! The diode in parallel with the relay is to take care of back EMF from the relay.

We’d like the ball to do something when the player presses a button. In the original(?) game (https://www.youtube.com/watch?v=s2E9iSQfGdg), the ball is able to bounce off the net in the middle, and can be hit at a configurable angle. I’d say we skip the net for now, but we should implement the button. More on that later!

For now, let’s just quickly think about what we need to make happen when a player hits the ball.

  • The “gravity capacitor” keeps doing its thing at all times
  • We just want to momentarily input a higher voltage into the first op amp’s input
  • This voltage can be controlled using a potentiometer

We’ll talk about these potentiometers and buttons later.

The X axis

In a nice “Tennis for Two” game, players have beautiful controllers that have a button. This button changes the current direction of the ball. (Let’s define that a little more precisely: player 1 can only invert the direction of a ball flying right-to-left, and player 2 can only invert the direction of a ball flying left-to-right.)

In addition, there should be some kind of potentiometer inside the controller, allowing players to hit the ball hard or less hard.

Before we dream of making an ergonomic controller (yes yes, it should incorporate glass and steel and Nixie tubes to display the current score), let’s think about what the X axis is even supposed to look like.

The “distance traveled” plot for the Y axis was a curve, but for the X axis, it’s a straight line. Conversely, the “velocity” plot for the Y axis was linear, but for the X axis, it’s a constant. (For illustration: in the plots below, the ball travels at a constant velocity of x volts/second.) (In reality the X axis velocity should slow down a little bit over time due to air resistance; we could model this by fudging in some kind of damping mechanism.)

The X axis circuit works like the Y axis circuit, in that it is built from op amps. However, we integrate only once. (So just a single op amp, actually.) In the Y axis, we integrated twice. (See above if you can’t remember how that worked.)

First of all, some definitions: player 1 is on the left side of the screen (where the X axis is negative), player 2 on the right side (where the X axis is positive). Here are some example graphs of the X axis:

A negative voltage that is going down: ball is in left side of screen and moving farther left. Player 1 can hit the ball in this state. Player 2 can’t. Velocity is -0.3V/s.
A negative voltage that is going up (like in the above picture): ball is in left side of screen and moving right. Player 1 can’t hit the ball in this state. (Most likely they already hit the ball.) Player 2 can’t hit it either. After we cross 0V (at the 5 second mark), player 2 can hit the ball, player 1 can’t. Velocity is 1V/s.
A positive voltage that is going up: ball is in right side of screen and moving right. Player 2 can hit the ball. Player 1 can’t. Velocity is 0.3V/s.
A positive voltage that is going down: ball is in right side of screen and moving left. Player 2 can’t hit the ball in this state (they probably already did). Player 1 can’t hit it either. (Once the voltage crosses 0V (at the 5 second mark in this graph) and goes negative, player 1 can hit the ball.) Velocity is -1V/s.

Players use a potentiometer to decide how hard the ball is hit. This potentiometer sets a voltage that represents the velocity. This voltage is connected to op amp 1.

Here’s an example of the ball being hit:

Here player 2 (positive voltage, so on the right side) hits the ball relatively softly at the 4 second mark and the ball changes its direction.

(Hey, gnuplot, it’s been a while.)

There is one minor engineering problem: as the player sets the velocity with their controller, we have to make sure they can’t change the velocity after hitting the ball. If we just hooked up the potentiometer directly to the first stage op amp, the player could/would make the ball slower or faster in mid-air by fiddling with their potentiometer after hitting the ball. Not very tennis-like!

How can we do this? Well, there should be a momentary push button. If this button is pushed, there should be a check to see if we are on the correct half of the screen, and if the ball is traveling in the correct direction. This is implemented using comparators and AND circuits. If the checks pass, we apply the voltage set by the potentiometer to feed op amp 1.

  • We need to check conditions. We use op amps configured as comparators. We could easily use NPN transistors to form an AND gate but we’re lazy and will use 74-series AND logic chips. (Which weren’t quite around yet when this game was invented!)
  • Our comparators compare against the middle of the screen, which is 0V. This makes the wiring very easy: one input is the current voltage, and the other input is GND.
  • The other player has the inputs on the comparators swapped. (I.e., “one input is GND, the other input is the current voltage”)
  • One voltage is the current voltage as it is output by the X axis op amp (the absolute position of the ball on the screen), the other voltage is the voltage that is input into the op amp (i.e., the output of the sample and hold circuit mentioned below).
  • After checking the conditions, the new voltage is set. This immediately changes the ball’s direction, which means that the conditions are no longer true after a very short moment.
  • However, we need to keep applying the same voltage until the other player presses their button.
  • We can store the potentiometer voltage in a sample and hold circuit.

Sample and hold circuit

Well, apparently there are many ways to build a sample and hold circuit. I happen to have a (Japanese) book called 回路の素 101 that has a whole bunch of analog circuits, many of them using op amps, and among those, two circuits to do sample and hold.

If you like reading Japanese text, I recommend this book. I read it cover-to-cover a couple years back. I certainly don’t remember everything, and it doesn’t always explain everything, but I did (e.g.) remember reading about the precision rectifier (mentioned above) and that there was something similar to the hold and sample circuit.

Here are the two circuits in a simulator:

The (very) simple version (link to falstad simulation), circuit 53 in the book. (I’m using a different capacitor value.)

We can see that this circuit only holds the sampled value for a few milliseconds. The charging of the capacitor is slow, and it discharges itself pretty quickly.

The advanced version (link to falstad simulation), circuit 54 in the book. It holds the voltage rather nicely. The book uses a 220p capacitor.

In real life, I went for a 1 uF capacitor, for improved debuggability: when using an oscilloscope (or even multimeter) to make sure that your capacitor is holding the expected voltage, you will find that the voltage drops very quickly. This is due to the oscilloscope’s input impedance, which is likely 1 MΩ. Using a tiny capacitor, the voltage will be gone almost immediately. Using a 1 uF capacitor, you have a couple seconds.

“Controllers”

The controllers are conveniently placed on a hard to reach corner on the breadboard. We have two potentiometers and one button per player. When the button is pressed and the comparators’ outputs going to the AND chip look good, we drive an SPDT relay. SPDT means that the relay normally connects player 2’s potentiometer to the input of the sample and hold circuit. But when the relay is active, it instead connects player 1’s potentiometer to the input of the sample and hold circuit. (Or the other way round, it doesn’t matter much.)

The sample and hold circuit must hold a new value when either player 1 or player 2 presses their button (and the abovementioned conditions pass). This means that we need an OR circuit as well.

Outside the controller, we have a circuit that is inserted into the aforementioned sample and hold ciruit, as shown below. “Input 1” and “Input 2” are directly attached to the potentiometer outputs. The “R1 coil” (relay 1 coil) and “R2 coil” inputs are the binary signals mentioned above.

Here’s a schematic of this sub-circuit:

Circuit (Falstad simulation). AND circuit output is amplified using a transistor and goes to “R1 coil”. When this signal is 5V, the coil forces the relay to connect “POT CONT 1” to the output. When the signal is 0V “POT CONT 2” is connected. This goes into the input of our sample and hold circuit. The sample and hold circuit requires a switch in the middle. This switch is implemented using another relay that is controlled by the output of the OR circuit mentioned above. When the relay is off, it leaves the capacitor and op amp on the right floating. When the relay is turned on, we sample one of the potentiometers.

Other implementation notes

I used a 74LS08 chip for the AND gates. I’m using all four. I used a 74LS02 chip for the OR gate. This is a NOR chip, and I’m using two NOR gates to emulate one OR gate. The remaining two NOR gates are currently not used. These logic chips could be replaced with BJTs if you want to make the whole thing more era-appropriate, but you might find that the naive AND/OR gate implementations are a bit janky.

There is a lot of space between the “sample and hold” op amp and the “comparator” op amps. I added an op amp I didn’t need!

Putting it all together

I decided to squeeze everything onto a single breadboard. To make this a little easier, I decided to build the circuit in Fritzing before building it in real life.

Breadboard layout designed in Fritzing.

Change history:
2024-08-05 Initial upload (“v2”)
Notes:
The relays may not be connected correctly here — some things may not make sense (e.g. X and Y could be the wrong way round, or player 1’s potentiometer could be wired to player 2’s comparators, etc.)
The relays shown in Fritzing (and thus the above image) aren’t the actual relays used. (Actual relays used are Omron G2R-1 5V relays. These don’t fit on breadboards, but we didn’t really have any space remaining on the breadboard anyway.)
The real-life version (as seen in the picture/videos below) uses two dual op amps in the “sample and hold” region, though one is not required. There might be issues in this area.
Red means a connection to +5V, purple means connection to -5V, black means connection to GND, green means connection from transistor to relay, yellow means connection from relay to somewhere on the breadboard, blue means “other connection” on the breadboard. I mostly tried to follow this rule when building the real thing, but 1) my connections from board to relay are random, 2) connections from power supply to board are all black, 3) the white wire sticking out to the right is GND and should be black.
You don’t have to use LM324s and LM358s. It’s just that I had a bunch of these lying around, waiting to be used.

Pics/videos

In this video, I set the Y potentiometers to a rather low resistance. This causes the op amp’s capacitor to charge quickly. This means that the ball will be hit very high. Note: In the first shot, the ball goes above +4V, which is beyond the currently set voltage range (1V per division, oscilloscope is showing 4 divisions)
In this video, I set player 1’s X potentiometer to hit the ball rather hard.

Taking things further

Here is a list of features that might be pleasant to have:

  • Proper controllers
  • A ceiling to bounce off of (I think this is just one extra diode). Should look better than a flat line when the ball hits the -5/+5V limits!
  • A nicer-looking ball
  • Some audio feedback (I thought the relays would provide this, but they’re relatively quiet, actually)
  • A net in the middle (both visually and functionally)
  • A way to detect if a player misses
  • Perform some game-like action when a player misses
  • A score counter

I think you’d need another breadboard to add all this!

I think I’ve had enough of this stuff for now.

Simple Apple 2 PSU repair

Well, I finally had a chance to see a few of those infamous RIFA metallized paper capacitors first hand!

Yesterday, I had a look at two Apple IIe machines. One was from 1987. This computer works and is in really good condition and bears the signature of Steve Wozniak. I thought it would be a good idea to go and check for RIFA capacitors in there, but there weren’t any. Here’s a picture of its really clean power supply:

I am guessing that those two white things are the noise suppression caps.

The other Apple IIe doesn’t work. The person running this machine immediately switched off the mains line when smoke started coming out of its power supply. That’s good! The capacitor didn’t go short, and the fuse was still intact. Maybe it even reduced the amount of smoke and brown juice sprayed about its perimeter.

From the symptoms I’d correctly assumed that the owner had witnessed a RIFA capacitor explosion, so I came prepared by buying some replacement caps! I’d expected to have to bodge them due their size difference, but to my surprise, the PSU board had three holes for the noise suppression caps. The RIFA cap was using the outer holes (“holes 1 and 3”), and my smaller cap fit perfectly into holes 2 and 3! Cool beans.

Original RIFA cap (pin spacing of around 2 cm I believe) and replacement (1.5 cm). (The replacement cap has its X2-certification on the other side.)

The PSU had two RIFA caps. The one pictured above is “intact”, though it has a huge crack in its plastic case. Here are the pictures that we’ve all been waiting for, the exploded cap:

The ejected brown juice wasn’t too bad and cleaned up quite nicely.

Here is the computer put back together again, already performing advanced calculations.

The end.

Edit 2024-07-11: one more, an Apple II Plus

This guy’s power supply was riveted. What the actual? I think the screws used were some tamper-proof kind, too. The rivets were drilled out and the screws still wouldn’t come out, beyond a millimeter or so. So the screws were drilled out too. It was a bit of a nightmare. The label on the power supply said Astec AA1040. There were no RIFA caps in it.

Apple 1 (replica) power supply build and repair

A while ago an acquaintance asked me to build a power supply for his replica Apple 1 board. He had all parts on hand (in fact, they were the exact parts mentioned in the Apple 1 documentation and schematics, Stancor P-380 and Stancor P-8667). My acquaintance probably knows a lot more than me, so I basically just did what he asked: I drilled holes into a sufficiently nice piece of wood from the 100-yen shop, soldered connections to the transformers (I believe they were probably salvaged, but luckily still had wires attached), added a fuse, soldered an AC cable into the mix, and soldered the Apple 1 power connector. Doing all this took between 4 and 8 hours, I don’t really remember. Here’s a picture of the assembled power supply.

With multimeters hooked up to each of the voltage rails (-12V, -5V, 5V, 12V), I hid behind a big rock and pushed down on an ACME plunger detonator. The explosion… didn’t take place, and the voltages were all good. Phew! Next I hooked up a period-correct monochrome CRT, and saw an image! A very jumpy image. It was possible to adjust HSYNC on the back of the CRT, which stabilized the image, but it still didn’t look correct. Pressing RESET or any other key on the keyboard didn’t do anything.

Stable (i.e., not rolling) but incorrect image

(Note on fuses: my fuse is on the primary side, mains voltage here is about 100V. I blew a 0.3A fuse on power up, then a 0.5A fuse on power up, and am currently using a 2.5A fuse. Maybe I’ll try 1.0A or 1.5A. Or maybe I should go and look at slow blow fuses.)

Before studying the schematics, I fired up an oscilloscope and had a look at the frequencies of the video signal. Here’s what I found:

Those small yellow blobs in regularly-spaced intervals close to the center line are the wannabe horizontal blank periods. They are supposed to have a frequency of 15.something KHz. In the above picture, I moved the oscilloscope’s vertical cursors further apart such that I get a frequency somewhat close to 15 (12.1 KHz), and as you can see we get 6 horizontal blanks in that period! This means the video signal is much too fast. I also looked for the vertical blank. It’s supposed to be 60 Hz and measured as 243 Hz. (Probably actually 240 Hz.) (At this point it’s worth worrying that we might be overclocking the CPU as well, but that wasn’t the case, it was running at a safe speed.)

At this point it was time to print out the schematics and study them. Here’s a picture of my annotated Apple 1 schematics that I used to debug the problem:

Teletype section (Note that I cannot guarantee that all my annotations are 100% correct.)
CPU section (nothing much annotated here)

Some repair guidelines

There isn’t all that much repair information on the Apple 1, but it’s not too complicated. The computer is basically two devices, a teletype that generates video, and a “computer” section that has the CPU, ROM, and memory. The two devices are linked through an MC6820 chip. There’s one more link; we also get the clock signal for the CPU from teletype section.

One more thing: when you turn on the Apple 1, the CPU isn’t live yet, because there is no reset circuit! (You have to reset the CPU manually using a special key on the keyboard.) So anything that looks too garbage-y right on power up isn’t likely to have much to do with the CPU.

Repair

After studying the schematics, I fired up an oscilloscope and traced the oscillator’s output, through the flip-flop, through various counter stages. At this point, a little luck, or a different strategy, would have helped me find the problem rather quickly, but it took at least an hour (probably more) of poking around until I found something obviously out of the ordinary: the “TC” output of the 74160 decade-counter IC was very low. Man, if it had been a Q output I’d have found it in minutes, heh.

74160’s TC output

I removed other chips the TC signal goes to, removed the 74160, checked for shorts to adjacent pins on the board, but couldn’t find anything. Which means the bad signal is produced by the 74160. Perhaps it would work with a slightly higher voltage, so maybe we’ll keep it. So we decided to see if we could get some replacement 74160 ICs (none of that Low-Power Schottky rubbish), and fortunately someone was selling them on Yahoo Auctions! We procured a few and the replacement 74160 made the problem go away!

(74LS160s or even modern HCTs/ACTs/whathaveyous would have likely worked too, but this replica board is proudly populated with original non-LS chips from mostly the 1970s. It’s all fun and games until they say good-bye!)

There were no more defects and we were able to boot into Wozmon and load BASIC from tape.

How to boot into BASIC

After powering up, clear the screen and reset the CPU using the dedicated keys. You should get a ‘\’ prompt. Type C100R and enter. This runs the cassette firmware. You should get “C100: A9*”. Then type in E000.EFFFR and at the same time you press enter, start playing the tape. (This loads the BASIC interpreter into memory at 0xE000 to 0xEFFF.) When the audio stops and you get ‘\’ you can start the BASIC interpreter by typing E000R and enter. The BASIC prompt is ‘>’.

The acrylic case

For some reason, the acrylic case that was purchased to put this system into didn’t have the screw holes in the right location, so I had the pleasure of adding more screw holes into the case. (Not just that, the lid needed some trimming in order to fit the cassette board, and we also decided to mod the air inlet/outlet a bit.) And though I didn’t have any experience drilling holes into acrylic (or even a lot of experience drilling anything, really), it actually went really well, using these drill bits specially made for use with acrylic: https://www.amazon.co.jp/dp/B00A630ZRE: Acrysunday アクリル板専用ビット.

It all worked out, mostly. When I did the four holes for the fan I unfortunately didn’t take into account that when screwing the nuts onto the bolts, the nut will obviously add a millimeter (or so) of height vs. just a naked bolt. (The case isn’t that high, so I just had a couple of millimeters I could move the holes up or down.) Putting the nuts on the two top bolts will prevent the lid (the one pictured above) from fitting properly, it would rest on the nuts instead of on the bottom case.

We added some heatsinks on all of the counter ICs, all of which were running rather hot (about 65 degrees after 10 minutes of operation), a powerful fan to blow air on the main voltage regulator (which sucks air through a filter so we don’t end up with a bunch of dust inside), and made a make-shift connector for the video port (J2).

Makeshift video connector using screw terminals: general idea
Makeshift video connector: placed on J2 and connected to TV input

Building a new ZX80 / Making PCBs in Inkscape / ZX80 replica Gerber files

Jump to ZX80 replica PCB Gerber files

A while ago, I watched a series of videos by DrMattRegan on the ZX80 and was very impressed with the uber-hacks the designers put in there. I’m not going to go into much detail here, but here is a set of facts that may pique your interest:

  • The CPU’s A6 pin is permanently shorted to its \INT pin
  • The ZX80 has only 1 KB of RAM
  • The RAM is SRAM but the CPU’s internal DRAM refresh counter is not wasted

Additionally, the ZX81 was one of the first retro computers I ever repaired (article 1, article 2), which made the prospects of getting a ZX80 of my own even more attractive to me.

You have a lot of options if you want to build a ZX80 yourself. First of all, all the ICs (except the 2114 SRAM) can be bought new. There are multiple PCB designs that you can download for free, some electrically equivalent, some using chips that are a little more common than the ones on the original PCB. (For example, apart from the 2114 RAM, the 74LS93 is pretty rare because it is essentially useless nowadays. It was probably cheaper than the other 74-series counter ICs about 50 years ago, because a lot of “maybe nice to have” features were removed. It can easily be replaced using a different 74-series counter IC, and these days there really is no price difference.)

One thing that I really liked about the ZX81 that I worked on was the curved traces and the absence of solder mask on the traces. On this page by Grant Searle, you can find instructions to re-create the original PCB. There is a PDF that contains high-res images that look like this:

These can be printed out and turned into PCBs. I’m not much of a chemist, and even if the etching and tinning processes went well, I’d still need to do a bunch of drilling and then plate the newly drilled vias. Nowadays, there are companies such as PCBWay and JLCPCB that can do all this for you, and in an automated fashion, and for very attractive prices! (I do not intend to steer you away from etching PCBs yourself, in fact it always impresses me when people make PCBs themselves!)

However, these companies (currently?) do not accept bitmaps (as far as I know), they want Gerber files. So I traced the (600 dpi) bitmaps in Grant Searle’s PDF file and sent them out. I chose JLCPCB after some research because I found a pic of a board manufactured by JLCPCB that didn’t have any solder mask applied, and it looked pretty much the way I wanted it! However, I’m sure that PCBWay could do the same.

In order to get a board without solder mask, you have to get rid of the solder mask layer in the SVG or Gerber file, and you probably should also add a comment stating that this is intentional. I added this in my order, no guarantees the Chinese is correct: “The Gerber files don’t have a solder mask. This is intentional. Please do not apply a solder mask. Gerber文件没有焊膜。这是有意的。请不要应用焊膜。 我们有会说中文的人,所以如果你有什么问题,可以说中文。” I ordered 5 boards (which is the minimum order), lead-free HASL, white silkscreen (“ink-jet/screen printing silkscreen”), no gold fingers (not present on the original PCB either), regular PCB thickness (1.6 mm), regular outer copper weight (1 oz), regular via covering (tented), no castellated holes. JLCPCB’s customer service recommended I switch to ENIG, but it worked out fine with regular lead-free HASL. The price was $18.02 plus $11.70 shipping, minus the first time discount ($5).

When exporting the Gerber files in KiCad, you should follow the PCB manufacturer’s recommended settings. JLCPCB and probably most other places have a support page for this, this is JLCPCB’s.

A glimpse of the outcome

Tracing bitmapped PCB foils

I used Inkscape to trace the PCBs in the PDF. Tracing is easy, you just load a bitmap and then go to “Path” -> “Trace Bitmap…”. But how do you convert that into Gerber files? Well, there is an extension that converts SVG files to KiCad files, svg2shenzhen.

It still took me many, many hours though. Why?

Converting traced holes to actual drill holes

svg2shenzhen expects “circle” shapes in a layer called “Drill”. If that layer doesn’t exist and/or it doesn’t contain circles, there won’t be any drill holes in your Gerber file. I wrote a very simple Inkscape extension to do this: Converting paths to circles in Inkscape

But first, we need to separate out the holes into a single layer. To do this, we first break apart the path containing all the traces (“Path” -> “Break Apart”). Then everything that isn’t connected together, and everything overlapping other things will exist as separate paths. Holes overlap other things and thus will be separate paths. And they will be obscured by the outer path. Now you just click on the surrounding path and remove it, perhaps like this:

One more consideration is hole size. Though the details are a little hazy now, selecting multiple circles and changing their size all at once didn’t work very well for me in Inkscape, so I just edited the SVG file directly.

The traced circles are paths that more or less look like circles, sure. But they’re often slightly oval or otherwise unshapely and not all the same size. So when you (after using the extension) crack open the SVG in a text editor, you may see that the radii are slightly different everywhere:

holes_circles.svg:    <circle
holes_circles.svg-       cx="50.392669494501675"
holes_circles.svg-       cy="193.60723742169256"
holes_circles.svg-       r="0.3154674216925315"
holes_circles.svg:       id="circle12107" />
holes_circles.svg:    <circle
holes_circles.svg-       cx="64.40136232421558"
holes_circles.svg-       cy="193.60780999999997"
holes_circles.svg-       r="0.31595000000000084"
holes_circles.svg:       id="circle12109" />
holes_circles.svg:    <circle
holes_circles.svg-       cx="78.4180010571009"
holes_circles.svg-       cy="193.60854169447143"
holes_circles.svg-       r="0.3165816944714095"
holes_circles.svg:       id="circle12111" />

I just did a find and replace operation here and used holes sizes that seemed good to me (after JLCPCB support telling me that my holes were rather small). And damn, by sheer dumb luck, I chose the best hole sizes ever (0.4 mm) and my jumper wires fit right into them without using clips; they make almost perfect contact. Maybe 0.375 mm or so would have been even better? (The other hole sizes I chose were 0.8 mm and 1 mm.) (Note: PCB manufacturers don’t have every drill bit in the universe, so 0.4 mm and 0.375 mm may just end up being the same drill bit.)

Excellent hole size.

Update 2024-04-19: the holes for the headphone/microphone/power connectors are too tight though.
:(

Adhering to minimum spacing constraints

PCB manufacturers require that certain spacing constraints are observed. For example, traces may need to be 0.125 mm apart. The original bitmap you have may or may not adhere to the spacing constraints. I don’t know whether the bitmap in the PDF adheres to them! But I certainly do know that my order was rejected because the traces in my Gerber file weren’t quite up to snuff.

How would I even find out what my minimum spacing is? Well, I found this page by yet another company that makes PCBs: https://instantdfm.bayareacircuits.com/. This page analyzes your Gerber file and sends you a snazzy PDF with close-ups of your horrible transgressions. This is a screenshot from an early version of my Gerber files:

2.97 mil… that’s 0.075438 mm. Less than 0.125 mm!

So I manually fixed this location and re-submitted, and sure enough, there were plenty of other similar narrow gaps. So I decided there must be a way to get Inkscape to find these critical regions, and this is what I came up with:

I just gave every object a 0.125/2 mm = 0.0625 mm border, and then zoomed in a bunch to look for objects that were touching each other. (Actually I probably added some to that value, but a couple months have passed and I don’t remember.)

Border color is set to red here. Lots of… intimate traces!

Mistakes

As mentioned earlier, in Inkscape, when you break apart a path that contains “holes”, you’ll end up with a large mass obscuring the holes. Here’s a video of exactly that:

Look at the large black regions with holes, especially near the top left. The holes will “disappear” (they will be obscured) after breaking apart the path.

Well, I failed to notice that a number of holes within these black regions had been obscured. And thus they came out like this on the PCB:

IC3 and IC14 have their left pins all sewn together. There were a couple similar spots elsewhere, but you get the idea.

While annoying, I was able to fix this using a utility knife. Boy, that utility knife’s blades went dull quick. (Luckily, my utility knife is one where you can just snap off consumed blade chunks. Having done this for the first time in my life, it was quite scary, to be honest. I did it behind a glass window.)

Very beautiful, I know. However, once you have soldered the IC sockets, all this is mostly hidden underneath the sockets.

Procurement and assembly

After taking care of these mishaps, it was time to do a bunch of other stuff, such as celebrating Christmas and the New Year, and at some point bite the bullet and solder sockets and resistors and capacitors. I soldered everything using lead-free solder, and most or maybe even all of the components I put on the PCB are lead-free too, including the Z80! So maybe this is the first RoHS-compatible ZX81?! (Except the 2114 SRAM chips most likely aren’t RoHS-compatible. We’ll talk about those in a later section, BTW.)

I had half of the 74-series logic chips in stock. (From when I got Ben Eater’s DIY 8-bit computer, which I never assembled. I used the breadboards and some of the other components for a host of other things though!) The other half came from a small electronics shop right next to my office in Machida, サトー電気. I wanted a RoHS Z80 (print on chip ends in -PEG rather than -PEC), and bought it off Amazon.

Update 2024-04-19: Zilog/Littelfuse are reportedly discontinuing the original Z80 CPUs after almost 50 years of production! Maybe get them while they’re still available. Only source so far: https://www.mouser.com/PCN/Littelfuse_PCN_Z84C00.pdf (zilog.com still seems to list everything as “active” at the time of this writing)

For the ROM I’m just using my EEPROM that I’ve been using in other projects. Since it’s huge and has a slightly different pinout, I’m currently using a breadboard as an adapter.

Putting jumper wires in IC sockets permanently damages the sockets. Here I am plugging the jumper wires into a sacrificial socket that I plug into the soldered socket. This way the soldered socket doesn’t get damaged.

The only thing I couldn’t get anywhere, including Akihabara, is the 6.5 MHz oscillator. Many people use a 6.5536 MHz crystal in their ZX80 builds (hmm, I’ve seen a number like that before!) because the 6.5 MHz ones are pretty rare. But even those didn’t seem to exact in my neck of the woods. I ordered a 6.5 MHz oscillator off Digikey (through Marutsu) and will probably have it in a few days. (By the way, the original ZX80 used a 6.5 MHz ceramic resonator, but these seem to be just as unavailable. But who knows, maybe I could have gotten a 6 MHz one and filed it down a bit to get it to 6.5 MHz? But crystals work even better, and are probably a little better for the environment because ceramic resonators are made of lead zirconate titanate. Note that they are exempted from RoHS regulations and can be labeled RoHS3-compliant as long as their leads do not contain lead, I guess.) So I’m using a Raspberry Pi Pico to generate the clock signal for now.

I had some problems with the clock though:

  • I forgot to solder on R20. This resistor is needed in the clock circuit. Fixed using jumper wires.
  • I changed the Pico’s output pin from GPIO0 to GPIO2 and failed to re-wire. I then suspected the Z80 was bad and tested it on a breadboard with the data pins tied to ground to emulate NOPs. Eventually figured out the problem (dur). It was difficult to figure out at first because the circuitry on the PCB picked up the output from GPIO2 as noise, and amplified it to produce something like a clock signal. Except that this was a very dirty clock signal (see image below)! It is interesting that the Z80 seems to detect that the clock is a little cuckoo, and while it works for a split second, it quickly calls it quits and the address bus freezes in a random state. That seems like a useful thing to be aware of! (Could of course be that this is not an intentional feature, or that it’s just seeing a HALT instruction or something.)
  • While testing the Z80 on the breadboard, I set the Pico’s clock output to 3.25 MHz. I then forgot to change it back to 6.5 MHz.
Temporary R20 fix
Dirty clock that quickly made the Z80 seize up.

RAM problem

The 2114 RAM chips originally came out of the Commodore PET that I fixed two years ago. I removed them from the PET because they were a little faulty, but kept them because they still mostly worked. Lucky! I had three, and hoped two of them would work well enough to at least get the ZX80 booted. Well, I picked a lucky one and an unlucky one. I didn’t see anything on the composite output. (Which at the time of writing is just a hole in the PCB. The ZX80 and early versions of the ZX81 don’t produce a back porch, but that problem is somehow fixed or alleviated or otherwise rendered irrelevant inside the modulator. The problem just manifests itself when you decide that the wire leading into the modulator is now going to be composite output. Back when I was looking at the ZX81 at the computer museum in Oume, I used a 555-based circuit that I got from here to fix the problem.)

Not knowing whether it’s a RAM problem or a serious problem with the PCB, I broke out my Pico-based logic analyzer. This time I didn’t bother adding resistor dividers because all Picos I have (I think I have four) turned out to be a least a little 5V-tolerant, and I wouldn’t be using the logic analyzer for a long time.

I needed to make some minor modifications to the logic analyzer code: the reset circuit in the ZX80 uses a 220K pull-up resistor (it’s very high value because it is also a 1 second (or so) RC delay circuit), and the Pico by default has pull downs active on its input GPIOs. These pull downs are much lower in value than 220K, effectively keeping reset asserted forever. (They are surprisingly low!) So the init code now looks like this:

    stdio_init_all();
    gpio_init_mask(ALL_REGULAR_GPIO_PINS);
    gpio_set_dir_masked(ALL_REGULAR_GPIO_PINS, GPIO_IN);
    gpio_disable_pulls(TRIGGER_PIN); // default is pull down; this pull down is much higher in value the the zx80's reset circuit's pull-up and therefore holds the cpu in reset

Of course, it would probably be even better if we disabled all pulls on all GPIOs. Anyway, running the logic analyzer, I quickly found a RAM problem with bit 4.

Here is the logic analyzer output with the problematic signals:

    115       6 0283 40
    116      16 0283 2a
    117       1 0f10 2a
    118      21 0f50 00
    119       6 0284 00
    120      27 0284 0a
    121       6 0285 0a
    122      27 0285 40
    123       8 000a 40
    124      25 000a 21
    125       8 000b 21
    126      25 000b 40

The first number is just the line number, the second number is the number of times the third and fourth numbers are repeated in the logic analyzer output (which is not in sync with the clock, but much faster). You can ignore everything that is less than around 10. In the above output, we are fetching and then executing the instruction at 0x0283~0x0285. Here is the relevant assembly source:

        ld iy,04000h            ;0273   fd 21 00 40     . ! . @ 
        ld hl,04028h            ;0277   21 28 40        ! ( @ 
        ld (04008h),hl          ;027a   22 08 40        " . @ 
        ld (hl),080h            ;027d   36 80   6 . 
        inc hl                  ;027f   23      # 
        ld (0400ah),hl          ;0280   22 0a 40        " . @ 
        ld hl,(0400ah)          ;0283   2a 0a 40        * . @

So we can see that we are putting 0x4028 into hl, and then incrementing hl, which means that hl should now be 0x4029. The instruction at 0x280~0x282 puts hl into 0x400a, and the instruction at 0x0283~0x0285 reads it back from the same address. So we should be putting 0x4029 in there (though not shown above, we are) and should be reading 0x4029 back. But the relevant parts of the logic analyzer output are like this:

    124      25 000a 21
    126      25 000b 40

Don’t worry about this showing 000a and 000b rather than 400a and 400b. I just don’t have the higher address lines connected to the logic analyzer. We’re reading back 0x4021! That’s missing the fourth bit. So I put in the other RAM chip and bam! I see a lovely 15.something KHz signal on the through hole that would normally be occupied by one of the modulator’s leads! (Well, actually I saw a 7.65 KHz signal because I forgot to change the Pico’s clock output back to 6.5 MHz, but while that was very puzzling for a bit, it was very easy to fix.)

The computer resting on top of that old laptop is the newer one!
It’s working! Never mind the Hitachi MB-H2 in the reflection.
I’m currently using two of these smartphone stylus…es to operate the keyboard on the PCB (one for shift and the other for every other key).

There’s a problem though: I’m not able to press keys like ” and many other shifted keys. I haven’t properly looked into that problem yet. However, I did do a quick internet search and found someone with the same problem:

For example, all of 1 to 0 keys work but shifted, only 1 and 0 work. Some other shifted keys don’t work but there doesn’t seem to be a consistent pattern on each row. The display still flickers when say shift 2 or shift 9 (or other combinations) is pressed, just nothing else happens.

https://sinclairzxworld.com/viewtopic.php?p=32893&sid=b108eefd0c71a1bedd148b3049db2673#p32893

But their problem was apparently caused by using super long wires on their keyboard. I’m not doing that as far as I know?!

As my EEPROM is currently mounted on a breadboard and my EEPROM is 512 KB while the ZX80’s ROM is just 4 KB, I have plenty of space left. So I put the ZX81 ROM image at address 0x10000, which means I just need to change the wire on the EEPROM’s A16 pin from 0V to 5V to change to that ROM. And it works too! Plus, the keyboard layout is different, which means I’m able to access the ” key and print out a proper message rather than just numbers:

To do

  • Solder crystal oscillator (when I get it) and R20, and maybe headphone/microphone jacks, etc.
  • Backporch generator
  • Make a proper ROM adapter?
  • Get all shifted keys to work
  • RAM expansion
  • Try to load some software
  • Make the keyboard more ergonomic
  • Maybe get some kind of case

KiCad / Gerber files

Here are the SVG and KiCad files, with the above mentioned shorting issues most likely fixed. Some important notes:

  • I do not have permission from Grant Searle nor from the copyright holders of the original ZX80 PCB to post anything like this, and if either of these parties asks me to take my files down, I intend to comply swiftly. (I’d just need to be sure that you are who you claim to be. I apologize in advance for any grievance caused and will apologize again if grievance is actually caused.)
  • I used Grant Searle’s replica foils as a base and traced them in Inkscape. I performed some manual and some automated tweaks. This version of the board is likely a less faithful replica of the original board than Grant Searle’s foils. (I don’t think anything’s shifted more than even 1 mm though. Though maybe the holes are quite different. I didn’t encounter any problems with the drill holes while soldering though.)
  • I haven’t tested (i.e. manufactured) this version of the KiCad files, I have only ordered one set of the ZX80 boards, with the shorted pins. This issue should be fixed now, and I hope I didn’t add any new issues.

Commodore SR-37 calculator “repair” and review

Before Commodore made computers, they made typewriters, and later calculators. I scored one such calculator, one that was listed as “non-functional”. The repair itself was very quick, as I had expected.

Repair

Honestly, it was just the power adapter. And some gunk in the keys. I’ll spare you the details on the gunk for today. Let’s see what’s wrong with the plug. Here’s the picture from the listing:

昭和の頃 赤色表示電卓 コモドール Commodore SR-37 難有品(^00WH10A_画像1
The pic from the listing.

Why would you bother to take a picture with the AC adapter cord connected to the calculator but the AC adapter not plugged in? Well, while I didn’t really think too much of it when I saw the listing, I quickly found the answer after it arrived here: it’s damn near impossible to get out of there!

Until I got out some pliers and turned it left and right while pulling a little bit for a while. The cable was really sticky, and I believe (though this is half a year ago already) pretty much glued the connector to the device.

So, did they use a bit of an unusual plug shape? Yes, they did! It looks a bit like a mono headphone connector, maybe a bit thicker? (That reminds me, the ZX81’s power connector probably uses something similar.) But anyway, my trusty 28 in 1 “28 in 3” plug set (https://www.amazon.co.jp/gp/product/B01NCN3P3B/) contained something that fit beautifully, and applying power at about 9V, the calculator sprang to life.

Original connector (top) and replacement connector
Glorious LED display
Device’s innards. I didn’t take it apart any further than this, but cleaned up the gunk. Apparently didn’t take an “after” picture though, so you’ll just have to trust me that it looked as clean as a whistle afterwards. Maybe.

Review

So you are thinking of buying a calculator, and hey, you’ve always wanted to show off how cool retro tech can be. Someone nearby is selling a retro calculator with an LED display. It looks fantastic! But will it be useful?

The Commodore SR-37 does pack a lot of functions, maybe not quite as many as a modern scientific calculator, but it’s not far away. (For example, you can’t easily convert degrees to radians.)

So let’s see how useful this thing is. I’ve thought of some expectations the modern calculator user may have that aren’t quite fulfilled by this device:

ExpectationTrue:
False:
Doesn’t use power when turned off using power switch on device.
Doesn’t use a lot of power. So for example not 1.4 W even when you make it display 8888888888888888888888.8.
Doesn’t turn off the display to conserve power after a short while.
Doesn’t take a long time to compute, e.g., exponentials or roots. Definitely not like 2 seconds!
Comes with a boring LCD rather than a beautiful LED display.
Table 1.1: table of broken expectations

The main problem in my opinion is that it uses a lot of power. Even if you turn it off, it’ll use some power (100 mA or so). At least my model didn’t come with a place to put in batteries so it’s external power only. The fact that this device is basically slightly on all the time (unless you unplug the cord or have a switch nearby), I’m a bit concerned for its longevity. So I don’t think I’ll use it much. :(

Besides, what I really need is a calculator that is really good at converting between bases, like kcalc. I’m not sure such a device even exists!

Displaying any image on an MSX, loading from a ROM

This article is basically just a note that I can come back to in case I forget some details.

The tool on this page (Japanese) takes an image file and adds dithering and stuff: https://nazo.main.jp/prog/retropc/gcmsx.html

The output is something that can be BLOAD’ed straight to VRAM memory using BLOAD’s S parameter. Didn’t even know that option existed!

This means we can easily convert this to a ROM by adding a loader that pokes everything into VRAM. We just need to get rid of the BSAVE header at the start (or ignore it in the loader), which looks like this according to http://www.faq.msxnet.org/suffix.html#BIN:

byte 0  : ID byte #FE
byte 1+2: start-address
byte 3+4: end-address
byte 5+6: execution-address

So we just cut off 8 bytes at the beginning, e.g. by doing:

tail -c +8 msx_20231111232339933.SC2 > foo.bin

And the loader in assembly (z80asm-flavor) could look like this:

SetVdpWrite: macro high low ; from http://map.grauw.nl/articles/vdp_tut.php
	ld a,low
	out (0x99),a
	ld a,high+0x40
	out (0x99),a
endm

vpoke: macro value
; 	ld a,value ; not needed in this implementation
	out (0x98),a
; 	nop ; nops not needed in this implementation
; 	nop
; 	nop
endm

	org 0x4000
	db "AB" ; magic number
	dw entry_point
	db 00,00,00,00,00,00,00,00,00,00,00,00 ; ignored

entry_point:
	ld a,2
	call 0x005f
	SetVdpWrite 00 00
	ld hl,data
loop:
	ld a,(hl)
	vpoke a
	inc hl
	ld a,h
	cp end>>8
	jr nz,loop
	ld a,l
	cp end&0xff
	jr nz,loop
inf:
	jr inf
data:
	incbin "foo.bin" ; read data from file foo.bin
end:
	ds 0x8000-$ ; fill remainder of 16 KB chunk with 0s

Of course, this approach is quite wasteful; we need almost 16 KB of memory to display any image, even if it’s mostly empty.

Using a USB-HID game controller on the MSX, using a Raspberry Pi Pico for signal conversion

There is very little code that I wrote myself in this project, but it’s the first time I’m looking at USB-HID on the protocol level and the end result is something mildly useful. So possibly worth a post? (Note: code is at the bottom of this post.)

All we’ll be doing here is modify an example from https://github.com/sekigon-gonnoc/Pico-PIO-USB, namely, the capture_hid_report example. You’ll need a good way to connect a USB-A port to your Raspberry Pi Pico. I’m using an old piece of hardware that was meant for use in desktop PCs to add USB ports on the back, internally connected to motherboard headers. (In the demo we’re using, USB D+ and D- are GPIO pins 0 and 1, respectively.)

Once you have your Pico flashed, open a serial terminal, and then connect a USB-HID gamepad. I’m using a fake PS3 controller. It looks very similar to a Sony PS3 controller but bears a “P3” mark instead of the PlayStation logo on the middle button. (I don’t know if it works with a real PS3.)
(I’m leaving out the wiring details here, but it’s all exactly as explained in the README.md in the repo.) I get the following messages when I plug in my controller. The “EP 0x81” messages are sent continuously.

Device 0 Connected
control in[complete]
Enumerating 054c:0268, class:0, address:1
control out[complete]
control in[complete]
control in[complete]
Manufacture:GASIA CORP.
control in[complete]
control in[complete]
Product:PLAYSTATION(R)3 Controller
control in[complete]
control in[complete]
control out[complete]
inum:0, altsetting:0, numep:2, iclass:3, isubclass:0, iprotcol:0, iface:0
        bcdHID:1.11, country:0, desc num:1, desc_type:34, desc_size:148
control out[error]
control in[complete]
                Report descriptor:05 01 09 04 a1 01 a1 02 85 01 75 08 95 01 15 00 26 ff 00 81 03 75 01 95 13 15 00 25 01 35 00 45 01 05 09 19 01 29 13 81 02 75 01 95 0d 06 00 ff 81 03 15 00 26 ff 00 05 01 09 01 a1 00 75 08  
                        epaddr:0x02, attr:3, size:64, interval:1
                        epaddr:0x81, attr:3, size:64, interval:1
...
054c:0268 EP 0x81:      01 00 00 00 00 00 80 80 80 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 ef 10 00 00 00 00 23 6d 77 01 80 02 00 02 00 01 80 02 00 
054c:0268 EP 0x81:      01 00 00 00 00 00 80 80 80 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 ef 10 00 00 00 00 23 6d 77 01 80 01 ff 01 ff 01 7f 01 ff 
054c:0268 EP 0x81:      01 00 00 00 00 00 80 80 80 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 ef 10 00 00 00 00 23 6d 77 01 80 01 fe 01 fe 01 7e 01 fe 
054c:0268 EP 0x81:      01 00 00 00 00 00 80 80 80 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 ef 10 00 00 00 00 23 6d 77 01 80 01 ff 01 ff 01 7f 01 ff 
054c:0268 EP 0x81:      01 00 00 00 00 00 80 80 80 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 ef 10 00 00 00 00 23 6d 77 01 80 02 00 02 00 01 80 02 00
...

You can also use Linux tools such as usbhid-dump to get reports, but for some reason they look quite different from what I get here. Don’t know if the driver is doing something (odd, or not so odd) or if the Pico is doing something odd. (It’s quite likely that there’s a driver in Linux that configures the controller automatically. On the Pico you have to press the P3 button every time after plugging in, on Linux it lights up the first player LED immediately.)

Decoding the USB HID report descriptor

First of all, we don’t actually need to do all this. Pressing buttons and looking at the output makes it quite obvious what we have to do. But for let’s edify outselves anyway. Here’s a great tutorial on USB HID report descriptors: https://eleccelerator.com/tutorial-about-usb-hid-report-descriptors/. The same guy has a tool on their website that allows us to quickly decode the descriptor: https://eleccelerator.com/usbdescreqparser/.

When we paste in our descriptor, we get the following output:

0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
0x09, 0x04,        // Usage (Joystick)
0xA1, 0x01,        // Collection (Application)
0xA1, 0x02,        //   Collection (Logical)
0x85, 0x01,        //     Report ID (1)
0x75, 0x08,        //     Report Size (8)
0x95, 0x01,        //     Report Count (1)
0x15, 0x00,        //     Logical Minimum (0)
0x26, 0xFF, 0x00,  //     Logical Maximum (255)
0x81, 0x03,        //     Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x01,        //     Report Size (1)
0x95, 0x13,        //     Report Count (19)
0x15, 0x00,        //     Logical Minimum (0)
0x25, 0x01,        //     Logical Maximum (1)
0x35, 0x00,        //     Physical Minimum (0)
0x45, 0x01,        //     Physical Maximum (1)
0x05, 0x09,        //     Usage Page (Button)
0x19, 0x01,        //     Usage Minimum (0x01)
0x29, 0x13,        //     Usage Maximum (0x13)
0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x01,        //     Report Size (1)
0x95, 0x0D,        //     Report Count (13)
0x06, 0x00, 0xFF,  //     Usage Page (Vendor Defined 0xFF00)
0x81, 0x03,        //     Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x15, 0x00,        //     Logical Minimum (0)
0x26, 0xFF, 0x00,  //     Logical Maximum (255)
0x05, 0x01,        //     Usage Page (Generic Desktop Ctrls)
0x09, 0x01,        //     Usage (Pointer)
0xA1, 0x00,        //     Collection (Physical)
0x75, 0x08,        //       Report Size (8)

// 63 bytes

This doesn’t look quite exactly the same as the examples in the tutorial. Below is my understanding, which may not be 100% correct, but makes sense to me at least. Let’s look at some output lines from the capture_hid_report example. The first line was:

054c:0268 EP 0x81:      01 00 00 00 00 00 80 80 80 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 ef 10 00 00 00 00 23 6d 77 01 80 02 00 02 00 01 80 02 00

First of all, all lines start with “054c:0268 EP 0x81”. That’s just added by the example code. Let’s go through the remaining numbers in the line (actually, just the bolded ones) and see how they relate to the descriptor we saw earlier.

01: report ID, same as the report ID defined in the descriptor.

00: this is the entire report that was described in the following extract from the descriptor, exactly 1 byte (8 bits):

0x75, 0x08,        //     Report Size (8)
0x95, 0x01,        //     Report Count (1)
0x15, 0x00,        //     Logical Minimum (0)
0x26, 0xFF, 0x00,  //     Logical Maximum (255)
0x81, 0x03,        //     Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)

00 00 01 00: the (two) reports referenced in the following parts of the descriptor:

0x75, 0x01,        //     Report Size (1)
0x95, 0x13, // Report Count (19)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x35, 0x00, // Physical Minimum (0)
0x45, 0x01, // Physical Maximum (1)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x13, // Usage Maximum (0x13)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x01, // Report Size (1)
0x95, 0x0D, // Report Count (13)
0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00)
0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)

This is actually two reports. One is 19 bits, the other is 13 bits. Together that’s exactly 32 bits. (This is similar to the three mouse buttons example in the tutorial, where we only have three buttons and therefore want to pad this to 8 bits.) Here, we have 19 buttons (though the controller only sports 16 physical buttons, unless I’m much mistaken), each of which is 1 bit, i.e., either pressed, or not. The remaining 13 bits are just to pad the report to make it easier to handle on the software side, or perhaps the padding is required somehow. (Looks like it is on Windows at least: https://stackoverflow.com/questions/65846159/usb-hid-report-descriptor-multiple-reports.)

And indeed, these four bytes (well, the first 19, er, 16 bits) do change when buttons are pressed. This is basically all we need to figure out how to use this controller with an MSX. We just need to figure out which button is responsible for which bit in the third and fourth bytes. (Unless we also wanted to translate analog stick input.)

What about the rest of the bytes? I don’t know how they relate to our descriptor above, but pressing buttons and moving the analog sticks makes it pretty obvious what they mean. We can see that the analog sticks are two bytes each, and there’s another byte for each button, and the value that comes up depends on how hard the button was pressed. I never knew that the △□○☓ buttons were pressure-sensitive on the PS3! Anyway, perhaps it’s possible that our descriptor is incomplete, as the frame they are transmitted in is limited to 64 (or maybe 63?) bytes?

So without going into any hardware details yet, here’s how we could modify the example code to translate our button presses to electric pulses on a GPIO pin. Here’s the part of the code that we’ll replace:

if (len > 0) {
  printf("%04x:%04x EP 0x%02x:\t", device->vid, device->pid,
        ep->ep_num);
  for (int i = 0; i < len; i++) {
    printf("%02x ", temp[i]);
  }
  printf("\n");
}

And here’s some of the code we could maybe replace the above block with. Note that this doesn’t actually consider the hardware details of the actual MSX interface yet, it’s just an example, so don’t copy this code. The actual code is in the last section of this blog post.

if (len > 0) {
  if (temp[0] == GAMEPAD_REPORT_ID) { // GAMEPAD_REPORT_ID is 1 in our case
    uint16_t button_state = (temp[2] << 8) | temp[3];
    if (button_state & BUTTON_UP) {
      gpio_put(BUTTON_UP_PIN, 1);
    } else {
      gpio_put(BUTTON_UP_PIN, 0);
    }
    if (button_state & BUTTON_DOWN) {
...
  }
}

My controller’s button mappings are as follows:

#define BUTTON_UP 0x1000
#define BUTTON_DOWN 0x4000
#define BUTTON_LEFT 0x8000
#define BUTTON_RIGHT 0x2000
#define BUTTON_A 0x40 // X or O, can't remember
#define BUTTON_B 0x20 // X or O, can't remember

MSX interface

Good news: the MSX joystick ports come with 5V and GND pins. According to https://www.msx.org/wiki/General_Purpose_port, we can draw up to 50 mA from these pins. Is that enough for the Pico and a controller? It is for mine! According to my measurements, the Pico + fake PS3 controller together draw about 40 mA. (Instrument’s display precision is 10 mA, so it could be up to 45 mA, or more if the instrument is inaccurate.) Note that many modern controllers (including real PS3 controllers) contain a battery, and will attempt to charge it. That will most likely take the current way above 50 mA.
(Note: I don’t think that drawing slightly more than 50 mA would be a huge problem at least on my MSX, which I’ve disassembled and reassembled a couple times.)

Slightly bad news (1): pressing buttons on MSX joysticks connects pins to pin 8, which is not GND. Pin 8 is “strobe” and can be GND or 5V. (Button pins are normally pulled high.)

Slightly bad news (2): on the MSX side, the joystick ports are actually GPIO ports and can be configured for both input (normal) and output! We don’t want the Pico to output when the MSX’s PSG is outputting too!

1: addressing this in software might be possible, albeit with a (very) short time lag. I don’t really know anything about this feature and have no easy way to test a software-based solution with MSX software that actually uses this feature.
2: I don’t think regular MSX joysticks worry about this. Some MSX systems may have safeguards in place.

Some measurements and wiring details

Using my multimeter’s rarely used ammeter mode, with the probes between STROBE and any button pin, I see a current of ~0.7 mA. (Two button pins, and the ammeter shows 1.5 mA. It probably goes up linearly the more buttons you press.) That’s quite a lot by modern standards!

On my MSX, there are two 74LS157 chips that implement joystick selection. The 74LS157’s outputs are directly connected to the PSG’s inputs. (Only one joystick’s state is visible to software at a time; a PSG register change is required to switch to the other one.) If we change the PSG’s I/O port’s direction, I think we’ll be pitting the 74LS157’s outputs against the PSG’s outputs. Doesn’t sound so great, eh.
Except, the 74LS157 has an \ENABLE pin! When the \ENABLE pin on a 74LS157 is disabled, the outputs will be high-Z, potentially allowing a signal coming from the PSG to make its way somewhere. Is there such functionality on the MB-H2? Answer after some probing: no. \ENABLE on these two chips is tied to GND.

While most button pins have 10K pull-up resistors (some seem to have 3.3K pull-up resistors), the STROBE pins are connected directly to PSG pins 8 and 9.

Deciding how to hook up the Pico to the joystick port

In real joysticks, when you press a button, you short the STROBE pin to the button pin, and STROBE can be HIGH or LOW. (When STROBE is HIGH, we short HIGH to HIGH, and nothing happens. Since on my MSX, as discussed above, the button pins are connected to 74LS157 inputs, they should always be pulled high, and never go low during device operation.) In real joysticks, when nothing is pressed, there is no connection, period. So that’s more like a tri-state affair, so instead of producing a HIGH level when nothing is pressed, we should set our Pico’s GPIO to high-Z (which we can do by setting it to INPUT).

Armed with the measurements above, I think we can hook up the Pico directly (let’s ignore 5V vs 3.3V for now), producing a LOW level when a button is pressed. We’ll have the Pico’s GPIO pin sink a little bit of current, but not too much. Even if all 6 buttons are pressed, the Pico shouldn’t sink more than around 4.5 mA across multiple pins. That’s okay, really. Even if we decided to make the Pico interface with two controllers, that’s still comfortably under our limit.

Now we could start thinking about reducing the 5V on the button pins to 3.3V on the Pico’s GPIO pins. But according to many accounts, 5V is okay if it’s sufficiently current-limited, which it is, in our case. So let’s ignore this for now.

Other MSX machines

(I will probably expand this section at some point.)

YIS-503

The YIS-503 (schematics, look at the circuits near the JOY1 and JOY2 connectors on the left) has 22K pull-up resistors and an MSX Engine. I doubt that MSX Engine chips (which have the PSG integrated) can be configured to destruct themselves.

Kuninet’s homebrew MSX

10k pull-up resistors. The rest of the wiring looks exactly like on the Hitachi MB-H2 as far as I can tell.

So does it actually work?

Yes. Here’s a pic of my trusty Hitachi MB-H2 MSX running its built-in sketch program, controlled through the fake PS3 controller. (Sorry, the computer’s still open from the probing I did earlier.)

The USB port is a PC part that I found at my local Hard Off. The RS232 cable is also from Hard Off. In fact, the fake PS3 controller is also from Hard Off! The USB cable is from my own cable collection.

Totally off-topic, but there’s a spot in this pic that has been cleaned up using my Pixel phone’s magic eraser. Can you see where it is? (I didn’t touch it up in an image editor afterwards.)

Pacman <3

Problems

Plugging our contraption into the joystick port while the MSX is running crashes the machine! The screen goes very slightly dark for a split second too. Probably in-rush current. I’m pretty sure I blew my multimeter’s (200 mA) fuse trying to measure the current. Laff. (Having the controller already plugged in before powering on the computer works fine.)

Source code

Replace your Pico-PIO-USB/examples/capture_hid_report.c with the following code, (re-)make, flash, and you’re set. (I’m basing my modifications on git commit d00a10a8c425d0d40f81b87169102944b01f3bb3.)

#include <stdio.h>
#include <string.h>

#include "pico/stdlib.h"
#include "pico/multicore.h"
#include "pico/bootrom.h"

#include "pio_usb.h"

#define GAMEPAD_REPORT_ID 1

// 0x1: L2
// 0x2: R2
// 0x4: L1
// 0x8: R1
// 0x10: Triangle
// 0x20: Circle
// 0x40: X
// 0x80: Square?
// 0x100: ?
// 0x200: ?
// 0x400: R3
// 0x800: Start
// 0x1000: Up
// 0x2000: Right
// 0x4000: Down?
// 0x8000: Left?

#define BUTTON_UP 0x1000
#define BUTTON_DOWN 0x4000
#define BUTTON_LEFT 0x8000
#define BUTTON_RIGHT 0x2000
#define BUTTON_A 0x20
#define BUTTON_B 0x40
#define BUTTON_A_ALT 0x10
#define BUTTON_B_ALT 0x80

#define BUTTON_UP_PIN 16
#define BUTTON_DOWN_PIN 17
#define BUTTON_LEFT_PIN 18
#define BUTTON_RIGHT_PIN 19
#define BUTTON_A_PIN 20
#define BUTTON_B_PIN 21

static usb_device_t *usb_device = NULL;

void core1_main() {
  sleep_ms(10);

  // To run USB SOF interrupt in core1, create alarm pool in core1.
  static pio_usb_configuration_t config = PIO_USB_DEFAULT_CONFIG;
  config.alarm_pool = (void*)alarm_pool_create(2, 1);
  usb_device = pio_usb_host_init(&config);

  //// Call pio_usb_host_add_port to use multi port
  // const uint8_t pin_dp2 = 8;
  // pio_usb_host_add_port(pin_dp2);

  while (true) {
    pio_usb_host_task();
  }
}

static void gpio_setup()
{
  gpio_init(BUTTON_UP_PIN);
  gpio_init(BUTTON_DOWN_PIN);
  gpio_init(BUTTON_LEFT_PIN);
  gpio_init(BUTTON_RIGHT_PIN);
  gpio_init(BUTTON_A_PIN);
  gpio_init(BUTTON_B_PIN);

  gpio_set_dir(BUTTON_UP_PIN, GPIO_IN);
  gpio_set_dir(BUTTON_DOWN_PIN, GPIO_IN);
  gpio_set_dir(BUTTON_LEFT_PIN, GPIO_IN);
  gpio_set_dir(BUTTON_RIGHT_PIN, GPIO_IN);
  gpio_set_dir(BUTTON_A_PIN, GPIO_IN);
  gpio_set_dir(BUTTON_B_PIN, GPIO_IN);

  gpio_put(BUTTON_UP_PIN, 0);
  gpio_put(BUTTON_DOWN_PIN, 0);
  gpio_put(BUTTON_LEFT_PIN, 0);
  gpio_put(BUTTON_RIGHT_PIN, 0);
  gpio_put(BUTTON_A_PIN, 0);
  gpio_put(BUTTON_B_PIN, 0);
}

int main() {
  // default 125MHz is not appropreate. Sysclock should be multiple of 12MHz.
  set_sys_clock_khz(120000, true);

  stdio_init_all();
  printf("hello!");

  sleep_ms(10);

  multicore_reset_core1();
  // all USB task run in core1
  multicore_launch_core1(core1_main);

  gpio_setup();

  while (true) {
    if (usb_device != NULL) {
      for (int dev_idx = 0; dev_idx < PIO_USB_DEVICE_CNT; dev_idx++) {
        usb_device_t *device = &usb_device[dev_idx];
        if (!device->connected) {
          continue;
        }

        // Print received packet to EPs
        for (int ep_idx = 0; ep_idx < PIO_USB_DEV_EP_CNT; ep_idx++) {
          endpoint_t *ep = pio_usb_get_endpoint(device, ep_idx);

          if (ep == NULL) {
            break;
          }

          uint8_t temp[64];
          int len = pio_usb_get_in_data(ep, temp, sizeof(temp));

          if (len > 0) {
            if (temp[0] == GAMEPAD_REPORT_ID) {
              uint16_t button_state = temp[2] << 8 | temp[3];
              if (button_state & BUTTON_UP) {
                gpio_put(BUTTON_UP_PIN, 0);
                gpio_set_dir(BUTTON_UP_PIN, GPIO_OUT);
                printf("BUTTON_UP_PIN\n");
              } else {
                gpio_set_dir(BUTTON_UP_PIN, GPIO_IN);
              }

              if (button_state & BUTTON_DOWN) {
                gpio_put(BUTTON_DOWN_PIN, 0);
                gpio_set_dir(BUTTON_DOWN_PIN, GPIO_OUT);
                printf("BUTTON_DOWN_PIN\n");
              } else {
                gpio_set_dir(BUTTON_DOWN_PIN, GPIO_IN);
              }

              if (button_state & BUTTON_LEFT) {
                gpio_put(BUTTON_LEFT_PIN, 0);
                gpio_set_dir(BUTTON_LEFT_PIN, GPIO_OUT);
                printf("BUTTON_LEFT_PIN\n");
              } else {
                gpio_set_dir(BUTTON_LEFT_PIN, GPIO_IN);
              }

              if (button_state & BUTTON_RIGHT) {
                gpio_put(BUTTON_RIGHT_PIN, 0);
                gpio_set_dir(BUTTON_RIGHT_PIN, GPIO_OUT);
                printf("BUTTON_RIGHT_PIN\n");
              } else {
                gpio_set_dir(BUTTON_RIGHT_PIN, GPIO_IN);
              }

              if (button_state & BUTTON_A || button_state & BUTTON_A_ALT) {
                gpio_put(BUTTON_A_PIN, 0);
                gpio_set_dir(BUTTON_A_PIN, GPIO_OUT);
                printf("BUTTON_A_PIN\n");
              } else {
                gpio_set_dir(BUTTON_A_PIN, GPIO_IN);
              }

              if (button_state & BUTTON_B || button_state & BUTTON_B_ALT) {
                gpio_put(BUTTON_B_PIN, 0);
                gpio_set_dir(BUTTON_B_PIN, GPIO_OUT);
                printf("BUTTON_B_PIN\n");
              } else {
                gpio_set_dir(BUTTON_B_PIN, GPIO_IN);
              }
            }
          }
        }
      }
    }
    stdio_flush();
    sleep_us(10);
  }
}

Another Hitachi MB-H2 MSX repair

Introduction and conclusion

I bought another Hitachi H2 MSX last year, mostly because I wanted the manual, which I’ve scanned. Unfortunately for my free time but fortunately for my, um, education in retro computing, this computer had issues with its video RAM. Often, the computer would boot up with a garbled screen. Resetting after a couple minutes would usually fix the issue. The video RAM is made by Toshiba, and is called TMM416P-2 (also marked 4116-2). If you have this memory, I’d recommend you look out for issues, because all eight ICs had the same issue, namely: crazy-ass noise on the -5V line. (How much noise is “crazy-ass” noise? In this case, it’s +-3V.) The noise sort of comes and goes, or at least gets stronger and weaker, randomly, which made it too hard for me to find a combination of capacitors to tame it. (Though it’s more likely to be present after turning the computer on after a long while.) I ended up socketing them all, replacing one that unfortunately died during the very professional desoldering process, and added 103 ceramic capacitors to (almost) every one, between the -5 and GND pins, which seems to have a slight positive effect. (The bottom part of the case has a hook that requires some clearance and prevents two of the chips from getting their capacitor.) I also replaced the zener diode with a 7905, which fit perfectly after bending the legs a little bit.

Details

The -5V rail for the 4116 VRAM chips is generated using a zener diode. Replacing this, or the capacitor on the rail, unfortunately didn’t have any effect. Hmm, odd!

Next, I decided to desolder the -5V pin on the first 4116 IC, and drive it using my own known good -5V supply (using a standard 7905 regulator). Result: noise both on the first chip and all the others. Hmm, odd!

Next, I did this for the rest of the 4116 ICs, and was able to see that each and every one generates noise.

Next, I decided to desolder all of them and individually test them on my 4116 tester. (They still produced the noise while in the tester.) I decided to desolder all of them because the H2 seemed to support 4416 ICs for the video RAM, and I happened to have some of those that were waiting to be put to use. I.e., there are holes of the right size, right next to the VDP, and the silkscreen on those holes says “TMS4416”. ;)

Well, today’s lesson is, do not necessarily trust the silkscreen. The TMS9928A doesn’t even support 4416 VRAM! The holes where the data pins go didn’t even have any traces on them.
The TMS9928A can be made to support 4416 RAM using a custom circuit, though. Maybe I should have implemented this circuit. I even bought the two required parts! But then decided against it for complexity management reasons.

Unfortunately, expecting to be able to use the 4416 slots, I had desoldered the original VRAM ICs in a rather brutish manner, losing vias and traces in the process, which meant that I needed to add a bunch of bodge wires to get them to work again. At least the bodge wires aren’t too complex to figure out, if at some point one of them decides to become loose again. I ended up keeping the original, noisy, RAM chips. But since they’re now all socketed, it shouldn’t be too hard to replace them at some point, if necessary.

Pictures

Garbled screen
Hitachi MB-H2 logic board from above. The misleading silkscreen is in the top left, above the TMS9928ANL VDP.
Example with a lot of noise
And an example with a lot less. (This picture is from six months ago. It’s entirely possible that I had extra capacitors for this shot.)
TMM416P-2 noise closeup. Intensity varies. Here it’s about 3V peak-to-peak.
TMM416P-2 noise closeup, one more example.

After

Noise after “completion” of this repair (note: using AC coupling here). Note that noise intensity has always been a bit random, so I this can’t be taken as proof that adding 103 capacitors to each chip is going to help in any case, and I am not too interested in performing rigorous testing. Anecdotally, I haven’t seen any garbled screens yet after the “repair”!
Noise closeup (note: using AC coupling here)
Check out this 7905’s limbo dance moves

Before looking at the picture of the bodge wires below, please keep in mind that it is rude to stare.

Ahem

Testing 4164/4116 DRAM ICs in-circuit/live with a Raspberry Pi Pico, without removing them (WIP)

Hi! My sabbatical ended and I’ve been working again since two months ago. Boo. However there’s this thing I just wanted to get off my chest, so I spent a few hours that I did not really have and wrote some code and this blog post about it!

Last year, I made a 4164/4116 DRAM tester for the Raspberry Pi Pico, which works just as you would expect, you program the Pico, place it on a breadboard, add some wires and something to drop the 5V to 3.3V for the Q output, place the 4164 or 4116 chip you’d like to test on the breadboard, connect a USB cable from the Pico to a computer, and look at the terminal output (or just the on-board LED). This is useful if you have already extracted a 4164 chip that you have determined to be bad. (I have written previously how you could determine whether a 4164 chip is bad, here and here.)

I previously made a “live” chip tester for the Arduino, capable of checking whether simple logic chips are doing what they’re supposed to be doing in a powered on system. I used the Arduino rather than the Pico because the Arduino is 5V tolerant. In this post, we’re not doing simple logic chips, but DRAM chips. And since we need to be super-fast, we’re using a Pico.

Executive summary

We “allocate” 64 KB of RAM on the Pico. We need to read in two 8-bit addresses, and combine them to a 16-bit address. If a write is being attempted, we write the same bit value into the appropriate address of Pico’s RAM. If a read is being attempted, we check if the DRAM’s output is the same as what we have in the Pico’s RAM. (We could also use 64 Kb of RAM on the Pico at a minimum, but as we’ll see in the next section we do not really have a lot of time for such shenanigans.)

To use this software, having an IC test clip would probably be very beneficial. I use these: https://www.kandh.com.tw/ic-test-clips-ic-test-clips.html. These are available in Akihabara from Akizuki: https://akizukidenshi.com/catalog/g/gC-04753/. If they aren’t available in your market, perhaps these somewhat expensive test clips from 3M might be more available: https://www.digikey.jp/ja/products/detail/3m/923700/3852.

Note: to use this software, you need to at least mostly know what you’re doing.

Example usage
Test clip close-up
Wiring closeup

One note on hooking up the Pico directly to 5V components, as seen in the above pictures

Not guaranteed to not fry your Pico (note the double negation), do this at your own risk. Your Pico will possibly also draw more current than a normal TTL chip when driven above 3.6V or so, which could easily damage your precious hardware! (The current on the address pins will likely be supplied by a pair of 74LS157 chips, on the Q pins by the RAM chip, and the current on the RAM’s data in pin by the CPU or any other. \RAS and \CAS probably by custom logic chips.) Use a 74HCT245 between the Pico and the device you’d like to test.

Caveat 1

The Pico is very fast when compared to an 8-bit computer from the 1980s, but 4164 transition times are extremely fast too. If you look at a timing diagram for the 4164 (which you will find in any 4164 datasheet), you will notice that all transition times listed are on the order of <ten, tens, or low hundreds of nanoseconds. Most 4164 chips have a -20, -15, -12, or -10 suffix in their part number. This indicates the minimum allowable number of nanoseconds × 10 for the sum of all transitions. (If the DRAM is driven faster, it probably won’t work correctly. However, if it’s driven slower, most things will generally work out, though if your system e.g. reads the DRAM’s output too slowly it might be too late and not work out.)

The stock Pico runs at 125 MHz, which means that one CPU cycle is 8 ns. From hearsay, you can probably overclock any Pico to 200 MHz (clock cycles are 5 ns), and many people report that their Pico runs fine even at 400 MHz (clock cycles are 2.5 ns). For -15 DRAMs, you have 150/8 = 18.75 CPU cycles per transition if the DRAM is driven at its max speed. (Note: it isn’t on MSX machines, at least.) 18 CPU cycles isn’t a lot. Remember, we need to convert two 8-bit addresses to a 16-bit address, and then check if the newly read value matches our previously recorded value. Is that doable in 18 CPU cycles? I don’t think so, but I’m not an ARM assembly expert.

So, did I get it to work? Well… sort of but not quite.

Caveat 2

Note that there are many failure modes for DRAM chips. For example, if the chip gets super-hot within a few seconds, it’s probably shorted. I’d expect there to be a very low resistance between ground and another pin. Before hooking up the “live” tester I’m going to explain on this page, check for that kind of stuff. I would not recommend using the live tester on a chip that gets super-hot within seconds. You could risk melting your connectors, and if the short is not between VCC and GND, potentially also risk your Pico due to excessive current on a pin driven by the Pico.

Caveat 3

Untested with 4116 chips, only tested with my MSX’s main RAM.

Current status

The live tester successfully verifies that a TMS4164-15 DRAM chip under test in my stand-alone 4164 RAM tester is outputting the correct values. (There is no reason why it shouldn’t work with a 4116 chip. The tester certainly does! You just need to re-wire slightly. Also, 5V on the Pico, yeah, it doesn’t seem to “explode immediately.” But -5V or 12V? You’d better leave those pins unconnected!)

On a real system (my trusty Hitachi MB-H2 MSX) with TMS4164-15NL DRAM chips, the live tester manages just fine from power up, up until the first ~11000 comparisons (which is a split second), but at some point reads a 0 when it should have been a 1, and prints an error. Printing errors takes a long time at 115200 bps, so we go completely off the rails once we’ve encountered the first error. (That’s slightly configurable though, see “Lnobs” section for details.)

However, in the live tester’s “DEBUG” mode, it just collects a lot of samples into memory, and prints them out when the sample memory is full. Using a simple script (the Perl script included in the repo), I can then verify that all the samples check out. Note that the DEBUG code also prints out who many times it had to wait until it got data from the PIO. The answer is 0 times every time, which means that we’re too slow or almost too slow. (Sometimes there is a handful of mismatches, I’ll look into those at some point. Could be that we were just too slow, or the 5V is messing with the system ;D)

There’s a lot that could be improved, hence the “WIP” attribute in title. The first obvious improvement would be to try a little harder in the non-debug mode. The Pico has two CPU cores, and we’re only using one. We could attain more throughput by running according to the following scheme:

Core 1:
Wait for sample 1
Tell core 2 to wait for sample 2
Process sample 1
Wait for sample 3
Tell core 2 to wait for sample 4
Process sample 3

Core 2:
Wait for instructions from CPU 1
Wait for sample 2
Process sample 2
Wait for instructions from CPU 1
Wait for sample 4
Process sample 2

Another potential optimization would be to write the processing code in ARM assembly. (My experience with ARM assembly is mostly read-only, so not sure how much better I can get without spending way too much effort.)

Also I haven’t tried overclocking yet. Probably should!

Some more technical details

We use two PIO state machines. One waits for RAS high→low (“RAS SM”, and the other one waits for CAS high→low (“CAS SM”, which comes after the RAS transition.

Not all RAS transitions are followed by CAS transitions. For example, refresh is mostly RAS-only. In addition, though perhaps not used on the MSX(?), RAS transitions may be followed by multiple CAS transitions.

In the C code, we wait for events on the CAS SM, and then read from both the RAS SM’s FIFO and the CAS SM’s FIFO. In the PIO code, the CAS SM tells the RAS SM whether to push its address or not. (We could alternatively (maybe) always push and have the CPU make sure the FIFO never gets full, but my experiments in that regard didn’t go that well.)

There are a lot of defines that change the way the system works.

Knobs

  • Setting PRINT_ERROR_THRESHOLD to something above 0 only starts printing errors after encountering that many errors.
  • CORRECT_ERRORS causes the Pico’s memory to be updated when we encounter a mismatch
  • VERBOSE_STATUS_LEDS causes the Pico to perform GPIO writes at GPIO16+ (or so, I recommend you check the source to find the exact GPIO pin number) to indicate whether we’re reading or writing. This isn’t very beneficial performance-wise.
  • SWAP_RAS_AND_CAS_ADDRESSES: my Hitachi MB-H2 MSX applies the CPU’s A0-A7 to the RAM pins at RAS time, and A8-A15 at CAS time. When thinking “rows” and “columns”, most people would probably assume that “rows” use the more significant bits, but that is not necessarily the case, and it doesn’t matter. When operating in DEBUG mode, you’ll see accesses that are mostly linear if this define is set correctly. Otherwise each access will be 256 apart.

The code is at https://github.com/qiqitori/pico_live_chip_tester.

So… is this likely to find a fault?

I haven’t seen any DRAM chip failures (except on YouTube) where only some addresses were broken and the chip appeared to work otherwise. (SRAM chips are different story. I’m sort of planning on doing an SRAM tester too, but probably not too soon.) Most DRAM chips I’ve seen are an all-or-nothing affair. For all-or-nothing affairs, this chip tester is very likely to find the problem immediately, especially if you compare all 8 chips and only one is weird. For hypothetical chips with just a single address problem (or perhaps, a single broken row), either it’ll be difficult with the code not 100% working right now, or it might take several attempts and statistics.

Playing .psg tunes (or even .asc/.pt3/… tunes) on the MSX (part 2)

(I do not recommend watching this demo on a smartphone. It’s quite flashy and exacerbated my headache. Also, the WebMSX code will ask you to go fullscreen, but I don’t think you can start the demo without going fullscreen and then back again.)

In part 1, we constructed a 48 KB ROM to play a tune called “Popsa 2”. We didn’t apply any real compression algorithms but implemented a set of scripts to find repeated sections in a binary file and added an instruction to the .psg file format to “call” repeated sections. Using real compression algorithms we could achieve much better compression, and each tune would just occupy a couple KBs. Using our method, we _just_ manage to fit the tune into a single cartridge. Popsa 2 fit into 48 KB, and the tune we’re going to do today is going to require a 64 KB ROM. If the previous track didn’t quite do it for you, I think it might be worth giving this one a chance. It’s a very complex piece of wonder-inducing music in my opinion. (Press the power button and in the menu that pops up, choose “Power” to boot the ROM.)

64 KB ROMs still require a header at 0x4000 or 0x8000. This means that we need to add a header and some entrypoint code to set up the slots right in the middle of our data. That’s inconvenient, but I didn’t feel like changing the structure of the program, so I just added one check each at the beginning and end of the main loop to see if the HL register has gone above a certain value. If yes: before the main loop, it adds an offset; after the main loop, it subtracts the same offset again. This way, we don’t have to do anything too complex when jumping to a previous section of the track.

Mic – Dreamless is #2 on the top 300 of ZX Spectrum music. There’s a bunch of other nice stuff on that list!

In part 1, I mentioned a problem in WebMSX that prevented the 48 KB ROM from working. 64 KB ROMs are not affected by this problem. The WebMSX player at the top of this article page plays the ROM linked to above. The ROM also works on real hardware (the Hitachi MB-H2 MSX1 I repaired a while ago).

Aside: disabling WebMSX’ auto-scroll

In the unlikely event that you have read this blog’s front page sometime in the last few months, you might have noticed that it scrolled automatically to this WebMSX player, even though this post is now very much not the newest post on this blog! I only noticed this a short while ago and decided to fix it, because it’s quite annoying. The below code snippets are taken from the WebMSX commit with the tag “v6.0.4”. Older or newer versions may look different.

All you need to do is remove the “this.focus()” line in the powerOn function in CanvasDisplay.js:

    this.powerOn = function() {
        this.setDefaults();
        updateLogo();
        document.documentElement.classList.add("wmsx-started");
        setPageVisibilityHandling();
        this.focus(); // <-- this is the line you need to remove or comment out
        if (WMSXFullScreenSetup.shouldStartInFullScreen()) {
            setFullscreenState(true);
            if (FULLSCREEN_MODE !== 2 & isMobileDevice) setEnterFullscreenByAPIOnFirstTouch();       // Not if mode = 2 (Windowed)
        }
    };

If you prefer to just edit the minified version, search for the call to setPageVisibilityHandling() and then edit out the “this.focus(),” bit.

Before:

documentElement.classList.add("wmsx-started"),setPageVisibilityHandling(),this.focus(),WMSXFullScreenSetup.shouldStar

After:

documentElement.classList.add("wmsx-started"),setPageVisibilityHandling(),WMSXFullScreenSetup.shouldStartInFullScreen