ROBOTECA FREE SAMPLE
Dashboard

A free lesson from Embedded Systems & Electronics: the whole module, nothing cut short.

LESSON · Embedded Systems

Microcontroller anatomy & GPIO

Turn 1 30 min LESSON

ALearning Material

A microcontroller (MCU) is a whole computer on one chip: CPU + memory + peripherals, designed to control hardware. Unlike a PC (which boots an OS and runs apps), an MCU usually runs one program directly "on the bare metal."

‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍A microcontroller is a whole computer squeezed onto one chip (CPU, memory, and peripherals) built not to run apps but to control hardware. Unlike a PC that boots an operating system, an MCU usually runs a single program directly 'on the bare metal', which means you are much closer to the silicon and responsible for things a desktop hides from you.

The central idea that unlocks everything is memory-mapped peripherals: you control hardware by reading and writing special memory addresses called registers. Set a particular bit and a pin goes high; set another and a timer starts. The datasheet is the map from peripheral to address. GPIO is the simplest example, and even it teaches a fundamental gotcha. An input pin left unconnected floats and reads random noise, so it needs a pull-up or pull-down to give it a defined level. Registers, bits, and defined logic levels are the vocabulary of every peripheral that follows.

What's inside:

  • CPU core (e.g. ARM Cortex-M): executes your instructions.
  • Flash: non-volatile program storage (your code lives here).
  • RAM‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍: volatile working memory (variables); small, often KB.
  • Peripherals: hardware blocks: GPIO, timers, ADC, UART/SPI/I²C, PWM.

Memory-mapped peripherals: the central idea. You control hardware by reading and writing special memory addresses called registers. Setting a bit in a register can switch a pin, start a timer, or send a byte. The MCU's datasheet maps every peripheral to addresses.

GPIO (General-Purpose Input/Output) is the simplest peripheral: pins you can set HIGH/LOW (output) or read as HIGH/LOW (input). Each GPIO pin needs configuring:

  • Direction: input or output.
  • For inputs: often a pull-up or pull-down resistor so a disconnected pin reads a defined level instead of "floating" (random noise). A button to GND with a pull-up reads HIGH when released, LOW when pressed.

Two ways to write the same blink (Arduino-style vs register-level):

// High-level (Arduino) - readable, portable
pinMode(13, OUTPUT);
digitalWrite(13, HIGH);   // LED on

// Register-level (what's really happening on an AVR/STM32-like MCU)
DDRB  |= (1 << 5);        // set pin 5 of port B as output (Direction Register)
PORTB |= (1 << 5);        // drive it HIGH (Port Output Register)
PORTB &= ~(1 << 5);       // drive it LOW

‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍The |= (1 << n) / &= ~(1 << n) idiom (bit manipulation) sets/clears a single bit without disturbing the others, essential embedded vocabulary.

Syntax you need here. Two ways to reach a pin, and you need both: the portable one to get working, and the register one because that is what the datasheet documents and what a debugger shows you.

// THE PORTABLE FORM (Arduino and most vendor HALs offer an equivalent)
pinMode(pin, OUTPUT);            // OUTPUT, INPUT, INPUT_PULLUP
digitalWrite(pin, HIGH);         // HIGH or LOW
int v = digitalRead(pin);        // HIGH or LOW
analogRead(pin);                 // an ADC channel, not a digital pin

// THE REGISTER FORM: a peripheral register is a variable at a fixed address
#define GPIOA_MODER  (*(volatile uint32_t *)0x40020000)   // volatile: never cache it

// THE FOUR BIT OPERATIONS, and they are the whole vocabulary
reg |=  (1u << n);               // SET   bit n, leaving the others alone
reg &= ~(1u << n);               // CLEAR bit n
reg ^=  (1u << n);               // TOGGLE bit n
if (reg & (1u << n)) { }         // TEST  bit n

// a multi-bit FIELD: clear the field first, then write it. Two steps, always.
reg &= ~(0x3u << (2*n));         // clear the 2-bit field for pin n
reg |=  (0x1u << (2*n));         // then write the new value into it

volatile is not decoration. It tells the compiler that this location can change without any code in your program writing to it, so it must be re-read every time rather than kept in a register. Omit it and a loop polling an input pin is optimised into a loop that reads once and spins on the stale value forever, which compiles cleanly and is nearly impossible to see in the source.

‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍Read-modify-write rather than assignment is the other habit. Writing reg = (1u << n) sets your bit and clears every other bit in that register, which is how one pin's configuration silently deconfigures the pin next to it. |= and &= ~ touch only what you named.

Why it exists. A microcontroller is the brain inside almost every robot, tool, and appliance. A whole computer on one chip that runs your code directly on the bare metal. The first mental shift is from "a program" to "flipping hardware bits at memory addresses."

Mental model. Registers are a wall of labelled light switches wired to the hardware. Flipping switch #5 in the "direction" panel makes a pin an output; flipping #5 in the "output" panel turns the actual pin on. Your code just flips switches.

Common misunderstandings.

  • "PORTB = (1<<5) is fine to set one pin." Plain = overwrites all the bits; use |= / &= ~ to change one bit without disturbing the others.
  • "An unconnected input pin reads 0." It floats and reads random noise. Give it a pull-up or pull-down for a defined level.
  • ‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍"The MCU runs an operating system like a PC." Usually not. It runs one bare-metal program directly; your code is the system.

Connections. Memory-mapped registers and bit manipulation are the foundation for interrupts and timers (next lesson), the Turn-2 STM32-peripherals lesson, and the button+UART project. Pull-ups reappear on the I2C bus (Turn 2) and the PCB project, and the fixed-rate loop from Python Turn 1 becomes a timer interrupt here.

BImmediate Active Recall

QUERY

How does a microcontroller control its hardware peripherals?

REVEAL
ANSWER

Through memory-mapped registers: special addresses where writing/reading bits configures and operates peripherals (GPIO, timers, etc.).

Did you recall it?
QUERY

What does PORTB |= (1 << 5) do, and why use |= instead of =?

REVEAL
ANSWER

Sets bit 5 of PORTB to 1 (drives that pin high). |= changes only that bit, leaving the other pins untouched; = would overwrite all bits.

Did you recall it?
‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍QUERY

Why does a digital input pin need a pull-up or pull-down resistor?

REVEAL
ANSWER

An unconnected ("floating") input picks up noise and reads random HIGH/LOW. A pull resistor ties it to a defined level so it's stable when nothing drives it.

Did you recall it?
QUERY

Difference between Flash and RAM on an MCU?

REVEAL
ANSWER

Flash is non-volatile program storage (your code persists without power); RAM is volatile working memory for variables (lost on power-off), and is usually much smaller.

‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍Did you recall it?

CConceptual Questions

Answer each in your own words in the box, then reveal the model answer to compare. These ask why, not how, and your answers are saved.

PROMPT

What does 'memory-mapped peripherals' mean, and why is it the central idea of microcontroller programming?

REVEAL MODEL ANSWER
‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍MODEL ANSWER

Hardware blocks (GPIO, timers, UART) are controlled by reading and writing special memory addresses called registers. Setting a bit in a register can flip a pin, start a timer, or send a byte, and the datasheet maps every peripheral to its addresses. So controlling hardware reduces to writing the right bits to the right memory locations. That single mechanism underlies every peripheral you'll ever use.

Compared to the model answer - did you get it?
PROMPT

Why does a floating (unconnected) input pin need a pull-up or pull-down resistor?

‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍REVEAL MODEL ANSWER
MODEL ANSWER

An undriven CMOS input is high-impedance and picks up electrical noise, so it reads a random, fluctuating HIGH/LOW, 'floating'. A pull-up or pull-down resistor gently ties the pin to a defined level when nothing else drives it. For example, a button to ground with a pull-up reads HIGH when released and LOW when pressed, a deterministic, noise-immune reading.

Compared to the model answer - did you get it?
PROMPT

Comparing Arduino's digitalWrite to the register-level DDRB/PORTB version, what does the high-level call hide?

‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍REVEAL MODEL ANSWER
MODEL ANSWER

It hides the direct register writes: configuring the data-direction register (DDR) to make the pin an output, then setting or clearing the corresponding bit in the port output register to drive it HIGH or LOW. The Arduino call is readable and portable, but underneath it's just bit manipulation of memory-mapped registers, which is what's really happening on the chip.

Compared to the model answer - did you get it?

DPractice Problems

P1 (easy). Write the bit operation to clear bit 3 of register PORTC without affecting other bits.

P2 (medium). A button wired between a pin and GND reads random values when not pressed. What's missing, and which level will the pin read when the button ‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍is pressed?

P3 (harder). Work out the bit patterns and show what each write actually leaves in the register.

Starting from PORTB = 0b10100011 (pins 0, 1, 5 and 7 driven high by other parts of the program), compute the resulting register for PORTB = (1 << 5), PORTB |= (1 << 5), PORTB &= ~(1 << 5) and PORTB ^= (1 << 5). Produce the table in binary. Then write the four idioms as named macros, and state the one hazard read-modify-write has that a plain write does not, with the fix.

Solutionsclick to reveal

P1. PORTC &= ~(1 << 3);. (1<<3) is 0b1000; ~ inverts it to ...11110111; ANDing forces only bit 3 to 0.

‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍P1Compared to this solution - did you get it right?

P2. A pull-up resistor (enable the internal pull-up, or add an external one to VCC). With a pull-up, the released button reads HIGH; pressing it connects the pin to GND, so it reads LOW (active-low input).

P2Compared to this solution - did you get it right?

P3. Starting state.

PORTB = 0b10100011        pins 7, 5, 1, 0 high; pins 6, 4, 3, 2 low
        ||||||||
        76543210

‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍The four writes.

Operation Result What happened
PORTB = (1 << 5) 0b00100000 pins 7, 1 and 0 were cleared: the whole register was overwritten
PORTB \|= (1 << 5) 0b10100011 bit 5 set (it already was); everything else preserved
PORTB &= ~(1 << 5) 0b10000011 bit 5 cleared; everything else preserved
PORTB ^= (1 << 5) 0b10000011 bit 5 toggled, so it went high to low; everything else preserved

The first row is the bug. PORTB = (1 << 5) is a write, not a modification: it assigns 0b00100000 to the entire port, so every other output on port B is driven low in the same instruction.

What that means physically: if pin 0 was an enable line, the driver is now disabled; if pin 1 was a chip select, an SPI transaction is aborted mid-word; if pin 7 was a relay, the relay drops out. ‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍Three unrelated subsystems fail because one LED was turned on, and nothing in the source mentions them.

And it is intermittent in the worst way. The program works while nothing else uses port B and breaks when a colleague adds a pin, in a function neither of you is looking at.

The four idioms, named.

#define BIT(n)            (1u << (n))
#define SET_BIT(reg, n)   ((reg) |=  BIT(n))     // drive high,  leave others alone
#define CLR_BIT(reg, n)   ((reg) &= ~BIT(n))     // drive low,   leave others alone
#define TGL_BIT(reg, n)   ((reg) ^=  BIT(n))     // invert,      leave others alone
#define GET_BIT(reg, n)   (((reg) >> (n)) & 1u)  // read one bit
SET_BIT(PORTB, 5);        // instead of PORTB |= (1 << 5)
CLR_BIT(PORTB, 5);

~BIT(n) is the part that is easy to get wrong. &= ~(1 << 5) clears bit 5 by ANDing with 0b11011111‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍; writing &= (1 << 5) instead clears everything except bit 5, which is the same class of error as the first row.

The hazard read-modify-write has that a plain write does not: it is not atomic.

PORTB |= BIT(5) compiles to three instructions on most architectures: load the register, OR the bit, store it back. If an interrupt fires between the load and the store and that ISR also writes to PORTB, the ISR's change is overwritten by the store that was already in flight.

main:  load PORTB   -> 0b10100011
                        <-- INTERRUPT: the ISR does SET_BIT(PORTB, 3), PORTB = 0b10101011
main:  OR  bit 5    -> 0b10100011
main:  store        -> 0b10100011        the ISR's bit 3 is GONE

A lost pin change, at random, once every few thousand interrupts. It is untraceable from the source, because both pieces of code are individually correct.

The three fixes, in order of preference.

Use the hardware's atomic bit-access if it has one. ARM Cortex-M3 and M4 provide ‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍bit-banding, a memory region where each word maps to one bit, so a single store changes one bit atomically. STM32 also has BSRR, a set/reset register where writing a bit sets a pin and no read is involved at all:

GPIOB->BSRR = (1u << 5);          // atomic set,   no read-modify-write
GPIOB->BSRR = (1u << (5 + 16));   // atomic clear, no read-modify-write

‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍Disable interrupts around the sequence, if no atomic access exists:

uint8_t s = SREG; cli();
PORTB |= BIT(5);
SREG = s;                          // restore, do not blindly sei()

Saving and restoring the flag matters: calling sei() at the end re-enables interrupts even if the caller had deliberately disabled them.

Or partition the pins, so that one port is written by the ISR and another by main, and the conflict cannot arise. ‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍That is the cheapest fix of all when the board layout is still yours to choose.

P3Compared to this solution - did you get it right?

EFeynman Exercise

Explain to a beginner what a microcontroller register is, using the "wall of labelled light switches" analogy. Then explain why we flip just one switch (|=/&=) instead of resetting the whole panel, and what "a floating input" means using the idea of a switch wired to nothing.

‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍REVEAL MODEL ANSWER
MODEL ANSWER

Think of a microcontroller's registers as a wall of labelled light switches, each at a fixed address. Flipping the switch at one address turns on a pin; flipping another starts a timer. Programming the chip is really just walking up to the right switch (memory address) and toggling the right toggle (bit). And a switch with nothing wired to it doesn't sit reliably up or down. It flutters in the breeze (a floating input), which is why you tie it down with a little resistor so it has a definite resting position.

Compared to the model answer - did you get it?

FError Analysis Framework

  • Overwriting a whole register. Why: using = not |=/&=. Recognise: unrelated pins change. ‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍Avoid: read-modify-write single bits.
  • Floating inputs. Why: no pull resistor. Recognise: random reads. Avoid: enable pull-up/down; know active-high vs active-low.
  • Wrong direction register. Why: setting output value before direction. Recognise: pin won't drive. Avoid: configure direction first, then value.
  • Bit-position off-by-one. Why: counting pins from 1 not 0. Recognise: wrong pin toggles. Avoid: check datasheet bit numbering (usually 0-based).

GMini Challenge

A button wired from an MCU pin to ground reads erratically. Sometimes 'pressed' when it isn't. Name the cause and the one-component fix, and state what level the pin reads when pressed versus released once it's fixed.

‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍REVEAL MODEL ANSWER
MODEL ANSWER

The cause is a floating input: with the button open, nothing drives the pin, so it picks up noise and reads randomly. The fix is a pull-up resistor (often the MCU's built-in internal pull-up). With a pull-up and the button to ground, the pin reads HIGH when released (pulled up to VCC) and LOW when pressed (the button shorts it to ground), a clean, deterministic reading.

Compared to the model answer - did you get it?

Quiz Check

A quick auto-graded check, separate from the recall cards above. Your score is pooled with the recall cards into this module's Mastery score, and completing this lesson requires the quiz submitted with pooled mastery at 80% or above.

‍​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​‌​‌‌​​‌​‌​​‌​‌‌​‌​‌‌‌​​‌‌​‌‌​​​​‌​‌‌​‌‌​‌​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​‌​‌‍QUIZAuto-graded check · feeds your mastery score
  1. A microcontroller controls its peripherals through:

  2. PORTB |= (1 << 5) does what, and why |=?

  3. An unconnected (floating) input pin:

  4. Your program code is stored in:

This is a free sample

Progress and the spaced-repetition reviews are part of the course. The full track continues from here.