aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlexander Bazarov <63168142+bazarovdev@users.noreply.github.com>2024-12-02 13:23:46 -0500
committerGitHub <noreply@github.com>2024-12-02 13:23:46 -0500
commitaecb29d2b02eed7846204fad9b40b584f79d0095 (patch)
tree895b10a129be50ef95dac116b7f13bac7a57d943
parent9ce631e8d1a7b739f96c360a7a4250489ae8deb8 (diff)
downloadkutter-aecb29d2b02eed7846204fad9b40b584f79d0095.tar.gz
kutter-aecb29d2b02eed7846204fad9b40b584f79d0095.tar.xz
kutter-aecb29d2b02eed7846204fad9b40b584f79d0095.zip
display: Add support for `AIP31068` based displays (#6639)
display: Add support for `AIP31068` based displays
-rw-r--r--docs/Config_Reference.md32
-rw-r--r--klippy/extras/display/aip31068_spi.py209
-rw-r--r--klippy/extras/display/display.py5
3 files changed, 241 insertions, 5 deletions
diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md
index d73a3a64..6a5a7582 100644
--- a/docs/Config_Reference.md
+++ b/docs/Config_Reference.md
@@ -4139,15 +4139,16 @@ Support for a display attached to the micro-controller.
[display]
lcd_type:
# The type of LCD chip in use. This may be "hd44780", "hd44780_spi",
-# "st7920", "emulated_st7920", "uc1701", "ssd1306", or "sh1106".
+# "aip31068_spi", "st7920", "emulated_st7920", "uc1701", "ssd1306", or
+# "sh1106".
# See the display sections below for information on each type and
# additional parameters they provide. This parameter must be
# provided.
#display_group:
# The name of the display_data group to show on the display. This
# controls the content of the screen (see the "display_data" section
-# for more information). The default is _default_20x4 for hd44780
-# displays and _default_16x4 for other displays.
+# for more information). The default is _default_20x4 for hd44780 or
+# aip31068_spi displays and _default_16x4 for other displays.
#menu_timeout:
# Timeout for menu. Being inactive this amount of seconds will
# trigger menu exit or return to root menu when having autorun
@@ -4273,6 +4274,31 @@ spi_software_miso_pin:
...
```
+#### aip31068_spi display
+
+Information on configuring an aip31068_spi display - a very similar to hd44780_spi
+a 20x04 (20 symbols by 4 lines) display with slightly different internal
+protocol.
+
+```
+[display]
+lcd_type: aip31068_spi
+latch_pin:
+spi_software_sclk_pin:
+spi_software_mosi_pin:
+spi_software_miso_pin:
+# The pins connected to the shift register controlling the display.
+# The spi_software_miso_pin needs to be set to an unused pin of the
+# printer mainboard as the shift register does not have a MISO pin,
+# but the software spi implementation requires this pin to be
+# configured.
+#line_length:
+# Set the number of characters per line for an hd44780 type lcd.
+# Possible values are 20 (default) and 16. The number of lines is
+# fixed to 4.
+...
+```
+
#### st7920 display
Information on configuring st7920 displays (which is used in
diff --git a/klippy/extras/display/aip31068_spi.py b/klippy/extras/display/aip31068_spi.py
new file mode 100644
index 00000000..75f4978f
--- /dev/null
+++ b/klippy/extras/display/aip31068_spi.py
@@ -0,0 +1,209 @@
+# Support for YHCB2004 (20x4 text) LCD displays based on AiP31068 controller
+#
+# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
+# Copyright (C) 2018 Eric Callahan <arksine.code@gmail.com>
+# Copyright (C) 2021 Marc-Andre Denis <marcadenis@msn.com>
+# Copyright (C) 2024 Alexander Bazarov <oss@bazarov.dev>
+#
+# This file may be distributed under the terms of the GNU GPLv3 license.
+
+# This file is a modified version of hd44780_spi.py, introducing slightly
+# different protocol as implemented in Marlin FW (based on
+# https://github.com/red-scorp/LiquidCrystal_AIP31068 ).
+# In addition, a hack is used to send 8 commands, each 9 bits, at once,
+# allowing the transmission of a full 9 bytes.
+# This helps avoid modifying the SW_SPI driver to handle non-8-bit data.
+
+from .. import bus
+
+LINE_LENGTH_DEFAULT=20
+LINE_LENGTH_OPTIONS={16:16, 20:20}
+
+TextGlyphs = { 'right_arrow': b'\x7e' }
+
+# Each command is 9 bits long:
+# 1 bit for RS (Register Select) - 0 for command, 1 for data
+# 8 bits for the command/data
+# Command is a bitwise OR of CMND(=opcode) and flg_CMND(=parameters) multiplied
+# by 1 or 0 as En/Dis flag.
+# cmd = CMND | flg_CMND.param0*0 | flg_CMND.param1*1
+# or just by OR with enabled flags:
+# cmd = CMND | flg_CMND.param1
+class CMND:
+ CLR = 1 # Clear display
+ HOME = 2 # Return home
+ ENTERY_MODE = 2**2 # Entry mode set
+ DISPLAY = 2**3 # Display on/off control
+ SHIFT = 2**4 # Cursor or display shift
+ FUNCTION = 2**5 # Function set
+ CGRAM = 2**6 # Character Generator RAM
+ DDRAM = 2**7 # Display Data RAM
+ WRITE_RAM = 2**8 # Write to RAM
+
+# Define flags for all commands:
+class flg_ENTERY_MODE:
+ INC = 2**1 # Increment
+ SHIFT = 2**0 # Shift display
+
+class flg_DISPLAY:
+ ON = 2**2 # Display ON
+ CURSOR = 2**1 # Cursor ON
+ BLINK = 2**0 # Blink ON
+
+class flg_SHIFT:
+ WHOLE_DISPLAY = 2**3 # Shift whole display
+ RIGHT = 2**2 # Shift right
+
+class flg_FUNCTION:
+ TWO_LINES = 2**3 # 2-line display mode
+ FIVE_BY_ELEVEN = 2**2 # 5x11 dot character font
+
+class flg_CGRAM:
+ MASK = 0b00111111 # CGRAM address mask
+
+class flg_DDRAM:
+ MASK = 0b01111111 # DDRAM address mask
+
+class flg_WRITE_RAM:
+ MASK = 0b11111111 # Write RAM mask
+
+DISPLAY_INIT_CMNDS= [
+ # CMND.CLR - no need as framebuffer will rewrite all
+ CMND.HOME, # move cursor to home (0x00)
+ CMND.ENTERY_MODE | flg_ENTERY_MODE.INC, # increment cursor and no shift
+ CMND.DISPLAY | flg_DISPLAY.ON, # keep cursor and blinking off
+ CMND.SHIFT | flg_SHIFT.RIGHT, # shift right cursor only
+ CMND.FUNCTION | flg_FUNCTION.TWO_LINES, # 2-line display mode, 5x8 dots
+]
+
+class aip31068_spi:
+ def __init__(self, config):
+ self.printer = config.get_printer()
+ # spi config
+ self.spi = bus.MCU_SPI_from_config(
+ config, 0x00, pin_option="latch_pin") # (config, mode, cs_name)
+ self.mcu = self.spi.get_mcu()
+ self.icons = {}
+ self.line_length = config.getchoice('line_length', LINE_LENGTH_OPTIONS,
+ LINE_LENGTH_DEFAULT)
+ # each controller's line is 2 lines on the display and hence twice
+ # line length
+ self.text_framebuffers = [bytearray(b' '*2*self.line_length),
+ bytearray(b' '*2*self.line_length)]
+ self.glyph_framebuffer = bytearray(64)
+ # all_framebuffers - list of tuples per buffer type.
+ # Each tuple contains:
+ # 1. the updated framebuffer
+ # 2. a copy of the old framebuffer == data on the display
+ # 3. the command to send to write to this buffer
+ # Then flush() will compare new data with data on the display
+ # and send only the differences to the display controller
+ # and update the old framebuffer with the new data
+ # (immutable tuple is allowed to store mutable bytearray)
+ self.all_framebuffers = [
+ # Text framebuffers
+ (self.text_framebuffers[0], bytearray(b'~'*2*self.line_length),
+ CMND.DDRAM | (flg_DDRAM.MASK & 0x00) ),
+ (self.text_framebuffers[1], bytearray(b'~'*2*self.line_length),
+ CMND.DDRAM | (flg_DDRAM.MASK & 0x40) ),
+ # Glyph framebuffer
+ (self.glyph_framebuffer, bytearray(b'~'*64),
+ CMND.CGRAM | (flg_CGRAM.MASK & 0x00) ) ]
+ @staticmethod
+ def encode(data, width = 9):
+ encoded_bytes = []
+ accumulator = 0 # To accumulate bits
+ acc_bits = 0 # Count of bits in the accumulator
+ for num in data:
+ # check that num will fit in width bits
+ if num >= (1 << width):
+ raise ValueError("Number {} does not fit in {} bits".
+ format(num, width))
+ # Shift the current number into the accumulator from the right
+ accumulator = (accumulator << width) | num
+ acc_bits += width # Update the count of bits in the accumulator
+ # While we have at least 8 bits, form a byte and append it
+ while acc_bits >= 8:
+ acc_bits -= 8 # Decrease bit count by 8
+ # Extract the 8 most significant bits to form a byte
+ byte = (accumulator >> acc_bits) & 0xFF
+ # Remove msb 8 bits from the accumulator
+ accumulator &= (1 << acc_bits) - 1
+ encoded_bytes.append(byte)
+ # Handle any remaining bits by padding them on the right to byte
+ if acc_bits > 0:
+ last_byte = accumulator << (8 - acc_bits)
+ encoded_bytes.append(last_byte)
+ return encoded_bytes
+ def send(self, data, minclock=0):
+ # different commands have different processing time
+ # to avoid timing violation pad with some fast command, e.g. ENTRY_MODE
+ # that has execution time of 39us (for comparison CLR is 1.53ms)
+ pad = CMND.ENTERY_MODE | flg_ENTERY_MODE.INC
+ for i in range(0, len(data), 8):
+ # Take a slice of 8 numbers
+ group = data[i:i+8]
+ # Pad the group if it has fewer than 8 elements
+ if len(group) < 8:
+ group.extend([pad] * (8 - len(group)))
+ self.spi.spi_send(self.encode(group), minclock)
+ def flush(self):
+ # Find all differences in the framebuffers and send them to the chip
+ for new_data, old_data, fb_cmnd in self.all_framebuffers:
+ if new_data == old_data:
+ continue
+ # Find the position of all changed bytes in this framebuffer
+ diffs = [[i, 1] for i, (n, o) in enumerate(zip(new_data, old_data))
+ if n != o]
+ # Batch together changes that are close to each other
+ for i in range(len(diffs)-2, -1, -1):
+ pos, count = diffs[i]
+ nextpos, nextcount = diffs[i+1]
+ if pos + 4 >= nextpos and nextcount < 16:
+ diffs[i][1] = nextcount + (nextpos - pos)
+ del diffs[i+1]
+ # Transmit changes
+ for pos, count in diffs:
+ chip_pos = pos
+ self.send([fb_cmnd + chip_pos])
+ self.send([CMND.WRITE_RAM | byte for byte in
+ new_data[pos:pos+count]])
+ old_data[:] = new_data
+ def init(self):
+ curtime = self.printer.get_reactor().monotonic()
+ print_time = self.mcu.estimated_print_time(curtime)
+ for i, cmds in enumerate(DISPLAY_INIT_CMNDS):
+ minclock = self.mcu.print_time_to_clock(print_time + i * .100)
+ self.send([cmds], minclock=minclock)
+ self.flush()
+ def write_text(self, x, y, data):
+ if x + len(data) > self.line_length:
+ data = data[:self.line_length - min(x, self.line_length)]
+ pos = x + ((y & 0x02) >> 1) * self.line_length
+ self.text_framebuffers[y & 1][pos:pos+len(data)] = data
+ def set_glyphs(self, glyphs):
+ for glyph_name, glyph_data in glyphs.items():
+ data = glyph_data.get('icon5x8')
+ if data is not None:
+ self.icons[glyph_name] = data
+ def write_glyph(self, x, y, glyph_name):
+ data = self.icons.get(glyph_name)
+ if data is not None:
+ slot, bits = data
+ self.write_text(x, y, [slot])
+ self.glyph_framebuffer[slot * 8:(slot + 1) * 8] = bits
+ return 1
+ char = TextGlyphs.get(glyph_name)
+ if char is not None:
+ # Draw character
+ self.write_text(x, y, char)
+ return 1
+ return 0
+ def write_graphics(self, x, y, data):
+ pass # this display supports only hardcoded or 8 user defined glyphs
+ def clear(self):
+ spaces = b' ' * 2*self.line_length
+ self.text_framebuffers[0][:] = spaces
+ self.text_framebuffers[1][:] = spaces
+ def get_dimensions(self):
+ return (self.line_length, 4)
diff --git a/klippy/extras/display/display.py b/klippy/extras/display/display.py
index 2ce352de..e9ba31d6 100644
--- a/klippy/extras/display/display.py
+++ b/klippy/extras/display/display.py
@@ -6,7 +6,7 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, os, ast
-from . import hd44780, hd44780_spi, st7920, uc1701, menu
+from . import aip31068_spi, hd44780, hd44780_spi, st7920, uc1701, menu
# Normal time between each screen redraw
REDRAW_TIME = 0.500
@@ -17,7 +17,8 @@ LCD_chips = {
'st7920': st7920.ST7920, 'emulated_st7920': st7920.EmulatedST7920,
'hd44780': hd44780.HD44780, 'uc1701': uc1701.UC1701,
'ssd1306': uc1701.SSD1306, 'sh1106': uc1701.SH1106,
- 'hd44780_spi': hd44780_spi.hd44780_spi
+ 'hd44780_spi': hd44780_spi.hd44780_spi,
+ 'aip31068_spi':aip31068_spi.aip31068_spi
}
# Storage of [display_template my_template] config sections