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

Jump to ZX80 replica PCB Gerber files

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

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

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

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

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

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

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

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

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

A glimpse of the outcome

Tracing bitmapped PCB foils

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

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

Converting traced holes to actual drill holes

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

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

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

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

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

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

Excellent hole size.

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

Adhering to minimum spacing constraints

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

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

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

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

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

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


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

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

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

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

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

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

Procurement and assembly

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

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

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

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

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

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

I had some problems with the clock though:

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

RAM problem

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

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

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

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

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

Here is the logic analyzer output with the problematic signals:

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

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

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

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

    124      25 000a 21
    126      25 000b 40

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

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

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

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

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

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

To do

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

KiCad / Gerber files

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

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

PS3 controller on the Switch (or Xbox, etc.?)

Last year, I posted a couple of articles on using a USB-HID controller on an MSX and on a Nintendo Switch. I also posted an article on a PS3 controller repair.

A happy Pico enjoying a round of Mario Kart. (This particular Pico is doing something else, but it’s content watching its brother play.)

Well, it’s time to put 2 and 2 together and get that silly PS3 controller to work on the Switch! (Or on other devices supported by GP2040-CE, which as I understand includes the Xbox and PS4/PS5?) To do this, we just have to realize that the PS3 controller works on Linux. So we just need to figure out how Linux does it. And a quick Google query (though I can’t remember the words I chose) surfaced up an old patch from 2007 introducing PS3 support to the Linux kernel:

Add the USB HID quirk HID_QUIRK_SONY_PS3_CONTROLLER. This sends an
HID_REQ_GET_REPORT to the the PS3 controller to put the device into
‘operational mode’.

Looking into the patch, we see that there isn’t much to it, it’s just, as the description says, sending a single request to the controller:

+	result = usb_control_msg(dev, usb_rcvctrlpipe(dev, 0),
+				 (3 << 8) | 0xf2, ifnum, buf, 17,

So, how could we do this in 1) our code that we used for the MSX, and 2) in GP2040-CE?

Modifying Pico-PIO-USB to work with the PS3 controller

Pico-PIO-USB isn’t very over-engineered yet(?!), and we can just modify the enumerate_device function in pio_usb_host.c to check if we’re seeing a PS3 controller, and send the request. Near the bottom of this function, we send the “get_hid_report_descrpitor_request” to the USB device. Immediately after we add our own code, as below:

diff --git a/src/pio_usb_host.c b/src/pio_usb_host.c
index 864d048..0efc567 100644
--- a/src/pio_usb_host.c
+++ b/src/pio_usb_host.c
@@ -1018,6 +1018,21 @@ static int enumerate_device(usb_device_t *device, uint8_t address) {
+        // We need to send a special HID request otherwise the contoller won't do anything
+        if ((device->vid == 0x054c) && (device->pid == 0x0268)) {
+          usb_setup_packet_t get_hid_report_ps3_controller_request =
+          control_in_protocol(
+              device, (uint8_t *)&get_hid_report_ps3_controller_request,
+              sizeof(get_hid_report_ps3_controller_request), rx_buffer, LINUX__SIXAXIS_REPORT_0xF2_SIZE);
+          printf("\t\tPS3 controller response:");
+          for (int i = 0; i < LINUX__SIXAXIS_REPORT_0xF2_SIZE; i++) {
+            printf("%02x ", device->control_pipe.rx_buffer[i]);
+          }
+          printf("\n");
+          stdio_flush();
+        }
       } break;

This requires the following definitions to be added in usb_definitions.h. I just used the same names used in Linux (not the ancient 2007 version, but something recent):

diff --git a/src/usb_definitions.h b/src/usb_definitions.h
index c345bdd..845fbbc 100644
--- a/src/usb_definitions.h
+++ b/src/usb_definitions.h
@@ -302,10 +302,16 @@ enum {
   USB_REQ_REC_OTHER = 0x03,
+// some constants taken from Linux source code (.../ch9.h, .../hid.h)
 #define GET_DEVICE_DESCRIPTOR_REQ_DEFAULT                                      \
-  { USB_REQ_DIR_IN, 0x06, 0, 0x01, 0, 0, 0x12, 0 }
+  { USB_REQ_DIR_IN, LINUX__USB_REQ_GET_DESCRIPTOR, 0, 0x01, 0, 0, 0x12, 0 }
 #define GET_CONFIGURATION_DESCRIPTOR_REQ_DEFAULT                               \
-  { USB_REQ_DIR_IN, 0x06, 0, 0x02, 0, 0, 0x09, 0 }
+  { USB_REQ_DIR_IN, LINUX__USB_REQ_GET_DESCRIPTOR, 0, 0x02, 0, 0, 0x09, 0 }
 #define SET_CONFIGURATION_REQ_DEFAULT                                          \
   { USB_REQ_DIR_OUT, 0x09, 0, 0, 0, 0, 0, 0 }
 #define SET_ADDRESS_REQ_DEFAULT                                                \
@@ -313,10 +319,13 @@ enum {
 #define SET_HID_IDLE_REQ_DEFAULT                                               \
   { USB_REQ_TYP_CLASS | USB_REQ_REC_IFACE, 0x0A, 0, 0, 0, 0, 0, 0 }
 #define GET_HID_REPORT_DESCRIPTOR_DEFAULT                                      \
-  { USB_REQ_DIR_IN | USB_REQ_REC_IFACE, 0x06, 0, 0x22, 0, 0, 0xff, 0 }
+#define GET_HID_REPORT_PS3_CONTROLLER                               \
 #define GET_HUB_DESCRPTOR_REQUEST                                              \
   {                                                                            \
-    USB_REQ_DIR_IN | USB_REQ_TYP_CLASS | USB_REQ_REC_DEVICE, 0x06, 0, 0x29, 0, \
         0, 8, 0                                                                \

And that’s all!

So, will the same modifications work in GP2040-CE too? No, while GP2040-CE uses Pico-PIO-USB to get USB host functionality to work at all while the Pico is busy being a USB device, protocol-y stuff is handled by TinyUSB, which by the way is included in the Pico SDK. Looking at pspassthrough.cpp for inspiration, I found that if we just add the following code to our previously modified version of src/addons/keyboard_host.cpp, our PS3 controller wakes up from its daze and starts sending our inputs!

diff --git a/src/addons/keyboard_host.cpp b/src/addons/keyboard_host.cpp
index 59bc85e..88d82da 100644
--- a/src/addons/keyboard_host.cpp
+++ b/src/addons/keyboard_host.cpp
@@ -2,6 +2,8 @@
 #include "storagemanager.h"
 #include "usbhostmanager.h"
+uint8_t report_buffer[64];
 bool KeyboardHostAddon::available() {
     const KeyboardHostOptions& keyboardHostOptions = Storage::getInstance().getAddonOptions().keyboardHostOptions;
     return keyboardHostOptions.enabled &&
@@ -77,6 +79,10 @@ void KeyboardHostAddon::preprocess() {
 void KeyboardHostAddon::mount(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len) {
+  uint8_t* buf = report_buffer;
+  tuh_hid_get_report(dev_addr, instance, 0xf2, HID_REPORT_TYPE_FEATURE, buf, 17);
   _keyboard_host_enabled = true;
Works for me!

Commodore SR-37 calculator “repair” and review

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


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

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

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

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

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

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


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

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

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

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

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

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

Christmas 2023: DIY electronics kit for children

Wow, two weeks have passed since Christmas. Wow, we’re one week into 2024. Happy New Year!

I have an 8-year-old niece and I thought I’d give her an electronics kit for Christmas. Well, it seems that in modern electronics kits, electronic components are covered in a bunch of plastic, making everything look kind of childish. They are a little safer I suppose because they are too big to be swallowed. But at least I (as a child) didn’t really like stuff with a “Fisher-Price” look, I tended to want the real deal. So I decided to make my own electronics kit with my own manual! I just put in a few components I mostly had on hand (I also washed them before, just in case I’d used them somewhere dirty before):

  • 5 red LEDs with a low forward voltage (around 1.7V), this way you can show that they light up (barely) with very fresh alkaline batteries. (Note: most red LEDs have a higher forward voltage of 1.85V or so, and wouldn’t light up at all)
  • 5 blue LEDs
  • 5 white LEDs
  • 5 100 uF (or so) electrolytic capacitors
  • 5 470 uF (or so) electrolytic capacitors
  • 5 470 ohm (or so) resistors
  • 5 2n2222 transistors
  • 2 CR2032 batteries
  • 2 CR2032 battery holders (the ones I bought can just barely fit into a breadboard)
  • Optional: 4x AA/AAA battery holder and AA/AAA batteries (referenced in text, so make sure to remove reference if you don’t want to include them)
  • Some wires
  • 1 breadboard

I didn’t include a multimeter, but if the child’s/person’s household doesn’t have one, it might make sense to include one. (The manual doesn’t really fully explain how to use multimeters, however.)

In the manual, the first experiment makes use of the fact that blue and white LEDs have a forward voltage that is quite compatible with CR2032 coin cells. The experiment just sandwiches a coin cell between an LED’s legs.

Then the manual explains that this doesn’t work with red LEDs and a resistor is used to limit the current. (Actually it will probably be okay for a while because coin cells don’t give a lot of current.) Some effort is made to explain voltage, current, and resistance, but (hopefully) on a level that is (possibly, barely) understandable by an 8-year-old.

A little later, capacitors are explained a bit. And transistors. The final experiment is a circuit that has some twinkling LEDs. The following is a similar circuit, just to give you an idea:

Old video shot on a potato.

And here’s the completed set, just before wrapping it:

DIY electronics kit, complete with “Instuction Manual” (d’oh!)

The manual is in .odt format (can be opened in LibreOffice and similar) and you can download it below. It includes some copyrighted pictures from other sites (the battery pic, the diode symbol(s), the transistor pic, the capacitor pic, and the empty breadboard pics). The front page image is AI-generated but heavily edited. Note: LibreOffice may have a bug that prevents emoji from being included in exported PDF files. My printed booklet ended up not containing emoji and I noticed only much later. :(

I made a booklet using the following LibreOffice print settings and a stapler:

Make sure to select “A4” and “Landscape” before selecting “Brochure”, otherwise LibreOffice might get the layout wrong.

If you want to customize the manual for somebody else, you should read through the entire thing. Edit it to your heart’s content. There is at least one reference to “8-year-olds”, maybe do a search. There are references to “dad or uncle” and “parents”. The “license” is “public domain”. Feel free to give credit, but you don’t have to.

Displaying any image on an MSX, loading from a ROM

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

The tool on this page (Japanese) takes an image file and adds dithering and stuff:

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

This means we can easily convert this to a ROM by adding a loader that pokes everything into VRAM. We just need to get rid of the BSAVE header at the start (or ignore it in the loader), which looks like this according to

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

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

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

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

SetVdpWrite: macro high low ; from
	ld a,low
	out (0x99),a
	ld a,high+0x40
	out (0x99),a

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

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

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

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

Converting paths to circles in Inkscape

Or alternatively: how to get svg2shenzhen to recognize your drill paths as drill holes

(My) rationale: there is an Inkscape extension called svg2shenzhen. This extension creates Gerber and KiCad files that can be used to create printed circuit boards, from standard SVG files. Also, this extension has a funny name. Older, DIY printed circuit boards are just a high-DPI bitmap. Using Inkscape and this extension, you can trace the bitmap and then convert it to Gerber. However, most PCBs (especially old PCBs) need to have holes drilled. The drill locates are just circles in the bitmap, and after tracing, they’re just paths. The svg2shenzhen extension (at the time of this writing) detects circles in a certain layer as locations that need to be drilled, but not paths.

I’m not an Inkscape expert, but AFAIK Inkscape (at the time of this writing) doesn’t have a built-in tool to convert (mostly) circular paths to circles. So I wrote a simple extension that does this! It works fine on Linux. Not so sure about Windows.

Extensions are made of only two files, a file that describes the extension, and the extension code (which is in Python in many cases). These two files just have to be placed into the location shown in Edit -> Preferences -> System -> User extensions, which in my case is ~/.config/inkscape/extensions/.

Here are the two files, you can copy them into a text editor like Kate or gedit or Notepad, what have you, and save them into the above directory. I recommend keeping my file names, path2circle.inx and Note, some skeleton code in was generated by ChatGPT, though it was quite wrong. That’s where some of the verbose comments and extraneous code came from.


<?xml version="1.0" encoding="UTF-8"?>
            <submenu name="Custom"/>
        <command location="inx" interpreter="python"></command>

import inkex
from inkex import Circle

class Path2Circle(inkex.EffectExtension):
    def effect(self):
        # Iterate through all the selected objects in the SVG
        for node in self.svg.selection:
            # Check if the object is a path (or any other object type)
            if node.tag.endswith("path"):
                # Get the bounding box of the object
                x, y, width, height = self.get_object_dimensions(node)
                x = x.minimum
                y = y.minimum
                # with open('/path/to/debug/directory/debug_output.txt', 'a') as f:
                    # print(f"Object Dimensions: x={x}, y={y}, width={width}, height={height}", file=f)
                layer = self.svg.get_current_layer()
                diameter = min(width, height)
                layer.add(self.add_circle(x, y, diameter/2))

    def add_circle(self, x, y, radius):
        """Add a circle at the given location"""
        elem = Circle() = (x+radius, y+radius)
        elem.radius = radius
        return elem

    def get_object_dimensions(self, object_node):
        # Get the bounding box of the object
        bbox = object_node.bounding_box()

        # Extract the bounding box coordinates
        x = bbox.x
        y = bbox.y
        width = bbox.width
        height = bbox.height

        return x, y, width, height

if __name__ == '__main__':

To use the extension, you probably first need to restart Inkscape. (You do not need to restart Inkscape after changing the extension, however.) Select all the paths you’d like to convert, and then hit Extensions -> Custom -> Path2Circle. Note: the extension doesn’t actually care if the paths even remotely look like circles, so make sure to select the correct paths. You can easily modify the extension to calculate the radius differently, or e.g. replace paths with other objects, such as squares, rectangle, or ellipses. Let me know if you need help doing that.

Playing on the Nintendo Switch with a generic USB-HID controller (specifically, a fake PS3 controller)

Update: You can also make this work with real PS3 controllers

In a previous post, we did some warmups — playing MSX games using a fake PS3 controller. In this post, we’ll be using GP2040-CE to control a Nintendo Switch. (No analog stick support implemented at the moment, but it wouldn’t be hard.) At the time of writing, most of the code to do this is already there! And I’m sure there will be support for USB-HID controllers in no time, so this post will probably be outdated soon.
Update: Analog is implemented too, and the diff below has been updated.

Anyway, you just need to put GP2040-CE on your Pico and get into the web configuration. In add-ons, you enable keyboard support, and then set up the “Keyboard Host Configuration”, which looks like this:

GP2040-CE keyboard mapping and other configuration

Then you can connect a generic USB keyboard to the Raspberry Pi Pico, and connect the Pico to the Nintendo Switch. (For electrical reasons, I do not recommend setting a pin for 5V power here, and just putting the host USB +5 on the VBUS pin of the Pico.)

Things that could come in handy: Breadboard, Raspberry Pi Pico, USB port that can be connected to one of the Pico’s GPIO pins

If everything works and you can control Sonic using your keyboard, great, you can move on to the next step! If it didn’t work, it probably won’t magically get better from here on out, so make sure to check those connections. The green/white wires can be D+/D- or D-/D+!

Now we’ll perform a small modification to the existing code. I’m basing my work on commit 961c49d5b969ee749ae17bd4cbb2f0bad2380e71. Beware, this may or may not work with your controller. I’d recommend taking a look at the above-mentioned previous post where we modify a Pico-PIO-USB example and to check if your controller behaves the same way. I have only two controllers to test with, and I only tested with one! Anyway, here’s the diff:

diff --git a/headers/addons/keyboard_host.h b/headers/addons/keyboard_host.h
index af9c61b..74fb628 100644
--- a/headers/addons/keyboard_host.h
+++ b/headers/addons/keyboard_host.h
@@ -53,6 +53,7 @@ private:
 	bool _keyboard_host_enabled;
 	uint8_t getKeycodeFromModifier(uint8_t modifier);
 	void process_kbd_report(uint8_t dev_addr, hid_keyboard_report_t const *report);
+	void process_usb_gamepad_report(uint8_t dev_addr, const uint8_t *report);
 	GamepadState _keyboard_host_state;
 	KeyboardButtonMapping _keyboard_host_mapDpadUp;
 	KeyboardButtonMapping _keyboard_host_mapDpadDown;
@@ -74,4 +75,4 @@ private:
 	KeyboardButtonMapping _keyboard_host_mapButtonA2;
-#endif  // _KeyboardHost_H_
\ No newline at end of file
+#endif  // _KeyboardHost_H_
diff --git a/src/addons/keyboard_host.cpp b/src/addons/keyboard_host.cpp
index a5294e9..8f59f4a 100644
--- a/src/addons/keyboard_host.cpp
+++ b/src/addons/keyboard_host.cpp
@@ -63,12 +63,15 @@ void KeyboardHostAddon::setup() {
 void KeyboardHostAddon::preprocess() {
   Gamepad *gamepad = Storage::getInstance().GetGamepad();
+  gamepad->setDpadMode(DpadMode::DPAD_MODE_DIGITAL);
+  gamepad->hasLeftAnalogStick = true;
+  gamepad->hasRightAnalogStick = true;
   gamepad->state.dpad     |= _keyboard_host_state.dpad;
   gamepad->state.buttons  |= _keyboard_host_state.buttons;
-  gamepad->state.lx       |= _keyboard_host_state.lx;
-  gamepad->       |=;
-  gamepad->state.rx       |= _keyboard_host_state.rx;
-  gamepad->state.ry       |= _keyboard_host_state.ry;
+  gamepad->state.lx       = _keyboard_host_state.lx;
+  gamepad->       =;
+  gamepad->state.rx       = _keyboard_host_state.rx;
+  gamepad->state.ry       = _keyboard_host_state.ry;
   gamepad->       |=;
   gamepad->state.rt       |= _keyboard_host_state.rt;
@@ -89,10 +92,11 @@ void KeyboardHostAddon::report_received(uint8_t dev_addr, uint8_t instance, uint
   uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, instance);
   // tuh_hid_report_received_cb() will be invoked when report is available
-  if (itf_protocol != HID_ITF_PROTOCOL_KEYBOARD)
-    return;
-  process_kbd_report(dev_addr, (hid_keyboard_report_t const*) report );
+  if (itf_protocol == HID_ITF_PROTOCOL_KEYBOARD) {
+    process_kbd_report(dev_addr, (hid_keyboard_report_t const*) report );
+  } else {
+    process_usb_gamepad_report(dev_addr, report);
+  }
 uint8_t KeyboardHostAddon::getKeycodeFromModifier(uint8_t modifier) {
@@ -161,4 +165,96 @@ void KeyboardHostAddon::process_kbd_report(uint8_t dev_addr, hid_keyboard_report
         _keyboard_host_state.rt = 0;
\ No newline at end of file
+// 0x1: L2
+// 0x2: R2
+// 0x4: L1
+// 0x8: R1
+// 0x10: Triangle
+// 0x20: Circle
+// 0x40: X
+// 0x80: Square?
+// 0x100: ?
+// 0x200: ?
+// 0x400: R3
+// 0x800: Start
+// 0x1000: Up
+// 0x2000: Right
+// 0x4000: Down?
+// 0x8000: Left?
+#define BUTTON_L2 0x1
+#define BUTTON_R2 0x2
+#define BUTTON_L1 0x4
+#define BUTTON_R1 0x8
+#define BUTTON_SQUARE 0x10
+#define BUTTON_CROSS 0x20
+#define BUTTON_CIRCLE 0x40
+#define BUTTON_TRIANGLE 0x80
+#define BUTTON_SELECT 0x100 // ?
+#define BUTTON_L3 0x200 // ?
+#define BUTTON_R3 0x400 // ?
+#define BUTTON_START 0x800
+#define BUTTON_UP 0x1000
+#define BUTTON_RIGHT 0x2000
+#define BUTTON_DOWN 0x4000
+#define BUTTON_LEFT 0x8000
+void KeyboardHostAddon::process_usb_gamepad_report(uint8_t dev_addr, const uint8_t *report)
+  _keyboard_host_state.dpad = 0;
+  _keyboard_host_state.buttons = 0;
+  _keyboard_host_state.lx = GAMEPAD_JOYSTICK_MID;
+  _keyboard_host_state.rx = GAMEPAD_JOYSTICK_MID;
+  _keyboard_host_state.ry = GAMEPAD_JOYSTICK_MID;
+ = 0;
+  _keyboard_host_state.rt = 0;
+  uint16_t button_state = report[2] << 8 | report[3];
+  uint8_t left_analog_x = report[6];
+  uint8_t left_analog_y = report[7];
+  uint8_t right_analog_x = report[8];
+  uint8_t right_analog_y = report[9];
+  const GamepadOptions& gamepadOptions = Storage::getInstance().getGamepadOptions();
+  _keyboard_host_state.dpad |=
+            ((button_state & BUTTON_UP)    ? (gamepadOptions.invertYAxis ? _keyboard_host_mapDpadDown.buttonMask : _keyboard_host_mapDpadUp.buttonMask) : _keyboard_host_state.dpad)
+          | ((button_state & BUTTON_DOWN)  ? (gamepadOptions.invertYAxis ? _keyboard_host_mapDpadUp.buttonMask : _keyboard_host_mapDpadDown.buttonMask) : _keyboard_host_state.dpad)
+          | ((button_state & BUTTON_LEFT)  ? _keyboard_host_mapDpadLeft.buttonMask  : _keyboard_host_state.dpad)
+          | ((button_state & BUTTON_RIGHT) ? _keyboard_host_mapDpadRight.buttonMask : _keyboard_host_state.dpad)
+        ;
+  _keyboard_host_state.buttons |=
+      ((button_state & BUTTON_CROSS)  ? _keyboard_host_mapButtonB1.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_CIRCLE)  ? _keyboard_host_mapButtonB2.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_SQUARE)  ? _keyboard_host_mapButtonB3.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_TRIANGLE)  ? _keyboard_host_mapButtonB4.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_L1)  ? _keyboard_host_mapButtonL1.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_R1)  ? _keyboard_host_mapButtonR1.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_L2)  ? _keyboard_host_mapButtonL2.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_R2)  ? _keyboard_host_mapButtonR2.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_SELECT)  ? _keyboard_host_mapButtonS1.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_START)  ? _keyboard_host_mapButtonS2.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_L3)  ? _keyboard_host_mapButtonL3.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_R3)  ? _keyboard_host_mapButtonR3.buttonMask  : _keyboard_host_state.buttons)
+  ;
+  /*
+   * #define GAMEPAD_JOYSTICK_MIN 0
+   * lx, ly, rx, ry are 16-bit values, but our joystick produces 8-bit values
+   * our joystick's middle is at 0x80, so it would probably be slightly better to adjust that.
+   */
+  _keyboard_host_state.lx = (left_analog_x == 0x80) ? GAMEPAD_JOYSTICK_MID : (left_analog_x << 8 | left_analog_x);
+ = (left_analog_y == 0x80) ? GAMEPAD_JOYSTICK_MID : (left_analog_y << 8 | left_analog_y);
+  _keyboard_host_state.rx = (right_analog_x == 0x80) ? GAMEPAD_JOYSTICK_MID : (right_analog_x << 8 | right_analog_x);
+  _keyboard_host_state.ry = (right_analog_y == 0x80) ? GAMEPAD_JOYSTICK_MID : (right_analog_y << 8 | right_analog_y);
+ = 0;
+  _keyboard_host_state.rt = 0;

Good luck. Miraculously, everything worked perfectly for me. The keyboard worked immediately, the above code modification worked immediately without having to do any debugging, and I’ve gotta say, my fake PS3 controller feels quite okay! (Note that you will have to press the PlayStation button after connecting your PS3 controller.)

The red part just hides some clutter.

PS3 controller repair log

Symptoms on real PS3: probably “crazy behavior”, I didn’t actually test on a real PS3.
Symptoms when connected to a computer with a program open that displays the gamepad status: random button presses, button “flickering”, buttons going on and off randomly, possibly depending on how the controller is held.

Cause in my case: rubber cushion is worn out, and/or physical damage to the controller’s case and/or loss of one of the screws. The rubber cushion sits on a piece of plastic, and a flex cable is sandwiched between the rubber cushion and the PCB. If the rubber cushion loses some of its original height, for example due to wear, or if one of the controller’s screws are lost and the PCB isn’t pressed as hard against the rubber cushion as it used to, buttons will randomly appear pressed or unpressed. (When there is absolutely no connection between the flex cable and the PCB, all buttons will appear pressed. When the connection is flaky, buttons may appear pressed all the time, or go on and off.)

Fix: adding a little height to the cushion fixed the problem for me.

In this pic, I’m holding the flex cable with one of my fingers. The tube (it’s a piece of heat shrink, actually) is what I added to improve contact between the flex cable and PCB. The original rubber is still there.

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

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

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

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

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

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

Decoding the USB HID report descriptor

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

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

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

// 63 bytes

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

My controller’s button mappings are as follows:

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

MSX interface

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

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

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

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

Some measurements and wiring details

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

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

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

Deciding how to hook up the Pico to the joystick port

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

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

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

Other MSX machines

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


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

Kuninet’s homebrew MSX

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

So does it actually work?

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

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

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

Pacman <3


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

Source code

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

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

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

#include "pio_usb.h"


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

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

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

static usb_device_t *usb_device = NULL;

void core1_main() {

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

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

  while (true) {

static void gpio_setup()

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

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

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



  // all USB task run in core1


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

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

          if (ep == NULL) {

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

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

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

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

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

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

              if (button_state & BUTTON_B || button_state & BUTTON_B_ALT) {
                gpio_put(BUTTON_B_PIN, 0);
                gpio_set_dir(BUTTON_B_PIN, GPIO_OUT);
              } else {
                gpio_set_dir(BUTTON_B_PIN, GPIO_IN);

「DETEKER」の「電子工作でよく使う電子部品セット」の トランジスターについて


トランジスターには色々な特性があります。例えば、電圧の限界値、許容電流、周波数の限界、ノイズ、ゲイン。これらの特性によって、そのトランジスターの用途が決まってきます。これらの特性は、トランジスターの「データシート」で簡単に調べられます。ネットで、例えば「2n2222 datasheet」のような検索をすればすぐに出てきます。

DETEKER のキットに含まれているトランジスターは18種もありますが、残念ながらすべての用途がカバーできるわけではないです。キットに含まれているすべてのトランジスターを調べた結果、これらのカテゴリーのトランジスターがありました。

  • オーディオ
  • 汎用
  • 電波
  • 高電圧

また、NPN トランジスターと同じ特性を持った PNP 版のトランジスターも多く含まれています。

個人的には残念だと思うのは、モーターの駆動に使えるトランジスターがないこと。(小型モーターなら、2n2222 あたりでいけなくはないかもしれませんが。)

さて、表を作ったのでご確認ください。載せていない属性もたくさんあります。データシートも、用途によっては、ノイズ値などはあったりなかったりします。(PC でご覧の場合は Shift+マウスホイールなどで横にスクロールができます。)

The following table shows several important properties of the transistors that come in DETEKER’s “Basic Electronic Components Kit” that is available on Amazon. (Use Shift+mouse wheel to scroll horizontally)

トランジスター名C-E 耐電圧C 電流ゲイン (最低は)
低電流時 (1 mA)
ゲイン (最大)
低電流時 (1 mA)
ゲイン (標準値)
低電流時 (1 mA)
ゲイン (最低は)
高電流時 (50 mA)
周波数特性種類データシートに書いてある用途カテゴリーデータシートの URL
Transistor nameMax C-E VoltageC currentGain (min)
at low current
(e.g. 1 mA)
Gain (max)
at low current
(e.g. 1 mA)
Gain (typical)
at low current
(e.g. 1 mA)
Gain (min)
(at high current)
FrequencyTypeDatasheets explicitly mentionAuthor’s interpretationDatasheet URL
S805020700 mA10040011040NPNClass B push-pull audio amplifier
General purpose
Complementary to S8550
Audio (オーディオ)
General purpose (汎用)
S855020700 mA10040011040PNPSee S8050Audio
General purpose
S901240500 mA6440040150 MHzPNPAudio
S9013NPNDesigned for use in 1W output amplifier of portable radios in class B push-pull operation
Complementary to S9012
S901445100 mA601000280NPNPre-amplifier, low level & low noise
Complementary to S9015
S9015PNPSee S9014Audio
S90183050 mA28198100GBWP
700 MHz min, 1100 typ
NPNAM/FM amplifier, local oscillator of FM/VHF tunerRF (電波)
A1015-50-150 mA7040025
(at -150 mA)
PNPLow-frequency amplifier
Complementary to C1815
C1815NPNSee A1015Audio
A42305200 mA2540
(at 30 mA)
50 MHz
20V, 10 mA
NPNHigh voltageHigh voltage (高電圧)
A92-305-200 mA2540
(at 30 mA)
50 MHz
20V, 10 mA
PNPHigh voltageHigh voltage
A733-50-100 mA90600200Current Gain Bandwidth
100 MHz min, 180 typ
PNPAF output amplifierAudio
C94550100 mA90600200Current Gain Bandwidth
100 MHz min, 180 typ
NPNAF output amplifierAudio
2N222230800 mA50300250 MHzNPNGeneral purpose
2N3906-40200 mA80300250 MHzPNPSwitching and amplifier
Complementary to 2N3904
General purpose
2N3904NPNHigh-speed switching
Complementary to 2N3906
General purpose
2N5401150200 mA50240Current gain bandwidth product
100 MHz min, 300 max
PNPGeneral purposeGeneral purpose
2N5551160600 mA8030
(at 50 mA)
NPNHigh voltage