Controlling led’s brightness with arduino without IDE -embedded tutorials 2

Tobias Aguiar
8 min readMar 26, 2023

--

Photo by Harrison Broadbent on Unsplash

Preface

On the last embedded tutorial I said that if you want to take embedded development seriously, you need to forget a little bit about hardware abstractions, so you could get the full access to learn the low-level structure to develop a solid foundation in this field.

In this tutorial, following this line of not using hardware abstractions and IDEs.

Tools you’re going to need

Comparing with last tutorial, you’re going to need a bit more gear :

  1. An arduino board with its cable
  2. A potentiometer
  3. A led
  4. A 220 omh (minimum) resistor
  5. Some jumper wires

The idea

How can we see a led’s brightness? Usually a digital signal can be either 0V or 5V, which means on or off. Is it possible to see some brightness spectrum between this voltage?

With digital signals, certainly not.

If it’s not possible, how are we going to make this project happen?

Calm down, kid. Let me explain.

Fortunately we live in a analog word, where things are not just on or off. So, we can get an analog signal to control the led’s brightness.

But, how we can generate a variable tension to control the led?

Good question! That’s when the potentiometer comes into place. It will be the responsible for generating this variable tension.

The potentionemter

This is the main tool we’re going to have to be able to generate a tension between 0V and 5V.

Why?

Because the potentiometer is simply a variable resistance, we can construct a variable voltage divider, which is a classic circuit from your electronics classes. The circuit diagram is as follows:

potentiometer circuit diagram

Analog-to-Digital Converter (ADC) : the bridge between two worlds

We’re going to use a potentiometer to adjust the light intensity, but who said that the computer can comprehend this type of signal? In this article I explain why computers can’t understand such thing.

So, in order to make this signal a suitable one so the processor can comprehend and utilize the signal, that’s when the ADC comes in.

It is the bridge between the real, physical and analogical world and the digital one.

Thus, with and ADC we have a digital signal that can be processed by the CPU.

Pulse-Width Modulation (PWM) : the real light controller

Ok, now that we have a digital signal, how to send to led so it can control its brightness? Calm down, it does not work that way.

That is because LEDs work differently than traditional light bulbs, for example. Unlike a light bulb that gets brighter as you send it more power, LEDs have a fixed voltage drop across them, and if you send too much power, you can damage them.

And that’s when PWM comes in. In short, PWM is a technique for controlling the power sent to a device by turning it on and off rapidly (We can talk more about this topic in another article).

And that’s actually the secret behind “controlling” LED’s brightness. The fact is that the brightness itself is not controlled, what it does is turn it on and off really quickly so that it looks like it’s dimming.

That’s what PWM does — it turns the LED on and off rapidly, and by varying the duration of the “on” time versus the “off” time, it can create the illusion of dimming.

Sorry to disappoint you, but it is just an illusion.

Anyway, with the parts explained, let’s get to the project.

The setup

The setup will be made of the tools mentioned in the beginning of this article. Here’s a picture of it:

I know, it’s a bit crowded, but let’s summarize how to put the pieces together :

  1. Connect the Arduino board to your computer using the USB cable.
  2. Place the LED and the potentiometer on the breadboard. Connect the anode of the LED to digital pin 9 (or any other digital pin) on the Arduino board. Connect the cathode of the LED to a 220 (or higher) ohm resistor, and then connect the other end of the resistor to ground on the breadboard.
  3. Connect the middle pin of the potentiometer to analog pin 0 (or any other analog pin) on the Arduino board. Connect one of the outer pins of the potentiometer to 5V on the breadboard, and connect the other outer pin to ground.
  4. Connect a jumper wire from the 5V on the breadboard to the other side of the 220 ohm resistor.
  5. Create your C code and then execute it.

Too much theory, let’s get our hands dirty

This is getting too theoretical, let’s get to the best part.

The C code to make all this happen

Let’s take a look at the code :

#include <avr/io.h>
#include <util/delay.h>

#define LED_PIN PB1
#define POT_PIN PC0

void init_led() {
DDRB |= (1 << LED_PIN);
PORTB &= ~(1 << LED_PIN);
}

void init_adc() {
ADMUX |= (1 << REFS0);
ADCSRA |= (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1);
}

int read_adc() {
ADCSRA |= (1 << ADSC);
while (ADCSRA & (1 << ADSC));
return ADC;
}

void init_pwm() {
TCCR1A = (1 << COM1A1) | (1 << WGM11);
TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS11);
ICR1 = 255;
OCR1A = 0;
}

int main(void) {
init_led();
init_adc();
init_pwm();

while (1) {
int pot_value = read_adc();
int brightness = pot_value >> 2;

OCR1A = brightness;

_delay_ms(100);
}

return 0;
}

Let’s analyse this code together, but before : if you didn’t see my first embedded project tutorial with arduino, please read it here. I explain the basic parts, such as the headers inclusion and the main function.

#define LED_PIN PB1
#define POT_PIN PC0

These two lines use the preprocessor directive #define to create constants for the pin numbers that will be used for the LED’s digital pin and the potentiometer’s analog pin.

void init_led() {
DDRB |= (1 << LED_PIN);
PORTB &= ~(1 << LED_PIN);
}

This function initializes the LED pin by setting the corresponding bit in the DDRB register to 1, which configures it as an output pin, and by setting the corresponding bit in the PORTB register to 0, which sets the pin to a low state. I explain more about those registers on my first embedded tutorial article.

void init_adc() {
ADMUX |= (1 << REFS0);
ADCSRA |= (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1);
}

This function initializes the ADC by setting the voltage reference to use Vcc as the reference voltage, enabling the ADC by setting the corresponding bit in the ADCSRA register to 1, and setting the ADC prescaler to 64.

int read_adc() {
ADCSRA |= (1 << ADSC);
while (ADCSRA & (1 << ADSC));
return ADC;
}

This function reads the analog value from the potentiometer by setting the ADC start conversion bit in the ADCSRA register to 1, and waiting for the conversion to complete by waiting in a loop while the ADC is still busy. Once the conversion is complete, the function returns the 10-bit value stored in the ADC register.

void init_pwm() {
TCCR1A = (1 << COM1A1) | (1 << WGM11);
TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS11);
ICR1 = 255;
OCR1A = 0;
}

This function initializes the PWM output by setting the waveform generation mode to fast PWM with top value of 0xFF, enabling the PWM output on the OCR1A pin, setting the PWM prescaler to 8, and setting the initial duty cycle to 0.

int main(void) {
init_led();
init_adc();
init_pwm();

while (1) {
int pot_value = read_adc();
int brightness = pot_value >> 2;

OCR1A = brightness;

_delay_ms(100);
}

return 0;
}

This is the main function of the program. It calls the three initialization functions, and then enters an infinite loop that reads the potentiometer value, scales it down to an 8-bit value by bit-shifting right by two bits, sets the PWM output duty cycle to the scaled value, and then waits for 100 milliseconds before repeating the loop.

And how do I know these strange registers name? Datasheet.

The makefile

MCU = atmega328p
F_CPU = 16000000UL
PROGRAMMER_TYPE = arduino
PORT = /dev/ttyACM0
HEXFILE = main.hex
SOURCES = main.c
OBJECTS = $(SOURCES:.c=.o)
CFLAGS = -mmcu=$(MCU) -DF_CPU=$(F_CPU) -Wall -Os
AVRDUDE = avrdude

.PHONY: all clean install

all: $(HEXFILE)

$(HEXFILE): $(OBJECTS)
avr-gcc $(CFLAGS) -o $@ $(OBJECTS)
avr-objcopy -O ihex $@ $(HEXFILE)

$(OBJECTS): $(SOURCES)
avr-gcc $(CFLAGS) -c $(SOURCES)

clean:
rm -f $(OBJECTS) $(HEXFILE)

install: $(HEXFILE)
$(AVRDUDE) -p $(MCU) -c $(PROGRAMMER_TYPE) -P $(PORT) -b 115200 -D -U flash:w:$(HEXFILE)

Let’s go through this make file.

MCU = atmega328p
F_CPU = 16000000UL
PROGRAMMER_TYPE = arduino
PORT = /dev/ttyACM0
HEXFILE = main.hex
SOURCES = main.c
OBJECTS = $(SOURCES:.c=.o)
CFLAGS = -mmcu=$(MCU) -DF_CPU=$(F_CPU) -Wall -Os
AVRDUDE = avrdude

First, we define our variables:

  1. MCU = atmega328p : defines the target microcontroller as ATmega328P.
  2. F_CPU = 16000000UL : sets the clock frequency to 16 MHz.
  3. PROGRAMMER_TYPE = arduino : specifies the programmer type as “arduino”.
  4. PORT = /dev/ttyACM0 : specifies the serial port for the programmer.
  5. HEXFILE = main.hex : sets the output file name to “main.hex”.
  6. SOURCES = main.c : specifies the source code file as “main.c”.
  7. OBJECTS = $(SOURCES:.c=.o) : specifies the object file as “main.o”.
  8. CFLAGS = -mmcu=$(MCU) -DF_CPU=$(F_CPU) -Wall -Os : pecifies the compiler flags, including the microcontroller, clock frequency, warning level, and optimization level.
  9. AVRDUDE = avrdude : specifies the avrdude command to use for uploading the code to the microcontroller.
.PHONY: all clean install

This line specifies the targets that are not actual files, but rather are just commands to be executed.

all: $(HEXFILE)

This line specifies the default target to be built, which depends on the hex file.

$(HEXFILE): $(OBJECTS)
avr-gcc $(CFLAGS) -o $@ $(OBJECTS)
avr-objcopy -O ihex $@ $(HEXFILE)

This line specifies how to build the hex file from the object file using the avr-gcc and avr-objcopy commands.

$(OBJECTS): $(SOURCES)
avr-gcc $(CFLAGS) -c $(SOURCES)

This line specifies how to build the object file from the source file using the avr-gcc command.

clean:
rm -f $(OBJECTS) $(HEXFILE)

This line specifies how to clean up the object and hex files using the rm command.

install: $(HEXFILE)
$(AVRDUDE) -p $(MCU) -c $(PROGRAMMER_TYPE) -P $(PORT) -b 115200 -D -U flash:w:$(HEXFILE)

This line specifies how to upload the hex file to the microcontroller using avrdude.

Now, you’re good to execute. You can write :

make

Followed by

make install

Final results

Unfortunately, I can’t paste here my video where I show the project working.

Soon, I’ll provide a link to my instagram (if you want to see daily contents about embedded systems, follow me here) so you can see the video.

You can test by yourself, anyway.

As always, thank you for taking the time to read me!

More articles

  1. A harsh truth to deal with if you are starting embedded software development
  2. Master the fundamentals of embedded systems with these 2 must-ask questions

--

--

Tobias Aguiar

Software developer | Trying to make complex concepts look easy | Want help or discuss about embedded software development? Email me! tobi.aguiar01@gmail.com