Week 10 - Output Devices
This week we have the following tasks to complete:
- measure the power consumption of an output device
- add an output device to a microcontroller board you've designed and program it to do something
Group Assignment
The weekly group assignment can be accessed here.
To determine the power consumption, I decided to measure the current draw of the small OLED displays I planned to use later. For this purpose, I wrote a script that turns on all pixels for 10 seconds, then turns them off for another 10 seconds, repeating this cycle continuously.
#include <Wire.h> // include wire library need it for the Adafruit_GFX library
#include <Adafruit_GFX.h> // include Adafruit_GFX library need it for the Adafruit_SSD1306 library
#include <Adafruit_SSD1306.h> // include Adafruit_SSD1306 library tp control the OLED display
#define SCREEN_WIDTH 128 // set OLED display width
#define SCREEN_HEIGHT 64 // set OLED display height
#define OLED_RESET -1 // set the reset pin
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // initialize an Adafruit_SSD1306 object called display with the parameters from above
void setup() {
Serial.begin(115200); // initialize serial communication with a baudrate of 115200
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // checked if a display is available
Serial.println(F("SSD1306 allocation failed")); // if no display is available send an error message to the serial monitor
for(;;);
}
display.clearDisplay(); // clear the display --> set every pixel to black
}
void loop() {
display.fillScreen(SSD1306_WHITE);
display.display(); // show data on the display with the parameters from above
delay(10000);
display.clearDisplay();
display.display();
delay(10000);
}
To measure the power consumption of the display, I conducted five measurements.
- Measured the power consumption of the XIAO ESP32C3 without any additional modules connected.
- Measured the power consumption of the XIAO ESP32C3 with the OLED display connected but turned off.
- Measured the power consumption of the XIAO ESP32C3 with the OLED display turned on.
Afterward, I subtracted the first measurement from the others to determine the actual power consumption of the display module in both the on and off states.
Power consumption was measured using a USB power meter. The setup is shown below:
Measurement | Voltage | Current | Power Consumption | Display Module Consumption |
---|---|---|---|---|
1 | 5.06 V | 0.016 A | 80.96 mW | — |
2 | 5.06 V | 0.018 A | 91.08 mW | 10.12 mW |
3 | 5.06 V | 0.048 A | 242.88 mW | 161.92 mW |
OLED Display
For testing, I used the development board from week 08.
In addition to the NeoPixel module I worked with in week 08, I experimented with a 0.96" 128×64 px OLED display. To begin, I ran a simple "Hello World" program, inspired by Sungmoon Lim. I modified his code and arrived at the following version:
#include <Wire.h> // include wire library need it for the Adafruit_GFX library
#include <Adafruit_GFX.h> // include Adafruit_GFX library need it for the Adafruit_SSD1306 library
#include <Adafruit_SSD1306.h> // include Adafruit_SSD1306 library tp control the OLED display
#define SCREEN_WIDTH 128 // set OLED display width
#define SCREEN_HEIGHT 64 // set OLED display height
#define OLED_RESET -1 // set the reset pin
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // initialize an Adafruit_SSD1306 object called display with the parameters from above
void setup() {
Serial.begin(115200); // initialize serial communication with a baudrate of 115200
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // checked if a display is available
Serial.println(F("SSD1306 allocation failed")); // if no display is available send an error message to the serial monitor
for(;;);
}
display.clearDisplay(); // clear the display --> set every pixel to black
}
void displayMessage(const char* message) { // function to visualize a message on the display
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE); // set the text color
display.setCursor(0,0); // set the position where the text is shown on the display
display.println(message); // send the input from the function to the display
display.display(); // show data on the display with the parameters from above
}
void loop() {
displayMessage("Hello World"); // display "Hello World" on the display
delay(3000); // delay the next message so there is a chance to read it
displayMessage("Hello FabAcademy"); // display "Hello FabAcademy" on the display
delay(3000); // delay the next message so there is a chance to read it
displayMessage("Hello Ilmenau"); // display "Hello Ilmenau" on the display
delay(3000); // delay the next message so there is a chance to read it
}
The next step was to display more meaningful data — specifically, the current value from the rotary encoder.
#include <Wire.h> // include wire library need it for the Adafruit_GFX library
#include <Adafruit_GFX.h> // include Adafruit_GFX library need it for the Adafruit_SSD1306 library
#include <Adafruit_SSD1306.h> // include Adafruit_SSD1306 library tp control the OLED display
#include <Encoder.h> // library for rotary encoder
#define SCREEN_WIDTH 128 // set OLED display width
#define SCREEN_HEIGHT 64 // set OLED display height
#define OLED_RESET -1 // set the reset pin
#define dt D8 // Xiao pin that connects to rotary encoder dt
#define clk D10 // Xiao pin that connects to rotary encoder clk
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // initialize an Adafruit_SSD1306 object called display with the parameters from above
Encoder interface(dt,clk); // initiate rotary encoder
int encoder_value = 0; // value of rotary encoder
void setup() {
Serial.begin(115200); // initialize serial communication with a baudrate of 115200
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // checked if a display is available
Serial.println(F("SSD1306 allocation failed")); // if no display is available send an error message to the serial monitor
for(;;);
}
display.clearDisplay(); // clear the display --> set every pixel to black
}
void displayMessage(int message) { // function to visualize a message on the display
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE); // set the text color
display.setCursor(0,0); // set the position where the text is shown on the display
display.println("Encoder Value:"); // send the input from the function to the display
display.setCursor(0,10); // set the position where the text is shown on the display
display.println(message); // send the input from the function to the display
display.display(); // show data on the display with the parameters from above
}
void loop() {
if (encoder_value != interface.read()){ // read out the value from the rotary encoder
encoder_value = interface.read(); // overwrite the old stored encoder value
}
displayMessage(encoder_value); // display the encoder value on the display
}
In preparation for my final project, I tested a larger 2.42" OLED display in addition to the smaller 0.96" version. To interface with it, I designed and manufactured a small PCB adapter for this display module.
To enable I2C communication, I followed the instructions in the official manual and resoldered two resistors on the back of the display.
The adapter was necessary because, in addition to the +5V, GND, SCL, and SDA lines, an additional I/O pin is required, as described in the manual. Additionally, the pin layout on the display differs from the small OLED display.
To test the display, I uploaded the recommended example code with the required modifications from the manual to check if it functioned correctly. The code is part of the U8g2 library by Oliver.
Before uploading, the display was already showing the rotary encoder’s current value. After investigating the purpose of the additional I/O pin, I found out it is only used for resetting the display. Therefore, I decided to ignore this pin and proceed with the regular I2C interface, which I had initially overlooked. Since I confirmed that the display was functioning correctly, I discontinued further testing and did not experiment with the library's example code.
For the student club "bc-students", of which I am a member, I designed a remote control system for the bar lighting a few months ago. The remote itself will be designed in week 11, as it involves implementing a specific communication protocol.
As a preliminary step, I tested light control using the XIAO ESP32-C3. On the existing lighting control board, the final output stage consists of MOSFETs. Between the XIAO and the MOSFETs are driver ICs. At the time I designed the PCB, I was not yet aware of the efficient logic-level MOSFETs available from the FabInventory, which can be driven directly by a microcontroller.
Since the MOSFETs used here cannot be controlled directly by the microcontroller, I chose to include driver ICs, which turned out to be a more elegant and even slightly cheaper solution than using bipolar transistor driver stages.
Due to communication issues I encountered in Week 11, I created a small adapter board to replace the originally used Wrover module with a XIAO ESP32-C3.
Each lighting channel receives a PWM signal, requiring a total of 8 PWM outputs. My initial idea was to use analogWrite()
on each pin. However, the XIAO ESP32-C3 supports only 6 independent hardware PWM channels.
To resolve this limitation, I implemented two additional PWM outputs using software PWM. To maintain consistent light behavior, I used hardware PWM for the LED strips and software PWM for the LED spotlights, even though only one spotlight channel is currently in use.
To implement software PWM, I reviewed Neil’s example code and also used ChatGPT with the following prompt for assistance:
Light Controller
For the student club "bc-Keller", of which I am a member, I began designing a remote control system for the bar lighting a few months ago. The remote itself will be designed in week 11, as it involves implementing a specific communication protocol.
As a preliminary step, I tested light control using the XIAO ESP32-C3. On the existing lighting control board, the final output stage consists of MOSFETs. Between the XIAO and the MOSFETs are driver ICs. At the time I designed the PCB, I was not yet aware of the efficient logic-level MOSFETs available from the FabInventory, which can be driven directly by a microcontroller.
Since the MOSFETs used here cannot be controlled directly by the microcontroller, I chose to include driver ICs, which turned out to be a more elegant and even slightly cheaper solution than using bipolar transistor driver stages.
Due to communication issues I encountered in Week 11, I created a small adapter board to replace the originally used Wrover module with a XIAO ESP32-C3.
Each lighting channel receives a PWM signal, requiring a total of 8 PWM outputs. My initial idea was to use analogWrite()
on each pin. However, the XIAO ESP32-C3 supports only 6 independent hardware PWM channels.
To resolve this limitation, I implemented two additional PWM outputs using software PWM. To maintain consistent light behavior, I used hardware PWM for the LED strips and software PWM for the LED spotlights, even though only one spotlight channel is currently in use.
To implement software PWM, I reviewed Neil’s example code and also used ChatGPT with the following prompt for assistance:
"Generate 2 software pwm signal for an xiao esp32-c3 from seedstudio with there board files. Add 6 more hardware pwm channels with the analogWrite() function to the code. Hardware PWM for pins D1, D2, D3, D6, D8 and D9. Software pwm on pins D4 and D7. The code will be compiled in the arduino ide."
Which results in following output:
"Here's a full Arduino sketch that generates:
2 software PWM signals on pins D4 and D7
6 hardware PWM signals using analogWrite() on pins D1, D2, D3, D6, D8, and D9
This sketch is compatible with the Seeed Studio XIAO ESP32-C3 using the Seeed ESP32 board definitions in the Arduino IDE.
✅ Arduino Sketch// Pin definitions
const int hwPWM[] = {D1, D2, D3, D6, D8, D9}; // Hardware PWM pins
const int swPWM1 = D4; // Software PWM pin 1
const int swPWM2 = D7; // Software PWM pin 2// Software PWM settings
const int swPWMFreq = 500; // Frequency in Hz
const int swPWMResolution = 8; // 8-bit resolution (0-255)
const int swPWMDuty1 = 128; // 50% duty cycle
const int swPWMDuty2 = 64; // 25% duty cycleunsigned long swPWMPeriod = 1000000UL / swPWMFreq; // Period in microseconds
unsigned long swPWMPulse1 = swPWMPeriod * swPWMDuty1 / 255;
unsigned long swPWMPulse2 = swPWMPeriod * swPWMDuty2 / 255;unsigned long lastToggle1 = 0;
unsigned long lastToggle2 = 0;bool swPWMState1 = false;
bool swPWMState2 = false;void setup() {
// Initialize hardware PWM pins
for (int i = 0; i < 6; i++) {
pinMode(hwPWM[i], OUTPUT);
analogWrite(hwPWM[i], 127); // 50% duty cycle
}// Initialize software PWM pins
pinMode(swPWM1, OUTPUT);
pinMode(swPWM2, OUTPUT);
}void loop() {
unsigned long now = micros();// Software PWM for swPWM1 (D4)
if (swPWMState1 && (now - lastToggle1 >= swPWMPulse1)) {
digitalWrite(swPWM1, LOW);
lastToggle1 = now;
swPWMState1 = false;
} else if (!swPWMState1 && (now - lastToggle1 >= (swPWMPeriod - swPWMPulse1))) {
digitalWrite(swPWM1, HIGH);
lastToggle1 = now;
swPWMState1 = true;
}// Software PWM for swPWM2 (D7)
if (swPWMState2 && (now - lastToggle2 >= swPWMPulse2)) {
digitalWrite(swPWM2, LOW);
lastToggle2 = now;
swPWMState2 = false;
} else if (!swPWMState2 && (now - lastToggle2 >= (swPWMPeriod - swPWMPulse2))) {
digitalWrite(swPWM2, HIGH);
lastToggle2 = now;
swPWMState2 = true;
}// You can do other processing here; loop runs fast due to micros()
}🔧 Notes:
You must select the correct board in the Arduino IDE: "Seeed XIAO ESP32C3".
Software PWM is handled manually via micros() to maintain timing independently for both pins.
Hardware PWM is supported natively via analogWrite() thanks to the ESP32's LEDC functionality.
For more precise control (e.g. frequency), you could use ledcAttachPin() and ledcWrite() with specific channels if you want hardware PWM tuning beyond > analogWrite().Let me know if you want to configure different frequencies or duty cycles per channel."
I then modified the code to make it easier for me to understand and adapt for networking purposes.
// Pin definitions
const int red1_pin = 4; // Hardware PWM pin connected to the red channel of the first LED strip
const int green1_pin = 3; // Hardware PWM pin connected to the green channel of the first LED strip
const int blue1_pin = 5; // Hardware PWM pin connected to the blue channel of the first LED strip
const int red2_pin = 21; // Hardware PWM pin connected to the red channel of the second LED strip
const int green2_pin = 9; // Hardware PWM pin connected to the green channel of the second LED strip
const int blue2_pin = 8; // Hardware PWM pin connected to the blue channel of the second LED strip
const int spot1_pin = D4; // Software PWM pin connected to the first raw of spots
const int spot2_pin = D7; // Software PWM pin connected to the second raw of spots
// initial PWM values
int red1 = 255; // set initial duty cycle for red1 to 100%
int green1 = 0; // set initial duty cycle for green1 to 0%
int blue1 = 0; // set initial duty cycle for blue1 to 0%
int red2 = 255; // set initial duty cycle for red2 to 100%
int green2 = 0; // set initial duty cycle for green2 to 0%
int blue2 = 0; // set initial duty cycle for blue2 to 0%
int spot1 = 128; // set initial duty cycle for spot1 to 50%
int spot2 = 128; // set initial duty cycle for spot1 to 50%
// Software PWM settings
const int swPWMFreq = 500; // Frequency in Hz
const int swPWMResolution = 8; // 8-bit resolution (0-255)
unsigned long swPWMPeriod = 1000000UL / swPWMFreq; // Period in microseconds
unsigned long spot1_pulse = swPWMPeriod * spot1 / 255; // pulse length for spot1 in microseconds
unsigned long spot2_pulse = swPWMPeriod * spot2 / 255; // pulse length for spot2 in microseconds
unsigned long spot1_last_toggle = 0; // time since the last trigger of spot1
unsigned long spot2_last_toggle = 0; // time since the last trigger of spot2
bool spot1_state = false; // control variable if spot1 was triggered
bool spot2_state = false; // control variable if spot2 was triggered
void setup() {
// Initialize hardware PWM pins
pinMode(red1_pin, OUTPUT); // set the pin for the first red channel as an output
pinMode(green1_pin, OUTPUT); // set the pin for the first green channel as an output
pinMode(blue1_pin, OUTPUT); // set the pin for the first blue channel as an output
pinMode(red2_pin, OUTPUT); // set the pin for the second red channel as an output
pinMode(green2_pin, OUTPUT); // set the pin for the second green channel as an output
pinMode(blue2_pin, OUTPUT); // set the pin for the second blue channel as an output
// Initialize software PWM pins
pinMode(spot1_pin, OUTPUT); // set the pin for the first raw of spots as an output
pinMode(spot2_pin, OUTPUT); // set the pin for the second raw of spots as an output
}
void loop() {
spot1 = 200; // set duty cycle for spot1
spot2 = 200; // set duty cycle for spot2
spot1_pulse = swPWMPeriod * spot1 / 255; // recalculate pulse length for spot1
spot2_pulse = swPWMPeriod * spot2 / 255; // recalculate pulse length for spot2
unsigned long now = micros(); // define a counter for the momentary time and get the time after the start with the micros() function
// Software PWM for spot1
if (spot1_state && (now - spot1_last_toggle >= spot1_pulse)) { // if spot1_state and the momentary time minus the time since the last toggle is greater equal then the pulse length for spot 1 is true
digitalWrite(spot1_pin, LOW); // set the signal for spot1 low
spot1_last_toggle = now; // set the last last toggle "timer" to momentary time
spot1_state = false; // set the control variable for spot1 to false
} else if (!spot1_state && (now - spot1_last_toggle >= (swPWMPeriod - spot1_pulse))) { // if spot1_state is false and the momentary time minus the time since the last toggle is greater equal then the period length minus the pulse length for spot 1 is true
digitalWrite(spot1_pin, HIGH); // set the signal for spot1 high
spot1_last_toggle = now; // set the last last toggle "timer" to momentary time
spot1_state = true; // set the control variable for spot1 to true
}
// Software PWM for spot2
if (spot2_state && (now - spot2_last_toggle >= spot2_pulse)) { // repeat for spot2
digitalWrite(spot2_pin, LOW);
spot2_last_toggle= now;
spot2_state = false;
} else if (!spot2_state && (now - spot2_last_toggle >= (swPWMPeriod - spot2_pulse))) {
digitalWrite(spot2_pin, HIGH);
spot2_last_toggle = now;
spot2_state = true;
}
analogWrite(red1_pin, red1); // output pwm signal related to the duty cycle value
analogWrite(green1_pin, green1); // output pwm signal related to the duty cycle value
analogWrite(blue1_pin, blue1); // output pwm signal related to the duty cycle value
analogWrite(red2_pin, red2); // output pwm signal related to the duty cycle value
analogWrite(green2_pin, green2); // output pwm signal related to the duty cycle value
analogWrite(blue2_pin, blue2); // output pwm signal related to the duty cycle value
}
For future integrations, a second spot channel was added, even though no second row of spotlights is currently installed. The first RGB LED strip is mounted on top of the shelf, while the second strip is placed inside the shelf. The spotlights are positioned above the counter.
What I Learned This Week?
- Working with displays is easier than I initially expected.
- Exploring the internal structure of libraries provides better insight than using them as black boxes.
- The XIAO ESP32-C3 supports only 6 hardware PWM channels, which I had already learned in week 04 but had forgotten.
What I Want to Improve Next Week
- Improve my time management. I tried to maximize output again, which didn’t work. I need to take a step back to complete everything on time.
Design Files
dev board project
oled board schematic
oled board traces
oled board holes outline
light controller schematic
adapter schematic
adapter traces
adapter holes outline
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