Raspberry Pi Pico implementation of the YM3012 DAC (mono)

Introduction

The YM3012 IC is a DAC that requires two external op amp circuits and turns a serial digital audio signal consisting of a 10-bit mantissa and 3-bit exponent into an analog signal.

I am currently investigating a fault in an audio module (SFG-01) for certain MSX computers (mostly Yamaha). This audio module is pretty capable and sports a YM2151 FM audio synthesis chip and comes with MIDI input and output ports, a connector for a digital piano keyboard, and software to use the keyboard of course. (I actually never checked if the software is in the module or in the computer.) See this for more information on the SFG-01: https://www.msx.org/wiki/Yamaha_SFG-01.

The fault becomes apparent as soon as two keys are pressed at the same time on the digital piano keyboard. You get a kind of growling/distorted effect. The audio doesn’t sound clean. (Head to the video section below to hear what it sounds like.) My first thought was, that sounds like an analog problem. Aw, I wish. I replaced a couple capacitors without any improvement whatsoever. The removed capacitors all tested fine out-of-circuit, too. A few people said it could be a problem with the op amps. One (relatively) quick way to check if that is the case, is to replace the op amps and try again. But why do it the quick and simple way (with possibly nothing to show at the end) if you can do it the slow and complicated way (with maybe something to show at the end)?

YM3012 pinout

The Raspberry Pi Pico is very good at IO. Not only do we have a lot of pins, but we can read from and write to them very, very fast. However, we aren’t going to go that fast today actually. Neither are we going to be using a lot of pins. In order to build a DAC, we need to read the CLOCK φ1, SD (DATA) and SAM1 and/or SAM2 pins. And then we need output, which in my case is a single pin outputting PWM audio. (It sounds okay, probably not exactly Hi-Fi.) My implementation only reads SAM1 and only outputs a single channel, completely discarding the other channel. It wouldn’t be too hard to get the second channel to work too — the Pico is a dual-core jobby after all, so you could just run the same code on the second core and it’d work. (As there isn’t really a lot of post-processing going on at all, you could most likely even get it to work with just a single core, but I haven’t tried.)

So, in order to test if our DAC, or one of the op amp circuits, or the filter circuits are misbehaving, we just need our Raspberry Pi Pico and check if we’re getting the faulty audio there too. If yes, the DAC is innocent. If no, the DAC or related circuitry would be implicated.

PWM audio

Researching PWM audio on the Pico, I first came across this YouTube video: https://www.youtube.com/watch?v=rwPTpMuvSXg. It turns out, however, that PWM audio is discussed in https://datasheets.raspberrypi.com/rp2040/hardware-design-with-rp2040.pdf, and the creator of the above YouTube video had mostly taken the circuit from there. Basically, you need a medium-sized capacitor to remove the DC bias, some resistors and smaller caps to filter out high-frequency components, and optionally a buffer IC. It’s all right to use a digital buffer IC (I’m using a 74-series logic hex inverter), which then drives the above-mentioned resistors and caps. (The Pico can’t output a lot of current, so I decided to include the buffer, as recommended in the PDF.)

Overview

Since the MSX and its audio module and the keyboard are museum exhibits, and the museum isn’t exactly next door (fortunately not too far away though), I only had limited time to experiment with the original hardware. So what do you do in such a case? Well, I think we all agree that any sane person would immediately head to the internets and check if anyone’s ever implemented the YM2151 (the FM synthesis chip) on an FPGA. (Well, any sane person who owns an unused FPGA. Mine is an UPduino that I bought a couple years ago. They’re actually more expensive now than back then.) As a bonus, if it turns out that the DAC is fine, we should (sometime in the future) be able to hook up our FPGA to the SFG-01 and see if it produces the same weird distorted sound. If it doesn’t, we can be reasonably sure that the YM2151 on the SFG-01 is the one causing the weird sound. (Assuming there are no bad solder joints, etc.)

It turns out that the the YM2151 does indeed exist in the form of Verilog code: https://github.com/jotego/jt51. Amazing! Thank you very much. Impressive. 😳 So all we have to do is:

  1. Put this on our FPGA
  2. Find a way to control the FPGA
  3. Connect the FPGA’s output to our DAC and experiment until it sounds okay

On 1: unfortunately our FPGA is a little bit too small to fit the entire thing. Also, the inputs and outputs are slightly different from the original chip! What do we do? Lowering the footprint of JT51 (YM2151 Verilog clone) to work on smaller FPGAs, specifically the ICE40UP5K (Part 1? WIP? Progress diary?) / UPduino mini-tutorial

On 2: I took this: https://github.com/iComputer7/RaspiPicoVGM.git. Nice work, thank you very much! And modified it to only support the YM2151, remove SD card support, and instead read the VGM data from a header file. My modified code is at https://github.com/qiqitori/RaspiPicoVGM.

On 3: that’s this post, I guess.

Debugging methodology

There were many hours spent debugging this. How do you even debug audio that sounds wrong somehow? Well, as with all debugging, you break things up into smaller things that you can actually verify to be correct (or prove incorrect):

  1. Make sure the digital data you are receiving on the Pico is the same as what the FPGA is supposed to be putting on the wire.
    1. Make the FPGA always output the same dummy value. Not the case. The most significant bit is flipped sometimes.
    2. Check if the Pico’s pio_sm_is_rx_fifo_empty() function is lying or something. Yes, looks like it.
    3. Implement a workaround. (More on that later in this post.)
  2. Audio sounds slightly better but overall still crappy.
    1. Forget about the mantissa + exponent algorithms for a second and make the FPGA output straight 16-bit signed PCM.
    2. There’s a hiss but generally speaking it sounds pretty good!
    3. Play around with the PWM audio parameters
    4. Oh wow, the hiss is gone and things sound almost perfect.
  3. Raw PCM audio sounds good, but mantissa + exponent audio still doesn’t.
    1. Make the FPGA output PCM for one sample, and mantissa + exponent of the exact same sample on the next sample.
    2. Put a hexdump in a spreadsheet and see if we can spot the problem. The mantissa + exponent samples should be exactly the same (but with some of the lower bits all 0s), but often they’re somewhat different.
    3. Fix some issues that we introduced in the FPGA code
      1. Output changes continuously and must be latched on the first clock cycle of a new sample
      2. reg/wire confusion
    4. Pico DAC’s mantissa + exponent code was slightly wrong too

The thing mentioned in 1-2 could be a bug in the Pico SDK (or documentation). I’ll probably look into that at some point. The workaround consists of reading from the FIFO twice.

Here’s a screenshot of the aforementioned spreadsheet:

The 2d layout, the conditional formatting, VLOOKUP, string processing functions all make it pretty easy to figure stuff out, in my opinion. YMMV. It would have been helpful if LibreOffice’s HEX2BIN could support more than 8 bits, but 8 bits should be enough for anybody, right?

I also used a tiny script (that I’m including below, just for my own convenience for when I need to get back to something related) to convert a hex dump into audio, using xxd and sox:

#!/bin/bash

# assumes a log file generated e.g. like this: minicom -C sample_dump1.log -D /dev/ttyACM0

tail -n +2 $1 > $1.trunc # get rid of hello world debug output
xxd -p -r $1.trunc > $1.trunc.raw
sox -c 1 -r 62000 -t u16 $1.trunc.raw -b 16 -e signed-integer $1.trunc.wav

Pic/audio/video

JT51 running on the UPduino, RaspiPicoVGM running on a Pico (top right) pico_ym3012 running on a Pico (top left)

I obtained a VGM for the YM2151 from this page: https://vgmrips.net/packs/pack/fantasy-zone-ii-dx-sega-system-16c. I chose “10 Years After ~ Cama-Ternya [Demo]”, and converted this from VGM to a header file for use with RaspiPicoVGM using xxd -i. Below is some audio of this VGM being played back using the above pictured setup. Note that it isn’t perfect, most likely due some issues on the FPGA side:

Played on JT51 controlled by RaspiPicoVGM, DAC’d by ym3012_dac

(Here’s a YouTube video of how this song is actually supposed to sound: https://www.youtube.com/watch?v=5sBDx56lv7g)

The below video shows the pico_ym3012 connected to the SFG-01 using tiny test clips, fully reproducing the growling/distorted sound that is the source of this whole investigation.

Verilog lessons learned

  • If you have a `define in one file and an `ifdef in another file, that `ifdef could very well evaluate as true.
  • Latching is pretty important
  • Executing always blocks on the correct conditions is pretty important
  • The synthesis tool won’t always catch wire vs. reg mistakes
  • Verilator will catch some things that yosys will just interpret in the probably correct way

The code

The code is also available at https://github.com/qiqitori/pico_ym3012/. License is GPLv3 for ten years after release. If there is no update saying something to the contrary, consider it public domain. I have only reproduced the major bits below.

ym3012_dac.c:

#include <stdio.h>

#include "pico/stdlib.h"
#include "pico/multicore.h"
#include "hardware/pio.h"
#include "hardware/uart.h"
#include "hardware/pwm.h"
#include "ym3012_dac.pio.h"
#include "hardware/irq.h"  // interrupts

#define PIN_BASE 0
#define AUDIO_PIN 28

// #define DEBUG 1
// #define JT51 1

#ifdef JT51
#define DESIRED_SAMPLE_RATE 62000 // 4 MHz VGM
#else
#define DESIRED_SAMPLE_RATE 57000 // 315/88 MHz / 2 / 32
#endif

uint16_t samples[110000] = { 0 };
uint16_t last_sample;

int main() {
#ifdef DEBUG
    stdio_init_all();
    sleep_ms(5000);
    printf("Hello world\n");
#endif

    // Init PWM for audio out
    gpio_set_function(AUDIO_PIN, GPIO_FUNC_PWM);
    int audio_pin_slice = pwm_gpio_to_slice_num(AUDIO_PIN);

    // Setup PWM for audio output
    // We run at around 125 MHz. If we set the pwm counter's top value (== wrap value) to 8192 (generally, bigger is better), the pwm counter can reach the top value 15258.7890625 times per second, which would be our effective sample rate. (Calculation: 125000000/8192)
    // However, our target sample rate is larger than that. Let's say if we wanted 44100 Hz: 125000000/44100 = 2834.46712018, so that's the max top value we should set.
    // However, our target sample rate is even larger than that. Let's say we want 60 KHz. Then the max top value is 2083.33333333.
    // In that case, our samples' max loudness should be about half that, 1041.66666667.
    // That's pretty close to 1024. That's good.
    // Let's not hard-code this but calculate based on the desired sample rate.
    // Note that the desired sample rate depends on the VGM tune played.
    uint16_t pwm_wrap = clock_get_hz(clk_sys)/DESIRED_SAMPLE_RATE-24; // TODO: Check if -24 actually improves anything (original intent is to buy microcontroller some time to move to the next sample -- if we don't have enough time, pwm_set_gpio_level might not make it in time and the entire next PWM cycle would be played using the level of the previous sample. I think so anyway.)
    pwm_config config = pwm_get_default_config();
    pwm_config_set_clkdiv(&config, 1.0f);
    pwm_config_set_wrap(&config, pwm_wrap);
    pwm_set_gpio_level(AUDIO_PIN, 0);
//     pwm_set_phase_correct(audio_pin_slice, true); // TODO: maybe test if this changes anything?
    pwm_init(audio_pin_slice, &config, true);

    // Init state machine for PIO
    PIO pio = pio0;
    uint sm = 0;
    uint offset = pio_add_program(pio, &ym3012_dac_program);
    ym3012_dac_init(pio, sm, offset, PIN_BASE);

#ifdef DEBUG
    for (int j = 0; j < 15; j++) {
        for (int i = 0; i < 110000; i++) {
            samples[i] = ym3012_dac_get_sample(pio, sm);
        }
        for (int i = 0; i < 110000; i+=8) {
            printf("%04x %04x %04x %04x %04x %04x %04x %04x\n", samples[i], samples[i+1], samples[i+2], samples[i+3], samples[i+4], samples[i+5], samples[i+6], samples[i+7]);
        }
    }
#else
    while (true) {
        last_sample = ym3012_dac_get_sample(pio, sm); // same as above
//         printf("%04x\n", last_sample);
        last_sample = last_sample >> 5;

        pwm_set_gpio_level(AUDIO_PIN, last_sample);
    }
#endif
}

ym3012_dac.pio:

.program ym3012_dac

; // WARNING you need to switch between JT51/YM2151/PCM code yourself by commenting/uncommenting the relevant PIO code blocks below!

; for man+exp (YM2151):
    set x, 12            ; Preload bit counter, delay until eye of first data bit
    wait 1 pin 1        ; Wait for SAM HIGH // WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING change required on JT51: wait 0 pin 1
    wait 0 pin 1        ; Wait for SAM LOW // WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING change required on JT51: wait 1 pin 1
    ; ignore first three bits, as specified in data sheet
    wait 1 pin 2        ; Wait for clock HIGH
    wait 0 pin 2        ; Wait for clock LOW
    wait 1 pin 2        ; Wait for clock HIGH
    wait 0 pin 2        ; Wait for clock LOW
    wait 1 pin 2        ; Wait for clock HIGH
bitloop: ; Loop x times
    wait 0 pin 2        ; Wait for clock LOW
    wait 1 pin 2        ; Wait for clock HIGH
    in pins, 1          ; Sample data
    jmp x-- bitloop     ;

; for JT51 linear signed 16-bit PCM:
; for linear s16:
;    set x, 15            ; Preload bit counter
;    wait 0 pin 1        ; Wait for SAM HIGH
;    wait 1 pin 1        ; Wait for SAM LOW
;bitloop: ; Execute following code x+1 times
;    wait 1 pin 2        ; Wait for clock HIGH
;    in pins, 1          ; Sample data
;    wait 0 pin 2        ; Wait for clock LOW
;    jmp x-- bitloop     ;

% c-sdk {
#include "hardware/clocks.h"
#include "hardware/gpio.h"

// #define YM3012_CLK 2000000 // for 4 MHz tunes
#define YM3012_CLK 1790000 // SFG-01 runs at NTSC speed
#define CLK_MULTIPLIER 8 // we need to run faster because we do "wait 1"/"wait 0"s for every transition in PIO code (and have some other extra instructions too)
#define NEGATE_EXP 1
// #define LINEAR_PCM_S16_INPUT 1
// #define DEBUG 1

static inline void ym3012_dac_init(PIO pio, uint sm, uint offset, uint pin_base) {
    pio_sm_set_consecutive_pindirs(pio, sm, pin_base, 3, false);
    pio_gpio_init(pio, pin_base);

    pio_sm_config c = ym3012_dac_program_get_default_config(offset);
    sm_config_set_in_pins(&c, pin_base);
    // Shift existing values to the right when new value comes in
    // The YM3012 receives D0 first, which is the least significant bit
#if LINEAR_PCM_S16_INPUT
    sm_config_set_in_shift(&c, true, true, 16); // signed 16-bit linear, shift to right
#else
    sm_config_set_in_shift(&c, true, true, 13); // man+exp, 10+3 bits, shift to right
#endif
    sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_RX); // appears to be necessary??
    float div = (float)clock_get_hz(clk_sys) / (YM3012_CLK*8); // TODO: 4 * actual clock rate would be nice // "For example, the YM2151 internally divides the clock by 2, and has 32 operators to iterate through. Thus, for a nominal input clock of 3.58MHz, you end up at around a 55.9kHz sample rate." https://github.com/aaronsgiles/ymfm/blob/main/README.md
    sm_config_set_clkdiv(&c, div);

    pio_sm_init(pio, sm, offset, &c);
    pio_sm_set_enabled(pio, sm, true);
}

static inline uint16_t ym3012_dac_get_sample(PIO pio, uint sm) {
    // 10-bit read from the FIFO (data is left-justified)
    uint16_t data_and_exp, data, result, leading_ones;
    uint8_t exp;
    io_rw_32 *rxfifo_shift = (io_rw_32*)&(pio->rxf[sm]);
    while (pio_sm_is_rx_fifo_empty(pio, sm))
        tight_loop_contents();
    uint16_t rxfifo_contents = *rxfifo_shift; // HACK. If we don't read this twice we may get a stale?? value with the last bit sometimes missing. (HOWEVER reading thrice we get something stale again. Though maybe we're just a little late when reading the third time?) (see example below)
#ifdef LINEAR_PCM_S16_INPUT
#ifdef DEBUG
    return (uint16_t)((int16_t)(*rxfifo_shift >> 16)); // don't want that ugly offset when we're debugging
#else
    return (uint16_t)((int16_t)(*rxfifo_shift >> 16)+32768);
#endif // DEBUG
#else // !LINEAR_PCM_S16_INPUT:

    data_and_exp = (uint16_t)(*rxfifo_shift >> 19);

#ifdef NEGATE_EXP // not needed on JT51
    exp = ~((data_and_exp) >> 10) & 0b111; // top 3 bits, negated
#else
    exp = ((data_and_exp >> 10) & 0b111); // top 3 bits
#endif

    data = data_and_exp & 0b1111111111; // lower 10 bits
    if (exp == 0) { // probably doesn't happen on the JT51 at least, and shouldn't happen on YM2151 according to datasheet
        result = 0; // according to jt51_exp2lin.v
    } else {
#ifdef JT51
        result = (data << (exp-1));
        // For signed numbers (first bit of mantissa is 1) we need to sign extend by adding a bunch of ones.
        // The number of ones to be added is: 16 (because uint16_t) - (left_shift_amount (== exp-1) + 10 (mantissa length)).
        // We can create a value with the specified number of leading ones by left shifting a value that is all ones.
        // We need to shift by (16-number_of_desired_leading_ones) (e.g., 0xffff with 16 leading ones can only be achieved by left shifting by 0).
        // 16 - (16-((exp-1)+10)) = 16 - (16 - (exp-1) - 10) = 0 - -(exp-1) - -10 = (exp-1) + 10 = exp + 9
        leading_ones = 0xffff << ((exp-1) + 10);
        if (data & (1<<9)) // test for first bit of mantissa
            result |= leading_ones; // add leading ones
        result = (int16_t)result + 32768;
#else
        result = data << 6;
        result = result / (2<<(exp-1));
#endif
    }

    // related to above HACK:
    // example output of below printf demonstrating the stale output when reading the first and third times
    // first read: 0
    // third read: 715653120 or 2863136768
    // second read (>> 19): always 5461
//     0 715653120 5461 341 2 170
//     0 2863136768 5461 341 2 170
//     0 2863136768 5461 341 2 170
//     0 715653120 5461 341 2 170
//     0 2863136768 5461 341 2 170
//     0 2863136768 5461 341 2 170
//     0 715653120 5461 341 2 170
//     0 715653120 5461 341 2 170
//     0 715653120 5461 341 2 170
//     0 2863136768 5461 341 2 170
//     printf("%u %u %u %u %u %u\n", rxfifo_contents, *rxfifo_shift, data_and_exp, data, exp, result);

    return result;
#endif // LINEAR_PCM_S16_INPUT
}
%}

The scaffolding is basically the same as usual. See the Github repository for details.

Cloning an old (extremely difficult) puzzle game made by the company that probably invented Sokoban

Link to game for impatient readers: https://blog.qiqitori.com/tnt_bomb_bomb/
シンキングラビットのTNTボムボムのJavaScript版です。どうぞお遊びください。
以下は英語しかありません。(´・ω・`)

I like Sokoban. A while ago, I saw someone play a Sokoban-like game called T.N.T. Bomb Bomb on a Sharp MZ-1500. I wanted it and almost immediately headed to the internets to find a disk image or ROM or whatever of it. And while I could find references and YouTube videos, I couldn’t find anything playable. (Note 1: me not being able to find the ROM doesn’t mean that it really doesn’t exist, of course. In fact, maybe this isn’t the first clone of these levels either. Note 2: it is likely that this copy of the game will be properly dumped in the near future.)

Fortunately, the game is partially implemented in BASIC. Which means you could just press Shift+Break and type LIST whenever you wanted! Then you could very easily modify variables and type RUN and play with extra lives or whatever. In my case, I just wanted a picture of every level, so I added a line (line 5) to specify the level to show, hit RUN, and took a picture. Here’s an example:

Hitting enter in this state will render level 4.

(As you can see, the graphics remain on screen after breaking, and sometimes the listing is difficult to see because of this. The graphics can be cleared by executing INIT “CRT:I” in BASIC, but that will cause rendering of the next level to fail.)

It looked like I got correct views of levels 1-10, and I have added these into my JavaScript clone of the game. Levels 11-20, on the other hand, instead of displaying the level number, displayed a game tile (a wire or part of the battery) inside the upper-right corner of the screen. I have therefore not added these levels to my implementation.

Level 1, original game

My very analog way of copying levels into my clone: 1) look at picture like the one above, 2) type out an array like this:

[
    [ 0, 1, 1, 1, 1, 1, 1, 1, 0, 0 ],
    [ 0, 1, 0, 0, 0, 0, 0, 1, 0, 0 ],
    [ 0, 1, 0, 0, 0, 0, 0, 1, 1, 0 ],
    [ 1, 1, 0, 10, 11, 0, 0, 0, 1, 0 ],
    [ 1, 0, 32, 12, 13, 20, 31, 0, 1, 0 ],
    [ 1, 0, 33, 40, 41, 42, 30, 53, 1, 0 ],
    [ 1, 0, 0, 0, 0, 0, 0, 0, 1, 0 ],
    [ 1, 1, 1, 1, 1, 1, 0, 0, 1, 0 ],
    [ 0, 0, 0, 0, 0, 1, 1, 1, 1, 0 ],
    [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
]

(In reality I added the commas after the fact, using a single find and replace operation. I think it took an hour or two for 10 levels.)

The original game has a concept of “lives”, but I don’t think this is a valuable concept in a Sokoban-like game. So I didn’t port that over. In fact, I added functionality to make it easy to go right back to a point you were at before noticing the smell of brain fart. The game is fiendishly difficult in my opinion, I don’t think it’s necessary to make it any more difficult. In fact, if they hadn’t made it so damn difficult, maybe it would be up there with Sokoban and other famous puzzle games from the 1980s.

By the way, I’ve only solved level 1. It was super hard. Update 2023/03/01: And level 2 and 3! It probably took longer to solve level 1 than implementing the basic game logic. No guarantees that levels 2 3 4 and beyond are solvable. If you solve anything beyond level 2 3 4, please send me your sequence strings. I’ll verify them and add a note here or maybe in the game that the level has been shown to be solvable. :)

Making games like this is pretty straightforward, but:

There is one part in my implementation of this game that I think is slightly interesting. Since I do not know the solutions to the puzzles (and there may even be puzzles with multiple solutions), I wrote a small recursive function (trace_wire_path) that traces the electric path and returns true if it leads to the bomb (it starts tracing at a battery terminal). It’s not optimized at all, and I didn’t bother cleaning up the code after getting it to work for the first time (my gut feeling says that it should be easy to replace a lot of the if-thens with lookup tables), but this kind of stuff doesn’t occur too often in regular day-to-day programming, so I thought it was kind of fun. (Though it all depends on what you do for a living, I guess?) Let me know if there’s some corner case where it didn’t work for you. ;)

// for simplicity we always trace from the battery
function trace_wire_path(x, y, dir_x, dir_y) {
    // if tile at x, y is inside ELEMENTS_COMPATIBLE_WITH_POS_DIRX array
    var tile_to_check = levels[current_level][y][x];

    // is this tile compatible with the previous tile?
    if ((dir_x == 1) &&
        (ELEMENTS_COMPATIBLE_WITH_POS_DIRX.indexOf(tile_to_check) == -1))
        return false;
    else if ((dir_x == -1) &&
             (ELEMENTS_COMPATIBLE_WITH_NEG_DIRX.indexOf(tile_to_check) == -1))
        return false;
    else if ((dir_y == 1) &&
             (ELEMENTS_COMPATIBLE_WITH_POS_DIRY.indexOf(tile_to_check) == -1))
        return false;
    else if ((dir_y == -1) &&
             (ELEMENTS_COMPATIBLE_WITH_NEG_DIRY.indexOf(tile_to_check) == -1))
        return false;

    // are we done? (we already know we must be on the right side)
    if ((tile_to_check == TNT_BOTTOM_LEFT) ||
        (tile_to_check == TNT_BOTTOM_RIGHT))
        return true;

    // what's our new direction?
    if ((tile_to_check == HORIZONTAL_WIRE) ||
        (tile_to_check == VERT_WIRE)) {
        new_dir_x = dir_x;
        new_dir_y = dir_y;
    } else if ((tile_to_check == CORNER_WIRE_NW) ||
                (tile_to_check == CORNER_WIRE_SW) ||
                (tile_to_check == CORNER_WIRE_NE) ||
                (tile_to_check == CORNER_WIRE_SE)) {
        if (dir_x) { // dir_x is 1 or -1
            new_dir_x = 0;
            if ((tile_to_check == CORNER_WIRE_NW) ||
                (tile_to_check == CORNER_WIRE_NE))
                new_dir_y = -1; // up
            else if ((tile_to_check == CORNER_WIRE_SW) ||
                     (tile_to_check == CORNER_WIRE_SE))
                new_dir_y = 1; // down
        } else { // dir_x is 0
            new_dir_y = 0;
            if ((tile_to_check == CORNER_WIRE_NW) ||
                (tile_to_check == CORNER_WIRE_SW))
                new_dir_x = -1;
            else if ((tile_to_check == CORNER_WIRE_NE) ||
                     (tile_to_check == CORNER_WIRE_SE))
                new_dir_x = 1;
        }
    }

    // recurse
    return trace_wire_path(x+new_dir_x, y+new_dir_y, new_dir_x, new_dir_y);
}

Performance

It shouldn’t be a big deal to leave this game open in a tab somewhere. Virtually no CPU and not a lot of memory should be in use when nothing is happening. about:performance snapshot with the game just sitting there, waiting for user input:

No quantifiable energy impact; memory is low too.

In case anyone wants pictures of levels 11-20, which I haven’t included in my clone because they looked a bit suspicious:

11
12
13
14
15
16
17
18
19
20

Yeah, I don’t quite get why the battery isn’t inside the playfield, and there’s no TNT either… If anyone wants to convert these levels into my format, patches are welcome. :)

Copyrights

Copyright status of my clone: I recreated the original graphics in Inkscape. I do not claim any copyright on the graphics. As they are recreated somewhat faithfully, the graphics probably are technically pirated and not copyrightable. The code may or may not be copyrightable. If it is, let’s say it’s GPLv3 for now. However, I disclaim all copyright after release + 15 years.

Lowering the footprint of JT51 (YM2151 Verilog clone) to work on smaller FPGAs, specifically the ICE40UP5K (Part 1? WIP? Progress diary?) / UPduino mini-tutorial

Update 2023/03/09

I will upload the changes necessary to run the JT51 as a drop-in replacement of a real YM2151 relatively soon. Things aren’t 100% ironed out yet.

Update 2023/03/06

The below update states that there are errors in jt51_phrom and jt51_exprom.v, but these errors were minor and have been fixed. However, the fixed jt51_phrom.v doesn’t appear to have a large effect on the final number of LUT4s used. It looks like the mistake I had originally made (a race condition-type of mistake) was responsible for the majority of the savings. Boo.

Here’s a short sound recording with the mistake left in:

And here’s a short sound recording with the mistake ironed out:

In addition, the changes to jt51_sh.v mentioned in the below update might suffer from some problems too. So far I have only managed to run with jt51_sh8 enabled, so I have no way to compare the unmodified jt51_sh implementation to my modified implementation, but I also tried adding jt51_sh10 for another shift register, and that made things sound rather weird. It’s currently not clear to me why that is the case.

Important update 2023/03/01

I finally managed to test the modified code. Do not use it, there are probably errors in it. Using the modified sine tables (jt51_phrom.v) causes everything to sound noisy. Using the modified exprom.v messes something up, but the effect is rather subtle.

Instead, you can save on LUTs by modifying jt51_sh.v as follows. This is the original code:

module jt51_sh #(parameter width=5, stages=32, rstval=1'b0 ) (
    input                           rst,
    input                           clk,
    input                           cen,
    input       [width-1:0]         din,
    output      [width-1:0]         drop
);

reg [stages-1:0] bits[width-1:0];

genvar i;
generate
    for (i=0; i < width; i=i+1) begin: bit_shifter
        always @(posedge clk, posedge rst) begin
            if(rst)
                bits[i] <= {stages{rstval}};
            else if(cen)
                bits[i] <= {bits[i][stages-2:0], din[i]};
        end
        assign drop[i] = bits[i][stages-1];
    end
endgenerate

endmodule

It looks like the logic yosys synthesizes from this code is inefficient. I haven’t looked too much into it, but writing this code out (and removing one of the channels, etc.) causes yosys to synthesize more efficient code. As you can see, this code uses parameters that affect the way it is generated. I just picked one set of parameters that appeared multiple times, width=14 and stages=8, and that was enough to get the logic to just fit. I.e., I appended the following code inside the same file:

module jt51_sh8 #(parameter rstval=1'b0 ) (
    input                           rst,
    input                           clk,
    input                           cen,
    input       [13:0]         din,
    output      [13:0]         drop
);

reg [7:0] bits[13:0];

// jt51_sh #( .width(14), .stages(8)) prev1_buffer(
// reg [7:0] bits[13:0]

genvar i;
// generate
//     for (i=0; i < 14; i=i+1) begin: bit_shifter
//         always @(posedge clk, posedge rst) begin
//             if(rst)
//                 bits[i] <= {8{rstval}};
//             else if(cen)
//                 bits[i] <= {bits[i][6:0], din[i]};
//         end
//         assign drop[i] = bits[i][7];
//     end
// endgenerate
        always @(posedge clk, posedge rst) begin
            if(rst) begin
                bits[0] <= {8{rstval}};
                bits[1] <= {8{rstval}};
                bits[2] <= {8{rstval}};
                bits[3] <= {8{rstval}};
                bits[4] <= {8{rstval}};
                bits[5] <= {8{rstval}};
                bits[6] <= {8{rstval}};
                bits[7] <= {8{rstval}};
                bits[8] <= {8{rstval}};
                bits[9] <= {8{rstval}};
                bits[10] <= {8{rstval}};
                bits[11] <= {8{rstval}};
                bits[12] <= {8{rstval}};
                bits[13] <= {8{rstval}};
            end
            else if(cen) begin
                bits[0] <= {bits[0][6:0], din[0]};
                bits[1] <= {bits[1][6:0], din[1]};
                bits[2] <= {bits[2][6:0], din[2]};
                bits[3] <= {bits[3][6:0], din[3]};
                bits[4] <= {bits[4][6:0], din[4]};
                bits[5] <= {bits[5][6:0], din[5]};
                bits[6] <= {bits[6][6:0], din[6]};
                bits[7] <= {bits[7][6:0], din[7]};
                bits[8] <= {bits[8][6:0], din[8]};
                bits[9] <= {bits[9][6:0], din[9]};
                bits[10] <= {bits[10][6:0], din[10]};
                bits[11] <= {bits[11][6:0], din[11]};
                bits[12] <= {bits[12][6:0], din[12]};
                bits[13] <= {bits[13][6:0], din[13]};
            end
        end
        assign drop[0] = bits[0][7];
        assign drop[1] = bits[0][7];
        assign drop[2] = bits[0][7];
        assign drop[3] = bits[0][7];
        assign drop[4] = bits[0][7];
        assign drop[5] = bits[0][7];
        assign drop[6] = bits[0][7];
        assign drop[7] = bits[0][7];
        assign drop[8] = bits[0][7];
        assign drop[9] = bits[0][7];
        assign drop[10] = bits[0][7];
        assign drop[11] = bits[0][7];
        assign drop[12] = bits[0][7];
        assign drop[13] = bits[0][7];
endmodule

And adjusted jt51_op.v to use jt51_sh8 instead of jt51_sh for prev1_buffer, prevprev1_buffer, and prev2_buffer.

Original post follows:

Quick summary

I took JT51 (https://github.com/jotego/jt51) and shrunk it down a little. I got it down to just barely fit. There are some lookup tables that are processed down by a couple hundred LUT4s, I made the lookup tables contain the already processed values instead. We’re now using slightly more RAM.

How we got here

I am currently debugging a YM2151-based device, the Yamaha SFG-01 sound module for MSX PCs. There is… wonky audio when two notes are played at once on the attached keyboard. I started off by emulating the YM3012 DAC on a Raspberry Pi Pico. More on that in a future post. More on the whole repair in a future post, in fact. My plan was to run the original YM2151 and the FPGA version side-by-side (with the exact same inputs) and to compare the audio outputs. However, after I already did most things detailed in this post, I realized that plan probably wasn’t going to work, as (if I read the datasheet correctly) the YM2151 generates interrupts which probably have to be acknowledged, and the data bus is bidirectional, and actually does get read out by the CPU occasionally. So the original chip and the FPGA would have to work in 100% perfect sync, and who knows how achievable that is.

I have two FPGA boards, and they’re both exactly the same, UPduino v3.0. I bought these back in 2020 or so, expecting I’d maybe come up with a project at some point. They were cheaper back then! I paid 43.20 USD + 6 USD shipping for 2! So per device, in JPY at that time: 21.6 * 103 = 2225 JPY. Currently, the price is $30 per device, and USD/JPY is 133.8. 30 * 133.8 = 4014 JPY, so almost double. Yikes.

Only have an ICE40UP3K? Allegedly, if you use the open-source toolchain, it’ll have exactly the same amount of LUT4s available as an ICE40UP5K. Apparently it’s just the official IDE enforcing an artificial limit?

So all I’d done up to this point was: I installed the open-source toolchain, changed the speed of the LED blinking example, re-flashed, and got some satisfaction that it all worked. Let’s start from that point. I think the official tutorials should get you there (except for the speed change maybe).

Also: important: I haven’t tested my revised Verilog yet. That’s something for part 2 (not done/written yet).

Going beyond the rgb_blink example

This is the first time I’m compiling feral Verilog code for this board, so I took notes along the way. This blog post is just what I’d written down, just polished a little. First of all, make sure you can compile and flash the rgb_blink example. Follow the documentation, at the very least https://upduino.readthedocs.io/en/latest/getting_started/tool_installation.html and https://upduino.readthedocs.io/en/latest/tutorials/blink_led.html.

Then, git clone https://github.com/jotego/jt51. Copy UPduino-v3.0/RTL/common from the toolchain to jt51/ and UPduino-v3.0/RTL/blink_led/Makefile to jt51/hdl/. Perhaps cd to jt51/hdl and modify the Makefile as follows.

Note: Makefiles consist of rules laying out how to build a certain file. Rule blocks start like this: “filename: dependencies”. The dependencies are filenames. There is only one rule in our Makefile that directly depends on .v files:

rgb_blink.json: rgb_blink.v

Instead of rgb_blink.v, we’ll replace that by all the jt51_….v files we have in jt51/hdl:

jt51_acc.v jt51_csr_ch.v jt51_csr_op.v jt51_eg.v jt51_exp2lin.v jt51_exprom.v jt51_kon.v jt51_lfo.v jt51_lin2exp.v jt51_mmr.v jt51_mod.v jt51_noise_lfsr.v jt51_noise.v jt51_op.v jt51_pg.v jt51_phinc_rom.v jt51_phrom.v jt51_pm.v jt51_reg.v jt51_sh.v jt51_timers.v jt51.v

Then also change the vosys command to synthesize from these .v files instead of rgb_blink.v:

yosys -q -p "synth_ice40 -json rgb_blink.json" jt51_acc.v jt51_csr_ch.v jt51_csr_op.v jt51_eg.v jt51_exp2lin.v jt51_exprom.v jt51_kon.v jt51_lfo.v jt51_lin2exp.v jt51_mmr.v jt51_mod.v jt51_noise_lfsr.v jt51_noise.v jt51_op.v jt51_pg.v jt51_phinc_rom.v jt51_phrom.v jt51_pm.v jt51_reg.v jt51_sh.v jt51_timers.v jt51.v

And finally, let’s change all names from “rgb_blink” to “jt51” using search and replace: “rgb_blink” -> “jt51”. You should end up with a Makefile like this:

# Makefile to build UPduino v3.0 rgb_blink.v  with icestorm toolchain
# Original Makefile is taken from: 
# https://github.com/tomverbeure/upduino/tree/master/blink
# On Linux, copy the included upduinov3.rules to /etc/udev/rules.d/ so that we don't have
# to use sudo to flash the bit file.
# Thanks to thanhtranhd for making changes to thsi makefile.

rgb_blink.bin: rgb_blink.asc
	icepack rgb_blink.asc rgb_blink.bin

rgb_blink.asc: rgb_blink.json ../common/upduino.pcf
	nextpnr-ice40 --up5k --package sg48 --json rgb_blink.json --pcf ../common/upduino.pcf --asc rgb_blink.asc   # run place and route

rgb_blink.json: rgb_blink.v
	yosys -q -p "synth_ice40 -json rgb_blink.json" rgb_blink.v

.PHONY: flash
flash:
	iceprog -d i:0x0403:0x6014 rgb_blink.bin

.PHONY: clean
clean:
	$(RM) -f rgb_blink.json rgb_blink.asc rgb_blink.bin

Make sure you have tab characters, not space characters in the rule block indentation. (Trap for young players.) Make sure you also copied the common/ directory as instructed above. Then, execute “make”. If you get the following error:

$ make
nextpnr-ice40 --up5k --package sg48 --json jt51.json --pcf ../common/upduino.pcf --asc jt51.asc   # run place and route
/bin/sh: 1: nextpnr-ice40: not found
make: *** [Makefile:12: jt51.asc] Error 127

That means you need nextpnr-ice40 in your PATH. Figure out the path, and then execute:

PATH=$PATH:/path/to/directory/containing/nextpnr-ice40

Next, you should get the following error:

$ make
nextpnr-ice40 --up5k --package sg48 --json jt51.json --pcf ../common/upduino.pcf --asc jt51.asc   # run place and route
ERROR: IO 'xright[15]' is unconstrained in PCF (override this error with --pcf-allow-unconstrained)
ERROR: Loading PCF failed.
0 warnings, 2 errors
make: *** [Makefile:12: jt51.asc] Error 255

For now, override this error as instructed, by changing the nextpnr-ice40 command in the Makefile as follows:

nextpnr-ice40 --up5k --package sg48 --json jt51.json --pcf ../common/upduino.pcf --asc jt51.asc --pcf-allow-unconstrained

At this point we’ll finally get some actually interesting error output.

As-is, the project doesn’t fit on the ICE40

...
Info: Device utilisation:
Info:            ICESTORM_LC:  6680/ 5280   126%
Info:           ICESTORM_RAM:     6/   30    20%
Info:                  SB_IO:    91/   96    94%
Info:                  SB_GB:     8/    8   100%
Info:           ICESTORM_PLL:     0/    1     0%
Info:            SB_WARMBOOT:     0/    1     0%
Info:           ICESTORM_DSP:     0/    8     0%
Info:         ICESTORM_HFOSC:     0/    1     0%
Info:         ICESTORM_LFOSC:     0/    1     0%
Info:                 SB_I2C:     0/    2     0%
Info:                 SB_SPI:     0/    2     0%
Info:                 IO_I3C:     0/    2     0%
Info:            SB_LEDDA_IP:     0/    1     0%
Info:            SB_RGBA_DRV:     0/    1     0%
Info:         ICESTORM_SPRAM:     0/    4     0%

Info: Placed 0 cells based on constraints.
ERROR: Unable to place cell '$abc$113462$auto$blifparse.cc:492:parse_blif$114175_LC', no BELs remaining to implement cell type 'ICESTORM_LC'
91 warnings, 1 error
make: *** [Makefile:13: jt51.asc] Error 255

Okay, first things first. How old is our toolchain?

$ yosys -V
Yosys 0.8 (git sha1 5706e90)

Let’s see, the newest version of yosys, at the time of this writing, is… 0.26. Wait what? Ah, it looks like a smaller number, but is probably intended to be a larger number. It appears that my version is from 2018. Likely, I’d just installed it from Debian’s repositories. Let’s try building yosys from Git so we can upgrade from 0.8 to 0.26. It would like to build using clang by default, but you can build using gcc too. You also need tcl8.6-dev (or probably other versions work fine too).

$ git clone https://github.com/YosysHQ/yosys
$ cd yosys
$ make
/bin/sh: 1: clang: not found
[  0%] Building kernel/version_4c334b905.cc
[  0%] Building kernel/version_4c334b905.o
/bin/sh: 1: clang: not found
make: *** [Makefile:754: kernel/version_4c334b905.o] Error 12
$ make config-gcc
...
In file included from kernel/calc.cc:24:
./kernel/yosys.h:81:12: fatal error: tcl.h: No such file or directory
 #  include <tcl.h>
...
$ sudo apt-get install tcl8.6-dev
...
$ make config-gcc
...
$ # success

And if we try synthesizing again now, we do get a significant improvement. (Also synthesis time is faster I think.) But we are not quite there yet:

Info: Device utilisation:
Info:            ICESTORM_LC:  5836/ 5280   110%
Info:           ICESTORM_RAM:     3/   30    10%
Info:                  SB_IO:    91/   96    94%
Info:                  SB_GB:     8/    8   100%
Info:           ICESTORM_PLL:     0/    1     0%
Info:            SB_WARMBOOT:     0/    1     0%
Info:           ICESTORM_DSP:     0/    8     0%
Info:         ICESTORM_HFOSC:     0/    1     0%
Info:         ICESTORM_LFOSC:     0/    1     0%
Info:                 SB_I2C:     0/    2     0%
Info:                 SB_SPI:     0/    2     0%
Info:                 IO_I3C:     0/    2     0%
Info:            SB_LEDDA_IP:     0/    1     0%
Info:            SB_RGBA_DRV:     0/    1     0%
Info:         ICESTORM_SPRAM:     0/    4     0%

Shrinking the footprint by changing yosys options (using DSP cells)

110% isn’t too far from where we need to be, so let’s investigate if we can do anything to reduce our FPGA footprint. First of all, there are three files that include the word ‘rom’, which may have a significant effect on our footprint. But it looks like our toolchain is clever — it actually uses ICESTORM_RAM to implement the ROM. (Replacing the entire case/endcase block in the rather large jt51_phinc_rom.v file with a single statement reduced the LC count by 2-3%, and ICESTORM_RAM from 10% to 0%.)

Next, we forget about yosys for a second, and attempt to synthesize this using the official toolchain from Lattice, IceCube2. You’ll need an account and follow a link to generate a license file. You need to enter a MAC address to bind the license to a certain computer. (Or maybe a computer with a certain network adapter.)

IceCube2’s synthesis finishes in a few seconds, and only uses 11 logic cells. Hmm, so efficient! Or more likely, something’s weird. And yes, indeed it’s getting confused and thinks that jt51_noise_lfsr.v is the main file. Apparently, this file’s modules aren’t actually used anywhere. So we get rid of that file (and also get rid of it in our Makefile above) and re-synthesize. Synthesis finishes successfully, and apparently uses 1698 LUTs. Hmm, really? (No, but let me go off a quick tangent first.)

Okay, let’s assume for a second that yosys is much, much worse than IceCube2. It’s time to google for something like ‘yosys vs icecube2’. A person on the EEVblog forums says, “The IceCube2 generates smaller and faster design (most visible with larger designs) than the IceStorm does, it can infer ie. multipliers with built-in DSP modules (UP5k) etc. The IceStorm is less effective, and infers ie. multipliers in fabric (you have to instantiate the modules/primitives manually).” Hmm, interesting. Well, it turns out you can enable the DSP modules in yosys using the -dsp option, so we modify the Makefile as follows:

yosys -q -p "synth_ice40 -dsp -json jt51.json" jt51_acc.v jt51_csr_ch.v jt51_csr_op.v jt51_eg.v jt51_exp2lin.v jt51_exprom.v jt51_kon.v jt51_lfo.v jt51_lin2exp.v jt51_mmr.v jt51_mod.v jt51_noise.v jt51_op.v jt51_pg.v jt51_phinc_rom.v jt51_phrom.v jt51_pm.v jt51_reg.v jt51_sh.v jt51_timers.v jt51.v

That reduces our LUT count by ~2% percent. Every percent counts, but we’re not quite there yet. Looking at https://github.com/YosysHQ/yosys/blob/master/techlibs/ice40/synth_ice40.cc, we see a few more options we could try, e.g., -spram, -noabc, -abc2, -abc9 (experimental), -flowmap (experimental).

-noabc brings us back up to 120%. -flowmap also increases the number of logic cells to a similar number. -abc2 eliminates 19 logic cells vs. just -abc, but that’s not a lot, and our percentage doesn’t change. -abc9 doesn’t yield much of an improvement either. Hmm, looks like we’ve exhausted some of the lower hanging fruit. Anyway, let’s take another closer look at the official toolchain’s output. When your eyes get a little more used to its output you actually notice that it says:

Cell usage:
GND             39 uses
SB_CARRY        366 uses
SB_DFF          22 uses
SB_DFFE         276 uses
SB_DFFER        2709 uses
SB_DFFES        747 uses
SB_DFFESR       8 uses
SB_DFFESS       10 uses
SB_DFFR         29 uses
SB_DFFS         1 use
SB_DFFSR        23 uses
SB_GB           3 uses
SB_RAM1024x4    3 uses
VCC             39 uses
SB_MAC16        2 uses
    MULTONLY    1 use
    MULTADD     1 use
SB_LUT4         1698 uses

Hey. 1698 LUTs, but 3825 DFFs, and the P&R Flow tool confirms this:

Number of LUTs      :   1698
Number of DFFs      :   3825
Number of Carrys    :   366

These DFFs also use up LUTs, so the total number of LUTs used is 5523, which is actually extremely close to yosys, and also too much. (Note that I already edited the Verilog a little bit at this point, so the number on an unmodified repository would be a little higher.)

Let’s remove the -q option from yosynth’s synth_ice40 command in the Makefile, and take a look at the output close to the summary that we looked at before. Scrolling way past a lot of verbose output, we get a summary like the following, and can see that yosys is indeed very close.

Info: Packing constants..
Info: Packing IOs..
Info: Packing LUT-FFs..
Info:     1462 LCs used as LUT4 only
Info:      515 LCs used as LUT4 and DFF
Info: Packing non-LUT FFs..
Info:     3367 LCs used as DFF only
Info: Packing carries..
Info:      184 LCs used as CARRY only
Info: Packing indirect carry+LUT pairs...
Info:       63 LUTs merged into carry LCs

Shrinking the footprint by removing features

Next, we could try and cut down on features in order to reduce the required number of logic cells. First of all, I nuked the entire right channel (“right” and “xright”) by commenting out a couple lines in jt51.v and jt51_acc.v. That shaved off about 2%. I kept “xleft” but also got rid of the converted “left”. That means we no longer need to compile jt51_exp2lin.v, which seems to save 9 LUTs.

Shrinking the footprint by trading LUTs for RAM

A cursory (liar liar pants on fire) glance over the code revealed an opportunity to potentially save a more significant number of LUTs. In jt51_op.v, we refer to a sine table (which is in jt_phrom.v) and concatenate certain bits from this table. In the following snippet, the sine table is already in the sta_XI register:

case( phaselo_XI[7:6] )
    2'b00: stb = { 10'b0, sta_XI[29], sta_XI[25], 2'b0, sta_XI[18], 
        sta_XI[14], 1'b0, sta_XI[7] , sta_XI[3] };
    2'b01: stb = { 6'b0 , sta_XI[37], sta_XI[34], 2'b0, sta_XI[28], 
        sta_XI[24], 2'b0, sta_XI[17], sta_XI[13], sta_XI[10], sta_XI[6], sta_XI[2] };
    2'b10: stb = { 2'b0, sta_XI[43], sta_XI[41], 2'b0, sta_XI[36],
        sta_XI[33], 2'b0, sta_XI[27], sta_XI[23], 1'b0, sta_XI[20],
        sta_XI[16], sta_XI[12], sta_XI[9], sta_XI[5], sta_XI[1] };
    2'b11: stb = {
            sta_XI[45], sta_XI[44], sta_XI[42], sta_XI[40]
        , sta_XI[39], sta_XI[38], sta_XI[35], sta_XI[32]
        , sta_XI[31], sta_XI[30], sta_XI[26], sta_XI[22]
        , sta_XI[21], sta_XI[19], sta_XI[15], sta_XI[11]
        , sta_XI[8], sta_XI[4], sta_XI[0] };
    default: stb = 19'dx;

If you are new to Verilog, numbers often look like this: <total bit width>'<letter indicating number format, e.g., b for binary><number>. The array indices refer to bit numbers. E.g., sta_XI[38] is bit 38 in sta_XI, counting from 0. “case” is like a switch statement in C. So up here, we do something like:

switch(bits 7 and 6 of phaselo_XI) {
    case 0: ...;
    case 1: ...;
    case 2: ...;
    case 3: ...;
    default: ...;
}

(The “default” clause is extraneous, but doesn’t cause harm.)

The sine table is fairly large, at 32 entries of 46 bits. In the above code snippet, we pick (to me, super random) bits from the table and also insert constant 0s and 1s here and there. E.g., the first line reads in plain words: ten 0s, followed by sinetable[i][29], followed by sinetable[i][25], followed by two 0s, etc. The sine table isn’t used anywhere else.

Our opportunity is: instead of generating a circuit to combine bits from the sinetable together, we can just rewrite the sine lookup table to already contain what we call stb above. It doesn’t matter if our table ends up a little larger (it could be up to four times larger), because as mentioned above, RAM is used to store these tables. But our table isn’t that much larger, really. Before we had 32×46=1472 bits, now we have a three-dimensional array of dimensions 4x32x19=2432 bits, not even twice as large.

This optimization takes us to 5363/5280 (101%), which means we’re almost done! (If we use four two-dimensional arrays and a case block, the savings are much less pronounced, 104%.) Of course, there is no free lunch: we now use more RAM: ICESTORM_RAM 5/30 (16%). Before it was 3/30 (10%). But we still have a lot of RAM left.

Rewriting the table by hand presumably gets old quickly, so I wrote a short Perl script to do it. (Luckily, it can sometimes be very easy to transform Verilog source code to Perl using find and replace with regular expressions.)

#!/usr/bin/perl

$sta_XI = [[0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0],
    [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1],
    [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1],
    [0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1],
    [0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0],
    [0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0],
    [0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1],
    [0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1],
    [0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1],
    [0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1],
    [0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1],
    [0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1],
    [0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1],
    [0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1],
    [0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1],
    [0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1],
    [0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0],
    [0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1],
    [1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0],
    [1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1],
    [1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0],
    [1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1],
    [1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1]];

for $i (0..31) {
    $stb->[0][$i] = [ (0)x10, $sta_XI->[$i][29], $sta_XI->[$i][25], (0)x2, $sta_XI->[$i][18], 
            $sta_XI->[$i][14], 0, $sta_XI->[$i][7] , $sta_XI->[$i][3] ];
    $stb->[1][$i] = [ (0)x6, $sta_XI->[$i][37], $sta_XI->[$i][34], (0)x2, $sta_XI->[$i][28], 
            $sta_XI->[$i][24], (0)x2, $sta_XI->[$i][17], $sta_XI->[$i][13], $sta_XI->[$i][10], $sta_XI->[$i][6], $sta_XI->[$i][2] ];
    $stb->[2][$i] = [ (0)x2, $sta_XI->[$i][43], $sta_XI->[$i][41], (0)x2, $sta_XI->[$i][36],
            $sta_XI->[$i][33], (0)x2, $sta_XI->[$i][27], $sta_XI->[$i][23], 0, $sta_XI->[$i][20],
            $sta_XI->[$i][16], $sta_XI->[$i][12], $sta_XI->[$i][9], $sta_XI->[$i][5], $sta_XI->[$i][1] ];
    $stb->[3][$i] = [$sta_XI->[$i][45], $sta_XI->[$i][44], $sta_XI->[$i][42], $sta_XI->[$i][40],
            $sta_XI->[$i][39], $sta_XI->[$i][38], $sta_XI->[$i][35], $sta_XI->[$i][32],
            $sta_XI->[$i][31], $sta_XI->[$i][30], $sta_XI->[$i][26], $sta_XI->[$i][22],
            $sta_XI->[$i][21], $sta_XI->[$i][19], $sta_XI->[$i][15], $sta_XI->[$i][11],
            $sta_XI->[$i][8], $sta_XI->[$i][4], $sta_XI->[$i][0] ];
}

for $j (0..3) {
    for $i (0..31) {
        print "stb[$j]\[5'd$i] = 19'b";
        for $k (0..18) {
            print $stb->[$j][$i][$k];
        }
        print ";\n"
    }
}

We could actually go even further; looking a little further ahead, stb is only used to fill in stf and stg:

    stf = { stb[18:15], stb[12:11], stb[8:7], stb[4:3], stb[0] };
    // Gated value to sum; bit 14 is indeed used twice
    if( phaselo_XI[0] )
        stg = { 2'b0, stb[14], stb[14:13], stb[10:9], stb[6:5], stb[2:1] };
    else
        stg = 11'd0;

Which means we could change our lookup table once more and directly read out stf and stg. However, scrolling down a little further in the same file, we see the same kind of pattern in the code doing the post-processing for jt51_exprom, so let’s tackle that one instead. Changing jt51_exprom to directly return etf and etg gets us: 5196/ 5280 (98%). Yay!

Now, if we wanted to make a drop-in replacement for an actual YM2151 chip, we’d have to serialize sound output. JT51 outputs xleft/xright/left/right using 16 IO pins each. (We don’t even have enough IO pins on our FPGA.) But the actual YM2151 uses four pins: clock, SH1, SH2, and SO. SO is the serialized representation of left/right, synced with clock. SH1 is high if SO is currently outputting left, SH2 is high is if SO is currently outputting right. In order to implement that, we need a few more LUTs.

Anyway, that was a rather long-winded explanation. Below is the code. I also have it on https://github.com/qiqitori/jt51. Note that the code hasn’t been tested yet at the time of this writing.

Revised jt51_phrom.v (still GPLv3 or later but the copyright header is a little too big for this space):

module jt51_phrom
(
	input [4:0] addr,
	input clk,
	input cen,
	input [1:0] phaselo_XI_76,
	output reg [18:0] ph,
);
	reg [18:0] stb[3:0][31:0];
	initial
	begin
		stb[0][5'd0] = 19'b0000000000000000001;
		stb[0][5'd1] = 19'b0000000000100000001;
		stb[0][5'd2] = 19'b0000000000000000001;
		stb[0][5'd3] = 19'b0000000000100000001;
		stb[0][5'd4] = 19'b0000000000100010001;
		stb[0][5'd5] = 19'b0000000000000010001;
		stb[0][5'd6] = 19'b0000000000100010001;
		stb[0][5'd7] = 19'b0000000000100000001;
		stb[0][5'd8] = 19'b0000000000000000001;
		stb[0][5'd9] = 19'b0000000000100000001;
		stb[0][5'd10] = 19'b0000000000100010001;
		stb[0][5'd11] = 19'b0000000000000010001;
		stb[0][5'd12] = 19'b0000000000000000000;
		stb[0][5'd13] = 19'b0000000000100000000;
		stb[0][5'd14] = 19'b0000000000110000000;
		stb[0][5'd15] = 19'b0000000000100010000;
		stb[0][5'd16] = 19'b0000000000000010000;
		stb[0][5'd17] = 19'b0000000000000000000;
		stb[0][5'd18] = 19'b0000000000000000000;
		stb[0][5'd19] = 19'b0000000000010010000;
		stb[0][5'd20] = 19'b0000000000000010000;
		stb[0][5'd21] = 19'b0000000000110010000;
		stb[0][5'd22] = 19'b0000000000110000001;
		stb[0][5'd23] = 19'b0000000000110000001;
		stb[0][5'd24] = 19'b0000000000010010001;
		stb[0][5'd25] = 19'b0000000000010000001;
		stb[0][5'd26] = 19'b0000000000010001001;
		stb[0][5'd27] = 19'b0000000000010011000;
		stb[0][5'd28] = 19'b0000000000110011000;
		stb[0][5'd29] = 19'b0000000000110000010;
		stb[0][5'd30] = 19'b0000000000010011011;
		stb[0][5'd31] = 19'b0000000000010010000;

		stb[1][5'd0] = 19'b0000000100100011100;
		stb[1][5'd1] = 19'b0000001000000001100;
		stb[1][5'd2] = 19'b0000001000000001100;
		stb[1][5'd3] = 19'b0000001000100000000;
		stb[1][5'd4] = 19'b0000001000100000000;
		stb[1][5'd5] = 19'b0000001100000001000;
		stb[1][5'd6] = 19'b0000001000000001000;
		stb[1][5'd7] = 19'b0000001100100001000;
		stb[1][5'd8] = 19'b0000001000100000100;
		stb[1][5'd9] = 19'b0000000000010010100;
		stb[1][5'd10] = 19'b0000000000110011100;
		stb[1][5'd11] = 19'b0000000000110011100;
		stb[1][5'd12] = 19'b0000000100010010000;
		stb[1][5'd13] = 19'b0000001100110011000;
		stb[1][5'd14] = 19'b0000001100110011000;
		stb[1][5'd15] = 19'b0000001100010000100;
		stb[1][5'd16] = 19'b0000000000110001100;
		stb[1][5'd17] = 19'b0000000000010001100;
		stb[1][5'd18] = 19'b0000000100010000000;
		stb[1][5'd19] = 19'b0000001100110001000;
		stb[1][5'd20] = 19'b0000001000010010100;
		stb[1][5'd21] = 19'b0000001000100011100;
		stb[1][5'd22] = 19'b0000001000000010001;
		stb[1][5'd23] = 19'b0000000000100011001;
		stb[1][5'd24] = 19'b0000000000010001101;
		stb[1][5'd25] = 19'b0000000000110000001;
		stb[1][5'd26] = 19'b0000001100000000101;
		stb[1][5'd27] = 19'b0000000100110000001;
		stb[1][5'd28] = 19'b0000000000000011101;
		stb[1][5'd29] = 19'b0000001000110011101;
		stb[1][5'd30] = 19'b0000000100010010001;
		stb[1][5'd31] = 19'b0000001100100010111;

		stb[2][5'd0] = 19'b0001001000000000000;
		stb[2][5'd1] = 19'b0000001100000000000;
		stb[2][5'd2] = 19'b0000001100010000000;
		stb[2][5'd3] = 19'b0001001100010000010;
		stb[2][5'd4] = 19'b0000001000000000010;
		stb[2][5'd5] = 19'b0001001000000000010;
		stb[2][5'd6] = 19'b0001001100000000010;
		stb[2][5'd7] = 19'b0011001000010001010;
		stb[2][5'd8] = 19'b0011001000010001010;
		stb[2][5'd9] = 19'b0010001100010001010;
		stb[2][5'd10] = 19'b0001001000010001010;
		stb[2][5'd11] = 19'b0011001100110001010;
		stb[2][5'd12] = 19'b0011001000110000101;
		stb[2][5'd13] = 19'b0000001100100000101;
		stb[2][5'd14] = 19'b0010000000100000101;
		stb[2][5'd15] = 19'b0001001000110000101;
		stb[2][5'd16] = 19'b0010001100100000101;
		stb[2][5'd17] = 19'b0001001100010101101;
		stb[2][5'd18] = 19'b0011001000000101111;
		stb[2][5'd19] = 19'b0000000000010101111;
		stb[2][5'd20] = 19'b0001001000110101111;
		stb[2][5'd21] = 19'b0010000100110101111;
		stb[2][5'd22] = 19'b0011000100100100001;
		stb[2][5'd23] = 19'b0001000100110100001;
		stb[2][5'd24] = 19'b0001000000000010001;
		stb[2][5'd25] = 19'b0001000000010011011;
		stb[2][5'd26] = 19'b0000000000100011011;
		stb[2][5'd27] = 19'b0001000100110011000;
		stb[2][5'd28] = 19'b0001000000100011000;
		stb[2][5'd29] = 19'b0000000100000110110;
		stb[2][5'd30] = 19'b0000000000010110110;
		stb[2][5'd31] = 19'b0010000100100110111;

		stb[3][5'd0] = 19'b0100100101101000010;
		stb[3][5'd1] = 19'b1000100000100101010;
		stb[3][5'd2] = 19'b0001100101110101010;
		stb[3][5'd3] = 19'b0101100000110001010;
		stb[3][5'd4] = 19'b1011100101100101010;
		stb[3][5'd5] = 19'b0111100000111001010;
		stb[3][5'd6] = 19'b0110100100111101010;
		stb[3][5'd7] = 19'b0011110001101101010;
		stb[3][5'd8] = 19'b1101100111111001010;
		stb[3][5'd9] = 19'b0101011011010101010;
		stb[3][5'd10] = 19'b0111100011000001010;
		stb[3][5'd11] = 19'b1101100101010101010;
		stb[3][5'd12] = 19'b1101011001001001010;
		stb[3][5'd13] = 19'b0111001000001001010;
		stb[3][5'd14] = 19'b0100100111011101010;
		stb[3][5'd15] = 19'b1011100110000000110;
		stb[3][5'd16] = 19'b1111110010110000110;
		stb[3][5'd17] = 19'b1001011000101100110;
		stb[3][5'd18] = 19'b1111011100111100110;
		stb[3][5'd19] = 19'b1000011111101000110;
		stb[3][5'd20] = 19'b1001100101110000110;
		stb[3][5'd21] = 19'b1110001001010010110;
		stb[3][5'd22] = 19'b1100011010001110100;
		stb[3][5'd23] = 19'b1111011010111110100;
		stb[3][5'd24] = 19'b1010001001010011100;
		stb[3][5'd25] = 19'b0100011010100111100;
		stb[3][5'd26] = 19'b1101001000011101100;
		stb[3][5'd27] = 19'b0110011000001101101;
		stb[3][5'd28] = 19'b1011001010110111101;
		stb[3][5'd29] = 19'b0001011001000001101;
		stb[3][5'd30] = 19'b1001011010101011101;
		stb[3][5'd31] = 19'b1101011000111011101;
	end

	always @(posedge clk)
		addr_latched <= addr;

	always @(*)
		ph <= stb[phaselo_XI_76][clk ? addr : addr_latched]; // addr_latched might be stale on clk edge
endmodule

Update 2023/03/06: fixed inaccurate conversion. Bold lines mark changes from last time.

In jt_op.v, replace the original jt51_phrom “call” as follows:

jt51_phrom u_phrom(
    .clk    ( clk       ),
    .cen    ( cen       ),
    .addr   ( aux_X[5:1]),
    .phaselo_XI_76 ( phaselo_XI[7:6] ),
    .ph     ( stb    )
);

And jt51_exprom.v, also GPL 3 or later but header removed for brevity.

module jt51_exprom
(
    input [4:0]         addr,
    input               clk,
    input               cen,
    input [1:0]         totalatten_XII_76,
    output reg [9:0]        etf,
    output reg [2:0]        etg
);
    reg [9:0] explut_etf[31:0];
    reg [2:0] explut_etg[31:0];
    initial
    begin
        explut_etf[0][5'd0] = 10'b1110110111;
        explut_etf[0][5'd1] = 10'b1110101011;
        explut_etf[0][5'd2] = 10'b1110011101;
        explut_etf[0][5'd3] = 10'b1110000101;
        explut_etf[0][5'd4] = 10'b1110100001;
        explut_etf[0][5'd5] = 10'b1110110110;
        explut_etf[0][5'd6] = 10'b1001001010;
        explut_etf[0][5'd7] = 10'b1110011100;
        explut_etf[0][5'd8] = 10'b1110000100;
        explut_etf[0][5'd9] = 10'b1110010000;
        explut_etf[0][5'd10] = 10'b1110110111;
        explut_etf[0][5'd11] = 10'b1110101011;
        explut_etf[0][5'd12] = 10'b1110111101;
        explut_etf[0][5'd13] = 10'b1110100101;
        explut_etf[0][5'd14] = 10'b1110110001;
        explut_etf[0][5'd15] = 10'b1110101110;
        explut_etf[0][5'd16] = 10'b1110111010;
        explut_etf[0][5'd17] = 10'b1110100010;
        explut_etf[0][5'd18] = 10'b1110110100;
        explut_etf[0][5'd19] = 10'b1110101000;
        explut_etf[0][5'd20] = 10'b1110111111;
        explut_etf[0][5'd21] = 10'b1110100111;
        explut_etf[0][5'd22] = 10'b1110110011;
        explut_etf[0][5'd23] = 10'b1110101101;
        explut_etf[0][5'd24] = 10'b1010000101;
        explut_etf[0][5'd25] = 10'b1110010001;
        explut_etf[0][5'd26] = 10'b1110001110;
        explut_etf[0][5'd27] = 10'b1110011010;
        explut_etf[0][5'd28] = 10'b1110100010;
        explut_etf[0][5'd29] = 10'b1110110100;
        explut_etf[0][5'd30] = 10'b1010011000;
        explut_etf[0][5'd31] = 10'b1110000000;
        explut_etf[1][5'd0] = 10'b0010000010;
        explut_etf[1][5'd1] = 10'b1101001100;
        explut_etf[1][5'd2] = 10'b0011000100;
        explut_etf[1][5'd3] = 10'b0011001000;
        explut_etf[1][5'd4] = 10'b0011000000;
        explut_etf[1][5'd5] = 10'b0010101111;
        explut_etf[1][5'd6] = 10'b0010100111;
        explut_etf[1][5'd7] = 10'b0011101011;
        explut_etf[1][5'd8] = 10'b0011100011;
        explut_etf[1][5'd9] = 10'b0010011101;
        explut_etf[1][5'd10] = 10'b0010010101;
        explut_etf[1][5'd11] = 10'b0011011001;
        explut_etf[1][5'd12] = 10'b1100110001;
        explut_etf[1][5'd13] = 10'b0010111110;
        explut_etf[1][5'd14] = 10'b0011110110;
        explut_etf[1][5'd15] = 10'b0010000110;
        explut_etf[1][5'd16] = 10'b1101001010;
        explut_etf[1][5'd17] = 10'b1100100010;
        explut_etf[1][5'd18] = 10'b1101101100;
        explut_etf[1][5'd19] = 10'b1100010100;
        explut_etf[1][5'd20] = 10'b0010011000;
        explut_etf[1][5'd21] = 10'b1100110000;
        explut_etf[1][5'd22] = 10'b1101111111;
        explut_etf[1][5'd23] = 10'b1100001111;
        explut_etf[1][5'd24] = 10'b1101000111;
        explut_etf[1][5'd25] = 10'b1100101011;
        explut_etf[1][5'd26] = 10'b0011100011;
        explut_etf[1][5'd27] = 10'b0010011101;
        explut_etf[1][5'd28] = 10'b1100110101;
        explut_etf[1][5'd29] = 10'b1101111001;
        explut_etf[1][5'd30] = 10'b0010001001;
        explut_etf[1][5'd31] = 10'b1100100001;
        explut_etf[2][5'd0] = 10'b0101000110;
        explut_etf[2][5'd1] = 10'b0100001010;
        explut_etf[2][5'd2] = 10'b0110111100;
        explut_etf[2][5'd3] = 10'b0111010100;
        explut_etf[2][5'd4] = 10'b0110011000;
        explut_etf[2][5'd5] = 10'b0111100000;
        explut_etf[2][5'd6] = 10'b0110101111;
        explut_etf[2][5'd7] = 10'b0111000111;
        explut_etf[2][5'd8] = 10'b0110001011;
        explut_etf[2][5'd9] = 10'b0111111101;
        explut_etf[2][5'd10] = 10'b0101110101;
        explut_etf[2][5'd11] = 10'b0100111001;
        explut_etf[2][5'd12] = 10'b0101010001;
        explut_etf[2][5'd13] = 10'b0110011110;
        explut_etf[2][5'd14] = 10'b0100010110;
        explut_etf[2][5'd15] = 10'b0111101010;
        explut_etf[2][5'd16] = 10'b0101100010;
        explut_etf[2][5'd17] = 10'b0100101100;
        explut_etf[2][5'd18] = 10'b0100100100;
        explut_etf[2][5'd19] = 10'b0111001000;
        explut_etf[2][5'd20] = 10'b0101000000;
        explut_etf[2][5'd21] = 10'b0101001111;
        explut_etf[2][5'd22] = 10'b0010000111;
        explut_etf[2][5'd23] = 10'b0000001011;
        explut_etf[2][5'd24] = 10'b0000000011;
        explut_etf[2][5'd25] = 10'b0000001101;
        explut_etf[2][5'd26] = 10'b0000000101;
        explut_etf[2][5'd27] = 10'b0000001001;
        explut_etf[2][5'd28] = 10'b0000000001;
        explut_etf[2][5'd29] = 10'b0000001110;
        explut_etf[2][5'd30] = 10'b0000000110;
        explut_etf[2][5'd31] = 10'b0000001010;
        explut_etf[3][5'd0] = 10'b0010101011;
        explut_etf[3][5'd1] = 10'b0010010101;
        explut_etf[3][5'd2] = 10'b0010111110;
        explut_etf[3][5'd3] = 10'b0001001010;
        explut_etf[3][5'd4] = 10'b0001100100;
        explut_etf[3][5'd5] = 10'b0010111111;
        explut_etf[3][5'd6] = 10'b0010001011;
        explut_etf[3][5'd7] = 10'b0010100101;
        explut_etf[3][5'd8] = 10'b0010111110;
        explut_etf[3][5'd9] = 10'b0010001010;
        explut_etf[3][5'd10] = 10'b0010010100;
        explut_etf[3][5'd11] = 10'b0010111111;
        explut_etf[3][5'd12] = 10'b0010101011;
        explut_etf[3][5'd13] = 10'b0001010101;
        explut_etf[3][5'd14] = 10'b0010000001;
        explut_etf[3][5'd15] = 10'b0010011010;
        explut_etf[3][5'd16] = 10'b0010001100;
        explut_etf[3][5'd17] = 10'b0010010000;
        explut_etf[3][5'd18] = 10'b0010000111;
        explut_etf[3][5'd19] = 10'b0010011101;
        explut_etf[3][5'd20] = 10'b0010001001;
        explut_etf[3][5'd21] = 10'b0010010110;
        explut_etf[3][5'd22] = 10'b0010000010;
        explut_etf[3][5'd23] = 10'b0010011000;
        explut_etf[3][5'd24] = 10'b0010101111;
        explut_etf[3][5'd25] = 10'b0010110011;
        explut_etf[3][5'd26] = 10'b0010100101;
        explut_etf[3][5'd27] = 10'b0010000001;
        explut_etf[3][5'd28] = 10'b0010011010;
        explut_etf[3][5'd29] = 10'b0010101100;
        explut_etf[3][5'd30] = 10'b0000001000;
        explut_etf[3][5'd31] = 10'b0010010111;
        explut_etg[0][5'd0] = 3'b101;
        explut_etg[0][5'd1] = 3'b101;
        explut_etg[0][5'd2] = 3'b101;
        explut_etg[0][5'd3] = 3'b101;
        explut_etg[0][5'd4] = 3'b101;
        explut_etg[0][5'd5] = 3'b101;
        explut_etg[0][5'd6] = 3'b101;
        explut_etg[0][5'd7] = 3'b101;
        explut_etg[0][5'd8] = 3'b101;
        explut_etg[0][5'd9] = 3'b101;
        explut_etg[0][5'd10] = 3'b110;
        explut_etg[0][5'd11] = 3'b110;
        explut_etg[0][5'd12] = 3'b110;
        explut_etg[0][5'd13] = 3'b110;
        explut_etg[0][5'd14] = 3'b110;
        explut_etg[0][5'd15] = 3'b110;
        explut_etg[0][5'd16] = 3'b110;
        explut_etg[0][5'd17] = 3'b110;
        explut_etg[0][5'd18] = 3'b110;
        explut_etg[0][5'd19] = 3'b110;
        explut_etg[0][5'd20] = 3'b100;
        explut_etg[0][5'd21] = 3'b100;
        explut_etg[0][5'd22] = 3'b100;
        explut_etg[0][5'd23] = 3'b100;
        explut_etg[0][5'd24] = 3'b100;
        explut_etg[0][5'd25] = 3'b100;
        explut_etg[0][5'd26] = 3'b100;
        explut_etg[0][5'd27] = 3'b100;
        explut_etg[0][5'd28] = 3'b100;
        explut_etg[0][5'd29] = 3'b100;
        explut_etg[0][5'd30] = 3'b100;
        explut_etg[0][5'd31] = 3'b100;
        explut_etg[1][5'd0] = 3'b101;
        explut_etg[1][5'd1] = 3'b101;
        explut_etg[1][5'd2] = 3'b101;
        explut_etg[1][5'd3] = 3'b101;
        explut_etg[1][5'd4] = 3'b101;
        explut_etg[1][5'd5] = 3'b100;
        explut_etg[1][5'd6] = 3'b100;
        explut_etg[1][5'd7] = 3'b100;
        explut_etg[1][5'd8] = 3'b100;
        explut_etg[1][5'd9] = 3'b100;
        explut_etg[1][5'd10] = 3'b100;
        explut_etg[1][5'd11] = 3'b100;
        explut_etg[1][5'd12] = 3'b100;
        explut_etg[1][5'd13] = 3'b100;
        explut_etg[1][5'd14] = 3'b100;
        explut_etg[1][5'd15] = 3'b100;
        explut_etg[1][5'd16] = 3'b100;
        explut_etg[1][5'd17] = 3'b100;
        explut_etg[1][5'd18] = 3'b100;
        explut_etg[1][5'd19] = 3'b100;
        explut_etg[1][5'd20] = 3'b100;
        explut_etg[1][5'd21] = 3'b100;
        explut_etg[1][5'd22] = 3'b101;
        explut_etg[1][5'd23] = 3'b101;
        explut_etg[1][5'd24] = 3'b101;
        explut_etg[1][5'd25] = 3'b101;
        explut_etg[1][5'd26] = 3'b101;
        explut_etg[1][5'd27] = 3'b101;
        explut_etg[1][5'd28] = 3'b101;
        explut_etg[1][5'd29] = 3'b101;
        explut_etg[1][5'd30] = 3'b101;
        explut_etg[1][5'd31] = 3'b101;
        explut_etg[2][5'd0] = 3'b101;
        explut_etg[2][5'd1] = 3'b101;
        explut_etg[2][5'd2] = 3'b101;
        explut_etg[2][5'd3] = 3'b101;
        explut_etg[2][5'd4] = 3'b101;
        explut_etg[2][5'd5] = 3'b101;
        explut_etg[2][5'd6] = 3'b001;
        explut_etg[2][5'd7] = 3'b001;
        explut_etg[2][5'd8] = 3'b001;
        explut_etg[2][5'd9] = 3'b001;
        explut_etg[2][5'd10] = 3'b001;
        explut_etg[2][5'd11] = 3'b001;
        explut_etg[2][5'd12] = 3'b001;
        explut_etg[2][5'd13] = 3'b001;
        explut_etg[2][5'd14] = 3'b001;
        explut_etg[2][5'd15] = 3'b001;
        explut_etg[2][5'd16] = 3'b001;
        explut_etg[2][5'd17] = 3'b001;
        explut_etg[2][5'd18] = 3'b001;
        explut_etg[2][5'd19] = 3'b001;
        explut_etg[2][5'd20] = 3'b001;
        explut_etg[2][5'd21] = 3'b110;
        explut_etg[2][5'd22] = 3'b110;
        explut_etg[2][5'd23] = 3'b110;
        explut_etg[2][5'd24] = 3'b110;
        explut_etg[2][5'd25] = 3'b110;
        explut_etg[2][5'd26] = 3'b110;
        explut_etg[2][5'd27] = 3'b110;
        explut_etg[2][5'd28] = 3'b110;
        explut_etg[2][5'd29] = 3'b110;
        explut_etg[2][5'd30] = 3'b110;
        explut_etg[2][5'd31] = 3'b110;
        explut_etg[3][5'd0] = 3'b111;
        explut_etg[3][5'd1] = 3'b111;
        explut_etg[3][5'd2] = 3'b111;
        explut_etg[3][5'd3] = 3'b111;
        explut_etg[3][5'd4] = 3'b111;
        explut_etg[3][5'd5] = 3'b011;
        explut_etg[3][5'd6] = 3'b011;
        explut_etg[3][5'd7] = 3'b011;
        explut_etg[3][5'd8] = 3'b011;
        explut_etg[3][5'd9] = 3'b011;
        explut_etg[3][5'd10] = 3'b011;
        explut_etg[3][5'd11] = 3'b101;
        explut_etg[3][5'd12] = 3'b101;
        explut_etg[3][5'd13] = 3'b101;
        explut_etg[3][5'd14] = 3'b101;
        explut_etg[3][5'd15] = 3'b101;
        explut_etg[3][5'd16] = 3'b101;
        explut_etg[3][5'd17] = 3'b101;
        explut_etg[3][5'd18] = 3'b001;
        explut_etg[3][5'd19] = 3'b001;
        explut_etg[3][5'd20] = 3'b001;
        explut_etg[3][5'd21] = 3'b001;
        explut_etg[3][5'd22] = 3'b001;
        explut_etg[3][5'd23] = 3'b001;
        explut_etg[3][5'd24] = 3'b110;
        explut_etg[3][5'd25] = 3'b110;
        explut_etg[3][5'd26] = 3'b110;
        explut_etg[3][5'd27] = 3'b110;
        explut_etg[3][5'd28] = 3'b110;
        explut_etg[3][5'd29] = 3'b110;
        explut_etg[3][5'd30] = 3'b110;
        explut_etg[3][5'd31] = 3'b010;
    end

//    always @ (posedge clk) if(cen) begin
//        etf <= explut_etf[totalatten_XII_76][addr];
//        etg <= explut_etg[totalatten_XII_76][addr];
//    end
    always @ (posedge clk) // only update addr on clock edge
        addr_latched <= addr;

    always @(*) begin // allow etf and etg updates whenever totalatten_XII_76 changes
        etf <= explut_etf[totalatten_XII_76][clk ? addr : addr_latched]; // addr_latched might be stale on clk edge
        etg <= explut_etg[totalatten_XII_76][clk ? addr : addr_latched]; // addr_latched might be stale on clk edge
    end

endmodule

Update 2023/03/06: bold lines. Need to keep always conditions intact.

And the original jt51_exprom call as follows:

reg  [ 9:0] etf;
reg  [ 2:0] etg;

jt51_exprom u_exprom(
    .clk    ( clk           ),
    .cen    ( cen           ),
    .addr   ( atten_internal_XI[5:1] ),
    .totalatten_XII_76 ( totalatten_XII[7:6] ),
    .etf    ( etf ),
    .etg    ( etg )
);

And the Perl script to generate the exprom table:

#!/usr/bin/perl

use Data::Dumper;

$exp_XII = [ [ 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1 ],
    [ 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1 ],
    [ 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1 ],
    [ 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1 ],
    [ 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1 ],
    [ 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1 ],
    [ 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0 ],
    [ 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1 ],
    [ 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1 ],
    [ 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1 ],
    [ 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1 ],
    [ 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1 ],
    [ 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1 ],
    [ 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1 ],
    [ 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1 ],
    [ 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1 ],
    [ 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1 ],
    [ 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1 ],
    [ 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1 ],
    [ 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1 ],
    [ 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1 ],
    [ 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1 ],
    [ 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1 ],
    [ 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1 ],
    [ 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0 ],
    [ 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1 ],
    [ 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1 ],
    [ 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1 ],
    [ 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1 ],
    [ 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1 ],
    [ 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0 ],
    [ 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 ] ];

for $i (0..31) {
    $etf->[0][$i] = [ 1, reverse(@{$exp_XII->[$i]}[36..44]) ];
    $etg->[0][$i] = [ 1, reverse(@{$exp_XII->[$i]}[34..35]) ];

    $etf->[1][$i] = [ reverse(@{$exp_XII->[$i]}[24..33]) ];
    $etg->[1][$i] = [ 1, 0, @{$exp_XII->[$i]}[23] ];

    $etf->[2][$i] = [ 0, reverse(@{$exp_XII->[$i]}[14..22]) ];
    $etg->[2][$i] = [ reverse(@{$exp_XII->[$i]}[11..13]) ];

    $etf->[3][$i] = [ 0, 0, reverse(@{$exp_XII->[$i]}[3..10]) ];
    $etg->[3][$i] = [ reverse(@{$exp_XII->[$i]}[0..2]) ];
}
# original code:
# 2'b00: begin
#         etf = { 1'b1, exp_XII[44:36]  };
#         etg = { 1'b1, exp_XII[35:34] };             
#     end
# 2'b01: begin
#         etf = exp_XII[33:24];
#         etg = { 2'b10, exp_XII[23] };               
#     end
# 2'b10: begin
#         etf = { 1'b0, exp_XII[22:14]  };
#         etg = exp_XII[13:11];               
#     end
# 2'b11: begin
#         etf = { 2'b00, exp_XII[10:3]  };
#         etg = exp_XII[2:0];
#     end

for $j (0..3) {
    for $i (0..31) {
        print "etf[$j]\[5'd$i] = 10'b";
        for $k (0..9) {
            print $etf->[$j][$i][$k];
        }
        print ";\n"
    }
}
for $j (0..3) {
    for $i (0..31) {
        print "etg[$j]\[5'd$i] = 3'b";
        for $k (0..2) {
            print $etg->[$j][$i][$k];
        }
        print ";\n"
    }
}

The exprom code used [44:36], so we need to reverse that using Perl’s array-reversing function, reverse(). The notation used here (reverse(@{$exp_XII->[$i]}[36..44])) is probably one of the reasons why Perl has fallen out of favor. :)

Two Raspberry Pi Picos pretending to be a Z80

Last year, I bought a faulty Hitachi MB-H2 (MSX) in order to gain electronics and repair experience. Using my oscilloscope and two simple 74-series (NOT and AND) logic ICs, I managed to figure out that one of the RAM chips was faulty. I replaced the RAM chip, but it still wouldn’t work. I did one more slightly less reliable oscilloscope-based test and replaced one more chip, and it still wouldn’t work. How many faults can this machine have? Well it turns out that probably only the first RAM chip was broken in the first place, and I just didn’t solder properly. I thought I had checked my connections, but I guess one was border-line. (I have more soldering experience now.)

So, suspecting that I had some kind of severe fault, and not having come up with the logic analyzer “idea” yet, and noticing that the CPU was socketed, I decided to take out the CPU and just generate the signals that the CPU would generate myself, using two Raspberry Pi Picos. (Because I needed a lot of pins, not necessarily performance.) One Pico is responsible for the address and control pins, the other for the data pins. As I noticed some time in, Pico 1 should have had the data and control pins, Pico 2 the address pins. Why? Timing matters with the data pins, but for address pins you can be super slow and it’ll be fine. It still worked out in the end.

Pico 1 controls Pico 2 via UART. A host computer (yes, you, Mr. ThinkPad) controls Pico 1 via serial. Then some idiot (yes, me) types in commands into a serial terminal, and Pico 1 does the idiot’s bidding. The following commands are recognized:

  • i, for IOREQ input
  • o, for IOREQ output
  • v, for VRAM manipulation, which I actually couldn’t get to work the way I expected, but it still does something
  • r, for RAM reads
  • w, for RAM writes (and a simple readback to make sure the RAM stores stuff)
  • W, for RAM writes with RAM refresh (and a readback after every refresh). You can specify the amount of writes between refreshes and stuff.
  • s, sync UART (flushes out all characters stuck in the UART read buffer)
  • 0: ask Pico 2 to set data bus to 0
  • u: ask Pico 2 to unset data bus (i.e., to set bus direction from: GPIO_OUT to: GPIO_IN)

So, how does it work? How does the Z80 work? Let’s have a look at the Z80 pinout:

The A pins are the address pins. The D pins are the data pins. So if you want to write 0 to address 0, all those pins will be 0. In addition, RD will be high, and WR will be low, because we are writing. (Yes, 0 means “active” and 1 means “inactive”.) In addition, we are writing to memory, not to IO space. So IORQ is 1 and MREQ is 0. (Also M1 goes from 1 to 0 too, but I don’t remember the details there.) If we instead want to talk to hardware, we need to know the hardware address and set IORQ to 0 instead of MREQ. On the Z80, only A0 to A7 matter for IO addresses. Well, that’s the gist of it.

With a crude thing like this, we can:

  • Dump the main ROM and check that the contents are correct
  • Check if memory works
  • Check if the sound chip works
  • Check if the video chip works
  • Check if the IO controller works
    • We can turn the tape motor on and off
    • We can map memory
    • Etc.?
  • (Provided the connection from CPU to the above peripherals is working)

I was able to check all of the above. Note that in the highly unlikely event that you decide to run any of this on your MSX machine, note that memory mapping is a bit different from machine to machine. (Which is important, otherwise RAM expansions wouldn’t work, right?)

So here are some examples of commands I’d paste into my terminal:

# Turn off tape motor (which is on by default IIRC?), map memory, maybe some other stuff (I got these by running the MB-H2 in openmsx and checking the earliest 'in's and 'out's in openmsx-debugger, also see below screenshot)
# Execute this before executing anything else!
o00ab82o00aa50o00a800o00a850o00a8a0o00a8f0

# Read first 16 bytes of ROM
r0000rr0001rr0002rr0003rr0004rr0005rr0006rr0007rr0008rr0009rr000arr000brr000crr000drr000err000frr0010r

# Read bits 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, and 15 (counting bits from 0) (if you get the right result here you'll know that your connections are good)
r0001rr0002rr0004rr0008rr0010rr0020rr0040rr0080rr0100rr0200rr0400rr0800rr1000rr2000rr4000rr8000r

# Expected result of above command for MB-H2:
sed -n -e '2p' -e '3p' -e '5p' -e '9p' -e '17p' -e '33p' -e '65p' -e '129p' -e '257p' -e '513p' -e '1025p' -e '2049p' -e '4097p' -e '8193p' -e '16385p' 32k_v2.hex
c3
d7
bf
c3
c3
c3
13
06
ee
2a
e5
32
a4
00
e5
head -n 1 16k_v2.hex
41

# Set background colors:
o00990fo009987 # white background
o00990eo009987 # gray
o00990do009987 # magenta
o00990co009987 # dark green
o00990bo009987 # light yellow
o00990ao009987 # dark yellow
o009909o009987 # light red
o009908o009987 # medium red
o009907o009987 # cyan
o009906o009987 # dark red
o009905o009987 # light blue
o009904o009987 # dark blue
o009903o009987 # light green
o009902o009987 # medium green
o009901o009987 # black

# click sound test:
o00ab0fo00ab0eo00ab0fo00ab0eo00ab0fo00ab0e

# VRAM notes (worked partially, but I think you may need to change the video mode to something else in order to get full VRAM access like this? Never really got it to work as expected. IIRC, the values would stick for a bit, and then go back to 0f or something)
# To read from 0000 to ... (address is auto-incremented, so you only have to set it once):
o009900 # set lower byte of address
o009900 # set upper byte of address (bit 7 and 6 are low to indicate that we want to read)
i0098 # read from 0000
i0098 # read from 0001
i0098 # read from 0002
i0098 # read from 0003
...
# bunched into a single line:
o009900o009900i0098i0098i0098i0098
# To write ff to 0000-... (address is auto-incremented, so you only have to set it once):
o009900 # set lower byte of address
o009940 # set upper byte of address (bit 7 is low and bit 6 is high to indicate that we want to write)
o0098ff # set data register to ff to write to 0000
o0098ff # set data register to ff to write to 0001
o0098ff # set data register to ff to write to 0002
o0098ff # set data register to ff to write to 0003
...
# bunched into a single line:
o009900o009940o0098ffo0098ffo0098ff

What’s a good story without pictures?

openmsx-debugger early in the boot process (looking for RAM)
Serial terminal for Pico 1 (left) and Pico 2 (right) (Pico 2’s output is just debug output, and doesn’t accept commands from this serial connection)
I put a 40-pin socket in the existing 40-pin socket, and directly clipped in breadboard jumper wire. It worked, but wasn’t fun. I still stuck with this setup. Note: maybe half the wires pictured here are actually part of the computer. Yes, this computer has many, many wires inside.
BTW, just one Pico here because I thought it’d be worth something with just a couple bits on the data bus. But yeah, that wasn’t very fun.
Now running with two Raspberry Pi Picos. At first I tried using an automatic level shifter, but that didn’t work. Possibly because the data bus’ idle voltage (when everything is at high impedance) is at around 2V. I think I even saw a datasheet somewhere recommending doing that, maybe the 4164 RAM?
And here’s a tiny minicom script cycling through the background colors (see below)
sleep 5
send o009900o009980\c
send o009903o009981\c
send o009900o009987\c
sleep 1
send o009901o009987\c
sleep 1
send o009902o009987\c
sleep 1
send o009903o009987\c
sleep 1
send o009904o009987\c
sleep 1
send o009905o009987\c
sleep 1
send o009906o009987\c
sleep 1
send o009907o009987\c
sleep 1
send o009908o009987\c
sleep 1
send o009909o009987\c
sleep 1
send o00990ao009987\c
sleep 1
send o00990bo009987\c
sleep 1
send o00990co009987\c
sleep 1
send o00990do009987\c
sleep 1
send o00990eo009987\c
sleep 1
send o00990fo009987\c

The code

Danger: do not submit code to code beauty contests.

The code consists of two separate projects, in CMake terms. One is for Pico 1, the other is for Pico 2. First of all the CMakeLists.txt files are as follows:

Pico 1 (create a directory called e.g. inspect_system_interactively and in there, create a file called CMakeLists.txt with the following contents):

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
        -O3
        )

add_executable(inspect_system_interactively
        inspect_system_interactively.c
        )

# pull in common dependencies
target_link_libraries(inspect_system_interactively pico_stdlib)

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

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

# add url via pico_set_program_url
example_auto_set_url(inspect_system_interactively)

Pico 2 (my directory name is inspect_system_interactively_databus):

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
        -O3
        )

add_executable(inspect_system_interactively_databus
        inspect_system_interactively_databus.c
        )

# pull in common dependencies
target_link_libraries(inspect_system_interactively_databus pico_stdlib)

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

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

# add url via pico_set_program_url
example_auto_set_url(inspect_system_interactively_databus)

You can get the referenced pico_sdk_import.cmake and example_auto_set_url.cmake files from the pico-examples repository (https://github.com/raspberrypi/pico-examples.git).

Next, you need to place the C files into the corresponding directories. Then you just need to execute two commands, “cmake .” followed by “make”, in both directories.

inspect_system_interactively/inspect_system_interactively.c:

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

#define A11          0
#define A12          1
#define A13          2
#define A14          3
#define A15          4

#define A10         28
#define A9          27
#define A8          26
#define A7          22
#define A6          21
#define A5          20
#define A4          19
#define A3          18
#define A2          17
#define A1          15
#define A0          14

#define MREQ        16 /* active-low */
#define M1          13 /* active-low, we should probably set this and un-set when we're done reading */
#define RFSH        12 /* active-low and we won't be refreshing at all; tie to +5; could also feed inverted MREQ_RD if things don't work otherwise <-- chose to do this */
#define RD           5 /* active-low */
#define WR           6 /* active-low */
#define IOREQ        7 /* active-low */
/* #define HALT        active-low but shouldn't be a problem to keep this floating */

#define UART1_TX     8
#define UART1_RX     9
// #define BAUD    345600
#define BAUD    460800

#define READ_BACK_SIGNAL 10
#define READ_BACK_SIGNAL_ACK 11

#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 16

// #define SLOW_PERF 1

const unsigned int a_bus[BUS_SIZE] = {
  A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15
};

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

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

void setup() {
    gpio_init(A0);
    gpio_init(A1);
    gpio_init(A2);
    gpio_init(A3);
    gpio_init(A4);
    gpio_init(A5);
    gpio_init(A6);
    gpio_init(A7);
    gpio_init(A8);
    gpio_init(A9);
    gpio_init(A10);
    gpio_init(A11);
    gpio_init(A12);
    gpio_init(A13);
    gpio_init(A14);
    gpio_init(A15);
    gpio_init(MREQ);
    gpio_init(IOREQ);
    gpio_init(RD);
    gpio_init(WR);
    gpio_init(M1);
    gpio_init(RFSH);
    gpio_init(STATUS_LED);
    gpio_init(READ_BACK_SIGNAL);
    gpio_init(READ_BACK_SIGNAL_ACK);

    /* Not 100% sure if the default is LOW for all pins so let's set this early and once more after gpio_set_dir to be 100% sure */
    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);
    gpio_put(A8, LOW);
    gpio_put(A9, LOW);
    gpio_put(A10, LOW);
    gpio_put(A11, LOW);
    gpio_put(A12, LOW);
    gpio_put(A13, LOW);
    gpio_put(A14, LOW);
    gpio_put(A15, LOW);
    gpio_put(MREQ, HIGH);
    gpio_put(IOREQ, HIGH);
    gpio_put(RD, HIGH);
    gpio_put(WR, HIGH);
    gpio_put(M1, HIGH);
    gpio_put(RFSH, HIGH);
    gpio_put(READ_BACK_SIGNAL, LOW);

    gpio_set_dir(A0, GPIO_OUT);
    gpio_set_dir(A1, GPIO_OUT);
    gpio_set_dir(A2, GPIO_OUT);
    gpio_set_dir(A3, GPIO_OUT);
    gpio_set_dir(A4, GPIO_OUT);
    gpio_set_dir(A5, GPIO_OUT);
    gpio_set_dir(A6, GPIO_OUT);
    gpio_set_dir(A7, GPIO_OUT);
    gpio_set_dir(A8, GPIO_OUT);
    gpio_set_dir(A9, GPIO_OUT);
    gpio_set_dir(A10, GPIO_OUT);
    gpio_set_dir(A11, GPIO_OUT);
    gpio_set_dir(A12, GPIO_OUT);
    gpio_set_dir(A13, GPIO_OUT);
    gpio_set_dir(A14, GPIO_OUT);
    gpio_set_dir(A15, GPIO_OUT);
    gpio_set_dir(MREQ, GPIO_OUT);
    gpio_set_dir(IOREQ, GPIO_OUT);
    gpio_set_dir(RD, GPIO_OUT);
    gpio_set_dir(WR, GPIO_OUT);
    gpio_set_dir(M1, GPIO_OUT);
    gpio_set_dir(RFSH, GPIO_OUT);
    gpio_set_dir(STATUS_LED, GPIO_OUT);
    gpio_set_dir(READ_BACK_SIGNAL, GPIO_OUT);
    gpio_set_dir(READ_BACK_SIGNAL_ACK, GPIO_IN);

    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);
    gpio_put(A8, LOW);
    gpio_put(A9, LOW);
    gpio_put(A10, LOW);
    gpio_put(A11, LOW);
    gpio_put(A12, LOW);
    gpio_put(A13, LOW);
    gpio_put(A14, LOW);
    gpio_put(A15, LOW);
    gpio_put(MREQ, HIGH);
    gpio_put(IOREQ, HIGH);
    gpio_put(RD, HIGH);
    gpio_put(WR, HIGH);
    gpio_put(M1, HIGH);
    gpio_put(RFSH, HIGH);
    gpio_put(READ_BACK_SIGNAL, LOW);

    gpio_set_function(UART1_TX, GPIO_FUNC_UART);
    gpio_set_function(UART1_RX, GPIO_FUNC_UART);
}

void set_bus(uint16_t address) {
    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], address & 1);
        address >>= 1;
    }
}

bool uart_read_data_bus(bool data_bus_is_already_set, uint32_t *recv_data) {
    char buf[3] = { 0 };

    if (!data_bus_is_already_set) {
#ifdef SLOW_PERF 
        gpio_put(STATUS_LED, HIGH);
#endif
        uart_putc_raw(uart1, 'r');
    }
    uart_read_blocking(uart1, (uint8_t*)buf, sizeof(buf)-1);
    if (!data_bus_is_already_set) {
#ifdef SLOW_PERF
        while (uart_is_readable(uart1)) {
            printf("Read one more unexpected byte: %02x\n", uart_getc(uart1));
        }
        gpio_put(STATUS_LED, LOW);
#endif
    }
//     printf("Read something: %x %x\n", buf[0], buf[1]);
    if (!sscanf((char*)buf, "%02x", recv_data)) {
//         printf("Communication error?\n");
        return false;
    }
#ifdef SLOW_PERF
    else {
        printf("Read %02x / %03d\n", *recv_data, *recv_data);
    }
#endif
    return true;
}

void uart_write_data_bus(bool io, uint8_t send_data) {
    char buf[3] = { 0 };
    sprintf(buf, "%02x", send_data);
    gpio_put(STATUS_LED, HIGH);
    uart_putc_raw(uart1, io ? 'o' : 'w');
    uart_puts(uart1, buf);
    gpio_put(STATUS_LED, LOW);
}

uint32_t get_address_from_stdin() {
    char stdin_buf[5] = { 0 };
    uint32_t address = 0;
    stdin_buf[0] = getchar();
    stdin_buf[1] = getchar();
    stdin_buf[2] = getchar();
    stdin_buf[3] = getchar();
    sscanf(stdin_buf, "%04x", &address);
    printf("\n");
    return address;
}

uint32_t get_data_from_stdin() {
    char stdin_buf[5] = { 0 };
    uint32_t data = 0;
    printf("Data to write? (Two digit hex)\n");
    stdin_buf[0] = getchar();
    stdin_buf[1] = getchar();
    stdin_buf[2] = 0;
    sscanf(stdin_buf, "%02x", &data);
    printf("\n");
    return data;
}

void refresh_dram() {
    uint16_t row_address, temp;
    int i;
    set_bus(0);
    for (row_address = 0; row_address < 256; row_address++) {
        temp = row_address;
        for (i = 0; i < 8; i++) {
            gpio_put(a_bus[i], temp & 1);
            temp >>= 1;
        }
        gpio_put(RFSH, LOW);
        gpio_put(MREQ, LOW);
        // Need to wait at least 150 ns, 20 * (1000/133) ~= 150. In reality this probably works out to be more than 150 ns
        nop();
        nop();
        nop();
        nop();
        nop();

        nop();
        nop();
        nop();
        nop();
        nop();

        nop();
        nop();
        nop();
        nop();
        nop();

        nop();
        nop();
        nop();
        nop();
        nop();
        gpio_put(MREQ, HIGH);
        gpio_put(RFSH, HIGH);
        // Need to delay at least 100 ns, which we'll probably already be doing by setting up the bus with the next address, but let's add some NOPs anyway and then measure
        nop();
        nop();
        nop();
        nop();
        nop();

        nop();
        nop();
        nop();
        nop();
        nop();
    }
}

void write_vram_address(bool is_write, uint8_t high_byte, uint8_t low_byte) {
    if (is_write) {
        high_byte |= 0x40;
    }
    set_bus(0x99);

    uart_write_data_bus(true, low_byte);
    uart_getc(uart1); // Slave takes time to actually set data bus, let's wait for an 'f' char to indicate this op is done
    gpio_put(IOREQ, LOW);
    gpio_put(WR, LOW);
    sleep_ms(1);
    gpio_put(WR, HIGH);
    gpio_put(IOREQ, HIGH);

    uart_write_data_bus(true, high_byte);
    uart_getc(uart1); // Slave takes time to actually set data bus, let's wait for an 'f' char to indicate this op is done
    gpio_put(IOREQ, LOW);
    gpio_put(WR, LOW);
    sleep_ms(1);
    gpio_put(WR, HIGH);
    gpio_put(IOREQ, HIGH);
}

int main() {
//     char stdin_buf[5] = { 0 };
    uint32_t address = 0, end_address = 0;
    int i = 0;
    uint32_t j = 0, k = 0;
    char command = 0;
    uint32_t data = 0;
    uint32_t read_val = 0;
    char data_bus_ready = 0;
    uint32_t n_refresh_cycles = 0, n_writes_until_refresh = 0, only_write_first_time = 0;
    uint8_t high_byte = 0, low_byte = 0;
    int n_vram_writes = 0;
//     bool status = 0;

    stdio_init_all();
    setup();
    uart_init(uart1, BAUD);
    
    /* play with the LED so we can see that the program is about to start */
    for (i = 0; i < 5; i++) {
        gpio_put(STATUS_LED, HIGH);
        sleep_ms(500);
        gpio_put(STATUS_LED, LOW);
        sleep_ms(500);
    }
    sleep_ms(4000);

    printf("Ready\n");

    while (true) {
        read_val = 0;
    
        printf("Command?\n");
        command = getchar();
        printf("\nAddress? (Four digit hex)\n");
        address = get_address_from_stdin();
        switch (command) {
            case 'i':
                printf("Sending IOREQ (input)\n");
                set_bus(address);
                gpio_put(IOREQ, LOW);
                gpio_put(RD, LOW);
                if (!uart_read_data_bus(false, &read_val)) {
                    printf("RECV ERR\n");
                }
                gpio_put(RD, HIGH);
                gpio_put(IOREQ, HIGH);
                printf("Read from IO: %04x %02x\n", address, read_val);
                break;
            case 'o':
                printf("Sending IOREQ (output)\n");
                data = get_data_from_stdin();
                set_bus(address);
                uart_write_data_bus(true, data);
                uart_getc(uart1); // Slave takes time to actually set data bus, let's wait for an 'f' char to indicate this op is done
                gpio_put(IOREQ, LOW);
                gpio_put(WR, LOW);
                sleep_ms(1);
                gpio_put(WR, HIGH);
                gpio_put(IOREQ, HIGH);
                printf("Wrote to IO: %04x %1x\n", address, data);
                break;
            case 'v':
                low_byte = address & 0xff;
                high_byte = (address & 0x3f00) >> 8;
                printf("Number of writes? (Four digit hex)\n");
                n_vram_writes = get_address_from_stdin();
                data = get_data_from_stdin();

                write_vram_address(true, high_byte, low_byte);
                set_bus(0x98);
                uart_write_data_bus(true, data);
                uart_getc(uart1); // Slave takes time to actually set data bus, let's wait for an 'f' char to indicate this op is done
                for (i = 0; i < n_vram_writes; i++) {
                    gpio_put(IOREQ, LOW);
                    gpio_put(WR, LOW);
                    sleep_ms(1); // use VRAM_IO_DELAY macro?
                    gpio_put(WR, HIGH);
                    gpio_put(IOREQ, HIGH);
                }

                write_vram_address(false, high_byte, low_byte);
                set_bus(0x98);
                for (i = 0; i < n_vram_writes; i++) {
                    gpio_put(IOREQ, LOW);
                    gpio_put(RD, LOW);
                    uart_read_data_bus(false, &read_val);
                    gpio_put(RD, HIGH);
                    gpio_put(IOREQ, HIGH);
                    if (read_val != data) {
                        printf("Mismatch at VRAM address %04x: read %02x expected %02x\n", address+i, read_val, data);
                    }
                }
                break;
            case 'r':
                set_bus(address);
                gpio_put(MREQ, LOW);
                gpio_put(RD, LOW);
                gpio_put(M1, LOW);
//                 sleep_ms(1);
                if (!uart_read_data_bus(false, &read_val)) {
                    printf("RECV ERR\n");
//                     return 1;
                }

                printf("%04x %1x\n", address, read_val);
                printf("Press any key to advance cycle\n");
                getchar();
                printf("\n");
                sleep_ms(1);
                gpio_put(M1, HIGH);
                gpio_put(RD, HIGH);
                gpio_put(MREQ, HIGH);
                gpio_put(RFSH, LOW);
                sleep_ms(1);
                gpio_put(RFSH, HIGH);
                break;
            case 'w':
            case 'W':
                printf("End address? (Four digit hex, 0000 for 1-byte write)\n");
                end_address = get_address_from_stdin();
                if (end_address < address) {
                    end_address = address;
                }
                data = get_data_from_stdin();
                if (command == 'W') {
                    printf("Number of refresh cycles? (Four digit hex)\n");
                    n_refresh_cycles = get_data_from_stdin(); // use get_address_from_stdin to actually get four bytes
                    printf("Refresh every n writes? (Four digit hex)\n");
                    n_writes_until_refresh = get_data_from_stdin();
                    printf("Only write first time? (0000: no, 0001: yes)\n");
                    only_write_first_time = get_data_from_stdin();
                } else {
                    n_refresh_cycles = 1;
                }
                for (k = 0; k < n_refresh_cycles; k++) {
                    for (j = address; j <= end_address; j++) {
                        set_bus(j);
                        if (k == 0 || !only_write_first_time) {
                            uart_write_data_bus(false, data);
                            data_bus_ready = uart_getc(uart1);
                            if (data_bus_ready == 'f') { // Slave takes time to actually set data bus, let's wait for an 'f' char to indicate this op is done
                                printf("Got 'f'\n");
                            } else {
                                printf("Warning: got '%c' (%02x) instead of 'f'\n", data_bus_ready, data_bus_ready);
                            }
                            gpio_put(WR, LOW);
                            gpio_put(MREQ, LOW);
#define SLEEP_US_DURING_WRITE 3 // was 1
                            sleep_us(SLEEP_US_DURING_WRITE); // should be ~219 ns without this. that's okay generally speaking.
                            gpio_put(WR, HIGH);
                            gpio_put(MREQ, HIGH);
                        }

                        // S command has to take longer than RAM cycle time. should be okay.
                        uart_putc_raw(uart1, 'S'); // force data bus to gpio_in, and stand by for read
                        data_bus_ready = uart_getc(uart1); // gpio_in is done, slave is standing by

                        gpio_put(RD, LOW);
                        gpio_put(MREQ, LOW);
                        // perhaps our timing is just a bit tight? let's add a small delay
                        // much better but still not that great, let's do 1 us
#define SLEEP_US_DURING_READ_BACK 1 // was 1
                        sleep_us(SLEEP_US_DURING_READ_BACK);
                        gpio_put(READ_BACK_SIGNAL, HIGH);
                        while (!gpio_get(READ_BACK_SIGNAL_ACK)) {}

                        gpio_put(READ_BACK_SIGNAL, LOW);
                        gpio_put(RD, HIGH);
                        gpio_put(MREQ, HIGH);

                        if (!uart_read_data_bus(true, &read_val)) {
                            printf("RECV ERR\n");
                        }
                        printf("Wrote %04x and read back: %04x %1x\n", data, j, read_val);
                        if (command == 'W') {
                            if ((j-address) % n_writes_until_refresh == 0) {
                                refresh_dram();
                            }
                        }
                    }
                }
                break;
            case 's': // sync uart
                while (uart_is_readable(uart1)) {
                    printf("%c", uart_getc(uart1));
                }
                printf("\n");
                break;
            case '0':
                uart_putc_raw(uart1, '0');
                break;
            case 'u':
                uart_putc_raw(uart1, 'u');
                break;
            default:
                printf("Unknown command: \"%c\"\n", command);
        }
    }

    while (true) {
        gpio_put(STATUS_LED, HIGH);
        sleep_ms(1000);
        gpio_put(STATUS_LED, LOW);
        sleep_ms(1000);
    }
}

inspect_system_interactively_databus/inspect_system_interactively_databus.c:

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

#define D7           7
#define D6           6
#define D5           5
#define D4           4
#define D3           3
#define D2           2
#define D1           1
#define D0           0

#define UART1_TX     8
#define UART1_RX     9
// #define BAUD    345600
#define BAUD    460800

#define READ_BACK_SIGNAL 28
#define READ_BACK_SIGNAL_ACK 27

#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 println printf
// #define println(...) ;

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

void setup() {
    gpio_init(D0);
    gpio_init(D1);
    gpio_init(D2);
    gpio_init(D3);
    gpio_init(D4);
    gpio_init(D5);
    gpio_init(D6);
    gpio_init(D7);

    gpio_init(STATUS_LED);
    gpio_init(READ_BACK_SIGNAL);
    gpio_init(READ_BACK_SIGNAL_ACK);
    
    gpio_set_dir(D0, GPIO_IN);
    gpio_set_dir(D1, GPIO_IN);
    gpio_set_dir(D2, GPIO_IN);
    gpio_set_dir(D3, GPIO_IN);
    gpio_set_dir(D4, GPIO_IN);
    gpio_set_dir(D5, GPIO_IN);
    gpio_set_dir(D6, GPIO_IN);
    gpio_set_dir(D7, GPIO_IN);
    gpio_set_dir(STATUS_LED, GPIO_OUT);
    gpio_set_dir(READ_BACK_SIGNAL, GPIO_IN);
    gpio_set_dir(READ_BACK_SIGNAL_ACK, GPIO_OUT);

    gpio_put(READ_BACK_SIGNAL_ACK, LOW);

    gpio_set_function(UART1_TX, GPIO_FUNC_UART);
    gpio_set_function(UART1_RX, GPIO_FUNC_UART);
}

#define DATA_BUS_SIZE 8

const unsigned int d_bus[DATA_BUS_SIZE] = {
  D0, D1, D2, D3, D4, D5, D6, D7
};

void set_data_bus_dir(uint32_t dir) {
    gpio_set_dir(D0, dir);
    gpio_set_dir(D1, dir);
    gpio_set_dir(D2, dir);
    gpio_set_dir(D3, dir);
    gpio_set_dir(D4, dir);
    gpio_set_dir(D5, dir);
    gpio_set_dir(D6, dir);
    gpio_set_dir(D7, dir);
}

void set_data_bus(uint8_t data) {
    int i;
    set_data_bus_dir(GPIO_OUT);
    /* Write lowest bit into lowest address line first, then next-lowest bit, etc. */
    for (i = 0; i < DATA_BUS_SIZE; i++) {
        gpio_put(d_bus[i], data & 1);
        data >>= 1;
    }
}

void get_data_bus(uint8_t *data) {
    int i;
    *data = 0;
    /* Write lowest bit into lowest address line first, then next-lowest bit, etc. */
    for (i = 0; i < DATA_BUS_SIZE; i++) {
        *data |= (gpio_get(d_bus[i]) << i);
    }
}

int main() {
    uint32_t recv_data = 0;
    uint8_t recv_data8 = 0;
    char c;
    uint8_t buf[3] = { 0 };
    setup();
    stdio_init_all();
    uart_init(uart1, BAUD); // 115200);

    while(true) {
        printf("Repeating loop\n");
        gpio_put(STATUS_LED, HIGH);
        c = uart_getc(uart1);
        gpio_put(STATUS_LED, LOW);
        switch (c) {
            case 'w':
            case 'o':
                printf("Received w\n");
                uart_read_blocking(uart1, buf, sizeof(buf)-1);
                if (!sscanf((char*)buf, "%02x", &recv_data)) {
                    printf("Communication error?\n");
                } else {
                    printf("Going to put on data bus: %02x / %03d\n", recv_data, recv_data);
                }
                set_data_bus(recv_data);
                uart_putc_raw(uart1, 'f'); // indicate that we're done setting the bus
                break;
            case 'u': // unset data bus
            case 'S': // unset data bus and then stand by for read
                set_data_bus_dir(GPIO_IN);
                uart_putc_raw(uart1, 'f');
                if (c == 'S') { // stand by for read
                    // UART communication turned out to be a bit slow so we sacrificed one more pin just to get a signal when to read the bus
                    while (!gpio_get(READ_BACK_SIGNAL)) {} // wait for read signal
                    nop();
                    nop();
                    nop();
                    nop();
                    nop();
                    nop();
                    nop();
                    nop();
                    nop();
                    nop();
                    get_data_bus(&recv_data8);
                    gpio_put(READ_BACK_SIGNAL_ACK, HIGH);
                    recv_data = recv_data8;
                    sprintf((char*)buf, "%02x", recv_data);
                    printf("Read from data bus: %s\n", (char*)buf);
                    uart_puts(uart1, (char*)buf);
                    gpio_put(READ_BACK_SIGNAL_ACK, LOW);
                }
                break;
            case 'r':
                printf("Received r\n");
                set_data_bus_dir(GPIO_IN);
                get_data_bus(&recv_data8);
                recv_data = recv_data8;
                sprintf((char*)buf, "%02x", recv_data);
                printf("Read from data bus: %s\n", (char*)buf);
                uart_puts(uart1, (char*)buf);
                break;
            case '0': /* set data bus to 0 */
                set_data_bus(0);
                break;
            default:
                printf("Unexpected command char\n");
        }
    }
}

I’ll consider putting this on my Github account at some point. Code license is public domain.

Testing live (powered) 74xx logic chips in-circuit

Update 2023/08/11: Testing live (powered) DRAM chips in-circuit.

Happy New Year! Hopefully with less coronavirus and less violence.

Somebody I know told me about their broken Amiga and how they had pulled out and tested every RAM/ROM/CPU chip and couldn’t find the fault, and now wanted to pull out every 74-series logic chip to test those too.

I didn’t have much to say at the time, but at some point I decided to try my hand at building an in-circuit chip tester, i.e., something that passively monitors a chip’s input and output pins while the device is running, and figures out if the chip is behaving correctly. All that is needed is an Arduino (Nano in my case) and a large IC test clip. The Arduino repeatedly samples all the pins and works out if the chip’s output pins are valid for the given inputs. This works great for simple logic chips (like NOT or AND), but not quite so well for chips with high-impedance modes.

The Arduino runs at 16 MHz, and is capable of reading in all pins in about 3 CPU cycles. The device under test would have to be slow enough (which is true for most computers considered “retro” at the time of this writing) to make this work. To avoid sampling while the inputs are changing (which is likely to show as a state that isn’t possible for a correctly functioning chip), the pins are sampled twice, and if samples 1 and 2 aren’t equal, the code samples the pins again, until they’re consistent. (Search for “WAIT_UNTIL_CONSISTENT” in the code for details.)

Note: I originally coded this for the Raspberry Pi Pico, but decided to use the Arduino instead because it’s fast enough and is 5V-tolerant.

How to use this

  1. I’d recommend probing using an oscilloscope first
  2. Paste program listing into the Arduino IDE
  3. Connect Arduino via USB to host computer
  4. Press Upload button
  5. Disconnect Arduino from host computer
  6. Wire up Arduino to IC test clip (D2 == pin 1, D3 == pin 2, …, D12 == pin 11, A0 == pin 12, …, A4 == pin 16)
  7. Make sure device to be tested is powered off and attach test clip to chip inside device
  8. Connect Arduino to host computer and open Serial Monitor in the Arduino IDE (or e.g. minicom)
  9. Turn on device to be tested and make sure device powers on normally (or if it never powered on normally in the first place, make sure that it isn’t worse now)
  10. In your serial terminal application, select what kind of chip to test and start monitoring (currently supported chips are: 74×00, 74×02, 74×04, 74×08, 74×32, 74×125, 74×138, 74×157)
  11. If you get errors where you didn’t expect any, check your connections (check if the test clip is seated correctly, especially)
  12. Turn off device under test
  13. Disconnect Arduino from host computer (Warning: It’s possible to power the Arduino through the GPIO pins. Doing this may damage the Arduino, so always make sure that the device under test is powered off before the Arduino is disconnected from USB.)

Here are some pics and screenshots:

Checking a working 74LS157 (terminal screenshot)
Checking a 74LS157
Probably also a 74LS157

Note, this is very beta-quality, or even “POC-quality”, software. In particular, I implemented some chip tests without ever testing them (because my device doesn’t have those chips). And the ones I did test, I tested only once or so. In even more particular, I don’t think I ever had a chance to test any chips with high impedance states. So it’s entirely possible that the code related to that is 100% bollocks. Use this code at your own risk and only if you mostly know what you are doing.

Another note: my test clip is a 14-pin clip, which means that pins 8 and 9 are read using GPIO pins that aren’t adjacent to the ones reading pins 7 or 10. Look for “USING_A_14_PIN_TEST_CLIP” to see how this is done.

#include <string.h>
#include <unistd.h>

#define ARRAY_SIZE(array) (sizeof(array)/sizeof(array[0]))

#define ALL_REGULAR_GPIO_PINS 0b00011100011111111111111111111111
#define CHIP_NAME_MAX_LENGTH 6

#define LOGIC_BUFFER_LEN 128
#define PRINTF_BUFFER_LEN 96

#define MAX_BAD_RATE 1 // percent
#define MAX_HIGH_Z_UNLIKELY_RATE 10 // percent

#define REPORT_STATISTICS_EVERY_N_SAMPLES 100000

#define WAIT_UNTIL_CONSISTENT 1

// set below define when testing 16-pin chips using a 14-pin test clip and two extra wires for pin 8 and pin 9, as such:
// gpio 0 will be connected to pin 1
// gpio 1 will be connected to pin 2
// ...
// gpio 6 will be connected to pin 7
// gpio 7 will be connected to pin 10(!)
// gpio 8 will be connected to pin 11
// ...
// gpio 13 will be connected to pin 16
// gpio 14 will be connected through extra wire to pin 8
// gpio 15 will be connected through extra wire to pin 9
#define USING_A_14_PIN_TEST_CLIP 1

#define printf(...) sprintf(printf_buffer, __VA_ARGS__); Serial.println(printf_buffer); Serial.flush();
#define printf_verbose(...) { if (verbose == true) { printf(__VA_ARGS__); } }

char printf_buffer[PRINTF_BUFFER_LEN];
uint16_t logic_buffer[LOGIC_BUFFER_LEN];
bool verbose = false;

// organically grown enum
enum test_result_enum {
    BAD = 0,
    GOOD = 1,
    HIGH_Z_UNLIKELY = 2,
    HIGH_Z_LIKELY = 3,
    HIGH_Z_INT2 = 4
};
struct test_result {
    enum test_result_enum test_result_enum;
    unsigned int int1;
    unsigned int int2;
};

// some definitions for convenience
const struct test_result TEST_RESULT_GOOD = {
    .test_result_enum = GOOD,
    .int1 = 0,
    .int2 = 0
};
const struct test_result TEST_RESULT_BAD = {
    .test_result_enum = BAD,
    .int1 = 0,
    .int2 = 0
};
const struct test_result TEST_RESULT_HIGH_Z_UNLIKELY = {
    .test_result_enum = HIGH_Z_UNLIKELY,
    .int1 = 0,
    .int2 = 0
};
const struct test_result TEST_RESULT_HIGH_Z_LIKELY = {
    .test_result_enum = HIGH_Z_LIKELY,
    .int1 = 0,
    .int2 = 0
};
const struct test_result TEST_RESULT_HIGH_Z_INT2 = {
    .test_result_enum = HIGH_Z_INT2,
    .int1 = 0,
    .int2 = 0
};

struct overlay_struct_14 {
    bool pin1 : 1;
    bool pin2 : 1;
    bool pin3 : 1;
    bool pin4 : 1;
    bool pin5 : 1;
    bool pin6 : 1;
    bool pin7 : 1;
    bool pin8 : 1;
    bool pin9 : 1;
    bool pin10 : 1;
    bool pin11 : 1;
    bool pin12 : 1;
    bool pin13 : 1;
    bool pin14 : 1;
} __attribute__((packed));

#ifndef USING_A_14_PIN_TEST_CLIP
struct overlay_struct_16 {
    bool pin1 : 1;
    bool pin2 : 1;
    bool pin3 : 1;
    bool pin4 : 1;
    bool pin5 : 1;
    bool pin6 : 1;
    bool pin7 : 1;
    bool pin8 : 1;
    bool pin9 : 1;
    bool pin10 : 1;
    bool pin11 : 1;
    bool pin12 : 1;
    bool pin13 : 1;
    bool pin14 : 1;
    bool pin15 : 1;
    bool pin16 : 1;
} __attribute__((packed));
#else // renumber some pins
struct overlay_struct_16 {
    bool pin1 : 1;
    bool pin2 : 1;
    bool pin3 : 1;
    bool pin4 : 1;
    bool pin5 : 1;
    bool pin6 : 1;
    bool pin7 : 1;
    bool pin10 : 1;
    bool pin11 : 1;
    bool pin12 : 1;
    bool pin13 : 1;
    bool pin14 : 1;
    bool pin15 : 1;
    bool pin16 : 1;
    bool pin8 : 1;
    bool pin9 : 1;
} __attribute__((packed));
#endif

static_assert(sizeof(struct overlay_struct_14) == 2, "overlay_struct_14 has to be exactly 2 bytes, otherwise it isn't a valid overlay struct for 14 pins!");
static_assert(sizeof(struct overlay_struct_16) == 2, "overlay_struct_16 has to be exactly 2 bytes, otherwise it isn't a valid overlay struct for 16 pins!");

union uint_on_overlay_struct {
    uint16_t uint;
    struct overlay_struct_14 overlay_struct_14;
    struct overlay_struct_16 overlay_struct_16;
};

enum chip_type {
    STATELESS_LOGIC = 0,
    STATEFUL_LOGIC = 1
};

const char *chip_names[] = {
    "74x00",
    "74x02",
    "74x04",
    "74x08",
    "74x32",
    "74x125",
    "74x138",
    "74x157"
};
const enum chip_type chip_types[] = {
    STATELESS_LOGIC,
    STATELESS_LOGIC,
    STATELESS_LOGIC,
    STATELESS_LOGIC,
    STATELESS_LOGIC,
    STATELESS_LOGIC,
    STATELESS_LOGIC,
    STATELESS_LOGIC
};

void no_power_wait(union uint_on_overlay_struct original_input) {
    printf("Chip isn't powered, inserting 1000 ms sleep\n");
    printf_verbose("Current state: %04x\n", original_input.uint);
    delay(1000);
}

void error_blink(void) {
    while (true) {
        digitalWrite(13, false);
        delay(50);
        digitalWrite(13, true);
        delay(50);
    }
}

struct test_result validate_74x00(union uint_on_overlay_struct original_input) {
    struct overlay_struct_14 input = original_input.overlay_struct_14;
    if (input.pin14) { // VCC
        if (((input.pin1 & input.pin2) != input.pin3) &&
            ((input.pin4 & input.pin5) != input.pin6) &&
            ((input.pin13 & input.pin12) != input.pin11) &&
            ((input.pin10 & input.pin9) != input.pin8)) {
            return TEST_RESULT_GOOD;
        } else {
            return TEST_RESULT_BAD;
        }
    } else {
        no_power_wait(original_input);
    }
    return TEST_RESULT_GOOD;
}
struct test_result validate_74x02(union uint_on_overlay_struct original_input) {
    struct overlay_struct_14 input = original_input.overlay_struct_14;
    if (input.pin14) { // VCC
        if (((input.pin2 | input.pin3) != input.pin1) &&
            ((input.pin5 | input.pin6) != input.pin4) &&
            ((input.pin12 | input.pin11) != input.pin13) &&
            ((input.pin9 | input.pin8) != input.pin10)) {
            return TEST_RESULT_GOOD;
        } else {
            return TEST_RESULT_BAD;
        }
    } else {
        no_power_wait(original_input);
    }
    return TEST_RESULT_GOOD;
}
struct test_result validate_74x04(union uint_on_overlay_struct original_input) {
    struct overlay_struct_14 input = original_input.overlay_struct_14;
    if (input.pin14) { // VCC
        if ((input.pin1 != input.pin2) &&
            (input.pin3 != input.pin4) &&
            (input.pin5 != input.pin6) &&
            (input.pin13 != input.pin12) &&
            (input.pin11 != input.pin10) &&
            (input.pin9 != input.pin8)) {
            return TEST_RESULT_GOOD;
        } else {
            return TEST_RESULT_BAD;
        }
    } else {
        no_power_wait(original_input);
    }
    return TEST_RESULT_GOOD;
}
struct test_result validate_74x08(union uint_on_overlay_struct original_input) {
    struct overlay_struct_14 input = original_input.overlay_struct_14;
    if (input.pin14) { // VCC
        if (((input.pin1 & input.pin2) == input.pin3) &&
            ((input.pin4 & input.pin5) == input.pin6) &&
            ((input.pin13 & input.pin12) == input.pin11) &&
            ((input.pin10 & input.pin9) == input.pin8)) {
            return TEST_RESULT_GOOD;
        } else {
            return TEST_RESULT_BAD;
        }
    } else {
        no_power_wait(original_input);
    }
    return TEST_RESULT_GOOD;
}
struct test_result validate_74x32(union uint_on_overlay_struct original_input) {
    struct overlay_struct_14 input = original_input.overlay_struct_14;
    if (input.pin14) { // VCC
        if (((input.pin1 | input.pin2) == input.pin3) &&
            ((input.pin4 | input.pin5) == input.pin6) &&
            ((input.pin13 | input.pin12) == input.pin11) &&
            ((input.pin10 | input.pin9) == input.pin8)) {
            return TEST_RESULT_GOOD;
        } else {
            return TEST_RESULT_BAD;
        }
    } else {
        no_power_wait(original_input);
    }
    return TEST_RESULT_GOOD;
}

struct test_result validate_74x125(union uint_on_overlay_struct original_input) {
    struct overlay_struct_14 input = original_input.overlay_struct_14;
    unsigned int unlikely = 0;
    struct test_result res = TEST_RESULT_HIGH_Z_INT2;
    if (input.pin14) { // VCC
        if (!input.pin1) {
            if (input.pin3 != input.pin2) return TEST_RESULT_BAD;
        }
        if (!input.pin4) {
            if (input.pin6 != input.pin5) return TEST_RESULT_BAD;
        }
        if (!input.pin13) {
            if (input.pin12 != input.pin11) return TEST_RESULT_BAD;
        }
        if (!input.pin10) {
            if (input.pin9 != input.pin8) return TEST_RESULT_BAD;
        }

        if (input.pin1) {
            if (input.pin3 != input.pin2) unlikely++;
        }
        if (input.pin4) {
            if (input.pin6 != input.pin5) unlikely++;
        }
        if (input.pin13) {
            if (input.pin12 != input.pin11) unlikely++;
        }
        if (input.pin10) {
            if (input.pin9 != input.pin8) unlikely++;
        }
        res = TEST_RESULT_HIGH_Z_INT2;
        res.int1 = unlikely;
        res.int2 = 4-unlikely; // 4 is the number of outputs on this chip
        return res;
    } else {
        no_power_wait(original_input);
    }
    return TEST_RESULT_GOOD;
}

struct test_result validate_74x138(union uint_on_overlay_struct original_input) {
    struct overlay_struct_16 input = original_input.overlay_struct_16;
    uint8_t select = input.pin1 | (input.pin2 << 1) | (input.pin3 << 2);
    uint8_t output = input.pin15 | (input.pin14 << 1) | (input.pin13 << 2) | (input.pin12 << 3) | (input.pin11 << 4) | (input.pin10 << 5) | (input.pin9 << 6) | (input.pin7 << 7);
    if (input.pin16) { // VCC
        if (input.pin6 & !input.pin4 & !input.pin5) { // chip enabled
            // select == 0 then 1, select == 1 then 2, select == 2 then 4, select == 3 then 8, ...
            if (output == ~(1<<select)) {
                return TEST_RESULT_GOOD;
            } else {
                return TEST_RESULT_BAD;
            }
        } else { // chip not enabled
            if (output == 0xff) {
                return TEST_RESULT_GOOD;
            } else {
                return TEST_RESULT_BAD;
            }
        }
    } else {
        no_power_wait(original_input);
    }
    return TEST_RESULT_GOOD;
}
struct test_result validate_74x157(union uint_on_overlay_struct original_input) {
    struct overlay_struct_16 input = original_input.overlay_struct_16;
    uint8_t select = input.pin1;

    if (input.pin16) { // VCC
        if (!input.pin15) { // chip enabled
            if (!select) {
                if ((input.pin4 == input.pin2) &&
                    (input.pin7 == input.pin5) &&
                    (input.pin12 == input.pin14) &&
                    (input.pin9 == input.pin11)) {
                    return TEST_RESULT_GOOD;
                } else {
                    return TEST_RESULT_BAD;
                }
            } else {
                if ((input.pin4 == input.pin3) &&
                    (input.pin7 == input.pin6) &&
                    (input.pin12 == input.pin13) &&
                    (input.pin9 == input.pin10)) {
                    return TEST_RESULT_GOOD;
                } else {
                    return TEST_RESULT_BAD;
                }
            }
        } else { // chip not enabled
            // high-impedance
            // we can check for high-impedance heuristically
            // check for activity that would not be possible with an enabled (and working) chip
            // partially mirrors above code
            if (!select) {
                if ((input.pin4 == input.pin2) &&
                    (input.pin7 == input.pin5) &&
                    (input.pin12 == input.pin14) &&
                    (input.pin9 == input.pin11)) {
                    return TEST_RESULT_HIGH_Z_UNLIKELY;
                } else {
                    return TEST_RESULT_HIGH_Z_LIKELY;
                }
            } else {
                if ((input.pin4 == input.pin3) &&
                    (input.pin7 == input.pin6) &&
                    (input.pin12 == input.pin13) &&
                    (input.pin9 == input.pin10)) {
                    return TEST_RESULT_HIGH_Z_UNLIKELY;
                } else {
                    return TEST_RESULT_HIGH_Z_LIKELY;
                }
            }
        }
    } else {
        no_power_wait(original_input);
    }
    return TEST_RESULT_GOOD;
}

struct test_result (*chip_check_funcs[])(union uint_on_overlay_struct) = {
    validate_74x00,
    validate_74x02,
    validate_74x04,
    validate_74x08,
    validate_74x32,
    validate_74x125,
    validate_74x138,
    validate_74x157
};

void setup() {
    unsigned int i = 0, j = 0;
    size_t chars_read = 0;
    char input_buffer[CHIP_NAME_MAX_LENGTH+1] = { 0 };
    bool found = false;
    union uint_on_overlay_struct gpio_input = { 0 };
    register uint8_t gpio_input_b = 0, gpio_input_c = 0, gpio_input_d = 0; // intended effect of 'register': gpio_input_b = PORTB is translated into a single instruction (e.g., in r24, 0x05). seems to work as intended.
    register uint8_t gpio_input_b2 = 0, gpio_input_c2 = 0, gpio_input_d2 = 0;
    struct test_result test_result = { 0 };
    unsigned long int bad = 0, good = 0, high_z_likely = 0, high_z_unlikely = 0, high_z_fifty_fifty_matched = 0, high_z_fifty_fifty_unmatched = 0;
    unsigned long int bad_rate = 0, high_z_unlikely_rate = 0, high_z_fifty_fifty_matched_rate = 0;
    unsigned long int n_bool = 0, n_high_z = 0, n_high_z_fifty_fifty = 0, n = 0;

    Serial.begin(115200);

    pinMode(2, INPUT);
    pinMode(3, INPUT);
    pinMode(4, INPUT);
    pinMode(5, INPUT);
    pinMode(6, INPUT);
    pinMode(7, INPUT);
    pinMode(8, INPUT);
    pinMode(9, INPUT);

    pinMode(10, INPUT);
    pinMode(11, INPUT);
    pinMode(12, INPUT);
    pinMode(A0, INPUT);
    pinMode(A1, INPUT);
    pinMode(A2, INPUT);
    pinMode(A3, INPUT);
    pinMode(A4, INPUT);

    pinMode(13, OUTPUT); // onboard LED

    // wait a little bit
    for (i = 0; i < 5; i++) {
        digitalWrite(13, HIGH);
        delay(500);
        digitalWrite(13, LOW);
        delay(500);
    }
    digitalWrite(13, HIGH);
    printf("Passive logic IC tester\n");
    printf("Make sure that voltages on pins to be tested are between 0 and 5V\n");
    printf("Don't forget to connect GND\n\n");

    Serial.flush(); // flush serial output
    while (Serial.available()) {
        Serial.read(); // flush serial input
    }

    printf("Verbose mode? (y/N)\n");
    while (!Serial.available());
    switch (Serial.read()) {
        case 'y':
        case 'Y':
            verbose = true;
            break;
        default:
            verbose = false;
    }

    while (true) {
        printf("Please enter IC to test (e.g., '74x04'). Backspace, cursor keys, etc., aren't supported.\n");
        printf("Supported ICs:\n");
        printf("74x00 (quad 2-input nand)\n");
        printf("74x02 (quad 2-input nor)\n");
        printf("74x04 (hex inverter)\n");
        printf("74x08 (quad 2-input and)\n");
        printf("74x32 (quad 2-input or)\n");
        printf("74x125 (quad bus buffer, negative enable)\n");
        printf("74x138 (3-to-8 decoder, inverting inputs)\n");
        printf("74x157 (quad 2-line to 1-line data selector, non-inverting outputs)\n");

        Serial.flush(); // flush serial output
        while (Serial.available()) {
            Serial.read(); // flush serial input
        }
        for (chars_read = 0; chars_read < CHIP_NAME_MAX_LENGTH; chars_read++) {
            while (!Serial.available());
            input_buffer[chars_read] = Serial.read();
        }
        printf("\n");
        for (i = 0; i < ARRAY_SIZE(chip_names); i++) {
            if (memcmp(input_buffer, chip_names[i], min(chars_read, strlen(chip_names[i]))) == 0) {
                printf("Found at %d\n", i);
                delay(1000);
                printf("Going to test a %s IC, press 'r' to re-select, or any other key to continue\n", chip_names[i]);
                while (!Serial.available());
                if (Serial.read() != 'r') {
                    found = true;
                }
                printf("\n");
                break; // break inner for loop
            }
        }
        if (found) {
            break; // break while loop
        }
        if (i == ARRAY_SIZE(chip_names)) {
            printf("Unknown chip: \"%s\"\n", input_buffer);
        }
    }

    if (chip_types[i] == STATELESS_LOGIC) {
        while (true) {
            while (true) {
                gpio_input_d = PIND;
                gpio_input_b = PINB;
                gpio_input_c = PINC;
#ifdef WAIT_UNTIL_CONSISTENT
                gpio_input_d2 = PIND;
                gpio_input_b2 = PINB;
                gpio_input_c2 = PINC;
                if ((gpio_input_d == gpio_input_d2) &&
                    (gpio_input_b == gpio_input_b2) &&
                    (gpio_input_c == gpio_input_c2)) {
                    break;
                }
#else
                break;
#endif
            }
            gpio_input.uint = ((gpio_input_d & 0xfc) >> 2) | ((uint16_t)(gpio_input_b & 0x1f) << 6) | ((uint16_t)(gpio_input_c & 0x1f) << 11);
            test_result = chip_check_funcs[i](gpio_input);
            switch (test_result.test_result_enum) {
                case BAD:
                    bad++;
                    printf_verbose("Bad state: %04x\n", gpio_input.uint);
                    break;
                case GOOD:
                    good++;
                    break;
                case HIGH_Z_UNLIKELY:
                    high_z_unlikely++;
                    break;
                case HIGH_Z_LIKELY:
                    high_z_likely++;
                    break;
                case HIGH_Z_INT2:
                    high_z_fifty_fifty_matched += test_result.int1;
                    high_z_fifty_fifty_unmatched += test_result.int2;
                    break;
            }
            n_bool = good + bad;
            n_high_z = high_z_likely + high_z_unlikely;
            n_high_z_fifty_fifty = high_z_fifty_fifty_matched + high_z_fifty_fifty_unmatched;
            n = n_bool + n_high_z + n_high_z_fifty_fifty;
            if ((n % REPORT_STATISTICS_EVERY_N_SAMPLES) == 0) {
                bad_rate = (100*bad)/n_bool; // fixed point, 0.01 -> 1
                high_z_unlikely_rate = (100*high_z_unlikely)/n_high_z;
                high_z_fifty_fifty_matched_rate = (100*high_z_fifty_fifty_matched)/n_high_z_fifty_fifty;
                printf("n: %lu\n", n);
                printf("n_bool: %lu bad_rate: %lu%%\n", n_bool, bad_rate);
                printf("n_high_z: %lu high_z_unlikely_rate: %lu%%\n", n_high_z, high_z_unlikely_rate);
                printf("n_high_z_fifty_fifty: %lu high_z_fifty_fifty_matched_rate: %lu%%\n", n_high_z_fifty_fifty, high_z_fifty_fifty_matched_rate);

// below lines are commented out because i didn't need this functionality after all    
//                if (bad_rate > MAX_BAD_RATE) { // some bad results are allowed because we might be sampling right before the chip had a chance to respond to inputs
//                    error_blink();
//                }
//                if (high_z_unlikely_rate > MAX_HIGH_Z_UNLIKELY_RATE) { // some bad results are allowed because we may sometimes sample right before the chip had a chance to respond to inputs
//                    error_blink();
//                }
            }
        }
    } else {
        for (j = 0; j < LOGIC_BUFFER_LEN; j++) {
            gpio_input_d = PORTD;
            gpio_input_b = PORTB;
            gpio_input_c = PORTC;
            logic_buffer[j] = ((gpio_input_d & 0xfc) >> 2) | ((uint32_t)(gpio_input_b & 0x1f) << 6) | ((uint32_t)(gpio_input_c & 0x1f) << 11);
        }
        for (j = 0; j < LOGIC_BUFFER_LEN; j++) {
            gpio_input.uint = logic_buffer[j];
            test_result = chip_check_funcs[i](gpio_input);
            switch (test_result.test_result_enum) {
                case BAD:
                    bad++;
                    printf_verbose("Bad state: %04x\n", logic_buffer[j]&0xffff);
                    break;
                case GOOD:
                    good++;
                    printf_verbose("Good state: %04x\n", logic_buffer[j]&0xffff);
                    break;
                case HIGH_Z_UNLIKELY:
                    high_z_unlikely++;
                    break;
                case HIGH_Z_LIKELY:
                    high_z_likely++;
                    break;
                case HIGH_Z_INT2:
                    high_z_fifty_fifty_matched += test_result.int1;
                    high_z_fifty_fifty_unmatched += test_result.int2;
                    break;
            }            
        }

        n_bool = good + bad;
        n_high_z = high_z_likely + high_z_unlikely;
        n_high_z_fifty_fifty = high_z_fifty_fifty_matched + high_z_fifty_fifty_unmatched;
        n = n_bool + n_high_z + n_high_z_fifty_fifty;

        bad_rate = (100*bad)/n_bool; // fixed point, 0.01 -> 1
        high_z_unlikely_rate = (100*high_z_unlikely)/n_high_z;
        high_z_fifty_fifty_matched_rate = (100*high_z_fifty_fifty_matched)/n_high_z_fifty_fifty;
        printf("n: %lu\n", n);
        printf("n_bool: %lu bad_rate: %lu%%\n", n_bool, bad_rate);
        printf("n_high_z: %lu high_z_unlikely_rate: %lu%%\n", n_high_z, high_z_unlikely_rate);
        printf("n_high_z_fifty_fifty: %lu high_z_fifty_fifty_matched_rate: %lu%%\n", n_high_z_fifty_fifty, high_z_fifty_fifty_matched_rate);
    }
}

void loop() {
}

再び、「アネックス(ANEX) なめたネジはずし」を使ってみました。

今度はうまくいきませんでした。普通の使い方でうまくいかなかったし、前回紹介した「横に穴を掘る」というやり方も、うまくいきませんでした。

なぜ、横に穴を掘ってもうまくいかなかったかというと、「余白」の多いネジだったからです!(※専門用語ではありません)

穴を掘り始める時は、ANEX君はずれやすい。(※正式名称ではありません)なので、マイナスドライバーなどで、固定する。「余白」の多いネジだと、穴が中心部のくぼみから離れたところにできてしまうリスクがあります。そうすると、マイナスドライバーがはまらない。(ここで諦めず中心部の穴と新たに掘った穴の間に穴を掘ることもできそうですけど、もう筋肉痛になりそうです)

今度は、プライヤでネジが取れました!(アホ、それ最初にやるよ、普通)

最初も、プライヤで試しましたよ。だけど、ネジの端はすべすべで、つかめなかったんだよ。

色々試した後、ネジの頭部がなんかギザギザになってきていた。すべすべじゃなくなったネジ、プライヤで回せるようになって、3時間の戦いが急に終わった。

色々試したというのは?下記の通りでございます。

  1. 色々なドライバー
  2. プライヤ
  3. ANEX君
  4. ネジすべり止め剤(ANEX君で作った穴はできが悪かったから)
  5. 半田でドライバーをネジに付ける(私は成功しなかったが、ユーチューブで見る限り、成功する人はそこそこいるみたい)
  6. アロンアルファ
  7. ネジがボロボロになっていたので、再びプライヤで

半田ごてを使うことによって、ネジは非常に熱くなり、もしかしたら、取れやすくなるのかな?

今度ねじがなめそうになったら、すぐにネジすべり止め剤を使おうと思います。また、それでも取れなかった場合に備えて、ロータリーツールを買おうと思います!!

なめたプラスネジ。ネジの「傘」(※正式名称ではありません)が広い。ANEX君で掘った穴はかろうじて見えます。もともと、きれいなネジだったが、色々試したらこんなにぎざぎざになって、プライヤで回せるようになりました。

Hitachi MB-H2 manual partial scan, function key card, tape button label scans / 日立 MB-H2 取扱説明書等

I bought another Hitachi MB-H2. Why? Who knows.

There are six major problems with this MB-H2, but it was worth buying because it came in the original box, complete with the manual and function key cards! Which I’ve scanned (i.e., the manual and function key cards); scroll right past the moldy pictures and you’ll find download links. Anyway, here are this machine’s problems:

  1. The cassette belt had turned into goo. I think all cassette belts turn into goo on this machine. Yum. (Fixed)
  2. The data recorder’s reed switch to detect the record protection tab is slightly broken (you have to push it when starting a recording). (Not fixed because easy to work around)
  3. There were keys that don’t work well. (Fixed). I have a blog post going into more detail about the tape drive and keyboard repair process.
  4. One key cap has gone missing. (Fix in progress)
  5. The screen is sometimes garbled. (Fix in progress.) (At first it looked like the VRAM was partially bad, but actually, after a couple resets, the screen looked fine. It looked like it was the -5V power supply, which widely swung around between maybe -3V and -6V, and just consists of a -12V input coming from the main power supply board, a Zener diode and a capacitor, but replacing that didn’t improve the situation, so it currently seems likely to me that one of (or multiple) RAM chips for the VRAM have an unusual fault. But that’s a story for another blog post.)
    Update 2023/08/13: And here is that blog post!
  6. The MB-H2 had a lot of mold inside! (Fixed)

I’m not a huge fan of mold. Seeing the inside of this machine honestly made me question my hobby choices!

Lid
Not-lid
Not-lid with board still in it

Eurghhhhh. I tried a couple of things:

  1. Soapy water, rubbed in using a toothbrush. Some improvement. Maybe 70% there?
  2. Window cleaner, rubbed in using a toothbrush. Some more improvement. Maybe 80%?
  3. Bleachy water, rubbed in using a toothbrush. Marginal improvement. Maybe 85%?
  4. Specialized mold cleaning solution (“カビキラー”), rubbed in using a toothbrush. Almost no mold left. Let’s say 98%.
  5. Magic eraser (“激落ち君”). No mold left.

Should have skipped (1) and (3). I normally wouldn’t have done (1) anyway, but I was out of window cleaner. Here’s an after pic:

Looking good

Finally, the lid had one more cosmetic problem bothering me:

Once the (rather heavy-duty) protective plastic sticker is off (which was easy, just some prying, but YMMV), a very brittle label reveals itself, which I removed using a hair dryer and a cup of patience.

Since my other MB-H2 doesn’t have this problem, I took a high-res picture of the label, traced it in Inkscape, and printed a new sticker. Some parts of the sticker are supposed to be cut out or translucent so you can see an LED shining through. Here’s my Inkscape SVG:

And another version with three slightly different sizes (the top one is the same size as the SVG above):

Japan’s convenience store printers (at the time of this writing) let you print “2L” (127 mm x 178 mm, or 5″ x 7.01389″) stickers, and the above SVG with three different sizes just fits on such a 2L format is just a little too big for that format, though it may depend on the printer and your print margins. Still usable though!

In addition, here are (400 dpi) scans of the two cards you slip into the card holder above the function keys:

The above PDF files should be exactly the correct size; you should be able to just print this on a piece of paper of your choosing without having to fiddle with the print settings. Note: Hitachi used paper that is somewhat thicker than regular office paper, perhaps post card-thick. Note: I am not 100% sure if the dot after “LIST” on F9 is supposed to be there. I believe that the Hitachi MB-H1 (and perhaps other variations) used the exact same cards.

And here’s the finished lid. I had some stickers lying around that are very easy to remove, and used those as my base, and printed my label on regular sticker paper at a nearby convenience store. I also put a slightly thicker semi-translucent sticker on top of that. (I probably should have kept the original heavy-duty translucent protective sticker, oh well.) Unfortunately the colors don’t quite look the same as the original, but they look believable, which is all that counts, right? (And maybe they’ll look closer in a few years.)

Unfortunately I screwed up a bit, you can see that the translucent sticker didn’t quite make it to the right edge. The bubbles are mostly gone by now.
ダイソーダイソー♪

I also did some “dry retrobrighting”. In the below picture, I had left the top lid hanging in a very sunny spot for multiple days (until I no longer thought it was getting better), and hadn’t applied any intentional retrobrighting to the case’s bottom part. You can just see that the lid’s plastic is a little brighter than the bottom part. (It looks more pronounced in real life, you’ll just have to trust me on that though.)

Manual

I scanned the manual using a book edge scanner! I didn’t scan the vast majority of the book dealing with how to program in BASIC because that part is almost identical to what’s in the manual for the H1, available at https://archive.org/details/Hitachi_MB-H1_documentation/. (Except for occasional explanations of H2-specific features, such as “CALL HCOPY”.)

I did scan the parts of the BASIC reference detailing the cassette-related keywords, though. I also scanned the pages detailing the monitor commands, only later realizing that the H1 has these pages too. There are some differences though, so phew I guess.

I only just now realized that the manual for the H2 contains two extra sample programs not included in the H1’s manual. I didn’t include them in the PDF but since they are cute, here are pictures:

One month early at the time of this writing

Sony HitBit MSX repairathon, mixed results

HB-10

See https://blog.qiqitori.com/2022/09/sony-hb-10-msx-repair-using-a-raspberry-pi-pico-based-logic-analyzer/.

HB-F900

See https://blog.qiqitori.com/2022/11/sony-hb-f900-repair/.

HB-T7

Symptoms: black screen, no sound, no activity whatsoever.

Summary: this machine contains a NiCad battery, which leaked. The machine started working after cleaning up the leak. That’s the good part. Let’s jump into the nitty-gritty.

That doesn’t look so bad?
That looks bad! Rest in peace, C88, keyboard connector, and various traces(?) D:
TBH, I thought this’d be a no-fix at this point.

Some electrolyte also made its way up to the keyboard PCB. All these traces tested fine.

The first step is to remove the battery. The second step is cleanup. Fortunately, all internet sources I read stated that cadmium doesn’t leak out of the battery, just the electrolyte (maybe at ppm or ppb levels, but nothing to worry about IMO as long as you don’t use your bare skin or tongue to clean up the leak). There are various sources out there about how to best go about the cleanup. I only used IPA for now. I’m thinking of giving it a water bath… But I’m too much of a chicken. D:

The NiCad battery destroyed a joystick trace, which prevents one of the joysticks from working correctly. (The traces to the right of the NiCad battery, right at the edge of the board, are for the upper joystick.) Unfortunately, this trace goes into a SMD chip, the Yamaha S3527. With through-hole chips, we could just add a bodge wire on the back of the board. With SMD chips, we have to repair the trace. I elected to skip doing this, as I’m not sure where the trace is broken. Judging by the visuals, it could be broken along its entire length or in multiple sections. Also, adding a long bodge wire on the top of the board seemed kind of messy. So we’ll live without one of the joysticks for now.

Edit 2022/12/04: I repaired the trace. I scraped off solder mask in various places, narrowed it down to a small region, scraped off more solder in that region, and tinned the trace. (I used my poor multimeter probes to scrape off the solder mask.) That made it a bit easier to find the exact location of the two breaks. I ended up using solder to bridge the breaks. These traces are tiny, and I also had to take out a ferrite bead to get to the right one. And of course, while soldering the ferrite bead back in, I melted the solder bridge and had to have another go at it. Though it was much easier of course as it was already mostly there. As the drop of solder could break off, or nearby rework could break the trace again, so this isn’t the best way to fix it. It would probably be best to solder in a very tiny wire.

Note: the camera used to take this picture is probably AI-interpolating, so it doesn’t really make sense to look at the hi-res version of this image.

The battery leak also destroyed a capacitor (C88) and the flexible cable for the keyboard (and its connector, more or less).

This picture was taken before I managed to open the computer. (Which was quite hard! Definitely the least “repairable” computer in this bunch.) I’m guessing that the previous owner probably opened this computer once (as there were a couple screws missing too) and this cable had already been exposed to the electrolyte at this point, causing the metal to separate from the plastic? Something like that, anyway.

I desoldered the keyboard connectors (both on the keyboard itself and the mainboard), and added pin headers instead. Here are some pictures of the removal process:

Using a knife to remove the stress relief(?) fabric.
At this point we can clean up the remaining bits of fabric with a couple cotton swabs dipped in IPA.
New pin headers on the keyboard PCB. These pin headers have a 90 degree bend. I made them point up a little bit though.
Bad quality and vertically long video showing how to loosen the pins after sucking away the solder. (This is the keyboard connector on the mainboard.)
Traces underneath the keyboard connector are looking bad, but are actually still okay.
This wire was too inflexible, the DuPont connectors are a little too high, the wire was too long, and unplugging and replugging is a lot of effort.

I’d planned on just running jumper wires with DuPont connectors, but my jumper wires were too long. The keyboard worked, but I was no longer able to fit the computer’s lid on its case because the jumper wires would bump against the cartridge slot. Putting on and taking off the jumper wires one-by-one was pretty annoying, so I decided to use a ribbon cable as used for IDE drives, plus four of the jumper wires. That made it pretty easy to plug and unplug the keyboard, which is useful when switching between software-based tests and hardware fixes. However, the lid still wouldn’t fit very well. It was just barely possible to close it, but it very much relied on the screws to hold it in place.

The keyboard had a couple of non-working keys. The keyboard isn’t very “repair-friendly”. Each key switch has two little feet. When taking out the key switches, you are likely to break them off.

When pulling off the key caps, the key switch will sometimes come off as well. If you’re unlucky, you may already have broken clips on the key switch at this point.
Usually, when pulling off the key caps, they will separate from the key switch below. However, to clean the metal pads below the key switch, you have to take out the key switch. Do not use pliers like I did in this image, or you’ll risk breaking the clips.
That white bit of plastic isn’t supposed to come off, but if you use pliers it probably will!
On the bright side, there are two of these clips, and one is generally enough to hold the key cap + key switch into the keyboard assembly.
I’m guessing that there is a special tool for these key switches. Or maybe you can put in a fine piece of metal (e.g., a dull needle) somehow. I don’t know. :(

HB-101

This machine just worked. The keyboard was filthy, so I cleaned the key caps using my ultrasonic cleaner. Removing the key caps was straightforward.

Dirty key caps.

Some time passes…

… Oh, that power cable looks safe, doesn’t it?

Maybe it’s a good idea to, at the very least, first of all examine the power cable before plugging in old electronics. I fixed the cable by cutting off this section, which meant I had to desolder the old section from the terminals, wire strip, and re-solder.

Stress relief thingy, not scratched and generally looking fine.
Stupid stress relief thingy, finally mostly in place but not quite. Scratched. Had to cut off some plastic. It’s still not 100% tight. Cut my finger too.
Glue. Take that, silly stress relief thingy.
Hardened glue. Also started desoldering the existing wire. Unfortunately melted the plastic a bit. Better to first desolder the pin from the back side of the board, then desolder the wire wrapped around it.
Apparently I forgot to take a picture after cleaning up. But here’s the machine, booted.
Those tape icons look like unhappy faces, which had me worried for a second.

HB-11

Just broken solder joints on the AV connector.

Just touching the AV connectors a little bit.
This was two months ago. I don’t remember whether this is before or after the repair. These bodge wires are factory and presumably help a bit even if the AV connectors are manhandled too roughly. (Or perhaps the audio pad was already broken at the factory?)
BTW I think audio broke again. I’ll take another look sometime soon.

Update 2022/12/03: Audio was indeed broken again. Bad solder joint on Q2’s emitter. Factory didn’t use enough solder. Hardly any solder, in fact. Fixed.

Summary

None of these repairs went off without a hitch.

HB-F900: success!

HB-10: success!

HB-T7: broken clips on key switches. Also need to put some more thought into the keyboard connection. Joystick not fixed. (The lower joystick is fine.) Update 2022/12/04: joystick traces are fixed (see above)

HB-101: needed glue. Perhaps there’s a special tool that they used at the factory for the stress relief thingy. Or perhaps they’re one-way. Not a huge problem IMO.

HB-11: audio is broken again. Sure, the connectors are probably not that great in the first place, but still… Update 2022/12/03: fixed.

Sony HB-F900 repair

Symptoms: black screen, no sound, nothing.

There was some NiCad battery leakage. Nothing compared to what I saw on the Sony HB-T7, and I was able to clean it up quickly.

There was no oscillating signal on the VDP’s XTAL 1/XTAL 2 (pins 63 and 64). Unlike all other retro computers I have seen thus far, this signal is generated by a 74LS628 IC on the analog board. However, it took me a while to figure out that that is the case. In fact, I did not realize this until I decided to take a look at the service manual for a very similar computer, the Sony HB-G900P, linked from the bottom of the page at https://www.msx.org/wiki/Sony_HB-G900P. This service manual mentioned the 74LS628 IC, and how to adjust it.

However, this IC wasn’t even getting 5V, and it turns out that there’s a 5V supply separate from the 5V supply used to supply power to all the other logic chips on the two boards. The IC gets its 5V through two linear regulators, first a 7809 turning 12V into 9V, and a 7805 turning 9V into 5V. The 7809 was broken with the following failure mode: up to about 10V, it output input minus 1-2V, and beyond that, it output 0.5-2V.

Input: 10V (display is inaccurate, actually closer to 9.7V), output: 8.93V
Input: 12V (actually 11.8V), output: 1.8V.
Running the machine with the regulator removed and my trusty USB power supply (set to 9V, display is imprecise) attached instead.

Replacing the 7809 fixed the machine. However, it appears that the floppy drives may be somewhat broken. I’ll look into that soon.

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

日立 MB-H2 アナログ基盤・ロジック基盤の裏側

See also Hitachi MB H2 (MSX) partial schematics and repair.

Raw analog board pic:

Just some colored traces on a board pic with RF shield removed:

(Grey: GND
Pink: 12V
Violet: 5V
I do not really remember the rest…)

Logic board:

(This is the backside of the logic board, left side is bottom side.)
Logic board, front side. This is actually a different H2’s board (with highly readable print on the chips), but should be the same.