LDR in AVR

This project was an exploration into how to use the ADC and the fast PWM signaling on the AVR architecture. Although there is nothing revolutionary going on in this project, I wanted to start building a library of Embedded C AVR functions. Firstly, to get better at embedded systems work because I find it really fun and satisfying to create pieces of technology that people use in everyday life and I want to build up to more ambitious projects using the modern tools of the trade (32-bit ARM Microcontrollers, USB and other complex serial protocols, etc.) For now, 8-bit AVR with a simple sensor input (in the form of a photoresistor) and feedback LED which has a brightness controlled by a pulse width modulated signal from the microcontroller.

The microcontroller of choice was an ATtiny261 for the simple reason that I found a lot of these while dumpster diving near the robotics lab. It still worked perfectly fine and programmed with out issue with help from my Atmel ICE and ISP Breadboard Adapter (check that out here!).

Moving onto the firmware, I break all my AVR programs into 6 sections for each file.

  • Macros
  • Includes
  • Globals
  • Functions
  • Interrupt Service Routines
  • Main

Macros
/******************** Macros *****************************/
#ifndef F_CPU
#define F_CPU 1000000UL //Set clock speed to 1MHz
#endif
#define BIT_SET(byte, bit) (byte & (1<<bit))
                                        

Starting with macros, I only specified the CPU clock speed which is mandatory to ensure any delay statements work properly (Need to have some reference to count the ticks). With no external clock for simplicity and no concern about temperature change for this demo, I set the AVR Fuse bits to use the internal 8MHz RC Oscillator with 65ms delay and divide the clock by 8. This is generally inadvisable since the internal oscillator is susceptible to significant clock skew and jitter as there are environmental/temperature changes around your microcontroller. However, you can improve the performance by increasing the start-up delay to 65ms and dividing the clock.

Includes
/******************** Includes ***************************/
#include <avr/io.h>
                                                            

In the includes section, I include the avr/io.h library to get all the macros relating to the input/output pins of this microcontroller.

Globals

No globals were needed for this project, so the section is left empty.

Functions
/******************** Functions **************************/
//Initialize the PWM Output for OCR1B, set the max value in OCR1B
void PWM_init(){
	//Enable PWM Output B
	TCCR1A = (1<<PWM1B) | (0<<COM1B1) | (1<<COM1B0);
	//Enable Clock Divider/64
	TCCR1B = (0<<CS13)|(1<<CS12)|(1<<CS11)|(1<<CS10);
	//Enable Fast PWM Mode
	TCCR1D = (0<<WGM11)|(0<<WGM10);
	//Use PLL Clock for PWM Generator
	PLLCSR |= (1<<PCKE)|(1<<PLLE);
	//Max PWM value possible set though OCR1C
	OCR1C = 0xFF;
	return; //Return to call point
}

//Initialize the ADC
void ADC_init(){
	//Use VCC for the analog reference voltage, left justify result, only use ADC0 for input
	ADMUX = (0<<REFS1)|(0<<REFS0)|(0<<ADLAR)|(0<<MUX4)|(0<<MUX3)|(0<<MUX2)|(0<<MUX1)|(0<<MUX0);
	//Enable ADC, use for single conversion mode, clk/8 prescaler
	ADCSRA = (1<<ADEN)|(0<<ADPS2)|(1<<ADPS1)|(1<<ADPS0);
	//Use ADC0 for input
	ADCSRB = (0<<MUX5);
	return; //Return to call point
}

//Reads the value from the ADC (Battery level)
uint16_t ADC_value(){
	//Pass bit in to get request measurement
	ADCSRA |= (1<<ADSC);
	//Wait for conversion
	while(BIT_SET(ADCSRA, ADSC)) {}
	//Return the left justified result
	return ADC;
}

In the function section I have functions for the initialization for the fast PWM signaling, 10-bit ADC, and obtaining an ADC value. I will cover these functions in more detail as they are encountered in main.

Interrupt Service Routines

No interrupt service routines as they are not used in this program.

Main
/******************** Main *******************************/
int main(void)
{
    /* Initialize the I/O Registers */
    /*				-I/O Map-
        *	Reactive LED: PB3 (OC1B Timer Counter) (1: Output)
        *  Light Dependent Voltage: PA0 (0: Input)
        */
    DDRB |= (1<<PB3);
    //Set the default values for outputs to zero and inputs to have pull-up resistors
    PORTB |= (0<<PB3);

    /* Initialize the timer/counter0 (Fast PWM Mode) */
    PWM_init();
    
    /* Initialize the analog input */
    ADC_init();

    /* Storage for the LDR value from the ADC */
    uint16_t LDRvalue = 0;
    
    /* State machine loop */
    while(1) 
    {
        //Get the LDR value
        LDRvalue = ADC_value();
        //Based on the LDR value set the brightness of the LED though OCR1B
        if(LDRvalue < 100){
            OCR1B = 255;
        }
        else if(LDRvalue >= 100 && LDRvalue < 290){
            OCR1B = 180;
        }
        else if(LDRvalue >= 290 && LDRvalue < 400){
            OCR1B = 120;
        }
        else if(LDRvalue >= 400 && LDRvalue < 800){
            OCR1B = 80;
        }
        else if(LDRvalue >= 800 && LDRvalue < 1000){
            OCR1B = 20;
        }
        else{
            OCR1B = 10;
        }
    }
}
                                                                                

In the main section there is the main entry point of the program. I start by initializing the outputs I have selected for the LED. PB3 was chosen because that this the timer output for Timer/Counter 1B which is the PWM timer that I will be configuring in the following function call PWM_init().
In PWM_init() I modify the timer/counter 1 control registers to enable output B, enable this timer/counter for its fast PWM option, set the PWM frequency to F_PLL/64. Below that is a more interesting register that enables a Phase Locked Loop that can be used to drive Timer/Counter 1 on the ATtiny261! Even though I am running the chip at 1MHz already I utilize the PLL to send a 1MHz signal to the timer. The PLL generates a 64MHz signal that I clock down to divide by 64 as previously mentioned. After the PWM registers are configured the OCR1C register controls max value for timer/counter 1, so I initialize it with the max value of 0xFF (8-bit max value).
Following, the PWM_init() function is ADC_init(). This function starts by modifying the ADMUX register to use the system VCC voltage as the reference for the 10-bit ADC. Meaning, if 5V powers the system and the ADC receives a voltage of 5V on the input, the value in the ADC result register will be 1024 (max 10-bit value), 2.5V will appear as 512, etc. The result of the ADC is also left justified which means that the result will be stored in bits 15-6 of the ADCH and ADCL registers. Right justified would place the result in bits 9-0. The following line adjusts ADCSRA to enable the ADC, set-up single conversion mode, and set the ADC to use a clock of F_CPU/8 (selected for clock stability reasons and speed is not a big concern in this context). Single conversion mode allows us to request the ADC to check its status when we request it to do so. This is opposed to continuous conversion mode where the ADC is constantly updating its value. The MUX bits are also adjusted throughout these registers to initialize ADC0 as an input for the ADC.
After initializing our special functions, I declare a 16-bit integer to hold our value from the ADC ‘LDRvalue’. After that we move into the stare machine loop that will repeat forever.
In the main loop we call the ADC_value() to initiate that single conversion transaction from the ADC. The LDR/ADC value is then put through an if else block to determine the OCR1B to change the duty cycle of the PWM signal and therefore the brightness of the LED. The higher the resistance, the darker the room and the brighter the LED.

Thanks for reading this article! This is the start to many more embedded adventures.

Here’s the GitHub link to the project repository.

Project Links

Article last edited: August 2021