if !btnA.Get() { // LOW = pressed (active LOW with pull-up)
led.High()
} else {
led.Low()
}
time.Sleep(time.Millisecond * 10)
}
}
```
```sh
tinygo flash -target nicenano ./basics/step1
```
Hold button A — the LED turns on. Release it — the LED turns off.
**Key concepts**
-`machine.PinInputPullup` — the pin floats HIGH internally; pressing the button connects it to GND → LOW.
-`!btnA.Get()` — because the logic is inverted (active LOW), we negate the reading.
**Challenge:** modify the code so that the LED turns on when button **B** is pressed instead.
---
### step2 — WS2812 RGB LEDs
**Goal:** drive the two addressable RGB LEDs.
WS2812 (SK6812MINI-E) LEDs are controlled with a single data wire using a timed pulse protocol. The `ws2812` driver handles the timing; you just provide colors.
The two LEDs alternate between red and green every 300 ms.
**Key concepts**
-`color.RGBA{R, G, B, A}` — standard Go color type; A (alpha) is always 255 for LEDs.
-`leds.WriteColors(slice)` — pushes the entire color slice to the strip in one call.
**Challenge:** add a third color (blue) and cycle through all three.
---
### step3 — WS2812 LEDs + buttons
**Goal:** combine inputs and outputs — each button sets a different LED color.
```sh
tinygo flash -target nicenano ./basics/step3
```
- Press **A** → both LEDs turn red.
- Press **B** → both LEDs turn blue.
- Press the **rotary button** → both LEDs turn green.
- Release all → LEDs stay on the last color.
**Key concepts**
- Multiple inputs read in the same loop.
- State is kept across loop iterations with the `c` variable.
---
### step3b — Rainbow LEDs
**Goal:** generate smooth color transitions (hue wheel) and let buttons scroll through them.
The `getRainbowRGB` function maps a `uint8` (0–255) to a point on the RGB color wheel, dividing it into three 85-step segments: red→green, green→blue, blue→red.
```sh
tinygo flash -target nicenano ./basics/step3b
```
- Hold **A** → hue advances (warm colors).
- Hold **B** → hue recedes (cool colors).
- The two LEDs are always offset by 10 hue steps apart.
**Challenge:** increase the offset between the two LEDs to 128 (opposite colors on the wheel).
---
### step4 — Display: text
**Goal:** initialize the ST7789 display and render text.
The display talks over SPI and needs explicit pin configuration on the nice!nano. After configuration, the drawable area is **240 × 135 pixels** in landscape orientation.
"Hello" and "Gophers!" appear on the display in yellow and magenta.
**Key concepts**
-`RowOffset` / `ColumnOffset` — the ST7789 physical memory does not always start at (0,0); these offsets align the driver to the actual pixel grid of this display module.
-`tinyfont.WriteLine(&display, &font, x, y, text, color)` — `x`, `y` are the **baseline** position of the text (not the top-left corner).
**Challenge:** change the font to `freesans.Regular9pt7b` and add a third line.
---
### step5 — Display + buttons
**Goal:** update the display in real time based on button state.
Three filled circles represent the three buttons. When a button is pressed a ring appears around its circle.
```sh
tinygo flash -target nicenano ./basics/step5
```
- Press **B** (left circle), **rotary** (center), or **A** (right circle) to see the corresponding ring.
**Key concepts**
-`tinydraw.FilledCircle` / `tinydraw.Circle` — drawing primitives from `tinygo.org/x/tinydraw`.
- Re-drawing a shape in the background color erases it — no need to clear the whole screen.
---
### step6 — Analog joystick
**Goal:** read the joystick's two analog axes and visualize the position on the display.
The joystick outputs a voltage proportional to its position on each axis. The ADC converts that voltage to a 16-bit integer (0–65535). The center resting position is approximately 32767.
```go
machine.InitADC()
ax := machine.ADC{Pin: machine.P0_02} // X axis
ay := machine.ADC{Pin: machine.P0_29} // Y axis
ax.Configure(machine.ADCConfig{})
ay.Configure(machine.ADCConfig{})
// map 0-65535 → display width/height
dotX := int16(uint32(ax.Get()) * 240 / 65535)
dotY := int16(uint32(ay.Get()) * 135 / 65535)
```
```sh
tinygo flash -target nicenano ./basics/step6
```
A green dot follows the joystick across the display.
**Key concepts**
-`machine.InitADC()` — must be called once before using any ADC pin.
- The cast chain `uint32(ax.Get()) * 240 / 65535` avoids integer overflow during the mapping (a plain `int16` multiplication would overflow).
---
### step7 — Rotary encoder
**Goal:** use the quadrature encoder to cycle through hue values on the LEDs.
A rotary encoder outputs two square waves (A and B) 90° out of phase. The `encoders` driver decodes the direction and accumulates a signed position value using interrupts.
- Turn the encoder knob → LED hue shifts smoothly.
- Press the encoder knob → resets position to zero (both LEDs snap back to the same starting hue).
**Key concepts**
-`Precision: 4` — number of encoder pulses per detent; adjust if your encoder feels too coarse or too fine.
-`enc.Position()` returns a signed `int32`; casting to `uint8` wraps around naturally (256 → 0), which is exactly what we want for the hue wheel.
---
### step8 — Buzzer
**Goal:** generate audio tones by toggling the buzzer pin at audio frequencies.
The buzzer is passive: it only makes sound when driven with an alternating signal. We create a tone by toggling the pin HIGH/LOW at the target frequency.
The half-period in microseconds equals `1_000_000 / (2 * freq_Hz)`, but here `freq` is passed directly as the half-period value in microseconds, so `freq=1046` means a half-period of 1046 µs ≈ 478 Hz. Use this table to pick a value:
| Note | Approx. half-period (µs) |
|------|--------------------------|
| C5 | 523 |
| F#5 | 739 |
| C6 | 1046 |
```sh
tinygo flash -target nicenano ./basics/step8
```
Each button plays a different note while held.
**Challenge:** compose a short melody by chaining `tone()` calls with `time.Sleep` pauses between them.
Open any online MIDI player (e.g. [muted.io/piano](https://muted.io/piano/)) or connect to a DAW. The three buttons play C4, E4, and G4 (a C major triad).
**Key concepts**
- MIDI NoteOn/NoteOff must be paired: always send NoteOff for the previous note before sending a new NoteOn, otherwise notes get stuck.
- Velocity (last parameter, `50`) controls how hard the note is "hit" (0–127).
**Challenge:** map the rotary encoder position to an octave shift (transpose all notes up or down by 12 semitones per detent).
**Goal:** use the joystick as a mouse pointer and buttons as mouse clicks.
The ADC center resting position (~32767) produces a raw offset of 0. A dead zone filters out the natural jitter around center so the cursor stays still when you're not touching the stick.
The nice!nano's nRF52840 chip has built-in Bluetooth Low Energy. These examples use the [`tinygo.org/x/bluetooth`](https://tinygo.org/x/bluetooth) library.
Connection state is tracked via `adapter.SetConnectHandler`, which receives real events from the nRF52840 SoftDevice. The handler only sets flags — display and advertising calls happen in the main loop to avoid re-entering the SoftDevice from its own event callback.
-`adapter.AddService` must be called **before**`adv.Start()` on nRF52840 — the GATT table is frozen once advertising starts.
-`adapter.SetConnectHandler` — the correct way to track connection state; `txChar.Write()` always returns `nil` on nRF52840 regardless of whether a central is connected.
- Keep BLE callbacks short and flag-only — calling SoftDevice functions (like `adv.Start()`) or SPI ops from within a SoftDevice event handler causes a deadlock.
**Goal:** receive data from a mobile app and use it to set the LED color.
A custom service exposes a single writable characteristic. The central writes 3 bytes `[R, G, B]`; the badge lights both WS2812 LEDs immediately and shows the color + RGB values on the display.
The `WriteEvent` callback handles the color update directly (SPI and GPIO — no SoftDevice re-entry). Connection tracking uses the same flag pattern as step1.
`adapter.Scan` is a blocking call that drives its own internal event loop via `sd_app_evt_wait` — the nRF52840 SoftDevice primitive for waiting on BLE events. While inside this loop, **TinyGo's cooperative scheduler never gets CPU time**, so goroutines spawned to call `adapter.StopScan()` after a timeout never run.
Running `adapter.Scan` and SPI display operations concurrently causes a second problem: the SoftDevice can hold interrupts during event processing, and TinyGo's SPI driver waits for a DMA-completion interrupt using `wfe` (Wait For Event). If the SoftDevice consumes that wake-up event, the SPI transfer hangs forever.
The solution for both problems is to call `adapter.StopScan()`**from inside the scan callback** using `time.Since`:
Device names and addresses are stored as fixed-size byte arrays, not `string` fields. Strings returned by `result.LocalName()` and `result.Address.String()` may point into SoftDevice-managed buffers that are recycled after the callback returns; accessing them later from the main loop causes memory corruption.
-`adapter.Scan` runs its own internal `sd_app_evt_wait` loop — it never yields to TinyGo's cooperative scheduler. A goroutine that calls `StopScan()` after a `time.Sleep` will never execute while `Scan` is running.
- On nRF52840, **never run SPI and `adapter.Scan` at the same time**. The SoftDevice can consume the WFE wake-up that TinyGo's SPI driver needs to detect DMA completion, causing the SPI bus to hang indefinitely. Always stop the scan before doing any display update.
- **BLE callbacks must not store strings** that point into SoftDevice buffers. Use fixed-size `[N]byte` arrays and `copy()` in callbacks; convert to `string` only in the main loop.
-`result.LocalName()` returns the advertised name, if any. Devices that don't advertise a name are shown by their MAC address (`result.Address.String()`).