A Disposable Vape as a Web Server
I disassembled a disposable vape, found an ARM Cortex-M0+ microcontroller with 24 KiB of flash and 3 KiB of RAM inside, and turned it into a working web server — using SLIP over semihosting and the uIP TCP/IP stack.
Note: the original version of this article is actually served from a web server running on a disposable vape. To see it live, visit http://ewaste.fka.wtf/.
Background
For years I have been collecting disposable vapes from friends and family, originally extracting their batteries for "future projects." Recently these devices have become more sophisticated — they now come with USB-C connectors and rechargeable batteries, which creates interesting classification challenges.
While disassembling one of them, I discovered a chip marked "PUYA" instead of the typical ASIC designs I had seen before. This manufacturer is known for flash chips and ARM Cortex-M0+ microcontrollers, as highlighted in Jay Carlson's article "the cheapest flash microcontroller you can buy."
Over roughly a year I accumulated several disposable vapes containing PY32 chips from one particular manufacturer. I especially appreciated that the debug contact pads were clearly labeled on the board.
What We Are Working With
The chip marked "PUYA C642F15" was initially suspected to be a PY32F002A. After exploring it with pyOCD and measuring the flash (24 KiB) and RAM (3 KiB), I confirmed it was actually a PY32F002B — a somewhat different chip.
Microcontroller specifications:
- ARM Cortex-M0+, 24 MHz
- 24 KiB flash memory
- 3 KiB static RAM
- Various unused peripherals
The specs look modest. But I saw an opportunity: an amazingly fast web server.
Going Online
The web server idea grew out of exploring semihosting — a mechanism in ARM microcontrollers that uses register values and breakpoint instructions to make system calls. An attached debugger intercepts these and executes the requested actions on the host machine.
Before Wi-Fi and Ethernet, internet access required a modem. Most USB serial devices emulate a 56k modem operating at 57,600 baud. Early modems used SLIP (Serial Line Internet Protocol) for transmitting IP data.
Linux (and macOS with some configuration) supports SLIP natively. The slattach utility can turn any /dev/tty* device into a network interface that sends and receives IP packets, as long as the data is properly formatted.
The setup script looks like this:
pyocd gdb -S -O semihost_console_type=telnet -T $(PORT) $(PYOCDFLAGS) &
socat PTY,link=$(TTY),raw,echo=0 TCP:localhost:$(PORT),nodelay &
sudo slattach -L -p slip -s 115200 $(TTY) &
sudo ip addr add 192.168.190.1 peer 192.168.190.2/24 dev sl0
sudo ip link set mtu 1500 up dev sl0
For TCP/IP on the device side, I chose uIP — a small, RTOS-free TCP/IP stack that is easy to port and comes with a minimal HTTP server example.
After porting the SLIP code to use semihosting, initial attempts worked inconsistently. It turned out that uIP, designed for 8- and 16-bit machines, assumed data alignment that is not required on ARM. The uip_chksum function assumed 16-bit alignment, causing exceptions on odd addresses. I fixed the alignment issues, adjusted the filesystem structure for portability, and got my first taste of Perl while optimizing data handling.
Remarkably Fast
Initial performance was dismal: roughly 1.5-second ping times with 50% packet loss, and simple pages took over 20 seconds to load. The problem: reading one character at a time created enormous overhead.
Previous semihosting benchmarks had achieved around 20 KiB/s. The issue was that uIP's SLIP implementation serialized data byte-by-byte, which was fine for memory-constrained 8-bit devices but terrible here. With 3 KiB of RAM available, I added ring buffering for host-side read caching and passed that to the SLIP polling functions. Write operations were grouped to allow proper escape encoding.
Results after optimization:
- Ping: ~20 ms with zero packet loss (down from 1.5 s with 50% loss)
- Full page load: ~160 ms (down from 20+ seconds)
Memory utilization:
Memory Region Used Size Region Size % Used
FLASH: 5116 B 24 KiB 20.82%
RAM: 1380 B 3 KiB 44.92%
(For the blog version, all available RAM is used.)
Less than 20% of flash storage is consumed — not enough to ship a React release, but plenty to host entire blog posts. And this is not just static file serving: any server-side C code you write runs directly on the chip.
For fun, I added JSON API endpoints that return the number of requests to the main page and the microcontroller's unique hardware ID.
Resources
Footnote 1: During research I found a project that correctly identifies these chips as PY32C642, which is virtually identical to the 002B.
Footnote 2: Newer modems used PPP (Point-to-Point Protocol) rather than SLIP.