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.
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 - trasmits 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); } } |