Beetle — Mini Arduino Leonardo (ATmega32U4)
Board-specific firmware knowledge. The Beetle is an ATmega32U4 with native
USB and ten pads, and almost every trap on it comes from one of those
two facts: USB means the programming interface is the sketch you are about
to replace, and ten pads means most of the Leonardo pin numbering the
compiler accepts goes nowhere on this board.
reference/board-hardware.md — the complete board reference: pad
inventory and full pin map, the sixteen I/O lines that are not brought
out, power tree, clock and PLL, memory map, fuses and the Caterina
bootloader plus a development guide (Part II: §8 toolchain, §9 USB and
the two serial ports, §10 peripheral cookbook, §11 gotchas, §12 flashing
and recovery, §13 symptom → cause → fix table).
reference/recipes.md — copy-paste code: platformio.ini, board.h,
non-blocking timing, USB-CDC that never blocks, Serial1 on the TX/RX
pads, I2C and SPI, the ADC channel map, external and pin-change
interrupts, PWM and the timer map, Servo vs tone(), HID keyboard with
its safety guards, EEPROM, power-down sleep with USB.
template/ — a project that builds clean, in three variants, plus a
scaffold script. See template/README.md.
Orientation
|
|
| MCU |
ATmega32U4, 8-bit AVR @ 16 MHz (crystal), 44-pin QFN. 26 I/O lines on the chip, 10 on the pads |
| Memory |
32 KB flash, 28,672 B usable (Caterina bootloader keeps 4 KB), 2,560 B SRAM, 1 KB EEPROM, + 832 B of USB DPRAM that is not part of SRAM |
| Board |
DFRobot: 20 × 22 × 3.8 mm; the larger clone this skill was written against: 21 × 28 mm. Castellated pads, micro-USB. No reset button, no regulator, no VIN, no 3V3 |
| Exposed pads |
RX(D0) TX(D1) SDA(D2) SCL(D3) 9 10 11 A0 A1 A2 + + (VCC) and - (GND) — DFRobot's own pad↔pin table |
| LED |
D13 = PC7, on = HIGH, no pad. DFRobot advertise a "magic light blue soft BLINK indicator" — the blue LED is D13 |
| PWM |
4 pads: D3, D11 on Timer0 (the millis() timer, ~977 Hz) · D9, D10 on Timer1 (the Servo timer, ~490 Hz) |
| ADC |
5 × 10-bit: A0 (PF7/ADC7), A1 (PF6/ADC6), A2 (PF5/ADC5), and D9 = A9 (ADC12), D10 = A10 (ADC13) |
| Interrupts |
4 external: D3→INT0, D2→INT1, D0→INT2, D1→INT3 · pin-change on D9/D10/D11 (PCINT5/6/7) |
| USB |
Native, on-chip. Serial = USB CDC (no baud rate). HID keyboard/mouse/joystick supported |
| UART |
Serial1 on TX/RX (D1/D0) — a genuinely free hardware UART, unlike on an Uno/Nano |
| I2C / SPI |
SDA=D2, SCL=D3 (SCL doubles as a PWM pad) · SPI on unlabelled test pads: MISO=D14/PB3, SCK=D15/PB1, MOSI=D16/PB2, SS=D17/PB0 |
| Power |
4.5–5 V reliable · 3–4.5 V "may work, not guaranteed" · 6 V destroys it (DFRobot). + is unregulated. Motor/relay loads need ≥ 10 µF and their own supply feed |
| Debug |
No SWD/JTAG in practice — the JTAG pins are A0–A3 and the Arduino fuse set disables them. 6-dot ICSP pad array on the back = SPI + RESET |
| Toolchain |
PlatformIO 6.1 + atmelavr 5.3.0 + Arduino AVR core, board = leonardo (there is no beetle board in PlatformIO) |
Rules that prevent the expensive mistakes
Each of these produces a failure that looks like something else.
- The board is two serial ports, not one. Running the sketch it
enumerates as VID:PID
0x2341:0x8036; in the bootloader it enumerates as
0x2341:0x0036 — a different device with a different port name.
Uploading means: open the sketch's port at 1200 baud, the chip resets, the
sketch's port vanishes, the bootloader's port appears, avrdude
speaks avr109 to that one. Consequences: never hard-code upload_port
(it breaks the handshake); a serial monitor holding the sketch port open
can block the touch; and "the port disappeared mid-upload" is the process
working, not failing.
- The upload route runs through the sketch you are replacing. The reset
is performed by the sketch's own USB interrupt handler: on seeing the
1200-baud port close it writes the magic key
0x7777 to RAMEND-1 and
calls wdt_enable(WDTO_120MS) on itself (core CDC.cpp). So a sketch that
hangs, blocks interrupts or never attached USB cannot be reset by any
host — nothing external is listening. The escape is manual and takes
one touch, not a double-tap: short ICSP pin 5 (RESET) to pin 6 (GND)
on the six dots on the back. Caterina enters the bootloader on any
external reset and holds it TIMEOUT_PERIOD = 8000 ms — eight seconds,
from its own source. Locate those dots before you need them.
while (!Serial); hangs the board forever off a PC. On a 328P that
line is a no-op; here Serial is a USB CDC port and the operator is false
until a host opens it. On battery, in a prop, in a keyboard — nothing
runs. Use if (Serial) { … } around prints, or a timed wait with an exit
(template/src/main.cpp). But note the second edge: operator bool()
contains a delay(10), so every if (Serial) costs 10 ms whichever way
it answers. One test per pass through loop() caps that loop at 100 Hz —
cache it in a bool if you check it often.
Serial.begin(115200) sets nothing. USB CDC has no baud rate; the
number is discarded. Chasing a "baud mismatch" on the USB port is chasing
a symptom that cannot exist. The real UART, where baud does matter, is
Serial1 on the TX/RX pads.
- D3 and D11 PWM come out of Timer0 — the
millis() timer. This is the
opposite of an Uno/Nano, where D3/D11 are Timer2. Touching TCCR0A/
TCCR0B to "change the PWM frequency" on those two pads makes delay(),
millis() and every baud-timed bit-bang lie, with no error anywhere. Two
of the Beetle's four PWM pads are on that timer. They also run at a
different frequency from the other two — ~977 Hz on D3/D11 (Timer0, fast
PWM) against ~490 Hz on D9/D10 (Timer1, phase-correct), both at prescaler
/64. Do not assume one pitch across the pad set.
Servo takes Timer1 — that is D9 and D10, the other two PWM pads.
One servo.attach() and analogWrite() on D9/D10 is dead. With a Servo
attached the Beetle has exactly two usable PWM pins left, both on the
timer you must not reconfigure (rule 5). tone(), by contrast, takes
Timer3, whose only output OC3A = D5 is not on a Beetle pad — so
tone() costs you no PWM here, unlike on an Uno.
analogRead() takes an analog channel index, not a pin number. On
this core analogRead(2) samples A2 (PF5), not the SDA pad; the core
subtracts 18 only from values ≥ 18 and then runs everything through
analog_pin_to_channel_PGM. Always pass the constants: A0, A1, A2,
and A9/A10 for the D9/D10 pads. Any other bare integer silently reads
the wrong pin, or a channel that reaches no pad at all.
- Nine of the pin numbers you know from an Uno reach nothing here. D4,
D5, D6, D7, D8, D12, D13, A3, A4, A5 all compile, all toggle real port
bits, and none of them are on a Beetle pad (D13 drives the onboard LED
and stops there). Library examples written for a Leonardo or Pro Micro
will happily target them. Check
include/board.h before wiring.
- A HID sketch that types at boot locks you out of your own machine.
The Beetle enumerates ~2 s after plug-in and starts typing into whatever
window has focus — including the editor you need to fix it in — every
time you plug it in. Gate every HID output behind a pin held to GND
and a multi-second grace window after reset
(
template/variants/hid/main.cpp). Recovery once it happens is a RESET
short of ICSP pin 5 to pin 6 while the host is being spammed.
Keyboard.h / Mouse.h are not in the core PlatformIO installs.
Only the low-level HID library is bundled. #include <Keyboard.h> is
a compile error until lib_deps = arduino-libraries/Keyboard is added —
the Arduino IDE bundles them, which is exactly why the same sketch
builds there and not here.
- The
+ pad is an unregulated rail and DFRobot say 6 V destroys the
board. Their numbers, verbatim: reliable at 4.5–5 V, "may work" at
3–4.5 V with reliability not guaranteed, and "6V will damage the product
by overvoltage." A regulated input would absorb that extra volt, so this
is as good as a statement that there is no regulator — there is also no
VIN and no 3V3 output. Driving + from a bench supply while USB is
plugged in ties your supply to the host's VBUS; pick one.
- A Beetle that reboots when a motor or relay switches is a power
problem, not a firmware one. DFRobot flag it explicitly: large loads
must take their VCC and GND from the supply directly rather than through
the board, with ≥ 10 µF in parallel, "in order to prevent restart or
malfunction caused by large load transient". Chasing this as a watchdog
bug, a USB bug or a stack overflow is the classic wasted evening — an
unregulated 5 V rail feeding both a motor and an MCU browns out the MCU.
- All I/O are 5 V push-pull, and there is no 3.3 V anywhere. Into a
3.3 V-only sensor that is damage, not a level mismatch. Level-shift or
power the peripheral from its own rail with grounds common.
wdt_disable() first in setup() (and clear MCUSR). Caterina
uses the watchdog itself to jump from bootloader to sketch, so a sketch
that leaves the WDT armed with a short timeout reset-loops faster than
the 8 s window can be caught — and re-uploading does not fix it, only a
caught ICSP reset does.
- A factory-fresh ATmega32U4 has JTAG enabled, and JTAG is A0–A3.
Default
JTAGEN is programmed (enabled); the Arduino Leonardo fuse
set (HIGH = 0xD8) turns it off. On a re-fused or badly-fused clone,
A0/A1/A2 — three of your ten pads — read garbage and cannot be driven.
Symptom: exactly PF4–PF7 dead, everything else fine. Fix by re-burning
the bootloader, or at runtime by writing JTD in MCUCR twice within
four cycles.
- Budget flash from ~28 KB, and expect ~4 KB gone before you write a
line. Caterina keeps 4 KB of the 32 KB, and the core enumerates USB
from
main() whether or not you mention Serial: the template's bare
Blink is 3,956 B here against 924 B for the same sketch on a Nano.
When the task is USB HID
This is what the board is bought for — macropads, foot pedals, cosplay props
— and the whole difficulty is that HID and the upload path share one USB
device.
Keyboard.begin() adds a HID interface alongside the CDC one; the board
stays a composite device and uploads keep working. It does re-enumerate,
so the host may briefly re-assign the port.
- HID carries key positions, not characters.
Keyboard.print("y") on a
host set to a non-US layout produces whatever key sits at that position.
Restrict yourself to letters and digits, or map layouts yourself.
Keyboard.releaseAll() on every exit path. A key left down by a reset
mid-press repeats into the host until it is unplugged.
- The arm-pin + grace-window shape in
template/variants/hid/main.cpp is
not a nicety; it is the difference between a board you can iterate on and
one you recover with tweezers. Keep both guards while developing, even if
the shipped firmware drops them.
Mouse.h is a separate dependency (arduino-libraries/Mouse) from
Keyboard.h. Joystick/gamepad needs a third-party descriptor library —
none is bundled.
When the task is memory
2,560 B of SRAM, of which the USB stack has already spent ~150 B before
setup() runs — the template's blink reports 149 B used with no globals of
its own. The arithmetic that matters:
- Every
Serial.print("…") literal lands in SRAM unless wrapped in F().
The full template — heartbeat, CDC report, ADC, EEPROM, free-RAM watch —
totals 5,214 B flash and 164 B RAM precisely because every literal is
F()-wrapped.
- The 832 B of USB DPRAM is separate silicon and does not come out of the
2,560 B. Endpoint buffers are not your SRAM budget; CDC ring buffers are.
String fragments the heap and there is no MPU: the heap grows up, the
stack grows down, and the collision shows up as random corruption minutes
or days later, never as an error. Use snprintf() into fixed char[].
- Instrument, don't estimate. The
free_ram() probe in template/src/main.cpp
costs ~20 B of flash. Below ~250 B free, restructure instead of adding.
Starting a new project
Do not hand-assemble one. template/ builds clean; scaffold from it:
~/.claude/skills/atmega32u4-beetle/template/variants/new-project.sh <target-dir> [--full|--minimal|--hid]
cd <target-dir> && pio run
--minimal — blink the onboard D13 LED. 3,956 B flash, 149 B RAM.
Flash this first on an unfamiliar Beetle: it touches one GPIO and nothing
else, so a failure is the toolchain, the port, the bootloader window or a
clone with no LED fitted — not your code.
--full (default) — non-blocking heartbeat, USB-CDC report guarded by
if (Serial), free-RAM watch, ADC on A0, EEPROM boot counter. 5,214 B
flash, 164 B RAM.
--hid — USB keyboard with the arm pin and boot grace window from the
section above; the script appends lib_deps = arduino-libraries/Keyboard.
6,206 B flash, 231 B RAM.
All three build as-is with platform-atmelavr 5.3.0 (verified — those are the
build's own figures). Nothing is generated and no paths are embedded, so
copying the tree by hand works identically. template/README.md maps files
to subsystems.
When the user already has a project, prefer bringing it in line with the
template's platformio.ini and include/board.h over rewriting their code.
Flashing
Over the micro-USB cable, nothing to press while the sketch is healthy:
pio run -t upload -t monitor
PlatformIO opens the sketch's CDC port at 1200 baud (use_1200bps_touch),
the chip resets into Caterina, PlatformIO waits for the bootloader's port to
appear (wait_for_upload_port) and avrdude flashes it over avr109 at
57600. Leave upload_port unset so that dance can happen (rule 1).
Recovery ladder, in order — stop at the first that works:
- Retry, watching
pio device list before and after. Two different port
names appearing is the handshake working.
- Close the serial monitor and any other program holding the port.
- Short ICSP pin 5 (RESET) to pin 6 (GND) once, then upload inside eight
seconds. No button on this board — the six dots on the back are the
reset. One touch is enough; Caterina enters the bootloader on any external
reset and waits
TIMEOUT_PERIOD = 8000 ms. This is the fix for a crashed
sketch, a WDT boot-loop (rule 14) and a HID sketch spamming the host
(rule 9). DFRobot's tip for a board whose sketch killed USB: click Upload
first, then touch the pads — "you need try several times, because the
correct time is not easy to be caught".
- Pass the bootloader port explicitly for one upload:
pio run -t upload --upload-port <the port that appeared>.
- ISP. The same six dots are a standard ICSP footprint — a second Arduino
running
11.ArduinoISP or a USBasp. This bypasses USB and the bootloader
entirely, so no firmware permanently bricks this board. Burning the
bootloader also restores the fuse set — LOW 0xFF, HIGH 0xD8, EXT
0xCB (Arduino's boards.txt values; some tools read the extended byte
back differently because its top four bits are unimplemented) — which is
the fix for a clone with JTAG wrongly enabled (rule 15) or a mis-set
clock fuse.
Reporting
Three tiers of evidence, in descending order. Pass the distinction on rather
than presenting it all in one voice.
1. Read out of the sources the toolchain itself uses — exact.
variants/leonardo/pins_arduino.h (pin↔port map, PWM↔timer map, the
analog_pin_to_channel_PGM table, the interrupt map), wiring.c (Timer0
fast-PWM and Timer1 phase-correct at /64, hence 977 Hz vs 490 Hz),
Tone.cpp (Timer3), ServoTimers.h (Timer1 only), CDC.cpp (the 1200-baud
close trigger, magic key, wdt_enable(WDTO_120MS), operator bool and its
delay(10), begin() discarding its argument), boards.txt (VID/PIDs,
fuses, avr109, 28672/2560), the bundled-library list, and
bootloaders/caterina/Caterina.c (TIMEOUT_PERIOD = 8000, bootKey = 0x7777 at 0x0800, the EXTRF/PORF/WDRF reset-cause logic — and the absence
of any double-tap detection). Plus the ATmega16U4/32U4 datasheet Atmel-7766J
for the memory map, JTAGEN defaulting to programmed (Table 28-4), BOD
levels (Table 8-1), the PLL and the package pin numbers (Figure 1-1). All
three template variants were built; every flash/RAM figure is that
build's own report.
2. From the vendor's documentation — authoritative for the board, but a
wiki page, not a schematic. DFRobot's Beetle SKU:DFR0282 documentation
supplies the pad↔pin table transcribed in reference/board-hardware.md §2.1,
the supply limits (4.5–5 V reliable, 3–4.5 V unguaranteed, 6 V destroys
it), the ≥ 10 µF load-transient warning, the six-dot ICSP array on the back
and its use as the reset, and the "magic light blue soft BLINK indicator"
that establishes the D13 LED exists.
3. Unresolved — say so when it matters.
- Clone variance. DFRobot's board is 20 × 22 × 3.8 mm; the reseller
listing for the board this skill was written against says 21 × 28 mm. Same
advertised interface counts, so the pin table almost certainly transfers,
but the outline is redrawn and the ICSP dots or LED may sit elsewhere.
reference/board-hardware.md §2.4 confirms a pad map in ten minutes.
- No schematic exists for any Beetle, DFRobot's included. "No regulator"
is inferred from "6 V will damage the product" — sound, but inference.
- TX/RX LEDs. The core drives D30/D17 with known polarity; whether a
Beetle fits those diodes is undocumented, and there is little room at
20 × 22 mm. Do not rely on them.
Nothing here was run on a physical Beetle. Everything marked
⚠︎ compile-checked only in reference/recipes.md was compiled against
this core with -Wall and no warnings, and not executed. If the board
disagrees with this skill, the board is right — that is a gap to report, not
a fault to debug around.
1---2name: atmega32u4-beetle3description: Beetle — Mini Arduino Leonardo (ATmega32U4)4---56# Beetle — Mini Arduino Leonardo (ATmega32U4)78Board-specific firmware knowledge. The Beetle is an ATmega32U4 with **native9USB** and **ten pads**, and almost every trap on it comes from one of those10two facts: USB means the programming interface is the sketch you are about11to replace, and ten pads means most of the Leonardo pin numbering the12compiler accepts goes nowhere on this board.1314- `reference/board-hardware.md` — the complete board reference: pad15 inventory and full pin map, the sixteen I/O lines that are not brought16 out, power tree, clock and PLL, memory map, fuses and the Caterina17 bootloader **plus** a development guide (Part II: §8 toolchain, §9 USB and18 the two serial ports, §10 peripheral cookbook, §11 gotchas, §12 flashing19 and recovery, §13 symptom → cause → fix table).20- `reference/recipes.md` — copy-paste code: `platformio.ini`, `board.h`,21 non-blocking timing, USB-CDC that never blocks, `Serial1` on the TX/RX22 pads, I2C and SPI, the ADC channel map, external and pin-change23 interrupts, PWM and the timer map, Servo vs `tone()`, HID keyboard with24 its safety guards, EEPROM, power-down sleep with USB.25- `template/` — a **project that builds clean**, in three variants, plus a26 scaffold script. See `template/README.md`.2728## Orientation2930| | |31|---|---|32| MCU | ATmega32U4, 8-bit AVR @ **16 MHz** (crystal), 44-pin QFN. 26 I/O lines on the chip, **10 on the pads** |33| Memory | 32 KB flash, **28,672 B usable** (Caterina bootloader keeps 4 KB), **2,560 B SRAM**, 1 KB EEPROM, + 832 B of USB DPRAM that is *not* part of SRAM |34| Board | DFRobot: 20 × 22 × 3.8 mm; the larger clone this skill was written against: 21 × 28 mm. Castellated pads, micro-USB. **No reset button, no regulator, no VIN, no 3V3** |35| Exposed pads | `RX(D0) TX(D1) SDA(D2) SCL(D3) 9 10 11 A0 A1 A2` + `+` (VCC) and `-` (GND) — DFRobot's own pad↔pin table |36| LED | **D13 = PC7**, on = **HIGH**, no pad. DFRobot advertise a "magic light blue soft BLINK indicator" — the blue LED is D13 |37| PWM | 4 pads: **D3, D11 on Timer0** (the `millis()` timer, ~977 Hz) · **D9, D10 on Timer1** (the `Servo` timer, ~490 Hz) |38| ADC | 5 × 10-bit: A0 (PF7/ADC7), A1 (PF6/ADC6), A2 (PF5/ADC5), and D9 = **A9** (ADC12), D10 = **A10** (ADC13) |39| Interrupts | **4** external: D3→INT0, D2→INT1, D0→INT2, D1→INT3 · pin-change on D9/D10/D11 (PCINT5/6/7) |40| USB | **Native**, on-chip. `Serial` = USB CDC (no baud rate). HID keyboard/mouse/joystick supported |41| UART | `Serial1` on TX/RX (D1/D0) — a **genuinely free** hardware UART, unlike on an Uno/Nano |42| I2C / SPI | SDA=D2, SCL=D3 (SCL doubles as a PWM pad) · SPI on unlabelled **test pads**: MISO=D14/PB3, SCK=D15/PB1, MOSI=D16/PB2, SS=D17/PB0 |43| Power | **4.5–5 V reliable · 3–4.5 V "may work, not guaranteed" · 6 V destroys it** (DFRobot). `+` is unregulated. Motor/relay loads need ≥ 10 µF and their own supply feed |44| Debug | No SWD/JTAG in practice — the JTAG pins are A0–A3 and the Arduino fuse set disables them. **6-dot ICSP pad array on the back** = SPI + RESET |45| Toolchain | PlatformIO 6.1 + `atmelavr` 5.3.0 + Arduino AVR core, **`board = leonardo`** (there is no `beetle` board in PlatformIO) |4647## Rules that prevent the expensive mistakes4849Each of these produces a failure that looks like something else.50511. **The board is two serial ports, not one.** Running the sketch it52 enumerates as VID:PID `0x2341:0x8036`; in the bootloader it enumerates as53 `0x2341:0x0036` — a *different device* with a *different port name*.54 Uploading means: open the sketch's port at 1200 baud, the chip resets, the55 sketch's port **vanishes**, the bootloader's port appears, `avrdude`56 speaks `avr109` to that one. Consequences: never hard-code `upload_port`57 (it breaks the handshake); a serial monitor holding the sketch port open58 can block the touch; and "the port disappeared mid-upload" is the process59 working, not failing.602. **The upload route runs through the sketch you are replacing.** The reset61 is performed by the *sketch's own* USB interrupt handler: on seeing the62 1200-baud port close it writes the magic key `0x7777` to `RAMEND-1` and63 calls `wdt_enable(WDTO_120MS)` on itself (core `CDC.cpp`). So a sketch that64 hangs, blocks interrupts or never attached USB **cannot be reset by any65 host** — nothing external is listening. The escape is manual and takes66 **one** touch, not a double-tap: **short ICSP pin 5 (RESET) to pin 6 (GND)67 on the six dots on the back.** Caterina enters the bootloader on any68 external reset and holds it `TIMEOUT_PERIOD = 8000` ms — eight seconds,69 from its own source. Locate those dots before you need them.703. **`while (!Serial);` hangs the board forever off a PC.** On a 328P that71 line is a no-op; here `Serial` is a USB CDC port and the operator is false72 until a host opens it. On battery, in a prop, in a keyboard — nothing73 runs. Use `if (Serial) { … }` around prints, or a timed wait with an exit74 (`template/src/main.cpp`). But note the second edge: **`operator bool()`75 contains a `delay(10)`**, so every `if (Serial)` costs 10 ms whichever way76 it answers. One test per pass through `loop()` caps that loop at 100 Hz —77 cache it in a `bool` if you check it often.784. **`Serial.begin(115200)` sets nothing.** USB CDC has no baud rate; the79 number is discarded. Chasing a "baud mismatch" on the USB port is chasing80 a symptom that cannot exist. The real UART, where baud *does* matter, is81 `Serial1` on the TX/RX pads.825. **D3 and D11 PWM come out of Timer0 — the `millis()` timer.** This is the83 opposite of an Uno/Nano, where D3/D11 are Timer2. Touching `TCCR0A`/84 `TCCR0B` to "change the PWM frequency" on those two pads makes `delay()`,85 `millis()` and every baud-timed bit-bang lie, with no error anywhere. Two86 of the Beetle's four PWM pads are on that timer. They also run at a87 *different frequency* from the other two — ~977 Hz on D3/D11 (Timer0, fast88 PWM) against ~490 Hz on D9/D10 (Timer1, phase-correct), both at prescaler89 /64. Do not assume one pitch across the pad set.906. **`Servo` takes Timer1 — that is D9 and D10, the other two PWM pads.**91 One `servo.attach()` and `analogWrite()` on D9/D10 is dead. With a Servo92 attached the Beetle has exactly two usable PWM pins left, both on the93 timer you must not reconfigure (rule 5). `tone()`, by contrast, takes94 Timer3, whose only output OC3A = D5 is *not* on a Beetle pad — so95 `tone()` costs you no PWM here, unlike on an Uno.967. **`analogRead()` takes an analog channel index, not a pin number.** On97 this core `analogRead(2)` samples **A2 (PF5)**, not the SDA pad; the core98 subtracts 18 only from values ≥ 18 and then runs everything through99 `analog_pin_to_channel_PGM`. Always pass the constants: `A0`, `A1`, `A2`,100 and `A9`/`A10` for the D9/D10 pads. Any other bare integer silently reads101 the wrong pin, or a channel that reaches no pad at all.1028. **Nine of the pin numbers you know from an Uno reach nothing here.** D4,103 D5, D6, D7, D8, D12, D13, A3, A4, A5 all compile, all toggle real port104 bits, and none of them are on a Beetle pad (D13 drives the onboard LED105 and stops there). Library examples written for a Leonardo or Pro Micro106 will happily target them. Check `include/board.h` before wiring.1079. **A HID sketch that types at boot locks you out of your own machine.**108 The Beetle enumerates ~2 s after plug-in and starts typing into whatever109 window has focus — including the editor you need to fix it in — every110 time you plug it in. Gate every HID output behind a pin held to GND111 **and** a multi-second grace window after reset112 (`template/variants/hid/main.cpp`). Recovery once it happens is a RESET113 short of ICSP pin 5 to pin 6 while the host is being spammed.11410. **`Keyboard.h` / `Mouse.h` are not in the core PlatformIO installs.**115 Only the low-level `HID` library is bundled. `#include <Keyboard.h>` is116 a compile error until `lib_deps = arduino-libraries/Keyboard` is added —117 the Arduino IDE bundles them, which is exactly why the same sketch118 builds there and not here.11911. **The `+` pad is an unregulated rail and DFRobot say 6 V destroys the120 board.** Their numbers, verbatim: reliable at **4.5–5 V**, "may work" at121 3–4.5 V with reliability not guaranteed, and *"6V will damage the product122 by overvoltage."* A regulated input would absorb that extra volt, so this123 is as good as a statement that there is no regulator — there is also no124 VIN and no 3V3 output. Driving `+` from a bench supply while USB is125 plugged in ties your supply to the host's VBUS; pick one.12612. **A Beetle that reboots when a motor or relay switches is a power127 problem, not a firmware one.** DFRobot flag it explicitly: large loads128 must take their VCC and GND *from the supply directly* rather than through129 the board, with **≥ 10 µF** in parallel, "in order to prevent restart or130 malfunction caused by large load transient". Chasing this as a watchdog131 bug, a USB bug or a stack overflow is the classic wasted evening — an132 unregulated 5 V rail feeding both a motor and an MCU browns out the MCU.13313. **All I/O are 5 V push-pull, and there is no 3.3 V anywhere.** Into a134 3.3 V-only sensor that is damage, not a level mismatch. Level-shift or135 power the peripheral from its own rail with grounds common.13614. **`wdt_disable()` first in `setup()`** (and clear `MCUSR`). Caterina137 uses the watchdog itself to jump from bootloader to sketch, so a sketch138 that leaves the WDT armed with a short timeout reset-loops faster than139 the 8 s window can be caught — and re-uploading does not fix it, only a140 caught ICSP reset does.14115. **A factory-fresh ATmega32U4 has JTAG enabled, and JTAG is A0–A3.**142 Default `JTAGEN` is *programmed* (enabled); the Arduino Leonardo fuse143 set (HIGH = `0xD8`) turns it off. On a re-fused or badly-fused clone,144 A0/A1/A2 — three of your ten pads — read garbage and cannot be driven.145 Symptom: exactly PF4–PF7 dead, everything else fine. Fix by re-burning146 the bootloader, or at runtime by writing `JTD` in `MCUCR` twice within147 four cycles.14816. **Budget flash from ~28 KB, and expect ~4 KB gone before you write a149 line.** Caterina keeps 4 KB of the 32 KB, and the core enumerates USB150 from `main()` whether or not you mention `Serial`: the template's bare151 Blink is **3,956 B** here against 924 B for the same sketch on a Nano.152153## When the task is USB HID154155This is what the board is bought for — macropads, foot pedals, cosplay props156— and the whole difficulty is that HID and the upload path share one USB157device.158159- `Keyboard.begin()` adds a HID interface *alongside* the CDC one; the board160 stays a composite device and uploads keep working. It does re-enumerate,161 so the host may briefly re-assign the port.162- HID carries **key positions, not characters**. `Keyboard.print("y")` on a163 host set to a non-US layout produces whatever key sits at that position.164 Restrict yourself to letters and digits, or map layouts yourself.165- `Keyboard.releaseAll()` on every exit path. A key left down by a reset166 mid-press repeats into the host until it is unplugged.167- The arm-pin + grace-window shape in `template/variants/hid/main.cpp` is168 not a nicety; it is the difference between a board you can iterate on and169 one you recover with tweezers. Keep both guards while developing, even if170 the shipped firmware drops them.171- `Mouse.h` is a separate dependency (`arduino-libraries/Mouse`) from172 `Keyboard.h`. Joystick/gamepad needs a third-party descriptor library —173 none is bundled.174175## When the task is memory1761772,560 B of SRAM, of which the USB stack has already spent ~150 B before178`setup()` runs — the template's blink reports 149 B used with no globals of179its own. The arithmetic that matters:180181- Every `Serial.print("…")` literal lands in SRAM unless wrapped in `F()`.182 The full template — heartbeat, CDC report, ADC, EEPROM, free-RAM watch —183 totals 5,214 B flash and 164 B RAM precisely because every literal is184 `F()`-wrapped.185- The 832 B of USB DPRAM is *separate* silicon and does not come out of the186 2,560 B. Endpoint buffers are not your SRAM budget; CDC ring buffers are.187- `String` fragments the heap and there is no MPU: the heap grows up, the188 stack grows down, and the collision shows up as random corruption minutes189 or days later, never as an error. Use `snprintf()` into fixed `char[]`.190- Instrument, don't estimate. The `free_ram()` probe in `template/src/main.cpp`191 costs ~20 B of flash. Below ~250 B free, restructure instead of adding.192193## Starting a new project194195Do not hand-assemble one. `template/` builds clean; scaffold from it:196197```sh198~/.claude/skills/atmega32u4-beetle/template/variants/new-project.sh <target-dir> [--full|--minimal|--hid]199cd <target-dir> && pio run200```201202- `--minimal` — blink the onboard D13 LED. **3,956 B flash, 149 B RAM.**203 Flash this first on an unfamiliar Beetle: it touches one GPIO and nothing204 else, so a failure is the toolchain, the port, the bootloader window or a205 clone with no LED fitted — not your code.206- `--full` (default) — non-blocking heartbeat, USB-CDC report guarded by207 `if (Serial)`, free-RAM watch, ADC on A0, EEPROM boot counter. **5,214 B208 flash, 164 B RAM.**209- `--hid` — USB keyboard with the arm pin and boot grace window from the210 section above; the script appends `lib_deps = arduino-libraries/Keyboard`.211 **6,206 B flash, 231 B RAM.**212213All three build as-is with platform-atmelavr 5.3.0 (verified — those are the214build's own figures). Nothing is generated and no paths are embedded, so215copying the tree by hand works identically. `template/README.md` maps files216to subsystems.217218When the user already has a project, prefer bringing it in line with the219template's `platformio.ini` and `include/board.h` over rewriting their code.220221## Flashing222223Over the micro-USB cable, nothing to press *while the sketch is healthy*:224225```sh226pio run -t upload -t monitor227```228229PlatformIO opens the sketch's CDC port at 1200 baud (`use_1200bps_touch`),230the chip resets into Caterina, PlatformIO waits for the bootloader's port to231appear (`wait_for_upload_port`) and `avrdude` flashes it over `avr109` at23257600. Leave `upload_port` unset so that dance can happen (rule 1).233234Recovery ladder, in order — stop at the first that works:2352361. **Retry**, watching `pio device list` before and after. Two different port237 names appearing is the handshake working.2382. **Close the serial monitor** and any other program holding the port.2393. **Short ICSP pin 5 (RESET) to pin 6 (GND) once, then upload inside eight240 seconds.** No button on this board — the six dots on the back are the241 reset. One touch is enough; Caterina enters the bootloader on any external242 reset and waits `TIMEOUT_PERIOD = 8000` ms. This is the fix for a crashed243 sketch, a WDT boot-loop (rule 14) and a HID sketch spamming the host244 (rule 9). DFRobot's tip for a board whose sketch killed USB: click Upload245 *first*, then touch the pads — "you need try several times, because the246 correct time is not easy to be caught".2474. **Pass the bootloader port explicitly** for one upload:248 `pio run -t upload --upload-port <the port that appeared>`.2495. **ISP.** The same six dots are a standard ICSP footprint — a second Arduino250 running `11.ArduinoISP` or a USBasp. This bypasses USB and the bootloader251 entirely, so **no firmware permanently bricks this board**. Burning the252 bootloader also restores the fuse set — LOW `0xFF`, HIGH `0xD8`, EXT253 `0xCB` (Arduino's `boards.txt` values; some tools read the extended byte254 back differently because its top four bits are unimplemented) — which is255 the fix for a clone with JTAG wrongly enabled (rule 15) or a mis-set256 clock fuse.257258## Reporting259260Three tiers of evidence, in descending order. Pass the distinction on rather261than presenting it all in one voice.262263**1. Read out of the sources the toolchain itself uses — exact.**264`variants/leonardo/pins_arduino.h` (pin↔port map, PWM↔timer map, the265`analog_pin_to_channel_PGM` table, the interrupt map), `wiring.c` (Timer0266fast-PWM and Timer1 phase-correct at /64, hence 977 Hz vs 490 Hz),267`Tone.cpp` (Timer3), `ServoTimers.h` (Timer1 only), `CDC.cpp` (the 1200-baud268close trigger, magic key, `wdt_enable(WDTO_120MS)`, `operator bool` and its269`delay(10)`, `begin()` discarding its argument), `boards.txt` (VID/PIDs,270fuses, `avr109`, 28672/2560), the bundled-library list, and271`bootloaders/caterina/Caterina.c` (`TIMEOUT_PERIOD = 8000`, `bootKey =2720x7777` at `0x0800`, the EXTRF/PORF/WDRF reset-cause logic — and the absence273of any double-tap detection). Plus the ATmega16U4/32U4 datasheet Atmel-7766J274for the memory map, `JTAGEN` defaulting to *programmed* (Table 28-4), BOD275levels (Table 8-1), the PLL and the package pin numbers (Figure 1-1). All276three template variants were **built**; every flash/RAM figure is that277build's own report.278279**2. From the vendor's documentation — authoritative for the board, but a280wiki page, not a schematic.** DFRobot's Beetle SKU:DFR0282 documentation281supplies the pad↔pin table transcribed in `reference/board-hardware.md` §2.1,282the supply limits (4.5–5 V reliable, 3–4.5 V unguaranteed, **6 V destroys283it**), the ≥ 10 µF load-transient warning, the six-dot ICSP array on the back284and its use as the reset, and the "magic light blue soft BLINK indicator"285that establishes the D13 LED exists.286287**3. Unresolved — say so when it matters.**288289- **Clone variance.** DFRobot's board is 20 × 22 × 3.8 mm; the reseller290 listing for the board this skill was written against says 21 × 28 mm. Same291 advertised interface counts, so the pin table almost certainly transfers,292 but the outline is redrawn and the ICSP dots or LED may sit elsewhere.293 `reference/board-hardware.md` §2.4 confirms a pad map in ten minutes.294- **No schematic exists** for any Beetle, DFRobot's included. "No regulator"295 is inferred from "6 V will damage the product" — sound, but inference.296- **TX/RX LEDs.** The core drives D30/D17 with known polarity; whether a297 Beetle fits those diodes is undocumented, and there is little room at298 20 × 22 mm. Do not rely on them.299300**Nothing here was run on a physical Beetle.** Everything marked301**⚠︎ compile-checked only** in `reference/recipes.md` was compiled against302this core with `-Wall` and no warnings, and not executed. If the board303disagrees with this skill, the board is right — that is a gap to report, not304a fault to debug around.