atmega328p-nano · git:20260821.cc0e8f7 · 2026-08-21 · sha256 60c88a4c5746098b

atmega328p-nano git:20260821.cc0e8f7A

Immutable. This exact content is served forever at /api/v1/blob/60c88a4c5746098b.

---
name: atmega328p-nano
description: Firmware development for the Arduino Nano (A000005, ATmega328P @ 16 MHz, 5 V) — its 30 KB usable flash, 2 KB SRAM, 1 KB EEPROM, 8-channel 10-bit ADC, six 8-bit PWM pins across three timers, UART shared with the USB-serial chip, I2C on A4/A5, SPI on D10–D13, sleep modes, and the Arduino AVR core + PlatformIO setup around them. Use when working on this board or any ATmega328P Nano clone (FTDI or CH340 USB): project setup, platformio.ini, old-vs-new bootloader uploads, pin mapping, A6/A7 restrictions, timer/Servo/tone conflicts, the 2 KB SRAM budget, analog references, powering from VIN, ICSP recovery, or debugging why something on the board does not work.
---

# Arduino Nano (ATmega328P)

Board-specific firmware knowledge. The Nano is simple enough that every
failure mode is silent — the sketch compiles, uploads, runs, and does
something subtly wrong — so read the reference file rather than guessing.

- `reference/board-hardware.md` — the complete board reference: corrected pin
  map (the official manual's own tables contain errors), power tree, clock,
  memory map, connector/LED inventory **plus** a development guide (Part II:
  §8 toolchain, §10 peripheral cookbook, §11 gotchas, §12 flashing, §13
  symptom → cause → fix table).
- `reference/recipes.md` — copy-paste code: `platformio.ini`, `board.h`,
  non-blocking timing, serial + free RAM, EEPROM with update semantics, ADC
  and references, interrupts, the timer/PWM map, Servo vs `tone()`,
  power-down sleep, PROGMEM tables.
- `template/` — a **project that builds clean**, in two variants, plus a
  scaffold script. See `template/README.md`.

## Orientation

| | |
|---|---|
| MCU | ATmega328P, 8-bit AVR @ **16 MHz** (crystal — fixed, no PLL), Harvard architecture |
| Memory | 32 KB flash (30,720 B usable — 2 KB bootloader reserve), **2 KB SRAM**, 1 KB EEPROM. No DMA, no MPU |
| LED / button | **L** = D13 (`LED_BUILTIN`), on = **HIGH** · **RESET** button, **active-low** |
| ADC | 8 × 10-bit, 0–5 V, ~100 µs per `analogRead()`; A6/A7 are **analog-input only** |
| PWM | 6 × 8-bit: D3, D11 (~490 Hz, Timer2), D9, D10 (~490 Hz, Timer1), D5, D6 (~980 Hz, Timer0) |
| UART | D0/D1 — **shared with the USB-serial chip**, not free pins |
| I2C / SPI | A4=SDA, A5=SCL · D10=SS, D11=MOSI, D12=MISO, D13=SCK (+ICSP header) |
| Power | 5 V logic. USB Mini-B, VIN 7–12 V via LDO, +5V pin (bypasses regulator). 3V3 pin **50 mA max** |
| Debug | No SWD/JTAG — only the 6-pin **ICSP** header (pin 1 at the outer edge; some diagrams rotate it 180°) |
| Toolchain | PlatformIO 6.1 + `atmelavr` 5.3.0 + Arduino AVR core, `board = nanoatmega328new` (Optiboot) / `nanoatmega328` (old bootloader) |

## Rules that prevent the expensive mistakes

Each of these produces a failure that looks like something else.

1. **`upload_speed = 57600` on old-bootloader boards.** Genuine Nanos sold
   before Jan 2018 and most clones run the old ATmegaBOOT at 57600 baud, not
   Optiboot's 115200. The wrong choice fails uploads with `not in sync` /
   `stk500_recv` — which reads like a driver or cable problem, not a
   firmware-era problem. In the Arduino IDE the equivalent is
   `Tools → Processor → ATmega328P (Old Bootloader)`.
2. **The 2 KB SRAM is the wall, not the 30 KB flash.** Every `Serial.print`
   string literal, `const` array and `String` copy lands in SRAM by default.
   The failure is a heap/stack collision that corrupts variables or crashes
   after minutes-to-days — nothing prints "out of memory". `F()` on every
   literal, `PROGMEM` for tables, static buffers instead of `String`, and
   watch free RAM (recipe 4).
3. **`digitalWrite(A6)` / `digitalRead(A7)` compile and silently misbehave.**
   A6/A7 feed the ADC mux only — there is no port register behind them. They
   are analog-input-only; any digital use needs the signal moved to a real
   pin. A0–A5 are full digital pins (aliases 14–19).
4. **D0/D1 are wired to the FTDI/CH340 chip, not to free GPIO.** Using them
   while `Serial` is open produces garbage in both directions, and anything
   external wired to them blocks uploads. The TX/RX LEDs show USB-serial
   traffic only — they say nothing about D1 driven manually.
5. **Never write `TCCR0A`/`TCCR0B`.** Timer0 is `millis()`, `micros()` and
   `delay()` **and** the ~980 Hz PWM on D5/D6. "Just changing the PWM
   frequency" on those pins makes `delay()` lie and baud-rate-timed software
   (SoftwareSerial, DHT, NeoPixel) glitch, with no error anywhere.
6. **Every PWM pin is rented from a library.** `Servo` takes Timer1 — D9/D10
   `analogWrite()` dead while attached. `tone()` takes Timer2 — D3/D11 dead
   while playing. Servo + tone coexist (different timers) but neither
   survives touching Timer0. Also: PlatformIO's AVR core bundles only
   EEPROM/HID/SoftwareSerial/SPI/Wire — `#include <Servo.h>` is a compile
   error until `lib_deps = arduino-libraries/Servo` is added (the Arduino
   IDE bundles it).
7. **D13 is the LED and the SPI clock.** The LED + series resistor load the
   line, which matters when D13 is an input or SCK: a flaky SPI bus that
   blinks L with the clock is this, not your wiring.
8. **All GPIO are 5 V push-pull.** Into a 3.3 V-only peripheral that is
   damage, not a logic level. The 3V3 pin is an output limited to 50 mA —
   powering a sensor is fine, a radio module is not.
9. **`analogReference(EXTERNAL)` before any voltage touches AREF**, and AREF
   ≤ 5 V always. Driving AREF while an internal reference is selected burns
   the pin.
10. **The +5V pin bypasses the regulator.** Feeding it from anything above
    5.0 V — or from a "5 V" supply that drifts — goes straight into the MCU.
    Unregulated input belongs on VIN, which needs ≥ 7 V to clear the LDO
    dropout (7–12 V recommended).
11. **`wdt_disable()` first in `setup()`** (and clear `MCUSR`). A sketch that
    leaves the watchdog enabled boot-loops boards with the old ATmegaBOOT
    bootloader — it does not clear the WDT on entry — and the loop survives
    re-uploading; only ICSP recovery or Optiboot fixes it.
12. **Unconnected inputs are not zero — they float.** Pull-ups (20–50 kΩ)
    are off by default: `INPUT_PULLUP` or an external resistor, or the pin
    reads random noise.
13. **USB + VIN together = auto power selection, highest voltage wins.** A
    9 V VIN while debugging over USB back-feeds the USB port. Power
    actuators from VIN, the debug session from USB, grounds common — or
    accept VIN < 5 V.

## When the task is memory

2,048 B covers globals, heap and stack together, and there is no MPU: the
heap grows up, the stack grows down, and their collision is whatever random
corruption results. The arithmetic that matters:

- A `Serial.println("temperature sensor ready")` costs 26 B of SRAM forever;
   in `F()` it costs 0. A 256-entry `uint16_t` table is an eighth of the
   chip — `PROGMEM` it (recipe 11).
- The Arduino core + a Blink already use ~600 B flash and single-digit bytes
   of RAM; `Serial` adds ~1.5 KB flash. The template's full variant —
   heartbeat + serial report + ADC + EEPROM counter — totals 2,874 B flash,
   201 B RAM. Budget accordingly: a 1 KB receive buffer is half the machine.
- `String` fragments the small heap; static `char[]` buffers do not. On this
   part, prefer `snprintf()` into a fixed buffer.
- Instrument, don't estimate: the `free_ram()` probe in recipe 4 costs 20 B
   flash. Below ~200 B free, stop adding features and restructure.

## When the task is timing

The three timers are the whole scheduling story, and all of them are spoken
for:

| Timer | Owned by | PWM pins | Free to reconfigure? |
|---|---|---|---|
| Timer0 | `millis()`/`micros()`/`delay()` | D5, D6 (~980 Hz) | **never** |
| Timer1 | `Servo` library | D9, D10 (~490 Hz) | if Servo is not used (recipe 8 has the fast-PWM setup) |
| Timer2 | `tone()` | D3, D11 (~490 Hz) | if tone is not used |

Consequences worth internalizing: an ISR or `noInterrupts()` section longer
than ~100 µs starts eating UART characters and skewing `millis()`-based
intervals; `delayMicroseconds()` is cycle-counted and stays exact; and
anything bit-banged (DHT, one-wire, SoftwareSerial) degrades when interrupts
are blocked — including by long `Servo` pulses. The non-blocking
`millis()` pattern (recipe 3) is the default shape of every loop on this
board.

## Starting a new project

Do not hand-assemble one. `template/` builds clean; scaffold from it:

```sh
~/.claude/skills/atmega328p-nano/template/variants/new-project.sh <target-dir> [--full|--minimal]
cd <target-dir> && pio run
```

- `--minimal` — the classic Blink on the "L" LED, hardware-verified. 924 B
  flash, 9 B RAM. **Flash this first on an unfamiliar board**: if it does
  not blink, the problem is the toolchain, the COM port or the bootloader
  era (rule 1), not the code.
- `--full` (default) — non-blocking heartbeat, serial report with `F()`
  strings, free-RAM watch, ADC sample on A0, EEPROM boot counter. 2,874 B
  flash, 201 B RAM.

Both build as-is with platform-atmelavr 5.3.0 (verified). 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 Mini-B USB cable, nothing to press — the DTR auto-reset opens the
bootloader (Optiboot listens ~1 s at 115200, old ATmegaBOOT ~1–2 s at
57600):

```sh
pio run -t upload -t monitor
```

The board enumerates as a COM port: FTDI VCP on genuine boards, CH340 on
most clones (needs the WCH driver — its absence is why a clone "does not
appear on any port").

Upload failure checklist, in order: wrong bootloader era (rule 1), wrong
COM port / missing CH340 driver, something wired to D0/D1 (disconnect), and
only then assume the sketch. A sketch that crashes instantly or blocks
interrupts can be replaced by timing a RESET press during upload.

The recovery route is the 6-pin ICSP header with a second Arduino running
`11.ArduinoISP`, a USBasp, or `avrdude` — it bypasses the bootloader
entirely, so **no bad firmware permanently bricks this board**. `Burn
Bootloader` over ICSP also restores the fuse set (LOW=0xFF, HIGH=0xDA,
EXT=0x05) — the fix for clones with mis-set clock fuses and for WDT
boot-loops. ICSP is also how to upload bootloader-less and reclaim the
reserved 2 KB.

## Reporting

Say what is verified on hardware and what is derived. In this skill the
**Blink** (the template's `--minimal` variant) ran on an Arduino Nano
A000005 v3.x with the Optiboot bootloader, and the pin map it relies on is
the corrected physical pinout of §2 — the official A000005 manual's own pin
tables contain errors (they omit RESET/GND and AREF, mislabel 3V3, and
invent "Serial Wire Debug" pins), so quote this skill's tables, not
Arduino's. Everything marked **⚠︎ compile-checked only** in
`reference/recipes.md` — ADC references, interrupts, Timer1 PWM, Servo/
tone, sleep, PROGMEM — compiles against the Arduino AVR core but was not
run on hardware by the author. The VIN range is reported inconsistently by
the sources (7–15 V, 7–12 V, 6–20 V); this skill states 7–12 V recommended
as the conservative intersection.