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.
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 Color | Forward Voltage | Resistor Range | Common Value |
|---|---|---|---|
| Red | 1.8-2.2V | 330Ω - 1kΩ | 470Ω |
| Green | 2.0-3.0V | 220Ω - 680Ω | 330Ω |
| Blue | 3.0-3.3V | 100Ω - 330Ω | 220Ω |
| Orange/Yellow | 2.0-2.2V | 330Ω - 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 blinkmorse.go- Morse code SOS signalserial.go- Serial output debuggingrgb_led_esp32.go- RGB LED for ESP32rgb_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).
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.
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 (
-targetflag) - 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#
- Visit wokwi.com/esp32
- The ESP32 board with LED is pre-configured
- Copy your
main.gocode to thesketch.inofile - Click “Run” to start simulation
Using Wokwi with VS Code#
Install Wokwi VS Code Extension:
- Open VS Code
- Press
Ctrl+Shift+X(Windows/Linux) orCmd+Shift+X(macOS) - Search for “Wokwi for ESP-IDF”
- 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
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!
