Week 04 - Embedded Programming
This week, we have the following tasks to complete:
- Demonstrate and compare the toolchains and development workflows for available embedded architectures.
- Review the datasheet of your microcontroller.
- Write a program for a microcontroller.
- Simulate its operation, enabling interaction with local input/output devices and communication via remote wired or wireless connections.
- Optional: Test the program on a development board.
- Optional: Experiment with different programming languages and development environments.
Preparing for the Week
In preparation for this week, I switched from VS Codium to VS Code to facilitate the installation of PlatformIO. To program ESP32 microcontrollers from Espressif, the ESP-IDF framework is required. I followed the official setup guide for the ESP-IDF extension in VS Code.
After the installation, I executed the following command in the command palette and followed the setup wizard as outlined in the extension documentation
ESP-IDF: Configure ESP-IDF Extension
Group assignment
We conducted an Arduino workshop led by Ferdi and compared different boards and their functionality on our group assignment page.
Selecting a Microcrontroller
For my final project, I will require a wireless communication module. ESP32 microcontrollers are ideal for this purpose, as they offer integrated Wi-Fi and Bluetooth capabilities. Our lab provides the Xiao ESP32C3 and Xiao ESP32S3, so I will use one of these models for my initial experiments.
For the final project, I will likely use an ESP32 without a development board, rather than a Xiao board. However, this decision will be made in a future phase of the project. Neil recommended using the Raspberry Pi Pico W, so I will test it in Week 06 and design a development board around it.
To establish a better comparison baseline, I reviewed the datasheets of the ESP32C3, the Raspberry Pi Pico W, and for more details, the RP2040. This was my first experience with an ARM processor, providing me with a fundamental insight into the world of ARM architectures.
In my final project I need rotary encoders as a input device, ARGB LED's as a output device, components that require pwm signals and some webinterface interaction. So in the following I look up in some of these areas.
Working with Rotary Encoders
I started by researching rotary encoders to gain a better understanding of their functionality. I found an interesting project called StepperTester, which demonstrates stepper motor control using a rotary encoder.
The program operates in three modes:
- Adjust the RPM for mode 2.
- Rotate the stepper motor by one step when the rotary encoder is turned.
- Control the stepper motor's RPM using the rotary encoder.
The mode is selected by pressing the encoder button, which is built into the rotary encoder. This functionality is implemented using interrupts.
The simulation requires a stepper driver and a display to function correctly. The display shows the selected mode and the configured value.
Controlling WS2812B LEDs
For controlling WS2812B LEDs, libraries such as FastLED, NeoPixelBus, and NeoPixel are commonly used. I found an insightful benchmark comparing the performance of these three libraries. I tried both the FastLED library and the Neopixel library.
To work with rotary encoders in my own projects, I explored the ai-esp32-rotary-encoder and the Encoder libraries. My implementation was based on their example files for better understanding.
Simulating ESP32C3 in Wokwi
I attempted to adapt a Wokwi project for the Xiao ESP32-C3. Primarily, I only needed to remove parts of the code, adjust a few variables, and set different boundaries. Additionally, I experimented with both encoder libraries and used the provided example codes as a reference. The results are shown below:
#include <Adafruit_NeoPixel.h> // include the library for NeoPixels
#include <AiEsp32RotaryEncoder.h> // include the library for a rotary encoder
#include <Arduino.h> // include arduino library which is need it for the rotary encoder library
#define ROTARY_ENCODER_A_PIN D10 // set first pin to connect the rotary encoder to the esp32
#define ROTARY_ENCODER_B_PIN D9 // set second pin to connect the rotary encoder to the esp32
#define ENCODER_BTN D8 // set button pin to connect the rotary encoder to the esp32
#define ROTARY_ENCODER_VCC_PIN -1 // set VCC level for the rotary encoder (-1 = 3V3; 0 = 5V)
#define ROTARY_ENCODER_STEPS 2 // set the steps that rotary encoder have
#define PIN_NEO_PIXEL D6 // Arduino pin that connects to NeoPixel
#define NUM_PIXELS 4 // number of LED's (pixels) on NeoPixel
AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ENCODER_BTN, ROTARY_ENCODER_VCC_PIN, ROTARY_ENCODER_STEPS); // initialize an AiEsp32RotaryEncoder object named rotaryEncoder using the previously defined values
Adafruit_NeoPixel NeoPixel(NUM_PIXELS, PIN_NEO_PIXEL, NEO_GRB + NEO_KHZ800); // initialize an Adafruit_NeoPixel object named NeoPixel using the previously defined values
void IRAM_ATTR readEncoderISR(){ // interrupt function that is triggered by manipulating the rotary encoder
rotaryEncoder.readEncoder_ISR(); // read the value from the rotary encoder
}
byte color_blue_RGB[3] = {0, 0, 255}; // blue color in RGB color space
byte color_red_RGB[3] = {255, 0, 0}; // red color in RGB color space
byte color_blend_RGB[3]; // variable to store blended RGB color
byte color_blue_HSV[3] = {170, 255, 255}; // blue color in HSV color space
byte color_red_HSV[3] = {0, 255, 255}; // red color in HSV color space
byte color_blend_HSV[3]; // variable to store blended HSV color
int encoder_value_old = 0; // previous value of rotary encoder
int encoder_value_new = 0; // current value of rotary encoder
int percentage_value = 50; // percentage value to fill the ring: 0 - 100%
int mode = 0; // 0 = white, 1 = blue to red RGB, 2 = blue to red HSV
void blend_colors (byte color_start[3], byte color_end[3], byte color_blended[3], float percentage /*0-100*/) { // function to blend between two colors
for (int i = 0; i < 3; i ++) {
color_blended[i] = round((float)color_start[i] + (((float)color_end[i] - (float)color_start[i]) * (percentage / 100.0))); // linear interpolation between two values
}
}
void setup() { // setup function executed ones
Serial.begin(115200); // start serial communication with a baudrate of 115200
NeoPixel.begin(); // initialize the NeoPixel strip object
pinMode(ENCODER_BTN, INPUT_PULLUP); // set pin for rotary encoder button
rotaryEncoder.begin(); // initiate the rotary encoder
rotaryEncoder.setup(readEncoderISR);
bool circleValues = false; // define if the value from the rotary encoder returns to the maximum value if its goes under the minimum value and the other way around or not
rotaryEncoder.setBoundaries(-100, 100, circleValues); // set the minimum and maximum value
rotaryEncoder.setAcceleration(250); //set up acceleration which allows the recognition how fast the rotary encoder is rotated if its turned fast the value will change with a higher multiplier and if its turned slowly it changes with a smaller multiplier (larger number = more accelearation; 0 or 1 means disabled acceleration)
}
void loop() { // loop function executed infinite
Serial.print(percentage_value); // send how much leds in percent are activated from the ring light
encoder_value_new = rotaryEncoder.readEncoder(); // reads the encoder value
if (encoder_value_new != encoder_value_old) { // if the old value does not equal the new value
percentage_value = constrain(percentage_value - (encoder_value_new - encoder_value_old), 0, 100); // set the percentage value between 0-100%
encoder_value_old = encoder_value_new; // update the old encoder value
}
if (digitalRead(ENCODER_BTN) == LOW) { // if the button is pressed the lightning mode from the Neopixels are changed
mode++;
if (mode > 2) {
mode = 0;
}
delay(500); // add a small delay
}
NeoPixel.clear(); // set all pixel colors to 'off'. It only takes effect if pixels.show() is called
for (int pixel = 0; pixel < NUM_PIXELS; pixel++) { // set a value for each pixel by increment the pixel id from 0 to number of pixel
if ((pixel * (100 / (NUM_PIXELS - 1))) < percentage_value) { // fill pixels based on the rotary encoder value
if (mode == 0) { // toggle the color of each pixel to white if mode 0 is selected
NeoPixel.setPixelColor(pixel, NeoPixel.Color(255, 255, 255)); // set all pixels to white color
} else if (mode == 1) { // blend between two colors in RGB color space if mode 1 is selected
blend_colors(color_blue_RGB, color_red_RGB, color_blend_RGB, pixel * (100.0 / (NUM_PIXELS - 1)));
NeoPixel.setPixelColor(pixel, NeoPixel.Color(color_blend_RGB[0], color_blend_RGB[1], color_blend_RGB[2])); // set the pixel to the blended color
} else if (mode == 2) { // blend two colors in HSV color space if mode 2 is selected
blend_colors(color_blue_HSV, color_red_HSV, color_blend_HSV, pixel * (100.0 / (NUM_PIXELS - 1)));
NeoPixel.setPixelColor(pixel, NeoPixel.ColorHSV((unsigned int)color_blend_HSV[0] * 256, color_blend_HSV[1], color_blend_HSV[2])); // set the pixel to the blended color
}
}
}
NeoPixel.show(); // show all the set pixels on neopixel ring
}
This setup integrates a rotary encoder with WS2812B LEDs.
Wokwi enables code simulation for various microcontrollers, including the Arduino Uno, Xiao ESP32-C3, and Raspberry Pi Pico. However, it does not simulate actual electrical circuits—e.g., resistors are not required in Wokwi but are necessary in real-world applications.
Additional Experiments
I also tested the Arduino IDE by programming an Arduino Nano with the blink example:
All pseudo C programs for microcontrollers consist of at least two functions: void setup()
and void loop()
. The setup()
function is executed once after the microcontroller is powered on or reset. Within setup()
, the pinMode()
function is used to configure the behavior of a specific pin. In this case, pin D13 is configured as an output, since it is connected to both the onboard LED and an additional external LED via a current-limiting resistor.
The pinMode()
call only runs once during setup, unlike the loop()
function, which executes continuously. Inside loop()
, the digitalWrite()
function is used to change the logic level of the configured pin. digitalWrite()
sets a pin to either logic HIGH (≥ 4.44 V for a CMOS logic output) or logic LOW (≤ 0.5 V for a CMOS logic output). When the pin is set to HIGH, the LED turns on; when set to LOW, the LED turns off. Combined with the delay()
function, this creates a blinking effect. However, during a delay, the microcontroller is idle and cannot perform any other tasks.
To avoid this blocking behavior, a more efficient approach is to use the millis()
function, which returns the number of milliseconds since the microcontroller was powered on. By using conditional if
statements, it is possible to check whether a certain time interval has passed, and only then toggle the LED state. This non-blocking method allows the microcontroller to remain responsive and continue performing other tasks in parallel.
A practical example of this method is provided in the official Arduino example called "BlinkWithoutDelay".
Since breadboards are not allowed, I performed a quick soldering job, which, despite its imperfections, worked successfully:
Another workflow for programming microcontrollers is using PlatformIO within VS Code. As a demonstration, I programmed an Attiny84V soldered onto a binary clock used in workshops at Unikat, the origin of our FabLab.
What I learn this week
- I learned about the term "pin multiplexing." I was already familiar with the concept but did not know the specific term for it.
What I want to improve next week
- Take regular breaks to recover. I felt somewhat exhausted from the past weeks, which made this week unproductive due to the time my body needed to recover properly. In the coming weeks, I need to monitor my well-being more consistently and ensure that I take breaks before reaching exhaustion.
Design Files
To create this page, I used ChatGPT to check my syntax and grammar.
Copyright 2025 < Benedikt Feit > - Creative Commons Attribution Non Commercial
Source code hosted at gitlab.fabcloud.org