Chapters

16 Visual and Audio Actuators: LED Feedback

actuators
visual
audio

16.1 Start With the Decision

A cold-room alarm should not go silent when one output path fails. LEDs and sound must expose state through separate cues.

16.2 Route Overview

This is part 1 of 2. Continue with Visual and Audio Actuators: Displays and Buzzers.

16.3 Part Objectives

  • Choose LED colour, drive, and brightness for a warning.
  • Compare visual and audio feedback under failure and access needs.

16.4 Start With the Story

Make the Local Warning Fail Loudly

Picture a cold-room panel with a green lamp, a red lamp, and a buzzer. A loose display lead must not turn a real high-temperature alarm into silence. The first design question is what a nearby person can still see or hear when one output path fails.

GPIO means a general-purpose input/output pin used for a direct control line. I2C is a shared two-wire link for nearby parts, while SPI is a clocked link with separate data paths. Modulation means changing a signal over time to carry a control value. PWM means pulse-width modulation: the output switches on and off, and the on-time share sets the apparent brightness or drive level.

Command each lamp and tone alone, then together. Disconnect one lead, dim the supply, repeat the alarm, and restart the board while the unsafe input remains present. Record the physical light or sound beside the command and time so a neat screen cannot hide a failed local warning.

This runway does not prove that every person will notice an alert or that an output is safe for every load. The deeper sections cover current limits, timing, display choices, sound patterns, power budgets, and multi-modal design.

Imagine a leak detector hidden under a sink. A dashboard notification may be useful later, but the device needs a local story too: flash, beep, show status, and make the warning impossible to miss in the place where the problem happens.

Visual and audio actuators turn invisible system state into human feedback. Choose LEDs, displays, buzzers, tones, and patterns around attention, power, environment, and how quickly a person must understand the message.

In 60 Seconds

Visual actuators (LEDs, LCD/OLED displays, addressable LED strips) and audio actuators (passive and active buzzers) provide essential user feedback in IoT devices. LEDs are controlled via PWM for brightness dimming, addressable strips like NeoPixels allow individual pixel control over a single data wire, and passive buzzers generate variable-frequency tones for alerts and melodies.

The mathematical gist. An 8-bit LED command has a 1/255=0.392%1/255=0.392\% duty step and an ideal 49.9 dB quantiser SNR. A camera samples the 5 kHz PWM in time, so falias=fPWMround(fPWM/fs)fsf_{alias}=|f_{PWM}-\operatorname{round}(f_{PWM}/f_s)f_s| can fold invisible switching into a visible 8-20 Hz beat.

Math Bridge · guided foundationsWhy can a fast LED still flicker on camera?Let Max connect 8-bit brightness steps and the chapter's 5 kHz PWM to camera beat frequency.

Learning Objectives

After completing this chapter, you will be able to:

  • Configure PWM parameters to dim LEDs across 256 brightness levels
  • Program addressable RGB LED strips (NeoPixels/WS2812B) for independent pixel control
  • Explain why LED matrix blocks use driver ICs such as MAX7219/MAX7221 instead of direct GPIO control
  • Interface LCD and OLED displays using I2C/SPI communication protocols
  • Generate tones and melodies with passive buzzers using frequency modulation
  • Design multi-modal visual and audio feedback systems for IoT applications
Quick Check: Feedback Modality Selection

Think of how a microwave beeps when your food is ready, or how a traffic light changes color to tell you when to stop or go. Visual actuators (like LEDs and screens) and audio actuators (like buzzers) are the ways IoT devices communicate with people. They turn invisible data into something you can see or hear, making smart devices feel responsive and helpful.

Chapter Roadmap
  • Start With the Story
  • In 60 Seconds
  • Phoebe’s Field Notes: Two Physical Limits Hiding Behind “256 Levels” And “5 kHz”
  • Quick Check: Feedback Modality Selection
  • For Beginners: Visual and Audio Feedback
  • LED Control
  • LED Current-Limiting Resistor Calculator
  • How It Works: LED Brightness Control via PWM
  • Interactive PWM Power Calculator
  • Try It: RGB Color Mixer
  • Checkpoint: LED Feedback
  • Addressable LED Strips (NeoPixel/WS2812B)
  • NeoPixel Strip Power Calculator
  • LED Matrices and Driver ICs
  • Checkpoint: Pixel Chains and Matrices
  • LCD Displays
  • Try It: LCD Character Layout Planner

16.5 LED Control

LEDs (Light Emitting Diodes) are the simplest visual actuators, used for indicators, status lights, and ambient lighting.

Calculate the required series resistor for safe LED operation:

How It Works: LED Brightness Control via PWM

You might think dimming an LED means reducing voltage, but that’s not how it works. Here’s the trick:

Step 1: On/Off at High Speed - Instead of lowering voltage (which would change LED color), the microcontroller rapidly switches the LED fully ON and fully OFF — thousands of times per second (typically 1-20 kHz).

Step 2: Vary the Duty Cycle -

  • 100% brightness = LED on 100% of the time
  • 50% brightness = LED on 50%, off 50% (alternating)
  • 10% brightness = LED on 10%, off 90%

Step 3: Your Eye Averages It - The switching happens so fast (>500 Hz) that your eye perceives a smooth brightness level, not flickering. It’s like spinning a fan — individual blades blur into a circle.

Real-World Analogy: Imagine a light switch you flick on and off 1,000 times per second. If you keep it on 70% of the time (on for 0.7 ms, off for 0.3 ms in each 1 ms cycle), your eye sees 70% brightness — even though the LED is always either fully ON or fully OFF, never “dim.”

Why This Method?

  • Maintains LED color accuracy (full voltage = correct wavelength)
  • No heat dissipation in resistors (efficient)
  • Fine brightness control (8-bit PWM = 256 levels from 0-255)
  • Works with any LED without special dimming circuits

Calculate power consumption and energy savings for LED brightness control:

Example: At 50% PWM brightness (duty cycle = 0.5), an LED with forward voltage Vf=2.1V_f = 2.1 V and If=20I_f = 20 mA appears half as bright and uses half the power. Average current is Iavg=0.5×20=10I_{avg} = 0.5 \times 20 = 10 mA, so power is P=Vf×Iavg=2.1×0.01=0.021P = V_f \times I_{avg} = 2.1 \times 0.01 = 0.021 W. Over 24 hours, this saves E=(0.0420.021)×24=0.5E = (0.042 - 0.021) \times 24 = 0.5 Wh compared to full brightness.

16.5.1 Basic LED with PWM Brightness

#define LED_PIN 25

void setup() {
  // Configure PWM for LED dimming
  ledcSetup(0, 5000, 8);  // Channel 0, 5kHz, 8-bit resolution
  ledcAttachPin(LED_PIN, 0);
}

void loop() {
  // Fade in
  for (int brightness = 0; brightness <= 255; brightness++) {
    ledcWrite(0, brightness);
    delay(10);
  }

  // Fade out
  for (int brightness = 255; brightness >= 0; brightness--) {
    ledcWrite(0, brightness);
    delay(10);
  }
}

16.5.2 RGB LED Control

Read the RGB example as three coordinated PWM channels. Setup assigns one channel to each colour lead; setColor() then writes independent red, green, and blue intensities; the loop combines them into primary colours, secondaries, and white. Verify common-anode or common-cathode wiring before interpreting the values, because the wrong polarity reverses the apparent intensity command.

// RGB LED pins (common cathode)
#define RED_PIN 25
#define GREEN_PIN 26
#define BLUE_PIN 27

void setup() {
  // Configure PWM for each color channel
  ledcSetup(0, 5000, 8);  // Red
  ledcSetup(1, 5000, 8);  // Green
  ledcSetup(2, 5000, 8);  // Blue

  ledcAttachPin(RED_PIN, 0);
  ledcAttachPin(GREEN_PIN, 1);
  ledcAttachPin(BLUE_PIN, 2);
}

void setColor(int red, int green, int blue) {
  ledcWrite(0, red);
  ledcWrite(1, green);
  ledcWrite(2, blue);
}

void loop() {
  setColor(255, 0, 0);    // Red
  delay(1000);
  setColor(0, 255, 0);    // Green
  delay(1000);
  setColor(0, 0, 255);    // Blue
  delay(1000);
  setColor(255, 255, 0);  // Yellow
  delay(1000);
  setColor(0, 255, 255);  // Cyan
  delay(1000);
  setColor(255, 0, 255);  // Magenta
  delay(1000);
  setColor(255, 255, 255);// White
  delay(1000);
}
Try It: RGB Color Mixer
Motor MaxCheckpoint: LED Feedback

You now know:

  • A visible indicator still needs electrical limits: the resistor calculator uses supply voltage, LED forward voltage, and desired current.
  • PWM controls perceived brightness by changing duty cycle, not by lowering the LED’s forward voltage.
  • The RGB examples use three PWM channels with 8-bit values from 0 to 255, so color choice and current budget move together.

With single LEDs and RGB packages under control, the next question is how to scale beyond three channels without spending one GPIO per color.

16.6 Addressable LED Strips (NeoPixel/WS2812B)

Addressable LEDs allow individual control of each LED in a strip using a single data wire.

Calculate power requirements for addressable LED strips:

Before accepting the calculator result, inspect Figure to connect per-pixel current to the repeated physical load. WS2812B pixels draw approximately 60 mA at full white and about 20 mA for a single colour, so strip length and credible display state set the supply requirement.

A flexible strip of small square RGB LED modules connected in a row
A WS2812B strip like this is the hardware behind the note above: each pixel draws about 60mA at full white, so a full strip's current adds up fast enough to need an external supply sized with the recommended 20% safety margin, not USB power. Photo: nabseguf, CC BY-SA 4.0

In Figure, follow the flexible conductors past each repeated RGB pixel. Data is forwarded along the chain, but power current accumulates across all illuminated channels; that is why the calculator adds every pixel and then applies margin rather than treating the data pin or USB connector as the load supply.

CheckArithmetic carried throughResult to use in the review
Single LED full power2.1 V x 0.020 A = 0.042 WOne full-bright indicator is about 42 mW.
50% PWM LED current0.50 x 20 mA = 10 mA; 2.1 V x 0.010 A = 0.021 WThe same LED is about 21 mW before driver losses.
24-hour PWM saving(0.042 W - 0.021 W) x 24 h = 0.504 WhRound at the end: about 0.5 Wh saved per LED-day.
5 kHz PWM timing1 / 5000 = 0.0002 s = 0.2 ms; 0.2 ms x 0.50 = 0.1 msA 50% command is roughly 100 microseconds on and 100 microseconds off.
8-bit brightness step1 / 255 = 0.0039216; 5 V x 0.0039216 = 0.0196 VOne code step is about 0.392%, or 19.6 mV of ideal average on a 5 V LED driver.
30-pixel full-white strip30 x 60 mA = 1800 mA = 1.8 A; 5 V x 1.8 A = 9 WFull-white worst case needs a supply sized above 1.8 A.
20% supply margin1.8 A x 1.20 = 2.16 AChoose at least a 2.16 A 5 V supply for the full-white 30-pixel case.
Code brightness limit50 / 255 = 0.1961; 1.8 A x 0.1961 = 0.353 A; 5 V x 0.353 A = 1.76 WThe code’s setBrightness(50) keeps a full-white frame near 0.35 A, but release evidence should still check startup and fault cases.

The audit conclusion is bounded: PWM reduces average LED power, and the strip code greatly lowers the steady full-white current. It does not remove the need to size wiring, ground return, fuse behavior, and supply margin for the highest credible visual state.

#include <Adafruit_NeoPixel.h>

#define LED_PIN 18
#define NUM_LEDS 30

Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);

void setup() {
  strip.begin();
  strip.setBrightness(50);  // 0-255 (limit current draw)
  strip.show();
}

void loop() {
  rainbow(10);                                // Cycle all hues
  colorWipe(strip.Color(0, 255, 0), 50);     // Green wipe
  colorWipe(strip.Color(127, 0, 0), 50);     // Red wipe
}

// Animate a rainbow across the strip
void rainbow(int wait) {
  for (long hue = 0; hue < 65536; hue += 256) {
    for (int i = 0; i < strip.numPixels(); i++) {
      int pixelHue = hue + (i * 65536L / strip.numPixels());
      strip.setPixelColor(i, strip.gamma32(strip.ColorHSV(pixelHue)));
    }
    strip.show();
    delay(wait);
  }
}

// Fill strip one pixel at a time
void colorWipe(uint32_t color, int wait) {
  for (int i = 0; i < strip.numPixels(); i++) {
    strip.setPixelColor(i, color);
    strip.show();
    delay(wait);
  }
}

16.7 LED Matrices and Driver ICs

An LED matrix block packs many ordinary LEDs into a row/column grid. An 8x8 matrix has 64 emitters, but the package usually exposes row and column pins rather than 64 separate LED pairs. The controller lights a pattern by selecting one row or column at a time fast enough that the eye sees a stable image. That multiplexing saves pins, but it creates two practical problems: the firmware must refresh the matrix continuously, and the current peaks through the active row or column must stay inside the matrix and driver ratings.

This is why small LED matrix kits usually include more than the visible block. A review should identify the matrix block, socket or carrier PCB, headers, passive parts, and the display-driver IC before treating the kit as “just LEDs.” The driver owns the repetitive refresh and current-limited switching while the microcontroller sends compact display data.

The MAX7219/MAX7221 family is a common example. It sits between the MCU and the matrix: DIN receives serial data, CLK clocks bits in, LOAD or CS latches a command, and DOUT can pass data to another driver. On the display side, the DIG0 through DIG7 pins select rows or digits, while SEG A through SEG G and SEG DP drive the eight column or segment lines. V+ and GND power the chip, and the ISET resistor sets the peak segment current.

An 8x8 LED dot matrix display module with a grid of small LEDs and header pins along one edge
An 8x8 LED dot-matrix block like this sits on the display side of the MAX7219/MAX7221 driver described above -- its DIG0-DIG7 and SEG A-G pins multiplex the 64 emitters so the row/column grid can be refreshed fast enough to look like a stable image. Photo: Shahbaz75, CC BY-SA 4.0

For a raw 8x8 LED matrix, leave decode mode off and write row or column bit patterns directly. The register addresses are part of the wiring evidence: 0x01 through 0x08 hold the eight digit or row data bytes, 0x09 controls decode mode, 0x0A controls intensity, 0x0B sets the scan limit, 0x0C leaves shutdown mode, and 0x0F enables or disables display test. A first proof can write a diagonal or single-row sweep, then record whether the image is rotated, mirrored, or missing rows.

#include <LedControl.h>

#define DIN_PIN 23
#define CLK_PIN 18
#define CS_PIN 5

LedControl matrix(DIN_PIN, CLK_PIN, CS_PIN, 1);  // one MAX7219 device

void setup() {
  matrix.shutdown(0, false);   // leave shutdown mode
  matrix.setIntensity(0, 4);   // 0-15 brightness range
  matrix.clearDisplay(0);
}

void loop() {
  for (int row = 0; row < 8; row++) {
    matrix.clearDisplay(0);
    matrix.setRow(0, row, 1 << row);  // diagonal orientation test
    delay(150);
  }
}

The chip interface is small, but it is still a protocol. Each update is a 16-bit instruction: one address byte selects the target register and one data byte supplies the value. CLK advances each bit, DIN carries the bit value, and LOAD or CS latches the complete command. If the display is blank, check shutdown mode first, then confirm that the data, clock, and load pins match the library constructor.

Treat the display library as an adapter, not as magic. A library such as LedControl hides the bit shifting and latch timing, but the review still needs to name the processor-side pins, the number of chained devices, the matrix orientation, and the character table used by the application. A character library is usually just a set of eight-byte patterns: each byte represents one row or column of an 8x8 glyph. That makes the first message display a useful integration test because it proves both the wiring and the glyph orientation.

const byte smile[8] = {
  B00111100,
  B01000010,
  B10100101,
  B10000001,
  B10100101,
  B10011001,
  B01000010,
  B00111100
};

void drawGlyph(int device, const byte glyph[8]) {
  for (int row = 0; row < 8; row++) {
    matrix.setRow(device, row, glyph[row]);
  }
}

When several LED blocks are chained, DOUT from one driver feeds DIN on the next and the constructor’s device count changes. That is the main code difference between a one-block demo and a multi-block message board. Keep the physical order explicit: device 0 might be the block nearest the microcontroller or the farthest block, depending on how the modules are wired. A scrolling message, a dice face, or a small game animation should be treated as a final demo after the evidence path has already proved power, orientation, scan limit, intensity, and per-block addressing.

Keep the evidence close to the hardware. Record supply voltage, common ground, library version, DIN/CLK/CS pins, matrix orientation, current setting, and the known-good test pattern. If the first display appears scrambled, fix the row/column mapping or module orientation before adding animations; otherwise the software may hide a wiring error behind a pretty pattern.

Motor MaxCheckpoint: Pixel Chains and Matrices

Before applying the specification, inspect the real buzzer (piezo) below: its package, terminals, scale, and installation context are part of the engineering evidence.

Real photograph of buzzer (piezo)
This real example (Electromagnetic buzzer 01) shows a physical form of buzzer (piezo). Use the visible package, interfaces, scale, mounting, and surrounding context as evidence; a catalogue label alone does not establish deployment fit. Photo: jdx; CC BY-SA 3.0

Carry those visible constraints into the surrounding analysis; the abstract symbol or capability name does not capture mounting, wiring, protection, or service access.

You now know:

  • WS2812B pixels trade GPIO count for power and timing discipline: full white is about 60 mA per pixel.
  • A 30-pixel full-white case reaches 1.8 A, so the data pin is not the power path.
  • Matrix drivers such as MAX7219/MAX7221 prove orientation, scan limit, intensity, and row data before animations hide wiring mistakes.

Once the output needs words, values, or menus instead of colored states, a display becomes the clearer actuator.

16.8 LCD Displays

Before applying the specification, inspect the real oled display (ssd1306) below: its package, terminals, scale, and installation context are part of the engineering evidence.

Real photograph of oled display (ssd1306)
This real example (Motorola-Timeport-P8767-OLED-OEL-display) shows a physical form of oled display (ssd1306). Use the visible package, interfaces, scale, mounting, and surrounding context as evidence; a catalogue label alone does not establish deployment fit. Photo: Babca; CC BY-SA 4.0

Carry those visible constraints into the surrounding analysis; the abstract symbol or capability name does not capture mounting, wiring, protection, or service access.

Before applying the specification, inspect the real 16x2 character lcd display below: its package, terminals, scale, and installation context are part of the engineering evidence.

Real photograph of 16x2 character lcd display
This real example (LCD display 16x2 alphanumeric) shows a physical form of 16x2 character lcd display. Use the visible package, interfaces, scale, mounting, and surrounding context as evidence; a catalogue label alone does not establish deployment fit. Photo: User:Mike1024; Public domain

Carry those visible constraints into the surrounding analysis; the abstract symbol or capability name does not capture mounting, wiring, protection, or service access.

16.8.1 16x2 LCD with I2C

Inspect Figure before writing the display loop. The visible two-row grid is the application’s hard information budget, while the pin header or I2C backpack is only the transport used to fill those cells.

A blue 16-column by 2-row character LCD module with a row of interface pins
A 16x2 character LCD gives the code below exactly two rows of sixteen character cells. The module's exposed interface pins are why many builds add an I2C backpack at address 0x27: it reduces the microcontroller wiring while leaving the visible layout constraint unchanged. Photo: oomlout, CC BY-SA 2.0

In Figure, read the sixteen columns across the first row and then the second row before noticing the interface pins. The code’s cursor coordinates and truncation behavior must fit that visible geometry; I2C reduces wiring but does not expand the display’s character capacity.

#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);  // Address 0x27, 16 columns, 2 rows

void setup() {
  lcd.init();
  lcd.backlight();

  lcd.setCursor(0, 0);
  lcd.print("IoT System");
  lcd.setCursor(0, 1);
  lcd.print("Initializing...");

  delay(2000);
}

void loop() {
  // Display temperature
  float temperature = 25.4;

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Temperature:");
  lcd.setCursor(0, 1);
  lcd.print(temperature);
  lcd.print(" C");

  delay(2000);

  // Display humidity
  float humidity = 65.2;

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Humidity:");
  lcd.setCursor(0, 1);
  lcd.print(humidity);
  lcd.print(" %");

  delay(2000);
}
Try It: LCD Character Layout Planner

16.9 Continue to the Next Part

Carry this evidence into Visual and Audio Actuators: Displays and Buzzers, which begins with OLED Displays.