Beginner / 45 min

Build a light that listens

BREAD 2040 development board with its USB-C port, RP2040 microcontroller, and labeled GPIO connections

A blinking LED is useful because it checks several things at once. Your board has power, your program is running, and a pin is controlling something you can see. Adding a button gives us a complete input-and-output project with only a few more connections.

We'll use a BREAD 2040 for the steps below. CAST-AWAY RP2040 also exposes the D0 and D1 names used here, but use its own CircuitPython firmware and check its physical pin locations. Other boards may use different names or reserve those pins for other hardware.

Gather the parts

The headers must make reliable electrical contact. Loose pins pushed through unsoldered board holes won't give you a dependable circuit.

We're using an external LED deliberately. BREAD 2040 has an addressable RGB LED, which needs a data protocol and a library. It isn't controlled like an ordinary LED connected to a GPIO pin. RPGA Feather's onboard RGB LED is controlled by its FPGA, so this example shouldn't be redirected to that LED either.

Install CircuitPython

Back up any files already on your board before replacing its firmware.

  1. Open the BREAD 2040 CircuitPython download page and download its stable .uf2 release. For CAST-AWAY, use the separate CAST-AWAY RP2040 download page. These may appear under the earlier Oak Development Technologies name.
  2. Disconnect USB. Hold the board's BOOT button while reconnecting USB, then release it. The RP2040 bootloader should appear as a drive named RPI-RP2.
  3. Copy the downloaded .uf2 file onto RPI-RP2. The board restarts, and a different drive named CIRCUITPY should appear.
  4. Open a plain-text code editor, such as Mu or an editor you already use. Save the following program as code.py directly on CIRCUITPY, not inside a subfolder.
print("Hello from the board!")

Open your editor's CircuitPython serial console to see the message. If the console was opened after the message printed, press Ctrl+D to reload the program. Ctrl+C stops the running program and opens the interactive prompt, called the REPL. There, import board followed by dir(board) shows the pin names available in your installed firmware.

If you need help connecting the console, follow Adafruit's serial console setup instructions. Start with this small program before adding wiring. It gives you a known working point to return to.

Wire the LED

Unplug USB before making connections. Put the board across the breadboard's center gap so the two header rows aren't connected together. Check which holes your breadboard connects; power rails may be split halfway along their length.

Make this series circuit:

BREAD 2040 D0 -> 1 kΩ resistor -> LED anode (+)
LED cathode (-) -> BREAD 2040 GND

The LED's longer lead usually identifies its anode. The flat edge on the package usually identifies the cathode; check the part's documentation if its leads have been trimmed. The resistor can go on either side of the LED as long as it stays in series.

For a typical red LED with roughly a 2-volt forward drop, the resistor current is approximately (3.3 - 2.0) / 1000, or 1.3 milliamps. That's enough for a visible indicator without asking the pin to supply much current.

Make it blink

Reconnect USB and replace code.py with this complete program:

import time
import board
import digitalio

led = digitalio.DigitalInOut(board.D0)
led.switch_to_output(value=False)

blink_delay = 0.5

while True:
    led.value = True
    time.sleep(blink_delay)
    led.value = False
    time.sleep(blink_delay)

import makes a module available to the program. board supplies pin names, digitalio controls digital pins, and time provides the delay. These modules are built into this board's firmware; this project needs no additional library downloads.

DigitalInOut creates an object representing our pin. switch_to_output() configures it to drive a signal, initially low. The loop drives it high for half a second, then low for half a second. One full cycle takes about one second, plus a little execution overhead.

Try changing blink_delay to 0.1. Predict the result before saving. Then set it back. Changing one thing at a time makes it easier to connect your code to what the circuit does.

The digitalio API reference documents the pin configuration methods and Boolean values we're using.

Add the button

Unplug USB again. Keep the LED circuit in place and add these connections:

BREAD 2040 D1 -> one side of the pushbutton
Other side of the pushbutton -> BREAD 2040 GND

Many four-leg tactile buttons have two pairs of legs that are permanently connected within each pair. Pressing connects the pairs together. Use one connection from each pair, checking with a continuity meter or the button's datasheet. Connecting two legs from the same pair makes the input look permanently pressed.

Reconnect USB and replace code.py with this complete program:

import time
import board
import digitalio

led = digitalio.DigitalInOut(board.D0)
led.switch_to_output(value=False)

button = digitalio.DigitalInOut(board.D1)
button.switch_to_input(pull=digitalio.Pull.UP)

system_enabled = True

while True:
    button_pressed = not button.value

    if button_pressed and system_enabled:
        led.value = True
    else:
        led.value = False

    time.sleep(0.01)

The internal pull-up holds D1 high when the button is released. Pressing the button connects it to ground, so button.value becomes False. Applying not gives us button_pressed = True. The if statement then turns on the LED if the system is also enabled.

Hold the button and the light should stay on. Release it and the light should turn off. Change system_enabled to False, save, and try again. The button still changes the input, but our decision keeps the output off. That's the truth table from the first page doing useful work.

The short sleep spaces out our checks. It isn't a precise timer or a complete debounce solution. Mechanical contacts can bounce between states during a press. That rarely matters for this hold-to-light project, but a press counter or toggle switch needs debouncing so one press doesn't become several events. CircuitPython's keypad module is one option to explore for that next project.

When something doesn't work

Check the simplest possible failure first. A data cable and a charging-only cable can look identical.

If you later add sensors or displays, their drivers usually go in the lib folder on CIRCUITPY. Use the CircuitPython library bundle matching your firmware's major version. Our programs above use built-in modules, so missing third-party libraries aren't a possible cause here.

Choosing a board for what comes next

The useful part of this project is how much carries forward. You've assigned values, repeated instructions, made a decision, configured an output, and read an active-low input. A sensor-controlled indicator uses the same structure. So does a small test fixture, with more inputs and more careful checks.

If you're choosing a board from the Kavrous shop, start with the project you want to build:

The shop also carries IcyBlue Feather V2 for a more focused FPGA path. It isn't a board for running this CircuitPython project. Read its product page's documented 1.2-volt regulator limitation and workarounds before choosing it for an FPGA design.

You don't need to learn every interface or choose your forever board today. Get this small circuit working, explain to yourself why the input reads low when pressed, and change the program until you can predict its behavior. That gives you a foundation you can use when the next project has a display, a sensor, or a whole row of outputs.

That's the kind of progress we want to support at Kavrous: hardware you can understand, tools you can build with, and a clear path to trying something more ambitious.

Return to page 1: Programming and digital logic.