I have a mysterious passion for making maps as small as possible.
Before this project, I had already built the **Super Tiny Map**, a small map terminal based on a Raspberry Pi Zero 2 WH. Later, I remembered that I had ordered an ATOMS3R because I thought I might be able to make an even smaller map.
That thought became the **Ultra Tiny Map**.
It is a map browser running on an M5Stack ATOMS3R with a 128 × 128 pixel display. You select a direction by tilting the device, then click the screen to pan the map in that direction. A long press zooms in, and a double click zooms out.
This is not intended to be a practical navigation device. It is an experiment to find out whether viewing and interacting with a map can still work on an extremely small screen, using only device tilt and clicks on the display.
The ATOMS3R is extremely compact, but it has almost no conventional input controls.
The device provides:
- a 128 × 128 pixel LCD
- a built-in IMU
- a clickable screen button
- Wi-Fi
- an ESP32-S3 with PSRAM
There is no directional pad, keyboard, touch panel, or rotary encoder.
Displaying raster map tiles was relatively straightforward. The difficult part was deciding how a person should operate a map using only an accelerometer and one clickable surface.
A map browser normally needs at least two kinds of input:
1. A direction for panning
2. An action for zooming
I therefore divided the interaction into two stages:
1. Tilt the device to select a direction.
2. Click the screen to confirm the pan.
The current controls are:
| Input | Action |
| ----------------------- | --------------------------------------------- |
| Tilt the device | Show a yellow arrow in the selected direction |
| Click while tilted | Pan the map in that direction |
| Single click while flat | Refetch the current tiles |
| Double click while flat | Zoom out |
| Long press while flat | Zoom in |
The supported zoom range is z2 to z18.
## When I felt that it workedThe first raster tile appeared fairly quickly.
However, displaying a map image was not enough for me to consider the project complete. It became a map browser only when all four pan directions, long-press zoom-in, and double-click zoom-out worked together.
That was the moment when I felt that the Ultra Tiny Map Pager had become real.
The central question was no longer:
> Can a Raspberry Pi Zero 2 WH display a raster map tile?
It became:
> Can a person actually browse a map on a device this small?
## Turning tilt into a directional inputThe ATOMS3R accelerometer reports continuous values, but a map browser needs discrete directions: left, right, up, down, or neutral.
The first implementation used a simple threshold. When the acceleration value passed the threshold, the firmware selected a direction.
Technically, this worked. In practice, the yellow direction arrow visibly twitched between states.
Small fluctuations in the accelerometer readings caused the selected direction to change repeatedly near the threshold. This was not merely a visual problem. Because the arrow would not remain stable, I could not accurately select the intended pan direction before clicking.
I stabilized the input using two techniques:
- an exponential moving average to smooth the accelerometer signal
- hysteresis, using separate thresholds for entering and leaving a direction
The relevant values are:
static const float TILT_ON = 0.28f;
static const float TILT_OFF = 0.14f;
static const float TILT_EMA = 0.30f;A direction is selected only after the smoothed signal exceeds `TILT_ON`, approximately 16 degrees of tilt.
Once selected, the direction remains active until the signal falls below `TILT_OFF`, approximately 8 degrees.
Using different entry and exit thresholds prevents the state from rapidly switching near a single boundary.
Conceptually, the logic is:
if (current_direction_is_still_valid &&
magnitude > TILT_OFF) {
keep_current_direction();
} else if (magnitude > TILT_ON) {
select_new_direction();
} else {
return_to_neutral();
}The yellow arrow now remains stable enough for the user to aim the device and click with confidence.
## Why one tile per click felt wrongMy first implementation moved the map by one full tile for each click.
Since each tile was rendered as 128 × 128 pixels, one action replaced almost the entire visible map.
The implementation was simple, but it did not match the human mental model of panning a map. People expect a map to move gradually while nearby roads, labels, and landmarks remain visually connected between frames.
With full-tile movement, the view changed too abruptly. It was difficult to follow a feature from its old position to its new position.
I changed the map state to support quarter-tile offsets.
// World top-left:
// (tx + fi / 4, ty + fj / 4) at zoom tz
static int tz = 10;
static int tx = 909;
static int ty = 403;
static int fi = 0;
static int fj = 0;`tx` and `ty` store the integer tile coordinate.
`fi` and `fj` store fractional positions from 0 to 3.
Each pan action moves the view by one quarter of a tile:
32 pixels = 128 pixels / 4This produced an interaction that was much closer to the feeling of moving a map a little at a time.
When a fractional coordinate crosses a tile boundary, the integer tile coordinate is updated and the next group of tiles is loaded.
## Raster tile renderingThe tile server provides standard 256 × 256 pixel PNG raster tiles.
The ATOMS3R display is 128 × 128 pixels, so each source tile is decoded at half scale:
dst.drawPng(
buffer,
size,
0,
0,
0,
0,
0,
0,
0.5f,
0.5f
);The renderer uses three levels of image storage:
| Buffer | Size | Purpose |
| ---------- | ------------- | -------------------------------- |
| Tile cache | 9 × 128 × 128 | Keep recently decoded map tiles |
| Mosaic | 256 × 256 | Combine a 2 × 2 tile area |
| Frame | 128 × 128 | Compose the final visible screen |
The nine-tile cache uses an LRU replacement policy.
The renderer first builds a 2 × 2 mosaic from four cached or downloaded tiles. The final 128 × 128 view is then copied from the mosaic using the quarter-tile offsets.
mosaic.pushSprite(
&frame,
-fi * 32,
-fj * 32
);This separation is important.
A quarter-tile pan does not always require a new network request. As long as the requested view remains inside the existing mosaic, the firmware only needs to copy a different 128 × 128 region into the frame buffer.
A new tile is fetched only after the view crosses a tile boundary.
## PerformanceThe perceived response time depends on whether the required raster tiles are already cached.
In my environment:
| Situation | Approximate response |
| ----------------------------- | -------------------- |
| Required tiles already cached | Milliseconds |
| A new tile must be downloaded | About one second |
The uncached time depends on Wi-Fi conditions, tile size, server response time, and PNG decoding time, so one second should be understood as an approximate result rather than a strict benchmark.
Cached panning feels immediate because it mainly consists of copying a different region from the existing mosaic.
## What can be seen on 128 × 128 pixels?The display is extremely small, but the map retains more geographic information than I initially expected.
At wider zoom levels, I can recognize terrain and the overall shape of the region.
Labels such as the following remain readable:
- Tokyo
- Setagaya
- Kawasaki
Major roads are visible, and at some zoom levels the ring-road structure of Tokyo appears clearly.
At closer zoom levels, recognizable places begin to emerge. The Imperial Palace and Tokyo Station can be identified.
At the highest zoom levels, even individual building footprints can be distinguished.
The primary limitation is not that the map lacks detail.
The limitation is that the visible geographic area is extremely narrow.
A normal map display provides spatial context by showing the relationship between many features at once. On a 128 × 128 display, the user must pan repeatedly to understand the surrounding area.
This creates a clear trade-off:
> Geographic features remain identifiable, but geographic context must be reconstructed through interaction.
That trade-off is part of the experiment.
## Wi-Fi and tile fetchingThe firmware scans for configured Wi-Fi access points before trying to connect.
This avoids waiting through a full connection timeout for an access point that is not currently nearby.
Credentials are stored in a local header that is excluded from Git:
cp src/wifi_credentials.h.example \
src/wifi_credentials.hThe default tile URL in the source code points to my own server:
http://tile.yuiseki.net/styles/osm-bright/256/{z}/{x}/{y}.pngAnyone building the project should replace this with an appropriate tile server and follow the provider's usage policy.
The current version is not an offline map terminal. It requires network access for uncached tiles.
## Building the firmwareThe project uses PlatformIO and the Arduino framework.
pio run
pio run -t uploadPSRAM is enabled using the `qio_opi` memory configuration:
board_build.arduino.memory_type = qio_opi
build_flags =
-DBOARD_HAS_PSRAMOne practical warning: when several ESP32 devices are connected, `/dev/ttyACM0` and similar device numbers may change after reconnecting them.
I recommend fixing the upload target using its stable serial-number path under:
/dev/serial/by-id/Otherwise, it is surprisingly easy to flash the wrong ESP32 board.
## Screen captureThe ATOMS3R has no keyboard, so I added a serial command that exports the frame buffer using the ESPREC1 screen-capture format.
esprec shotThis made it possible to obtain clean screenshots without photographing the physical LCD.
However, opening the USB serial port introduced another problem.
On the ESP32-S3 USB-Serial-JTAG interface, a particular DTR and RTS transition can be interpreted as a reset request. The normal `esprec` command changed these control lines while opening the port, causing the ATOMS3R to restart during every capture.
The result was that every screenshot showed only the boot screen.
I created a wrapper that opens the serial port without modifying DTR or RTS:
python tools/esprec-noreset.py snapshot \
--port /dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_... \
-o shot.pngI also submitted the underlying fix upstream:
tig/esprec#9
fix: leave DTR/RTS alone on POSIX hostsThis screen-capture work is not the main subject of the Ultra Tiny Map Pager, but it became an unexpected technical side project.
## Optional extensionsThe core project requires only the ATOMS3R.
I later added two optional extensions:
- a Grove 5-Way Tactile switch
- an ATOM cat-ear LED board
The 5-way switch provides a conventional alternative to the tilt gestures.
The cat ears visualize the same direction selected by the IMU:
- tilt left: the left ear lights
- tilt right: the right ear lights
- tilt up: the tips of both ears light
- tilt down: the bases of both ears light
Both the screen arrow and the LEDs use the same direction result, so they cannot disagree.
These additions are useful for demonstrations, but they are not the central idea of the project.
The Ultra Tiny Map Pager is fundamentally the small ATOMS3R itself: tilt to choose a direction, and click to move the map.
## LimitationsThis project has several deliberate and practical limitations.
- It is not intended for turn-by-turn navigation.
- It does not currently use GNSS.
- It is not an offline or off-grid map.
- The visible area is extremely limited.
- Uncached rendering depends on Wi-Fi and the tile server.
- Tilt gestures require a short period of learning.
- The approximate one-second uncached loading time varies by environment.
- A raster tile service must be used responsibly and with correct attribution.
These limitations do not invalidate the experiment. They define what the experiment is testing.
## What I learnedThe most difficult part was not displaying the map.
It was designing an interaction that matched the physical constraints of the device and the user's expectations.
Two changes were especially important.
First, a noisy continuous sensor could not be treated as a clean four-way switch. EMA smoothing and hysteresis were necessary to make the selected direction stable enough to use.
Second, technically valid movement was not necessarily perceptually valid movement. Moving by one full tile worked in the code, but conflicted with the human expectation that a map should pan gradually. Quarter-tile movement made the interaction much easier to understand.
The experiment also showed that a map can remain recognizable at an unexpectedly small scale.
Terrain, administrative labels, roads, landmarks, and individual buildings can all survive on a 128 × 128 display.
What disappears first is not necessarily the geographic information itself, but the amount of surrounding context visible at one time.
## ConclusionUltra Tiny Map Pager began with a simple and somewhat irrational motivation:
> I wanted to make a map as small as possible.
The result is a 128 × 128 map browser controlled using only tilt and clicks.
It is not a replacement for a smartphone map, and it is not trying to be one.
It is a small experiment in how far a map interface can be compressed while still remaining recognizable and operable.
When people see it, I hope their first reaction is:
> It is tiny, and it is interesting.
And when they hear that I may exhibit it at an event, I hope their next reaction is:
> I want to try it.
## Source codeThe source code, setup instructions, and technical notes are available here:
https://github.com/yuiseki/m5-atoms3r-ultra-tiny-map
## Hardware- M5Stack ATOMS3R
- USB cable
- Wi-Fi access point
Optional:
- Grove 5-Way Tactile switch
- nekomimi LED for the Atom series
- PlatformIO
- Arduino framework
- M5Unified
- M5GFX
- ESP32 Wi-Fi and HTTPClient
- tig/esprec for screen capture
- OpenStreetMap contributors
- M5Stack
- M5Unified / M5GFX
- tig/esprec
- washishi/nekomimi_led_Atom
- Adafruit NeoPixel
WTFPL





Comments