A compact multi-mode counter tool written in embedded C for the ATmega328P (e.g. Arduino Uno, 16 MHz).
It turns only three pieces of hardware into a complete, gesture-driven counting tool:
- 1 push button — all control flows through it
- 1 built-in LED — visual feedback for the button gestures
- 1 LCD 1602A (16x2) — shows the menu and the counter state
A UART driver is included and used for debugging (9600 baud).
- Multiple counting modes: CounterUp, CounterDown, and ManualCounter
- 4 distinct gestures from a single button:
- Click (single press)
- Double-click
- Long press
- Press-and-hold
- A menu mode to pick and switch between the counting modes
- CounterSetter: an intermediate mode to set the initial value before counting down
- Non-blocking drivers: everything is pumped from a single
while(true)main loop - Built-in LED gives feedback for every gesture
- UART logging of button gestures and LCD command traffic (useful for debugging)
| Part | Pin attached |
|---|---|
| Button (to GND, internal pull-up) | PB0 |
| Built-in LED | PB5 |
| LCD data D4–D7 | PD4–PD7 (4-bit mode) |
| LCD RS | PD3 |
| LCD EN | PD2 |
| UART TX / RX | PD1 / PD0 |
The button driver recognizes each press/release pattern and emits a gesture result that the counter logic reacts to:
| Gesture | Behavior |
|---|---|
| Click | Press and release within ~300 ms |
| Double-click | Two clicks within ~300 ms |
| Long press | Hold 300–1000 ms to release |
| Hold | Keep holding past ~1000 ms (fires repeatedly) |
Entry point after reset. Select a counting mode, then confirm with a hold.
| Gesture | Action |
|---|---|
| Click | Select next mode (Up / Down / Tick) |
| Double-click | Select previous mode |
| Hold | Enter the selected mode |
Counts automatically in steps of 1 while running.
| Gesture | Action |
|---|---|
| Click | Start / pause counting |
| Long press | Reset count to 0 |
| Hold | Back to menu |
Counts automatically in steps of 1 while running, starting from the initial value. When the counter hits 0 it reloads the initial value automatically.
| Gesture | Action |
|---|---|
| Click | Start / pause counting |
| Long press | Reset to the initial value |
| Hold | Back to menu |
Intermediate mode that runs before CounterDown. It lets you build up the initial
value by adding fixed steps: 1000, 100, 10, 5, 1.
| Gesture | Action |
|---|---|
| Click | Move the selection over the step options |
| Long press | Add the selected step to the initial value |
| Double-click | Zero the initial value |
| Hold | Confirm and start CounterDown |
Lets you count manually, one step per gesture.
| Gesture | Action |
|---|---|
| Click | Increment by 1 |
| Double-click | Increment by 5 |
| Long press | Reset to 0 |
| Hold | Back to menu |
┌──────────┐
│ MENU │ Up / Down / Tick
└────┬─────┘
Hold ─────────┤ Hold ───────────── Hold ──────────────────────┐
▼ │
┌──────────┐ Hold (confirm) ┌──────────┐ │
┌────────────▶│ CounterUp│──────▶ CounterDown ◀───────│CounterSet│ │
│Hold └──────────┘ Hold(back) │ ter │ │
│ └──────────┘ │
└──────────────────────────────────────────────────────────────────┤
Hold ──────────────────────────────────────────────────────────────┘
Every mode links back to the Menu with a Hold.
├── main.c # init + single main loop; wires everything together
├── drivers/
│ ├── button.c # gesture state machine (debounce, click, double, long, hold)
│ ├── counter.c # modes, UI rendering, gesture dispatch
│ ├── lcd.c # async LCD1602A driver (4-bit) with command queue
│ ├── timer.c # Timer0 overflow -> 1 ms tick source
│ └── uart.c # USART debug output
├── include/
│ ├── button.h
│ ├── counter.h
│ ├── gpio.h # dio_* register macros
│ ├── lcd.h
│ ├── timer.h
│ └── uart.h
├── meson.build # build + flash (avrdude) targets
└── meson-avr.ini # avr-gcc cross-file for Meson
A gesture engine from one button.
button.c is a non-blocking state machine (BS_IDLE → BS_PRESSED → BS_WAIT_SECOND → BS_PRESSED_SECOND → BS_HOLD) with software debouncing. From a single GPIO input it
derives four distinct gestures — single click, double-click, long press, and
press-and-hold — without ever blocking the main loop.
The UI macro: self-cadenced LCD refresh.
In counter.c, each mode screen redraw is wrapped in a for loop that only runs
when the display is due, spacing redraws ~800 ms apart and catching up instantly if
the loop ever falls behind. The body is called at most once per frame and the loop
self-terminates, so no busy-waiting is needed for the UI:
static uint32_t timer = 0;
#define UI \
uint32_t now = get_ms(); \
for (; timer < now; timer = now + 800)Async LCD driver.
lcd.c decouples the application from LCD timing: every command/character is pushed
into a small ring buffer and only sent to the display when lcd_update() is pumped
from the main loop. Init sequences and redraws get their 1 ms gaps from the shared
millisecond timer instead of long blocking delays.
Hardware millisecond clock.
timer.c reloads TCNT0 in the Timer0 overflow ISR to generate a 1 ms tick, plus a
get_delta_ms() counter and a small timer_target/timer_check API. All timing in
the project (debouncing, gesture windows, UI cadence) is built on this one source.
Token-pasting GPIO macros.
gpio.h uses the preprocessor's ## operator so you can write
dio_high(PORTB, LED) and dio_init_in(B, BTN) — register names are composed at
compile time, keeping the call sites readable.
LED as a feedback channel.
btn_feedback() flashes the LED on a click/double-click and keeps it lit while a
press is inside the "hold zone," so the user gets a live cue of how far a hold is
from triggering.
UART for debugging.
Every byte written to the LCD is echoed to the UART in hex (%02x\r\n), and a
commented-out block in main.c logs the detected gesture — handy for tracing
button behavior on the bench. The firmware greets with !!Super Counter!! at 9600
baud.
The project uses Meson with an AVR cross-file:
# configure (once)
meson setup build --cross meson-avr.ini
# build the firmware + .hex
ninja -C build
# flash via avrdude (Arduino bootloader, /dev/ttyUSB0)
ninja -C build flashmeson.build targets ATmega328P at 16 MHz (F_CPU), builds with -Os
(buildtype=minsize), and generates dio.hex with avr-objcopy. The flash
target calls avrdude through doas in Arduino mode at 115200.
The current control model is fully button-driven. The next step is a UART command interface: the firmware will accept commands from a PC over the same UART channel to:
- switch the counter mode (menu / up / down / setter / manual),
- start, pause, or reset a mode's state,
- read the current value, mode, and running state.
This opens the door to PC-driven automation and remote control without changing the hardware or the mode logic — the gesture driver remains for standalone use.
-
The menu "previous" selection uses
% 3on a decremented value; C truncates toward zero, so double-clicking from the first option temporarily lands on an invalid index (fixed by the next click). -
Everything is driven from one button, so some gestures overlap between modes by design — the mode tables above are the contract.
