I2C lcd with clock

In this project I created a library for the i2c lcd. I used the Arduino library as a reference, but I found that library a bit to complicated to use it completely. The Arduino library also have a lot of functions that I’m not planning to use, like creating your own characters. The ASCII characters are enough for my projects for example. So I wrote a new library.

Communicating with the i2c lcd test build.

 

Again I used the same framework to get the lcd working simultanesly with other devices in the future and for now I only added 1 led to test the multi threating.

The lcd unit has a backpack attached used to communicate with the lcd via i2c. The original lcd uses more then 11 pin’s and that’s a lot for a microcontroller like the Atmega328p. The backpack uses the PCA8574, which is a 8 bit i2c pin expander to reduce the amount of pins to be used on a microcontroller. The lcd on it’s own has already 8 pins for writing to a register, but it is possible to set the lcd to use 4 bits instead. That means there are 4 pins left on the PCA8574 and these are used for selecting a register, read or write, enable puls and the backlight. See the below schematic how to connect.

I2c lcd project schematic.

 

Working of the display

Communicating with the display is a bit complicated because we have to use it in 4 bits  mode. If the backpack used a 16 bit expander instead of 8 bit then life whas much easier, but 8 bits expanders are cheaper… That means to write to an 8 bit register we have to send the high 4 bits of a byte first and then the last 4 bits. There are 2 registers to write to. That’s the data register DR and the instruction register IR. Text you want to get on the display have to be set in the data register. Command’s like, changing the cursor position or clearing the display, have to be set in the instruction register. The register select RS pin will let you choose between both the registers. Puttting the pin low (0V) selects the IR and high (5V) selects the DR.

It is possible to read an address location or to read the busy flag BF. The BF is used to check if the lcd is still working on an internal command. If so, it’s not possible to send a new instruction. I didn’t do anything with the BF yet or reading the address locations. For writing text on the lcd it’s not needed. I used delays according the datasheet to wait for the time the lcd needs.

There is a read write RW pin. To read from the lcd this pin should be high and for writing low. There is also a pin for the backlight BL, but my display requires this to be activated all the times else you won’t see anything on the display.

To set a byte into a register the enable EN pin have to be set high. This works like a latched flip flop, that means whatever voltage you put on the pins they will only be read by the lcd if the enable pin is high. Writing an instruction or a character are basically the same. The only difference is the register select pin. Basically to write to a register the following steps have to be done:

  1. Put the correct voltages on all the lcd pins for the first 4 bits of a byte you want to send.
  2. Set the enable pin high for more then 450 nano seconds and keep the rest of the pins the same.
  3. Set the enable pin low for more then 37 micro seconds and keep the rest of the pins the same.
  4. Do the last 3 steps again, but now for the last 4 bits of 8.

In code it looks like this:

i2c_start();
i2c_write((LCD_I2C_ADDR<<1)|0);
i2c_write(highnib | LCD_BACKLIGHT | registerselect);
i2c_stop();
i2c_start();
i2c_write((LCD_I2C_ADDR<<1)|0); i2c_write(highnib | LCD_BACKLIGHT | registerselect | ENABLE); i2c_stop(); _delay_us(1); // enable pulse must be >450ns
i2c_start();
i2c_write((LCD_I2C_ADDR<<1)|0); i2c_write((highnib | LCD_BACKLIGHT | registerselect ) & ~ENABLE); i2c_stop(); _delay_us(50); // commands need > 37us to settle
	
// same as above but now for last 4 bits
i2c_start();
i2c_write((LCD_I2C_ADDR<<1)|0);
i2c_write(lownib | LCD_BACKLIGHT | registerselect);
i2c_stop();
i2c_start();
i2c_write((LCD_I2C_ADDR<<1)|0); i2c_write(lownib | LCD_BACKLIGHT | registerselect | ENABLE); i2c_stop(); _delay_us(1); // enable pulse must be >450ns
i2c_start();
i2c_write((LCD_I2C_ADDR<<1)|0); i2c_write((lownib | LCD_BACKLIGHT | registerselect ) & ~ENABLE); i2c_stop(); _delay_us(50); // commands need > 37us to settle

 

Initializing the display

This subject needs a separate chapter, because its a bit complicated. The datasheet is not very clear about this or it is my understanding… Default the display is set to 8 bit mode therefor we have to put it on 4 bit mode first and this can’t be changed later on, only by rebooting the lcd. The Arduino library sends the same command 3 times with different times between the instructions. I send the instruction only once, but I use a delay before proceding. This first instruction have to be send in 1 time 4 bits and all other instructions have to be send in 2 times 4 bits. After that the lcd works in 4 bit mode and then the lcd lines and character dots configuration can be send in 2 times 4 bits.  These settings can be send only once and after that all other instructions can be send any time you need.

The next settings are the cursor on/off and blinking of the cursor. The last instruction set up the direction of the characters. For western countries, my case, that’s from left to right. After that the display initial settings are set and the lcd can be used. I made a seperate function called i2cLcd_init() to get this all done.

The application

For testing I wrote my name on the display and a clock. This clock only counts for 1 hour and then starts over again. Minor changes are needed to write a 24 hour clock, but for now I thought this is a good start. My clock is not so accurate at the moment. It’s about 2 seconds a minute to fast. To get the clock more accurate you can change the CMR_SECOND and the CMR_MINUT number in the timer4clocklib.h.

The source code

See the source code of the changed files here. All other files remain the same as before.

initialize.c

/*
 * initialize.c
 *
 * Created: 20-5-2017 15:07:22
 * Author: HdH
 * Description: initialization off the project
 */ 

#define PIN_LED1 6
#define PIN_LED2 7
#define PIN_LED3 8

#include <avr/interrupt.h>
#include "gpiolib.h"
#include "timer4clocklib.h"
#include "i2c.h"
#include "i2cdisplay.h"


void initialize(){
	
	char openingText[] = "www.hdhprojects.nl";
	char clockText[] = "Clock";
	int i;

	cli();//interrupts disable
	tmr2_init(); // initialize timer2
	i2c_init(); // initialize i2c protocol
	i2cLcd_Init(); // initialize i2c lcd
	sei();//interrupts enable
	
	clear();
	setCursor(1,0);
	for(i=0; i<18; i++){		
		writeI2cDisplay(openingText[i], DATAREGISTER);
	}
	setCursor(1,2);
	for(i=0; i<5; i++){
		writeI2cDisplay(clockText[i], DATAREGISTER);
	}
	setCursor(10,2);
	writeI2cDisplay(':', DATAREGISTER);
	
	
	pinMode(PIN_LED1,PINMODEOUTPUT); //set pin to output
	pinMode(PIN_LED2,PINMODEOUTPUT); //set pin to output
	pinMode(PIN_LED3,PINMODEOUTPUT); //set pin to output	
}

 

application1.c

/*
 * operational.c
 *
 * Created: 20-5-2017 15:07:22
 * Author: HdH
 * Description: Application for multi threat, show lcd clock
 */ 

#include <avr/io.h>
#include "i2c.h"
#include "i2cdisplay.h"

void showTime(); // showing the time

/****************************************************************************
	Public vars
****************************************************************************/

extern int alarmCode;
extern int timeSecApp1;
extern int timeMinApp1;
int oldTime;

void Application1(void){

	showTime();
	
}

void showTime(){
	int seconds;
	int minutes;
	
	
	seconds = timeSecApp1;
	minutes = timeMinApp1;
	
	if (oldTime == timeSecApp1){
		return;
	}
	if (seconds >= 10){
		setCursor(11,2);
		writeI2cDisplay(seconds/10+48, DATAREGISTER);
		setCursor(12,2);
		writeI2cDisplay(seconds%10+48, DATAREGISTER);
		if (minutes>=10){
			setCursor(8,2);
			writeI2cDisplay(minutes/10+48, DATAREGISTER);
			setCursor(9,2);
			writeI2cDisplay(minutes%10+48, DATAREGISTER);
		}
		if (minutes<10){
			setCursor(8,2);
			writeI2cDisplay('0', DATAREGISTER);
			setCursor(9,2);
			writeI2cDisplay(minutes+48, DATAREGISTER);
		}
	}
	else if (seconds < 10){ setCursor(12,2); writeI2cDisplay(seconds+48, DATAREGISTER); setCursor(11,2); writeI2cDisplay('0', DATAREGISTER); if (minutes>=10){
			setCursor(8,2);
			writeI2cDisplay(minutes/10+48, DATAREGISTER);
			setCursor(9,2);
			writeI2cDisplay(minutes%10+48, DATAREGISTER);
		}
		if (minutes<10){
			setCursor(8,2);
			writeI2cDisplay('0', DATAREGISTER);
			setCursor(9,2);
			writeI2cDisplay(minutes+48, DATAREGISTER);
		}
	}
	oldTime = timeSecApp1;
}


 

display_main.c

/*
* main.c
*
* Created: 20-5-2017
* Author: HdH
* Description: main, initializing en starting the app
* Global functions:
*
*
*/

#define WATCHDOG_ALARM 2
#define NO_ALARM 0

#include <avr/io.h>
#include "i2cdisplay.h"

void errorHandle(int receivedAlarm);
int operational(void);
void initialize(void);

/****************************************************************************
	Public vars
****************************************************************************/

int errorCode = NO_ALARM; //
char alarmText[] = "Alarm in app";

int main(void)
{
	initialize(); //start only once (setup with Arduino)
	
	errorCode = operational(); //the main loop
	if (errorCode == WATCHDOG_ALARM){ // you only end up here if there is an alarm.
		errorHandle(errorCode);
	}
	else{
		errorHandle(errorCode); //one of the apps is returning the error
	}
}

void errorHandle(int receivedAlarm){
	
	int i;
	
	while(1){ //blink some leds or show an error message on a display..
		
		clear();
		setCursor(1,0);
		for(i=0; i<12; i++){
			writeI2cDisplay(alarmText[i], DATAREGISTER);
		}
	}
}

 

i2cdiplay.h

/*
 * i2cdisplay.h
 *
 * Created: 30-11-2017 19:18:52
 *  Author: acer
 */ 


#ifndef I2CDISPLAY_H_
#define I2CDISPLAY_H_

/****************************************************************************
	Bit and byte definitions
****************************************************************************/

// Display function settings can only be changed once
#define LCD_I2C_ADDR 0x3f // i2c address of the lcd
#define LCD_FUNCTIONSET 0x20
#define LCD_BITMODE 0x00 //4bit is 0x0, 8bit is 0x10
#define LCD_LINE 0x08 // 2 line is 0x08, 1 line is 0x00
#define LCD_DOTS 0x00 // 5x8 dots is 0x00, 5x10 dots is 0x04
#define LCD_ROWS 4 // Number of LCD rows
#define LCD_COLUMNS 20 // Number of LCD columns
#define DATAREGISTER 1
#define INSTRUCTIONREGISTER 0
#define ENABLE 4
#define READ_BF 2

// Display control settings
#define LCD_DISPLAYCONTROL 0x08 // Use this cmd combined with the below optional settings
#define LCD_DISPLAYON 0x04
#define LCD_DISPLAYOFF 0x00
#define LCD_CURSORON 0x02
#define LCD_CURSOROFF 0x00
#define LCD_BLINKON 0x01
#define LCD_BLINKOFF 0x00
#define LCD_BACKLIGHT 0x08
#define LCD_NOBACKLIGHT 0x00

// Display entry mode settings
#define LCD_ENTRYMODESET 0x04 // Use this cmd combined with the below optional settings. 
#define LCD_ENTRYRIGHT 0x00
#define LCD_ENTRYLEFT 0x02
#define LCD_ENTRYSHIFTINCREMENT 0x01
#define LCD_ENTRYSHIFTDECREMENT 0x00

// Send below commands to the Instruction register IR.
#define LCD_CLEARDISPLAY 0x01
#define LCD_RETURNHOME 0x02
#define LCD_SETDDRAMADDR 0x80 // Used to point to a position on the display
#define LCD_CURSORSHIFT 0x10
#define LCD_DISPLAYMOVE 0x08
#define LCD_MOVERIGHT 0x04
#define LCD_MOVELEFT 0x00



/****************************************************************************
	Function definitions
****************************************************************************/

void i2cLcd_Init();
void writeI2cDisplay(uint8_t, uint8_t);
uint8_t checkBusyFlag(); // not used yet

void clear();
void home();
void setCursor(uint8_t, uint8_t);
void noDisplay();
void display();
void noCursor();
void cursor();
void noBlink();
void blink();
void scrollDisplayLeft();
void scrollDisplayRight();
void leftToRight();
void rightToLeft();
void autoscroll();
void noAutoscroll();

#endif /* I2CDISPLAY_H_ */

 

i2cdisplay.c

/*
* i2cDisplay.c
*
* Created: 14-6-2017
* Author: HdH
* Description: functions to control the I2C Dsiplay
* Global functions:
*
*/

#define F_CPU 16000000UL // 16 MHz clock speed

#include <avr/io.h>
#include <util/delay.h>
#include "i2c.h"
#include "i2cdisplay.h"


/****************************************************************************
	Public vars
****************************************************************************/

uint8_t displayMainSettings;
uint8_t displayControl;
uint8_t displayEntryMode;
uint8_t bfPin7;


void i2cLcd_Init(){
	
	_delay_ms(50); // to get the display powered up (pg. 46 datasheet)
	
	i2c_start(); // below is needed to change the lcd from 8 bit to 4 bit
	i2c_write((LCD_I2C_ADDR<<1)|0);
	i2c_write(LCD_FUNCTIONSET);
	i2c_stop();
	i2c_start();
	i2c_write((LCD_I2C_ADDR<<1)|0); i2c_write(LCD_FUNCTIONSET | ENABLE); i2c_stop(); _delay_us(1); // enable pulse must be >450ns
	i2c_start();
	i2c_write((LCD_I2C_ADDR<<1)|0);
	i2c_write(LCD_FUNCTIONSET & ~ENABLE);
	i2c_stop();
	_delay_ms(10); // pg. 46, instead of sending the functionseet 3 times
	
	displayMainSettings = LCD_FUNCTIONSET | LCD_BITMODE | LCD_LINE | LCD_DOTS;
	writeI2cDisplay(displayMainSettings, INSTRUCTIONREGISTER); // these display settings can't be changed after this.
		
	_delay_us(100); // pg. 46
	displayControl = LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF;
	writeI2cDisplay(displayControl | LCD_DISPLAYCONTROL, INSTRUCTIONREGISTER);
	
	displayEntryMode = LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT;
	writeI2cDisplay(displayEntryMode | LCD_ENTRYMODESET, INSTRUCTIONREGISTER);
	home();
}

void writeI2cDisplay(uint8_t value, uint8_t registerselect){
	
	uint8_t highnib=value&0xf0;
	uint8_t lownib=(value<<4)&0xf0;
	
	// first byte send, set/prepare lcd pins
	// second byte send, same and turn enable on
	// third byte send, same and turn enable off
	i2c_start();
	i2c_write((LCD_I2C_ADDR<<1)|0);
	i2c_write(highnib | LCD_BACKLIGHT | registerselect);
	i2c_stop();
	i2c_start();
	i2c_write((LCD_I2C_ADDR<<1)|0); i2c_write(highnib | LCD_BACKLIGHT | registerselect | ENABLE); i2c_stop(); _delay_us(1); // enable pulse must be >450ns
	i2c_start();
	i2c_write((LCD_I2C_ADDR<<1)|0); i2c_write((highnib | LCD_BACKLIGHT | registerselect ) & ~ENABLE); i2c_stop(); _delay_us(50); // commands need > 37us to settle
	
	// same as above but now for last 4 bits
	i2c_start();
	i2c_write((LCD_I2C_ADDR<<1)|0);
	i2c_write(lownib | LCD_BACKLIGHT | registerselect);
	i2c_stop();
	i2c_start();
	i2c_write((LCD_I2C_ADDR<<1)|0); i2c_write(lownib | LCD_BACKLIGHT | registerselect | ENABLE); i2c_stop(); _delay_us(1); // enable pulse must be >450ns
	i2c_start();
	i2c_write((LCD_I2C_ADDR<<1)|0); i2c_write((lownib | LCD_BACKLIGHT | registerselect ) & ~ENABLE); i2c_stop(); _delay_us(50); // commands need > 37us to settle
	
}

uint8_t checkBusyFlag(){
	
	// Will be updated later
	return 0;
}

void clear(){
	writeI2cDisplay(LCD_CLEARDISPLAY, INSTRUCTIONREGISTER);// clear display
	_delay_us(2000);  // this command takes a long time!
}

void home(){
	writeI2cDisplay(LCD_RETURNHOME, INSTRUCTIONREGISTER);  // set cursor position to zero
	_delay_ms(2);  // this command takes a long time!
}

void setCursor(uint8_t col, uint8_t row){
	int row_offsets[] = { 0x00, 0x40, 0x14, 0x54 };
	if ( row > LCD_ROWS ) {
		row = LCD_ROWS-1;    // we count rows starting w/0
	}
	writeI2cDisplay(LCD_SETDDRAMADDR | (col + row_offsets[row]), INSTRUCTIONREGISTER);
}

void noDisplay() {
	displayControl &= ~LCD_DISPLAYON;
	writeI2cDisplay(LCD_DISPLAYCONTROL | displayControl, INSTRUCTIONREGISTER);
}

void display() {
	displayControl |= LCD_DISPLAYON;
	writeI2cDisplay(LCD_DISPLAYCONTROL | displayControl, INSTRUCTIONREGISTER);
}

void noCursor() {
	displayControl &= ~LCD_CURSORON;
	writeI2cDisplay(LCD_DISPLAYCONTROL | displayControl, INSTRUCTIONREGISTER);
}

void cursor() {
	displayControl |= LCD_CURSORON;
	writeI2cDisplay(LCD_DISPLAYCONTROL | displayControl, INSTRUCTIONREGISTER);
}

void noBlink() {
	displayControl &= ~LCD_BLINKON;
	writeI2cDisplay(LCD_DISPLAYCONTROL | displayControl, INSTRUCTIONREGISTER);
}

void blink() {
	displayControl |= LCD_BLINKON;
	writeI2cDisplay(LCD_DISPLAYCONTROL | displayControl, INSTRUCTIONREGISTER);
}

void scrollDisplayLeft(void) {
	writeI2cDisplay(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVELEFT, INSTRUCTIONREGISTER);
}

void scrollDisplayRight(void) {
	writeI2cDisplay(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVERIGHT, INSTRUCTIONREGISTER);
}

void leftToRight(void) {
	displayEntryMode |= LCD_ENTRYLEFT;
	writeI2cDisplay(LCD_ENTRYMODESET | displayEntryMode, INSTRUCTIONREGISTER);
}

void rightToLeft(void) {
	displayEntryMode &= ~LCD_ENTRYLEFT;
	writeI2cDisplay(LCD_ENTRYMODESET | displayEntryMode, INSTRUCTIONREGISTER);
}

void autoscroll(void) {
	displayEntryMode |= LCD_ENTRYSHIFTINCREMENT;
	writeI2cDisplay(LCD_ENTRYMODESET | displayEntryMode, INSTRUCTIONREGISTER);
}

void noAutoscroll(void) {
	displayEntryMode &= ~LCD_ENTRYSHIFTINCREMENT;
	writeI2cDisplay(LCD_ENTRYMODESET | displayEntryMode, INSTRUCTIONREGISTER);
}

 

The lcd comes in very handy as a debug tool. You can write everything to the display that might be important for trouble shooting. Besides that, there are many more reasons to use an lcd in your projects. I think my next blog will be an ADC library for the analog ports and I will write this on a display to understand the working of the analog pins.