back to archive

Building a USB display from a salvaged smartwatch LCD

2 Mbps of UART proved software USB was pointless. Then came an RP2040, a DMA rewrite, and a colour bug I could not see.

Building a USB display from a salvaged smartwatch LCD
on this page14 sections

In the last two posts I reverse engineered a ₹249 smartwatch, flashed my own firmware onto its TLSR8232, and then gutted it into an AirTag. Gutting it meant desoldering the display, which went into a parts box.

Hacking a ₹249 Smartwatch: Custom Firmware on TLSR8232
post
Hacking a ₹249 Smartwatch: Custom Firmware on TLSR8232

A bike navigation idea led me to reverse engineer a ₹249 smartwatch, crack its SWire debug port, and flash my own firmware on it.

Turning a ₹249 Smartwatch Into a DIY AirTag Tracker
post
Turning a ₹249 Smartwatch Into a DIY AirTag Tracker

I lost my wallet on a night bus, so I turned a hacked ₹249 smartwatch into an Apple FindMy tracker and built my own app to follow it.

I have wanted a desktop buddy for years. Small screen, sits on the monitor, shows whatever I tell it to. I had the screen. I had a soldering iron. What I did not have was any idea how much of this project would turn out to be about USB rather than about pixels.

The finished thing plays video at 15.7 fps and the number is not a disappointment. Getting there took two dead ends, an etched PCB, and a colour bug that survived every test I threw at it.

The screen I was working with

The panel is an ST7735S, 128 by 128, RGB565. Two bytes per pixel:

plain
128 × 128 × 2 = 32,768 bytes per frame

That number decides the whole project. At 30 fps you need 983,040 bytes per second, which is about 7.9 Mbps of payload before any protocol overhead. Keep it in mind, because every transport I considered gets measured against it.

Where the panel started: still attached to the watch board, running off a LiPo, wired up for single wire flashing. The amber flex is the only thing I needed to keep.

It comes off the watch motherboard on a 13 pin flex connector, and this is the one part of the project I never had to work out for myself. The panel's FPC pinout is already documented in rbaron's m6-reveng, thirteen pins from TP0 through GND, and I had used it in the previous post to trace which SoC pin each one landed on.

The documented pinout laid over the connector it belongs to. Seven of the thirteen matter: SDA, SCL, RS, RES, CS, VCC, GND. TP0 and TP1 are factory test pads and NC is nothing.

The wrong question, asked well

My first plan was to keep the TLSR8232 and make it a USB to SPI bridge. The watch already had it, it was already wired to the display, and I had already written firmware for it. Reusing it felt obvious.

The TLSR8232 has no USB peripheral. Not a limited one, none at all. It has SPI, UART, I2C, ADC, PWM, GPIO and a BLE radio, and its own single wire debug interface called SWire. So a USB bridge meant bit-banging USB on two GPIOs, the way V-USB does on AVR.

The timing budget is the first thing to check. USB Low Speed runs at 1.5 Mbps, so one bit lasts 667 ns.

plain
TC32 core at 24 MHz  →  41.7 ns per cycle  →  16 cycles per USB bit
TC32 core at 48 MHz  →  20.8 ns per cycle  →  32 cycles per USB bit

I had assumed 24 MHz. The datasheet says up to 48 MHz, which doubled the budget and made the idea look plausible for about a day. In those 32 cycles the firmware has to sample the line, decode NRZI, undo bit stuffing, update a CRC, advance a state machine and drive the next output bit. V-USB does it in cycle counted AVR assembly, which is exactly the part that does not port to a proprietary core.

There is no software USB implementation for the TLSR8232. I looked. There is none for anything in that family, because the family has no reason to want one.

Measuring instead of guessing

I have no oscilloscope and no logic analyzer. All I had was an FT232RL, which does up to 3 Mbaud.

So I used UART as a proxy. It does not prove USB will work, since USB is timing critical signalling and UART is not, but it does answer the question underneath: is this chip fast enough to be in the conversation at all? The watch already had PB4 and PB5 free, and they were the factory UART pins.

I already had the test rig from the previous project: a UART example ported from m6-reveng that sends a counter over the FPC debug pads every half second, and a helper that sets the baud rate up from a 16 MHz system clock.

cexamples/uart/main.c
int main() {
    cpu_wakeup_init();
    clock_init(SYS_CLK_16M_Crystal);
    gpio_init();

    uart_helper_init(recv_buff, sizeof(recv_buff));

    int tick = 0;
    while (1) {
        my_sprintf((char *)send_buff.data, "Tick counter: #%d\n", tick++);
        send_buff.len = strlen((char *)send_buff.data);
        uart_send(&send_buff);
        sleep_ms(500);
    }
}

A counter is a better test than a fixed byte, because a dropped or corrupted byte shows up as a gap in the sequence rather than as garbage you have to squint at. All I had to change was the divider pair in uart_helper_init, which is where the 115200 comes from.

921600 baud worked. Then 2,000,000 baud worked, stable, no dropped bytes, and that is from a 16 MHz system clock rather than the 48 MHz the datasheet allows. For a watch SoC that is genuinely impressive, and it removed every hardware doubt I had about the chip.

It also killed the project.

plain
UART at 2 Mbps      ≈ 200,000 bytes/s  →  6.1 fps full screen
USB Low Speed       ≈ 150,000 bytes/s  →  4.6 fps full screen

That is the whole decision, and it took a calculator rather than a sprint. Frame size against transport speed gives the frame rate, the frame rate against the risk of hand writing a USB stack gives the answer, and the answer was that succeeding would leave me slower than the UART cable already sitting on my desk. So the plan went in the bin the same evening it was measured, which is the cheapest possible way for a plan to die.

The experiment I ran to justify the plan is the one that ended it.

Why Full Speed is the only USB worth wanting

Once the numbers were on the table the shape of the problem changed. It was never "can I do USB", it was "which USB is worth doing".

Transport

Practical throughput

Full frame fps

BLE on this chip

10 to 20 KB/s

under 1

Software USB Low Speed

120 to 150 KB/s

4.6

UART at 2 Mbps (measured)

200 KB/s

6.1

CH343 UART at 6 Mbps

600 KB/s

18

USB Full Speed

about 1.2 MB/s

36

Only the last row clears the 30 fps bar. And Full Speed is exactly where software implementations stop being possible:

plain
USB Full Speed  = 12 Mbps    →  83.3 ns per bit
ESP32 at 240 MHz = 4.17 ns per cycle  →  20 cycles per bit

USB High Speed  = 480 Mbps   →  2.08 ns per bit
                             →  under one instruction per bit

Twenty cycles is not enough to decode a bit and also decide what to do about it. This is the part I found genuinely interesting: when people show off software USB on an RP2040, the CPU is not doing the work. The PIO block is. It is a hardware state machine that happens to be programmable, so the timing lives in silicon and the CPU only sees finished packets. That is the same reason a real USB peripheral works, just with the boundary drawn in a different place.

I did go down one more branch. The ESP32-WROOM-32 has no native USB either, and its dev board USB goes through a CP2102 or CH340, so the chip never sees USB at all. There is a working software USB host for it, but no maintained device stack, and on the ESP32 the software environment is a bigger enemy than the hardware: WiFi interrupts and FreeRTOS scheduling will eat your timing window. The TLSR8232 was actually the better candidate for software USB in one narrow sense, since it runs bare metal with nothing else competing for cycles.

Picking the chip

At this point I was buying hardware, so the question became which cheap board has real USB Full Speed silicon.

Board

USB

Price (India)

RP2040-Zero

native FS, 12 Mbps

₹180 to ₹250

ESP32-C3 SuperMini

native FS, 12 Mbps

₹200 to ₹250

STM32F103 Blue Pill

native FS, 12 Mbps

₹120 to ₹200

CH552

native FS, 12 Mbps

₹80 to ₹150

I already owned a Blue Pill, which by the "use what is on the desk" rule should have won. The ESP32-C3 SuperMini was the other real contender, and I nearly bought it.

The ESP32-C3 SuperMini. Native USB, no CH340 on the board, and it nearly got the job.

ESP32-C3

RP2040

Single RISC-V core

Dual Cortex-M0+

No PIO

PIO, the reason software USB works on this chip

400 KB SRAM

264 KB SRAM, still 8 framebuffers

USB Serial/JTAG, fixed function

TinyUSB, any class you like

WiFi and BLE you do not need here

No radio, no radio overhead

The deciding axis was the fixed function USB controller. The C3's USB is a Serial/JTAG peripheral, so CDC works fine but you cannot reshape it into a custom class later. The RP2040 with TinyUSB leaves that door open, and given that I did eventually need to think about vendor bulk classes, that turned out to matter.

₹235 for native USB, DMA, PIO and 264 KB of SRAM. I bought two.

Naming took a whole conversation. Both AIs pushed PixelBridge, on the reasonable grounds that it would still be accurate once ST7789 and ILI9341 were supported. I went with USB2TFT because it says what it does and matches the USB2UART and USB2SPI naming everyone already knows.

Then nothing worked for two weeks

Here is the part the throughput tables do not prepare you for.

I desoldered the display, stuck it on a piece of perf board, and connected it to the RP2040 with fine enamelled wire.

The first build. Thirteen flex fingers soldered to perf board pads and hand wired to the RP2040 with enamelled wire, which is about as many failure points as you can fit in one square inch.

Then I wrote firmware with Codex and a couple of other AIs, and got a blank screen.

Hours of it. I swapped pins. I tried hardware SPI, then software SPI, then different pins again. Nothing ever appeared. The problem with a blank screen is that it tells you nothing about where the fault is: bad solder joint on a 0.5 mm flex pad, a broken enamelled wire, wrong pin, wrong init sequence, wrong SPI mode. Every one of those looks identical from the outside, and I could not eliminate the wiring.

So I stopped. Not for an evening, for weeks. The display stayed on the perf board on my desk, in the open, with nothing protecting the glass.

That cost me. By the time I came back, two edges of the panel were damaged and the pixels along both of them are permanently scrambled. You can see it in every photo further down: a strip of confetti along the right side and the bottom. The picture area works perfectly, the edges are dead, and there is no firmware fix for a bruised panel. If you salvage a bare LCD, put it in a box the day you take it out.

When I did come back I finally did the thing I should have done first, which was to remove a variable instead of adding effort. I designed a board that carries both the display and the RP2040-Zero, so the whole assembly becomes one rigid object rather than a bundle of wires that moves every time I pick it up.

The replacement board in EasyEDA. Thirteen fingers at flex pitch at the bottom, fanned out to something solderable, with the RP2040-Zero sitting directly above it.

Then I made it the cheap way: printed on photo paper, toner transferred onto copper clad, etched at the kitchen sink.

Six copies, because toner transfer fails often enough that you want spares.
Copper cleaned back to bare metal. Any fingerprint here becomes a broken trace later.
Toner transferred off the paper and onto the copper. This is the mask.
Etched. Thirteen fingers at flex pitch, fanned out to something I can actually solder to.

I moved the display off the perf board onto that, and ran the same firmware test. Still nothing.

That was the point where the wiring stopped being a suspect. Same result on two completely different pieces of hardware means the fault is in the firmware, and now I could stop guessing and start reading code.

The bring-up bugs

A fresh Codex session found the display problem quickly, and it was not subtle. The firmware was holding chip select inactive for the entire transaction, skipped most of the initialisation sequence, and never actually wrote any pixels. CS on the ST7735S is active low. Fix those three things and the panel cycled red, green and blue once per second.

Pixels. Not the right ones, and not a colour I asked for, but after weeks of a dark panel a full screen of anything is the milestone.

Getting from there to USB took five more bugs, and the sequence is worth reading if you are ever stuck on TinyUSB not enumerating.

Five bugs between a working panel and a working USB device

  1. The 32 KB framebuffer was a local variable. The RP2040's default main stack is far smaller than that, so the firmware crashed before reaching its main loop and never enumerated. Moving it to static RAM fixed the crash.
  2. CFG_TUD_ENABLED was never set in tusb_config.h. The CDC class was configured, but the device stack itself was off, so tusb_init() was a no-op. Nothing on the wire at all.
  3. TinyUSB could not see tusb_config.h, because the project directory was not on the include path.
  4. With those fixed, still nothing. The next move was to add pico_fix_rp2040_usb_device_enumeration, the SDK's workaround for the RP2040-E5 erratum. That is where it got confusing, because the device started enumerating and then dropping off the bus.
  5. The diagnosis came from painting the screen as a status light, since there was no serial console: red on entry to USB init, green once TinyUSB was up, blue on mount, red again on unmount. The panel showed blue and then red. The host was completing enumeration and then losing the link, which pointed straight at the workaround that had just been added. Removing it fixed everything.

That last one deserves a note. The enumeration workaround exists for a real erratum on early RP2040 silicon, and on this board it was the cause rather than the cure. It is still deliberately disabled in the repo, with the reason written next to it, because it is exactly the kind of thing a future me would "fix" by turning it back on.

The other thing worth saying: none of that debugging needed to be that hard. dmesg is restricted on this machine, so several sessions went by without anyone reading the kernel log. journalctl -k works unprivileged and says precisely what the host thinks of your device:

plain
usb 1-4: new full-speed USB device number 34 using xhci_hcd
usb 1-4: New USB device found, idVendor=cafe, idProduct=4001
usb 1-4: Product: USB2TFT
usb 1-4: Manufacturer: e-lab innovations
cdc_acm 1-4:1.0: ttyACM0: USB ACM device

Eight lines that would have saved a lot of flash cycles.

The protocol

Deliberately boring. Eight byte header, then a full frame, big endian RGB565.

plain
"TFT1" + 0x00008000 + 32,768 bytes of pixel data

The reader scans for that header rather than assuming stream alignment, so a sender that dies halfway through a frame costs one frame instead of desynchronising everything after it. The board sends back a single K byte for each frame it has finished painting, which lets the host pace itself instead of guessing.

Making it fast

The first working version did about 12.5 fps and had two obvious problems, both of which were mine.

It read USB one byte at a time, which is 32,768 calls to tud_cdc_read() per frame. And it pushed pixels with a blocking SPI write at 8 MHz, which takes 32.8 ms per frame, during which tud_task() never runs and the USB stack is starved.

The rewrite was three changes:

What actually changed in the firmware

  1. Bulk reads straight into the framebuffer Only the eight header bytes are read one at a time. The payload is copied in whatever chunks the CDC FIFO happens to hold, which turns 32,768 calls into a handful.
  2. SPI at 31.25 MHz The RP2040 divides its 125 MHz peripheral clock, so the reachable rates are 125/2, 125/4, 125/6 and so on. 31.25 MHz sends a frame in 8.4 ms instead of 32.8 ms. The ST7735S datasheet wants about 15 MHz, so this is an overclock that happens to work on short wires.
  3. DMA into double buffers The panel gets painted from one framebuffer while USB fills the other, so the transfer costs no CPU time and tud_task() keeps running throughout. Two 32 KB buffers out of 264 KB is a cheap trade.

The DMA setup is the only part with a real trap in it:

cmain.c
static void display_poll(void) {
    if (!dma_busy || dma_channel_is_busy(dma_chan)) return;

    // DMA has queued the last byte; the SPI FIFO still has to shift it out
    // before CS may be released.
    while (spi_is_busy(SPI_PORT)) tight_loop_contents();
    cs_high();
    dma_busy = false;
    ack_pending = true;
}

DMA completion means the last byte reached the SPI peripheral, not that it reached the display. Release chip select on the DMA interrupt and you truncate the final pixels.

While I was in there I added one thing that changed the whole workflow. Opening the port at 1200 baud reboots the board into BOOTSEL, the same convention the Pico's own stdio driver uses:

cmain.c
void tud_cdc_line_coding_cb(uint8_t itf, cdc_line_coding_t const *coding) {
    (void) itf;
    if (coding->bit_rate == 1200) reset_usb_boot(0, 0);
}

Before this, every firmware iteration meant physically holding a button and replugging. After it, reflashing is two commands. When you are iterating twenty times an evening that is the difference between testing an idea and not bothering.

Where 16 fps actually comes from

The rewrite got me to 15.8 fps and no further. Something was capping it at almost exactly 512 KiB/s, and I wanted to know what before optimising anything else.

Three experiments, cheapest first, each designed to eliminate one suspect:

Test

Result

Paced at 30 fps

12.5 fps, 120 frames dropped

Unpaced, send everything

15.8 fps

Raw writes, no frame semantics

512 KiB/s

1, 4 and 16 frames per write()

512, 514, 514 KiB/s

Panel writes deleted from the firmware entirely

512 to 521 KiB/s

libusb straight to endpoint 0x02, cdc_acm detached

533, 543, 549 KiB/s

The batching test rules out host side write boundaries. Removing the display code rules out my SPI and DMA path, which costs about 2 percent. And the last row is the one I am glad I ran: with the kernel's cdc_acm driver detached and libusb writing to the same bulk endpoint, with completely unchanged firmware, throughput improves by 5 to 7 percent. Not double.

So the tty layer is not the problem, and neither is my code.

plain
USB Full Speed bulk ceiling = 19 packets × 64 bytes × 1000/s = 1,216,000 B/s
                            = 37.1 fps for 32,768 byte frames

Measured                    =   524,288 B/s  =  16.0 fps  =  43% of ceiling

Forty three percent is the normal figure for a single bulk endpoint on this silicon, because TinyUSB's RP2040 port does not double buffer bulk endpoints. The host cannot push into an endpoint that is not armed, and there is a re-arm gap after every 64 byte packet.

What I assumed was slow

What was actually slow

SPI clock at 8 MHz

Real, worth fixing, 2% of the total

Byte-at-a-time USB reads

Real, worth fixing, got 12.5 to 15.8 fps

The Linux tty layer

5 to 7%

Python doing the writes

Nothing, batching changed nothing

Nobody suspected the endpoint

Everything else

This is why the 15.7 fps in the demo video is not a disappointment. It is 43% of what the bus can do and the remaining 57% is not in my code. Every cheap option left points at sending fewer bytes rather than sending them faster, so I worked out what each one would buy:

Approach

Bytes per frame

fps

RGB565, today

32,768

16

RGB444, 12 bit colour

24,576

21

96 by 96, upscaled on the device

18,432

28

64 by 64, upscaled on the device

8,192

60 plus

Two alternating bulk OUT endpoints

32,768

about 32, estimated

Then I dropped the middle three. Every one of them pays for frame rate with picture quality, on a panel whose entire appeal is that it shows a real image, and 16 fps is already past the point where a video reads as video. Trading colour depth or resolution to chase a number I do not need is the kind of optimisation that feels productive and makes the product worse.

That leaves the last row, which is the only one that keeps full quality, and also the only number in this post I have not measured. Everything above 60 fps is academic anyway, because FRMCTR1 puts the panel's own refresh near 60 Hz and there is no TE pin wired to sync against.

Compression is a dead end here, which surprised me until I thought about it. RLE and inter-frame delta both need flat or static content. Video is neither. For a dashboard or a navigation screen it would be the single biggest win available.

Any video, any resolution

The host side stopped being interesting the moment I let ffmpeg do the work. It converts straight to rgb565be, which is exactly the byte order the firmware wants, so Python only relays finished frames.

bash
ffmpeg -i video.mp4 \
  -vf "fps=16,scale=128:128:force_original_aspect_ratio=increase,crop=128:128,setsar=1" \
  -an -f rawvideo -pix_fmt rgb565be -

The earlier image sender converts with a per pixel Python loop over 16,384 pixels, which is fine for a photo and hopeless for video. That is the entire reason there are two tools.

fps=16 resamples by time rather than by decimation, so the source rate genuinely does not matter. I tested that instead of assuming it:

Source

Result

1920 by 1080, h264, 60 fps

15.5 fps

480 by 854 portrait, 24 fps

15.6 fps

100 by 36 mkv, 5 fps

15.8 fps

720 by 576 anamorphic, SAR 16:11

15.6 fps

640 by 480 mpeg4 in an .avi

15.5 fps

The 5 fps clip has frames duplicated up and the 60 fps clip has them dropped, both at correct speed. setsar=1 is what keeps the anamorphic source from arriving squashed.

A real clip, 360 by 360, running for two and a half minutes:

plain
sent 2408 frames in 153.6s (15.7 fps), 24 dropped, 2407 acknowledged by the board

Twenty four dropped out of the 2432 it tried to send, and 153.6 seconds of wall clock for a 152.0 second video. No drift, no disconnect.

Here it is running, filmed off the panel:

▶ YouTube0:25
Watching The Odyssey as Christopher Nolan intended (128x128 over USB)16 fps of 128 by 128 RGB565, piped from ffmpeg over USB CDC. Filmed before the colour fix, so the tint is the bug in the next section.

The colour bug

Here is my favourite part, and the most embarrassing.

USB2TFT v0.1 playing a film on 1.44 inches. Two things wrong here: the confetti along the right and bottom edges is the desk damage, and the whole frame is the wrong colour because this was taken before the fix below.

I had that photo lined up for an Instagram story, captioned "watching The Odyssey as Christopher Nolan intended". What I did not notice is that the frame is wrong. Every frame I had ever sent was wrong.

The panel's colour filter is physically B-G-R behind the glass. Bit 3 of MADCTL tells the controller to swap red and blue for you, and my init sequence had it clear. So 0xF800, which every reference calls red, painted blue.

The pinout photo sent over USB and painted on the panel it documents. Recognisable, sharp, correctly framed, and completely the wrong colour. This is what a red and blue swap looks like when you are not looking for it.

I know this panel does that. I diagnosed it on the same display in the TLSR8232 project and wrote it up in that post. I still shipped the bug.

The reason it survived is worth more than the fix:

The BGR bit swaps red and blue and never touches green. So a frame with two channels transposed still shows correct greens, which makes it look plausible instead of broken.

The bring-up test was a red, green, blue colour cycle. That test cannot detect this fault. One channel is correct and the other two are silently traded, and the screen dutifully cycles three saturated colours in the right rhythm. Everybody involved, me included, took that as proof the panel was working and built DMA, double buffering, a full throughput investigation and video playback on top of it.

The fix is one bit:

cmain.c
// main.c
// This panel's colour filter is physically B-G-R, so bit 3 of MADCTL is set
// to make the controller swap red and blue for us and ordinary RGB565 comes
// out correct.  With the bit clear, 0xF800 paints blue.  Note that the bit
// never moves green, so a host-side (B, R, G) packing cannot be corrected
// here at all.
st_cmd(ST77XX_MADCTL); st_data8(0x08);

There is a corollary I had to relearn. Because green sits in the middle six bits either way, no MADCTL value can rescue a host that packs its pixels as blue, red, green. On the TLSR8232 the bug was on the host side and the panel bit was already correct. Here it was the exact opposite. The two failures look identical on screen.

So there is now a third tool in the repo whose only job is to make colour order measurable rather than a judgement call. It sends known words and prints what the panel should read:

bash
python3 tools/color_test.py $PORT --all

If red and blue trade places while green stays put, the MADCTL bit is wrong. Flat saturated bars are also the easiest way to spot SPI trouble, since signal noise shows up as speckle against a solid fill. On my panel the two damaged edges speckle no matter what, which is its own small lesson: once the hardware has a permanent fault, every test you run has to be read around it.

What I would tell past me

Remove variables before adding effort. Etching that PCB fixed nothing, and it was still the most useful thing I did, because it turned "wiring or firmware" into "firmware".

Put the fragile part away. Leaving a bare LCD face up on a desk for weeks cost me two edges of a panel I cannot buy a replacement for. That is the only damage in this project that no amount of debugging can undo.

A test that cannot fail is not evidence. The RGB cycle passed for weeks while the colours were wrong. I now write the test that would catch the specific thing I am claiming, not the test that makes the screen light up.

Read the kernel log first. journalctl -k had the answer for most of the USB debugging, and it went unread because dmesg was restricted and nobody looked for the alternative.

Measure before optimising, then measure what you eliminated. The three throughput experiments each cost about ten minutes and together they proved that 57% of the missing bandwidth was in neither my firmware nor my host code. Without them I would still be tuning SPI clocks.

The experiment that kills your plan is a good experiment. The 2 Mbps UART result was a success that told me to stop, and stopping saved weeks.

Where this stops, for now

The throughput work is finished as far as I am concerned. 16 fps at full colour is the honest capability of a single bulk endpoint on this chip, and the only upgrade I would still take is the two endpoint version that costs nothing in quality. That is a rewrite of the USB layer rather than an afternoon.

What I still want is the thing I wanted at the start, a desktop companion along the lines of Hey Taby but open. I have not started it. The hardware end is done, and 32 KB of pixels over USB at 16 fps is plenty for a face that blinks, so what is left is all software and design, which is the half I am worse at. I am not a designer or an animator. Taby's creator drew frames one by one in Figma and I know I would quit by the second animation, so if I do build it the approach will be to draw one character, lock it, produce eight or ten key expressions, and rig those rather than redraw a hundred frames.

That is a plan, not a project. If it turns into a project, it gets its own post.

The repo has the firmware, the three host tools, the wiring, and the measurements including the ones that ruled things out.

e-labInnovations/usb2tft
C 0 0
e-lab innovations

Browse the archive for more, or subscribe to get new posts in your inbox.

Discussion 0 comments

Be kind. I read everything but might take a day or two to reply.

no comments yet — be the first
// related

More from this category