LCD backlight and contrast manager

This article will present a circuit and accompanying source code that you can use to control the backlight and contrast functions of an HD44780-compatible LCD.

Controlling these functions from software allows you to cut down on external components such as dials, saving you space and money.

Goals of the project

The goals of this project are as follows

  • Control backlight and contrast via Arduino PWM pins.
  • Allow direct setting of values and fade-in/fade-out modes.
  • Provide power-saving functionality to switch off the backlight until woken up.

Circuit diagram

The circuit diagram shows the 16 pins of the HD44780 controller on the left together with our control circuitry on the right. Arduino pins are labelled Dn. You are free to use any Arduino pins that you like for the main LCD but you must use PWM pins for the ones labelled D9/D10 in the diagram.

The brightness of the LCD backlight will be controlled by a transistor used to regulate the current that flows through the internal LEDs. The contrast will be controlled by a filtered PWM signal generated by the Arduino.

Choosing the LCD

You can use any character LCD having a controller that is compatible with the HD44780. The cheapest source of these devices is usually direct from China from an ebay seller but try to get hold of the datasheet before you buy because nothing is likely to come with it. You can also buy them from online electronics stores or direct from a supplier such as CrystalFontz. The CrystalFontz devices come with a very comprehensive and well-written datasheet.

For this article I will use a 16×2 CrystalFontz CFAH1602B display.

Choosing R1

The resistor R1 should be chosen so that when the transistor is fully open the current that flows is just enough to light the backlight at full brightness. The datasheet for the LCD gives the maximum backlight current as 130mA. This value is low enough that we can connect LED+ to the 5V pin on the Arduino. If it were much higher then we would have to drive LED+ via a separate 7805 regulator to avoid overloading the Arduino

The datasheet applies Ohm’s law using the following equation to calculate an ideal value for R1.

6 ohms is not a popular size of resistor and I don’t have one to hand, nor do I have an easy combination that I could use in parallel, so I will default to the next one up that I have, 10 ohms. This will yield a current of 80mA.

Choosing Q1

Almost any general purpose NPN transistor will work here. You just need to check that it can handle the maximum current that you are going to supply to the backlight. In the transistor datasheet this will be referred to as the collector current, or Ic. I had a BC639 in my random bits box and it can handle 1000mA at the collector, more than enough for my needs.

Choosing R2

The resistor R2 is used to regulate the current to the base of the transistor. We need to choose a value for R1 that will allow the transistor to pass the desired 80mA when the maximum 5V average voltage comes from the Arduino PWM pin. The transistor equation is:

Iec=80mA and I measured the gain (beta) of the transistor using a multimeter to be 65. So, solving the equation we have Ieb = 1.23mA. Applying ohm’s law we get a value for R2 when the supply voltage is 5V of 4062 ohms. This is not a common value for a resistor and the nearest that I have is 3K3. This will result in 1.25mA. Close enough.

Choosing C1

The LCD ‘contrast’ pin Vee is used to control the LCD driving voltage. You can supply anywhere between 0V and Vcc (5V) and the LCD will then drive the display using a voltage of Vcc – Vee.

The problem is that the square wave PWM output from the Arduino at the default frequency of about 500Hz is not fast enough to fool the LCD into producing a continuous display. If you hook up the PWM pin directly to Vee then you will most likely see a shimmering effect as the LCD rapidly changes driving voltage.

The solution is to filter the PWM output using a low-pass capacitor so that the output wave looks more flat. It won’t be perfect, but it should be enough to get a steady display on the LCD throughout the acceptable contrast range. The value for the capacitor should be fairly large. Start with 100uF and if that’s not enough then try 220uF or 470uF.

This is what a PWM waveform looks like without any filtering.

After filtering with the 100uF capacitor shown in the circuit diagram the wave looks like this:

It’s far from a perfectly smooth wave but the deviations from the average are small enough that the LCD will appear to be perfectly steady.

The software interface

Here’s the header file that defines the interface to the adjustments manager.

/*
 * LCD Adjustments: Copyright (c) 2010 Andy Brown
 * http://www.andybrown.me.uk
 *
 * This work is licensed under a Creative Commons
 * Attribution_ShareAlike 3.0 Unported License.
 * http://creativecommons.org/licenses/by_sa/3.0/
 */

#ifndef __B66E1DAA_232C_4362_86C4_86F942C1F9C8
#define __B66E1DAA_232C_4362_86C4_86F942C1F9C8

#include <stdint.h>

/*
 * LCD Adjustments header file
 */

class LCDAdjustments
{
public:
	class PWMManager
	{
	private:
		uint8_t _pin;
		uint8_t _currentValue;
		uint8_t _defaultPercentage;

	public:
		// initialize on a PWM pin
		void setup(uint8_t pin_,uint8_t defaultValue_);

		// reset to the default percentage
		void reset();

		// set percent value immediately
		void setPercentage(uint8_t percentage_);

		// set absolute value immediately
		void setValue(uint8_t value_);

		// fade to the target percentage
		void fadeToPercentage(
				uint8_t targetPercentage_,
				uint32_t microsPerStep_);

		// fade to the target value
		void fadeToValue(
				uint8_t targetValue_,
				uint32_t microsPerStep_);

		// convert a percentage to an absolute value
		uint8_t percentageToValue(uint8_t value_) const;

		// get the current value
		uint8_t getCurrentValue() const;
	};

private:
	// brightness manager
	PWMManager _brightness;

	// contrast manager
	PWMManager _contrast;

	// saved power saving value
	uint8_t _powerSavingValue;

public:
	// create the class
	void setup(
			uint8_t brightnessPin_,
			uint8_t contrastPin_,
			uint8_t initialBrightnessPercent_,
			uint8_t initialContrastPercent_);

	// get the brightness handler
	PWMManager& brightness();

	// get the contrast handler
	PWMManager& contrast();

	// enter power saving immediately
	void enterPowerSaving();

	// enter power saving by fade out
	void enterPowerSaving(uint32_t microsPerStep_);

	// exit power saving immediately
	void exitPowerSaving();

	// exit power saving, fading up
	void exitPowerSaving(uint32_t microsPerStep_);
};

#endif

LCDAdjustments.h

Initialising the class

To initialise the class before you use it, you must call setup().

void setup(
  uint8_t brightnessPin_,
  uint8_t contrastPin_,
  uint8_t initialBrightnessPercent_,
  uint8_t initialContrastPercent_
);

The brightness and contrast pins must be capable of doing hardware PWM. The initial values will be loaded immediately and can be reverted back to by calling reset(). Percentage values are integers between 0 and 100 inclusive. For example, to use pins 9 and 10 with default values of 100 (brightness) and 30 (contrast):

LCDAdjustments adjustments;
adjustments.setup(9,10,100,30);

Setting a new value

setValue() and setPercentage() will set values with a range of 0-255 (setValue) or 0-100 (setPercentage). For example, to set the brightness to 50% and the contrast to 30%:

adjustments.brightness().setPercentage(50);
adjustments.contrast().setPercentage(30);

A note about the contrast

You will need to determine the acceptable contrast values empirically. The CrystalFontz LCD used in this article can only sensibly handle a range of 10% to 40%. Below 10% the LCD display appears as solid blocks and above 40% the text disappears completely. Go too far above 40% and the display will malfunction showing random characters. You will need to perform your own experiments and then set suitable limits in your code.

Fading to a new value

fadeToValue() and fadeToPercentage() will smoothly fade the current LCD value up or down to the desired target in a visually pleasing manner.

		// fade to the target percentage
		void fadeToPercentage(
				uint8_t targetPercentage_,
				uint32_t microsPerStep_);

		// fade to the target value
		void fadeToValue(
				uint8_t targetValue_,
				uint32_t microsPerStep_);

microsPerStep_ is the number of microseconds to wait between each transition in the animation sequence. I have found that between 2000 and 3000 are good choices. These methods are synchronous, that is they will block for the total time it takes to do the fade. For example, to fade the brightness to zero with 2500μs between each step:

adjustments.brightness().fadeToPercentage(0,2500);

Power saving

The class provides a ‘power saving’ function. That is, it can switch off the LCD backlight and then restore it to its previous value when woken up. The methods used to enter power-saving are:

	// enter power saving immediately
	void enterPowerSaving();

	// enter power saving by fade out
	void enterPowerSaving(uint32_t microsPerStep_);

enterPowerSaving() will switch off the backlight immediately. enterPowerSaving(uint32_t microsPerStep_) will switch off the backlight by fading it out to zero in a visually pleasing manner. For example, to enter power saving by fading out:

adjustments.enterPowerSaving(2500);

When you want to exit from power saving you have the following methods to do it:

	// exit power saving immediately
	void exitPowerSaving();

	// exit power saving, fading up
	void exitPowerSaving(uint32_t microsPerStep_);

These methods will restore the previous value of the LCD brightness either immediately or via a fade-in effect. For example, to immediately restore the brightness:

adjustments.exitPowerSaving();

Source Code

Here’s LCDAdjustments.cpp that contains the implementation.

/*
 * LCD Adjustments: Copyright (c) 2010 Andy Brown
 * http://www.andybrown.me.uk
 *
 * This work is licensed under a Creative Commons
 * Attribution-ShareAlike 3.0 Unported License.
 * http://creativecommons.org/licenses/by-sa/3.0/
 */

#include "LCDAdjustments.h"
#include <wiring.h>


/*
 * Setup the class with pins
 */

void LCDAdjustments::setup(
		uint8_t brightnessPin_,
		uint8_t contrastPin_,
		uint8_t initialBrightness_,
		uint8_t initialContrast_)
{
  // 32Khz on 9/10
	//TCCR1B=1;

	// setup the brightness
	_brightness.setup(brightnessPin_,initialBrightness_);

	// setup the contrast
	_contrast.setup(contrastPin_,initialContrast_);
}


/*
 * Return the brightness manager
 */

LCDAdjustments::PWMManager &LCDAdjustments::brightness()
{
	return _brightness;
}


/*
 * Return the contrast manager
 */

LCDAdjustments::PWMManager& LCDAdjustments::contrast()
{
	return _contrast;
}


/*
 * Enter power saving immediately
 */

void LCDAdjustments::enterPowerSaving()
{
// save the current value and switch off the backlight

	_powerSavingValue=_brightness.getCurrentValue();
	_brightness.setValue(0);
}


/*
 * Enter power saving with fade
 */

void LCDAdjustments::enterPowerSaving(uint32_t microsPerStep_)
{
// save current value and fade out the backlight

	_powerSavingValue=_brightness.getCurrentValue();
	_brightness.fadeToPercentage(0,microsPerStep_);
}


/*
 * Exit from power saving immediately
 */

void LCDAdjustments::exitPowerSaving()
{
	_brightness.setValue(_powerSavingValue);
}


/*
 * Exit from power saving with fade up
 */

void LCDAdjustments::exitPowerSaving(uint32_t microsPerStep_)
{
	_brightness.fadeToValue(_powerSavingValue,microsPerStep_);
}


/*
 * Setup the PWM manager
 */

void LCDAdjustments::PWMManager::setup(
		uint8_t pin_,
		uint8_t defaultPercentage_)
{
	_pin=pin_;
	_defaultPercentage=defaultPercentage_;

	pinMode(pin_,OUTPUT);
	setPercentage(defaultPercentage_);
}


/*
 * Set the value immediately
 */

void LCDAdjustments::PWMManager::setPercentage(uint8_t percentage_)
{
	setValue(percentageToValue(percentage_));
}


/*
 * Set the absolute value
 */

void LCDAdjustments::PWMManager::setValue(uint8_t value_)
{
// save the current value and write it to the pin

	_currentValue=value_;
	analogWrite(_pin,value_);
}


/**
 * Fade to a given percentage
 */

void LCDAdjustments::PWMManager::fadeToPercentage(
		uint8_t targetPercentage_,
		uint32_t microsPerStep_)
{
	fadeToValue(percentageToValue(targetPercentage_),microsPerStep_);
}


/**
 * Fade to a given value
 */

void LCDAdjustments::PWMManager::fadeToValue(
		uint8_t targetValue_,
		uint32_t microsPerStep_)
{
	int8_t direction;

	// figure out whether we're going up or down
	direction=targetValue_>_currentValue ? 1 : -1;

	while(_currentValue!=targetValue_)
	{
		setValue(_currentValue+direction);
		delayMicroseconds(microsPerStep_);
	}
}


/*
 * Convert percentage to absolute value
 */

uint8_t LCDAdjustments::PWMManager::percentageToValue(uint8_t percentage_) const
{
	return static_cast<uint8_t>((255*(int)percentage_)/100);
}


/*
 * Reset to the default percentage
 */

void LCDAdjustments::PWMManager::reset()
{
	setPercentage(_defaultPercentage);
}


/*
 * Get the current value
 */

uint8_t LCDAdjustments::PWMManager::getCurrentValue() const
{
	return _currentValue;
}

LCDAdjustments.cpp

Test Project

I built an Eclipse test project to exercise all the functionality of this class. You can download it from my downloads page.


click to enlarge

The test project has a dependency on the Arduino libraries for Eclipse that you can also get from my downloads page. You may have to adjust the Eclipse compiler C++ include and link settings to reflect the location of the Arduino libraries on your PC.

Here’s a video that shows the 16×2 LCD connected up to my Seeeduino board and running the test class.

  • A Concerned User

    This will eventually damage the Arduino. You need a resistor on the contrast circuit otherwise every time the Arduino drives the pin low, it is shorting that cap to ground. Inrush current into the AVR will be large (but brief). Eventually that port pin will likely die.

    • Thank you for the advice, much appreciated.

    • Lavesh

      How much resistance should be good enough to be connected between D10 and 100uF capacitor

      Also, will it be ok if I replace 10ohm resistor with 220ohm resistor

      • If you replace R1 with 220 then you will limit the backlight current to around 3.6mA which is around 3% brightness. I think your display will be very dark.

        • Lavesh

          Thanks for the reply Andy.

          Can I put 1Kohm resistance D10 and 100uF capacitor?

  • DarkXDroid

    Nice tutorial best explanation I could find around the web

    • valerio

      Buona sera e scusate il disturbo, ho provato ad utilizzare questa libreria ma mi da problemi in compilazione utilizzanto Arduino e il programma Arduino 1.5.4. Mi da i seguenti errori:

      LCDAdjustments.cpp: In member function 'void LCDAdjustments::PWMManager::setup(uint8_t, uint8_t)':
      LCDAdjustments.cpp:112: error: 'OUTPUT' was not declared in this scope
      LCDAdjustments.cpp:112: error: 'pinMode' was not declared in this scope
      LCDAdjustments.cpp: In member function 'void LCDAdjustments::PWMManager::setValue(uint8_t)':
      LCDAdjustments.cpp:136: error: 'analogWrite' was not declared in this scope
      LCDAdjustments.cpp: In member function 'void LCDAdjustments::PWMManager::fadeToValue(uint8_t, uint32_t)':
      LCDAdjustments.cpp:168: error: 'delayMicroseconds' was not declared in this scope

      Come posso rimediare?

      Cordiali saluti
      Valerio

      Good evening and sorry for the trouble, I tried to use this library but I utilizzanto problems in compiling the program Arduino and Arduino 1.5.4. It gives me the following errors:

      LCDAdjustments.cpp: In member function 'void LCDAdjustments::PWMManager::setup(uint8_t, uint8_t)':
      LCDAdjustments.cpp:112: error: 'OUTPUT' was not declared in this scope
      LCDAdjustments.cpp:112: error: 'pinMode' was not declared in this scope
      LCDAdjustments.cpp: In member function 'void LCDAdjustments::PWMManager::setValue(uint8_t)':
      LCDAdjustments.cpp:136: error: 'analogWrite' was not declared in this scope
      LCDAdjustments.cpp: In member function 'void LCDAdjustments::PWMManager::fadeToValue(uint8_t, uint32_t)':
      LCDAdjustments.cpp:168: error: 'delayMicroseconds' was not declared in this scope

      How can I fix it?

      Sincerely
      Valerio

      • Hi, the new 1.5.x IDE will not be officially supported while it's in beta. 1.0.5 is the latest supported IDE.

        • valerio

          Thk i try it.

          Sincerely
          Valerio

          • valerio

            Good evening, I just tried to uninstall the 1.5.x and I installed the 1.0.5 but nothing changes I have the usual mistakes. He would, by chance, the complete project whit file.ino for the Arduino 1.0.5?

          • valerio

            Good day if you are interested in making this change to the file LCDAdjustments.h:

            #ifndef __B66E1DAA_232C_4362_86C4_86F942C1F9C8
            #define __B66E1DAA_232C_4362_86C4_86F942C1F9C8

            #if (ARDUINO >= 100)
            #include <Arduino.h>
            #else
            #include <WProgram.h>
            #endif

            #include <stdint.h>

            leaving only the two files LCDAdjustments.he LCDAdjustments.cpp and LCDAdjustments.ino add:

            #include <LiquidCrystal.h>
            #include <LCDAdjustments.h>

            not be compile-time error, but if I insert the file tester gives me error in compiling the main.

            main.cpp.o: In function `main':
            C:Program FilesArduinohardwarearduinoavrcoresarduino/main.cpp:11: undefined reference to `setup'
            C:Program FilesArduinohardwarearduinoavrcoresarduino/main.cpp:14: undefined reference to `loop'

  • Matt

    By placing a resistor between the capacitor and the pin on your arduino, you create a LowPass filter. The frequency needs to be lower than the PWM frequency. The PWM frequency is 400HZ. If you want your filter to create a nice DC voltage, dimension it to pass only 100Hz or even 1Hz (cut off frequency).

    To calculate the cut off frequency you should use the next formula: F = 1 / (2*pi*C*R).

    Calculating R for F = 1HZ (with C = 100uF):

    R = 1 / (2*pi*C*F) => 1 / (2*pi*100u*1) =~ 15k Ohm

    For F = 10Hz => R= 1,5k Ohm

    It should work with 1k aswell 😉

  • Ray

    Great article!

    The 10R resistor on the backlight control switch transistor should be on the collector, not the emitter. Emitter resistors are used only in linear analog circuits involving thermal compensation. It was designed this wrong way on early versions of the 1602/2004 LCD I2C interface adapter modules. The latest (v2) versions were redesigned correctly.

    Also, selection of the proper value of this current limiting resistor is dependent on the particular LCD module model. The LED(s) used for the backlight should be driven with no more than 20mA. For example, the resistor value for the common 2004 (20 chars wide, 4-line) white-on-blue LCD display should be around 50 ohms.