dhylands / python_lcd

Python based library for talking to character based LCDs.
MIT License
298 stars 116 forks source link

Reduce code size and memory footprint #46

Open rkompass opened 1 year ago

rkompass commented 1 year ago

On the basis of the code here which I gratefully took as a starting point I derived a driver (I2C) which has reduced code size and memory footprint, while somewhat improving functionality. It is here. Memory consumption went down from 4896 to 1616 bytes. This was achieved by combining functions and replacing constant variables with their values. The new version also should be easier for new users, as it is only one file, which probably should run on many platforms. I would be happy to provide a set of (or a combined) pull request, if you are interested (thx for feedback here). The same for a version for other interfaces than I2C and a more universal test script.

dhylands commented 1 year ago

I'd definitely be interested in taking a look.

dhylands commented 1 year ago

@rkompass Unfortunately the link above doesn't seem to be working

rkompass commented 1 year ago

Hello Dave, thank you, I see, there went something wrong. I'm not experienced yet with Github.

The file lcd_i2c8574.py (my driver) is this:

# Implements a HD44780 character LCD connected via PCF8574 on I2C
# Should work with ESP8266 and (tested) Pyboard 1.1
#
# Derived from https://github.com/dhylands/python_lcd by rkompass 2022
#    (put everything in one file, remove constants, simplify....) resulting in only ~1.6 K memory consumption.

from time import sleep_ms, sleep_us

class I2cLcd: # Implements a HD44780 character LCD connected via PCF8574 on I2C

    def __init__(self, i2c, i2c_addr, n_lin, n_col):
        self.i2c = i2c
        self.i2c_addr = i2c_addr
        self.n_lin = min(n_lin, 4)
        self.n_col = min(n_col, 40)
        self.cur_x = 0
        self.cur_y = 0
        self.impl_nl = False         # implicit newline: we have just wrapped around to start of new line
        self.backl = 0x08

        self.i2c.writeto(self.i2c_addr, bytearray([0]))          # init I2C
        sleep_ms(20)                                             # Allow LCD time to powerup
        for _ in range(3):                                       # send reset 3 times
            self.i2c.writeto(self.i2c_addr, bytearray((0x34, 0x30))) # LCD_FUNCTION_RESET
            sleep_ms(5)                                          # need to delay at least 4.1 msec
        self.i2c.writeto(self.i2c_addr, bytearray((0x24, 0x20))) # LCD_FUNCTION, put LCD into 4 bit mode
        sleep_ms(1)
        self.set_display(False)
        self.clear()
        self._wr(0x06)                         # LCD_ENTRY_MODE | LCD_ENTRY_INC
        self.set_cursor(False)
        self.set_display(True)                 # we might include a backlight option here
        self._wr(0x28 if n_lin > 1 else 0x20)  # LCD_FUNCTION_2LINES if n_lin > 1 else LCD_FUNCTION

    def clear(self):        # Clears the LCD display and moves the cursor to the top left
        self._wr(0x01)      # LCD_CLR
        self._wr(0x02)      # LCD_HOME
        self.cur_x = 0
        self.cur_y = 0

    def set_cursor(self, show=False, blink=False): # Causes the cursor to be made visible if show or even blink
        self._wr(0x0f if blink else (0x0e if show else 0x0c))  # LCD_ON_CTRL | LCD_ON_DISPLAY | (LCD_ON_CURSOR) 

    def set_display(self, on=True, backl=None):  # Turns the LCD on (unblanks) or off, optionally sets backlight
        self._wr(0x0c if on else 0x08)  # LCD_ON_CTRL | LCD_ON_DISPLAY
        if backl is not None:
            self.backl = 0x08 if backl else 0x00
            self.i2c.writeto(self.i2c_addr, bytearray((self.backl,)))

    def move_to(self, cur_x, cur_y):  # Moves the cursor position to the indicated position
        self.cur_x = cur_x            # The cursor position is zero based (x == 0 -> first column)
        self.cur_y = cur_y
        addr = cur_x & 0x3f
        if cur_y & 1:     # Lines 1 & 3 add 0x40
            addr += 0x40 
        if cur_y & 2:    # Lines 2 & 3 add number of columns
            addr += self.n_col
        self._wr(0x80 | addr) # LCD_DDRAM | ..

    def write(self, string='', end='\n'):  # Writes the string at the current cursor pos and advances cursor
        for c in ''.join((string, end)):
            if self.cur_x == 0:                   # Clear the line that we have just begun.
                for _ in range(self.n_col):
                    self._wr(32, 1)               # ord(' ')  = 32
                self.move_to(0, self.cur_y)
            if c == '\n':
                if self.impl_nl:                  # We advanced due to a wraparound,
                    pass                          #   --> we ignore a newline right after that.
                else:
                    self.cur_x = self.n_col
            else:
                self._wr(ord(c), 1)
                self.cur_x += 1
            if self.cur_x >= self.n_col:
                self.cur_x = 0
                self.cur_y += 1
                self.impl_nl = (c != '\n')
                if self.cur_y >= self.n_lin:
                    self.cur_y = 0
                self.move_to(0, self.cur_y)
            else:
                self.impl_nl = False

    def define_char(self, location, charmap):   # Write a character to one of the 8 CGRAM locations,
        location &= 0x7                         #     available as chr(0) through chr(7).
        self._wr(0x40 | (location << 3))         # LCD_CGRAM | ..
        sleep_us(40)
        for i in range(8):
            self._wr(charmap[i], 1)
            sleep_us(40)
        self.move_to(self.cur_x, self.cur_y)

    def _wr(self, data, dbit=0):  # Write to the LCD; dbit: 0..command, 1..data
        b0 = dbit | self.backl | data & 0xf0
        b1 = dbit | self.backl | ((data & 0x0f) << 4)
        self.i2c.writeto(self.i2c_addr, bytearray((b0 | 0x04, b0, b1 | 0x04, b1)))
        if not dbit and data <= 3: # The home and clear commands require a worst case delay of 4.1 msec
            sleep_ms(5)

I'm thinking about adding an option for scrolling text, so that you may almost write like with print() with the difference that after a newline with print() you see the empty line but withlcd.write() you still are on the old line (because you only have 4 lines maximum) and only when the new line is really written then the scrolling takes place. This would require 80 bytes for storing the written content which is not much. Hopefully this would mean maximum simplicity as one would only have to memorize the behavior of print(). define_char() could be renamed back to custom_char() if you prefer that. putchar() imho is not necessary as it is as easy just to write the single char with write() or putstring() respectively.

The above code which imports only time should run on most platforms. A test script ideally would be written so that it queries the platform and then dependent on that instantiates i2c, perhaps queries the presence of devices there. My current test script is:

"""Implements a HD44780 character LCD connected via PCF8574 on I2C.
   This code should work for at least esp8266 and (tested) Pyboard"""
import gc
from time import sleep_ms
from machine import I2C
gc.collect(); mfree0 = gc.mem_free()
from lcd_i2c8574 import I2cLcd
# from esp8266_i2c_lcd import I2cLcd
i2c = I2C('Y', freq=100000)
PCF8574_ADDR = 0x27   # i2c address, may be changed by soldering connections
lcd = I2cLcd(i2c, PCF8574_ADDR, 4, 20)
gc.collect(); mfree1 = gc.mem_free()
print('Free mem:', mfree1, '. Used by LCD:', mfree0-mfree1)

print("Running test_main")
i2c = I2C('Y', freq=100000)
lcd = I2cLcd(i2c, PCF8574_ADDR, 4, 20)

lcd.write("It works!")
sleep_ms(2000)
print("LCD backlight off")
lcd.set_display(backl=False)
sleep_ms(2000)
print("LCD backlight on")
lcd.set_display(backl=True)
sleep_ms(2000)
print("LCD blink cursor on")
lcd.set_cursor(blink=True)
sleep_ms(2000)
print("LCD blink cursor off")
lcd.set_cursor(show=True)
sleep_ms(2000)
print("LCD hide cursor")
lcd.set_cursor(show=False);
sleep_ms(2000)

lcd.write("Second line")
sleep_ms(2000)
lcd.write("New line, which will be quite long and \nyet another line after all!")
sleep_ms(8000)
lcd.clear()

print("LCD define custom characters and write them")
# smiley faces as custom characters
happy = bytearray([0x00,0x0A,0x00,0x04,0x00,0x11,0x0E,0x00])
sad = bytearray([0x00,0x0A,0x00,0x04,0x00,0x0E,0x11,0x00])
grin = bytearray([0x00,0x00,0x0A,0x00,0x1F,0x11,0x0E,0x00])
shock = bytearray([0x0A,0x00,0x04,0x00,0x0E,0x11,0x11,0x0E])
meh = bytearray([0x00,0x0A,0x00,0x04,0x00,0x1F,0x00,0x00])
angry = bytearray([0x11,0x0A,0x11,0x04,0x00,0x0E,0x11,0x00])
tongue = bytearray([0x00,0x0A,0x00,0x04,0x00,0x1F,0x05,0x02])
lcd.define_char(0, happy)
lcd.define_char(1, sad)
lcd.define_char(2, grin)
lcd.define_char(3, shock)
lcd.define_char(4, meh)
lcd.define_char(5, angry)
lcd.define_char(6, tongue)

lcd.write("Custom chars:\n")
for i in range(7):
    lcd.write(" ", end='')
    lcd.write(chr(i), end='')
    sleep_ms(200)

sleep_ms(1000)
lcd.clear()

print("LCD write at (0,0)-position")
lcd.move_to(0, 0)
lcd.write("Here from (0,0) on")
sleep_ms(2000)
print("LCD write at (5,3)-position")
lcd.move_to(5, 3)
lcd.write("Here from (5,3) on")
sleep_ms(2000)
lcd.clear()
lcd.write("Again a line, which will be quite long.")
lcd.write("Short line")
lcd.write("X")
lcd.write()
lcd.write('Short', end='')
lcd.write('..short', end='')
lcd.write('..fine!')

Thank you for comments. Raul

freemansoft commented 1 year ago

The file doesn't appear to be in the referenced repository.

rkompass commented 1 year ago

For that reason I presented the file above. I now found a bug in write() and will try to remove it in the next days (without hurry). After that I will setup the repository again.

rkompass commented 1 year ago

@dhylands @freemansoft I have updated my code and it is now in the Github Repository here.