Embedded Systems Tutorials: From Basics to a Project

Build an embedded learning path through C/C++, GPIO, timers, interrupts, interfaces and debugging, with a compiled example and proposed projects.

An embedded system combines hardware and software to perform a defined job inside a device. Learning it means connecting project requirements to a board, C/C++ code, GPIO, timing, interfaces and fault handling. This guide organizes LCENJ's embedded systems tutorials around observable behavior, so a program that starts successfully can develop into a project you can debug and evaluate.

1. Define the job before choosing ESP32 or STM32

Describe a small project: its inputs, outputs, sampling interval and response to a disconnected sensor. Then list pins, interfaces, memory, wireless needs, power and development tools. Those requirements are more useful than choosing solely by CPU frequency or family name.

The ESP32 versus STM32 guide connects selection to the application. An ESP32-S3, classic ESP32 and a particular STM32 do not share identical pins or peripheral limits. Record the complete MCU or module number, board schematic and SDK version. Check programming, boot and memory pins before assigning them to a project.

If supply voltage, resistance or logic levels are unclear, begin with the electronics learning guide and 3.3 V and 5 V interfaces. These exercises are a development pathway, not instructions to replace firmware on an existing production device.

2. Learn enough C/C++ to explain your program

Start with types, conditions, loops, functions and arrays. Next, learn scope, object lifetime, pointers and references. A driver should distinguish an error from a legitimate zero reading. Make units explicit in names or documentation; an elapsed time and a raw ADC count are not interchangeable quantities.

Fixed-width types such as uint8_t and uint32_t help describe protocol data. Check bits, bytes, signedness and byte order against the device documentation. A cast, overflow or incorrect array length can turn compilable code into incorrect behavior.

Begin with a small example and a known library version. If it fails, record the target board, dependencies, build error and pin configuration before replacing the entire toolchain. A successful build is useful evidence, but it does not establish that the sensor is wired correctly.

3. GPIO is a real electrical interface

An input reads a logic level; an output drives a signal within specified current and voltage limits. An input without a defined path can float. Choose pull-ups or pull-downs to match the circuit and the actual pin. Mechanical contact bounce should not be interpreted as a sequence of deliberate state changes.

A digital pin does not make a motor or relay a suitable direct load. Study MOSFET switching and relay flyback paths for a power stage. For LED control, distinguish PWM frequency and duty cycle from changing a steady DC voltage.

Complete this stage when you can explain every pin's startup, normal and fault state and reconcile the wiring with the board schematic. Do not copy one example's GPIO number to a different board without checking it.

4. Scheduling, hardware timers and interrupts

List the work the program must perform and how long each job can wait. A well-structured main loop is enough for many initial exercises. The millis scheduling tutorial separates periodic work without delay calls; that alone does not provide hard real-time guarantees.

This example prints a counter about once a second. It has no external pin or sensor dependency. Unsigned subtraction handles ordinary counter rollover, provided checks do not pause long enough to miss a complete counter cycle. Setting lastReport = now intentionally avoids printing a burst of overdue reports.

#include <Arduino.h>
#include <stdint.h>

constexpr uint32_t REPORT_INTERVAL_MS = 1000;
uint32_t lastReport = 0;
uint32_t reportCount = 0;

void setup() {
  Serial.begin(115200);
}

void loop() {
  const uint32_t now = millis();
  if (uint32_t(now - lastReport) >= REPORT_INTERVAL_MS) {
    lastReport = now;
    Serial.print("Report: ");
    Serial.println(++reportCount);
  }
  // Add short, bounded foreground work here.
}

This sketch compiled successfully for Arduino Uno with Arduino AVR Boards 1.8.8: 1,768 bytes of program storage and 204 bytes of global data. It was not uploaded or hardware-tested. Avoiding delay does not make every operation non-blocking: serial output can wait for buffer space. This is not a hard real-time scheduler or a guarantee of exact message spacing.

For tighter timing, examine hardware timers, execution time and interrupt latency in the exact MCU's documentation. Keep long work, logging and unbounded waits out of an ISR. With an RTOS, use ISR-appropriate APIs and a defined data-transfer mechanism such as a queue. The volatile qualifier alone does not establish synchronization or atomic operations.

5. Connect UART, I2C, SPI and 1-Wire deliberately

InterfaceWhat to establishExisting exercise
UARTVoltage levels, TX/RX direction, baud rate and frame format. Logic-level UART is not the same electrical interface as RS-232 or RS-485.Interface comparison
I2CAddress, SDA/SCL, pull-ups and power. An address scan does not validate the measurement.SHT31 with CRC; I2C troubleshooting
SPIChip select, clock mode, bit order and device-specific requirements.Choosing I2C, SPI or UART
1-WirePull-up, sensor identifier, power mode and completed conversion.DS18B20 without blocking the loop

Start with one device and short wiring, then add the actual load, cable and other devices. Return paths and waveform quality still matter. Matching protocol names on two products does not prove electrical or timing compatibility.

6. Valid data, state machines and RTOS boundaries

A sensor sample should carry a value, validity status and acquisition time. After a failed read, do not label the previous value as a fresh sample. The DS18B20 tutorial handles a disconnected-device result separately; the SHT31 example checks transfer integrity with CRC. CRC does not establish sensor calibration.

Define states such as ready, waiting for conversion, valid data and fault. Give each an entry condition, exit condition and bounded wait. This makes behavior easier to follow than scattered delays and implicit flags. Establish the local data path before adding wireless communication.

An RTOS can help organize independent concurrent work, but priorities, queue capacity, data ownership and queue-full behavior still need design. Calling a function a task does not solve concurrency. Consult the official FreeRTOS queue interfaces and the version included by your SDK.

7. Debug from evidence

  1. Define a repeatable failure: the triggering event, observed result and expected behavior.
  2. Check power, reset, clocks and pin connections. For load-related resets, follow the brownout guide.
  3. Record driver errors with a timestamp and status. Compare protocol waveforms with software logs when necessary.
  4. Change one relevant variable at a time and record the board and firmware revision.
  5. Find the cause of unstable readings before adding a filter.

Do not remove a watchdog merely to hide a stalled program. Find the unbounded wait, deadlock or resource problem. For wireless behavior, BLE analysis tools and performance and power parameters provide a separate diagnostic path.

Three proposed projects, from local sensing to connectivity

These are project plans. No new physical build or measured hardware result is claimed for this guide.

ProjectReading pathWhat to demonstrate
Temperature loggerDS18B20 plus periodic schedulingDistinguish fresh data, disconnection and stale data. Keep the loop available for other work during conversion.
Temperature and humidity monitorSHT31 plus installation and measurement errorsRecord I2C/CRC failures and reference-comparison conditions. Displaying a number is not sufficient evidence.
BLE sensor reportingOne local sensing project plus ESP32 peripheral and notification setupDefine a payload and validity status. Disconnection and reconnection must not relabel old data as a new sample.

A final record should include requirements, schematic and pins, exact parts, compiler and library versions, test procedure, logs and remaining limits. The BLE product-development roadmap provides further progression; physical acceptance remains a separate task for every actual project.

Continue into wireless, audio and low-power systems

For RF and IoT, establish a reliable local data path before adding message rate, payload size, disconnection handling and power requirements. The BLE fundamentals and communication performance and power tutorials are existing starting points. A module name or CPU frequency alone does not determine wireless range.

For audio, define sample rate, channels, bit depth, buffer format and each processing block's deadline as well as the interface. Queue overflow and underrun need explicit behavior. For a battery-powered project, bring wake time, sleep and communication current peaks into both firmware and power requirements.

To move toward a PCB or product, document the pin contract, hardware revision and recovery procedure. A software update needs more than a success path: interrupted power and an invalid image are also scenarios to evaluate. These are further specializations, not claims of implementations or completed tests in this guide.

Primary sources and further study

Arduino and ESP-IDF are different development frameworks. The example's build evidence covers only its declared target. Return to the article library for other topics or the electronics guide for circuit foundations. No new electrical simulation or hardware test was performed for this guide.