Chapter 1: The Minimum You Need to Know Before Starting
You do not need an electrical engineering degree to program microcontrollers. But you do need to understand a few things about electricity, or you will destroy a chip. This chapter is your crash course.
Electricity in 5 Minutes
Three quantities govern everything:
| Quantity | Unit | Symbol | Analogy |
|---|---|---|---|
| Voltage | Volts (V) | V | Water pressure in a pipe |
| Current | Amperes (A) | I | Amount of water flowing |
| Resistance | Ohms (Ohm) | R | How narrow the pipe is |
They are related by Ohm's Law, the single most important equation in electronics:
V = I x R
If you know any two, you can calculate the third. You will use this constantly.
There is one more relationship you need — power:
P = V x I
Power is measured in Watts (W). It tells you how much energy a component consumes (and how much heat it generates). An STM32 running at full speed draws about 100 mA at 3.3V, which means P = 3.3 x 0.1 = 0.33 Watts. Barely warm to the touch.
Fun Fact: STM32 microcontrollers run at 3.3V. This is important. Not 5V like old Arduinos, not 1.8V like some ultra-low-power chips. 3.3 volts. Burn this number into your brain.
Analog vs. Digital
The physical world is analog — temperature varies smoothly, light dims gradually, sound is a continuous wave. Microcontrollers are digital — they think in ones and zeros.
A digital pin has two states:
| State | Voltage | Meaning |
|---|---|---|
| HIGH | 3.3V | Logic 1, ON, True |
| LOW | 0V (GND) | Logic 0, OFF, False |
That is it. A GPIO pin is either 3.3V or 0V. There is no "sort of on."
So how do we bridge the gap?
- ADC (Analog-to-Digital Converter) — reads a smooth analog voltage and converts it to a number. A 12-bit ADC on STM32 gives you values from 0 (0V) to 4095 (3.3V). This is how you read sensors.
- PWM (Pulse Width Modulation) — fakes an analog output by switching a digital pin on and off very fast. If a pin is HIGH 50% of the time, a motor connected to it sees roughly half the voltage. This is how you dim LEDs and control motor speed.
Think About It: If your ADC reads a value of 2048, what voltage is that? (Hint: 2048 is exactly half of 4096, and your reference voltage is 3.3V.)
Components You Will Actually Use
Resistors
A resistor limits current flow. That is its only job. You will use them constantly for:
- Current limiting — protecting LEDs and GPIO pins from drawing too much current
- Pull-up / pull-down — giving floating pins a defined state (more on this later)
- Voltage dividers — scaling voltages down to safe levels
Resistor values are marked with colored bands. You do not need to memorize the color code — just use a multimeter or look up a color band calculator online. Common values you will use: 220 Ohm, 1K Ohm, 4.7K Ohm, 10K Ohm, 100K Ohm.
Capacitors
Capacitors store small amounts of charge. In embedded systems, their most critical role is bypass (decoupling) capacitors.
Warning: Every STM32 chip requires 100nF ceramic capacitors between each VDD pin and GND. These are not optional. Without them, the chip will behave erratically — random resets, corrupted data, peripherals misbehaving. Every development board has them already soldered on. If you ever design your own PCB, this is rule number one.
LEDs
LEDs (Light Emitting Diodes) have polarity — they only work in one direction. The longer leg is the anode (positive), the shorter leg is the cathode (negative).
LEDs also require a current-limiting resistor. Without one, the LED will draw as much current as it can, overheat, and die — possibly taking your GPIO pin with it.
For a typical LED at 3.3V with a 2V forward voltage and 10mA desired current:
R = (3.3V - 2V) / 0.010A = 130 Ohm (use 150 Ohm or 220 Ohm, the nearest standard values)
Crystal Oscillators
These provide the clock signal that drives the microcontroller. Most STM32 boards have an 8 MHz crystal (HSE — High Speed External). The chip multiplies this internally using a PLL to reach its full speed (e.g., 8 MHz x 12 = 96 MHz on an F411).
Voltage Regulators
These convert one voltage to another. Your USB port provides 5V, but the STM32 needs 3.3V. A voltage regulator on the board handles this conversion. The popular AMS1117-3.3 is on almost every dev board.
How Not to Destroy Your Hardware
These rules are simple. Follow them and your chips will live long, productive lives.
- Never apply more than 3.3V to a GPIO pin. Some pins are 5V-tolerant (check the datasheet), but 3.3V is always safe.
- Never drive motors, relays, or solenoids directly from a GPIO pin. A GPIO pin can source about 20 mA. A small motor draws 200-500 mA. Use a transistor or motor driver.
- Always share GND. When connecting two devices, their ground pins must be connected together. No common ground, no communication.
- Always use current-limiting resistors with LEDs. The 10 seconds you save skipping the resistor is not worth the dead LED and possibly dead GPIO.
Voltage Dividers
A voltage divider uses two resistors to reduce a voltage proportionally:
V_in ---[R1]---+---[R2]--- GND
|
V_out
The formula:
V_out = V_in x R2 / (R1 + R2)
Practical Example: You want to measure a 12V car battery with your 3.3V ADC.
You need V_out = 3.3V when V_in = 12V:
3.3 = 12 x R2 / (R1 + R2)
Using R1 = 27K Ohm and R2 = 10K Ohm:
V_out = 12 x 10000 / (27000 + 10000) = 12 x 0.27 = 3.24V
Close enough, and safely under 3.3V even with some margin.
Think About It: What happens if R2 is much larger than R1? What if R1 is much larger than R2? Think about the formula and the extremes.
Pull-Up and Pull-Down Resistors
When a GPIO pin is configured as an input but nothing is connected to it, it is floating — its voltage drifts randomly between HIGH and LOW. This causes unpredictable behavior.
A pull-up resistor (typically 10K Ohm) connects the pin to 3.3V, giving it a default HIGH state. A pull-down resistor connects it to GND, giving it a default LOW.
Most STM32 pins have internal pull-up and pull-down resistors that you can enable in software:
#![allow(unused)] fn main() { use embassy_stm32::gpio::{Input, Pull}; // Enable internal pull-up — pin reads HIGH when nothing is connected let button = Input::new(p.PA0, Pull::Up); // Enable internal pull-down — pin reads LOW when nothing is connected let sensor = Input::new(p.PB5, Pull::Down); }
The internal pull-ups are weak (around 40K Ohm). For I2C communication, you must use external pull-up resistors (typically 4.7K Ohm on both SDA and SCL lines). The internal ones are too weak for I2C's open-drain signaling.
Binary, Hexadecimal, and Bits
Microcontroller registers are 32 bits wide. You need to be comfortable with binary and hexadecimal.
| Decimal | Binary | Hex |
|---|---|---|
| 0 | 0000 | 0x0 |
| 1 | 0001 | 0x1 |
| 2 | 0010 | 0x2 |
| 3 | 0011 | 0x3 |
| 4 | 0100 | 0x4 |
| 5 | 0101 | 0x5 |
| 6 | 0110 | 0x6 |
| 7 | 0111 | 0x7 |
| 8 | 1000 | 0x8 |
| 9 | 1001 | 0x9 |
| 10 | 1010 | 0xA |
| 11 | 1011 | 0xB |
| 12 | 1100 | 0xC |
| 13 | 1101 | 0xD |
| 14 | 1110 | 0xE |
| 15 | 1111 | 0xF |
Each hex digit represents exactly 4 bits. So a 32-bit register like 0x4001_0014 is really 32 binary digits grouped into 8 hex digits.
Rust makes working with these representations clean:
#![allow(unused)] fn main() { let binary_value = 0b0000_0000_0010_0000; // bit 5 set let hex_value = 0x0020; // same thing let decimal = 32; // same thing again // Rust lets you use underscores for readability let register_addr = 0x4002_0400; // GPIOB base address let big_number = 1_000_000; // one million, easy to read }
Fun Fact: The
0bprefix for binary literals is a Rust feature you will not find in standard C. It makes register manipulation dramatically more readable.
Bitwise Operations: The Language of Registers
Registers are controlled one bit at a time. Here are the four operations you will use constantly:
Set a Bit (Turn ON)
#![allow(unused)] fn main() { // Set bit 5 (turn on pin 5) register |= 1 << 5; // If register was 0b0000_0000, it becomes 0b0010_0000 }
Clear a Bit (Turn OFF)
#![allow(unused)] fn main() { // Clear bit 5 (turn off pin 5) register &= !(1 << 5); // If register was 0b0010_0000, it becomes 0b0000_0000 }
Check a Bit (Read state)
#![allow(unused)] fn main() { // Check if bit 5 is set if register & (1 << 5) != 0 { // Bit 5 is HIGH } }
Toggle a Bit (Flip state)
#![allow(unused)] fn main() { // Toggle bit 5 — if it was ON, turn OFF; if OFF, turn ON register ^= 1 << 5; }
These four patterns — OR to set, AND-NOT to clear, AND to check, XOR to toggle — are the fundamental vocabulary of hardware programming. Embassy abstracts most of this away, but understanding what happens underneath will save you when debugging.
Think About It: Why do we use
1 << 5instead of just writing0b0010_0000? What if you need to set bit 13, or bit 27? Shifting is easier to read and less error-prone for arbitrary bit positions.
Summary
You now know enough electronics to safely connect components to an STM32 and enough binary math to understand what registers are doing. None of this needs to be memorized — you will internalize it through practice.
In the next chapter, we will look at what a microcontroller actually is, what is inside it, and why it is different from the computer you are reading this on.