Ruggi.Site Ruggi.Site
Free guide · Arduino and MicroPython · Windows, macOS, Linux

Your first microcontroller project

What a microcontroller is, how to choose a board and set up the software, then ten small complete programs: Blink, button, potentiometer, temperature, serial, I2C scan, servo, millis() timing and a state machine.

FreeNo signupWindows · macOS · Linux
Keep it low voltage. Everything here runs from USB or a few batteries. Never connect a microcontroller project to mains, and do not drive motors, relays or LED strips straight from a pin (use a transistor or driver board and a proper supply). Lithium (LiPo / Li-ion) cells can catch fire if shorted or charged wrongly: use a charger module made for them. Soldering means a hot iron and fumes: see the soldering guide.

1. What is a microcontroller?

A microcontroller is a tiny computer on a single chip: a processor, a little memory and pins that can read buttons and sensors or switch LEDs and motors. There is no operating system: your program (called a sketch or firmware) starts when power is applied and runs forever. It is different from a single-board computer such as a Raspberry Pi, which runs Linux. A microcontroller boots instantly, uses little power and handles precise timing well.

The pattern of every program: a setup part that runs once (configure pins), and a loop that runs again and again (read inputs, decide, write outputs).

2. Choosing a board

For a first project pick a board with lots of tutorials. Good starting points are an Arduino Uno-class board (simplest, 5 V), a Raspberry Pi Pico (cheap, great with MicroPython), or an ESP32 (adds Wi-Fi and Bluetooth). To compare them and tick the features you need, use the hardware board chooser.

Check the pin voltage. Many boards are 3.3 V and their pins are not 5 V tolerant. Do not connect 5 V signals to them.

Beyond the board you need: a USB cable that carries data (some cables are charge-only), a breadboard, jumper wires, a few LEDs, 220 to 330 ohm resistors, a pushbutton, a 10 kohm potentiometer, a hobby servo, and a TMP36 temperature sensor. A cheap starter kit covers it.

3. Software setup

Arduino IDE 2 (Windows, macOS, Linux)

  1. Download and install the Arduino IDE 2 from arduino.cc (free). Verify the current download page.
  2. Plug in the board. Choose the board and the port from the menus at the top of the window.
  3. For non-Arduino boards, open Boards Manager and install the board package (for example "esp32" by Espressif, or an RP2040 core such as the Arduino mbed core or Earle Philhower's core). Some need an extra "Additional Boards Manager URL" in the preferences: copy it from the package's own documentation (verify, it changes).
  4. Upload with the arrow button. Open the Serial Monitor to see output.

Linux: if the port is denied, add your user to the group that owns serial devices (often dialout): sudo usermod -a -G dialout $USER, then log out and in again. The group name differs on some distributions (verify).

Windows: a few clone boards need a driver for their USB-serial chip (CH340 or CP210x); Arduino-branded boards install automatically. macOS: newer versions include drivers for most chips; clones sometimes need the vendor's driver.

MicroPython with Thonny

  1. Install Thonny (free; Windows, macOS, Linux).
  2. Put MicroPython on the board: for a Pico, hold BOOTSEL while plugging it in and copy the downloaded .uf2 file to the drive that appears; for ESP32 boards Thonny's "Configure interpreter" dialog can install the firmware (verify the steps for your board on micropython.org).
  3. In Thonny choose the MicroPython interpreter for your board and port, type code in the editor, and press Run. Save it on the board as main.py to run at power-up.

PlatformIO

A professional-style project system inside VS Code (install the PlatformIO IDE extension; verify the current instructions at platformio.org). You describe the board in a platformio.ini file and it fetches the compiler and libraries. In PlatformIO the Arduino-style sketch becomes src/main.cpp which must start with #include <Arduino.h>.

The "hello world". LED_BUILTIN names the board's on-board LED.

// Arduino C++
void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);
  delay(500);                      // milliseconds
  digitalWrite(LED_BUILTIN, LOW);
  delay(500);
}
# MicroPython (Raspberry Pi Pico / Pico W with current firmware; on many ESP32 boards use Pin(2, Pin.OUT) instead: check your board)
from machine import Pin
import time

led = Pin("LED", Pin.OUT)
while True:
    led.toggle()
    time.sleep(0.5)

An external LED needs a series resistor (220 to 330 ohms with a 3.3 V or 5 V pin) or it burns out.

5. Button with pull-up and debounce

A button between a pin and ground, with the chip's internal pull-up enabled, reads HIGH when released and LOW when pressed. Mechanical contacts bounce for a few milliseconds, so ignore changes for a short time (debounce). This sketch toggles the LED on each press. Wire the button between pin 2 and GND.

const int BUTTON_PIN = 2;
const unsigned long DEBOUNCE_MS = 30;

bool ledOn = false;
int lastReading = HIGH;          // raw reading last time
int stableState = HIGH;          // debounced state
unsigned long lastChange = 0;

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  int reading = digitalRead(BUTTON_PIN);
  if (reading != lastReading) {
    lastChange = millis();       // contact moved: restart the timer
    lastReading = reading;
  }
  if (millis() - lastChange > DEBOUNCE_MS && reading != stableState) {
    stableState = reading;
    if (stableState == LOW) {    // pressed
      ledOn = !ledOn;
      digitalWrite(LED_BUILTIN, ledOn ? HIGH : LOW);
    }
  }
}
# MicroPython (Pico; button between GP14 and GND)
from machine import Pin
import time

button = Pin(14, Pin.IN, Pin.PULL_UP)
led = Pin("LED", Pin.OUT)
last = 1
while True:
    now = button.value()
    if last == 1 and now == 0:       # pressed
        led.toggle()
        time.sleep_ms(30)            # simple debounce
    last = now
    time.sleep_ms(5)

6. Read a potentiometer and set LED brightness

Outer pins of the potentiometer to 5 V (3.3 V on a 3.3 V board) and GND, middle pin to the analog input A0. The ADC turns the voltage into a number (0 to 1023 on an Uno, which has a 10-bit converter). PWM then switches an LED on and off fast so its average brightness follows the number. LED with resistor on pin 9 (a PWM pin on an Uno; on other boards use a pin marked PWM / ~).

const int POT_PIN = A0;
const int LED_PIN = 9;           // must be a PWM-capable pin

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  int raw = analogRead(POT_PIN);                 // 0..1023 on an Uno
  int brightness = map(raw, 0, 1023, 0, 255);    // analogWrite takes 0..255
  analogWrite(LED_PIN, brightness);
  Serial.println(raw);
  delay(20);
}

Other boards have different ADC resolutions (for example 12-bit, 0 to 4095, on ESP32 and Pico in MicroPython’s 16-bit scaling), so change the range in map() to match.

# MicroPython (Pico): pot middle pin on GP26 (ADC0), LED + resistor on GP15
from machine import ADC, Pin, PWM
import time

pot = ADC(26)
led = PWM(Pin(15))
led.freq(1000)
while True:
    led.duty_u16(pot.read_u16())     # both are 0..65535
    time.sleep_ms(20)

7. Temperature sensor

A TMP36 has three pins (facing the flat side: 5 V, output, GND; check the datasheet for your package). Its output is 0.5 V at 0 degC, plus 10 mV for each degree. This sketch is for a 5 V Uno-class board with the default 5 V analog reference.

const int TEMP_PIN = A0;

void setup() {
  Serial.begin(9600);
}

void loop() {
  int raw = analogRead(TEMP_PIN);
  float volts = raw * 5.0 / 1024.0;
  float tempC = (volts - 0.5) * 100.0;
  Serial.print(tempC);
  Serial.println(" C");
  delay(1000);
}

On a 3.3 V board change 5.0 to 3.3 and the ADC range (for example 4096) and power the sensor from 3.3 V. Readings can be a degree or two off; the ESP32 ADC is not very linear. A DS18B20 or a digital sensor (BME280, SHT31) is more accurate and uses a library.

8. Serial print

The serial connection over USB is your best debugging tool. Open the Serial Monitor in the IDE and set its speed to match Serial.begin.

int counter = 0;

void setup() {
  Serial.begin(9600);
  Serial.println("Hello from the board");
}

void loop() {
  Serial.print("count = ");
  Serial.println(counter);
  counter++;
  delay(1000);
}
# MicroPython: print() goes to Thonny's shell
import time
counter = 0
while True:
    print("count =", counter)
    counter += 1
    time.sleep(1)

9. I2C scan

I2C uses two wires, SDA (data) and SCL (clock), plus power and ground, and many sensors and displays share them. Each device has an address. This scanner lists what it finds, which proves the wiring works. On an Uno SDA is A4 and SCL is A5; other boards use their own labelled pins. Most breakout boards include pull-up resistors already. Use a 3.3 V sensor on 3.3 V logic.

#include <Wire.h>

void setup() {
  Wire.begin();
  Serial.begin(9600);
  while (!Serial) { }
  Serial.println("Scanning I2C...");
  int found = 0;
  for (byte address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    if (Wire.endTransmission() == 0) {
      Serial.print("Found device at 0x");
      if (address < 16) Serial.print("0");
      Serial.println(address, HEX);
      found++;
    }
  }
  if (found == 0) Serial.println("No devices found");
}

void loop() { }
# MicroPython (Pico: I2C0 on GP4 = SDA, GP5 = SCL)
from machine import I2C, Pin
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400000)
print([hex(a) for a in i2c.scan()])

10. Servo

A hobby servo has three wires: signal (usually yellow or white), power (red) and ground (brown or black). Connect signal to pin 9. Small servos can draw an ampere when they move or stall, which a USB port or a board's 5 V pin may not supply: power the servo from a separate 4.8 to 6 V supply and join its ground to the board's ground. Add a capacitor (a few hundred microfarads) across the servo supply if the board resets when it moves.

#include <Servo.h>

Servo myServo;

void setup() {
  myServo.attach(9);
}

void loop() {
  for (int angle = 0; angle <= 180; angle += 2) {
    myServo.write(angle);
    delay(15);
  }
  for (int angle = 180; angle >= 0; angle -= 2) {
    myServo.write(angle);
    delay(15);
  }
}

The Servo library works on AVR and many other Arduino cores; ESP32 and some cores need a different library such as ESP32Servo (verify).

11. Non-blocking timing with millis()

delay() freezes everything. To blink and read a button at the same time, compare millis(), the milliseconds since power-up, with the time of the last action.

const unsigned long INTERVAL = 500;
unsigned long previous = 0;
bool state = false;

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  unsigned long now = millis();
  if (now - previous >= INTERVAL) {   // subtraction stays correct when millis() wraps around
    previous = now;
    state = !state;
    digitalWrite(LED_BUILTIN, state ? HIGH : LOW);
  }
  // other work can run here every pass: read buttons, sensors, etc.
}
# MicroPython equivalent
import time
from machine import Pin
led = Pin("LED", Pin.OUT)
previous = time.ticks_ms()
while True:
    now = time.ticks_ms()
    if time.ticks_diff(now, previous) >= 500:
        previous = now
        led.toggle()

12. A simple state machine

A state machine keeps one variable saying which "mode" the program is in. A traffic light is a good first one: three LEDs with resistors on pins 10 (red), 11 (yellow), 12 (green).

enum State { RED, GREEN, YELLOW };

const int RED_PIN = 10, YELLOW_PIN = 11, GREEN_PIN = 12;
State state = RED;
unsigned long stateStart = 0;

unsigned long durationOf(State s) {
  switch (s) {
    case RED:    return 4000;
    case GREEN:  return 3000;
    default:     return 1000;     // YELLOW
  }
}

void show(State s) {
  digitalWrite(RED_PIN,    s == RED    ? HIGH : LOW);
  digitalWrite(YELLOW_PIN, s == YELLOW ? HIGH : LOW);
  digitalWrite(GREEN_PIN,  s == GREEN  ? HIGH : LOW);
}

void setup() {
  pinMode(RED_PIN, OUTPUT);
  pinMode(YELLOW_PIN, OUTPUT);
  pinMode(GREEN_PIN, OUTPUT);
  show(state);
  stateStart = millis();
}

void loop() {
  if (millis() - stateStart >= durationOf(state)) {
    switch (state) {
      case RED:    state = GREEN;  break;
      case GREEN:  state = YELLOW; break;
      case YELLOW: state = RED;    break;
    }
    show(state);
    stateStart = millis();
  }
}

13. Power and wiring tips

  • Power from USB is fine for LEDs, sensors and one small servo; motors, long LED strips and Wi-Fi bursts need more current than a pin or a weak USB port gives.
  • Always connect grounds together when using more than one supply.
  • Never connect two outputs together, and never drive a pin above the board's logic voltage.
  • Power the board and wire everything with it unplugged; check the wiring before applying power.
  • Use a resistor with every LED, flyback diodes with relays and motors, and a transistor or driver board for anything beyond a few milliamps per pin.
  • Colour code wires (red = power, black = ground) and keep them short; breadboard contacts wear out and cause "impossible" bugs.
  • Use the electronics calculators for resistor values, and the circuit cookbook for tested circuits such as transistor switches and level shifters.

14. Common errors

SymptomLikely cause and fix
No port listed, or "port not found"Charge-only USB cable (try another), missing driver for the USB chip, or on Linux no serial permission (dialout group)
Upload fails or "not in sync"Wrong board or port selected; another program holds the port (close the serial monitor); ESP32 may need the BOOT button held while uploading
Code compiles but nothing happensWrong pin number; LED the wrong way round; forgot pinMode; missing common ground
Garbage in the serial monitorThe baud rate in the monitor does not match Serial.begin
Board resets randomly (brownout)Supply cannot deliver the current (servo, motor, Wi-Fi): use a bigger supply, a capacitor, and short thick power wires
3.3 V vs 5 VA 5 V signal into a 3.3 V pin can damage the chip; use a level shifter. A 3.3 V output into a 5 V input may not read as HIGH
Button seems to press by itselfFloating input: use INPUT_PULLUP or a pull-down resistor
Sensor values jump aroundNoise, loose wires or a missing decoupling capacitor; average several readings
Program works then freezesRunning out of RAM (many strings or arrays), or blocking calls such as long delay()
I2C scan finds nothingSDA and SCL swapped, no pull-ups, wrong voltage, or a bad address

15. Where to go next

  • Combine the pieces: a thermostat (sensor + state machine + relay module with proper isolation), a plant-water alarm, a data logger.
  • Read other people's libraries and examples; look at your IDE's File > Examples menu.
  • Learn to read datasheets: absolute maximum ratings, typical application circuit and pinout.
  • Try a second language (MicroPython vs C++) and a second board family. The board chooser helps pick.
  • When ready for a permanent version, design a PCB. For a cheat sheet and calculators, see the Electronics cheat sheet.

Sketches use the standard Arduino API and standard MicroPython modules (pin numbers are for the boards named in the comments). IDE menus, board packages and download pages change: verify against each project's own documentation. Last checked November 2026.