Skip to content

4.Embedded programming

Summary

What is Embedded Programming?

One kind of programming language used to create system code based on a microcontroller or microprocessor is called embedded programming. There are certain hardware requirements for this programming language. A common form of Internet of Things and electronic consumer applications are embedded systems, which are utilized inside industrial machinery, bicycle charging systems, and residential appliances.

Where is Embedded Programming Used?

Embedded programming is widely used across various industries. Industrial Automation: Used for production monitoring and machine control. Consumer Electronics: Found in devices like digital cameras, washing machines, and smart TVs. Internet of Things (IoT): Powers smart home appliances and connected devices. Automotive Systems: Plays a crucial role in airbags, ABS brakes, and engine control. Medical Equipment: Used in devices such as ventilators, insulin pumps, and heart rate monitors.

Group assignment

About Microcontroller

A circuit called a microcontroller is made to regulate particular operations in an embedded system. On a single chip, it combines a CPU, memory (RAM, ROM, and Flash), and input/output peripherals. Because of their low power consumption and real-time processing capabilities, microcontrollers are widely utilized in consumer electronics, automation, robotics, and the Internet of Things.

Microcontroller Comparison

  1. AVR Microcontrollers AVR microcontrollers are commonly used in small embedded systems and are based on the RISC (Reduced Instruction Set Computer) architecture. They are widely popular among beginners and hobbyists, often found in Arduino boards. Known for their low power consumption, simplicity, and ease of programming, AVR chips are a preferred choice for basic embedded applications.

  2. ARM Microcontrollers ARM microcontrollers are used in a wide range of high-performance applications. They are known for their scalability and power efficiency, making them ideal for industrial automation, automotive control, and mobile devices. The ARM Cortex-M family is widely utilized in various embedded systems.

  3. ESP Microcontrollers The ESP8266 and ESP32 are popular microcontrollers designed for Internet of Things (IoT) applications. These microcontrollers are ideal for cloud-based applications, wireless communications, and smart home automation due to their built-in Wi-Fi and Bluetooth capabilities. Among IoT projects, the ESP32 is widely used for its advanced features and versatility.

  4. STM32 Microcontroller STMicroelectronics' STM32 microcontrollers are based on the ARM Cortex-M core. They are widely used in automotive, medical, and industrial automation applications. These microcontrollers offer real-time processing, multiple peripherals, and high computational power, making them suitable for a variety of advanced embedded systems.

Reference by Search Engine

Toolchains and Development Workflows

The key instruments and procedures required for embedded system development are the subject of research on toolchains and development workflows. A compiler, assembler, linker, debugger, and integrated development environment (IDE) comprise a toolchain that facilitates the efficient writing, compilation, and debugging of code. Different toolchains are used by different microcontrollers, including AVR, ARM, ESP32, and STM32, to guarantee the seamless creation and operation of embedded applications. A clear workflow increases productivity, lowers errors, and boosts system performance as a whole.

Toolchain Components

Compiler: Transforms high-level code (C, C++) into machine code. Examples include GCC, Clang, IAR, and Keil. Assembler: Converts assembly language into binary instructions. Common assemblers include GNU Assembler and NASM. Linker: Combines compiled code and libraries to construct an executable. Examples include GNU LD and Keil Linker. Debugger: Enables step-by-step execution for troubleshooting. Popular debuggers include GDB, OpenOCD, and J-Link Debugger. Integrated Development Environment (IDE): Provides a complete development environment. Examples include VS Code, Eclipse, Keil, STM32CubeIDE, and Arduino IDE.

Development Workflow

Requirement Analysis: Select the appropriate hardware and define system objectives. Code Development: Write embedded code using C, C++, or Assembly. Compilation & Linking: Utilize the toolchain to convert source code into machine code. Flashing to Target: Use a programmer or debugger to upload the compiled application onto the microcontroller. Testing & Debugging: Employ tools like Serial Monitor, JTAG, or SWD for real-time debugging. Optimization: Enhance power efficiency, minimize memory usage, and improve overall performance. Maintenance & Deployment: Finalize and install the firmware on operational devices.

Example Toolchain for Microcontrollers

Which Tools Were Easier to Use?

I will perform two experiments, and for both experiments I will use two different microcontrollers: the ESP32 and the Arduino Uno. This will allow me to compare their performance, behavior, and signal quality under the same conditions.

Parameter ESP32-WROOM-32U Arduino Uno (ATmega328P)
Architecture 32-bit Xtensa LX6 (dual-core) 8-bit AVR
Clock Frequency 160–240 MHz 16 MHz
Number of Cores 2 1
RAM 520 KB SRAM 2 KB SRAM
Flash Memory 4 MB external SPI flash 32 KB flash
Connectivity Wi-Fi 802.11 b/g/n, Bluetooth, BLE No wireless
Antenna Type External U.FL antenna (WROOM-32U) No antenna
Operating Voltage 3.3V 5V
GPIO Pins Up to 34 multifunction GPIOs 14 digital, 6 analog
ADC Resolution 12-bit 10-bit
DAC Yes (2-channel) No
PWM Yes Yes
Communication Interfaces 3× UART, 3× SPI, 2× I2C, I2S, CAN UART, SPI, I2C
Power Consumption Medium/High Low
Programming Environment ESP-IDF, PlatformIO, Arduino IDE Arduino IDE
Typical Use Cases Wi-Fi devices, IoT, robotics, AI, real-time control Education, basic robotics, simple sensors

First Experiment

Arduino uno

I compared the output frequency of an Arduino Uno using two different programming environments. The first test was written in C++ using the Arduino IDE, and the second test was done in Python using the Firmata library. In both cases, I generated a square-wave signal on a GPIO pin and measured its frequency using an oscilloscope.

Arduino

What is Arduino Wiring?

Arduino Wiring is the simplified programming framework used to write code for Arduino boards. It is based on C/C++, but provides easier functions and abstractions so beginners can program microcontrollers without needing deep embedded-systems knowledge.

Wiring introduces simple functions such as:

  • pinMode()

  • digitalWrite()

  • digitalRead()

  • analogRead()

  • analogWrite()

It also defines the basic program structure:

  • setup() — runs once at startup

  • loop() — runs repeatedly

In short:

  • Arduino Wiring = simplified C/C++ layer for microcontroller programming.

What is the Arduino IDE?

The Arduino IDE (Integrated Development Environment) is the official software used to write, compile, and upload Wiring-based programs to Arduino boards.

Key features:

  • Code editor

  • Compiler and uploader

  • Serial Monitor

  • Library manager

  • Board and port selection

  • The IDE translates Wiring code into C/C++ and compiles it into machine code that runs on the microcontroller.

In short:

  • Arduino IDE = software used to write and upload Arduino Wiring programs.

I wanted to measure the output frequency of the Arduino using Arduino Wiring, so I opened the Arduino IDE and wrote a simple test program to generate a square-wave signal.

Here is code

const int OUT_PIN = 9;

void setup() {

  tone(OUT_PIN, 1000);   
}

void loop() {

}

After uploading the code, I connected the board to the oscilloscope and measured the output waveform. The maximum stable frequency I achieved using Arduino Wiring in the Arduino IDE was approximately 950 Hz, which is significantly faster than the Python + Firmata implementation.

Python

What is Python?

Python is a high-level, general-purpose programming language known for its simple syntax and readability. It is widely used in fields such as:

  • Embedded systems

  • Automation

  • Data science

  • Web development

  • Artificial intelligence

Python is interpreted rather than compiled, which makes it easy to test, modify, and run code quickly. However, because it runs through an interpreter, it is generally slower for real-time applications compared to C/C++ on microcontrollers.

In short:

  • Python = simple, powerful, and flexible programming language, but not as fast for direct hardware control.

What is Firmata?

Firmata is a communication protocol used to control microcontrollers such as Arduino from a computer. Instead of uploading a custom program to the Arduino, you upload the StandardFirmata firmware once, and then send commands from a host computer (e.g., Python) to control the pins.

Firmata allows you to:

  • Read and write digital pins

  • Read analog inputs

  • Control PWM outputs

  • Communicate over serial

Because commands must travel between the computer and the Arduino, Firmata is slower than direct C/C++ programming. This makes it useful for rapid prototyping or testing, but not ideal for high-speed or time-sensitive applications.

In short:

  • Firmata = protocol that lets you control Arduino from a computer, but introduces communication delays.

First, I opened the StandardFirmata example in the Arduino IDE (File → Examples → Firmata → StandardFirmata). Then I selected the correct board profile: Tools → Board → Arduino AVR Boards → Arduino Uno. With the example loaded and the board set to Uno, the IDE was ready to upload the Firmata firmware. After selecting the board, I uploaded the StandardFirmata firmware to the Arduino Uno successfully.

I am using Python 3.10, so I decided to work in VS Code. To communicate with the Arduino using Firmata, I installed the Firmata-related Python library via the terminal command.

pip install pyFirmata

from pyfirmata import Arduino
import time

PORT = '/dev/cu.usbmodem1201'  
board = Arduino(PORT)

OUT = board.get_pin('d:9:o')   

def square(f_hz, seconds):

    T2 = 0.5 / f_hz
    t_end = time.perf_counter() + seconds
    level = 0
    while time.perf_counter() < t_end:
        level ^= 1
        OUT.write(level)
        time.sleep(T2)  

try:
    print("Генерация ~10 Гц 5 секунд…")
    square(10, 5)
    print("Генерация ~50 Гц 5 секунд…")
    square(50, 5)
    print("Генерация ~100 Гц 5 секунд… (ожидай заметной ошибки)")
    square(100, 5)
finally:
    OUT.write(0)
    board.exit()

Then I ran the Python code, specifying the correct board port inside the script. This allowed my Python program to communicate with the Arduino through the Firmata protocol.

This Python + pyFirmata script generates a square wave on Arduino digital pin D9 via the Firmata protocol.

  • PORT = '/dev/cu.usbmodem1201' — serial port used to connect: Arduino(PORT).

  • OUT = board.get_pin('d:9:o') — configures D9 as a digital output.

  • square(f_hz, seconds):

  • Computes half-period T2 = 0.5 / f_hz (≈50% duty cycle).

  • Toggles the output 0/1 and sleeps T2 until the specified duration elapses.

In try: it runs three tests:

  • ~10 Hz (5 s), ~50 Hz (5 s), ~100 Hz (5 s).

In finally: it drives the pin low and closes the Firmata connection.

Notes on accuracy: frequency is limited by Python/OS/USB latency and the Firmata layer; at ~100 Hz and above you should expect noticeable error/jitter. The routine is blocking and keeps ~50% duty cycle.

VS Code — Python + Firmata Setup I opened my Python script in VS Code (Python 3.10 selected as the interpreter). In the editor, I set the correct serial port for the Arduino (e.g., /dev/cu.usbmodem1201) and ran the script from the integrated terminal. This setup let me generate square-wave signals via pyFirmata and monitor the print output directly in VS Code.

I connected the Arduino board to the oscilloscope to verify the output frequency from my Python + Firmata script. In practice, the maximum stable frequency I measured was ~11 Hz, which matches the expected limitation due to Python/USB/Firmata latency.

Second Experiment

ESP32

ESP32-WROOM-32U is a powerful Wi-Fi and Bluetooth module developed by Espressif Systems, based on the ESP32 microcontroller. The letter “U” means that this version includes a u.FL connector for an external antenna, which provides stronger and more stable wireless communication compared to the onboard antenna version.

Key Features:

Processor: Dual-core Xtensa LX6, up to 240 MHz

Memory: 520 KB SRAM

Wireless Connectivity: Wi-Fi 802.11 b/g/n and Bluetooth v4.2 (Classic + BLE)

Interfaces: GPIO, UART, SPI, I2C, PWM, ADC, DAC

Power Supply: 3.0–3.6 V

Antenna: External antenna via u.FL connector

In summary, the ESP32-WROOM-32U is an ideal module for IoT, robotics, and embedded projects that require high performance, wireless communication, and longer signal range using an external antenna. Datasheet click here.

Testing ESP32 Performance in Two Programming Environments

During this experiment, I wanted to compare the performance of the ESP32 microcontroller when programmed in two different environments — ESP-IDF (in VS Code) and Arduino IDE. The main goal was to find out which environment executes code faster and produces cleaner signals.

I wrote the same simple code in both environments — a digital output toggling on and off without any delay. By avoiding delays, I could measure the pure switching frequency of the microcontroller and determine which framework is more efficient at the low-level hardware level.

First, I uploaded the code using ESP-IDF through VS Code, then connected the ESP32 to an oscilloscope to observe the output signal. Next, I uploaded the exact same code through Arduino IDE and again checked the signal on the oscilloscope.

ESP-IDF

VS code witch ESP-IDF

Here is code

#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define GPIO_LED GPIO_NUM_25

static void init_hw(void) {
  gpio_config_t io_conf;
  io_conf.mode = GPIO_MODE_OUTPUT;
  io_conf.pin_bit_mask = (1ULL << GPIO_LED);
  io_conf.intr_type = GPIO_INTR_DISABLE;
  io_conf.pull_down_en = 0;
  io_conf.pull_up_en = 0;
  gpio_config(&io_conf);
}

void loop(void) {
  static int led_state = 0;
  led_state = !led_state;
  gpio_set_level(GPIO_LED, led_state);
}

void app_main() {
  init_hw();
  while (1) {
    loop();
  }
}

In this experiment, I wrote a simple ESP-IDF program to measure the switching frequency of the microcontroller. Instead of using a delay, I continuously toggled the GPIO pin (GPIO13) as fast as possible inside the main loop. By observing the output signal on the oscilloscope, I could determine the maximum frequency at which the ESP32 could switch the GPIO pin.

This experiment helped me understand the real-time performance of the ESP32 and how efficiently it can handle low-level hardware operations without software delays.

Arduino IDE

Here is code

void setup(){
 pinMode(25 , OUTPUT); 
}

void loop(){
  digitalWrite(25 , 1);
  digitalWrite(25 , 0);
}

In this micro-benchmark I wrote a minimal Arduino sketch that toggles GPIO25 as fast as possible without any delays. The goal is to observe the resulting square wave on an oscilloscope and infer the maximum practical toggle rate (i.e., how fast the MCU can switch a pin when using digitalWrite() in a tight loop).


From the oscilloscope readings, I noticed a clear difference:

the signal generated with ESP-IDF had a higher frequency and cleaner waveform, meaning it executed faster compared to the Arduino version. This shows that ESP-IDF provides better performance and more precise timing control because it works closer to the hardware and uses native APIs.

Although Arduino IDE is easier to use and perfect for quick prototyping, ESP-IDF is a more professional and optimized development framework, especially for large and performance-critical projects.

This test helped me understand the advantages of both environments - simplicity versus efficiency — and confirmed that for serious robotics or embedded system projects, ESP-IDF is the best choice.

Comparing Microcontroller Architectures: RISC-V vs AVR

This week, I decided to perform a detailed comparison between two microcontrollers based on different architectures — the Arduino Uno (AVR) and the ESP32-WROOM-32U (RISC-V architecture). My main goal was to understand the differences in performance, compilation speed, firmware uploading time, and execution efficiency when using various development environments.

When working with the Arduino IDE, I noticed that the entire workflow - including compilation, linking, and uploading the firmware to the microcontroller - was noticeably slower. The process takes more time because Arduino IDE uses a simplified, higher-level compilation pipeline that is optimized for ease of use rather than speed.

Then I switched to VS Code with the ESP-IDF framework (Espressif IoT Development Framework). Here, the workflow was completely different - the compilation was much faster, and the firmware upload to the ESP32 board took only a few seconds. The ESP-IDF environment provides a professional toolchain with CMake, Ninja, and direct hardware access, which makes it significantly more efficient for complex embedded projects.

To make the comparison even more complete, I also experimented with MicroPython on the ESP32. However, my observation is clear - Python is not suitable for professional microcontroller development. Although it’s great for quick prototyping, the execution speed of Python on microcontrollers is too slow, and it cannot handle tasks that require precise timing or real-time response. For professional embedded systems, timing accuracy and fast reaction are crucial — something that high-level interpreted languages cannot provide.

In conclusion, I can say that Arduino IDE is an excellent starting point for beginners to understand the basic principles of microcontrollers. But for professional applications, where performance and system control are essential, it is much better to work with low-level C or C++, use hardware registers, or rely on frameworks like ESP-IDF or STM32 HAL/LL for efficient embedded programming. This experiment gave me a deeper understanding of how microcontroller architectures and development tools affect both development speed and runtime performance.

Individual assignment

Nucleo F103RB

Nucleo F103RB is a development board by STMicroelectronics based on the STM32F103RB microcontroller (ARM Cortex-M3). It includes:

USB interface for programming and power,

Arduino-compatible pin headers,

ST-Link debugger built in.

It’s used for learning, prototyping, and developing embedded systems using STM32CubeIDE or Arduino IDE.

STM32 CudeMX

STM32CubeMX is an official software tool from STMicroelectronics that helps developers configure STM32 microcontrollers before programming them.

In short:

It is a graphical interface (GUI) where you can select a microcontroller, configure its pins, clock settings, and peripheral interfaces such as UART, SPI, and I2C.

It automatically generates C code for various IDEs, including STM32CubeIDE, Keil, and IAR.

It simplifies the configuration process and saves time during project setup.

In simple terms, STM32CubeMX is a configuration tool that allows you to easily set up an STM32 microcontroller and generate the necessary code for development.

CLion

CLion is a professional Integrated Development Environment (IDE) created by JetBrains for programming in C and C++. It offers powerful features such as:

intelligent code completion,

advanced debugging tools,

cross-platform project building (using CMake, Makefile, or Gradle),

code analysis and refactoring,

integration with Git, Docker, and other development tools.

CLion makes C/C++ development more convenient and efficient, especially when working on large-scale projects or microcontroller-based systems.

STM32 CudeProgrammer

STM32CubeProgrammer is an official software tool developed by STMicroelectronics for programming and managing the memory of STM32 microcontrollers.

With STM32CubeProgrammer, you can:

Flash firmware to STM32 devices via USB, UART, SWD, JTAG, or DFU;

Read and erase memory;

Modify Option Bytes and security settings;

Work with both internal and external flash memory;

Update firmware using either a graphical interface (GUI) or a command-line interface (CLI).

In simple terms, STM32CubeProgrammer is a universal tool for flashing, configuring, and maintaining STM32 microcontrollers.

These are the software tools I use when working with microcontrollers from STMicroelectronics.

How my MCU communicates (Nucleo-F103RB)

Overview

I use wired serial (UART) to communicate between the STM32F103RB on the Nucleo board and my computer.
The Nucleo’s on-board ST-LINK converts the MCU’s UART (USART2) to a USB Virtual COM Port (VCP), so I just connect a single USB cable to the Nucleo and open a serial terminal on the PC.

  • MCU peripheral: USART2
  • MCU pins: PA2 = TX, PA3 = RX
  • Bridge: ST-LINK V2-1 (on the Nucleo board)
  • PC side: USB (Virtual COM Port)
  • Serial settings: 115200 baud, 8 data bits, no parity, 1 stop bit (115200-8-N-1)

This setup lets me send data from the MCU to the PC (logs, sensor values) and receive commands from the PC (text commands, bytes).


Method for Sending Data (TX)

I transmit bytes/strings using the HAL UART driver in polling mode:

const char *msg = "Hello from Nucleo!\r\n";
HAL_UART_Transmit(&huart2, (uint8_t*)msg, strlen(msg), 100);

I created a running light effect using LEDs on the Nucleo-F103RB board.

I first created a new project in STM32CubeMX, configured the pins in output mode to connect the LEDs, and selected CMake as the toolchain.

This is the implementation of the running light effect based on the STM32 microcontroller — here is the result.

This here is code

#include "main.h"

int time = 20;

UART_HandleTypeDef huart2;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART2_UART_Init(void);

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART2_UART_Init();

  while (1)
  {
    HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, 1);
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2, SET);
    for (int i = 0; i < 4; i++) {
      if (i == 1) {
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, SET);
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, 0);
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, 0);
      }
      HAL_Delay(time);
      if (i == 2) {
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, SET);
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, 0);
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, 0);
      }
      HAL_Delay(time);
      if (i == 3) {
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, SET);
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, 0);
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, 0);
      }
      HAL_Delay(time);
    }
    for (int i = 0; i < 4; i++) {
      if (i == 1) {
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, 0);
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, 0);
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, 1);
      }
      HAL_Delay(time);
      if (i == 2) {
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, SET);
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, 0);
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, 0);
      }
      HAL_Delay(time);
      if (i == 3) {
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, 0);
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, 1);
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, 0);
      }
      HAL_Delay(time);
    }
  }
}
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI_DIV2;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL16;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}
static void MX_USART2_UART_Init(void)
{
  huart2.Instance = USART2;
  huart2.Init.BaudRate = 115200;
  huart2.Init.WordLength = UART_WORDLENGTH_8B;
  huart2.Init.StopBits = UART_STOPBITS_1;
  huart2.Init.Parity = UART_PARITY_NONE;
  huart2.Init.Mode = UART_MODE_TX_RX;
  huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart2.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart2) != HAL_OK)
  {
    Error_Handler();
  }
}
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

  HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET);

  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2, GPIO_PIN_RESET);

  GPIO_InitStruct.Pin = B1_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(B1_GPIO_Port, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = LD2_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(LD2_GPIO_Port, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
}

Code Description (STM32 HAL)


Overview

The program runs a “running light” (chaser) pattern across three LEDs on PB0, PB1, and PB2, first left-to-right and then right-to-left, continuously. The on-board LED LD2 is kept ON. A pushbutton (B1_Pin) is configured as an external interrupt input but is not used in the current logic.


Global Variables

  • int time = 20;
    Step delay for the LED sequence in milliseconds.
    Note: consider renaming to step_ms to avoid confusion with the standard time() function.

  • UART_HandleTypeDef huart2;
    UART2 handle (UART is initialized but not used in this program).


Initialization Functions

HAL_Init();

Initializes the HAL library, SysTick, and resets peripherals.

SystemClock_Config();

Configures system clock: HSI → PLL (HSI/2 × 16) to reach approximately 64 MHz.

MX_GPIO_Init();
  • Enables clocks for GPIOC, GPIOD, GPIOA, GPIOB.
  • Sets LD2 as push-pull output, initially RESET.
  • Sets PB0, PB1, PB2 as push-pull outputs, initially RESET.
  • Configures B1_Pin as external interrupt input on rising edge (GPIO_MODE_IT_RISING).
  • Enables EXTI15_10_IRQn in NVIC.
MX_USART2_UART_Init();

Initializes UART2 at 115200-8-N-1. Not used in the current code.


Button-controlled LED (step-by-step)

Idea / wiring

One button to GPIO with internal pull-up (pressed = LOW).

Three LEDs on GPIOs with series resistors.

Each valid press advances the state: LED1 → LED2 → LED3 → ALL OFF → repeat.

Debounce (≈30–50 ms) to ignore switch noise.

Algorithm

Read button.

If pressed and was previously released for > debounce time → state = (state + 1) % 4.

Update LEDs according to state.

This here is code

#include "main.h"

int count = 0;

UART_HandleTypeDef huart2;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART2_UART_Init(void);

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART2_UART_Init();

  while (1)
  {

    if (HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == 0) {
      count++;
      HAL_Delay(300);
    }
    if (count == 1) {
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, 1);
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, 0);
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, 0);
    }
    if (count == 2) {
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, 1);
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, 1);
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, 0);
    }
    if (count == 3) {
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, 1);
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, 1);
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, 1);
    }
    if (count == 4) {
      count = 0;
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, 0);
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, 0);
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, 0);
    }
  }
}
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI_DIV2;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL16;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}
static void MX_USART2_UART_Init(void)
{
  huart2.Instance = USART2;
  huart2.Init.BaudRate = 115200;
  huart2.Init.WordLength = UART_WORDLENGTH_8B;
  huart2.Init.StopBits = UART_STOPBITS_1;
  huart2.Init.Parity = UART_PARITY_NONE;
  huart2.Init.Mode = UART_MODE_TX_RX;
  huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart2.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart2) != HAL_OK)
  {
    Error_Handler();
  }
}
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  /* USER CODE BEGIN MX_GPIO_Init_1 */

  /* USER CODE END MX_GPIO_Init_1 */

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET);

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2, GPIO_PIN_RESET);

  /*Configure GPIO pin : B1_Pin */
  GPIO_InitStruct.Pin = B1_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(B1_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pin : LD2_Pin */
  GPIO_InitStruct.Pin = LD2_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(LD2_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pins : PB0 PB1 PB2 */
  GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
}

Code Description

This document explains what each part of the STM32 HAL code does.


Purpose of Each Function

  • HAL_Init(); Initializes the HAL library: configures the SysTick timer, resets peripherals, and prepares the MCU for operation.

  • SystemClock_Config(); Configures the system clock. Uses HSI with PLL (HSI/2 × 16) to achieve approximately 64 MHz for the CPU and peripherals.

  • MX_GPIO_Init(); Enables GPIO port clocks and configures pins.

Configured pins: - PB0, PB1, PB2 as output pins (used to control LEDs) - B1_Pin as external interrupt input, rising edge (GPIO_MODE_IT_RISING)

Also enables the EXTI interrupt line EXTI15_10_IRQn in NVIC.

  • MX_USART2_UART_Init(); Initializes UART2 with baud rate 115200, 8 data bits, 1 stop bit, no parity. UART is prepared for serial communication but is not used in this program.

Variables

  • int count = 0; Stores the number of button presses.

Main Loop

  • while (1) { ... } The program continuously checks the button state and controls LEDs according to the value of count.

Button Reading

HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) reads the button status.

If the pin is LOW (0), count is incremented. HAL_Delay(300) is used as a simple debounce.

LED Control Logic

LEDs on pins PB0, PB1, and PB2 are controlled based on count.

count value LED0 (PB0) LED1 (PB1) LED2 (PB2)
1 ON OFF OFF
2 ON ON OFF
3 ON ON ON
4 OFF OFF OFF (count resets to 0)

After count == 4, the count resets and all LEDs turn off.


Functions Used

  • HAL_GPIO_ReadPin() Reads the digital level of a pin.

  • HAL_GPIO_WritePin() Sets a GPIO pin to HIGH or LOW (turns LEDs on or off).

  • HAL_Delay() Blocking delay used for simple button debounce.


Simulator Wokwi

I worked with the Wokwi simulator, and I really liked it. You can quickly build a simple circuit, write the code, and instantly see the result. When you get an idea, you can test it within minutes.

Previously, I knew about Wokwi and other simulators like Tinkercad. Let me share a small story: when I was in the 9th grade, I really wanted to learn Arduino. I watched video tutorials, but realized that Arduino is hardware — to truly learn it, you need the physical board.

I saved money by myself, ordered an Arduino from China, and waited two months for it to arrive — shipping back then was slow. While waiting, I used Tinkercad. I found it incredibly exciting to build virtual circuits and write code.

When I finally received my first board and lit my first LED, I realized that this was my life path. Since then, I’ve sincerely wanted to become one of the strongest engineers in the world in embedded systems.

And I never knew you could write code using HAL in Wokwi — that’s really amazing!

I searched for “Wokwi” on Google. The first result shows the official website of Wokwi, an advanced STM32 simulator.

This is the component search panel in Wokwi. Here I can search and add components such as LEDs, pushbuttons, resistors, and OLED displays.

On the left is the code editor, and on the right is the STM32 circuit diagram. The highlighted buttons allow me to start and control the simulation.

After clicking the run button, Wokwi begins compiling the project. It shows compilation time and offers a paid upgrade to speed up building.

The simulation is running. The timer and performance indicator are shown at the top. LEDs connected to the STM32 board are blinking.

This here is code

const uint8_t LEDS[] = {2, 4, 7, 10};
const uint8_t N = sizeof(LEDS);

const uint16_t SPEED = 80;

void setup() {
  for (uint8_t i = 0; i < N; i++) {
    pinMode(LEDS[i], OUTPUT);
    digitalWrite(LEDS[i], LOW);
  }
}
void allOff() {
  for (uint8_t i = 0; i < N; i++) digitalWrite(LEDS[i], LOW);
}
void showOne(uint8_t idx) {
  allOff();
  digitalWrite(LEDS[idx], HIGH);
}
void effectBounce() {
  for (int i = 0; i < N; i++) {
    showOne(i);
    delay(SPEED);
  }
  for (int i = N - 2; i >= 1; i--) {
    showOne(i);
    delay(SPEED);
  }
}
void effectCounter() {
  for (uint8_t val = 0; val < (1 << N); val++) {
    for (uint8_t i = 0; i < N; i++) {
      bool on = (val >> i) & 0x01;
      digitalWrite(LEDS[i], on ? HIGH : LOW);
    }
    delay(200);
  }
}
void loop() {
  for (int k = 0; k < 6; k++) effectBounce();

  effectCounter();
}

Result video

Reflection

This week, I didn’t face any major challenges because I have over five years of experience working with microcontrollers. I first started learning about them back in school, and since then I’ve worked with several different types — from Arduino to more advanced platforms like STM32, where I programmed at the register level using professional development environments.

Thanks to this background, working with microcontrollers feels very natural to me. It allows me to understand both high-level frameworks like Arduino IDE and low-level embedded programming with ESP-IDF or STM32CubeIDE. This experience helped me complete all the weekly assignments efficiently and deepened my confidence in hardware programming and embedded systems development.

Conclusion

This week was smooth and productive since I already have over five years of experience working with microcontrollers. Starting from Arduino and progressing to STM32 and ESP32, I’ve learned to program both with high-level frameworks and directly through registers. This experience helped me complete all the tasks efficiently and deepened my understanding of embedded systems.

Download the code

Code for button here.

Code for light effect here.