Sony HB-10 MSX repair using a Raspberry Pi Pico-powered breadboard logic analyzer

This Sony HB-10 was kept inside its original shrink-wrapped box for around 35 years. (Which is longer than I’ve lived.) It’s incredibly clean. There are two clips on the joystick side that make it hard to open. You can imagine how nervous I was about fumbling about with a practically pristine red box, not knowing where the clips are. Fortunately, I found a YouTube video that showed where they are. They are right underneath where the green tape is in many of the images below. I was using the green tape to block the clips so I could half-close the computer while I wasn’t working on it, without again having to spend ages fighting those silly clips when getting back to the computer.

First of all, some board pics in case anyone needs them:

Cleanest retro computer ever
So reflective :O

None of the chips are socketed. So let’s spy through the oscilloscope and see some worrying things:

Fuzzy IO pin
Suspicious address signals

Anyway, these signals aren’t completely out of spec. (And indeed, at least the fuzzy IO turned out to be normal. I.e., this problem didn’t go away after fixing the computer. I didn’t check for the steppy address lines again after getting the computer to work, but I’d hazard a guess that they’re still there. (Update 2022/09/27: I also fixed an HB-11 a while after that, and it had the same fuzzy IO signal on the pin. Most likely nothing to worry about!)

Anyway, what we’ll do today is… build a 26-channel logic analyzer using a Raspberry Pi Pico! And a large handful of resistors to reduce the 5V signals to 3.3V. We connect the logic analyzer to the ROM chip. The ROM chip’s address and data lines are directly connected to the CPU’s address and data lines, and the RAM data lines. Except there’s no A15, but that’s probably all right for now. Using this, we may be able to figure out what’s going on. (Foreshadowing)

Resistors used for the resistor dividers: 10k, 20k on one side (gets us 3.333V) and 4.7k, 6.8k on the other (gets us 2.957V). Ran out of the higher valued ones. Higher values are better, as you’ll draw less current from the CPU (i.e., be less of a burden). I think you can go pretty high, but I’m sticking with what I’ve used before here.

What is that awesome connector? It’s this: https://akizukidenshi.com/catalog/g/gC-04756/.

And this is the program we’ll run on the Pico:

#include <stdio.h>
#include "pico/stdlib.h"

#define ALL_REGULAR_GPIO_PINS 0b00011100011111111111111111111111
#define LOGIC_BUFFER_LEN 62660

#define TRIGGER_PIN 28

uint32_t logic_buffer[LOGIC_BUFFER_LEN] = { 0 };

int main() {
    int i = 0;
    stdio_init_all();
    gpio_init_mask(ALL_REGULAR_GPIO_PINS);
    gpio_init(PICO_DEFAULT_LED_PIN);
    gpio_set_dir_masked(ALL_REGULAR_GPIO_PINS, GPIO_IN);
    gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT);

    // wait until /dev/ttyACM0 device is ready on host
    for (i = 0; i < 10; i++) {
        gpio_put(PICO_DEFAULT_LED_PIN, i%2==0);
        sleep_ms(500);
    }
    gpio_put(PICO_DEFAULT_LED_PIN, 1);
    printf("Logic analyzer ready, waiting for trigger\n");
    while (gpio_get(TRIGGER_PIN) == 0);
    for (i = 0; i < LOGIC_BUFFER_LEN; i++) {
        logic_buffer[i] = gpio_get_all() & ALL_REGULAR_GPIO_PINS;
    }
    printf("Done recording");
    for (i = 0; i < LOGIC_BUFFER_LEN; i++) {
        printf("%04x %04x\n", i, logic_buffer[i]);
    }
    printf("Done printing\n");
}

The TRIGGER_PIN is connected to the RESET line of the Z80. The while(gpio_get(TRIGGER_PIN) == 0) waits for this line to go high. (It’s active-low.) Then we just have a for loop that fills the logic_buffer array with the contents of the GPIO pins that we are using. (I.e., all 26 “normal” GPIO pins.)

Then there’s another for loop, which prints out the contents of the buffer.

Let’s avoid spaghetti wiring, and instead prioritize connection convenience. Which unfortunately means that the GPIO pin numbers and address/data line numbers will be pretty much shuffled now. Which means that we need something to decode the output of the logic analyzer to tell us the contents of the address bus and the data bus. And this is a quick and dirty Perl program to do that. Input is on standard input. The bold lines mean that A14 is on GPIO5, A13 on GPIO4, A12 on GPIO11, etc. D7 is on GPIO22, D6 is on GPIO21, etc.

In the unlikely event that you are reading this, and in the unlikelier event that you are thinking of building this thing, I strongly recommend you connect everything in a way that is convenient for you, and fix the values in these bold lines.

#!/usr/bin/perl

# logic_analyzer_raw2address.pl

@address_positions_from_a14 = (5, 4, 11, 1, 27, 2, 3, 12, 13, 14, 15, 10, 6, 7, 8);
@data_positions_from_d7 = (22, 21, 20, 19, 18, 17, 16, 9);

while (<>) {
    $address = 0;
    $data = 0;
    $num = hex($_);
    $current_address_pin = 14;
    foreach (@address_positions_from_a14) {
        if ($num & (1 << ($_))) {
            $address |= (1 << $current_address_pin);
        }
        $current_address_pin--;
    }
    $current_data_pin = 7;
    foreach (@data_positions_from_d7) {
        if ($num & (1 << ($_))) {
            $data |= (1 << $current_data_pin);
        }
        $current_data_pin--;
    }
    printf ("%04x %02x\n", $address, $data);
}

And the other way round, address&data to logic analyzer value, which will come in handy later. Note that you need to set the input values in the source code, $address_input and $data_input. (They are set to 0x7c86 and 0x21 respectively in the below example.)

#!/usr/bin/perl

# address2logic_analyzer_raw.pl

@address_positions_from_a14 = (5, 4, 11, 1, 27, 2, 3, 12, 13, 14, 15, 10, 6, 7, 8);
@data_positions_from_d7 = (22, 21, 20, 19, 18, 17, 16, 9);

$address_input = 0x7c86;
$data_input = 0x21;

$current_position = 14;
$current_position2 = 0;
foreach (@address_positions_from_a14) {
    if ($address_input & (1<<$current_position)) {
        $mask |= (1 << $address_positions_from_a14[$current_position2]);
    }
    $current_position--;
    $current_position2++;
}
$current_position = 7;
$current_position2 = 0;
foreach (@data_positions_from_d7) {
    if ($data_input & (1<<$current_position)) {
        $mask |= (1 << $data_positions_from_d7[$current_position2]);
    }
    $current_position--;
    $current_position2++;
}
printf("%04x\n", $mask);

So, to run this, you’d do the following:

  • Connect everything up, don’t forget to connect GND between the Pico and the device under test (the HB-10 in this case)
  • minicom -C logic_analyzer_output -D /dev/ttyACM0
  • Wait until you get the “ready” message
  • Turn on the device under test
  • Wait until the Pico is done printing (takes maybe two seconds)
  • Turn off the device under test
  • Exit minicom
  • awk ‘{print $2}’ logic_analyzer_output | perl logic_analyzer_raw2address.pl > logic_analyzer_output_decoded
  • Run openmsx and openmsx-debugger and display logic_analyzer_output_decoded side-by-side
Looking at 7c8c both in the trace and in the emulator

So what do you do if you have reached the end of your trace and would like to see what happens next? In my case I saw that we spent a lot of time in a tight loop initializing memory. That takes up the entire logic buffer. So I’d like to continue reading at a certain address (which can be determined easily by following along in openmsx-debugger), right after the memory is initialized.

That’s where the other Perl script comes in. You think of an address bus value and data bus value where you’d like to continue tracing, and convert that into a value that would be seen by the logic analyzer. Then you modify the logic analyzer program like this, for example:

#include <stdio.h>
#include "pico/stdlib.h"

#define ALL_REGULAR_GPIO_PINS 0b00011100011111111111111111111111
#define LOGIC_BUFFER_LEN 62660

#define ADDRESS_PINS 0b00001000000000001111110111111110
#define DATA_PINS    0b00000000011111110000001000000000
#define ADDRESS_DATA_PINS (ADDRESS_PINS | DATA_PINS)

#define AFTER_MEMCPY 0x27d3cc
#define AFTER_MEMCPY2 0x8101af2

#define TRIGGER_PIN 28

uint32_t logic_buffer[LOGIC_BUFFER_LEN] = { 0 };

int main() {
    int i = 0;
    stdio_init_all();
    gpio_init_mask(ALL_REGULAR_GPIO_PINS);
    gpio_init(PICO_DEFAULT_LED_PIN);
    gpio_set_dir_masked(ALL_REGULAR_GPIO_PINS, GPIO_IN);
    gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT);

    // wait until /dev/ttyACM0 device is ready on host
    for (i = 0; i < 10; i++) {
        gpio_put(PICO_DEFAULT_LED_PIN, i%2==0);
        sleep_ms(500);
    }
    gpio_put(PICO_DEFAULT_LED_PIN, 1);
    printf("Logic analyzer ready, waiting for trigger\n");
    while (gpio_get(TRIGGER_PIN) == 0);
    while ((gpio_get_all() & ADDRESS_DATA_PINS) != AFTER_MEMCPY2);
    for (i = 0; i < LOGIC_BUFFER_LEN; i++) {
        logic_buffer[i] = gpio_get_all() & ALL_REGULAR_GPIO_PINS;
    }
    printf("Done recording");
    for (i = 0; i < LOGIC_BUFFER_LEN; i++) {
        printf("%04x %04x\n", i, logic_buffer[i]);
    }
    printf("Done printing\n");
}

And then it’ll start tracing as soon as it sees that the relevant GPIO pins are equal to AFTER_MEMCPY2, which is just a name I came up with.

Logic analyzer output and analysis

Here are the raw traces I produced. You’d need to use the awk command above to convert them.

And here are the three relevant post-processed files:

You can see that we have a very detailed trace of the Z80’s execution. We can easily see what address is being set by the CPU, and what’s being read at or written to that address. You may also notice that we have a couple gaps in the data, which is why we needed a retake for logic_analyzer_output2. You may also be able to tell that things apparently start at 2 here.

  • We can easily see that the Z80 is executing code correctly
  • We can easily see that the ROM is giving us the correct code (the code is identical to what we see in the emulator)
  • We see that the code is trying to switch banks (out #a8) and identify RAM, by overwriting an address and reading back the same address
  • In the emulator, it finds the RAM on first try, because it’s connected on “slot 0”, same as the ROM. (Which is possible because this machine only has 16 KB of RAM and 32 KB of ROM, which is less than the 64 KB addressable by the Z80.)
  • In our logic trace, it gets back a slightly different value from what it had written, which indicates that the RAM is most likely bad!
    • Let’s take a look at 000-03b6_retake.txt around line 6430+, address 0365 to 036e.
    • In Z80 asm, we have here:
      ld hl,#fe00
      ld a,(hl)
      cpl
      ld (hl),a
      cp (hl)
      cpl
      ld (hl),a
      jr nz,#0379
    • This means that we load from #fe00, invert, write this inversion back to #fe00, compare contents of #fe00 with our inverted value, (restore original value,) and if the comparison didn’t quite work out, we jump to #0379.
    • This code is run a number of times, and it shouldn’t jump to #0379 the first time. (It doesn’t in the emulator. It ought to work the first time because ROM and RAM are both in bank 0. But if the RAM is defective, the comparison will fail!)
    • We can also see our loads and stores to memory in the logic analyzer:
      • Line 6510: 7e00 09 (Read 09 from fe00. A15 is missing so fe00 turns into 7e00.)
      • Line 6575: 7e00 f6 (Wrote f6 to fe00. That’s the inversion of 09.)
      • Line 6620: 7e00 f7 (Read f7 from fe00. Last time I checked f6 and f7 weren’t equal.)
  • In our third logic trace, we reach a point where a function is called (7c8c, lines 80-165 in trace) and that function attempts to return. When a function returns, it checks the stack to figure out the correct address to return to (lines 600- in trace). And again, that address doesn’t quite match the address we had written when we executed the CALL instruction! In the trace we can clearly see that it’s reading 7d8f, when it should have been 7c8f. 7d is 01111101, 7c is 01111100. So it would appear that we have a stuck bit in D0.
  • So we now jump to a rather random location, which means we start to execute nonsense code.
  • At some point, the nonsense code jumps to f380 (which is uninitialized RAM). (Note that the trace doesn’t have A15, so it looks like 7380.) And while we’re now completely off the rails and firmly in nonsense territory, the see that everything here appears to have D0 set!

So before we take out the RAM chip, let’s see if we can rule out any other possible malfunctions that could lead to this behavior.

  • The RAM’s address pins are not connected directly to the CPU’s address bus, instead they are most likely connected via the nearby 74LS157 chips (I didn’t check TBH). Could these be the cause of this failure?
    • They would have to magically produce addresses that always have D0 set; that’s very unlikely.
    • When writing the return address to the stack, we should get back the correct value because the same address should be generated when reading and writing. But we’re not reading the correct value back, so it’s very unlikely that the 74LS157 is translating our addresses incorrectly.
  • Some other chip is interfering with the RAM’s output
    • Unlikely, as it’s just a single bit that is erroneous
    • Nothing is interfering with the ROM’s output or IO outputs
    • We could probably see this on the oscilloscope

Checking RAM chips with just a multimeter?

Before taking out the chip (which is quite a chore without a desoldering iron), I put my multimeter in diode mode and checked if there’s anything unusual about the chip. And there was! Putting my positive lead on ground and the negative lead on each of the data pins, I noticed that I got a different voltage drop on the pin for the suspected defective bit, 515 mV. On all others I got 462 mV. (Disclaimer: note that this is an in-circuit test and the RAM chip isn’t the only path from ground to the data pin. I also forgot to check again after removing the chip, so take this with a heap of salt.)

So let’s see what happens when we replace that RAM chip and boot!

Silly metal bar got squashed at first and the silly author of this blog post didn’t notice at first. Now it looks like this. Also guess who didn’t have any replacement chips that day.
Replacement chips arrived. Yay, it works!
Sokoban! I cleared this level. Will take a look at the next level soon. Yeah, maybe tomorrow.

Did you guys know that the word “Sokoban” is Japanese? I only recently realized that when I saw the game for sale somewhere. 倉庫番!

Also, the Raspberry Pi Pico is fast. 3.3V is inconvenient, but not the end of the world.

Old AOC 15″ LM565 LCD repair

I’m not even sure this warrants a blog post. The only thing that was wrong with it was a loose ribbon cable connecting one of the two analog boards to the logic board.

Symptoms: LED and backlights power on, but no logo, no menu. If you connect a VGA cable, the monitor is identified correctly and can be enabled, but no picture.

So… just in case anyone needs board pictures, here they are:

Buttons and sound output
Boards, front side
Boards, back side, and LCD panel

Yay, 1024×768. It was an upgrade from 800×600 back when I got it…

Hitachi MB-H2 repair (tape recorder)

In a previous post, we talked about the Hitachi MB-H2 and how we found a RAM fault using a two-channel oscilloscope. Fixing this problem made the computer boot. In this post, we’ll talk about a fault in the analog board which prevented cassette playback from working.

The Hitachi MB-H2 has an integrated cassette deck, which allows both music playback (sounds quite okay even) and data recording. Below are two somewhat high-resolution images of the backside of the analog board with the RF shield removed. This is almost as good as having the schematics (which nobody does AFAIK), because the backside has labels for everything, including transistor orientation and lines indicating how things on the front side are connected, and the traces are relatively easy to see. If you are experiencing problems with the analog board, this may help you, and you won’t even have to get rid of your RF shield or take the board out of the computer (which is pretty annoying).

In my case, the pre-amplifier IC (left of the tape head connector when viewed from above) wasn’t getting much voltage, only around 1.21V. This is under the RF shield, but it’s quite easy to follow the trace using the above images. The pre-amplifier’s VCC is on the 12V rail, but there’s a slightly larger than normal 100 ohm resistor (according to the bands) (R126), and a cap to ground between it and the 12V rail. The gold band on my resistor looked slightly thicker than usual, and when I measured it I got 130 kiloohms! (The capacitor’s capacitance and ESR seemed okay still). I replaced the 100 ohm resistor of unknown power rating with a 2W 100 ohm resistor, and immediately playback and loading started working! Yay.

~12V on left side, 1.21V on right side of resistor
Low voltage drop after placing a 100 ohm resistor in parallel.
Note: I replaced the burned resistor using a 2W resistor, so it’s unlikely to burn again. (It doesn’t seem to get particularly hot.)

Tape drive mechanism repair and re-assembly

(Disclaimer: I didn’t know much about tape drives going into this)

Somebody before me had removed the belt (probably because it was broken), so I replaced that. I don’t remember the exact length, but this rather cheap set: https://www.amazon.co.jp/gp/product/B08JP7J5VX/ had a belt that fit. (Since the original belt was gone, I don’t know if they have the same thickness as the original belt, but they do feel a little thin maybe.)

Disassembling the tape drive isn’t that hard, but there is more than one way of putting it all back together, and only one way is the correct way! This tape drive has two motors and one electromagnet (blue). One motor is on all the time and drives the capstan roller (IIRC). The other motor only runs when you press play or rewind, etc. (Or issue CALL PLAY etc., from BASIC). The electromagnet exists in order to hold the head mechanism close to the tape. (Or to prevent it from staying in place?) There’s a white plastic piece of plastic that has a hole, and the electromagnet’s pin has to go through this hole. Important: the electromagnet has to be attached to the white plastic piece correctly. The electromagnet is necessary to release the head mechanism when you press stop, and if it isn’t attached correctly, the head mechanism stays attached to the tape. Since the motor driving the roller is running all the time, and thus keeps driving the capstan, but the reel stops, this means your cassette’s tape will be moving, but the cassette’s reels aren’t moving. Your cassette’s tape will be spilled all over the place!

Eject mechanism

The eject mechanism consists of a button (which probably contains a spring, but I wasn’t able to get it out), a metal rod with a 90 degree bend on one side, a piece of metal the rod gets mounted with, some plastic parts (including a spring) that are mounted on the tape mechanism itself, and the lid.

The metal rod would strongly prefer to be a straight rod, but is forced into an unnatural shape by the piece of metal (which needs to be screwed in tight). The metal rod’s bent end goes into a small hole on the piece of metal and the other end presses down on the lid’s hinge. The rod’s tension causes the lid to strongly prefer to be in the “eject” position.

Sorry, this picture was taken before cleaning the case.

The eject mechanism only works reliably when the computer’s top part of the case is firmly attached to the bottom part, because otherwise the plastic parts mounted on the tape mechanism itself don’t properly hook into the plastic on the eject button. When this is hooked properly, this rod is forced into a slightly more tense state. When you press eject, the lid opens up rather forcefully. Even with everything closed properly, I find that you also have to press rather hard on the tape lid to close it again after ejecting.

Keyboard “repair”

Many of the keys on my machine only worked barely. To get them to work properly, you need cotton swabs and IPA. I strongly recommend you do the cleaning while the computer is powered on (if you don’t short anything unrelated, nothing terrible will happen). Only this way will you be able to get a feeling for how much rubbing is required on your machine. (I had to rub quite hard.)

Take a key switch (with rubber attached) and press down to connect a pair of pads on the keyboard’s PCB. Does something appear on the screen without having to press super-hard? Good. Otherwise: more cotton swab + IPA rubbing is required.

Edit 2023/06

Edit one year later: Note: I am not a very experienced keyboard repairman! I _think_ that these contacts are just gold-plated copper and rubbing slightly harder won’t damage them, but I do not know for sure! My keyboard works 100% even one year after I did this, but TBH the keys feel very mushy and you have to make them go all the way down to effect a key press. I don’t know what the keyboard was like when it was new. I’ve heard of people using a pencil to put some more conductive carbon on the rubber pad, or rubbing the rubber into a freshly laser-printed black page, and there’s also this type of product here: https://www.ebay.co.uk/itm/REMOTE-CONTROL-REPAIR-KIT-CONDUCTIVE-RUBBER-KEYPAD-FIX-TV-DVD-ALARM-FOB-PHONE-/171656481000

Edit 2023/08

So I tried rubbing the rubber pads on copy paper. This worked brilliantly! Before, I had to push down keys pretty much all the way to effect a keypress, now I can type almost normally. I got this tip from here:

BTW I clean the contacts on the PCB side with isopropanol, and in extreme cases, with an ink eraser. On the rubber side, I gently wipe them on a piece if paper. This takes off a smidgin of the conductive carbon surface, exposing fresh carbon. It can help a lot.

https://ilike8bits.com/what-else-have-i-been-up-to/

Note: You’ll get a grey/black streak on your paper.

(Edits end here)

I cleaned the keyboard keycaps using an ultrasonic cleaner. I used lukewarm water and a small amount of dishwashing soap. This worked perfectly. Hint: ultrasonic cleaners can be bought used for pretty cheap. I guess it’s the sort of item people buy because they do clean glasses pretty well, but then rarely use, and eventually get rid of. (I also retrobrighted some of the keys (and the case). Most normal keys were fine, but the space bar, cursor keys, and most of the function keys were yellowed. I used sodium percarbonate and the sun. I’m not an expert on retrobrighting, but this was cheap and worked okay.)

Nice bubble bath

While we’re talking about cleaning, let’s take a look at these two lovely pictures.

Yum
激落ちくん (Gekiochikun) thought it was delicious (magic eraser brand)

Running software

I had a quick look at the prices for used cartridges and decided I don’t want to spend money on software that has been paid for already. (A couple hundred yen would be fine I guess.)

https://www.raphnet-tech.com/products/msx_64k_rom_pcb/index.php caught my attention. Especially as I was able to buy one off Yahoo Auctions without waiting too long. You just need to solder a socket and program an EPROM chip with the game data. Used EPROM chips are super cheap, but as I don’t have an EPROM eraser, I decided to get a Flash EEPROM chip with a similar pinout instead. The Flash EEPROM chip I got is larger (32 pins) than the ones the creator of this board had in mind (28 pins), which means that two address pins, +5 and \WE hang over the edge of the socket. Using conductive tape, I added a “trace” to the side of my socket to hold pins 1 and 2 (A16 and A18) to GND. On the other side, pin 30 (A17) sits where +5 is supplied by the PCB. I could have probably added traces using conductive tape, but since there are two tiny SMD capacitors right under my pin 32, I decided to cut the socket so I wouldn’t apply pressure to the caps, which means that my pin 31 (\WE) and 32 (VCC) are floating in mid-air. I used test clips to connect these to +5. Since A17 is where +5 is located, I have to program my software to 0x24000 instead of 0x4000. I use this to program my Flash EEPROM: https://www.kernelcrash.com/blog/arduino-uno-flash-rom-programmer/. (My Flash EPROM is an AMIC A29040 but works wonderfully with this.) This code appears to have gotten slightly old, I had to change my serial buffer size in HardwareSerial.h by setting:

#define SERIAL_RX_BUFFER_SIZE 256

SERIAL_BUFFER_SIZE (with “_RX_” or “_TX_”) doesn’t appear to be used in this context. At first I also set SERIAL_TX_BUFFER_SIZE to 256, but that appears to break the deploy process, so don’t touch that, I guess. (And yes, if you don’t set the serial buffer size, you won’t be able to write to your EEPROM. You will still be able to identify it though.)

Pac-Man, as listed on https://en.wikipedia.org/wiki/List_of_video_games_considered_the_best.
Note the test clips sticking out. Very decorative right?
Rat’s nest that doubles as a flash EEPROMs programmer
Rated for 10 insertions. (I removed the socket’s metal bits at position 1 and 2, tucked in the conductive tape, and put the metal bits back in again. I also had to cut off the socket’s legs at position 1 and 2, as the PCB’s holes only start at position 3.)

「アネックス(ANEX) なめたネジはずし」を使ってみました。ちょっと工夫してネジが外せました。

アマゾンのレビューなど、合わせて読むようにおすすめします。この商品です: https://www.amazon.co.jp/dp/B08W1RXZB9
EDIT 2022年11月29日: 再び、「アネックス(ANEX) なめたネジはずし」を使ってみました。

自分の場合はなかなかうまく行きませんでした。力を入れて回した結果、最初に+の名残がまだ残っていたところに、なめらかなくぼみができて、いくらやっても刃物が引っかかることはありませんでした。

ところで、この状態でも最終的に刃物が引っかかてネジが外せた!、とアマゾンのコメントにありました。

が、自分は、できてしまったくぼみの横に、同じ道具で、2つのくぼみを掘って、マイナスドライバーでネジを外すことができました。

こんな感じです。中心部にできてしまったくぼみの横に、2つの小さいくぼみを掘って、マイナスドライバーで外せました。

それぞれ5分で、合わせて15分くらいでネジが外せたのかな?大変でしたが、取れた時のほっとする瞬間は良かったです。ネジを記念にとっておきます。

“Almost Pong” for the Commodore 64

Original version: https://www.lessmilk.com/almost-pong/

I recently came across a funky version of Pong called “Almost Pong”. I really liked it and thought it would be fun to re-create it for the Commodore 64. I went about this entirely in BASIC, without writing any assembler routines — by using a fancy compiler that I recently came across: https://egonolsen71.github.io/basicv2/. (Before you get too excited, the physics in my game are quite different — I don’t think doing the same physics calculations would work in realtime on the C64, but using lookup tables it might be possible to produce very similar physics.)

So is this a usable game? I don’t know, when I play the above-mentioned Almost Pong it kind of feels more fun, maybe it’s the physics, maybe it’s because my game is even more bare-bones, or maybe I’m just biased against my own game? Anyway, without further ado:

  • Works in VICE and on real hardware
  • Press fire button on joystick #2 to jump
  • The source code has lines that are longer than 80 lines and will therefore produce syntax errors if you type it into a C64 verbatim
  • It’ll also be way too slow to play if you don’t compile it using Basicv2

Here’s the source code:

0 in=0
1 poke 53280,1:poke 53281,0:poke 646,1
2 v=53248:lv=1:co=1:c=0:fr=0:x=160:y=141:rem center
3 if in=0 then print chr$(147):print " press fire to play":wait 56320,16,16:wait 56320,16:in=1
4 for t=12288 to 12415 step 1:poke t,0:next
5 for t=12289 to 12350 step 3:poke t,255:next:for t=12373 to 12397 step 3:poke t,255:next
6 poke v+21,7:poke v+39,1:poke v+40,1:pokev+41,co
7 poke v,72:poke v+16,1:poke v+2,16:poke v+4,x:poke 53271,3
8 poke v+1,y:poke v+3,y:poke v+5,y
9 poke 2040,192:poke 2041,192:poke 2042,193
10 s=54272:w=17:poke s+5,97: poke s+6,200: poke s+4,w:poke s+24,15:rem sound
15 ox=4:oy=1:od=1:jm=4
20 sx=ox:sy=oy:dy=od
21 gosub 1500
30 for i=1 to 2 step 0:rem infinite loop
35 sc=peek(53278):rem sprite collision lag workaround
40 x=x+sx
50 y=y+0
60 if x>255 then poke v+16,5:poke v+4,x-256:goto 70
65 if x<=255 then poke v+16,1:poke v+4,x
70 y=y+sy:sy=sy+dy
73 if y>234 then y=234:poke v+5,y:gosub 1000:rem todo could add poke v+5,y to the subroutine
74 if y<43 then y=43:poke v+5,y:gosub 1010
75 poke v+5,y
80 rem joystick
90 f1=peek(56320) and 16
92 if f1<>0 then fr=0
93 poke 162,0:wait 162,2:rem wait 1/80s
99 rem only fire if fire wasn't pressed during last poll
100 if fr=0 and f1=0 then y=y-sy*jm:sy=oy:dy=od:fr=1:gosub 1700

110 rem bounce
120 if sx < 0 or x<=328 then goto 150
125 rem x=328:pokev+4,x-256
126 poke v+5,y
127 sc=peek(53278)
130 if sc <> 0 then goto 140:rem bounce if we collided
131 if x+sx < 344 then goto 180
135 gosub 1020:rem game over if no collision
140 gosub 1600:rem bounce
145 poke v+1,int(rnd(0)*158)+50

150 if sx > 0 or x>=32 then goto 180
155 rem x=32:pokev+4,x
156 poke v+5,y
159 sc=peek(53278)
160 if sc <> 0 then goto 170:rem bounce if we collided
161 if x+sx > 16 then goto 180
165 gosub 1030:rem game over if no collision
170 gosub 1600:rem bounce
175 poke v+3,int(rnd(0)*158)+50
180 gosub 1800:next:rem sound off
190 end

1000 print " you lose (don't fall into the abyss)":gosub 1100
1010 print " you lose (don't jump into the sky)":gosub 1100
1020 print " you lose (you need to bounce off the":print " right paddle)":gosub 1100
1030 print " you lose (you need to bounce off the":print " left paddle)":gosub 1100
1100 poke s,120:poke s+1,6:poke 162,0:wait 162,16
1110 wm=0
1120 gosub 1800
1130 print " press fire to play"
1140 wait 56320,16,16:wait 56320,16
1150 goto 1
1160 return:rem not reached

1500 rem print game status
1510 print chr$(147):print " level:", lv, "points:", c
1520 return

1600 rem bounce
1610 sx=-sx
1620 c=c+1
1630 if c/5 < lv then goto 1650
1640 lv=lv+1:co=co+1:poke v+41,co:ox=ox+1:if co=15 then co=1
1650 gosub 1500:rem print sc:poke 162,0:wait 162,8
1660 return

1700 rem fire button sound on
1710 poke s,133:poke s+1,11
1730 wm=3:rem num of sound off gosubs to wait before muting
1740 return

1800 rem sound off
1810 if wm>0 then wm=wm-1:goto 1830
1820 poke s,0:poke s+1,0
1830 return

Here’s the compiled .prg:

Here’s me playing the game.

Stupid game

How to load software from the internets on a real C64 using the Datasette drive

Now here’s a (zipped) .wav file that you can put on a tape (or much easier, stream through a cassette adapter into your datasette drive) and load on a real computer. I will describe how to generate .wav files from .prg/.tap/.d64 files in the next section.

Notes on using cassette adapters to load C64 software

When using a cassette adapter, you should make sure your playback device’s volume is neither too loud nor too quiet. On my computer, 50% volume appears to be the sweet spot. You may have to experiment a bit to find your own. When using a cassette adapter, you will have to pause streaming manually (after the C64 prints out “FOUND NAMEOFPROGRAM”, as whatever device you’re using to stream is completely unaware of whether the datasette drive’s motor is on or off and just continues streaming regardless. (I didn’t add a long enough pause between the header(?) and the actual data. You could maybe add a multiple-second pause yourself to automate this part.) It’s probably generally best to stream using Audacity (or similar software) and watch the datasette drive to see whether the motor is on or off. When it’s off, you press stop and when the motor starts running again, reposition the cursor into the nearest section of silence and press play again. I was able to load various kinds of software using this approach. Commercial software may have multiple bits of silence where the motor stops running for a bit.

In this Audacity screenshot, there’s a short bit of silence at the 11 second mark. This is where the C64 will stop the motor and print “FOUND NAMEOFPROGRAM”. (Note that in the above .wav file, the name of the program is blank, so the C64 will only print “FOUND”.) When the motor stops, press the stop button. When the motor starts running again, reposition the cursor somewhere in the middle of the silent section (11.139s in this example) and press play.

Here’s an Audacity screenshot for Great Giana Sisters (the tape version):

The cursor is positioned in the first short bit of silence, right after the “FOUND NAMEOFPROGRAM” but there are multiple points where the motor stops spinning and you will have to manually press stop/play each time.
Great Giana Sisters! I think you had to press space to continue loading beyond this screen, which means you’d again need to manually press stop/play in Audacity

Now that we have talked about how to load .wav files into the C64, here’s how to actually generate .wav files:

Generating .wav files from .tap/.prg/.d64 files

I spent a few hours evaluating multiple solutions, and have found that wav-prg (https://wav-prg.sourceforge.io/) did the best job.

Here’s how to build this program on Linux:

git clone https://git.code.sf.net/p/wav-prg/libtap
cd wav-prg-libtap
make libtapdecoder.so
make libtapencoder.so
cd ..
git clone https://git.code.sf.net/p/wav-prg/libaudiotap
cd wav-prg-libaudiotap
make clean # probably not needed but happened to be in my notes, possibly for a reason
make -j4 DEBUG=y LINUX64BIT=y libaudiotap.so
cd ..
git clone https://git.code.sf.net/p/wav-prg/code
cd wav-prg-code
make clean # probably not needed but happened to be in my notes, possibly for a reason
make -j4 cmdline/wav2prg DEBUG=y AUDIOTAP_HDR=../wav-prg-libaudiotap/ AUDIOTAP_LIB=../wav-prg-libaudiotap/
make -j4 cmdline/prg2wav DEBUG=y AUDIOTAP_HDR=../wav-prg-libaudiotap/ AUDIOTAP_LIB=../wav-prg-libaudiotap/

Here are some example invocations for an NTSC C64. You can also generate .wav files for use with the PET (full explanation here) or the VIC-20.

# (pwd is .../wav-prg-code)
LD_LIBRARY_PATH=../wav-prg-libaudiotap/:../wav-prg-libtap/ cmdline/prg2wav -m c64ntsc -t filename.tap /path/to/prg # convert prg to tap image for use in vice etc.
LD_LIBRARY_PATH=../wav-prg-libaudiotap/:../wav-prg-libtap/ cmdline/prg2wav -m c64ntsc -w filename.wav /path/to/prg # convert prg directly to wav file

Here’s how to extract a .prg file from a .d64 floppy image, which you can then convert to a .wav file. All you need is VICE, which comes with a tool to extract data from .d64 files:

./c1541 -attach ~/retro/c64/software/BLOCKNB1.D64 -list
0 "ass presents:   "      
66    "block'n'bubble"    prg 
6     "block'n'bub. dox"  prg 
1     "doc-maker.code"    prg 
1     "scores"            prg 
590 blocks free.

“ass”? :p Anyway, judging by the number of blocks, “block’n’bubble” seems like the program we’re most interested in. (You can try extracting the others and running them in an emulator.) To extract and to convert, run the following commands:

# (pwd is .../vice-3.5/src)
./c1541 -attach ~/retro/c64/software/BLOCKNB1.D64 -read "block'n'bubble" bnb.prg
reading file `block'n'bubble' from unit 8

# (change directory to .../wav-prg-code)
LD_LIBRARY_PATH=../wav-prg-libaudiotap/:../wav-prg-libtap/ cmdline/prg2wav -m c64ntsc -w bnb.wav bnb.prg # adjust input and output file paths

I don’t remember how to play, but I remember the intro screen music. Felt good to hear it played from a real Commodore 64 for the first time in 15-20 years! Here’s a clip. Sorry for the copyright violation:

Block’n’Bubble title music (looped)

The game’s playable as-is, but at least on my hardware the first time I started a game I got some weird colors. (The second time round the colors were normal.) Could be a PAL-vs.-NTSC issue, or could have something to do with the above conversion (for example, a program could assume that the C64’s tape buffer is available for temporary usage, but when loading from tape… it isn’t).

Note that large commercial software is often divided into multiple files, e.g., consisting of a loader and a main program, and perhaps a high score file. In that case, you will not be able to easily convert the software. (You would have to re-write the parts where the loader starts loading from the floppy, etc.)

For example, here’s what’s in the Sokoban .d64:

./c1541 -attach ~/retro/c64/SOKOBAN0.D64 -list
0 "ass presents:   "      
1     "soko-ban"          prg 
40    "soko-ban docs"     prg 
56    "sokoban"           prg 
41    "title"             prg 
2     "maze01"            prg 
2     "maze55"            prg 
9     "start"             prg 
3     "flip"              prg 
1     "test"              prg 
509 blocks free.

It would probably require some hacking to convert this to tape.

However, not all commercial software is this complex. For example, using this method, I was able to convert the main .prg file in “JupiterLander_1982_Commodore.d64” to .tap and load this in VICE (didn’t try on real hardware but should work) and play as normal.

~/src/vice-3.5/src/c1541 -attach JupiterLander_1982_Commodore.d64 -list
0 "1982 commodore  " -136-
30    "j.lander +2  /n0"  prg 
5     "j.lander docs/n0"  prg 
629 blocks free.

~/src/vice-3.5/src/c1541 -attach JupiterLander_1982_Commodore.d64 -read "j.lander +2  /n0.prg" jupiterlander.prg

LD_LIBRARY_PATH=~/src/wav-prg-libaudiotap/:~/src/wav-prg-libtap/ ~/src/wav-prg-code/cmdline/prg2wav -t jupiterlander.tap jupiterlander.prg
Stupid game

Commodore PET 2001 repair

I recently had a look (multiple long looks) at a Commodore PET 2001 (with the ~first version of BASIC etc.) and a special character ROM with Japanese katakana instead of lower-case characters. I was told it had worked in around 2000 when it was last turned on. I went right in without knowing much about the Commodore PET.

Wobbly screen, junk characters on boot

The wobbly/unstable/warped screen was caused by an oxidized and slightly broken molex connector supplying power to the board. This caused the voltages to jump between 4-5 volts multiple times per second. Well, nothing’s going to work that way. (Ideally I would have fixed this first, but I’d already noticed a bunch of other bad connections and didn’t think the molex connector would go bad.)

Stable screen, junk characters on boot

All chips that cost more than a couple cents (don’t know the prices in 1977 of course) were socketed. But boy, these are bad sockets. The one redeeming feature of the sockets that were used by Commodore at this time is that it’s easy to check if the inserted chip is making a connection with the socket. Just put your multimeter in continuity mode, one multimeter probe on the chip’s pin, and the other on the slightly exposed metal part belonging to the socket. No beep — bingo, that needs to be fixed.

This isn’t my machine and I’m not allowed to use contact cleaner (except IPA of course). I’m sure that contact cleaner would have helped here though. Anyway, not all of the bad connections were due to oxidization as far as I can tell — it seems like the socket and the pin just aren’t making physical contact, even after judicial use of IPA.

In these cases you can either replace the bad socket, or you can try to bend the chip’s legs to get better contact. I’d advise you not to do that with the chips in the PET; most of them were rather brittle in my case. Another thing you can do, and which I did here, is to put a new socket right on top of the existing socket. In most cases that’ll improve things immediately.

So I did that on all RAM chips after testing continuity almost everywhere (there was at least one pin that didn’t make contact on most chips) and all ROM chips except one (one didn’t have any bad contacts for some reason. The middle one, so maybe that one was more protected from the elements compared to ones closer to the edge?), and one of the 6520s. For the CPU and other chips, just re-seating did the trick.

Double sockets almost everywhere. I used extra high-quality sockets for the left-most RAM chips and the video RAM at the back, and (accidentally) in one other location.

One thing you have to know about the PET 2001 is that it’s possible to boot with just 2K (or even 1K?) of RAM. So make sure to put working RAM (making good contact) into the leftmost RAM sockets. If you have slightly faulty RAM, it doesn’t matter so much if you have that in the sockets beyond 2K. Your PET (if it boots) will tell you how many bytes of RAM you have free on boot. If it says 7167 bytes (on an 8K model), that means your RAM is probably fine. If it’s less than that, you probably have faulty RAM somewhere. (Broken RAM that misbehaves grossly may cause problems though.)

Keyboard fixes

If your PET has successfully booted, you should test all keys. If there are keys that don’t appear to work, try holding the key down for a little, or repeatedly pressing the key. If none of that helps, you will need to take apart the keyboard and clean it up using IPA. That fixed all problems for me.

Clean these contacts with IPA
I also cleaned some of these conductive rubber? thingies with IPA, but only the ones on keys that were kind of problematic.

Testing RAM and ROM with a short BASIC program

If your PET has successfully booted, you may want to test your RAM and ROM chips. I found a BASIC script on the web to test RAM, but wasn’t able to run it back when my PET reported it only had 363 (or so) bytes free — the program was just too long. Original program from https://www.commodore.ca/commodore-manuals/commodore-pet-memory-test/. Here’s my modified version, which will work with much less RAM, has some visual feedback and isn’t that tough to type in.

5 INPUTA,B
20 FORI=ATOB
21 FORY=1TO4
22 READN
23 POKEI,N:X=PEEK(I)
24 IFX=NTHEN26
25 GOSUB200
26 NEXT
27 RESTORE
30 DATA0,85,170,255
120 PRINT "."
121 NEXT
130 PRINT "DONE"
140 END
200 PRINT "ADDRESS ";I;" WROTE ";N;" READ ";X
210 RETURN

Note that the PET firmware will place the BASIC code at address 1025+, variables go between 1946-2071, and arrays go from 2072-2231. As long as our BASIC code doesn’t use arrays, not too many variables, and isn’t too long, the system won’t need to access memory beyond 2K. So it’s safe to run this program on all RAM, from 2048 to 8191. (It’s pretty slow BTW, you may want to run it multiple times, in 2K steps.)

Found some RAM errors! Stuck bit.
More RAM errors. Bit 3 (counting from 0) appears to be stuck at 0 here.

If you get errors, check the schematics for your board revision, re-test continuity between chip and socket on the supposedly faulty chip, and if there’s no connection error, replace it.

Here’s the ROM test, in ASM and BASIC (with data lines). On the PET, the ROM isn’t accessible from BASIC using PEEK, so assembly is required. Make sure you don’t make any mistakes when typing in the data lines.

.ORG = $1000
START:
LDA #0
LDY #$D0 ; stop when INC_ADDRESS+2 reaches this value
LOOP:
CLC ; clear carry flag, otherwise we'd add unneeded +1s
INC_ADDRESS:
ADC $C800 ; add contents of memory address $C800 to A (this address will be modified below)
JSR PRINT_HEX ; print out checksum for every added byte
INC INC_ADDRESS+1 ; increments insignificant byte
BNE LOOP ; if that address hasn't overflowed back to 0, go to LOOP
INC INC_ADDRESS+2 ; increments significant byte
CPY INC_ADDRESS+2 ; compare significant byte with Y register (into which we loaded the significant byte of the end address earlier)
BNE LOOP ; if unequal, go to LOOP
; JSR PRINT_HEX ; if we blazed past both BNEs then we're done so output checksum ; commented out because we currently print out the checksum for every added byte
RTS ; return from subroutine

PRINT_HEX:
PHA ; copy A to stack
LSR ; shift right 4 times so we get significant nibble
LSR
LSR
LSR
JSR PRINT_NIBBLE
PLA ; get fresh copy of A from stack
PHA ; and also write it back on stack
AND #$0f ; get lower nibble
JSR PRINT_NIBBLE
LDA #32 ; code for space
JSR $FFD2 ; print a space
PLA
RTS

PRINT_NIBBLE:
CMP #10 ; if we're >= 10
BCS HEX_ABCDEF ; branch
CLC ; clear carry flag, otherwise we'd add unneeded +1s
ADC #48 ; otherwise, add 48 to turn into a number
JSR $FFD2 ; print number
RTS ; return
HEX_ABCDEF: ; this is the a-f branch
CLC ; clear carry flag, otherwise we'd add unneeded +1s
ADC #55 ; we're at range 10-15 but want range 65-70, so add some
JSR $FFD2 ; print a-f
RTS

BASIC:

10 t=4096
15 input "decimal msb of start address"; sa
16 input "decimal msb of end address "; ea
17 input "carry over "; co
20 read x
30 if x=-1 then sys 4096:end
40 if x=-2 then poke t,sa:goto 80
50 if x=-3 then poke t,ea:goto 80
60 if x=-4 then poke t,co:goto 80
70 poke t,x
80 t=t+1
90 goto 20
1000data169,-4,160,-3,24,109,0,-2,32,25,16,238,6,16,208,244
1010data238,7,16,204,7,16,208,236,96,72,74,74,74,74,32,47
1020data16,104,72,41,15,32,47,16,169,32,32,210,255,104,96,201
1030data10,176,7,24,105,48,32,210,255,96,24,105,55,32,210,255
1040data96
1050data-1

Here’s a very lazy script (bin2data.sh) to assemble (using acme) and generate a simple BASIC loader:

#!/bin/bash

echo rem assuming compilation with acme --setpc 4096 -o foo check_rom.asm
echo
echo

echo '10 t=4096'
echo '20 read x:if x<>-1 then poke t,x:t=t+1:goto 20'
(od -Anone -tx1 $1 | perl -pe 's/([0-9a-fA-F]{2})/hex($1).","/eg' | sed -r -e 's/^/data/' -e 's/,$//'; echo 'data -1') | nl -i 10 -v 1000 -nln | sed -e 's/ //g' -e 's/ //g'

Here’s how to assemble using acme and use the bin2data.sh script:

acme --setpc 4096 -o check_rom.bin check_rom.asm; bin2data.sh check_rom.bin

Note that the BASIC listing above is almost identical to the output of the assembler script, except that I manually modified the output to place -4, -3, and -2 in the DATA lines. This is where the start/end addresses and the carry over go.

What this program does is sum up all values in the ROM. It also prints out the intermediate sum for each byte. Note that in the above loader, we poke the machine code into addresses 4096+. If you are on a 4K PET, that will not work. Just modify the .ORG, –setpc, and t=4096 lines in the assembly source, acme command line, and bin2data.sh, respectively.

The ROMs on my machine are 2K each. Their addresses are 0xC000-0xC7FF (most significant byte in decimal: 192), 0xC800-0xCFFF (200), 0xD000-0xD7FF (208), 0xD800-0xDFFF (216), 0xE000-0xE7FF (224), 0xF000-0xF7FF (240), 0xF800-0xFFFF (248).

So you enter 192 for the start address, 200 for the (non-inclusive) end address, 0 for carry over. On my machine (and in VICE), the last few numbers are 8F EF 91 CB. Note: the ROMs at https://www.zimmers.net/anonftp/pub/cbm/firmware/computers/pet/index.html yield different checksums for this region.

I had the same checksums as the emulator for C000-C7FF, C800-CFFF, D000-D7FF, D800-DFFF, E000-E7FF, but different values for F000-F7FF and F800-FFFF. However, the checksums for F000 to FFFF were identical with rom-1-f000.901439-04.bin and rom/rom-1-f800.901439-07.bin downloaded from the above site.

Same values, yay

Here’s a very lazy script to compute the checksums using perl:

od -Anone -tx1 roms/rom-1-f800.901439-07.bin | perl -ne 'while (s/([0-9a-fA-F]{2} ?)//) { $sum = ($sum + hex($1)); printf("%x\n", $sum%256) }'

Repairing the tape drive

First of all, one thing that is useful to know is that you can connect the C64’s Datasette to the edge connector for the first tape drive on the PET’s mainboard. It’ll work just the same as the built-in drive. (However the Datasette’s plastic case may get in the way if you attempt to connect it to the edge connector for the second tape drive.)

Datasette drive connected to PET edge connector

You can even close the PET in this state without pinching the cable; I think there is around 1 cm of empty space between the base and the “lid” of the PET. (Which might be the reason everything is so dusty and oxidized in there.) So if you have a working Datasette drive, you may want to see if you can load/save programs using that.

On the PET’s internal drive I had to replace the drive belt, which had snapped. Here’s someone who created replacement belts using their 3d printer: https://www.insentricity.com/a.cl/275/making-belts-with-a-3d-printer / https://www.thingiverse.com/thing:2600198/files Armed with this person’s measurements, I chose a pack of replacement drive belts from Amazon that seemed like they should have fitting ones (they did, though the belts were a bit thinner than advertised): https://www.amazon.co.jp/gp/product/B08JP7J5VX/. Cleaning the heads and the capstan and pinch roller (https://en.wikipedia.org/wiki/Tape_transport) with IPA made things work.

That was one brittle belt

Running some software

With a Datasette drive (for use with C64s and similar machines), it’s very easy to take off the cover (just lift it a bit further from it’s open position). With the cover removed, it’s very easy to put software on the PET using a 3.5mm/cassette adapter.

Datasette drive with cover removed

Most software written for the PET doesn’t seem to work on the PET 2001 (even the 8K model), but there are some games on http://www.zimmers.net/anonftp/pub/cbm/pet/games/english/index.html that work. (Search the page for ‘2001’)

There’s one more step however, as these are .prg files. I used wav-prg to convert the .prg files into .wav files. Quick setup:

git clone https://git.code.sf.net/p/wav-prg/libaudiotap
git clone https://git.code.sf.net/p/wav-prg/libtap
git clone https://git.code.sf.net/p/wav-prg/code
cd wav-prg-libtap
make libtapdecoder.so
make libtapencoder.so
cd ../wav-prg-libaudiotap
make clean # may not be necessary but it's in my notes, perhaps for a reason
make -j4 DEBUG=y LINUX64BIT=y libaudiotap.so # DEBUG=y is in my notes, perhaps for a reason
cd ../wav-prg-code
make clean
make -j4 cmdline/wav2prg DEBUG=y AUDIOTAP_HDR=../wav-prg-libaudiotap/ AUDIOTAP_LIB=../wav-prg-libaudiotap/
make -j4 cmdline/prg2wav DEBUG=y AUDIOTAP_HDR=../wav-prg-libaudiotap/ AUDIOTAP_LIB=../wav-prg-libaudiotap/

# for PET: (-s seems to be required)
LD_LIBRARY_PATH=../wav-prg-libaudiotap/:../wav-prg-libtap/ cmdline/prg2wav -s -w space\ invader.wav space\ invader.prg

I don’t remember why, but I’d enabled debugging in my Makefiles as follows. It’s unlikely that these changes are needed.

wav-prg-libaudiotap:

diff --git a/Makefile b/Makefile
index 61b0bac..7f3e573 100644
--- a/Makefile
+++ b/Makefile
@@ -14,7 +14,7 @@ libaudiotap.so: libaudiotap.o libaudiotap_external_symbols.o pthread_wait_event.
        $(CC) -shared -o $@ $^ -ldl $(LDFLAGS)
 
 ifdef DEBUG
- CFLAGS+=-g
+ CFLAGS+=-g -O0
 endif
 
 ifdef OPTIMISE

wav-prg-code:

diff --git a/Makefile b/Makefile
index f96b135..2442e6c 100644
--- a/Makefile
+++ b/Makefile
@@ -66,7 +66,7 @@ ifdef AUDIOTAP_HDR
   CFLAGS+=-I"$(AUDIOTAP_HDR)"
 endif
 ifdef DEBUG
-  CFLAGS+=-g
+  CFLAGS+=-g3 -O0
 endif
 
 LDLIBS=-laudiotap

Once you have .wav files, you can just use a cassette tape adapter for car stereos as described above. Unfortunately the edge connector for the PET’s second datasette drive doesn’t have enough clearance, so I disconnected the internal drive and once again connected the Datasette drive instead. Then you just type “load” and enter and press play, then you play back the .wav file from your computer.

Here’s the title screen of a random game that I found that just so happened to almost work on the PET 2001 with some minor bugs (perhaps it was developed with a different machine in mind), Diamond Hunt II:

Yay it’s working!

More two-channel oscilloscope-based RAM testing

In a previous article we were able to see that one of our 4164 RAM chips didn’t produce a clear 0 or a clear 1 when asked to read from its memory. In this article we’ll hunt for more broken RAM chips by comparing the output from a known good 4164 RAM chip with the output of each installed 4164 RAM chip.

Basically, we place the known good 4164 RAM chip on a breadboard next to the computer to test, and then connect all address, RAS, CAS, WRITE and D pins to the equivalent pins on one of the 4164 RAM chips on the computer (by using tiny test clips, of which you will need at least 12, but more would be better). Then, we attach one oscilloscope probe to the Q pin on the known good 4164 RAM chip and one probe on the Q pin on the RAM chip to test (we’ll test each one, one by one). (Note that D and Q pins are often shorted, so if that is the case we can also attach the probe to the D pin on the RAM chip under test.)

We should then hold down the computer’s reset button and (while doing so) set the oscilloscope to trigger on a positive edge from the known good RAM chip’s Q pin, zoom out so we can capture a good amount of activity, and then release the reset button. We expect both chip’s outputs to be exactly the same. If there is a discrepancy, either the connections are bad, or we have found a broken RAM chip. (Be sure to check your connections using your multimeter’s continuity mode, and try hard not to touch the cables while probing around.)

Here’s a picture of my setup:

The keyboard connector on the Hitachi MB-H2 provides power. I’m measuring the right-most chip in this example, and to avoid having to touch the cables I’m taking D7 from a completely different chip. I’m also taking some of the address pins from a different RAM chip, just because that was easier.

If your testing shows that the RAM chips under test never produce the same signal as the reference known-good chip, then your wiring may be messed up. If on the other hand the signals always match up perfectly, then it seems likely that none of your RAM chips are bad. Finally, if there is a single RAM chip that often (but perhaps not always) produces a late signal or a different signal, you may have found a bad RAM chip.

In my case, most chips seemed to produce consistent signals. However, touching the cables often messed up signal integrity and I had to re-measure multiple times. I believe that the RAM chip for D7 was bad, as it often produced late or wrong signals even though the connections should have been stable.

In the below video I’m scrolling along an oscilloscope capture. You can see that when the yellow line (which measures the output of the reference chip) goes high or low, the blue line also goes to (or stays at) the same level. This means that we’re getting the same output on both, which is good.

(There is a lot of other activity on the blue line, which we don’t know much about — some of it’ll be I/O to other chips, some will be writes to the RAM. The only thing we have that we can compare to is the Q output on the reference chip, so all we can do is check whether the transitions from 0 to 1 and from 1 to 0 on the output of the chip under test are correct. We are not able to check 1 to 1 or 0 to 0 outputs.)

For the less video-inclined readers, here is a screengrab showing a correct signal:

Confusing but okay signal

We have two transitions in the above screenshot. The first transition looks confusing at first because the blue signal takes a nosedive right after the yellow reference signal goes up — but that’s not too important, what matters is that they both have the same level for a short while. The blue line was already at “high” due to other I/O. (Also ignore the fact that the yellow line goes slightly higher than the blue one.) In the second transition we have a very neat and synchronized nosedive on both lines.

Here is a screengrab showing an erroneous signal:

Bad signal

Here we have two transitions on the yellow channel, and both times the blue channel goes to the exact opposite logic level. This isn’t perfect proof of a bad RAM chip — first of all we should make sure that this is repeatable and not due to wobbly probes. Second there is a slight possibility that some other device thinks it’s its turn to play with the bus (but normally that would show up as a voltage level somewhere between 0V and 5V).

4164 tester (including refresh testing) on the Raspberry Pi Pico and mini-review (updated)

I recently lost access to my Arduino for a couple days and decided to finally start playing around with my Raspberry Pi Pico. In case you don’t know, the Raspberry Pi Pico is even cheaper than most Arduinos. I think I paid 550 Japanese yen at a brick-and-mortar Marutsu for mine, and packs a lot of pins and quite some performance. (Amazon is way more expensive.)

So before we get to the 4164 tester, here’s my mini-review of the Raspberry Pi Pico from the perspective of someone who has never used a Raspberry Pi before and is used to his Arduino Nano:

The header pins weren’t attached so I had to solder them myself. I guess it’s possible to buy Picos with header pins soldered on, and I guess it’s also possible to buy Arduino Nanos without header pins. (Confirmed on amazon.co.jp). Soldering wasn’t too hard and I didn’t break anything.

Getting the (C/C++) development environment set up (on Debian Linux) wasn’t too hard for me personally, but I do this kind of thing a lot and would expect this to be much more difficult for someone who doesn’t have as much experience, especially if they are on a different distro. What I did was look at this script: https://raw.githubusercontent.com/raspberrypi/pico-setup/master/pico_setup.sh and instead of just running it, did the same things the script would have done — i.e., installed the prerequisite packages, cloned the relevant repositories, compiled and installed. If you can read bash scripts reasonably well you should be able to do this — otherwise you can just try and run the script verbatim and hope for the best. Note that the script is made to be run on a (non-Pico) Raspberry Pi.

The Arduino has an LED that is always on as long as there is power. The Pico has an onboard LED but it’s completely software-controlled. That’s great for non-beginners and perhaps not so great for beginners. (Is this thing even working?) To flash a program, you hold down a button and then connect USB. The Pico will identify as a sort of flash drive and you can copy over a .uf2 file. Once your OS is done copying (i.e. almost instantly) the Pico will immediately reboot and immediately start running your code. To the OS it will look like the flash drive suddenly disappeared. (This took me a little while to figure out.)

On the Arduino, you can be connected via serial at all times, though you’ll get a grey screen while flashing. On the Pico, USB serial feels much more “software-defined”, and you can’t be connected while re-flashing AFAICT. If you write a program that outputs something to serial right after starting, you probably won’t ever be able to see that output because your computer will take a while to notice there is something on USB. (For this reason, I added a 25 second pause at the beginning in my RAM tester program.)

So some parts of the development process are slightly more annoying than on the Arduino Nano, but the features may make up for it I think — more pins (but fewer analog I/O pins), much more performance, and step-by-step debugging via GDB (haven’t tried this yet).

One more very important thing to be aware of is that the Pico’s GPIO pins’ high logic level is 3.3V, not 5V. This doesn’t matter (IME) when driving the 4164’s input pins (address/RAS/CAS/WRITE/data in), but it’s likely to matter for the single GPIO pin connected to the 4164’s Q output pin. So you will need a resistor divider to bring the 4164’s 5V output down to 3.3V.

Update 2023/08/07: the code is also available on GitHub: https://github.com/qiqitori/pico_4164_tester

The code consists of three files, a very standard CMakeLists.txt (pretty much a straight amalgam of https://github.com/raspberrypi/pico-examples/blob/master/CMakeLists.txt and https://github.com/raspberrypi/pico-examples/blob/master/hello_world/usb/CMakeLists.txt), pico_sdk_import.cmake (straight from https://github.com/raspberrypi/pico-examples/blob/master/pico_sdk_import.cmake), and 4164_test.c. Some of the code in 4164_test.c has been adapted from https://ezcontents.org/4164-dynamic-ram-arduino.

CMakeLists.txt

cmake_minimum_required(VERSION 3.12)

# Pull in SDK (must be before project)
include(pico_sdk_import.cmake)

project(pico_examples C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

if (PICO_SDK_VERSION_STRING VERSION_LESS "1.3.0")
    message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.3.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}")
endif()

set(PICO_EXAMPLES_PATH ${PROJECT_SOURCE_DIR})

# Initialize the SDK
pico_sdk_init()

include(example_auto_set_url.cmake)

add_compile_options(-Wall -Wextra
        -Wno-format          # int != int32_t as far as the compiler is concerned because gcc has int32_t as long int
        -Wno-unused-function # we have some for the docs that aren't called
        -Wno-maybe-uninitialized
        )

add_executable(4164_test
        4164_test.c
        )

# pull in common dependencies
target_link_libraries(4164_test pico_stdlib)

# enable usb output, disable uart output
pico_enable_stdio_usb(4164_test 1)
pico_enable_stdio_uart(4164_test 0)

# create map/bin/hex file etc.
pico_add_extra_outputs(4164_test)

# add url via pico_set_program_url
example_auto_set_url(4164_test)

4164_test.c

#include <stdio.h>
#include "pico/stdlib.h"

#define D           0
#define WRITE       1
#define RAS         2
#define A0          3
#define A2          4
#define A1          5

#define A7          16
#define A5          17
#define A4          18
#define A3          19
#define A6          20
#define Q           21
#define CAS         22

#define HIGH        1
#define LOW         0

#ifndef PICO_DEFAULT_LED_PIN
#error blink requires a board with a regular LED
#endif

#define STATUS_LED PICO_DEFAULT_LED_PIN
// #define STATUS_LED 15

#define BUS_SIZE 8
#define MAX_ERRORS 20
#define REFRESH_EVERY_N_WRITES 4
#define REFRESH_EVERY_N_READS 256 // 256 is the maximum for this implementation
#define N_REFRESHES 256

const unsigned int a_bus[BUS_SIZE] = {
  A0, A1, A2, A3, A4, A5, A6, A7
};

// #define debug_print printf
#define debug_print(...) ;

void nop(void) {
    __asm__ __volatile__( "nop\t\n");
}

void set_bus(unsigned int a) {
    int i;
    /* Write lowest bit into lowest address line first, then next-lowest bit, etc. */
    for (i = 0; i < BUS_SIZE; i++) {
        gpio_put(a_bus[i], a & 1);
        a >>= 1;
    }
}

void refresh() {
    int row;
    for (row = 0; row < 256; row++) {
        set_bus(row);
        gpio_put(RAS, LOW);
        nop();
        nop();
        nop();
        nop();
        nop();
        nop();
        nop();
        nop();
        nop();
        nop();
        nop();
        nop();
        nop();
        nop();
        nop();
        nop();
        nop();
        nop();
        nop();
        nop();
        gpio_put(RAS, HIGH);
    }
}

void write_address(int row, int col, bool val) {
    // Pull RAS and CAS HIGH
    gpio_put(RAS, HIGH);
    gpio_put(CAS, HIGH);

    // Set row address
    set_bus(row);

    // Pull RAS LOW
    gpio_put(RAS, LOW);
    nop(); // need to wait 15 ns before setting column address
    nop(); // need to wait 15 ns before setting column address
    nop(); // need to wait 15 ns before setting column address
    nop(); // need to wait 15 ns before setting column address

    // Set column address
    set_bus(col);

    // Pull CAS LOW
    gpio_put(CAS, LOW);

    // Set Data in pin to HIGH (write a one)
    gpio_put(D, val);

    // Pull Write LOW (Enables write)
    gpio_put(WRITE, LOW);

    sleep_us(1);
    gpio_put(WRITE, HIGH);
    gpio_put(CAS, HIGH);
    gpio_put(RAS, HIGH);
}

void read_address(int row, int col, bool *val) {
    // Pull RAS and CAS and WRITE HIGH
    gpio_put(RAS, HIGH);
    gpio_put(CAS, HIGH);
    gpio_put(WRITE, HIGH);

    // Set row address
    set_bus(row);

    // Pull RAS LOW
    gpio_put(RAS, LOW);
    nop(); // need to wait 15 ns before setting column address
    nop(); // need to wait 15 ns before setting column address
    nop(); // need to wait 15 ns before setting column address
    nop(); // need to wait 15 ns before setting column address

    // Set column address
    set_bus(col);

    // Pull CAS LOW
    gpio_put(CAS, LOW);

    sleep_us(1);

    *val = gpio_get(Q);

    gpio_put(WRITE, HIGH);
    gpio_put(CAS, HIGH);
    gpio_put(RAS, HIGH);
}

void setup() {
    bool dummy = false;
    int i;

    gpio_init(D);
    gpio_init(WRITE);
    gpio_init(RAS);
    gpio_init(A0);
    gpio_init(A2);
    gpio_init(A1);
    
    gpio_init(A7);
    gpio_init(A5);
    gpio_init(A4);
    gpio_init(A3);
    gpio_init(A6);
    gpio_init(Q);
    gpio_init(CAS);

    gpio_init(STATUS_LED);

    gpio_set_dir(D, GPIO_OUT);
    gpio_set_dir(WRITE, GPIO_OUT);
    gpio_set_dir(RAS, GPIO_OUT);
    gpio_set_dir(A0, GPIO_OUT);
    gpio_set_dir(A2, GPIO_OUT);
    gpio_set_dir(A1, GPIO_OUT);

    gpio_set_dir(A7, GPIO_OUT);
    gpio_set_dir(A5, GPIO_OUT);
    gpio_set_dir(A4, GPIO_OUT);
    gpio_set_dir(A3, GPIO_OUT);
    gpio_set_dir(A6, GPIO_OUT);
    gpio_set_dir(Q, GPIO_IN);
    gpio_set_dir(CAS, GPIO_OUT);

    gpio_set_dir(STATUS_LED, GPIO_OUT);

    gpio_put(RAS, HIGH);
    gpio_put(CAS, HIGH);
    gpio_put(WRITE, HIGH);
    gpio_put(D, LOW);
    gpio_put(A0, LOW);
    gpio_put(A1, LOW);
    gpio_put(A2, LOW);
    gpio_put(A3, LOW);
    gpio_put(A4, LOW);
    gpio_put(A5, LOW);
    gpio_put(A6, LOW);
    gpio_put(A7, LOW);
    
    sleep_us(1110);
    for (i = 0; i < 8; i++) {
        read_address(0, 0, &dummy);
        sleep_us(1);
    }
}

void test(bool start_val) {
    int row, col, refresh_count;
    int errors = 0;
    bool read_val = false;
    bool val = start_val;

    for (row = 0; row < 256; row++) {
        debug_print("Testing row: %d\n", row);
        for (col = 0; col < 256; col++) {
            gpio_put(STATUS_LED, val);
            write_address(row, col, val);
            read_address(row, col, &read_val);
            if (val != read_val) {
                printf("ERROR: row %d col %d read %d but expected %d\n", row, col, read_val, val);
                if (++errors > MAX_ERRORS) {
                    while (true) {
                        gpio_put(STATUS_LED, HIGH);
                        sleep_ms(50);
                        gpio_put(STATUS_LED, LOW);
                        sleep_ms(50);
                    }
                }
            }
            val = !val;
            gpio_put(STATUS_LED, val);
            if (col % REFRESH_EVERY_N_WRITES == 0) {
                refresh();
            }
        }
    }
    for (refresh_count = 0; refresh_count < N_REFRESHES; refresh_count++) {
        printf("Refresh test %d\n", refresh_count);
        val = start_val; // start from start_val (which determines whether we're testing 10101010... or 01010101...)
        for (row = 0; row < 256; row++) {
            for (col = 0; col < 256; col++) {
                gpio_put(STATUS_LED, val);
                read_address(row, col, &read_val);
                if (val != read_val) {
                    printf("ERROR: row %d col %d read %d but expected %d\n", row, col, read_val, val);
                    if (++errors > MAX_ERRORS) {
                        while (true) {
                            gpio_put(STATUS_LED, HIGH);
                            sleep_ms(50);
                            gpio_put(STATUS_LED, LOW);
                            sleep_ms(50);
                        }
                    }
                }
                val = !val;
                gpio_put(STATUS_LED, val);
                if (col % REFRESH_EVERY_N_READS == 0) {
                    refresh();
                }
            }
        }
    }
}

int main() {
    stdio_init_all();
    setup();

    sleep_ms(10000); // wait until /dev/ttyACM0 device is ready on host
    printf("Starting 10101010... test\n");
    test(true);
    printf("Starting 01010101... test\n");
    test(false);
    printf("Test done. All OK!\n");
    while (true) {
        gpio_put(STATUS_LED, HIGH);
        sleep_ms(1000);
        gpio_put(STATUS_LED, LOW);
        sleep_ms(1000);
    }
}
No cross connections

The Raspberry Pi Pico has a lot more pins than the Arduino Nano, which means that we can connect everything in a very straight and orderly manner. The pins on the left of the IC are connected to pins on the left of the Pico, and the pins on the right of the IC are connected to pins on the right of the Pico, and there are no cross connections. That’s a huge plus, and (as in our case) if you’ve got just one pin that requires level shifting, I’m not sure I’d choose the Arduino Nano if I already knew and owned both. Adjust the #defines at the top if you want to connect things differently.

Also, the Pico could theoretically allow much faster testing than the Nano, but my test is pretty slow, especially when using USB serial output. I also added a couple of delays to be super sure we don’t go out of spec. As the library only has millisecond and microsecond delays, I added a NOP-based delay (which probably delays things much more than required).

Note that this code won’t do anything the first ~10 seconds, this delay gives the host computer time to identify the USB serial device (should appear as /dev/ttyACM0). If you then execute, e.g., ‘minicom -D /dev/ttyACM0’ you should be able to see output as the test progresses. It’s also possible to see whether the test passed by looking at the onboard LED — fast blinking means error, slow blinking means success. If you prefer running tests using the LED, I suggest you do the following:

  • Comment out the sleep_ms(10000); call
  • Remove ‘#define println printf’, add ‘#define println(…) ;’ instead
  • Set MAX_ERRORS to 0 (or remove the if (++error > MAX_ERRORS) logic entirely)

(The MAX_ERRORS logic causes the program to keep running after the first 20 errors, if you don’t want that, feel free to remove.)

There are three constants that control how refreshing works, REFRESH_EVERY_N_WRITES, REFRESH_EVERY_N_READS, and N_REFRESHES. If you want to test if your memory is refreshed correctly for a longer time, adjust N_REFRESHES.

To compile, copy the above files into a new directory, create a ‘build’ subdirectory and cd to it, run cmake and make:

mkdir build
cd build
cmake ../ -DCMAKE_BUILD_TYPE=Debug
make -j4
# hold BOOTSEL button while plugging in Pico USB
cp 4164_test.uf2 /media/.../RPI-RP2/

Hitachi MB H2 (MSX) partial schematics and repair

See also Hitachi MB-H2 board pics / more partial “schematics”.

I recently got hold of a Hitachi MB H2 that wouldn’t work. Attaching a composite cable, I’d just see a black screen with some faint colored vertical bars. I probed around the computer with an oscilloscope and found that the computer actually appears to execute code in the first few microseconds or milliseconds after powering it on (or resetting). There didn’t appear to be any bad connections.

I saw some very pronounced unusual voltages on the data bus; judging by the color intensity on the oscilloscope screen I got 33% 0V, 33% about 1-2 V, 33% 5V. The 1-2 V bar could just be everything attached being in a “don’t care”/”high impedance” state, so I proceeded to look for some schematics on the internets. But there were none as far as I can tell! (Anyway, it seems normal to have some not-one-but-not-quite-zero-either activity on this computer as I found out later.)

Having no prior MSX experience, I proceeded to trace out a lot of the connections from the CPU to the other chips. It turned out that most data lines are directly connected to the CPU, implying that most of the chips should have some kind of “don’t care”/”high impedance” state. From the tracing I did I managed to create some partial schematics, until I found I was wise enough to figure some stuff out.

Partial Hitachi MB-H2 schematics
Partial Hitachi MB-H2 schematics

So what’s wrong with my MSX?

There is one thing that seemed very wrong to me on my MSX, I had no activity on the Z80’s IOREQ pin. I also never had activity on the VDP’s pins that interface with the CPU. So I set a rising edge trigger on the Z80’s IOREQ pin and pressed the reset button and saw that there were a couple very early IOREQ signals, suggesting that the computer works normally for at least a while.

A Z80 starts executing from address #0, and the code at this address will have to be on ROM. This machine has 32 KB + 16 KB of ROM, and 64 KB of RAM. A Z80 can only address 64 KB, so we will have to have some kind of mechanism to switch between ROM and RAM. I am not entirely sure how this works on the MSX, but to me it just seemed likely that the early boot code would perhaps copy the ROM contents to RAM and then execute code from there. And if the RAM is defective somehow we’ll be in a bit of a situation… (Even if that isn’t correct, broken RAM should/could/might cause things to go haywire early in the boot process.)

So I suspected the (non-socketed) 4164 RAM is probably broken (I think this is a very common defect for early computers), and set up a small circuit to help me test that theory.

RAM can break in a number of ways, some RAM chips get hot, some pull the data pin to 0 or perhaps 1, and some are perpetually high-impedance, i.e. they do not affect the voltage of the data pin at all. (None of the RAM chips were hot in my case. The video chip is very hot, but the datasheet mentioned something about 70 degrees Celsius so that’s probably fine.)

Unfortunately my oscilloscope only has two inputs. However, to verify if a RAM chip actually does something when somebody reads from it, we may need more, depending on the RAM chip in question of course. Here are some screenshots from the datasheet (TMS4164-datasheet-texas-instruments.pdf):

4164 pinout
4164 pinout
4164 read sequence
4164 read sequence

In the read sequence, we see that \CAS (usually high) is low and \W (usually “don’t care”) is high, and after a short moment we see either a low or a high on Q (normally “high impedance”).

So with just two oscilloscope inputs we don’t get very far, we’d need three (for \W, \CAS and Q). However, with a very simple circuit we can get by with two inputs.

All we need is a NOT gate and an AND gate. Then we can combine \W and \CAS and produce H if and only if (\W is high and \CAS is (not high)). So we wire things like this:

       |----|
       |    |--\CAS------NOT----| 
--\W-- |    |--Q--probe         AND--probe
|      |4164|                   |
|      |----|                   |
|-------------------------------| 

All you need is a breadboard, 74LS04 and a 74LS08 chip. Here’s a picture of the setup:

MB-H2 4164 RAM under test
MB-H2 4164 RAM under test

So let’s have a look at the resulting oscilloscope captures to see if we can find anything interesting. In the captures, the output of the AND gate is in yellow, and the output Q (which is shorted to D on this computer) is blue. Maybe take a look and try to find the problem. The answer is right below the last image so, spoiler warning. ;)

D0
D0
D1
D1
D2
D2
D3
D3
D4
D4
D5
D5
D6
D6
D7
D7

Highlight (and possibly copy and paste somewhere) the next paragraph to read the answer:

D0. Yellow goes high, but blue doesn’t budge at all. We’re trying to read from this chip but the chip isn’t outputting anything!

I piggy-backed (and made sure this particular problem went away) and replaced the suspect chip, but unfortunately it appears that that isn’t all that is wrong with this computer. :/ (If you see this text instead of a link to additional blog posts, that means that I haven’t figured out the problem yet, or haven’t gotten around to creating a write-up for it yet.)

In fact this may have been the only thing wrong with the computer. Piggy-backing unfortunately didn’t work, but after soldering things eventually worked out. I also replaced another RAM chip that seemed like it was misbehaving (More two-channel oscilloscope-based RAM testing) because I was already desoldering, so that could have been part of it too.

70年代、80年代の8ビットのレトロパソコン(マイコン)を修理します

トロパソコンの修理の経験を結構積んできました。
メーカー問わず。
興味がありましたらコメントをください。(コメントをしてもすぐに反映されません。公開を希望しない場合はその旨を記載してください。)