One day, just like Rick from Rick and Morty, I got a hardware idea out of thin air. I ride with my phone mounted on the handlebar, Google Maps open, screen burning battery in the sun. It works, but I never liked it. I want the phone in my pocket, and I am not going to ride around with an earphone in one ear listening to turn-by-turn voice. What I actually need is tiny: a small display on the handlebar, connected to the phone over Bluetooth, showing the next turn, distance to it, time remaining, and the current location name. No map rendering. Just the numbers and texts.
To be clear up front: that navigation system does not exist yet. This post is about the step that had to come first, getting my own firmware running on a ₹249 smartwatch.
Rule number one: under ₹500
Every project I build has a budget rule, and this one was strict: the whole thing under ₹500. That rule killed the obvious answer immediately. An ESP32 plus a decent TFT display crosses the budget on its own, before you even think about a battery, a charging circuit, and an enclosure.
Around the same time I was deep into hardware hacking videos on YouTube, mostly from Aaron Christophel, aka atc1441. The guy flashes Doom onto anything with a screen. Watching enough of his videos rewires how you look at cheap electronics: a commercial product is not a product, it is a pre-assembled dev board with a battery, an enclosure, a display, and a BLE SoC, sold below the price of its parts.
So the plan became: find the cheapest commercial device that has a color display, Bluetooth, and a programmable SoC, and replace its firmware with mine.
Cheap Chinese smartwatches fit exactly. Alibaba had plenty, but shipping to India adds 10 to 20 dollars and weeks of waiting, which breaks both the budget and my patience. Amazon and Flipkart had the same class of watches for around ₹250.
Buying blind
The listings never tell you what chip is inside. A ₹249 watch listing says "smart bracelet fitness tracker" and shows you watch faces; it does not say "Telink TLSR8232, SWS pad exposed, 512K flash." The SoC is the one spec that decides whether the watch is hackable at all, and it is the one spec nobody prints.
So you guess from indirect signals. These watches are all clones of a few reference designs sold under dozens of random brand names, and some model families historically carry a TLSR8232 while others can have anything. I shortlisted two Amazon listings where the odds looked decent, shared both links with Claude, and asked what it thought of my chances.
Pure gambling 😄
That was the honest verdict. The only guaranteed way to know if a watch has a TLSR8232 is to open it and look at the chip, and no seller is going to tell you. Claude laid out the realistic options: buy both listings and hope one hits (low stakes, worst case two useless watches), buy a D20/Y68 clone instead since that family historically means TLSR8232, or skip the whole thing. It also listed the red flags worth avoiding: watches with Nordic nRF or Dialog chips inside are useless here because the open Telink flashing toolchain does not exist for them, and anything too smart (GPS, heart rate apps, fancy features) runs a different class of SoC entirely. The ideal target is the dumbest, cheapest watch on the page.
I gambled properly: bought both, with return policies as my hedge. The round one, the eventual winner, was ₹249 on the listing, ₹251.25 after the marketplace fee. Half my budget, bet on a coin flip.
Both arrived after days of waiting. I opened both. One square, one round, same UI, near-identical motherboards, different SoCs. The square one had an HS6620D, the round one had the TLSR8232. Same product family, two completely different outcomes in the silicon lottery. Meanwhile I had found another smartwatch hacker on YouTube, Amir (@AnothersProjects), and asked him a few doubts over Telegram. His verdict was short: the TLSR8232 is gold, the HS6620D is a dead end. My own research said the same, so the square watch went back for a refund and the round one went under the screwdriver. It turned out to be a FitPro LT715 (PCB marked LT71E_MD_V1.4), with an ST7735 128x128 TFT.
Why the TLSR8232 is "gold"
The value of this chip is not the chip. It is the trail of people who already suffered for it. Dale from xor.co.za tried to hack a TLSR8232 watch using an STM32 Blue Pill as a programmer for Telink's proprietary single-wire debug protocol (SWire/SWS), and got stuck. Raphael Baron, better known as rbaron, then cracked the same chip family for the M6 fitness band (m6-reveng): read the SOC ID, dumped the stock firmware, and wrote custom firmware, publishing the Python tooling.
What separates those two outcomes is worth looking at closely, because it is not the method. Both of them used an STM32 as a SWire bridge. Everything that mattered was execution detail:
Dale | rbaron | |
|---|---|---|
STM32 SWS programmer | Yes | Yes |
Reset pin access | Weak or missing | Soldered directly to the reset capacitor |
CPU halt at boot | Unreliable | Reliable |
SWS timing calibration | Incomplete | Worked out and implemented |
Flash read and write | No | Yes |
Dale found the wall. rbaron found the door. Add pvvx's TlsrTools with a ready-made USB-to-SWire firmware for the Blue Pill, and the OpenEPaperLink project shipping a prebuilt BLE firmware for these watches, and you have a full toolchain that someone else already debugged.
The HS6620D has none of that. Same watch, same board, wrong chip, and it is a brick forever.
First experiment: an OTA flash, no screwdriver
Before committing to any soldering, there was a cheap experiment to run. I had watched Aaron flash his OpenEPaperLink firmware onto these Telink watches entirely over the air: his web flasher runs in Chrome, connects to the watch over Web Bluetooth, and uploads a new .bin on top of the stock firmware. It was never going to be the long-term flashing setup for firmware development, but as a first test it was perfect: if a non-stock binary boots over OTA, the watch is provably hackable before I touch a soldering iron.
The experiment lasted one attempt. I had been working through the OEPL setup with an AI in a chat window, it handed me a download link for the firmware, and I took the link at face value. Wrong file. It was a build for a different device in the OEPL family, and I pushed it to my watch without checking what I had actually downloaded. The upload completed cleanly, the watch rebooted into nothing. Black screen, no BLE advertisement, no OTA path back in. Soft-bricked, exactly the failure mode the flasher page warns about in bold.
The lesson is not "do not trust AI" so much as "a URL is not a fact." A binary going onto a device you cannot recover without opening it deserves thirty seconds of checking the filename against the project's own repo. I skipped those thirty seconds and bought a full day of work.
So the cheap experiment ended with a dead watch, and the screwdriver came out earlier than planned. The only way back in was SWire through the test pads.
Getting inside
The physical interface is three test pads on the PCB: SWS, 3V3, and GND. There is also a pad labeled DAT that looks tempting, but it is just a secondary GPIO, not part of the SWire interface. I did not need it.
Here is the catch, and it is a packaging decision, not a silicon one. SWire flashing wants the chip held in reset, so the programmer can take over the wire before the firmware boots and claims it. The TLSR8232 has a reset pin, but only in the bigger packages. rbaron's M6 board used one of those: RST on pin 26, running to a tiny capacitor he could solder to directly, which gave him a reliable halt whenever he wanted one. My watch and Dale's both use the 24-pin ET24 package, where that pin simply does not exist. No reset line, no halt, and a stock firmware that boots in milliseconds and grabs SWS as a GPIO. That is the difference between rbaron finishing and Dale writing "further work needs to be done."
Which puts my situation in an odd place. I had Dale's hardware problem, no reset pin, but not Dale's software situation, because my flash was already destroyed. There was no firmware left to boot, nothing to race, nothing to grab the pin. The chip powered up into whatever a TLSR8232 does when its flash is garbage, and just sat there with the wire free. My stupidity with the OTA had handed me the one thing the 24-pin package takes away.
That was the theory, anyway. Reality still charged me a full day.
Talking SWire needs a programmer, so I went looking at the options. Telink sells an official burning kit, which fails the budget test instantly. The community route is repurposing some other microcontroller as a SWire bridge: pvvx maintains TLSRPGM and TlsrTools, and the m6-reveng work had already proven the STM32 Blue Pill variant against this exact chip. What decided it was not the comparison table, though. It was digging through my hardware boxes and finding an old Blue Pill I had bought from AliExpress years ago to learn STM32, back when AliExpress was still legal in India. It never got much use for that. I pushed it into a breadboard and flashed pvvx's USB2Swire firmware onto it with an ST-Link:
st-flash --format ihex write binaries/USB2Swire-STM32F103C8-v06.hex
After that, lsusb should show iProduct: USB to SWire. Then four connections to the watch:
Blue Pill | Watch PCB pad |
|---|---|
A7 (via 1K resistor) | SWS |
A6 (direct) | SWS |
GND | GND |
3V3 | 3V3 |
The wiring is minimal. Getting the chip to answer was not. I spent the whole day on it without a single byte of progress. The script has exactly two bad moods and I saw both of them all day: RuntimeError: Unable to find a suitable SPI speed, meaning the chip never entered SWire mode, and walls of ff ff ff ff ff ff, meaning the Blue Pill is not even responding. I rechecked the solder joints, rechecked the 1K resistor, replugged USB, reflashed the Blue Pill from scratch, and went back to rbaron's blog to figure out whether his --reset flag was required, which is a strange question to research for a chip that has no reset pin to pull. Nothing worked, and by evening I had stopped trusting every part of the chain at once: the joints, the Blue Pill, the script, and myself.
If you are attempting this yourself, here is the failure table I wish I had pinned to the wall that morning. Every row is something I saw:
Symptom | Cause | Fix |
|---|---|---|
| Wrong hex on the Blue Pill | Reflash |
| Chip is not in SWire mode | Power the watch first, then immediately run the script |
| Blue Pill not responding | Check the serial port, replug USB |
| Partial connection | Check SWS wiring, confirm the 1K resistor on A7 |
Then I took a break for Maghrib prayer. Came back, powered the watch, ran the same command I had been running all day:
python tlsr82-debugger-client.py --serial-port /dev/ttyACM0 --debug get_soc_id
Trying speed 2
Trying speed 3
Trying speed 4
Trying speed 5
Found and set suitable SWS speed: 5
SOC ID: 0x5316
Four lines of the tool walking up the SWS clock, and then the chip answered. That is what the failure had been all day: the client sweeps a range of speeds looking for one the chip will talk at, and when none of them land it gives up with the SPI speed error. This time speed 5 stuck.
0x5316 is the TLSR8232 saying its own name, after a full day of silence. I still do not know what was different about that attempt. I have decided not to question it.
From here the same script reads and writes the chip's flash. The stock firmware was already gone by this point, overwritten by my bad OTA, which is its own lesson: dump the flash before you flash anything, while the watch still works. I did it in the wrong order.
I had not written a single line of firmware at this point. All I did was upload someone else's binary, OpenEPaperLink's ATC_Cheapo_BLE_OEPL_Watch.bin, this time the correct file, downloaded from the project's own repo instead of a link in a chat window. It booted, at 11:20 that night.
That proved three things at once: the SWire path works end to end, the display physically works under non-stock firmware, and the BLE stack comes up.
Look at the third line on that screen: the watch is printing its own BLE name. That turned out to be an invitation. Aaron has a second web tool, an image uploader that pushes a picture to a running OEPL device over BLE. I clicked connect, the browser's pairing dialog opened, and there was ATC_717405, the exact string the watch was showing me. Connected first try.
The first upload turned the panel solid white. Not what I asked for, but the display had changed the instant I clicked a button in a browser, which is its own kind of progress. The cause was boring: the tool's canvas was still at its default 184x384, the shape of some shelf label, so the pixel data arrived with the wrong stride and the panel drew whatever that adds up to. Set the canvas to 128x128, upload again, and "Hello" came up on the watch.
Then it got more interesting. The uploader has Second Color and Third Color checkboxes next to Add Text, because OpenEPaperLink is built for electronic shelf labels and those panels are not RGB. They are black and white plus one accent ink, and the device type preset in the tool spells it out: 350 HS BWY UC, where BWY is black, white, yellow. So I stacked three "Hello" lines, one per color, and the tool's 128x128 canvas previewed them in yellow, black and red.
Upload Image Raw, "Upload Complete" under the canvas, and the watch redrew itself: black text in the middle, red above and below, on white. The middle line matched. The yellow one came out red, because a three-ink e-paper palette is being mapped onto a color TFT and whatever that mapping decides for yellow is what you get. The colors were not the point. The point was that they changed while the watch sat there untouched, over BLE, with no reflash and no wires.
The convenient OTA route for firmware stayed dead, though. I do not think that build implements it, and my own examples certainly do not, so from this point on the Blue Pill on the breadboard is the flashing setup, every single time.
The multimeter beats the language model
To write my own display driver I needed the display pin mapping: which SoC GPIOs drive the ST7735's MOSI, CLK, DC, CS, and RST. There is no source code and no schematic for this watch, so I worked down a list of options from laziest to most painful.
The laziest was to just ask the person who already knew. I DM'd Aaron on Telegram directly. No reply. Fair enough, he owes a random stranger nothing, and he is probably not active there. That was the free shortcut gone.
Next option: reverse engineer the pin mapping out of the working firmware I already had on the chip. I dumped the running binary and threw the whole current AI toolbox at it: ChatGPT, Claude, and Antigravity. The agentic ones went the furthest, installing Ghidra themselves and driving a disassembly session to trace which GPIO registers the display code writes to. It was genuinely interesting to watch an agent navigate Ghidra, but the TC32 architecture is niche, the firmware is stripped, and none of it converged on a pin map I would bet a soldering iron on. Educational, not conclusive.
So it came down to the oldest method: continuity testing between the display connector and the SoC pins with a multimeter. Except that the display connector and the chip sit on opposite sides of the board, and a multimeter needs both probes on the same side of the universe. Two hands, two probes, one board in between.
The workaround was slow and slightly stupid, which is usually the sign that it is going to work. I soldered a single wire onto one display pin, routed it around to the chip side, and clipped a probe to it. Then I walked the other probe across the SoC pins until the meter beeped. That gave me one mapping. Then I desoldered the wire, moved it to the next display pin, and did the whole thing again. Six pins, six solder-probe-desolder cycles, on a connector where the pads are smaller than the tip of the iron.
The display side of the map was the one piece I did not have to measure. The panel's own FPC pinout is documented, thirteen pins from TP0 through GND, so I knew that pin 3 is SDA, pin 4 is SCL, pin 5 is RS, and so on. What was missing was only ever the other end: which SoC pin each of those lands on.
Turning "display pin 3 connects to chip pin 22" into a GPIO name means reading the ET24 package diagram, and that is where it got funny. I wrote out my own mapping from the datasheet, then handed ChatGPT the measurements and the diagram to check my work. It told me three of my six lines were wrong and gave me a corrected set. I did not believe it, so I pushed back. It apologized, said it had misread the top row of the package, and gave me a second corrected set, different from both my version and its own first answer.
This is what I ended up shipping:
Signal | GPIO | MCU Pin | FPC Pin |
|---|---|---|---|
MOSI | PC3 | 22 | 3 |
CLK | PC5 | 24 | 4 |
DC/RS | PC1 | 20 | 5 |
RST | PA6 | 10 | 6 |
CS | PA1 | 2 | 7 |
BL | PB3 | 13 | 11/12 |
Here is the part worth sitting with. That table is the mapping I had written down before I asked anything. Both times it corrected me, it did it in the same format: a clean table, a green check against every row, a note explaining which of my labels were wrong, and a closing line that my pinout was now fully cracked. The second correction even apologized for the first one, which made it feel like the process was converging on truth. It was not. It was converging on nothing, twice, in a confident voice.
Confidence is free to generate. Continuity is not.
None of this makes the tools useless. They were genuinely good at the parts where being wrong is cheap and recoverable: explaining SWire, comparing Dale's attempt to rbaron's, generating driver boilerplate. But a package diagram is a picture of physical reality, and the cost of being wrong about it is a display that stays black while you wonder whether you killed the panel. That is a job for a meter that beeps.
A toolchain from the SDK dungeon
The TLSR8232 does not run standard ARM. It is Telink's own TC32 core, which means Telink's own GCC fork and Telink's own BLE SDK (ble_sdk_hawk), both distributed as wget-able archives from links you hope stay alive: a toolchain tarball on an Alibaba Cloud bucket and an SDK zip on Telink's wiki. Nothing I want installed on my laptop.
rbaron had already solved this too, with a Dockerfile in m6-reveng that downloads both and sets the environment up. I took it almost as-is; the only change needed was bumping the Ubuntu base image, since the original had aged out. Reproducible builds, zero pollution on the host:
docker build -t tlsr8232-sdk .
docker run --rm -v $(pwd):/src -w /src/examples/display -it tlsr8232-sdk make
That mounts the repo root rather than the one example directory, which matters as soon as examples start sharing code: mine reference ../../lib for the display driver and the fonts, and a container that can only see examples/display cannot follow that path.
The command used to be uglier. It mounted the single example and passed SDK=/opt/ble_sdk_hawk on every invocation, because the zip is named 8232_BLE_SDK.zip but extracts straight to /opt/ble_sdk_hawk, not the /opt/8232_BLE_SDK/ble_sdk_hawk the name implies, and the Dockerfile's own SDK env var pointed at that second path, which does not exist. I typed the override for two months before it occurred to me to just fix the env var.
One more thing I only hit months later, when I rebuilt the image on a Mac instead of the Linux laptop: the TC32 toolchain is x86_64 only. On Apple Silicon the image builds perfectly and then dies with exec format error the first time it runs the compiler, so the Dockerfile pins FROM --platform=linux/amd64 and lets Rosetta deal with it.
One stupid detour worth recording: my first Makefile failed with cryptic errors because copying it through a chat window had corrupted every $< automatic variable into $$, and some recipe lines had spaces where Make demands tabs. Twenty minutes of staring at a five-line rule. The kind of bug that does not exist in tutorials.
Blink, but the only LED is spoken for
The traditional first firmware is blink. There is exactly one LED on this board, sitting under the fake heart rate sensor window on the back, and it is wired straight to the SWS pin. The same pin the programmer talks on. So it is not a free blink target, but it does something better: every time I flash the watch, the LED flickers along with the SWire traffic. An accidental progress bar, and the fastest way to tell whether the Blue Pill is actually pushing bytes or just failing quietly.
So blink went to the UART TX pin instead (PB4, exposed on an FPC debug pad), toggling every 500ms. I soldered a wire to that pad, ran it over to the breadboard, and put an LED and a 100 ohm resistor across it, which is a slightly absurd thing to do to a smartwatch, but I wanted the real thing and not a number on a multimeter. It blinked. Half a hertz of proof that the whole chain works: TC32 compiler, linker script, startup code, my own binary running on a chip I bought inside a fitness tracker.
Porting instead of writing
The display driver was the next thing, and I did not write it. Amir had already written an ST7735 driver and a full GFX layer for his own smartwatch project (phy6222_smartwatch), and when I asked him about the display he told me to just port his code. Different SoC, same panel, same problem already solved.
Porting is mostly mechanical, which makes it exactly the kind of work worth handing to an agent. I ran it through Claude Code: swap the PHY6222 HAL calls for Telink's drivers/5316 equivalents, rewrite the SPI layer against Telink's SPI peripheral, replace the pin definitions with the ones my multimeter had found, and keep the geometry and font rendering code intact. The one part I had to reason about myself was the SPI clock, since the divider maths is Telink-specific: 16MHz / (2 * (SPI_DIV + 1)), with SPI_DIV set to 3, so 2 MHz on the wire.
What came out is a driver with the Adafruit GFX API shape (gfx_fill_rect, gfx_set_cursor, gfx_print) and the Adafruit bitmap font format, running on a chip that has never heard of Arduino. Text and shapes on a 128x128 panel that had been showing a Chinese watch face a few days earlier. After that a UART helper went in for printf-style debugging, and BLE advertising, so the watch shows up in a scanner under a name I chose.
The whole thing, from first contact over SWire to my own graphics on the screen, happened across one weekend in the middle of May. The full day of ff ff ff ff was the Saturday.
Pictures on a chip with no room for pictures
Shapes and text were enough to prove the driver worked. What I actually wanted on that screen was my own logo, and that is where the hardware gets rude about it: one full-screen 128x128 frame in RGB565 is 32 KB, and the TLSR8232 has 16 kB of SRAM. There is no framebuffer, there never will be one, and every pixel has to travel from flash to the panel with nothing in between.
The ST7735 is fine with that. You set an address window once (CASET, RASET, RAMWR) and then push pixels; the controller walks the rectangle for you. The GFX layer I had ported did the opposite. Its bitmap function called draw_pixel per pixel, and draw_pixel re-sent the whole window setup every time: 11 bytes of addressing for 2 bytes of color.
Full-screen 128x128 image | Bytes over SPI | At 2 MHz |
|---|---|---|
Per-pixel | ~213 KB | ~0.85 s |
One address window, streamed | 32 KB | ~0.13 s |
Same picture, same wire, six and a half times the traffic for nothing. Fixing it was a two-line change to make the bitmap function set one window and stream into it.
Flash is the other budget. 32 KB per full-screen image on a 512 KB part is real money once you want more than one, so the images are run-length encoded: pairs of (pixel count, color), decoded straight into the address window as they are read, still no buffer. My test image is a flat banded sunset, which is close to the best case: 16384 pixels collapse into 215 runs, 860 bytes instead of 32768. The three logo assets together come to 8.3 KB where the raw pixels would have been 29 KB. On a photo it would save nothing, and the converter prints the ratio so you can see when RLE is the wrong choice.
The converter is a Python script that reads a PNG and writes a C header. I wrote the PNG decoding by hand instead of pulling in Pillow: zlib is in the standard library, undoing the five scanline filters (None, Sub, Up, Average, Paeth) is about twenty five lines, and this way the build step has nothing to install on a machine I have not set up yet. A hundred lines of decoder to avoid one pip install, which sounds like a bad trade until you are on your third machine.
The last piece was text. I wanted www.elabins.com under the wordmark, and neither font in the repo fits: the string measures 165 px in FreeMono9pt7b and 181 px in FreeSans12pt7b on a 128 px panel, and GFX fonts do not scale down. So I drew a 7 px tall face by hand for the twelve characters the URL happens to need, rendered it into a PNG, and shipped it as one more RLE image. 896 bytes for a string. Not elegant, but it is one blit instead of fifteen glyph draws, and the day I add a smaller font it deletes itself.
The green logo
The splash flashed cleanly and the logo came up green. My logo is red.
Not "slightly off" green. Deep, saturated, wrong-channel green, which is the useful kind of wrong, because a whole channel landing in the wrong place is arithmetic and arithmetic can be worked backwards. The evidence came from the oldest example in the repo, the standalone display test, which paints three nested squares with the raw values 0xF800, 0x07E0 and 0x001F and sends INVON to invert the panel. A photo of it running gave me a table:
Code sends | Named | On screen | Inversion undone |
|---|---|---|---|
| red | yellow | blue |
| green | magenta | green |
| blue | cyan | red |
So with the MADCTL color bit clear, this panel reads the top five bits as blue and the bottom five as red. It is wired B-G-R behind the glass. My driver sets that bit, which tells the controller to swap red and blue itself, so what the driver should be sending is ordinary RGB565.
It was not. The color helper packed blue, then red, then green, and the middle six bits are green in both orderings. That is the part worth sitting with: the MADCTL bit swaps red and blue, it never moves green, so no configuration of that panel can make a (B, R, G) word come out right. My logo red, (233, 62, 90), was being sent with 233 sitting in the green field, and the panel dutifully painted (90, 233, 62). Green.
Fixed the packing, fixed the color constants while I was in there (cyan and magenta had been defined as the same value, which nobody had noticed because nothing used cyan), regenerated all five image headers, rebuilt, reflashed.
Still green.
That one took longer than the color bug, and it was not a color bug at all. My Makefiles had this rule:
$(BUILD_DIR)/%.o: %.c
$(CC) -c $(CCFLAGS) -o $@ $<
Objects depend on their .c file and nothing else. The image data lives in generated headers, and I had not touched main.c, so make looked at the timestamps, decided there was nothing to do, and relinked the old pixel data into a new binary. The fix had been correct for an hour. The binary just did not contain it.
The panel was telling the truth. The build was not.
The fix is the standard one: -MMD -MP so GCC writes a .d file listing every header it read, and -include those back into the Makefile. Touching a header now recompiles what depends on it. Nine example Makefiles, three lines each.
To close it out properly I wrote a diagnostic example that paints the same three raw words twice, left half through the plain fill path, right half through the image path, so a mismatch between the halves would separate a blit bug from a mapping bug. Bars came out red, green, blue, both halves identical. That is the whole answer to a question I had spent a day reading out of blurry photos of a screen.
The splash itself is two frames: the square mark for three seconds, then the wordmark with the URL under it.
What "power the watch first" actually means
Back at the top of this post there is a failure table with a row that says: chip is not in SWire mode, fix is "power the watch first, then immediately run the script." I wrote that in May as a superstition that worked. Today the watch taught me what is actually behind it.
The setup has changed slightly since May. There is a LiPo on the board now with a switch on the positive wire, but out of habit I was still powering everything from the Blue Pill's 3V3 pin. In that configuration flashing has a reliable rhythm: the first attempt always dies, and the second always works.
Trying speed 2
Exception: Invalid request/raw response pair:
5a 00 b2 00 02 ff
02 5a 00 00 00 04 00 00 00 10 00 3f
Look at the response. The 5a that should start it is one byte late. The chip is answering, just not from where the tool expects it to be, which is what talking to something mid-boot looks like.
Then I flashed a build with battery reading and BLE in it, and the Blue Pill route stopped working entirely. Not "first try fails", but nothing at all, however I timed it. I went through the usual list, replugged, rechecked the SWS wiring and the 1K resistor, tried the old trick of touching 3V3 on right after pressing enter. Nothing.
What worked was giving up on the Blue Pill as a power source. I disconnected 3V3 completely, started the tool, and flipped the battery switch a moment later. The chip answered. After that one cold start I put the 3V3 wire back and everything returned to the ordinary two-try rhythm.
My best guess, and it is a guess: the whole SWire entry depends on catching the chip in the short window between power coming up and the firmware claiming SWS as a GPIO. With the Blue Pill feeding 3V3, that rail is already up before I type the command, so there is no window to catch. The failed first attempt is doing the work of a reset. Add a battery, and the decoupling caps plus the cell keep the chip alive through anything short of the switch, so nothing is ever truly a cold start. A switch on the positive wire is the first thing in this setup that produces a real one, at a moment I choose.
What I cannot explain is why one battery-timed boot fixed the Blue Pill path for every attempt afterwards. Something stuck. On a chip with no reset pin, "power it in the right order" is not a superstition, it is the reset line, implemented with a finger.
Where it stands
The watch runs my code now: display, graphics, fonts, color images out of flash, UART debug, and BLE advertising, with the pinout, the pixel format, and the flashing procedure written down in the repo so the next person does not need the multimeter archaeology. That is the whole win here, and it is enough of one.
Confident enough in the process now that I went back and bought two more of the same watch, and tried the OTA route again on one of them, this time knowing exactly which file I was sending. Cheap hardware stops being scary once you know you can always get back in through the test pads.
Everything else is still open. The vibration motor, heart rate sensor, button, and accelerometer GPIOs are unmapped. Reading the battery turned into its own investigation, long enough that it deserves its own post. And the bike navigation system that started all of this is still just an idea, waiting on firmware I have not written and a phone app I have not started.
What I would tell past me: the hard part was never the code. It was the stretch between clicking "buy" on an anonymous ₹249 listing and seeing SOC ID: 0x5316 come back over a single wire soldered to a test pad. Everything after that number was just engineering.
Almost none of this was invented here. rbaron worked out the SWire tooling and the Docker build, pvvx wrote the USB2Swire bridge firmware, atc1441 proved these watches were worth opening in the first place, and Amir handed me a display library and told me to port it. All I really contributed was a multimeter, a bricked watch, and a day of stubbornness.
Almost none of this was invented here. rbaron worked out the SWire tooling and the Docker build, pvvx wrote the USB2Swire bridge firmware, atc1441 proved these watches were worth opening in the first place, and Amir handed me a display library and told me to port it. All I really contributed was a multimeter, a bricked watch, and a day of stubbornness.
Everything I leaned on
My firmware, the pin maps, the Docker build, and the flashing procedure are all in one repo:
Reverse engineering and custom firmware development for the FitPro LT715 Smartwatch, which uses the Telink TLSR8232 SoC.
Both datasheets took longer to track down than they should have, and the kind of link that hosts them tends to die, so here they are directly:
The people who did the hard parts
- Aaron Christophel, @atc1441 on YouTube and on GitHub. The reason I looked at cheap watches at all, and the author of the BLE web flasher I bricked my watch with, entirely through my own carelessness.
- Raphael Baron, rbaron on GitHub, who cracked the TLSR8232 on the M6 band and published m6-reveng plus the writeup. Everything I did stands on this.
- pvvx, on GitHub, who maintains TlsrTools and TLSRPGM, including the USB2Swire firmware that turns a Blue Pill into a Telink programmer.
- Amir, @AnothersProjects on YouTube and amir1387aht on GitHub, who told me the TLSR8232 was the one to keep and wrote the ST7735 driver I ported.
- Dale Nunns, whose writeup documents the exact wall I would have hit on this package, and the OpenEPaperLink project, whose firmware was the first non-stock code to boot on my watch.
electronic equipment developer
hardware & software developer
Discussion 0 comments
Be kind. I read everything but might take a day or two to reply.