[Rust] Controlling LED Brightness with a Potentiometer
Table of Contents
Overview
This guide demonstrates how to control LED brightness using a potentiometer (variable resistor) with Rust. Since Raspberry Pi cannot handle analog input natively, we use an ADC (Analog-to-Digital Converter) module to convert analog values to digital values. Based on the read values, we adjust the LED brightness by modifying the duty cycle of PWM (Pulse Width Modulation).
Components
The components required for this project are:
- Raspberry Pi (40 GPIO) × 1
- GPIO Extension Board & Ribbon Cable × 1
- Breadboard × 1
- Rotary potentiometer × 1
- Resistor 10kΩ × 2
- ADC module (PCF8591 or ADS7830) × 1
- LED × 1
- Jumper Wire M/M × 17
Circuit
For the circuit diagram, please refer to Freenove’s documentation. You can build the circuit using either the PCF8591 or ADS7830 ADC module.
Code
Add rppal and ctrlc to your Cargo.toml and implement the following code.
This code auto-detects the I2C device (PCF8591 or ADS7830) on the I2C bus and reads analog values from the potentiometer. Based on the read values, it adjusts LED brightness using software PWM control running in a separate thread.
use std::error::Error;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use rppal::gpio::Gpio;
use rppal::i2c::I2c;
const PCF8591_ADDR: u16 = 0x48;
const ADS7830_ADDR: u16 = 0x4b;
// GPIO 17 (BCM)
const LED_PIN: u8 = 17;
fn main() -> Result<(), Box<dyn Error>> {
println!("Program is starting ...");
// Initialize I2C
let mut i2c = I2c::new()?;
// Detect I2C device with retries
let mut is_pcf8591 = None;
for _ in 0..5 {
if i2c.set_slave_address(PCF8591_ADDR).is_ok() && i2c.read(&mut [0]).is_ok() {
is_pcf8591 = Some(true);
break;
} else if i2c.set_slave_address(ADS7830_ADDR).is_ok() && i2c.read(&mut [0]).is_ok() {
is_pcf8591 = Some(false);
break;
}
thread::sleep(Duration::from_millis(100));
}
let is_pcf8591 = match is_pcf8591 {
Some(v) => v,
None => {
eprintln!("No correct I2C address found after retries,");
eprintln!("Please use command 'i2cdetect -y 1' to check the I2C address!");
eprintln!("Program Exit.");
std::process::exit(-1);
}
};
println!(
"Detected I2C device: {}",
if is_pcf8591 { "PCF8591" } else { "ADS7830" }
);
// Shared state for SoftPWM
let running = Arc::new(AtomicBool::new(true));
let duty_cycle = Arc::new(AtomicU8::new(0));
// Spawn SoftPWM thread
let pwm_handle = {
let running = running.clone();
let duty_cycle = duty_cycle.clone();
thread::spawn(move || {
let gpio = match Gpio::new() {
Ok(g) => g,
Err(e) => {
eprintln!("Failed to access GPIO: {}", e);
return;
}
};
let mut pin = match gpio.get(LED_PIN) {
Ok(p) => p.into_output(),
Err(e) => {
eprintln!("Failed to get GPIO pin {}: {}", LED_PIN, e);
return;
}
};
// 1 kHz frequency = 1000 us period
let period_micros = 1000u64;
while running.load(Ordering::SeqCst) {
let duty = duty_cycle.load(Ordering::SeqCst) as u64;
if duty == 0 {
pin.set_low();
thread::sleep(Duration::from_micros(period_micros));
} else if duty == 255 {
pin.set_high();
thread::sleep(Duration::from_micros(period_micros));
} else {
// Calculate on/off times
// duty is 0..255
let on_time = (period_micros * duty) / 255;
let off_time = period_micros - on_time;
pin.set_high();
thread::sleep(Duration::from_micros(on_time));
if off_time > 0 {
pin.set_low();
thread::sleep(Duration::from_micros(off_time));
}
}
}
// Turn off LED on exit
pin.set_low();
})
};
// Setup CTRL-C handler
let running_clone = running.clone();
ctrlc::set_handler(move || {
println!("\nEnding program");
running_clone.store(false, Ordering::SeqCst);
})?;
// Main loop
while running.load(Ordering::SeqCst) {
let value_result: Result<u8, Box<dyn Error>> = if is_pcf8591 {
// PCF8591
i2c.set_slave_address(PCF8591_ADDR)
.and_then(|_| i2c.write(&[0x40]))
.and_then(|_| {
let mut buf = [0u8; 1];
i2c.read(&mut buf)?;
i2c.read(&mut buf)?;
Ok(buf[0])
})
.map_err(|e| e.into()) // Convert rppal::i2c::Error to Box<dyn Error>
} else {
// ADS7830
i2c.set_slave_address(ADS7830_ADDR)
.and_then(|_| i2c.write(&[0x84]))
.and_then(|_| {
let mut buf = [0u8; 1];
i2c.read(&mut buf)?;
Ok(buf[0])
})
.map_err(|e| e.into()) // Convert rppal::i2c::Error to Box<dyn Error>
};
match value_result {
Ok(value) => {
// Update PWM duty cycle
duty_cycle.store(value, Ordering::SeqCst);
// Display info
// Voltage reference 3.3V
let voltage = (value as f64 / 255.0) * 3.3;
println!("ADC Value : {}, Voltage : {:.2}", value, voltage);
}
Err(e) => {
eprintln!("Error reading I2C: {}", e);
// Optional: add a small delay or just continue to retry
}
}
thread::sleep(Duration::from_millis(30));
}
// Wait for PWM thread to finish
let _ = pwm_handle.join();
Ok(())
}Summary
We implemented I2C communication in Rust to read values from an ADC module and control LED brightness using those values. By using software PWM implementation, we achieved smooth control. Separating the PWM control and the main ADC reading loop into different threads ensures responsive operation.



![[Rust] Blinking an LED with a Raspberry Pi Learning Kit](https://b.rmc-8.com/img/2025/06/01/1fbe657aa246865432be2e1d53002531.jpg)


Loading comments...