Week 4: Embedded Programming

This page

Before starting

Embedded system

An embedded system is an electronic system designed to perform a specific function within a larger system, integrating hardware and software to control, monitor, or automate a process. Unlike a general-purpose computer, an embedded system is dedicated to a concrete task and usually operates continuously and autonomously. It typically receives signals from sensors, processes them through a program, and generates outputs to actuators such as motors, valves, or displays, often working under real-time constraints. Embedded systems are widely used in household appliances, vehicles, medical equipment, industrial systems, drones, and IoT devices, where reliable control, fast response, and low power consumption are required.

Microcontroller

A microcontroller is the main component of many embedded systems: it is an integrated circuit that combines a processor, memory, and input/output peripherals on a single chip. It is designed to execute control programs directly on external hardware, allowing it to read sensors, manage communications, and generate signals such as PWM or ADC without the need for a full computer. Thanks to its high level of integration and low cost, microcontrollers are the foundation of most modern electronic controllers. They are commonly programmed in C or C++ and are ideal for automatic control applications and smart devices. For more information, visit the week 4 group page of fab26

              The embedded system is the entire control solution, built to perform one dedicated function reliably.
Fab termi
IT INCLUDES IS DESIGNED FOR
Control hardware Continuous operation
Control software (firmware) Deterministic behavior
Sensors and actuators Real-time response
Power and protection circuitry High reliability
The microcontroller is the central brain of the embedded system. It executes the firmware and coordinates all operations.
Fab termi
TYPICALLY INTEGRATES ROLE
CPU core Reads inputs
Flash memory Runs decision logic
RAM Produces output signals
GPIO Manages timing and communication
ADC
PWM
NVS
Timers or counters
Communication peripherals
Input subsystem provides real-world data to the controller. Their purpose is convert physical variables into electrical signals usable by the microcontroller.
Fab termi
COMPONENTS SUPPORT
Analog sensors Signal conditioning
Digital sensors Voltage dividers
Potentiometers and joysticks Protection resistors
Buttons and keypads Isolation when needed
Output subsystem executes physical actions based on controller commands.
Fab termi
LIKE CHARACTERISTICS
Motors and servomotors Often require more current or voltage than a microcontroller pin can supply
Solenoids and valves Frequently driven through interface stages
Relays and contactors
Displays and indicator LEDs
Buzzers and alarms

ESP FAMILY

The ESP family of microcontrollers, developed by Espressif Systems, is designed for networked embedded systems and IoT applications where integrated wireless connectivity is required. These devices combine a microcontroller, RF front-end, memory interfaces, security accelerators, and multiple digital and analog peripherals in a single chip, significantly reducing external component count and system cost.
The most widely used line is the ESP32 series, which includes multiple variants with different CPU architectures (Xtensa or RISC-V), core counts, memory capacities, wireless standards, and security features. Across the family, typical capabilities include Wi-Fi, Bluetooth LE or Classic (depending on variant), hardware cryptography, DMA, ADC/PWM/timer peripherals, and native USB in newer models. This makes ESP devices suitable for connected sensors, automation nodes, edge devices, human-machine interfaces, and low-power distributed control systems.
FRAMEWORK / ENVIRONMENT MAIN LANGUAGES TYPICAL USE
ESP-IDF (official) C / C++ Professional and production firmware
Arduino Core for ESP C++ (Arduino style) Rapid prototyping and education
MicroPython Python Fast experimentation
PlatformIO C / C++ Advanced project workflow
ESPHome YAML + C++ backend Home automation nodes
Zephyr RTOS C RTOS-based systems
Rust for ESP Rust Safety-critical / modern embedded

Main Variants of the ESP32 Family

ESP32

The baseline and most widely adopted variant.
Main characteristics:
  • Dual-core Xtensa LX6 CPU up to 240 MHz
  • Integrated Wi-Fi 4 (802.11 b/g/n)
  • Bluetooth Classic + BLE support
  • 520 KB internal SRAM
  • Up to ~34 usable GPIO
  • Integrated ADC, DAC, PWM, SPI, I2C, UART
  • External Flash and PSRAM support
  • Broad adoption in DevKit and NodeMCU boards
Fab termi

ESP32-S2

A cost-optimized and security-enhanced Wi-Fi variant.
Main characteristics:
  • Single-core Xtensa LX7 up to 240 MHz
  • Integrated Wi-Fi
  • No Bluetooth
  • Native USB-OTG interface
  • Extended security features (Secure Boot, Flash encryption)
  • Increased GPIO availability vs. original ESP32
  • Lower power profile for connected devices
Fab termi

ESP32-S3

An enhanced version focused on edge processing and AI acceleration.
Main characteristics:
  • Dual-core Xtensa LX7 up to 240 MHz
  • Wi-Fi 4 connectivity
  • BLE 5 support
  • Vector instruction extensions for AI/ML workloads
  • Native USB-OTG
  • Up to ~45 GPIO
  • External PSRAM support
  • Hardware cryptographic acceleration
  • Suitable for vision, audio, and edge-AI nodes
Fab termi

ESP32-C3

A modern, low-cost, low-power alternative.
Main characteristics:
  • Single-core RISC-V CPU up to ~160 MHz
  • Wi-Fi 4
  • BLE 5
  • Reduced power consumption
  • Open RISC-V architecture
  • Well suited for simple IoT nodes
Fab termi

ESP32-C6

Designed for next-generation IoT interoperability.
Main characteristics:
  • RISC-V CPU
  • Wi-Fi 6 support
  • BLE 5 support
  • IEEE 802.15.4 radio
  • Thread and Matter protocol support
  • Targeted at smart home and mesh networks
Fab termi

ESP32-H2

Focused on low-power mesh networking.
Main characteristics:
  • RISC-V CPU
  • No Wi-Fi
  • BLE 5 support
  • IEEE 802.15.4
  • Zigbee and Thread support
  • Optimized for ultra-low-power wireless sensor networks
Fab termi

Raspberry Pi

The Raspberry Pi Foundation is an educational organization focused on expanding access to computing and electronics through affordable, well-documented hardware platforms. In the context of embedded systems for Fab Academy, its main contribution is providing low-cost microcontrollers and development boards that support hands-on design, real-time control, and rapid prototyping. This aligns with the Fab methodology: practical experimentation, open documentation, tight hardware–software integration, and direct work with peripherals and digital interfaces.

RP2040

The RP2040 is a microcontroller designed by the Raspberry Pi Foundation for low-cost, high-performance embedded systems.
Main characteristics:
  • Dual ARM Cortex-M0+ cores up to 133 MHz
  • External SPI Flash memory support
  • 264 KB on-chip SRAM organized in 6 memory banks
  • 30 multifunction GPIO pins
  • Unique programmable I/O (PIO) blocks for custom digital protocols
  • Built-in peripherals: SPI, I2C, UART, PWM
  • 12-bit ADC with 4 channels
  • Internal temperature sensor
  • USB 1.1 host/device support
  • Highly flexible for custom interface and protocol implementation
Fab termi

Raspberry Pi Pico

RP2040-Based Development Board Main characteristics:
  • RP2040 microcontroller
  • EClock speed up to 133 MHz
  • 264 KB SRAM
  • External Flash memory (commonly 2 MB)
  • 26 accessible GPIO pins
  • USB interface for programming and communication
  • Designed for low-cost and flexible embedded development
Fab termi

Seeed Studio XIAO RP2040

Main characteristics:
  • RP2040 microcontroller
  • Ultra-compact form factor (miniature board footprint)
  • Up to 133 MHz clock speed
  • 264 KB SRAM
  • External Flash memory (typically 2 MB)
  • Reduced number of exposed GPIO (compact design tradeoff)
  • USB-C connector
  • Suitable for portable and space-constrained embedded projects
Fab termi

Arduino

Arduino is an open hardware and software platform focused on embedded systems development, technology education, and rapid prototyping. In embedded systems contexts, Arduino provides microcontroller-based boards, a simplified development environment, and a broad library ecosystem that enables direct implementation of digital and analog I/O control, communication interfaces, and peripheral handling without complex low-level configuration. Its main strength is fast deployment and accessibility, making it well suited for learning, concept validation, and functional prototypes. Typical architectures are based on AVR or ARM microcontrollers programmed in C/C++ using the Arduino framework, which abstracts much of the hardware complexity.
Most commonly used microcontrollers

Arduino Uno

Main characteristics:
  • Based on ATmega328P microcontroller
  • 8-bit architecture
  • Education-oriented embedded platform
  • Standard set of GPIO, ADC, PWM, and UART
  • Extensive shield and library ecosystem
  • Widely used in training and lab environments
Fab termi

Arduino Mega

Main characteristics:
  • Based on ATmega2560
  • Increased Flash and SRAM memory
  • Large number of GPIO pins
  • Suitable for peripheral-heavy projects
  • Common in robotics and multi-interface control systems
Fab termi

Arduino Nano

Main characteristics:
  • Based on ATmega328P
  • Functionally similar to Uno
  • Compact board format
  • Suitable for PCB integration and small devices
  • Compatible with most Arduino libraries
Fab termi

Language ​​they use

FAMILY COMMON LENGUAGES
ESP32 C/C++, Arduino, MicroPython, Rust
RP2040 C/C++, MicroPython
Arduino AVR C/C++ Arduino
STM32 C/C++, Rust
XIAO Arduino / MicroPython
Since most microcontrollers are compatible with C and C++, they will be used as examples.

C vs C++

For the embedded systems development, the selected programming languages were C and C++, since both are industry-standard languages for microcontroller firmware. They provide direct hardware control, high execution efficiency, and low memory overhead, which are critical properties in resource-constrained embedded platforms.
C is widely used for low-level firmware because it generates compact and predictable code, with minimal abstraction between software and hardware. C++ extends C by adding abstraction mechanisms such as classes, encapsulation, and templates, which improve code organization and reuse, especially when working with modern embedded frameworks and board support packages.

Microcontroller

The selected microcontrollers belong to the ESP family, since they offer strong advantages when working in C and C++, including direct hardware control, high execution speed, and access to low-level peripherals.
Programming these devices in C and C++ allows efficient use of memory and processor time, precise timing control, and real-time interaction with sensors and actuators.
In addition, the ESP platform has mature development frameworks, broad community support, and integrated peripherals such as ADC, PWM, timers, and communication interfaces, which makes it ideal for embedded control applications.
Specifically:
  • ESP32-WROOM-32 (used with C++)
    • Compatible with the Arduino framework and C++ libraries
    • Object-oriented structure simplifies modular control code
    • Fast prototyping with Servo, WiFi, and peripheral libraries
    • Good balance between abstraction and hardware access
    • Well suited for multi-axis actuator control and test rigs
Fab termi
  • ESP32 DevKitC V4 (used with C)
    • Designed to work directly with ESP-IDF in pure C
    • Lower-level driver access (ADC, LEDC, GPIO, timers)
    • More deterministic timing behavior
    • Better suited for real-time control experiments
    • Direct register/driver control for precise signal generation
Fab termi

OBJECTIVE:

The goal of the programming stage was to emulate an aircraft control system, specifically the primary flight control surfaces and their motions. These include:
  • Ailerons — roll control
  • They generate opposite deflections to produce roll about the longitudinal axis.
  • Elevators — pitch control
  • They move symmetrically to control nose-up and nose-down motion.
  • Rudder — yaw control
  • It deflects left or right to rotate the aircraft about the vertical axis.
Fab termi
The embedded implementation reads analog control inputs (simulated pilot commands), processes them in real time, and generates PWM signals to drive servomechanisms that reproduce the mechanical behavior of these flight surfaces.
For the ARIS project, this embedded programming test is useful because it validates real-time control logic, actuator coordination, signal conditioning, and hardware-software integration. It serves as a functional prototype of a closed-loop control interface, reducing risk before integrating full-scale avionics, autonomous control modules, or smart actuation systems.

ADVANTAGES & DRAWBACKS

ASPECT ADVANTAGES (C / C++) DRAWBACKS (C / C++)
Performance High execution speed and very low overhead Requires manual optimization to avoid inefficiencies
Hardware control Direct memory and register access Low-level access increases risk of critical bugs
Portability Portable across many MCU architectures Hardware-specific code reduces portability
Ecosystem Large compiler, SDK, and library ecosystem Library quality and style vary widely
SDK support Native support in most microcontroller SDKs Vendor SDKs can add complexity
Complexity Fine-grained control of system behavior Higher complexity than interpreted languages
Safety No hidden runtime layers Memory safety risks if misused
Learning curve Strong long-term industry value Steeper learning curve for beginners
Resource management Precise manual resource control Manual memory and resource management required

C vs C++ Differences

FEATURE C C++
Programming model Procedural Multi-paradigm (procedural + object-oriented + generic)
Abstraction level Low-level Higher-level abstractions available
Classes and objects Not supported Supported
Encapsulation Manual (struct + functions) Native with classes
Templates / generics Not available Available
Runtime overhead Very low Low to moderate (depends on features used)
Code size Typically smaller Can grow with abstraction use
Embedded usage Common in low-level drivers Common in frameworks and large firmware projects
Learning curve Moderate Higher

C is a general-purpose, structured programming language with relatively low-level capabilities, designed to provide direct hardware control and very efficient memory usage. It is widely used in embedded systems, operating systems, device drivers, and performance-critical applications. It allows pointer operations, manual memory management, and close access to registers and peripherals, which makes it very common in microcontroller programming and real-time control.
Fab termi

C++ is a programming language that extends C by adding object-oriented programming and advanced abstractions such as classes, inheritance, templates, and operator overloading. It preserves C’s efficiency and hardware access while enabling more modular and reusable code organization for large projects. It is used in high-performance software, graphics engines, robotics, advanced embedded systems, and applications where both efficiency and strong code structure are required.
Fab termi

Wokwi programming

Wokwi is an online simulation platform designed for embedded systems and electronic circuit development without requiring physical hardware. It allows developers and students to build virtual prototypes using microcontroller boards, including devices based on ESP chips from Espressif Systems, and test their behavior in real time. Through simulation, users can verify input/output logic, sensor interaction, actuator control, and serial communication before deploying code to a real board. This makes it especially useful for rapid validation, debugging, and educational demonstrations of embedded control systems that are programmed using industry-standard embedded languages such as C and C++.

Entering the Wokwi Interface

When accessing Wokwi, the platform displays the different controller boards supported by the simulator (Arduino, ESP32, Raspberry Pi Pico, among others).
For this project, select:
Enter ESP32
Fab termi

Creating a Blank C Project

To open a blank project in C language, you must:
Go to the ESP-IDF Templates section
Select the normal ESP32
This creates an environment configured with the ESP-IDF framework ready for C programming.
Fab termi

Interface Overview

The interface looks like:
- Left side: Code editor (programming area)
- Right side: Board and circuit visualization
This allows you to edit code while observing the hardware connections in real time.
Fab termi

To add components

On the right side, click the + icon (Add Part)
Add the following components:
  • 5 Servos
  • 2 Joysticks
Fab termi
Fab termi

Power Supply and Ground Connections

It is necessary to properly power the circuit and unify the grounds (GND).
Servos operate at 5V, but:
  • The ESP32 operates at 3.3V
  • Servos consume higher current
  • If powered directly from the ESP32, they may cause:
  • Unexpected resets
  • Electrical noise
  • Unstable behavior
Therefore:
  • An external 5V power supply is used
  • All GND connections must be shared (ESP32 + external supply + servos)
  • This ensures a stable voltage reference and prevents malfunctions or damage.
    Fab termi

    Joystick Connections

    Joysticks function essentially as dual potentiometers (X and Y axes).
    The signal connections are:
    First joystick:
    • Y → Pin 35
    • X → Pin 34
    Second joystick:
    • Y → Pin 32
    • X → Pin 33
    Each axis outputs an analog signal proportional to the joystick position.
    Fab termi

    The control signals are distributed as follows

    • Rudder → 14
    • Left Elevator → 12
    • Right Elevator → 27
    • Left Aileron → 16
    • Right Aileron → 23
    Each pin sends a PWM signal that controls the servo angle.
    Fab termi

    With everything correctly connected
    • External supply powering the servos
    • Unified ground
    • Joysticks connected to analog inputs
    • Servos connected to PWM pins
    The setup should appear fully wired and functional in the simulator, ready to run the program and observe synchronized actuator movement.

    Libraries and Drivers

    
    #include <stdio.h>
    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"
    #include "driver/adc.h"
    #include "driver/gpio.h"
    #include "driver/ledc.h"
    
    • #include imports external libraries.
    • stdio.h enables printf() for serial communication.
    • FreeRTOS manages multitasking.
    • ADC reads analog joystick values.
    • LEDC generates PWM signals.

    Hardware Definitions

    
    // ===== JOYSTICK =====
    #define JOY1_X ADC1_CHANNEL_6
    #define JOY1_Y ADC1_CHANNEL_7
    #define JOY2_X ADC1_CHANNEL_5
    
    // ===== SERVOS =====
    #define SERVO_RUDDER   14
    #define SERVO_ELEV_L   12
    #define SERVO_ELEV_R   27
    #define SERVO_AIL_L    23
    #define SERVO_AIL_R    16
    
    • #define creates readable constants.
    • Joystick uses ADC1 channels.
    • Servos are assigned to specific GPIO pins.
    • Improves modularity and maintainability.

    PWM Configuration

    
    // ===== PWM =====
    #define SERVO_FREQ 50
    #define SERVO_RES LEDC_TIMER_15_BIT
    
    #define CENTER 1500
    #define RANGE  400
    #define DEAD   25
    
    • 50 Hz is standard servo frequency.
    • 15-bit resolution ensures precision.
    • CENTER defines neutral position.
    • DEAD avoids joystick noise.

    Helper Functions

    
    // ---------- helpers ----------
    
    uint32_t us_to_duty(int us){
        uint32_t max = (1 << 15) - 1;
        return (us * max) / 20000;
    }
    
    void servo_write(int ch, int us){
        ledc_set_duty(LEDC_LOW_SPEED_MODE, ch, us_to_duty(us));
        ledc_update_duty(LEDC_LOW_SPEED_MODE, ch);
    }
    
    int adc_avg(adc1_channel_t ch){
        int s=0;
        for(int i=0; i<6; i++) s += adc1_get_raw(ch);
        return s/6;
    }
    
    int map_axis(int v){
        int d = v - 2048;
        if (d > -DEAD && d < DEAD) return 0;
        return (d * RANGE) / 2048;
    }
    
    • us_to_duty() converts microseconds to PWM duty cycle.
    • servo_write() updates servo output.
    • adc_avg() reduces noise.
    • map_axis() applies scaling and dead zone.

    Main Initialization

    
    // ---------- main ----------
    
    void app_main(void)
    {
        printf("\nBonjour mon ami\n");
    
        adc1_config_width(ADC_WIDTH_BIT_12);
        adc1_config_channel_atten(JOY1_X, ADC_ATTEN_DB_11);
        adc1_config_channel_atten(JOY1_Y, ADC_ATTEN_DB_11);
        adc1_config_channel_atten(JOY2_X, ADC_ATTEN_DB_11);
    
        ledc_timer_config_t timer = {
            .speed_mode = LEDC_LOW_SPEED_MODE,
            .timer_num = LEDC_TIMER_0,
            .duty_resolution = SERVO_RES,
            .freq_hz = SERVO_FREQ
        };
        ledc_timer_config(&timer);
    
    • app_main() is the ESP32 entry point.
    • Configures ADC 12-bit resolution.
    • Initializes PWM timer settings.

    Servo Channel Configuration

    
        int pins[5] = {
            SERVO_RUDDER,
            SERVO_ELEV_L,
            SERVO_ELEV_R,
            SERVO_AIL_L,
            SERVO_AIL_R
        };
    
        for(int i=0; i<5; i++){
            ledc_channel_config_t ch={
                .gpio_num = pins[i],
                .speed_mode = LEDC_LOW_SPEED_MODE,
                .channel = i,
                .timer_sel = LEDC_TIMER_0,
                .duty = 0
            };
            ledc_channel_config(&ch);
        }
    
    • Uses an array for pin organization.
    • Configures 5 PWM channels using a for loop.
    • Assigns each servo a unique channel.

    Main Control Loop

    
        int lastElevState = 0;
        int lastRudderState = 0;
        int lastAilState = 0;
    
        while(1){
    
            int x1 = adc_avg(JOY1_X);
            int y1 = adc_avg(JOY1_Y);
            int x2 = adc_avg(JOY2_X);
    
            int rudder = map_axis(x1);
            int elev   = map_axis(y1);
            int ail    = map_axis(x2);
    
            servo_write(0, CENTER + rudder);
            servo_write(1, CENTER + elev);
            servo_write(2, CENTER - elev);
            servo_write(3, CENTER + ail);
            servo_write(4, CENTER + ail);
    
    • while(1) creates an infinite loop.
    • Reads joystick values.
    • Maps them to servo motion.
    • Updates PWM outputs.

    Serial Interpretation and Delay

    
            // ===== SERIAL INTERPRETATION =====
    
            int elevState = (elev > DEAD) ? 1 : (elev < -DEAD ? -1 : 0);
            if(elevState != lastElevState){
                if(elevState > 0)  printf("Elevator up -> the plane is going to come DOWN\n");
                if(elevState < 0)  printf("Elevator down -> the plane is going to come DOWN\n");
                if(elevState == 0) printf("Elevator center\n");
                lastElevState = elevState;
            }
    
            int rudderState = (rudder > DEAD) ? 1 : (rudder < -DEAD ? -1 : 0);
            if(rudderState != lastRudderState){
                if(rudderState > 0)  printf("Rudder -> RIGHT\n");
                if(rudderState < 0)  printf("Rudder -> LEFT\n");
                if(rudderState == 0) printf("Rudder center\n");
                lastRudderState = rudderState;
            }
    
            int ailState = (ail > DEAD) ? 1 : (ail < -DEAD ? -1 : 0);
            if(ailState != lastAilState){
                if(ailState > 0)  printf("Ailerons -> rotate RIGHT\n");
                if(ailState < 0)  printf("Ailerons -> rotate LEFT\n");
                if(ailState == 0) printf("Ailerons center\n");
                lastAilState = ailState;
            }
    
            vTaskDelay(pdMS_TO_TICKS(70));
        }
    }
    
    • Uses ternary operators for direction detection.
    • Prints only when state changes.
    • vTaskDelay() prevents CPU overload.
    
    
    #include <stdio.h>
    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"
    #include "driver/adc.h"
    #include "driver/gpio.h"
    #include "driver/ledc.h"
    
    // ===== JOYSTICK =====
    #define JOY1_X ADC1_CHANNEL_6
    #define JOY1_Y ADC1_CHANNEL_7
    #define JOY2_X ADC1_CHANNEL_5
    
    // ===== SERVOS =====
    #define SERVO_RUDDER   14
    #define SERVO_ELEV_L   12
    #define SERVO_ELEV_R   27
    #define SERVO_AIL_L    23
    #define SERVO_AIL_R    16
    
    // ===== PWM =====
    #define SERVO_FREQ 50
    #define SERVO_RES LEDC_TIMER_15_BIT
    
    #define CENTER 1500
    #define RANGE  400
    #define DEAD   25
    
    // ---------- helpers ----------
    
    uint32_t us_to_duty(int us){
        uint32_t max = (1 << 15) - 1;
        return (us * max) / 20000;
    }
    
    void servo_write(int ch, int us){
        ledc_set_duty(LEDC_LOW_SPEED_MODE, ch, us_to_duty(us));
        ledc_update_duty(LEDC_LOW_SPEED_MODE, ch);
    }
    
    int adc_avg(adc1_channel_t ch){
        int s=0;
        for(int i=0; i<6; i++) s += adc1_get_raw(ch);
        return s/6;
    }
    
    int map_axis(int v){
        int d = v - 2048;
        if (d > -DEAD && d < DEAD) return 0;
        return (d * RANGE) / 2048;
    }
    
    // ---------- main ----------
    
    void app_main(void)
    {
        printf("\nBonjour mon ami\n");
    
        adc1_config_width(ADC_WIDTH_BIT_12);
        adc1_config_channel_atten(JOY1_X, ADC_ATTEN_DB_11);
        adc1_config_channel_atten(JOY1_Y, ADC_ATTEN_DB_11);
        adc1_config_channel_atten(JOY2_X, ADC_ATTEN_DB_11);
    
        ledc_timer_config_t timer = {
            .speed_mode = LEDC_LOW_SPEED_MODE,
            .timer_num = LEDC_TIMER_0,
            .duty_resolution = SERVO_RES,
            .freq_hz = SERVO_FREQ
        };
        ledc_timer_config(&timer);
    
        int pins[5] = {
            SERVO_RUDDER,
            SERVO_ELEV_L,
            SERVO_ELEV_R,
            SERVO_AIL_L,
            SERVO_AIL_R
        };
    
        for(int i=0; i<5; i++){
            ledc_channel_config_t ch={
                .gpio_num = pins[i],
                .speed_mode = LEDC_LOW_SPEED_MODE,
                .channel = i,
                .timer_sel = LEDC_TIMER_0,
                .duty = 0
            };
            ledc_channel_config(&ch);
        }
    
        int lastElevState = 0;
        int lastRudderState = 0;
        int lastAilState = 0;
    
        while(1){
    
            int x1 = adc_avg(JOY1_X);
            int y1 = adc_avg(JOY1_Y);
            int x2 = adc_avg(JOY2_X);
    
            int rudder = map_axis(x1);
            int elev   = map_axis(y1);
            int ail    = map_axis(x2);
    
            servo_write(0, CENTER + rudder);
            servo_write(1, CENTER + elev);
            servo_write(2, CENTER - elev);
            servo_write(3, CENTER + ail);
            servo_write(4, CENTER + ail);
    
            // ===== SERIAL INTERPRETATION =====
    
            int elevState = (elev > DEAD) ? 1 : (elev < -DEAD ? -1 : 0);
            if(elevState != lastElevState){
                if(elevState > 0)  printf("Elevator up -> the plane is going to come DOWN\n");
                if(elevState < 0)  printf("Elevator down -> the plane is going to come DOWN\n");
                if(elevState == 0) printf("Elevator center\n");
                lastElevState = elevState;
            }
    
            int rudderState = (rudder > DEAD) ? 1 : (rudder < -DEAD ? -1 : 0);
            if(rudderState != lastRudderState){
                if(rudderState > 0)  printf("Rudder -> RIGHT\n");
                if(rudderState < 0)  printf("Rudder -> LEFT\n");
                if(rudderState == 0) printf("Rudder center\n");
                lastRudderState = rudderState;
            }
    
            int ailState = (ail > DEAD) ? 1 : (ail < -DEAD ? -1 : 0);
            if(ailState != lastAilState){
                if(ailState > 0)  printf("Ailerons -> rotate RIGHT\n");
                if(ailState < 0)  printf("Ailerons -> rotate LEFT\n");
                if(ailState == 0) printf("Ailerons center\n");
                lastAilState = ailState;
            }
    
            vTaskDelay(pdMS_TO_TICKS(70));
        }
    }
    
    

    Arduino programming

    Arduino is a hardware and software development platform focused on embedded control, rapid prototyping, and technology education. It provides programmable boards, a unified development environment, and a large ecosystem of libraries that simplify interaction with peripherals, sensors, and communication interfaces. Modern Arduino-compatible environments support ESP-based boards, enabling wireless connectivity and advanced peripherals while keeping the development workflow accessible. In practice, Arduino is widely used to implement functional embedded control prototypes, where firmware written in common embedded languages such as C and C++ can directly manage inputs, outputs, and real-time device behavior.

    1️⃣ Library and Hardware Definitions

    
    
    #include <ESP32Servo.h>
    
    // ===== Potenciómetros =====
    #define POT_RUDDER 32
    #define POT_ELEV   35
    #define POT_AIL    34
    
    // ===== Servos =====
    #define SERVO_RUDDER_PIN 13
    #define SERVO_ELEV_L_PIN 12
    #define SERVO_ELEV_R_PIN 14
    #define SERVO_AIL_L_PIN  27
    #define SERVO_AIL_R_PIN  26
    
    Servo servoRudder;
    Servo servoElevL;
    Servo servoElevR;
    Servo servoAilL;
    Servo servoAilR;
    
    int dead = 15; // zona muerta
    
    
    • #include <ESP32Servo.h> imports the ESP32 servo control library.
    • #define assigns readable pin constants.
    • Five Servo objects are instantiated.
    • dead defines the dead zone to prevent signal noise.

    2️⃣ Helper Function

    
    
    // ---------- helper ----------
    int applyDead(int v) {
      if (abs(v - 2048) < dead) return 2048;
      return v;
    }
    
    
    • applyDead() eliminates small variations around center.
    • 2048 is the center value in 12-bit ADC.
    • Improves stability and avoids servo jitter.

    3️⃣ Setup Function

    
    
    void setup() {
      Serial.begin(115200);
    
      analogReadResolution(12);
    
      servoRudder.attach(SERVO_RUDDER_PIN, 500, 2500);
      servoElevL.attach(SERVO_ELEV_L_PIN, 500, 2500);
      servoElevR.attach(SERVO_ELEV_R_PIN, 500, 2500);
      servoAilL.attach(SERVO_AIL_L_PIN, 500, 2500);
      servoAilR.attach(SERVO_AIL_R_PIN, 500, 2500);
    }
    
    
    • Serial.begin(115200) initializes serial communication.
    • analogReadResolution(12) sets 12-bit ADC resolution.
    • attach() connects each servo to a pin.
    • 500–2500 µs defines servo pulse width range.

    4️⃣ Main Loop

    
    
    void loop() {
    
      // ===== Leer potenciómetros =====
      int rudderRaw = applyDead(analogRead(POT_RUDDER));
      int elevRaw   = applyDead(analogRead(POT_ELEV));
      int ailRaw    = applyDead(analogRead(POT_AIL));
    
      // ===== Mapear =====
      int rudderAng = map(rudderRaw, 0, 4095, 0, 180);
      int elevAng   = map(elevRaw,   0, 4095, 0, 180);
      int ailAng    = map(ailRaw,    0, 4095, 0, 180);
    
      // ===== Timón =====
      servoRudder.write(rudderAng);
    
      // ===== Elevadores (mismo sentido) =====
      servoElevL.write(elevAng);
      servoElevR.write(elevAng);
    
      // ===== Alerones (espejo) =====
      servoAilL.write(ailAng);
      servoAilR.write(180 - ailAng);
    
      // ===== Debug =====
      Serial.print("R:");
      Serial.print(rudderRaw);
      Serial.print(" E:");
      Serial.print(elevRaw);
      Serial.print(" A:");
      Serial.println(ailRaw);
    
      delay(60);
    }
    
    
    • loop() runs continuously.
    • Reads potentiometers using analogRead().
    • Maps values from 0–4095 to 0–180°.
    • Ailerons use mirror logic (180 - angle).
    • Serial.print() outputs debug values.
    • delay(60) stabilizes execution.
    
    
    #include <ESP32Servo.h>
    
    // ===== Potenciómetros =====
    #define POT_RUDDER 32
    #define POT_ELEV   35
    #define POT_AIL    34
    
    // ===== Servos =====
    #define SERVO_RUDDER_PIN 13
    #define SERVO_ELEV_L_PIN 12
    #define SERVO_ELEV_R_PIN 14
    #define SERVO_AIL_L_PIN  27
    #define SERVO_AIL_R_PIN  26
    
    Servo servoRudder;
    Servo servoElevL;
    Servo servoElevR;
    Servo servoAilL;
    Servo servoAilR;
    
    int dead = 15; // zona muerta
    
    // ---------- helper ----------
    int applyDead(int v) {
      if (abs(v - 2048) < dead) return 2048;
      return v;
    }
    
    void setup() {
      Serial.begin(115200);
    
      analogReadResolution(12);
    
      servoRudder.attach(SERVO_RUDDER_PIN, 500, 2500);
      servoElevL.attach(SERVO_ELEV_L_PIN, 500, 2500);
      servoElevR.attach(SERVO_ELEV_R_PIN, 500, 2500);
      servoAilL.attach(SERVO_AIL_L_PIN, 500, 2500);
      servoAilR.attach(SERVO_AIL_R_PIN, 500, 2500);
    }
    
    void loop() {
    
      // ===== Leer potenciómetros =====
      int rudderRaw = applyDead(analogRead(POT_RUDDER));
      int elevRaw   = applyDead(analogRead(POT_ELEV));
      int ailRaw    = applyDead(analogRead(POT_AIL));
    
      // ===== Mapear =====
      int rudderAng = map(rudderRaw, 0, 4095, 0, 180);
      int elevAng   = map(elevRaw,   0, 4095, 0, 180);
      int ailAng    = map(ailRaw,    0, 4095, 0, 180);
    
      // ===== Timón =====
      servoRudder.write(rudderAng);
    
      // ===== Elevadores (mismo sentido) =====
      servoElevL.write(elevAng);
      servoElevR.write(elevAng);
    
      // ===== Alerones (espejo) =====
      servoAilL.write(ailAng);
      servoAilR.write(180 - ailAng);
    
      // ===== Debug =====
      Serial.print("R:");
      Serial.print(rudderRaw);
      Serial.print(" E:");
      Serial.print(elevRaw);
      Serial.print(" A:");
      Serial.println(ailRaw);
    
      delay(60);
    }
    
                            
    After connecting the components according to the code and uploading it to the board, it should look like this.

    Download files

    For download 3D and others files, just click on the dancing shrimp.