Arduino Projects skill for Claude Code

Sketches that never block and never cook a pin. Claude writes Arduino sketches as small non-blocking tasks driven by millis(), debounces inputs, keeps ISRs to a flag, and avoids heap fragmentation on 2 KB AVRs. Before any wiring advice it checks logic voltage and per-pin current, insists on drivers and flyback diodes for inductive loads, and builds with arduino-cli using pinned core and library versions.

  • namearduino-projects
  • size225 lines · ≈2,400 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Builds and debugs Arduino sketches that stay responsive — setup/loop structure, millis()-based non-blocking timing, state machines, debounced inputs, library selection, arduino-cli compile/upload, and wiring checks for voltage levels and current limits. Use when writing or fixing a .ino sketch, when a sketch freezes or resets, when delay() blocks buttons or sensors, when choosing pins or libraries for Uno, Nano, Mega, Nano ESP32, or RP2040 boards, or when wiring sensors, LEDs, relays, and motors.

Try asking

  • My sketch ignores the button while the LED blink delay runs — rewrite it without delay()
  • The Uno resets every time the servo moves. What is wrong with my wiring?
  • Set up arduino-cli builds for this sketch on a Nano ESP32 with pinned library versions

Use it when

  • Writing or fixing a .ino sketch for AVR, Renesas, RP2040, or ESP32 Arduino cores
  • A sketch freezes, resets randomly, or misses button presses
  • Wiring LEDs, relays, servos, or sensors and checking voltage and current
  • Moving builds from the IDE to arduino-cli or CI

Skip it when

  • Writing ESP-IDF firmware with Wi-Fi, NVS, and partitions — use esp32-firmware
  • Diagnosing I2C, SPI, or UART signal-level problems — use serial-protocols

How to check the result

arduino-cli compile with --warnings all is clean, measured loop time stays under about 5 ms, and the supply holds voltage under worst-case load during a multi-hour soak.

The common mistake it prevents

Powering servos or relays from the board's 5 V pin over USB, which browns out the microcontroller and looks like a software reset bug.

~/.claude/skills/arduino-projects/SKILL.md

--- name: arduino-projects description: Builds and debugs Arduino sketches that stay responsive — setup/loop structure, millis()-based non-blocking timing, state machines,… license: MIT ---

Done well means: loop() never blocks for more than a few milliseconds, every input is debounced, every output is driven within the pin’s current rating, the sketch compiles warning-free with arduino-cli, and RAM usage leaves headroom for the stack.

Ground rules

  • Check the board’s logic level before wiring anything: Uno R3/Nano/Mega are 5 V; Uno R4, Nano 33, Nano ESP32, RP2040, ESP32 and most modern boards are 3.3 V and are not 5 V tolerant. A 5 V sensor output into a 3.3 V pin needs a level shifter or divider.
  • No delay() in sketches that read inputs or run more than one task. Use millis().
  • Never drive motors, relays, solenoids or long LED strips straight from a GPIO pin. Use a transistor/MOSFET or driver board, a separate supply, a common ground, and a flyback diode across inductive loads.
  • Read the datasheet of every module, not the product listing: supply range, logic level, I2C address, current draw.
  • Ask before running anything that erases or overwrites a bootloader (arduino-cli burn-bootloader, ISP programming, fuse changes on AVR).

Electrical limits to respect

Board familyLogicPer-pin abs. maxPractical per-pinNotes
ATmega328P (Uno R3, Nano)5 V40 mA20 mA200 mA total for the chip
ATmega2560 (Mega)5 V40 mA20 mAPer-port group limits apply
RA4M1 (Uno R4)5 V I/O~8 mA8 mAMuch weaker than the R3; check per-pin table
ESP32 / Nano ESP323.3 V~40 mA10–20 mANot 5 V tolerant
RP2040 (Nano RP2040, Pico)3.3 Vconfigurable 2–12 mA4–8 mANot 5 V tolerant
  • LED resistor: R = (Vsupply - Vf) / I. Red LED on 5 V at 10 mA: (5 - 2.0) / 0.010 = 300 Ω, use 330 Ω.
  • USB ports supply 500 mA (USB 2.0); servos and motors brown out the board through USB. A reset when a servo moves means the supply sags — power it separately.
  • The onboard 5 V regulator on a barrel-jack Uno overheats above ~7–9 V input at a few hundred mA. Feed loads from their own regulator.

Workflow

- [ ] 1. Identify board, core, voltage, and every attached part (datasheets)
- [ ] 2. Draw the pin map: pin, direction, pull-up/down, voltage, current
- [ ] 3. Create/verify the toolchain with arduino-cli and a pinned core version
- [ ] 4. Write the sketch as non-blocking tasks + a state machine
- [ ] 5. Compile with warnings on; check flash and RAM report
- [ ] 6. Bring up one peripheral at a time with serial logging
- [ ] 7. Run the soak test (hours) and the worst-case input test

3. Toolchain with arduino-cli

arduino-cli config init
arduino-cli core update-index
arduino-cli board list                         # find port + FQBN
arduino-cli core install arduino:avr           # or arduino:renesas_uno, esp32:esp32, rp2040:rp2040
arduino-cli lib install "Adafruit BME280 Library"
arduino-cli compile --fqbn arduino:avr:uno --warnings all ./MySketch
arduino-cli upload  -p /dev/ttyACM0 --fqbn arduino:avr:uno ./MySketch
arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200
  • Third-party cores need their index URL in arduino-cli.yaml under board_manager.additional_urls (ESP32: Espressif’s package_esp32_index.json).
  • Pin core and library versions for reproducible builds (core install esp32:esp32@<ver>, lib install Name@<ver>) and record them in the README or a sketch.yaml profile.
  • The sketch folder name must match the .ino file name.

4. Non-blocking structure

const uint8_t BUTTON_PIN = 2;
const uint8_t LED_PIN = 13;

unsigned long lastBlink = 0;
const unsigned long BLINK_MS = 500;

bool stableState = HIGH, lastReading = HIGH;
unsigned long lastChange = 0;
const unsigned long DEBOUNCE_MS = 30;

enum class Mode : uint8_t { Idle, Running, Fault };
Mode mode = Mode::Idle;

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);   // button to GND, no external resistor
  pinMode(LED_PIN, OUTPUT);
}

void pollButton(unsigned long now) {
  bool reading = digitalRead(BUTTON_PIN);
  if (reading != lastReading) { lastChange = now; lastReading = reading; }
  if (now - lastChange >= DEBOUNCE_MS && reading != stableState) {
    stableState = reading;
    if (stableState == LOW) {          // pressed
      mode = (mode == Mode::Idle) ? Mode::Running : Mode::Idle;
    }
  }
}

void loop() {
  unsigned long now = millis();
  pollButton(now);
  if (mode == Mode::Running && now - lastBlink >= BLINK_MS) {
    lastBlink += BLINK_MS;             // no drift; use `= now` if catch-up is unwanted
    digitalWrite(LED_PIN, !digitalRead(LED_PIN));
  }
}
  • Always compare now - last >= interval with unsigned long. That form survives the 49.7-day millis() rollover; now >= last + interval does not.
  • One pollX(now) function per concern; loop() just calls them.
  • micros() wraps every ~71 minutes; same subtraction rule.

Interrupts

  • Use attachInterrupt(digitalPinToInterrupt(pin), isr, FALLING) only for events shorter than one loop pass (encoders, pulse counting). Buttons do not need interrupts.
  • ISRs: set a flag or increment a counter, nothing else. No Serial, no delay, no millis() advancement inside, no allocation.
  • Shared variables are volatile. Read multi-byte ones with interrupts paused:
volatile uint32_t pulses = 0;
void onPulse() { pulses++; }

uint32_t readPulses() {
  noInterrupts();
  uint32_t copy = pulses;
  interrupts();
  return copy;
}

Memory on small AVRs

The Uno has 2 KB of SRAM. Stack, heap, and globals share it.

  • Wrap constant strings: Serial.println(F("Sensor ready")); keeps them in flash.
  • Avoid String concatenation in loop() on AVR; it fragments the heap and causes resets after hours. Use fixed char buf[32] with snprintf.
  • snprintf on AVR has no %f; use dtostrf(value, width, prec, buf).
  • The compile report “Global variables use X bytes” — keep it under ~75% on AVR so the stack has room.
  • 32-bit boards (ESP32, RP2040, SAMD, Renesas) have far more RAM; String is fine there in moderation.

Library choice

NeedDefault
Debounced buttonsHand-rolled pattern above, or Bounce2
Many timed tasksmillis() schedulers; avoid “thread” libraries on AVR
ServoServo (AVR/Renesas), ESP32Servo on ESP32
Addressable LEDsFastLED or Adafruit_NeoPixel; 5 V strips need a level shifter from 3.3 V boards
SensorsThe vendor’s library (Adafruit, SparkFun) unless it blocks for long
StepperAccelStepper with a driver (A4988, TMC2209), never GPIO coils

Before adopting a library: check it supports your architecture (architectures= in library.properties), is maintained, and does not call delay() internally for long.

Debugging

SymptomLikely causeCheck
Board resets when a load turns onSupply sag, missing flyback diodeScope or multimeter on 5 V during switching; separate supply
Random resets after hoursHeap fragmentation, stack overflow, watchdogRemove String, print free RAM periodically
Button reads randomlyFloating inputINPUT_PULLUP or external pull resistor
Upload fails “not in sync”Wrong port/board, Serial pins 0/1 in useDisconnect modules on D0/D1, check FQBN
Sensor reads 0 or NaNWrong I2C address, missing pull-ups, wrong voltageRun an I2C scanner sketch; check address in datasheet
Garbage on serial monitorBaud mismatchMatch Serial.begin and monitor baud
analogRead noisyFloating reference, long wiresAverage N samples, add 100 nF at sensor, check AREF

Add a periodic heartbeat log so hangs are visible:

if (now - lastLog >= 1000) { lastLog = now; Serial.print(F("up ")); Serial.println(now / 1000); }

For bus-level problems (I2C NACKs, SPI garbage, UART framing), switch to the serial-protocols skill.

Gotchas

  • Pins 0 and 1 on Uno/Nano are the USB serial lines; using them breaks upload and logs.
  • analogWrite is PWM, not a voltage; frequency and resolution vary by board.
  • A4/A5 are SDA/SCL on Uno R3; on Mega I2C is on 20/21. Do not hardcode across boards.
  • Using Wire at 5 V with a 3.3 V-only sensor breakout that lacks a regulator and level shifter damages the sensor slowly.
  • EEPROM.write wears out after ~100,000 cycles per cell; use EEPROM.update and write only on change.
  • Large local arrays inside functions overflow the 2 KB AVR stack silently.

Verification

  1. arduino-cli compile --warnings all is warning-free; RAM report under ~75% on AVR.
  2. Measure the loop: toggle a spare pin each pass or log max loop time; it should stay under ~5 ms with all features active.
  3. Measure supply voltage under worst-case load with a multimeter; it stays within the board’s range.
  4. Soak test for several hours (or across a millis() rollover simulation by seeding the timing variables near 0xFFFFFFFF in a test build).
  5. Confirm every pin’s current from the pin map against the table above.

Output contract

When delivering a sketch, include:

**Board / core:** Arduino Uno R3, arduino:avr 1.8.x
**Pin map:** D2 button (INPUT_PULLUP, to GND) | D9 servo signal (servo powered from 5 V 2 A supply, common GND) | A4/A5 BME280 (I2C 0x76, 3.3 V breakout with onboard shifter)
**Libraries:** Servo (bundled), Adafruit BME280 Library 2.x
**Power:** USB for logic, separate 5 V 2 A for servo
**Build:** arduino-cli compile --fqbn arduino:avr:uno --warnings all
**Tested:** compile clean, 6 h soak, button debounced under rapid presses

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc