11/20/2019 at 23:18 •
When thinking about how to present them, I wondered whether it would be possible to stream their display content to a larger screen. They have WiFi and a beefy processor, after all. At first I was thinking of implementing a VNC server, but then decided against that when the color palette experiments described in the previous logs showed that I would need to transform the color values before they could be sensibly displayed on a typical RGB display. With VNC, I would have to do that in the server (or write a customized client on the desktop, but that would defeat the purpose of using a standard protocol in the first place). I would rather avoid spending that CPU time on the device, potentially slowing down the games.
pew.show()call, when a client was connected, the same buffer of red/green values that would be sent over I2C to the LED driver was also packed into a binary WebSocket message and sent to the client.
This was still without the color transformation in the client. After adding that, prettifying the looks, and also transmitting the state of the buttons at every
pew.keys()call, it now looks like this:
The client code is remarkably short and simple, all graphics are done declaratively using SVG. All that SVG manipulation and rerendering is taking some toll on CPU usage though, probably drawing into a canvas procedurally would be more efficient there.
The code is available in a separate branch “remotedisplay” in the Git repository.
11/19/2019 at 22:21 •
In a comment to the previous log, @de∫hipu suggested relaxing the requirement that the userdata bits have no influence on the visible color, in exchange for getting more colors for smooth gradients. In other words, going from the left palette to something like the right one in the picture below (rearranged to show the periodicities more clearly).
The bitwise interpretation then becomes
7 6 5 4 3 2 1 0 0 0 darkness red green red/green compatible 0 1 hue brightness single-color compatible 1 red green true-color
where the lower bits of darkness and hue may be used as userdata.
I gave this a try and tested it both with a dedicated program that blinks the userdata bits on all colors of the palette and with the Sokoban game. The results were: A shift of 8 (the higher userdata bit) is visible everywhere. A shift of 4 (the lower userdata bit) is hardly visible in the brighter colors, more so in the darker ones. In the Sokoban game, the difference is not noticeable in the red/green, green, and red color schemes. It is noticeable in the yellow and orange schemes. (In the red scheme, there is still strictly no difference, the colors are still repeated. This is because I want a color scheme that is full red, but it can’t start at the very end of the hue spectrum, because there still needs to be room for the userdata shifts.)
This looks promising, but the decision is not entirely clear. It becomes a question of priority – what is more important, better gradients or strictly invisible userdata? Thinking about it, I now tend towards the former. The gradients may come in handy e.g. in the Maze3D game, where I want to show depth as darkness, whereas using the frame buffer for userdata storage was a bit of a hack anyway – a nice trick to simplify a tutorial, but not a model to follow in more complex projects.
Let’s keep this change for now and see how far we get. As a side effect, there are now 13 instead of 4 single-hue schemes, more finely granulated by hue. I didn’t update the tint.py scheme selector though.
(To explain the title, there are now 197 distinct colors in the default palette. In the previous one, there were 145.)
11/18/2019 at 21:33 •
So far, my pew Library has only displayed the same four black/red/green/orange colors as existing PewPew devices with red/green displays. The IS31FL3733 LED driver however supports 256 shades of brightness per color component. Let’s take advantage of that!
There is no precedent for how to handle this in the pew API, because so far no PewPews except for some of Radomir’s internal prototypes have supported more than 4 colors. The opportunity is mine to come up with a way that can hopefully be standardized in the future.
The existing pew.Pix drawing surface class supports 256 colors. With only two color components and 64 pixels, that ought to be enough for anybody. No need to extend anything there until we get to the RGB PewPew.
But how to choose the palette mapping the 256 possible values to a sensible subset of the 65536 possible colors? Some desirable properties are the following:
- Programs written for existing four-color PewPews should look the way they do on red/green devices by default or the way they do on single-color devices at the user’s choice, with the possible exception of text rendering, which may always look the single-color way (antialiased). This includes reserving at least one bit of the color value for user data that doesn’t affect the visible color, currently used in the Sokoban and Boulder games.
- Programs written for 256-color devices should degrade gracefully on 4-color devices.
- There should be enough colors to fill the screen with a smooth red-yellow-green-black gradient, i.e. the cartesian product of 8 shades of red and 8 shades of green. Preferably with the red and green components separated into different groups of bits in the color value, with contiguous ranges, to achieve a kind of true-color mode.
The second and third property contradict each other: The second requires that the most significant bit of each component is placed in bit 0 and 1, respectively, while the third requires placing them with their group. They cannot both be satisfied by the same set of colors. However, there is enough space in the 256 colors to have two sets that satisfy one each. So at least a program gets the choice of whether to degrade gracefully to 4-color devices or to use true-color mode.
Here is a palette I have come up with according to these considerations. (The colors don’t look that crummy on the real device, but I had some trouble converting the values from the yellowish green, super-bright red, and vastly different gamma of the LED matrix to something suitable for computer monitors.)
Examined in terms of the bits of the color value, it works like this:
7 6 5 4 3 2 1 0 0 0 darkness user data red green red/green compatible 0 1 hue user data brightness single-color compatible 1 red green true-color
Besides this default palette, programs should be able to set an arbitrary palette of their own. In addition, rather than setting a completely new palette, they should be able to cyclically shift the palette by a specified offset. This is useful for certain sorts of color animations, and by selecting specific offsets in the default palette it also allows the user to choose the color scheme to use for four-color programs.
This is now implemented in the library as a new API function pew.palette(), and as the next step I intend to write some demos using it to put it to the test.
11/17/2019 at 11:41 •
The first r2 PicoPew was successfully assembled and appears to be working fine. This time I did both sides in the reflow oven, that worked without the IC and switch falling off from the underside. I’m pretty happy with the r2 layout and don’t see any immediate need for a further revision.
In parallel, I was working on making the device more comfortable to hold. In particular the power switch has sharp corners that cut into one‘s fingers. Since I couldn‘t find any other place for that switch on the crowded board, I decided that the only way out would be to cover it with some kind of enclosure. That would have the added benefit of protecting the electronics from potential ESD damage from constantly touching the contacts, and the exposed components of the TinyPICO from mechanical damage.
I briefly thought about laser-cut wood or acrylic, similar to the PewPew Standalone frame, but then settled on 3D printing a plastic shell. I designed it in Onshape with the main goal of adding no thickness to the device and generally using as little volume as possible. For this reason, the antenna is still exposed, but its surface sits flush with the bottom of the case, which otherwise covers all of the TinyPICO. The case is held in place by friction-fitting on the TinyPICO, no stronger attachment is needed because during use it is pressed against the PicoPew board anyway. The shrink-wrap-like interpolation between the rectangular shape of the TinyPICO and the wider, rounded PicoPew board looks a bit awkward, but makes for nice rounded indentations for the index fingers. After printing a first version and applying some tweaks to the design (for one thing, I had forgotten to leave holes for the charging LED and dotstar), I printed the second revision yesterday and am satisfied with it for now.
11/11/2019 at 21:47 •
Revision 2 of the PCB has arrived from PCBWay and is looking good!
This time, just in case I might want to produce a small series of PicoPews later and be too lazy to solder them all by myself, I wanted to try a PCB manufacturer that also offers assembly services. Based on these reviews, I went with PCBWay.
I will save final judgement until I have assembled one, but at the first glance, the result looks pretty good. Silkscreen quality is much better than on the r1 boards from OSH Park. They are cut out without any leftover tabs or mouse bites, only a small bump in one corner, which saves me some work. Via drills are dead center. Some traces look marginally thinner, but it’s hard to judge without a microscope.
Revision 2 has some minor tweaks to the layout, in particular the outer holes for the matrix are moved further out. As expected, this requires more force to bend the pins, but it’s still possible to get the matrix to sit flush. In addition, there is a JST connector footprint for those who want to connect a battery that way – I won’t, because there’s no space for it in the small package I’m after.
11/10/2019 at 16:15 •
Attempting to make the whole package thinner, I went looking for shorter female pin headers than the 8 mm ones I had used. At western distributors, I found four manufacturers with suitable products: Samtec, 3M, PRECI-DIP, and Mill-Max. Three of them offered free samples, so I ordered some. With mixed success: One didn’t react to my inquiry at all. One sent me two pieces of the same part instead of the two different ones I had ordered. And one wrote the wrong country on the package and sent it on an odyssey around the world until it finally made it to me. In addition, I found one AliExpress seller, “E-Simpo”, who had two promising parts. They didn’t offer samples, but their 100-piece bags were affordable enough to risk an order.
So I ended up with 5 different samples to test, ranging in height from 3.5 mm to 5.0 mm. They all worked well mechanically with the 0.63 mm square pins of the male header. All but one were 2.54 mm wide, some with grooves between the pins, which would be an advantage, because that’s where my matrix pins lie. At 3.05 mm width, the Samtec CES-110-01-L-S was the first to be eliminated, because it covered more than half of the matrix holes, despite grooves. The remaining four were compared not only by height, but also by the insertion depth of the pin, because I wanted to leave as long pins as possible on the TinyPICO to keep it breadboard-friendly and compatible with other shields, rather than locking it in to the PicoPew. Champion in that regard was the E-Simpo 5 mm, which lets the pin go all the way through. Incidentally, it looks almost exactly the same as the ones sold by Adafruit in their “Short Feather Header Kit”.
groove height (mm) insertion depth (mm) Samtec SLW-110-01-F-S y 4.5 2.9 Mill-Max 801-47-010-002000/012000 y 4.2 3.9 E-Simpo 5mm n 5.0 5.0 E-Simpo 3.5mm n 3.5 2.7
To fit a battery between the two boards, I need a gap of about 5.5–6 mm between the board surfaces. This could be achieved either by a 3.5 mm female header and the 2.5 mm plastic part of the male header, or by any of the three larger female headers and a gap, with the plastic part of the male header on the outside of the TinyPICO. The former case would definitely not be breadboard-friendly though, with only 2.7 mm pins remaining, and the latter, even with the longest pins afforded by the 5 mm header, only barely worked with the breadboards I tried.
Alternatively, I could mount the TinyPICO upside-down, with the components facing inward. That would require me to redesign the PicoPew board layout, but it would have the advantage of a nicer outer surface, with only the PSRAM chip sticking out. However, that wouldn’t be the usual way a TinyPICO is mounted on a breadboard or on another shield, and, more importantly, it turned out that to make space for the battery between the antenna and the USB plug, the whole thing would have to be made very thick again. With the plastic part of the male header on the outside, an 8 mm female header would be needed, and while the maximum thickness would actually be about the same, it would make the whole thing look much thicker. With the plastic part of the male header on the inside, a 5 mm female header would suffice. In both cases, the TinyPICO would just barely remain breadboard-friendly again, due to the space taken by the antenna.
With none of the options totally appealing, I decided to go with the first solution for now, but with the headers swapped: By putting the female 3.5 mm header on the TinyPICO, I could make it breadboard-friendly by inserting the short end of a male header – at the expense of some height, but that shouldn’t usually matter on a breadboard.
I assembled a second one of the three r1 boards that way and ended up with a perfectly working, nice and thin second PicoPew. The total thickness is about 0.5 mm thinner than a Feather board with a PewPew Lite now, but it looks thinner because it’s only the antenna sticking out that much.
This time, I also tried reflow soldering the components on the bottom of the board using a DIY oven built by a FabLab colleague, which worked well. Being extra careful around the header and matrix pins made that hand-soldering part less troublesome than the first time around too.
09/14/2019 at 20:38 •
I assembled the first PicoPew today, and it worked on the first try! That made me happy. The only thing that didn’t work right was that I had the X and O buttons swapped, which was a simple fix in the software since the GPIO assignment is arbitrary.
Soldering the 0.5 mm pitch eTQFP chip with a soldering iron and solder wire worked acceptably once I found the desoldering wick to remove excess solder stuck between the pins, even without additional flux (I thought there was a flux pen at the FabLab but there wasn’t). Still, for the next try I am going to buy solder paste and use the hot air rework station or ask colleagues to borrow a reflow hotplate or oven.
Soldering the buttons worked much better than I had feared, it was no problem at all to get them down without shorting the very close solder pads in the center of the d-pad.
The matrix with the bent pins worked acceptably too, but the outer holes are really close to the ones for the pin header and I accidentally filled two with solder while soldering the latter. Also, with the header covering about half of the holes, first inserting the matrix and then soldering it without leaving too large molten marks on the plastic was a challenge. I wonder if there is any way I can make this easier to assemble. I can probably move the holes 0.1–0.2 mm further out, that might help, even though it requires more bending.
What gave me most trouble was actually the female pin headers. Their pins just wouldn’t take on solder properly, even after scraping them with a knife and with sandpaper. They seem to make good enough mechanical and electrical contact now, but I’m a little suspicious. It occurs to me now I could have tried the stackable ones that came with the TinyPICO, maybe they would work better.
With the standard-height headers, the whole thing gets really thick, and there is space for more than two of the LiPo batteries inside. I should look for shorter headers, like the ones Adafruit sells for the Feather boards.
I still need to add the battery, and then I’ll play with it for a while to see if it’s comfortable to hold or anything else needs improvement. I suspect it will be less comfortable to hold than the PewPew Lite FeatherWing, which has a somewhat well-defined box shape because the Feather board has the same dimensions as the shield in front, whereas on the PicoPew there are many edges and protrusions on the back.Other observations for revision 2:
- The holes for the pin header and battery could be a bit larger, the header pins just barely fit, and when you accidentally fill a hole with solder, it’s very hard to get it open again with the suction pump and desoldering wick.
- The holes for the vias can be smaller, it looks like my specified diameter was rounded up to a larger drill bit, and there is now very little annular ring remaining. They are also larger than I have seen on other OSH Park boards.
09/10/2019 at 20:20 •
With the prototype circuit working, it was time for the interesting part: PCB design! I planned to use EAGLE for that, I had used it before and had gotten somewhat familiar with its quirky user interface. I had version 7.5 installed, which was from before the acquisition by Autodesk, and didn’t bother checking if they had managed to ruin it yet in newer versions. Someday I should, but then I also resolved that for some future project I’m going to try KiCad and see if it’s any more or less capable and compatible with my brain.
The main challenge in the basic layout was posed by the fact that the spacing between the outer rows of pins of the LED matrix was pretty much the same as that of the TinyPICO, meaning that I couldn’t put them exactly on top of each other. I didn’t want to offset them sideways either however because that would make the board larger, and I was really trying to get it no wider than the LED matrix, 20 mm. A promising idea came in the form of @de∫hipu’s #Matrix Sanitizer, where he had rotated the two layers of pins by 90° relative to each other, which made them interlock perfectly, although with very little space left for pads. These were both on a 2.54 mm grid though, and my matrices had a 7.5 mm by 2.4 mm grid, which didn’t fit between the TinyPICO’s 2.54 mm pins at all. However, since I had found that the matrices could be crammed into a 2.54 mm grid with some force to bend the wires, what about bending just the outer 6 pins into the 2.54 mm grid and leaving all the others on the 2.4 by 7.5 mm one? That worked beautifully on the screen, and it even let me run a trace between the pads on the inside of the TinyPICO pins, which wouldn’t have been possible with all matrix pins on the 2.54 mm grid, but turned out absolutely necessary. How well it works mechanically with the actual matrix remains to be seen when the PCBs arrive.
I proceeded with placing the ISSI chip and wiring it up to the matrix, which turned out harder than expected: The pin row of the matrix closest to the chip caused a bottleneck, acting like a fence through whose gaps a lot of traces needed to go. When using 8 mil traces, I could run two of them between each pair of matrix pins on both sides, except for the outermost ones, where only one would fit because a TinyPICO pin was in the way. That gave me space for 24 traces – just barely enough for 16 matrix pins, 2 I²C lines, 2 buttons, and 2 power lines if I made those 20 mil wide. Whew!
After several iterations of nudging the ISSI chip around to get it farther away from the corner of the board so I could round off more of it, and the corresponding twiddling of traces between the matrix pads, I had a rough layout that satisfied both the design rule check and my desire to squeeze things neatly together for the tiniest board outline possible. It consisted of the usual 45°-angled traces, placed by hand somewhat irregularly because of the different grids involved. That I was not happy with, I want my traces curved, not angular, aligned to the environment, not to an arbitrary grid, neatly curving around obstacles and regularly spaced where several run in parallel.
So, a final round of prettification started. EAGLE’s tools however turned out severely inadequate for that. Maybe not surprising, because this is work that no engineer working on a commercial product would do – but I have the luxury of being a hobbyist, here to spend more time on it because I’m having fun, not less because it costs money. What to do? As always, make your own tools.
EAGLE can be extended with so-called ULPs (user language programs), and learning how to do that was an interesting experience. They are written in a C-like language that is easy to pick up using the documentation, but the system is very awkward because these programs cannot modify the internal data structures of the document, to bend or add traces as I wanted. The document tree is read-only, and the only thing the program can do to modify it is to generate a script that is executed at the end, consisting of commands that basically simulate mouse clicks and command-line entries – it can only do things that a human could do through the UI too.
This required some workarounds and awkward UIs, but in the end I succeeded in making the geometrical tools I needed for my layout prettification: ULPs that make tangents to arcs, intersect lines, tangentially extend or offset curves, interpolate with biarcs, and place wires exactly centered between two pads. I might release them in the future after they mature some more.
After prettifying the traces, I turned to the silkscreen. Apart from minimal component labels, I wanted logos, revision information, the URL of this project, and some decoration there. For the graphical elements, there are converters that can import SVG into EAGLE, but I seemed to remember that last time I tried that, I wasn’t quite satisfied with it, so I did it my own way: After preparing the polygons in the right way in Inkscape, converting them from SVG path syntax to EAGLE’s XML library format becomes a simple series of text search-and-replace operations.
For the text, since I consider EAGLE’s built-in vector font aesthetically beneath my dignity, I had previously done the same thing for some reusable elements like “ON” and “OFF” labels, but that approach wouldn’t scale to one-off things like the project URL. Instead, I made a library containing all ASCII characters as polygons in a nice font derived from Myriad, created in Inkscape using curve-offset and flattening operations, and many manual tweaks where those didn’t yield satisfying results. (Myriad, despite looking quite even, has a suprising variance in stroke width and is not easy to approximate with a constant stroke!) Together with a ULP that would string these glyphs together in the correct advance widths, I had a rudimentary typesetting tool for acceptably-looking text – even though again with an awkward UI: because a ULP has no access to library elements that aren’t already present in the document, it needs to import all needed glyphs first (heaping them on top of each other), and them measure and typeset them in a second call. (I could have stored the advance widths in the ULP instead of the library to avoid that, but that didn’t feel right either.)
I had chosen different GPIOs for the buttons on the PCB than in the prototype to simplify the layout, and also wired the LED matrix to the ISSI chip a bit differently, partly for the same reason. The other reason was that in the prototype, I had wired all the green column pins of the matrix to the first half of CS outputs and all the red columns to the second half, which made for an awkward layout of color values in the frame buffer. On the PCB, I had now wired the green and red columns alternately instead, which meant that the green byte and the red byte of the same pixel would now be adjacent in the frame buffer. I rewired the prototype to match that layout and adjusted the software.
That worked, and after a final double-check that the wiring of the prototype matched the PCB, as well as inspecting the Gerber files in Gerbv and printing them out in original size to check if I had gotten all the component sizes right, I was confident that the board was ready for production. I placed an order for a prototype batch at OSH Park and got these nice pictures.
Miraculously, the rewiring of the prototype had also fixed the one dead pixel – I could now turn on all the LEDs. How could that be? I was expecting that there would simply be a different dead pixel now. Apparently it wasn’t a broken memory cell after all, but how could the wiring affect a single LED? I was quite perplexed, until I remembered that at the end of the rewiring, I had removed an accidental solder bridge between two stripboard traces from the matrix base board, which I had assumed had happened during the rewiring. Apparently not, it had been there all along, and by connecting one row pin to one column pin, it shorted out one LED. Doh! I could even have detected that in software using the short detection, but for some reason it hadn’t occurred to me to try that.
This post concludes the retrospective series, we have now caught up with the current state of the project. I am waiting for the PCBs to arrive from OSH Park, which should happen in the next few days, and am eager to try out if my design works!
09/08/2019 at 21:30 •
One of the nice features of the IS31FL3733 LED driver is that it has separate power supply pins for various parts, which allows running the I²C communication at a different voltage than the LEDs. I could therefore leave the communication at 3.3 V as required by the microcontroller, but connect the LED drivers directly to the battery at up to 4.2 V, which means the LED current would not have to go through the voltage regulator of the TinyPICO and part of the power be dissipated as heat there. Would that allow me to save power, a desirable thing when planning to use a battery as small as a 100 mAh LiPo? I took some measurements to find out.
In the final series of measurements, I powered the TinyPICO through its BAT pin from my bench power supply at different voltages – simulating a battery at different charge levels – and measured the total current there, with all LEDs fully on, but with different global brightness values, and with the power pins of the LED driver connected to either the BAT pin or the 3V3 pin on the TinyPICO. As a brightness reference, I used a PewPew Lite and adjusted the global brightness on the prototype until their brightnesses visually matched.
The results were not what I initially expected, but in hindsight completely explainable: It made no difference at all to the total current consumption at the same brightness whether the LED driver was connected at 3.3 V through the voltage regulator or directly to the battery. The current was constant independently of the supply voltage (once the voltage was high enough to actually reach that current, which except at the highest brightness setting generally happened earlier than the ESP32 would even run), varying from 50 mA at the lowest brightness to 350 mA at the highest. The brightness would not vary with the supply voltage, the match with the reference PewPew was always at the same setting. In other words, the additional power available with the higher voltage did not make it into light as I had hoped, but was dissipated somewhere in the driver chip or in the LEDs themselves.
Thinking about it afterwards, I realized that this was exactly the way it needed to be. The average current through an LED needs to be held constant not just over various numbers of LEDs being on, but also over various supply voltages, because on a power supply with a nonnegligible internal resistance, the voltage will automatically vary with the number of LEDs being on, and we do not want the brightness to vary then. Also, since the brightness of an LED is approximately proportional to the current, its power efficiency decreases with increasing voltage, and it is not beneficial to run it at a higher voltage but lower PWM duty cycle.
Another effect of connecting the LED power to the BAT pin is that when the system is running without a battery, powered from USB or the 5V pin, the LEDs are powered through the battery charger. It turned out that the charger was very confused by that load that didn’t act like a battery and would sometimes deliver little current and sometimes a lot, resulting in a randomly shifting display brightness.
In conclusion, since there was no power consumption advantage to connecting the LED power directly to the battery, but a disadvantage in the case of running without a battery, I decided to connect it to 3.3 V along with the logic power. This would also make the circuit simpler, and the voltage regulator of the TinyPICO, rated at 700 mA, should take it easily.
09/07/2019 at 16:18 •
Meanwhile, my TinyPICOs had arrived, and the picture shows one driving the prototype. With the wires going to the breadboard on the left, connected to one unused row output on the green PCB and two column outputs at the matrix, I was testing whether I could use the open/short detection feature of the ISSI chip to read buttons. It worked in principle, I could detect from the software whether the wires were connected, but unfortunately a scan resulted in a visible flash of the display, either dark when set to the minimum current as recommended, or bright at a higher current. I therefore concluded that this was not a practical solution.
As alternative solutions for connecting the buttons, I considered using some IO expander chip or a second microcontroller such as an ATtiny. With the latter, I would also be able to offload the latching feature of the PewPew key reading API from the main microcontroller (it remembers which keys have ever been down since the last check, no matter how briefly). Either would be connected to the same pair of I²C pins as the display driver and thereby leave lots of other GPIOs available for other uses, such as maybe a 12-pin PewPew Standalone connector. The disadvantage, on the other hand, would be the space needed on the board for at least one additional component, which might compromise my goal of making the device as tiny as possible.
In the end, I decided to just use ESP32 GPIOs for the buttons – the TinyPICO had enough of them, and I was unlikely to need them for anything else. If I left DAC1 and GPIO4 open, it would still be possible to connect an official TinyPICO audio shield, for people who want their games to make noise. (I usually don’t and am happy with the lack of sound in the PewPew standard.)
I had bought some tiny buttons from Mouser, with a rectangular package, a relatively large cap that is still comfortable to push with bare fingers, and a nice clicky feel. The same size was used in the PewPew Lite for D1 Mini. I experimented with several arrangements and settled on one where the directional buttons are arranged in a cross, visually emphasizing their meaning. It uses a little more space than the most compact arrangement possible, but assuming that I would arrange the buttons to the left and right of the display, as in the FeatherWing, my preferred arrangement, I would have that space. Whether to choose that arrangement or the Gameboy-like arrangement below the display as in the D1 Mini shield would be decided later based on what was easier to achieve in the PCB layout.
Radomir later also graciously gave me two of the impressively small D1 Mini shield PCBs. After populating them (including working around some design flaws where he had pushed Fritzing to its limits), I could test that arrangement and found that the buttons were a bit too close to the display, and the two groups a bit too close to each other, for me to comfortably use with two thumbs.
For my prototype, I soldered six buttons in my chosen arrangement onto a corner of an upside-down perfboard (luckily they fit well onto its 2.54 mm grid) and attached wires that would go into the breadboard with the TinyPICO.
With this, all the hardware for a fully functional PewPew was in place, and of course I couldn’t wait to also get the software ready. I started from the MicroPython pew library for PewPew Lite and replaced the display and key functions by ones suitable for my hardware. I used pin interrupts to read the buttons and at first only looked at which pin triggered the interrupt to recognize which key was pressed. This however resulted in some occasional crosstalk between keys: sometimes, pressing one key would register as a simultaneous press of that key and one of its neighbors. I do not know whether these spurious interrupts were caused by inductive coupling between the parallel wires or by something inside the microcontroller. The problem disappeared when I, inside the interrupt handler, actively read the state of all 6 pins to decide which ones were pressed. Success! I was now able to play all the existing PewPew games on my prototype.