Skip to main content

TinyGo Embedded Workshop - Assignment 2: Blinky

·6 mins·
WS002EN - This article is part of a series.
Part 3: This Article

Assignment 2: Blinky
#

Blinking an LED is the “Hello, World!” of embedded development. In this assignment, you’ll write your first TinyGo program to blink an LED.

Source Code Available: All example code for this assignment is available in the developer-portal-codebase repository. See content/workshops/tinygo/assignment_2/ for complete working examples including blinky, morse code, serial output, and RGB LED control.

Understanding GPIO
#

GPIO (General Purpose Input/Output) pins are the interface between your microcontroller and the physical world. They can be:

  • Inputs: Read digital signals (buttons, switches)
  • Outputs: Control digital signals (LEDs, relays, motors)

LED Basics
#

An LED (Light Emitting Diode) is a simple output device:

  • Anode (+): Longer leg, connects to positive voltage through a resistor
  • Cathode (-): Shorter leg with flat side on casing, connects to ground (GND)
  • Resistor: Limits current to prevent LED damage

Resistor Values for ESP32 (3.3V):

LED ColorForward VoltageResistor RangeCommon Value
Red1.8-2.2V330Ω - 1kΩ470Ω
Green2.0-3.0V220Ω - 680Ω330Ω
Blue3.0-3.3V100Ω - 330Ω220Ω
Orange/Yellow2.0-2.2V330Ω - 680Ω470Ω

Wiring for External LED: ESP32 GPIO → Resistor → LED Anode → LED Cathode → GND

Digital Logic:

  • HIGH (1): Pin outputs VCC (3.3V on ESP32) - LED ON
  • LOW (0): Pin outputs GND (0V) - LED OFF

Finding the LED Pin
#

Different boards have built-in LEDs on different pins:

ESP32 Built-in LED:

  • Pin: GPIO2 (most common) or GPIO10 (some boards)
  • Type: Active HIGH (LED on when pin is HIGH)

For external RGB LEDs: WS2812/NeoPixel on GPIO8

ESP32-S3 Built-in LED:

  • Pin: GPIO2 (most common)
  • Type: Active HIGH (LED on when pin is HIGH)

Many boards also include RGB LEDs.

ESP32-C3 Built-in LED:

  • Pin: GPIO2 (most boards, active HIGH)
  • Type: Built-in LED

For external RGB LEDs: WS2812/NeoPixel on GPIO2

Get the Source Code
#

The complete source code for this assignment is available in the developer-portal-codebase repository:

git clone https://github.com/espressif/developer-portal-codebase.git
cd developer-portal-codebase/content/workshops/tinygo/assignment_2

Available examples:

  • blinky.go - Basic LED blink
  • morse.go - Morse code SOS signal
  • serial.go - Serial output debugging
  • rgb_led_esp32.go - RGB LED for ESP32
  • rgb_led_esp32c3.go - RGB LED for ESP32-C3

Each example is a complete working program. Build by specifying the source file:

# Example: Build blinky for ESP32-C3
tinygo flash -target esp32c3-generic blinky.go

See the README.md in the assignment directory for detailed build instructions.

Creating Your Blinky Program
#

Step 1: Create Project Directory
#

mkdir blinky
cd blinky
go mod init blinky

Step 2: Write the Code
#

Create main.go:

main.go for ESP32:

package main

import (
    "machine"
    "time"
)

func main() {
    // ESP32: LED on GPIO2 (active HIGH)
    led := machine.GPIO2
    led.Configure(machine.PinConfig{Mode: machine.PinOutput})

    for {
        led.High()  // LED ON
        time.Sleep(time.Millisecond * 500)

        led.Low() // LED OFF
        time.Sleep(time.Millisecond * 500)
    }
}

main.go for ESP32-S3:

package main

import (
    "machine"
    "time"
)

func main() {
    // ESP32-S3: LED on GPIO2 (active HIGH)
    led := machine.GPIO2
    led.Configure(machine.PinConfig{Mode: machine.PinOutput})

    for {
        led.High()  // LED ON
        time.Sleep(time.Millisecond * 500)

        led.Low() // LED OFF
        time.Sleep(time.Millisecond * 500)
    }
}

main.go for ESP32-C3:

package main

import (
    "machine"
    "time"
)

func main() {
    // ESP32-C3: LED on GPIO2 (active HIGH)
    led := machine.GPIO2
    led.Configure(machine.PinConfig{Mode: machine.PinOutput})

    for {
        led.High()  // LED ON
        time.Sleep(time.Millisecond * 500)

        led.Low() // LED OFF
        time.Sleep(time.Millisecond * 500)
    }
}

Code Explanation
#

Importing packages:

import (
    "machine"  // Hardware access (GPIO, I2C, SPI, etc.)
    "time"     // Time and sleep functions
)

Configuring the LED pin:

led := machine.GPIO2                     // Select pin (varies by board)
led.Configure(machine.PinConfig{         // Configure pin
    Mode: machine.PinOutput,             //   as output
})

Note: Different boards use different GPIO pins for the built-in LED:

  • ESP32: GPIO2
  • ESP32-S3: GPIO2
  • ESP32-C3: GPIO2

**Blinking loop:**
```go
for {                                   // Infinite loop
    led.High()                          // Set pin HIGH (LED ON)
    time.Sleep(time.Millisecond * 500)  // Wait 500ms

    led.Low()                           // Set pin LOW (LED OFF)
    time.Sleep(time.Millisecond * 500)  // Wait 500ms
}

Step 3: Build and Flash
#

Build the firmware:

tinygo build -target esp32s3-generic -o firmware.bin .
tinygo build -target m5stack-core2 -o firmware.bin .
tinygo build -target esp32c3-generic -o firmware.bin .

Flash to board:

tinygo flash -target esp32s3-generic .
tinygo flash -target esp32-generic .
tinygo flash -target esp32c3-generic .

Tip: TinyGo can auto-detect the port and baudrate, so you don’t need to specify them manually.

Step 4: Observe the LED
#

The built-in LED should blink at 1Hz (500ms on, 500ms off).

Note: LED position varies by board. Some boards have LEDs on the back, others on the front. Check your board documentation.

Understanding Active HIGH
#

Built-in LEDs on ESP32 boards are “active HIGH”:

  • LED ON: Pin output HIGH (3.3V, VCC)
  • LED OFF: Pin output LOW (0V, GND)

This is straightforward: applying voltage turns the LED on.

To use an external LED with current-limiting resistor:

led.Low()  // LED OFF
led.High() // LED ON

Experiment: Change Blink Rate#

Modify the delay to change blink rate:

// Fast blink (100ms)
time.Sleep(time.Millisecond * 100)

// Slow blink (1000ms = 1 second)
time.Sleep(time.Millisecond * 1000)

// Blink in Hz (2Hz = 2 times per second)
time.Sleep(time.Second / 2)

Experiment: Morse Code
#

Blink “SOS” in Morse code:

  • S: *** (three short blinks)
  • O: — (three long blinks)

Create a new project:

mkdir morse
cd morse
go mod init morse

Create morse.go:

package main

import (
    "machine"
    "time"
)

var led machine.Pin

func shortBlink() {
    led.High()
    time.Sleep(time.Millisecond * 200)
    led.Low()
    time.Sleep(time.Millisecond * 200)
}

func longBlink() {
    led.High()
    time.Sleep(time.Millisecond * 600)
    led.Low()
    time.Sleep(time.Millisecond * 200)
}

func main() {
    // Configure LED pin for your board:
    // ESP32/ESP32-S3: GPIO2
    // ESP32-C3: GPIO2
    led = machine.GPIO2
    led.Configure(machine.PinConfig{Mode: machine.PinOutput})

    for {
        // S: ***
        shortBlink()
        shortBlink()
        shortBlink()
        time.Sleep(time.Millisecond * 400)

        // O: ---
        longBlink()
        longBlink()
        longBlink()
        time.Sleep(time.Millisecond * 400)

        // S: ***
        shortBlink()
        shortBlink()
        shortBlink()

        time.Sleep(time.Second * 2) // Pause between SOS
    }
}

Build and flash:

# ESP32
tinygo flash -target esp32-generic morse.go

# ESP32-S3
tinygo flash -target esp32s3-generic morse.go

# ESP32-C3
tinygo flash -target esp32c3-generic morse.go

Build and flash:

tinygo flash -target [your-target] .

Watch your LED blink the international distress signal: SOS!

Serial Output
#

Add debug output to monitor via USB:

package main

import (
    "machine"
    "time"
)

func main() {
    // Initialize serial (USB)
    serial := machine.Serial
    serial.Configure(machine.UARTConfig{
        BaudRate: 115200,
    })

    // Use appropriate GPIO for your board
    led := machine.GPIO2  // Most boards use GPIO2 for active HIGH
    led.Configure(machine.PinConfig{Mode: machine.PinOutput})

    serial.Write([]byte("Blinky starting!\r\n"))

    for {
        serial.Write([]byte("LED ON\r\n"))
        led.High()
        time.Sleep(time.Millisecond * 500)

        serial.Write([]byte("LED OFF\r\n"))
        led.Low()
        time.Sleep(time.Millisecond * 500)
    }
}

Monitor serial output:

tinygo monitor

Press Ctrl+C to stop monitoring.

screen /dev/ttyUSB0 115200

To quit: Press Ctrl+A then K (kill), then confirm with Y

picocom -b 115200 /dev/ttyUSB0

To quit: Press Ctrl+A then Ctrl+Q

Install picocom:

# Ubuntu/Debian
sudo apt-get install picocom

# macOS
brew install picocom

# Fedora
sudo dnf install picocom

RGB LED (NeoPixel)
#

Many ESP32 boards include RGB LEDs (WS2812B/SK68XX NeoPixels) that can display millions of colors. These are individually addressable LEDs that use a single data line.

ESP32-S3 Compatibility: The ws2812 driver requires a small wrapper for ESP32-S3 due to API differences. See the ESP32-S3 tab for the machine_esp32s3.go wrapper file.

Finding the RGB LED Pin
#

ESP32 RGB LED:

  • Pin: GPIO8 (common on many boards)
  • Type: WS2812B NeoPixel
  • Check your board documentation for exact pin

ESP32-S3 RGB LED:

ESP32-S3-DevKitC-1:

  • v1.0: GPIO48
  • v1.1: GPIO38 (recommended, works on all versions)

Important: The NeoPixel pin changed from GPIO48 to GPIO38 in v1.1 because GPIO47/48 operate at 1.8V on ESP32-S3R8V chips. GPIO38 is safer and works on all versions. Use GPIO38 for compatibility.

Other ESP32-S3 boards:

  • Check your board documentation for exact pin
  • Type: WS2812B NeoPixel
class="flex px-4 py-3 rounded-md" style="background-color: #fff3cd"

<span

  class="pe-3 flex items-center" style="color: #856404"

>
<span class="relative block icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z"/></svg>

<span

  class="dark:text-neutral-300"

><strong>Note:</strong> ESP32-S3 requires a compatibility wrapper. See the code example below for <code>machine_esp32s3.go</code>.</span>

ESP32-C3 RGB LED:

  • Pin: GPIO2 (common on many boards)
  • Type: WS2812B NeoPixel
  • Check your board documentation for exact pin

RGB LED Example
#

Create a new project for RGB LED control:

mkdir rgb-blinky
cd rgb-blinky
go mod init rgb-blinky

Add the TinyGo drivers to your go.mod file:

go get tinygo.org/x/drivers@v0.27.0

Prerequisites:

go mod download tinygo.org/x/drivers

This ensures the ws2812 driver package is available for TinyGo.

rgb_led.go for ESP32:

package main

import (
    "machine"
    "time"

    "tinygo.org/x/drivers/ws2812"
    "image/color"
)

func main() {
    // ESP32: RGB LED on GPIO8
    led := machine.GPIO8
    led.Configure(machine.PinConfig{Mode: machine.PinOutput})

    // NeoPixel driver
    neo := ws2812.New(led)

    // Brightness: 0-255 scale. RGB LEDs are extremely bright, so using 20%
    brightness := uint8(51)

    // Base colors at full brightness
    baseColors := []color.RGBA{
        {255, 0, 0, 255},     // Red
        {0, 255, 0, 255},     // Green
        {0, 0, 255, 255},     // Blue
        {255, 255, 0, 255},   // Yellow
        {0, 255, 255, 255},   // Cyan
        {255, 0, 255, 255},   // Magenta
        {255, 255, 255, 255}, // White
    }

    // Apply brightness scaling
    colors := make([]color.RGBA, len(baseColors))
    for i, c := range baseColors {
        colors[i] = color.RGBA{
            R: uint8(uint16(c.R) * uint16(brightness) / 255),
            G: uint8(uint16(c.G) * uint16(brightness) / 255),
            B: uint8(uint16(c.B) * uint16(brightness) / 255),
            A: 255,
        }
    }

    for {
        for _, c := range colors {
            neo.WriteColors([]color.RGBA{c})
            time.Sleep(time.Millisecond * 500)
        }
    }
}

ESP32-S3 requires local driver modification:

Due to API differences, ESP32-S3 needs a locally modified ws2812 driver. Follow these steps:

Step 1: Copy ws2812 driver locally

mkdir -p drivers/ws2812
cp -r $(go env GOMODCACHE)/tinygo.org/x/drivers@*/ws2812/*.go drivers/ws2812/

Step 2: Modify ws2812_xtensa.go Create drivers/ws2812/ws2812_xtensa_esp32s3.go:

//go:build xtensa && esp32s3

package ws2812

import (
	"device"
	"machine"
	"runtime/interrupt"
	"unsafe"
)

func (d Device) WriteByte(c byte) error {
	portSet, maskSet := d.Pin.PortMaskSet()
	portClear, maskClear := d.Pin.PortMaskClear()
	mask := interrupt.Disable()

	// ESP32-S3 uses GetCPUFrequency()
	cpuFreq, _ := machine.GetCPUFrequency()

	switch cpuFreq {
	case 160e6: // 160MHz
		// (same assembly code as original driver for 160MHz)
		// ... [full assembly code from original driver]
	case 80e6: // 80MHz
		// (same assembly code as original driver for 80MHz)
		// ... [full assembly code from original driver]
	default:
		interrupt.Restore(mask)
		return errUnknownClockSpeed
	}
}

Step 3: Update main.go

package main

import (
	"machine"
	"time"

	"rgb-blinky/drivers/ws2812"  // Use local driver
	"image/color"
)

func main() {
	// ESP32-S3-DevKitC-1: RGB LED on GPIO38
	led := machine.GPIO38
	led.Configure(machine.PinConfig{Mode: machine.PinOutput})

	neo := ws2812.New(led)
	neo.SetBrightness(51) // 20% brightness

	colors := []color.RGBA{
		{255, 0, 0, 255},    // Red
		{0, 255, 0, 255},    // Green
		{0, 0, 255, 255},    // Blue
		{255, 255, 0, 255},  // Yellow
		{0, 255, 255, 255},  // Cyan
		{255, 0, 255, 255},  // Magenta
		{255, 255, 255, 255}, // White
	}

	for {
		for _, c := range colors {
			neo.WriteColors([]color.RGBA{c})
			time.Sleep(time.Millisecond * 500)
		}
	}
}
class="flex px-4 py-3 rounded-md" style="background-color: #d1ecf1"

<span

  class="pe-3 flex items-center" style="color: #0c5460"

>
<span class="relative block icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M112.1 454.3c0 6.297 1.816 12.44 5.284 17.69l17.14 25.69c5.25 7.875 17.17 14.28 26.64 14.28h61.67c9.438 0 21.36-6.401 26.61-14.28l17.08-25.68c2.938-4.438 5.348-12.37 5.348-17.7L272 415.1h-160L112.1 454.3zM191.4 .0132C89.44 .3257 16 82.97 16 175.1c0 44.38 16.44 84.84 43.56 115.8c16.53 18.84 42.34 58.23 52.22 91.45c.0313 .25 .0938 .5166 .125 .7823h160.2c.0313-.2656 .0938-.5166 .125-.7823c9.875-33.22 35.69-72.61 52.22-91.45C351.6 260.8 368 220.4 368 175.1C368 78.61 288.9-.2837 191.4 .0132zM192 96.01c-44.13 0-80 35.89-80 79.1C112 184.8 104.8 192 96 192S80 184.8 80 176c0-61.76 50.25-111.1 112-111.1c8.844 0 16 7.159 16 16S200.8 96.01 192 96.01z"/></svg>

<span

  class="dark:text-neutral-300"

><strong>Working Example:</strong> A complete working ESP32-S3 RGB example is available at <code>/Users/georgik/projects/workshop/rgb-blinky</code> with the modified driver.</span>

rgb_led.go for ESP32-C3:

package main

import (
    "machine"
    "time"

    "tinygo.org/x/drivers/ws2812"
    "image/color"
)

func main() {
    // ESP32-C3: RGB LED on GPIO2
    led := machine.GPIO2
    led.Configure(machine.PinConfig{Mode: machine.PinOutput})

    // NeoPixel driver
    neo := ws2812.New(led)

    // Brightness: 0-255 scale. RGB LEDs are extremely bright, so using 20%
    brightness := uint8(51)

    // Base colors at full brightness
    baseColors := []color.RGBA{
        {255, 0, 0, 255},     // Red
        {0, 255, 0, 255},     // Green
        {0, 0, 255, 255},     // Blue
        {255, 255, 0, 255},   // Yellow
        {0, 255, 255, 255},   // Cyan
        {255, 0, 255, 255},   // Magenta
        {255, 255, 255, 255}, // White
    }

    // Apply brightness scaling
    colors := make([]color.RGBA, len(baseColors))
    for i, c := range baseColors {
        colors[i] = color.RGBA{
            R: uint8(uint16(c.R) * uint16(brightness) / 255),
            G: uint8(uint16(c.G) * uint16(brightness) / 255),
            B: uint8(uint16(c.B) * uint16(brightness) / 255),
            A: 255,
        }
    }

    for {
        for _, c := range colors {
            neo.WriteColors([]color.RGBA{c})
            time.Sleep(time.Millisecond * 500)
        }
    }
}

Build and Flash
#

Build commands (same as before, just in the new rgb-blinky directory):

tinygo flash -target m5stack-core2 .
tinygo flash -target esp32s3-generic .

Note: Make sure machine_esp32s3.go wrapper file is included in your project.

tinygo flash -target m5stack-stampc3 .

Understanding RGB LEDs
#

RGB LED Colors:

  • Red: {255, 0, 0} - Full red, no green, no blue
  • Green: {0, 255, 0} - No red, full green, no blue
  • Blue: {0, 0, 255} - No red, no green, full blue
  • White: {255, 255, 255} - All colors at full

Brightness:

  • Scale: 0-255 (0 = off, 255 = maximum)
  • 20% brightness: 51 (recommended for RGB LEDs)
  • RGB LEDs are extremely bright - even 20% is plenty!

Important: The SetBrightness() method is not available in the ws2812 driver. Use manual brightness scaling instead (see RGB LED examples below).

Color Mixing:

// Orange (red + green)
{255, 165, 0, 255}

// Purple (red + blue)
{128, 0, 128, 255}

// Pink (light red)
{255, 192, 203, 255}

Advanced: Rainbow Effect
#

Create a smooth rainbow transition:

func wheel(pos uint8) color.RGBA {
    if pos < 85 {
        return color.RGBA{255 - pos*3, pos * 3, 0, 255}
    }
    if pos < 170 {
        pos -= 85
        return color.RGBA{0, 255 - pos*3, pos * 3, 255}
    }
    pos -= 170
    return color.RGBA{pos * 3, 0, 255 - pos*3, 255}
}

func main() {
    // ... setup code ...

    var pos uint8 = 0
    for {
        neo.WriteColors([]color.RGBA{wheel(pos)})
        pos++
        time.Sleep(time.Millisecond * 10)
    }
}

Troubleshooting
#

“Board not found”
#

  • Check USB cable (must support data)
  • Try different USB port
  • Verify port with tinygo ports
  • Check board is powered (LED lit)

“Permission denied”
#

Linux:

sudo usermod -a -G dialout $USER
# Log out and in

macOS:

sudo chmod 666 /dev/cu.usbserial-*

LED not blinking
#

  • Check code compiles without errors
  • Verify correct target (-target flag)
  • Try pressing RESET button on board
  • Check LED is working (try different pin)

Compilation errors
#

  • Ensure Go 1.26+ installed: go version
  • Ensure TinyGo 0.41 installed: tinygo version
  • Check imports are correct
  • Verify board is supported

Simulation with Wokwi
#

Don’t have an ESP32 board? You can simulate this project using Wokwi!

What is Wokwi?
#

Wokwi is an online electronics simulator that supports ESP32 boards. It’s perfect for testing code without hardware.

Using Wokwi Web Interface
#

  1. Visit wokwi.com/esp32
  2. The ESP32 board with LED is pre-configured
  3. Copy your main.go code to the sketch.ino file
  4. Click “Run” to start simulation

Using Wokwi with VS Code
#

Install Wokwi VS Code Extension:

  1. Open VS Code
  2. Press Ctrl+Shift+X (Windows/Linux) or Cmd+Shift+X (macOS)
  3. Search for “Wokwi for ESP-IDF”
  4. Install the extension

Create Wokwi Configuration:

Create wokwi.toml in your project directory:

[wokwi]
version = 1
firmware = 'firmware.bin'
elf = 'firmware.elf'

Create Diagram Configuration:

Create diagram.json:

{
  "version": 1,
  "author": "TinyGo Workshop",
  "editor": "wokwi",
  "parts": [
    {
      "type": "board-esp32-c3-devkitm-1",
      "id": "esp",
      "top": 0,
      "left": 0,
      "attrs": {}
    },
    {
      "type": "wokwi-led",
      "id": "led1",
      "top": -150,
      "left": 150,
      "attrs": { "color": "red" }
    }
  ],
  "connections": [
    [ "led1:A", "esp:10", "red", [ "v0" ] ],
    [ "led1:C", "esp:GND.2", "black", [ "v0" ] ],
    [ "esp:TX", "$serialMonitor:RX", "", [] ],
    [ "esp:RX", "$serialMonitor:TX", "", [] ]
  ]
}

Build and Run:

# Build firmware
tinygo build -target xiao-esp32c3 -o firmware.bin .

# Start Wokwi simulation
# Press F1 in VS Code, type "Wokwi: Start Simulator"

Supported Boards in Wokwi
#

  • ESP32-C3: Many dev boards supported
  • ESP32-S3: Many dev boards supported
  • ESP32: Original ESP32 supported
Tip: Wokwi is great for testing and learning, but always verify your code on real hardware before deployment. Simulation may not perfectly match real-world behavior.

Summary
#

In this assignment, you learned:

  • What GPIO pins are and how to use them
  • How to configure pins as inputs or outputs
  • Digital output (HIGH/LOW, 3.3V/0V)
  • Active LOW vs active HIGH
  • Time delays and loops
  • Building and flashing TinyGo programs
  • Serial monitoring for debugging
  • RGB LED (NeoPixel) control with WS2812 driver
  • Color mixing and brightness control
  • Simulating projects with Wokwi

You now have the foundation to control any digital output and create colorful LED effects!

Assignment 3: Display

WS002EN - This article is part of a series.
Part 3: This Article

Related