M0AGX / LB9MG

Amateur radio and embedded systems

XMEGA and HD44780 LCD

Character LCD are one of the easiest and cheapest way of adding output to a microcontroller system. The world of character LCDs has mainly standarized on HD44780 controller chip, which was designed to be interfaced with the rest of the system by a parallel bus but today simple bit-banging does the job.

One of the obstacles to using HD44780 with XMEGA are different supply voltages. Displays usually require 5V, while XMEGA is 3.3V-only. The HD44780 controller will happily run from 3.3V but the display matrix itself requires higher voltage to move crystals around. 5V displays will not have enough contrast at lower voltage to show anything. There are some HD44780 displays that run at 3.3V but they are not available in all shapes, sizes and colors.

There are ways to run the display at 3.3V and generate bias voltage separately but they require extra components. For one of my designs I opted for separate supply voltages for the LCD and rest of the system.
Schematic with LCD

The display is centrally located in the schematic. All data lines go directly to the XMEGA. There are two separate voltage regulators (IC2, IC6) and two jumpers (solder bridges) to select a voltage for the display. To use 5V - IC6 has to be placed, J1 not soldered and J2 soldered in 2-3 position. To use 3.3V only - IC6 is not placed, J1 is soldered and J2 is soldered in 1-2 position. I had to use the first option. 3.3V signals from the MCU are correctly received by display's 5V logic. LCD's RW line is permanently grounded so the display can never output 5V to data lines. Without being able to read the busy flag from the display software has to use very relaxed timings so that the display always has enough time to process the commands.

Software

I have based my driver on this library. The main modifications are of course - different pin control macros for XMEGA, extra characters and mostly important - it is not busy waiting!. Well... it is very little busy waiting :)

This driver has a transmit buffer that can be loaded any time from main application context (main loop) without any delays. The transmit buffer is then being transmitted from a timer interrupt so even though the display is a "slow" device - it does not block the whole program. The only ugly part is a small 50µs delay in interrupt handler between outputting two nibbles but that could be easily improved by implementing a simple finite (2) state machine.

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <avr/io.h>
#include <avr/interrupt.h>
#include "HD44780.h"

void main(void){

    //timer 1C is used for lcd_tx_task_from_ISR execution
    TCC1.CTRLA = TC_CLKSEL_DIV1024_gc;
    TCC1.CTRLB = TC_WGMODE_NORMAL_gc;
    TCC1.PER = F_CPU / 1024 / 300; //top value = 3.33ms (300 interrupts/s)
    TCC1.INTCTRLA = TC_OVFINTLVL_MED_gc; //overflow interrupt
    sei(); //enable interrupts
    lcd_init(); //must be called after interrupts are configured

    lcd_goto_xy(0,1);
    lcd_write_text("Hello!");

    while(1){ //main loop
        //do something...
    }
}

ISR(TCC1_OVF_vect) { //timer 1C interrupt
    lcd_tx_task_from_ISR();
}

Driver - API

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#ifndef HD44780_H_
#define HD44780_H_
/* Non-blocking HD44780 character LCD driver
 * with interrupt-driven TX buffer for Atmel Xmegas.
 *
 * It runs in "open-loop" mode - transmits data to LCD
 * using relaxed timings and does not read anything from the LCD.
 *
 * LB9MG.no
 * heavily based on http://radzio.dxp.pl/hd44780/
 */

/* ----------- configuration ----------- */
//LCD pins
#define LCD_RS_PORT  PORTC
#define LCD_RS       PIN6_bm

#define LCD_E_PORT   PORTC
#define LCD_E        PIN5_bm

#define LCD_DB4_PORT PORTA
#define LCD_DB4      PIN7_bm

#define LCD_DB5_PORT PORTB
#define LCD_DB5      PIN0_bm

#define LCD_DB6_PORT PORTA
#define LCD_DB6      PIN5_bm

#define LCD_DB7_PORT PORTA
#define LCD_DB7      PIN6_bm

//Buffer should be the size of the display in characters
//plus some extra, example 2x16 LCD -> buffer = 36
#define LCD_RINGBUFFER_SIZE 36

//Uncommend this define to load extra arrow and degree signs
//to the display. Use character codes specified below
#define LCD_EXTRA_CHARACTERS
/* -------- end of configuration ------- */

#ifdef LCD_EXTRA_CHARACTERS
    #define LCD_CH_ARROW_UP    0x01
    #define LCD_CH_ARROW_DOWN  0x02
    #define LCD_CH_ARROW_LEFT  0x03
    #define LCD_CH_ARROW_RIGHT 0x04
    #define LCD_CH_ARROW_ENTER 0x05
    #define LCD_CH_DEGREE_SIGN 0x06
#else
    #define LCD_CH_ARROW_UP    ' '
    #define LCD_CH_ARROW_DOWN  ' '
    #define LCD_CH_ARROW_LEFT  ' '
    #define LCD_CH_ARROW_RIGHT ' '
    #define LCD_CH_ARROW_ENTER ' '
    #define LCD_CH_DEGREE_SIGN ' '
#endif

#include <stdbool.h>

//Call from timer every couple of miliseconds
void lcd_tx_task_from_ISR(void);

//Call lcd_init ONLY after ISR has been set up and lcd_tx_task_from_ISR is banging
//otherwise it will block!
void lcd_init(void);

void lcd_clear(void);
void lcd_home(void);

void lcd_write_char(char data);
void lcd_write_text(const char *);

void lcd_goto_xy(unsigned char x, unsigned char y);

void lcd_show_cursor(bool show);

#endif // HD44780_H_

Driver - internals

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#include <avr/io.h>
#include <avr/pgmspace.h>
#include "HD44780.h"
#include <util/delay.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>

#define HD44780_CLEAR                0x01

#define HD44780_HOME                 0x02

#define HD44780_ENTRY_MODE           0x04
    #define HD44780_EM_SHIFT_CURSOR     0
    #define HD44780_EM_SHIFT_DISPLAY    1
    #define HD44780_EM_DECREMENT        0
    #define HD44780_EM_INCREMENT        2

#define HD44780_DISPLAY_ONOFF        0x08
    #define HD44780_DISPLAY_OFF         0
    #define HD44780_DISPLAY_ON          4
    #define HD44780_CURSOR_OFF          0
    #define HD44780_CURSOR_ON           2
    #define HD44780_CURSOR_NOBLINK      0
    #define HD44780_CURSOR_BLINK        1

#define HD44780_DISPLAY_CURSOR_SHIFT 0x10
    #define HD44780_SHIFT_CURSOR        0
    #define HD44780_SHIFT_DISPLAY       8
    #define HD44780_SHIFT_LEFT          0
    #define HD44780_SHIFT_RIGHT         4

#define HD44780_FUNCTION_SET         0x20
    #define HD44780_FONT5x7             0
    #define HD44780_FONT5x10            4
    #define HD44780_ONE_LINE            0
    #define HD44780_TWO_LINE            8
    #define HD44780_4_BIT               0
    #define HD44780_8_BIT              16

#define HD44780_CGRAM_SET            0x40

#define HD44780_DDRAM_SET            0x80

/* --------- private data --------------- */
typedef struct{
    bool data_line_flag;
    uint8_t payload;
} lcd_byte_t;

static lcd_byte_t _lcd_ringbuffer[LCD_RINGBUFFER_SIZE];
static volatile uint8_t _ringbuffer_head = 0;
static volatile uint8_t _ringbuffer_tail = 0;

#ifdef LCD_EXTRA_CHARACTERS
static const uint8_t CUSTOM_CHARACTERS[] PROGMEM = {
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //Char0 - not used
        0x04, 0x0E, 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, //Char1 - left arrow
        0x04, 0x04, 0x04, 0x04, 0x04, 0x1F, 0x0E, 0x04, //Char2 - right arrow
        0x00, 0x00, 0x04, 0x0C, 0x1F, 0x0C, 0x04, 0x00, //Char3 - up arrrow
        0x00, 0x00, 0x04, 0x06, 0x1F, 0x06, 0x04, 0x00, //Char4 - down arrow
        0x01, 0x01, 0x05, 0x0D, 0x1F, 0x0C, 0x04, 0x00, //Char5 - enter arrow
        0x06, 0x09, 0x09, 0x06, 0x00, 0x00, 0x00, 0x00, //Char6 - degree sign
                             };
#endif

/* --------- private prototypes --------- */
static void lcd_output_nibble(unsigned char nibble);
static void lcd_write_from_ISR(unsigned char data);
static void lcd_ringbuffer_put(uint8_t byte, bool data_line_flag);
static void lcd_write_command(unsigned char cmd);
static void lcd_ringbuffer_busy_wait(void);
/* -------------------------------------- */

//to be executed by timer interrupt every couple of miliseconds
inline void lcd_tx_task_from_ISR(void){
    if ( _ringbuffer_head != _ringbuffer_tail ){ //if there is anything to transmit
        //the byte to transmit is: _lcd_ringbuffer[_ringbuffer_head]

        if (_lcd_ringbuffer[_ringbuffer_head].data_line_flag){
            LCD_RS_PORT.OUTSET = LCD_RS; //for writing data this line has to be high
        } else {
            LCD_RS_PORT.OUTCLR = LCD_RS; //for writing commands this line has to be low
        }
        lcd_write_from_ISR(_lcd_ringbuffer[_ringbuffer_head].payload);

        //advance ringbuffer index
        _ringbuffer_head = ( _ringbuffer_head+1 ) % LCD_RINGBUFFER_SIZE;
    }
}

static void lcd_ringbuffer_put(uint8_t byte, bool data_line_flag){
    uint8_t next = (_ringbuffer_tail + 1) % LCD_RINGBUFFER_SIZE;
    if (next == _ringbuffer_head){
        return; //overflow
    }
    _lcd_ringbuffer[_ringbuffer_tail].payload = byte;
    _lcd_ringbuffer[_ringbuffer_tail].data_line_flag = data_line_flag;
    _ringbuffer_tail = next;
}

static void lcd_ringbuffer_busy_wait(void){
    while (_ringbuffer_head != _ringbuffer_tail) {}
}

static void lcd_output_nibble(unsigned char nibble){
    if (nibble & 0x01){
        LCD_DB4_PORT.OUTSET = LCD_DB4;
    } else {
        LCD_DB4_PORT.OUTCLR = LCD_DB4;
    }

    if (nibble & 0x02){
        LCD_DB5_PORT.OUTSET = LCD_DB5;
    } else {
        LCD_DB5_PORT.OUTCLR = LCD_DB5;
    }

    if (nibble & 0x04){
        LCD_DB6_PORT.OUTSET = LCD_DB6;
    } else {
        LCD_DB6_PORT.OUTCLR = LCD_DB6;
    }

    if (nibble & 0x08){
        LCD_DB7_PORT.OUTSET = LCD_DB7;
    } else {
        LCD_DB7_PORT.OUTCLR = LCD_DB7;
    }
}

static void lcd_write_from_ISR(unsigned char data)
{
    LCD_E_PORT.OUTSET = LCD_E;
    lcd_output_nibble(data >> 4);
    LCD_E_PORT.OUTCLR = LCD_E;
    LCD_E_PORT.OUTSET = LCD_E;
    lcd_output_nibble(data);
    LCD_E_PORT.OUTCLR = LCD_E;
    _delay_us(50); //was 50
}

static void lcd_write_command(unsigned char cmd){
    lcd_ringbuffer_put(cmd, false/*data line low*/);
}

void lcd_write_char(char data){
    lcd_ringbuffer_put(data, true/*data line high*/);
}

void lcd_write_text(const char *text){
    while(*text){
            lcd_write_char(*text++);
    }
}

void lcd_goto_xy(unsigned char x, unsigned char y){
    lcd_write_command(HD44780_DDRAM_SET | (x + (0x40 * y)));
}

void lcd_clear(void){
    lcd_write_command(HD44780_CLEAR);
}

void lcd_home(void){
    lcd_write_command(HD44780_HOME);
}

void lcd_init(void){
    LCD_DB4_PORT.DIRSET = LCD_DB4;
    LCD_DB5_PORT.DIRSET = LCD_DB5;
    LCD_DB6_PORT.DIRSET = LCD_DB6;
    LCD_DB7_PORT.DIRSET = LCD_DB7;
    LCD_E_PORT.DIRSET   = LCD_E;
    LCD_RS_PORT.DIRSET  = LCD_RS;
    _delay_ms(50);
    LCD_RS_PORT.OUTCLR = LCD_RS;
    LCD_E_PORT.OUTCLR = LCD_E;

    //initialize 3 times to make sure that the display goes into 4-bit mode
    for(uint8_t i = 0; i < 3; i++){
        LCD_E_PORT.OUTSET = LCD_E;
        lcd_output_nibble(0x03);
        LCD_E_PORT.OUTCLR = LCD_E;
        _delay_ms(5);
    }

    LCD_E_PORT.OUTSET = LCD_E;
    lcd_output_nibble(0x02);
    LCD_E_PORT.OUTCLR = LCD_E;

    _delay_ms(2);
    lcd_write_command(HD44780_FUNCTION_SET | HD44780_FONT5x7 | HD44780_TWO_LINE | HD44780_4_BIT);
    lcd_write_command(HD44780_DISPLAY_ONOFF | HD44780_DISPLAY_OFF);
    lcd_write_command(HD44780_CLEAR);

#ifdef LCD_EXTRA_CHARACTERS
    lcd_write_command(0x40); //arrows will begin at code 0x01
    for (uint8_t i = 0; i < sizeof(CUSTOM_CHARACTERS); i++){
        lcd_write_char(pgm_read_byte(&CUSTOM_CHARACTERS[i]));
        //don't overflow the buffer, load up to 8 bytes at a time and wait
        if (i%8 == 0){
            lcd_ringbuffer_busy_wait();
        }
    }
#endif
    lcd_write_command(HD44780_ENTRY_MODE | HD44780_EM_SHIFT_CURSOR | HD44780_EM_INCREMENT);
    lcd_write_command(HD44780_DISPLAY_ONOFF | HD44780_DISPLAY_ON | HD44780_CURSOR_OFF | HD44780_CURSOR_NOBLINK);

}

void lcd_show_cursor(bool show){
    if (show){
        lcd_write_command(HD44780_DISPLAY_ONOFF |
                          HD44780_DISPLAY_ON    |
                          HD44780_CURSOR_ON     |
                          HD44780_CURSOR_NOBLINK);
    } else {
        lcd_write_command(HD44780_DISPLAY_ONOFF |
                          HD44780_DISPLAY_ON    |
                          HD44780_CURSOR_OFF    |
                          HD44780_CURSOR_NOBLINK);
    }
}

I release the code into public domain.