├── lcd ├── __init__.py ├── i2c_pcf8574_interface.py └── lcd.py ├── .gitignore ├── LICENSE └── README.rst /lcd/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.mpy 2 | *~ 3 | 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Dan Halbert 4 | Portions derived from code from https://github.com/dbrgn/RPLCD 5 | Copyright (C) 2013-2016 Danilo Bargen 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | CircuitPython library for HD77480 LCD character displays with an I2C backpack. 5 | Currently PCF8574 is supported. 6 | 7 | The original code started with the RPLCD library by Danilo Bargen, in https://github.com/dbrgn/RPLCD, 8 | but it has been reworked considerably. 9 | 10 | On SAMD21 (M0) boards, ``lcd/lcd.py`` is too big to use as ``.py``. Use ``mpy-cross`` to convert the ``.py`` files into ``.mpy``. 11 | Also, use the ``minimal`` branch, to save space, if you don't need all the features in the main branch. 12 | 13 | Usage Example 14 | ============= 15 | 16 | The ``LCD`` supports character LCDs using the HD77480 chip. 17 | 18 | The interface to the LCD is separated into an ``Interface`` class. 19 | Currently there is only one such class: ``I2CPCF8574Interface``. 20 | 21 | .. code-block:: python 22 | 23 | import board 24 | 25 | from lcd.lcd import LCD 26 | from lcd.i2c_pcf8574_interface import I2CPCF8574Interface 27 | 28 | from lcd.lcd import CursorMode 29 | 30 | # Talk to the LCD at I2C address 0x27. 31 | # The number of rows and columns defaults to 4x20, so those 32 | # arguments could be omitted in this case. 33 | lcd = LCD(I2CPCF8574Interface(board.I2C(), 0x27), num_rows=4, num_cols=20) 34 | 35 | lcd.print("abc ") 36 | lcd.print("This is quite long and will wrap onto the next line automatically.") 37 | 38 | lcd.clear() 39 | 40 | # Start at the second line, fifth column (numbering from zero). 41 | lcd.set_cursor_pos(1, 4) 42 | lcd.print("Here I am") 43 | 44 | # Make the cursor visible as a line. 45 | lcd.set_cursor_mode(CursorMode.LINE) 46 | -------------------------------------------------------------------------------- /lcd/i2c_pcf8574_interface.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Dan Halbert 2 | # Adapted from https://github.com/dbrgn/RPLCD, Copyright (C) 2013-2016 Danilo Bargen 3 | 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | # this software and associated documentation files (the "Software"), to deal in 6 | # the Software without restriction, including without limitation the rights to 7 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | # the Software, and to permit persons to whom the Software is furnished to do so, 9 | # subject to the following conditions: 10 | 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """Low-level interface to PCF8574.""" 22 | 23 | import busio 24 | import board 25 | import microcontroller 26 | from adafruit_bus_device.i2c_device import I2CDevice 27 | 28 | from .lcd import LCD_4BITMODE, LCD_BACKLIGHT, LCD_NOBACKLIGHT, PIN_ENABLE 29 | 30 | 31 | class I2CPCF8574Interface: 32 | 33 | # Bit values to turn backlight on/off. Indexed by a boolean. 34 | _BACKLIGHT_VALUES = (LCD_NOBACKLIGHT, LCD_BACKLIGHT) 35 | 36 | def __init__(self, i2c, address): 37 | """ 38 | CharLCD via PCF8574 I2C port expander. 39 | 40 | Pin mapping:: 41 | 42 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 43 | D7 | D6 | D5 | D4 | BL | EN | RW | RS 44 | 45 | :param address: The I2C address of your LCD. 46 | """ 47 | self.address = address 48 | 49 | self._backlight_pin_state = LCD_BACKLIGHT 50 | 51 | self.i2c = i2c 52 | self.i2c_device = I2CDevice(self.i2c, self.address) 53 | self.data_buffer = bytearray(1) 54 | 55 | def deinit(self): 56 | self.i2c.deinit() 57 | 58 | @property 59 | def data_bus_mode(self): 60 | return LCD_4BITMODE 61 | 62 | @property 63 | def backlight(self): 64 | return self._backlight_pin_state == LCD_BACKLIGHT 65 | 66 | @backlight.setter 67 | def backlight(self, value): 68 | self._backlight_pin_state = self._BACKLIGHT_VALUES[value] 69 | with self.i2c_device: 70 | self._i2c_write(self._backlight_pin_state) 71 | 72 | # Low level commands 73 | 74 | def send(self, value, rs_mode): 75 | """Send the specified value to the display in 4-bit nibbles. 76 | The rs_mode is either ``_RS_DATA`` or ``_RS_INSTRUCTION``.""" 77 | self._write4bits(rs_mode | (value & 0xF0) | self._backlight_pin_state) 78 | self._write4bits(rs_mode | ((value << 4) & 0xF0) | self._backlight_pin_state) 79 | 80 | def _write4bits(self, value): 81 | """Pulse the `enable` flag to process value.""" 82 | with self.i2c_device: 83 | self._i2c_write(value & ~PIN_ENABLE) 84 | # This 1us delay is probably unnecessary, given the time needed 85 | # to execute the statements. 86 | microcontroller.delay_us(1) 87 | self._i2c_write(value | PIN_ENABLE) 88 | microcontroller.delay_us(1) 89 | self._i2c_write(value & ~PIN_ENABLE) 90 | # Wait for command to complete. 91 | microcontroller.delay_us(100) 92 | 93 | def _i2c_write(self, value): 94 | self.data_buffer[0] = value 95 | self.i2c_device.write(self.data_buffer) 96 | -------------------------------------------------------------------------------- /lcd/lcd.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Dan Halbert 2 | # Adapted from https://github.com/dbrgn/RPLCD, Copyright (C) 2013-2016 Danilo Bargen 3 | 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | # this software and associated documentation files (the "Software"), to deal in 6 | # the Software without restriction, including without limitation the rights to 7 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | # the Software, and to permit persons to whom the Software is furnished to do so, 9 | # subject to the following conditions: 10 | 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | import time 22 | from micropython import const 23 | 24 | # Commands 25 | _LCD_CLEARDISPLAY = const(0x01) 26 | _LCD_RETURNHOME = const(0x02) 27 | _LCD_ENTRYMODESET = const(0x04) 28 | _LCD_DISPLAYCONTROL = const(0x08) 29 | _LCD_CURSORSHIFT = const(0x10) 30 | _LCD_FUNCTIONSET = const(0x20) 31 | _LCD_SETCGRAMADDR = const(0x40) 32 | _LCD_SETDDRAMADDR = const(0x80) 33 | 34 | # Flags for display entry mode 35 | _LCD_ENTRYRIGHT = const(0x00) 36 | _LCD_ENTRYLEFT = const(0x02) 37 | _LCD_ENTRYSHIFTINCREMENT = const(0x01) 38 | _LCD_ENTRYSHIFTDECREMENT = const(0x00) 39 | 40 | # Flags for display on/off control 41 | _LCD_DISPLAYON = const(0x04) 42 | _LCD_DISPLAYOFF = const(0x00) 43 | _LCD_CURSORON = const(0x02) 44 | _LCD_CURSOROFF = const(0x00) 45 | _LCD_BLINKON = const(0x01) 46 | _LCD_BLINKOFF = const(0x00) 47 | 48 | # Flags for display/cursor shift 49 | _LCD_DISPLAYMOVE = const(0x08) 50 | _LCD_CURSORMOVE = const(0x00) 51 | _LCD_MOVERIGHT = const(0x04) 52 | _LCD_MOVELEFT = const(0x00) 53 | 54 | # Flags for function set 55 | _LCD_8BITMODE = const(0x10) 56 | LCD_4BITMODE = const(0x00) 57 | _LCD_2LINE = const(0x08) 58 | _LCD_1LINE = const(0x00) 59 | _LCD_5x10DOTS = const(0x04) 60 | _LCD_5x8DOTS = const(0x00) 61 | 62 | # Flags for backlight control 63 | LCD_BACKLIGHT = const(0x08) 64 | LCD_NOBACKLIGHT = const(0x00) 65 | 66 | # Flags for RS pin modes 67 | _RS_INSTRUCTION = const(0x00) 68 | _RS_DATA = const(0x01) 69 | 70 | # Pin bitmasks 71 | PIN_ENABLE = const(0x4) 72 | PIN_READ_WRITE = const(0x2) 73 | PIN_REGISTER_SELECT = const(0x1) 74 | 75 | class CursorMode: 76 | HIDE = const(_LCD_CURSOROFF | _LCD_BLINKOFF) 77 | LINE = const(_LCD_CURSORON | _LCD_BLINKOFF) 78 | BLINK = const(_LCD_CURSOROFF | _LCD_BLINKON) 79 | 80 | MICROSECOND = 1e-6 81 | MILLISECOND = 1e-3 82 | 83 | class LCD(object): 84 | 85 | def __init__(self, interface, num_cols=20, num_rows=4, char_height=8): 86 | """ 87 | Character LCD controller. 88 | 89 | :param interface: Communication interface, such as I2CInterface 90 | :param num_rows: Number of display rows (usually 1, 2 or 4). Default: 4. 91 | :param num_cols: Number of columns per row (usually 16 or 20). Default 20. 92 | :param char_height: Some 1 line displays allow a font height of 10px. 93 | Allowed: 8 or 10. Default: 8. 94 | """ 95 | self.interface = interface 96 | 97 | if char_height not in (8, 10): 98 | raise ValueError('The ``char_height`` argument should be either 8 or 10.') 99 | self.char_height = char_height 100 | 101 | self.num_rows = num_rows 102 | self.num_cols = num_cols 103 | 104 | # get row addresses (varies based on display size) 105 | self._row_offsets = (0x00, 0x40, self.num_cols, 0x40 + self.num_cols) 106 | 107 | # Setup initial display configuration 108 | displayfunction = self.interface.data_bus_mode | _LCD_5x8DOTS 109 | if self.num_rows == 1: 110 | displayfunction |= _LCD_1LINE 111 | elif self.num_rows in (2, 4): 112 | # LCD only uses two lines on 4 row displays 113 | displayfunction |= _LCD_2LINE 114 | if self.char_height == 10: 115 | # For some 1 line displays you can select a 10px font. 116 | displayfunction |= _LCD_5x10DOTS 117 | 118 | # Choose 4 or 8 bit mode 119 | self.command(0x03) 120 | time.sleep(4.5*MILLISECOND) 121 | self.command(0x03) 122 | time.sleep(4.5*MILLISECOND) 123 | self.command(0x03) 124 | if self.interface.data_bus_mode == LCD_4BITMODE: 125 | # Hitachi manual page 46 126 | time.sleep(100*MICROSECOND) 127 | self.command(0x02) 128 | elif self.interface.data_bus_mode == _LCD_8BITMODE: 129 | # Hitachi manual page 45 130 | self.command(0x30) 131 | time.sleep(4.5*MILLISECOND) 132 | self.command(0x30) 133 | time.sleep(100*MICROSECOND) 134 | self.command(0x30) 135 | else: 136 | raise ValueError('Invalid data bus mode: {}'.format(self.interface.data_bus_mode)) 137 | 138 | # Write configuration to display 139 | self.command(_LCD_FUNCTIONSET | displayfunction) 140 | time.sleep(50*MICROSECOND) 141 | 142 | # Configure entry mode. Define internal fields. 143 | self.command(_LCD_ENTRYMODESET | _LCD_ENTRYLEFT) 144 | time.sleep(50*MICROSECOND) 145 | 146 | # Configure display mode. Define internal fields. 147 | self._display_mode = _LCD_DISPLAYON 148 | self._cursor_mode = CursorMode.HIDE 149 | self.command(_LCD_DISPLAYCONTROL | self._display_mode | self._cursor_mode) 150 | time.sleep(50*MICROSECOND) 151 | 152 | self.clear() 153 | 154 | def close(self): 155 | self.interface.deinit() 156 | 157 | def set_backlight(self, value): 158 | self.interface.backlight = value 159 | 160 | def set_display_enabled(self, value): 161 | self._display_mode = _LCD_DISPLAYON if value else _LCD_DISPLAYOFF 162 | self.command(_LCD_DISPLAYCONTROL | self._display_mode | self._cursor_mode) 163 | time.sleep(50*MICROSECOND) 164 | 165 | def set_cursor_mode(self, value): 166 | self._cursor_mode = value 167 | self.command(_LCD_DISPLAYCONTROL | self._display_mode | self._cursor_mode) 168 | time.sleep(50*MICROSECOND) 169 | 170 | def cursor_pos(self): 171 | """The cursor position as a 2-tuple (row, col).""" 172 | return (self._row, self._col) 173 | 174 | def set_cursor_pos(self, row, col): 175 | if not (0 <= row < self.num_rows): 176 | raise ValueError('row should be in range 0-{}'.format(self.num_rows - 1)) 177 | if not (0 <= col < self.num_cols): 178 | raise ValueError('col should be in range 0-{}'.format(self.num_cols - 1)) 179 | self._row = row 180 | self._col = col 181 | self.command(_LCD_SETDDRAMADDR | self._row_offsets[row] + col) 182 | time.sleep(50*MICROSECOND) 183 | 184 | def print(self, string): 185 | """ 186 | Write the specified unicode string to the display. 187 | A newline ('\n') will advance to the left side of the next row. 188 | Lines that are too long automatically continue on next line. 189 | 190 | Only characters with an ``ord()`` value between 0 and 255 are currently 191 | supported. 192 | 193 | """ 194 | for char in string: 195 | if char == '\n': 196 | # Advance to next row, at left side. Wrap around to top row if at bottom. 197 | self.set_cursor_pos((self._row + 1) % self.num_rows, 0) 198 | else: 199 | self.write(ord(char)) 200 | 201 | 202 | def clear(self): 203 | """Overwrite display with blank characters and reset cursor position.""" 204 | self.command(_LCD_CLEARDISPLAY) 205 | time.sleep(2*MILLISECOND) 206 | self.home() 207 | 208 | def home(self): 209 | """Set cursor to initial position and reset any shifting.""" 210 | self.command(_LCD_RETURNHOME) 211 | self._row = 0 212 | self._col = 0 213 | time.sleep(2*MILLISECOND) 214 | 215 | def shift_display(self, amount): 216 | """Shift the display. Use negative amounts to shift left and positive 217 | amounts to shift right.""" 218 | if amount == 0: 219 | return 220 | direction = _LCD_MOVERIGHT if amount > 0 else _LCD_MOVELEFT 221 | for i in range(abs(amount)): 222 | self.command(_LCD_CURSORSHIFT | _LCD_DISPLAYMOVE | direction) 223 | time.sleep(50*MICROSECOND) 224 | 225 | def create_char(self, location, bitmap): 226 | """Create a new character. 227 | 228 | The HD44780 supports up to 8 custom characters (location 0-7). 229 | 230 | :param location: The place in memory where the character is stored. 231 | Values need to be integers between 0 and 7. 232 | :type location: int 233 | :param bitmap: The bitmap containing the character. This should be a 234 | bytearray of 8 numbers, each representing a 5 pixel row. 235 | :type bitmap: bytearray 236 | :raises AssertionError: Raised when an invalid location is passed in or 237 | when bitmap has an incorrect size. 238 | 239 | Example: 240 | 241 | .. sourcecode:: python 242 | 243 | >>> smiley = bytearray( 244 | ... 0b00000, 245 | ... 0b01010, 246 | ... 0b01010, 247 | ... 0b00000, 248 | ... 0b10001, 249 | ... 0b10001, 250 | ... 0b01110, 251 | ... 0b00000, 252 | ... ) 253 | >>> lcd.create_char(0, smiley) 254 | 255 | """ 256 | if not (0 <= location <= 7): 257 | raise ValueError('Only locations 0-7 are valid.') 258 | if len(bitmap) != 8: 259 | raise ValueError('Bitmap should have exactly 8 rows.') 260 | 261 | # Store previous position 262 | save_row = self._row 263 | save_col = self._col 264 | 265 | # Write character to CGRAM 266 | self.command(_LCD_SETCGRAMADDR | location << 3) 267 | for row in bitmap: 268 | self.interface.send(row, _RS_DATA) 269 | 270 | # Restore cursor pos 271 | self.set_cursor_pos(save_row, save_col) 272 | 273 | def command(self, value): 274 | """Send a raw command to the LCD.""" 275 | self.interface.send(value, _RS_INSTRUCTION) 276 | 277 | def write(self, value): 278 | """Write a raw character byte to the LCD.""" 279 | self.interface.send(value, _RS_DATA) 280 | if self._col < self.num_cols - 1: 281 | # Char was placed on current line. No need to reposition cursor. 282 | self._col += 1 283 | else: 284 | # At end of line: go to left side next row. Wrap around to first row if on last row. 285 | self._row = (self._row + 1) % self.num_rows 286 | self._col = 0 287 | 288 | self.set_cursor_pos(self._row, self._col) 289 | 290 | --------------------------------------------------------------------------------