Beginner / 45 min
Getting started with code and digital logic
Getting started with embedded projects can feel like learning several things at once. There's the board, the wiring, the programming language, and whatever tool is supposed to put your program onto the board. Each part is manageable on its own. Put them together before you understand what they do, though, and a blinking light can become a surprisingly frustrating project.
That's what we're going to work through here. We'll start with a small program, connect it to the basics of digital logic, and use CircuitPython to make a light respond to a button. Along the way, you'll learn enough to understand why the project works and how to change it.
You can read this page without any hardware. For the project on the next page, we'll use a BREAD 2040, a breadboard, an LED, a resistor, and a pushbutton. No previous programming experience is required.
What is an embedded program doing?
A microcontroller is a small computer with a processor, memory, and peripherals for interacting with other devices. A development board adds the connections and supporting circuitry that make that chip easier to use. BREAD 2040, for example, puts an RP2040 microcontroller on a board you can connect to your computer over USB and wire into a breadboard circuit.
An embedded program often repeats three jobs: read an input, make a decision, and update an output. A thermostat reads a temperature and decides whether to turn on heating. A test fixture checks a board and reports whether it passed. Our project reads a button and controls a light.
The projects differ in complexity, but that pattern gives us a useful starting point.
Give a value a name
Python lets us store values under names called variables. Here's a small program you can try later in CircuitPython's serial console:
blink_delay = 0.5
blink_count = 4
message = "Let's build something."
ready = True
print(message)
print(blink_delay * blink_count)
blink_delay holds a decimal number, blink_count holds an integer, and message holds text, called a string. ready holds a Boolean: either True or False. Capitalization matters in Python.
The = sign assigns a value to a name. It doesn't ask whether two values are equal; that comparison uses ==. print() is a function that sends information to the console. The two calls above print the message and then 2.0.
Names help us explain our intentions. If the light blinks too slowly, changing blink_delay is easier to understand than hunting through a program for an unexplained number.
Make a decision
An if statement runs a block of code when its condition is true:
button_pressed = True
system_enabled = False
if button_pressed and system_enabled:
print("Turn the light on")
else:
print("Keep the light off")
This prints Keep the light off. The button is pressed, but the system isn't enabled. Both conditions must be true for and to produce True.
The colon starts a block, and indentation tells Python which instructions belong to it. Use four spaces for each level. Indentation is part of the language, so moving a line can change what the program does.
Three logical operations show up frequently in embedded projects:
andis true when both conditions are true. A machine might require a closed cover and an enabled control before operating.oris true when at least one condition is true. Either of two buttons could turn on an indicator.notreverses a Boolean.not TrueisFalse, andnot FalseisTrue.
We can show every combination of two Boolean inputs in a truth table. Read 0 as false and 1 as true:
A B | A and B | A or B | not A
0 0 | 0 | 0 | 1
0 1 | 0 | 1 | 1
1 0 | 0 | 1 | 0
1 1 | 1 | 1 | 0
These ideas also exist in digital hardware as logic gates. A processor executes our Python instructions over time. A physical logic gate responds to its inputs after a small propagation delay. When you eventually explore FPGAs, you'll use these same ideas to describe connected hardware that can operate concurrently.
Repeat something, then give it a job
A loop repeats a block of code. This one prints the numbers zero, one, and two:
for count in range(3):
print(count)
range(3) starts at zero and stops before three. A while loop instead repeats while a condition remains true. You'll often see while True: in embedded programs because a board is expected to keep checking its inputs until it loses power or resets.
Functions let us name a reusable job:
def should_light(button_pressed, system_enabled):
return button_pressed and system_enabled
print(should_light(True, True))
print(should_light(True, False))
The values inside the parentheses are inputs to the function. return sends its result back to the caller. This prints True, then False. A function becomes particularly useful when the same decision appears in several places or when you want to test it separately from the wiring.
How does a Boolean become a voltage?
GPIO means general-purpose input/output. An output pin drives an electrical signal; an input pin measures a signal and reports its logical state.
On the RP2040 boards used here, GPIO uses 3.3-volt logic. A low output is near ground, and a high output is near 3.3 volts. An input recognizes ranges of voltages as low or high, with an undefined region between them. Digital doesn't mean that the physical world contains only two exact voltages. It means the circuit interprets acceptable voltage ranges as two states.
A bit is one binary digit, 0 or 1. Eight bits make a byte. Numbers, text, and instructions can all be represented with groups of bits, but you don't need to write everything in binary to start programming.
A few electrical details matter right away:
- Connect circuits to a common ground so their signals share a reference.
- Keep these GPIO signals between ground and 3.3 volts. USB's 5-volt power connection is not a suitable GPIO signal.
- Use a series resistor with an ordinary LED to limit current. We'll use 1,000 ohms, also written as 1 kΩ.
- Use a suitable driver circuit for motors, relays, and other loads that need more current. A GPIO pin is a signal connection with limited drive capability.
An unconnected input can float, meaning its voltage isn't reliably high or low. A pull-up resistor gently brings an input toward the logic supply. A button can then connect that input to ground when pressed. The result is predictable: released reads high, pressed reads low.
That makes our button active-low. We'll use not to turn its electrical reading into a more convenient meaning: True when pressed.
Where CircuitPython fits
CircuitPython runs Python code on supported microcontrollers. You install firmware built for your particular board, then normally edit a file called code.py on the board's CIRCUITPY USB drive. Saving a change usually restarts the program, which makes experimentation quick. Adafruit's CircuitPython introduction explains this workflow in more detail.
CircuitPython is a useful place to start because you can make small changes and see their effects immediately. It has memory and timing limits, though. A Python loop with a delay is fine for a human-operated indicator; precise high-speed signaling may need hardware peripherals, RP2040 PIO, or a different implementation.
For now, we have everything we need to connect a decision in software to a real circuit.