EMBEDDED C SERIES · 24 lessons

Embedded C, at the register level — 24 lessons.

A step-by-step 24-lesson track: basics → 8051 → STM32 → applied. Control SFR·CMSIS registers directly without HAL/SPL, learning GPIO·timer·interrupt·UART·ADC·PWM bit by bit. Verify pure logic with PC `gcc` and hardware behavior in the Keil μVision simulator — follow along even without a board. Covers firmware patterns like debouncing, ring buffers, and HAL abstraction, plus a capstone project.

View source repository ↗
🧰

Basics

5 lessons · Keil · types/radix · bit ops · pointers/MMIO · functions

🔌
#01

01. Embedded C & Getting Started with Keil μVision

Basics · Prereq: none

C on a PC and C on a microcontroller use the same language but a different mindset. On a tiny chip with no OS and no screen, code drives the hardware directly. We learn what embedded C is, how Keil μVision (C51 / MDK-ARM) builds·debugs·simulates code, and the standard embedded program skeleton (main + while(1)). You can follow along with PC gcc even without a board.

🔌
#02

02. Data Types · Radix · stdint · sizeof

Basics · Prereq: lesson 1

An MCU's registers have exact widths (8/16/32-bit) and RAM is only a few KB. "Exactly how many bits is this variable?" decides whether it fits a register. We look at the reality of types (their width): why stdint.h's fixed-width types are the de-facto embedded standard, radix notation, sizeof, and the wrap-around when an unsigned value exceeds its max — all verified with PC gcc.

🔌
#03

03. Bit Operations & Register Control (set·clear·toggle)

Basics · Prereq: lesson 2

Driving hardware ultimately means turning specific register bits on·off·reading them — while never touching the other bits (a neighbor may control a different pin). We learn the bit operators (& | ^ ~ << >>) and the three golden idioms set(|=)·clear(&= ~)·toggle(^=), plus test(>> &), with PC gcc. These idioms are reused verbatim for 8051·STM32 register control from lesson 6.

🔌
#04

04. Pointers · volatile · Memory-Mapped I/O

Basics · Prereq: lesson 3

In embedded, a register is just a specific memory address (e.g. STM32 GPIOC->ODR is 0x4001100C). Write to that address and a pin moves; read it and you get the pin state — that's memory-mapped I/O (MMIO). We practice pointers (to handle addresses) and volatile (to safely read hardware-changed values) with PC gcc. The (*(volatile uint32_t *)addr) pattern is the foundation of STM32 control from lesson 14.

🔌
#05

05. Functions · Header Splitting · #define Macros

Basics · Prereq: lesson 4

Firmware is split into modules — "the GPIO part", "the UART part", "the app logic". The tools are functions (units of action), header(.h)/source(.c) separation (interface/implementation), and the preprocessor (#include·#define·include guards). We extract bit logic into a ledbank module and let main use only its interface, covering include guards and the macro-parenthesis trap. This modular sense carries through lesson 23 (HAL abstraction).

📟

8051

7 lessons · SFR · GPIO · timer · interrupt · UART · 7-segment

🔌
#06

06. 8051 Architecture · SFR · reg52.h

8051 · Prereq: lesson 5

On the foundation of lessons 1–5 we finally step onto a real chip — the classic 8051 (AT89C52). Its peripherals (ports·timers·serial·interrupts) are all controlled through SFRs (Special Function Registers). Keil C51's <reg52.h> pre-declares the SFRs by name, so we touch hardware as if assigning to a variable: P1 = 0x0F;. We learn the two ways to write a port (whole port / sbit single bit).

🔌
#07

07. GPIO Output — Blinking LEDs

8051 · Prereq: lesson 6

Embedded's "Hello, World" is blinking an LED. What value lights it (active-low/high wiring), how long to wait (delay), and how to make patterns with multiple LEDs (bit masks) — the essence of GPIO output is all here. We make a running light on 8 LEDs at P1 and observe the pin waveform in μVision's Logic Analyzer.

🔌
#08

08. GPIO Input — Reading Buttons/Switches

8051 · Prereq: lesson 7

Lesson 7 sent signals out; now we read them in. The 8051's quirk: its ports are quasi-bidirectional, so you must write 1 to a pin before reading it as input. We read a P3.2 button to control a P1.0 LED, and implement edge-detection that toggles on each press. Contact bounce (chatter) is covered in lesson 21.

🔌
#09

09. Timer0/1 and Precise Delays

8051 · Prereq: lesson 8

Lesson 7's software delay is imprecise and ties up the CPU. Precise time comes from a hardware timer — a counter that ticks independently of the CPU. We set 8051's Timer0/1 to 16-bit mode (Mode1) for exact delays. The keys: TMOD for mode, a reload value (65536−count) in TH/TL, and polling the overflow flag (TF). Reload math is verified on PC.

🔌
#10

10. External & Timer Interrupts

8051 · Prereq: lesson 9

Polling is precise but ties up the CPU. With interrupts, main does its work and, when an event (button·timer overflow) occurs, the CPU briefly jumps into an ISR and returns. "React only when something happens" is the core embedded paradigm. We enable interrupts via IE, write ISRs with C51's interrupt N syntax, and learn why shared variables need volatile.

🔌
#11

11. UART Serial Communication

8051 · Prereq: lesson 10

To exchange human-readable text with a PC you need communication; the most basic is UART (serial) — two wires (TXD/RXD) carrying text. UART is asynchronous, so both sides must agree on the baud rate. The 8051 sets serial mode via SCON, baud via Timer1, and moves a byte through SBUF. We send a string at 9600bps·8N1 and learn why 11.0592MHz is used for accuracy.

🔌
#12

12. 7-Segment Display (Multiplexing)

8051 · Prereq: lesson 11

Now we show human-readable digits. A 7-segment uses 7 bar LEDs (a~g) + a dot to draw 0–9. The keys: ① a lookup table mapping a digit to "which bars to light", and ② multiplexing (dynamic drive) that shows multiple digits while saving pins. We light "1234" one digit at a time, very fast, so persistence of vision makes all four appear lit at once.

🔌

STM32

8 lessons · CMSIS · GPIO · SysTick · PWM · EXTI · USART · ADC

🔌
#13

13. Cortex-M · CMSIS · Clock Tree

STM32 · Prereq: lesson 12

We graduate from the 8051 to the 32-bit Arm Cortex-M (STM32F103). Thanks to the CMSIS standard, any Cortex-M is accessed similarly at the register level. This track controls registers directly, without HAL/SPL. We lay three foundations for the next seven lessons — Cortex-M structure, the CMSIS header (stm32f10x.h), and the clock tree (HSE→PLL→SYSCLK→AHB/APB) — and adopt the iron rule "enable the peripheral clock via RCC before using it".

🔌
#14

14. RCC · GPIO Registers for LED

STM32 · Prereq: lesson 13

We put lesson 13's foundations (CMSIS·RCC·clock) to work for the first time: STM32's "Hello, World" — blinking an LED with registers only, no HAL. The Blue Pill's onboard LED is on PC13, active-low. The key is STM32F1's distinctive GPIO config — 4 bits per pin (MODE 2 + CNF 2) written to CRL(pins0–7)·CRH(pins8–15). Understand this 4-bit combo and every F1 GPIO setup follows the same pattern.

🔌
#15

15. GPIO Input · Pull-up/Pull-down

STM32 · Prereq: lesson 14

Lesson 14 sent signals out; now we read them in. A classic trap: a floating input — an unconnected pin is neither 0 nor 1 and wobbles with noise. Pull-up/pull-down resistors fix this, and STM32 has them built in. We set PA0 as input + internal pull-up, read the button via IDR, and control the PC13 LED. The F1 quirk — pull-up/pull-down is selected by ODR — is the key.

🔌
#16

16. SysTick Timer and Precise Delays

STM32 · Prereq: lesson 15

An empty-loop delay() is imprecise. The Cortex-M core's built-in SysTick timer solves it — a 24-bit counter present in every Cortex-M, so the same code runs across chips. The core pattern: "interrupt every 1ms → bump a global millisecond counter → measure time from it". This one pattern covers precise delays, periodic execution, and timeouts. We also build a delay_ms safe against wrap using unsigned subtraction.

🔌
#17

17. General-Purpose Timer · PWM Output

STM32 · Prereq: lesson 16

General-purpose timers (TIM) add channels·PWM·input capture. PWM is the most used — rapidly adjusting the on-time ratio (duty) controls LED brightness or motor speed "like analog". We make 1kHz PWM on TIM2 channel1 (PA0) and adjust the duty. Understand the three core registers PSC(prescale)·ARR(period)·CCR(duty), and the trap that an APB1 timer clock is 2× PCLK1.

🔌
#18

18. NVIC Interrupts and EXTI

STM32 · Prereq: lesson 17

Polling a button ties up the CPU and can miss short inputs. With interrupts, main does its work and jumps immediately to a handler (ISR) on an event, then returns. The NVIC governs interrupts on Cortex-M, and EXTI turns external pin changes into interrupts. We take a PA0 button as an EXTI0 interrupt to toggle an LED, and learn to always clear the pending flag (PR) in the ISR.

🔌
#19

19. USART Serial Communication

STM32 · Prereq: lesson 18

Now the chip talks to people·PCs in text. UART (universal async serial) is everywhere — debug messages, sensor modules, bootloaders. We set USART1 to 9600bps, send a greeting, and echo received characters back. μVision's Serial window shows the traffic directly, making UART the easiest peripheral to verify in the simulator.

🔌
#20

20. ADC Analog Input

STM32 · Prereq: lesson 19

Every pin so far has been digital (0/1). Most real-world signals — temperature·brightness·sound — are continuous analog. An ADC (Analog-to-Digital Converter) turns analog voltage into numbers the MCU can use. With STM32F103's 12-bit ADC we read PA0's voltage as 0–4095 and convert to millivolts. As the last of the STM32 peripheral series (13–20), it uses clock·pin·polling together.

🧩

Applications

4 lessons · debounce/FSM · ring buffer · HAL · capstone

🔌
#21

21. Debouncing · Finite State Machine (FSM) Patterns

Applied · Prereq: lesson 20

With peripherals learned, we move to firmware patterns. The first two appear in nearly every project — debouncing and finite state machines (FSM). A mechanical button's contacts bounce for a few ms on press (chatter), so one press reads as many. Debouncing filters the jitter to keep only real input; an FSM expresses behavior cleanly with "state+input→next state". It's pure logic with no hardware, verified exactly with gcc.

🔌
#22

22. Ring Buffer and Interrupt-Driven UART Receive

Applied · Prereq: lesson 21

When UART data pours in, a receive interrupt fires per byte. Handling each fully inside the ISR makes it long, and a slow main lets the next byte overwrite the previous — data loss. The fix: the ISR only enqueues; the idle main does the processing. The key data structure is the ring buffer. If producer (ISR) and consumer (main) touch different indices, it's safe without disabling interrupts.

🔌
#23

23. HAL Abstraction · Module Separation · Portability

Applied · Prereq: lesson 22

If register code (GPIOC->BRR=...) is scattered through app logic, switching chips means rewriting everywhere. A HAL (Hardware Abstraction Layer) splits "what to do" (turn on the LED) from "how" (write a specific register). We run the same app.c against three HAL implementations — PC (printf), STM32 (registers), and Arduino Uno (AVR ATmega328P) — and see the app code stay unchanged even as the architecture switches from ARM to AVR.

🔌
#24

24. Capstone — Control LED·Timer via UART Commands

Applied · Prereq: lesson 23

The final capstone. We weave together the pieces — USART (19)·interrupts (18)·ring buffer (22)·SysTick (16)·GPIO (14)·command parsing/modularity (22·23) — into one small firmware. Type on/off/blink/help in a PC terminal and the board parses it to control the LED and timer. The design key is role separation — the interrupt only receives, main only processes, the parser only parses. Each module is independently testable, and the command parser is verified on PC.