Source code for philander.ssd1803a

"""Display driver module for text LCDs driven by the SSD1803A controller.
"""
__author__ = "Oliver Maye"
__version__ = "0.1"
__all__ = ["SSD1803A"]

import logging

from .display import Display
from .display_text import TextDisplay, ColorSpace, Font
from .serialbus import SerialBusDevice, SerialBusType, SerialBus, SPIMode
from .systypes import ErrorCode, RunLevel


[docs] class SSD1803A( TextDisplay ): """Driver class for a text LCD driven by SSD1803A. Although, this module aims at being generic beyond the controller hardware, clearly the DOGM204-A text display by Display Visions was in mind when developing this driver. The DOGM204 is a 4x20 characters chip-on-glass display module driven by the SSD1803A display controller by Solomon Systech. The module can be controlled via SPI or I2C. More information on that hardware can be found at: https://www.lcd-module.de/fileadmin/pdf/doma/dogm204.pdf The SSD1803A supports display sizes of up to 80 characters (max. total). Character format may be 5x8 or 6x8. Detailed technical information on the controller can be found at: https://www.solomon-systech.com/product/SSD1803A """ ADDRESSES_ALLOWED = [0x3C, 0x3D] PART_ID = 0x1A # Internal part ID, can be used to check communication REG_CMD = 0 # Command register, RS=0, D/C#=0 REG_DATA = 1 # Data or RAM register, RS=1, D/C#=1 ROM_A = 0x00 ROM_B = 0x04 ROM_C = 0x08 # Font definition FONT_A_5x8 = Font( charWidth = 5, charHeight = 8, encoding = "ascii", colorspace = ColorSpace.GRAY_1, firstAscii = 0, numCharacters = 256, name = "Western Europe 5x8", idxAdr = ROM_A ) FONT_B_5x8 = Font( charWidth = 5, charHeight = 8, encoding = "ascii", colorspace = ColorSpace.GRAY_1, firstAscii = 0, numCharacters = 256, name = "Eastern Europe 5x8", idxAdr = ROM_B ) FONT_C_5x8 = Font( charWidth = 5, charHeight = 8, encoding = "ascii", colorspace = ColorSpace.GRAY_1, firstAscii = 0, numCharacters = 256, name = "Extended European 5x8", idxAdr = ROM_C ) BUILTIN_FONTS = [ FONT_B_5x8, FONT_A_5x8, FONT_C_5x8 ] BUILTIN_FONT_NAMES = [font.name for font in BUILTIN_FONTS] def __init__(self): """Initialize the instance with defaults. Also see: :meth:`.Params_init`, :meth:`.ShiftReg.Params_init` """ super().__init__() # Derived attributes self._widthChar = 20 self._heightChar = 4 # Own attributes self._serbusdev = None # # internal helper # """Serial communication for the SSD1803A is detailed in the datasheet, Rev. 2.0, chapter 7.10. SPI ("Serial Interface", 7.10.2). Bit-order is interpreted as LSB-first. This driver encapsulates serial communication with only a handful functions and tries to accommodate both, MSB- and LSB-first configurations. """ @classmethod def _reverseBitOrder(cls, buffer): """Reverse bit order for each byte in the given buffer in-place. """ for idx in range( len(buffer) ): b = buffer[idx] invb = 0 for _ in range(8): invb = (invb << 1) | (b & 0x01) b >>= 1 buffer[idx] = invb return None def _writeCmd(self, data): ret = ErrorCode.errOk if self._serbusdev.serialBus.type == SerialBusType.SPI: # RS=0, R/W=0 buffer = [0x1F, data & 0x0F, (data & 0xF0)>>4] if self._serbusdev.serialBus.spiBitOrder == "MSB": self._reverseBitOrder(buffer) ret = self._serbusdev.writeBuffer( buffer ) elif self._serbusdev.serialBus.type == SerialBusType.I2C: # D/C#=0, Co=0 ret = self._serbusdev.writeByteRegister( 0x00, data ) else: logging.error("SSD1803A._writeCmd> Bus type %s unsupported.", self._serbusdev.serialBus.type) ret = ErrorCode.errNotImplemented return ret def _readInfo(self): bf, ac, cid, ret = False, 0, 0, ErrorCode.errOk if self._serbusdev.serialBus.type == SerialBusType.SPI: # RS=0, R/W=1 # Remember readWordRegister() reads little-endian first. if self._serbusdev.serialBus.spiBitOrder == "MSB": data, ret = self._serbusdev.readWordRegister( 0xFC ) data = [ data & 0xFF, (data & 0xFF00)>>4] self._reverseBitOrder( data ) b1 = data[0] b2 = data[1] else: data, ret = self._serbusdev.readWordRegister( 0x3F ) b1 = data & 0xFF b2 = (data & 0xFF00) >> 8 bf = b1 & 0x80 ac = b1 & 0x7F cid = b2 & 0x7F elif self._serbusdev.serialBus.type == SerialBusType.I2C: # D/C#=0, Co=0 data, ret = self._serbusdev.readWordRegister( 0x00 ) bf = data & 0x80 ac = data & 0x7F cid = (data & 0x7F00) >> 8 else: logging.error("SSD1803A._readInfo> Bus type %s unsupported.", self._serbusdev.serialBus.type) ret = ErrorCode.errNotImplemented # busy flag, AC = address counter, ID, error return bf, ac, cid, ret def _writeRAM(self, data): ret = ErrorCode.errOk if not data: ret = ErrorCode.errFewData elif self._serbusdev.serialBus.type == SerialBusType.SPI: # Start byte, RS=1, R/W=0 if self._serbusdev.serialBus.spiBitOrder == "MSB": ret = self._serbusdev.writeBuffer( [0xFA] ) else: ret = self._serbusdev.writeBuffer( [0x5F] ) for b in data: wbuf = [ (b & 0x0F), (b & 0xF0)>>4 ] if self._serbusdev.serialBus.spiBitOrder == "MSB": self._reverseBitOrder( wbuf ) ret = self._serbusdev.writeBuffer( wbuf ) if not ret.isOk(): break elif self._serbusdev.serialBus.type == SerialBusType.I2C: # D/C#=1, Co=0 ret = self._serbusdev.writeBufferRegister( 0x40, data ) else: logging.error("SSD1803A._writeRAM> Bus type %s unsupported.", self._serbusdev.serialBus.type) ret = ErrorCode.errNotImplemented return ret def _readRAM(self, num): ret = ErrorCode.errOk data = None if not isinstance( num, int ) or (num<=0): ret = ErrorCode.errInvalidParameter elif self._serbusdev.serialBus.type == SerialBusType.SPI: # RS=1, R/W=1 data = [0] * num if self._serbusdev.serialBus.spiBitOrder == "MSB": ret = self._serbusdev.writeBuffer( [0xFE,] ) else: ret = self._serbusdev.writeBuffer( [0x7F,] ) for idx in range(num): b, ret = self._serbusdev.readBuffer( 1 ) if ret.isOk(): data[idx] = b[0] else: break if ret.isOk() and (self._serbusdev.serialBus.spiBitOrder == "MSB"): self._reverseBitOrder(data) elif self._serbusdev.serialBus.type == SerialBusType.I2C: # D/C#=1, Co data, ret = self._serbusdev.readBufferRegister( 0x40, num ) else: logging.error("SSD1803A._readRAM> Bus type %s unsupported.", self._serbusdev.serialBus.type) ret = ErrorCode.errNotImplemented return data, ret # # Higer level internal helpers # def _instrFunctionSet(self, RE=0, IS=0, BE=0, REV=0): cmd = 0x30 # DL = 1 if (self._heightChar==2) or (self._heightChar==4): cmd |= 0x08 # N=1 for 2 and 4 rows, 0 otherwise if RE==0: if self._font is not None: if self._font.charHeight==16: cmd |= 0x04 # DH=1 for double-height font # Leave RE=0 if IS: cmd |= 0x01 # Set IS, as requested. else: if BE: cmd |= 0x04 # Set BE, if necessary cmd |= 0x02 # RE=1 if REV: cmd |= 0x01 # Set REV, possibly. ret = self._writeCmd( cmd ) return ret def _instrExtendedFunctionSet(self): """ Set FW, B/W and NW. """ cmd = 0x08 if self._font and (self._font.charWidth==6): cmd |= 0x04 if self._cursorMode==self.CURSOR_MODE_INVERSE: cmd |= 0x02 if self._heightChar > 2: cmd |= 0x01 # NW=1 for 3 and 4 rows, 0 otherwise ret = self._writeCmd( cmd ) return ret # # Module API #
[docs] @classmethod def Params_init( cls, paramDict ): """Initialize parameters with default values. Supported key names and their meanings are: =========================== =============================================================================================== Key Meaning, Range, Default =========================== =============================================================================================== display.SerialBusDevice.* Serial bus device config; See :meth:`.SerialBusDevice.Params_init`. display.SerialBus.* Serial bus configuration; See :meth:`.SerialBus.Params_init`. =========================== =============================================================================================== :param dict(str, object) paramDict: Dictionary of configuration settings. :return: none :rtype: None """ prefix = cls.MODULE_PARAM_PREFIX + "." serDict = cls._extractParams( paramDict, prefix) SerialBus.Params_init(serDict) SerialBusDevice.Params_init(serDict) cls._aggregateParams( paramDict, serDict, prefix ) key = cls.MODULE_PARAM_PREFIX + "." + SerialBus.MODULE_PARAM_PREFIX + ".type" if key in paramDict: if paramDict[key] == SerialBusType.I2C: key = cls.MODULE_PARAM_PREFIX + "." + SerialBusDevice.MODULE_PARAM_PREFIX + ".address" if key in paramDict: if not paramDict[key] in cls.ADDRESSES_ALLOWED: paramDict[key] = cls.ADDRESSES_ALLOWED[0] else: paramDict[key] = cls.ADDRESSES_ALLOWED[0] super().Params_init(paramDict)
def _drvOpen(self, paramDict): """Open the low-level driver and prepare it for use. :param dict(str, object) paramDict: Configuration parameters. :return: An error code indicating either success or the reason of failure. :rtype: ErrorCode """ ret = ErrorCode.errOk if self._serbusdev: ret = ErrorCode.errResourceConflict if ret.isOk(): prefixMod = self.MODULE_PARAM_PREFIX + "." # Extract serial bus parameters sparams = self._extractParams( paramDict, prefixMod ) self._serbusdev = SerialBusDevice() ret = self._serbusdev.open(sparams) if ret.isOk(): # Fundamental settings self._instrFunctionSet(RE=1) # DL, RE self._instrExtendedFunctionSet() # FW, B/W, NW self._drvSetOrientation( Display.ORIENTATION_NATURAL ) # Recommended ramp-up procedure ret = self._drvClearScreen() self._instrFunctionSet(RE=1, IS=1) self._writeCmd( 0x13 ) # Reset Bias / OSC Frequency self._writeCmd( 0x7F ) # Maximum contrast self._writeCmd( 0x5C ) # ICON on, Booster reg. on self._writeCmd( 0x6E ) # Follower control self._instrFunctionSet(RE=0) self._writeCmd( 0x0F ) # Display on, Cursor on, Blink on else: logging.error("SSD1803A._drvOpen> Couldn't open serial bus device, error: %s.", ret) self._serbusdev = None logging.debug("SSD1803A._drvOpen> Return: %s.", ret) return ret def _drvClose(self): """Close this instance and release associated hardware resources. :return: An error code indicating either success or the reason of failure. :rtype: ErrorCode """ ret = ErrorCode.errOk err = self._drvClearScreen() ret = err if ret.isOk() else ret err = self._drvSetRunLevel( RunLevel.shutdown ) ret = err if ret.isOk() else ret if self._serbusdev: err = self._serbusdev.close() self._serbusdev = None ret = err if ret.isOk() else ret logging.debug("SSD1803A._drvClose> Return: %s.", ret) return ret def _drvSetRunLevel(self, level): """Select the power-saving operation mode. :param RunLevel level: The level to switch to. :return: An error code indicating either success or the reason of failure. :rtype: ErrorCode """ ret = ErrorCode.errOk if level in [RunLevel.active, RunLevel.idle]: ret = self._instrFunctionSet(RE=1) self._writeCmd( 0x02 ) # Power down PD=0 ret = self._instrFunctionSet(RE=0) self._writeCmd( 0x0F ) # Display on, Cursor on, Blink on elif level in [RunLevel.relax, RunLevel.snooze, RunLevel.nap]: ret = self._instrFunctionSet(RE=1) self._writeCmd( 0x02 ) # Power down PD=0 ret = self._instrFunctionSet(RE=0) self._writeCmd( 0x08 ) # Display off, Cursor off, Blink off else: ret = self._instrFunctionSet(RE=0) self._writeCmd( 0x08 ) # Display off, Cursor off, Blink off ret = self._instrFunctionSet(RE=1) self._writeCmd( 0x03 ) # Power down PD=1 return ret # # Display API # def _drvSetOrientation(self, orientation): """Switch to a new display orientation. :param int orientation: The new orientation to set. One of the ``Display.ORIENTATION_*`` values. :return: An error code indicating either success or the reason of failure. :rtype: ErrorCode """ ret = ErrorCode.errOk if orientation == Display.ORIENTATION_NATURAL: ret = self._instrFunctionSet(RE=1) ret = self._writeCmd( 0x06 ) # Bottom view elif orientation == Display.ORIENTATION_ROTATE_180: ret = self._instrFunctionSet(RE=1) ret = self._writeCmd( 0x05 ) # Top view else: logging.debug("SSD1803A._drvSetOrientation> Invalid orientation: %s.", orientation) ret = ErrorCode.errNotSupported return ret def _drvSetBrightness(self, value): """Configure the brightness intensity. Note that the LED backlight cannot be controlled by the display driver chip. As this this external circuitry, the brightness must be adjusted by either a potentiometer or PWM. :param int value: The new value to set the brightness to. :return: An error code indicating either success or the reason of failure. :rtype: ErrorCode """ logging.debug("SSD1803A._drvSetBrightness> Not supported. value=%d", value) return ErrorCode.errNotSupported def _drvSetContrast(self, value): """ Adjusts the contrast intensity. :param int value: The new value to set the contrast to. :return: An error code indicating either success or the reason of failure. :rtype: ErrorCode """ # Scale percentage 0...100 -> 0...63 6 bit number C5...C0 contrast = int( value / 101 * 64 ) & 0x3F # Upper C5 and C4 go to power control as DB1 and DB0 ret = self._instrFunctionSet(RE=0, IS=1) self._writeCmd( 0x5C | (contrast >> 4) ) # Ion=1, Bon=1, DB1=C5, DB0=C4 ret = self._writeCmd( 0x70 | (contrast & 0x0F) ) # Contrast set, DB3...DB0=C3...C0 logging.debug("SSD1803A._drvSetContrast> value=%d, return: %s", value, ret) return ret def _drvSetInverse(self, inverseOn=True): """ Invert the display. :param bool inverseOn: `True` for inverse mode, `False` for normal mode. :return: An error code indicating either success or the reason of failure. :rtype: ErrorCode """ ret = self._instrFunctionSet(RE=1, BE=1, REV=inverseOn) logging.debug("SSD1803A._drvSetInverse> value=%d, return: %s", inverseOn, ret) return ret # # TextDisplay API # def _drvSetCursorMode( self, mode ): """Switches the hardware cursor to a specific appearance. :param mode: The cursor mode to switch to. One of the `CURSOR_MODE_*` values. :return: An error code indicating either success or the reason of failure. :rtype: ErrorCode """ del mode return ErrorCode.errNotSupported def _drvSetCursorSize( self, width, height ): """Sets the size of the cursor. :param width: The width of the cursor. One of the `CURSOR_SIZE_*` values. :param height: The height of the cursor. One of the `CURSOR_SIZE_*` values. :return: An error code indicating either success or the reason of failure. :rtype: ErrorCode """ del width, height return ErrorCode.errNotSupported def _drvMoveCursorTo( self, x, y ): """Sets the new absolute position of the hardware cursor. :param int x: The horizontal cursor position, given in characters. :param int y: The vertical cursor position, given in characters. :return: An error code indicating either success or the reason of failure. :rtype: ErrorCode """ del x, y return ErrorCode.errNotSupported def _drvGoTo( self, x, y ): """Move the internal ``current position`` to the specified absolute position. :param int x: The new horizontal position. :param int y: The new vertical position. :return: An error code indicating either success or the reason of failure. :rtype: ErrorCode """ # According to the data sheet, chapter 7.2, the range (!) of the # address counter (AC) depends on the number of rows as follows: # 1 line : 0x00 - 0x4F # 2 lines: 0x00 - 0x27, 0x40 - 0x67 # 3 lines: 0x00 - 0x13, 0x20 - 0x33, 0x40 - 0x53 # 4 lines: 0x00 - 0x13, 0x20 - 0x33, 0x40 - 0x53, 0x60 - 0x73 # Actual addressing also depends on the character width (5/6 dots). if y==0: ac = 0x00 elif y==1: ac = 0x20 elif y==2: ac = 0x40 else: ac = 0x60 ac += x cmd = 0x80 | ac self._instrFunctionSet(RE=0) ret = self._writeCmd( cmd ) # set AC logging.debug("SSD1803A._drvGoTo> x=%d y=%d, new ac=0x%02x return: %s", x, y, ac, ret) return ret def _drvClearScreen( self ): """Clear all contents from screen. :return: An error code indicating either success or the reason of failure. :rtype: ErrorCode """ ret = self._writeCmd( 0x01 ) # Clear Display return ret def _drvScrollV( self, numLines ): """Scroll the contents by the given number of lines. A positive argument makes the content scroll up, so the view port moves down, while for a negative argument it's vice versa. The content scrolled off the screen is not buffered. So, when scrolling back, it's the responsibility of the caller to reproduce that content, again. :param int numLines: The number of lines to scroll. :return: An error code indicating either success or the reason of failure. :rtype: ErrorCode """ del numLines return ErrorCode.errNotSupported def _drvGetBuiltinFontNames(self): """Retrieve the names of the fonts possibly built-in the display hardware. The names of all built-fonts are returned as a list. If there is no built-in fonts, an empty list is returned. If there is a default font, its name is assumed to be the first in the list - at index zero. :return: The list of font names and an error code indicating either success or the reason of failure. :rtype: Tuple( list(str), ErrorCode) """ return SSD1803A.BUILTIN_FONT_NAMES, ErrorCode.errOk def _drvGetBuiltinFont(self, name=""): """Retrieve the built-in fonts given its name. If the name parameter is an empty string, the default font is retrieved. :param str name: The name of the font to retrieve. :return: The font and an error code indicating either success or the reason of failure. :rtype: Tuple( Font, ErrorCode) """ ret = None err = ErrorCode.errOk if not name: ret = SSD1803A.BUILTIN_FONTS[0] err = ErrorCode.errOk else: try: idx = SSD1803A.BUILTIN_FONT_NAMES.index( name ) ret = SSD1803A.BUILTIN_FONTS[idx] err = ErrorCode.errOk except ValueError: ret = None err = ErrorCode.errInvalidParameter return ret, err def _drvSetFont( self, font ): """Select a new font as current. Implementations will also want to update internal attributes like _widthChar, _currentX or _cursorX. :param font: The new character font to set as current. :return: An error code indicating either success or the reason of failure. :rtype: ErrorCode """ self._instrFunctionSet( RE=1 ) self._writeCmd( 0x72 ) # ROM selection self._writeRAM( [font.idxAdr] ) ret = self._instrExtendedFunctionSet() return ret def _drvPrintChar( self, code): """Print a character at the internal ``current position``. :param int code: The ASCII code of the character to print. :return: An error code indicating either success or the reason of failure. :rtype: ErrorCode """ ret = self._writeRAM( code ) return ret