Due to high demand expect some shipping delays at this time, orders may not ship for 3-4 business days. On MLK Day no orders will be shipped.

Forgive Me: hoping for help on abusing Displayio for old LCD
Moderators: adafruit_support_bill, adafruit

Please be positive and constructive with your questions and comments.

Forgive Me: hoping for help on abusing Displayio for old LCD

by stephenc1 on Tue Nov 30, 2021 2:01 pm


(TLDR: I have a 1983-vintage LCD screen that's going to need some bitbanged pixels, which I'd like to extract from a Group or Tilemap so I don't have make my own sad display stack for managing text streams, etc.)

I'm running CircuitPython on a Teensy 4.1, which I hope to use as the central brains of a retroconversion of a Tandy M100 ( https://en.wikipedia.org/wiki/TRS-80_Model_100 ): I'm just keeping the keyboard and 240x64 pixel monochrome LCD display and tossing the guts. To that end, does anyone have any tips on creating a screen framebuffer from a displayio Group or Tilemap, i.e. extracting the value of the composited pixels one by one?

I realize this goes against how displayio is supposed to be used, and I'm only doing it because of two hardware issues. First, the 1983-vintage LCD is squirrely by modern standards. It's actually driven by 10 individual LCD chips, each covering a 50x32 region of the screen, and each with their own chip select lines (5 by 50 is 250, but those extra 10 columns at the end are just not connected). Each region is then divided into 4 banks, 8 bits high by 50 bits/columns wide. Data and instructions are clocked in. So to write to a pixel, you select the chip for that region, then send the chip a bank select command, then a column select, then switch to data mode, and you write a byte containing the pixel you want to set. To speed up transfers, the chip then automatically advances the col position by one, so once selected you can fill an entire bank with one transfer of 50 bytes. Everything happens at 5V (plus a required -5V supply, and a polarization control voltage that varies between 0 and 4V and which was originally set by a potentiometer).

As you can see, this doesn't map neatly on to modern notions of how display rows and columns are supposed to work. So the idea is to have my Teensy talk to an Arduino Mega, where the Mega pretends to be a 8080-style interface with just one chip select, etc., and does the work of converting a received 240x64 pixel framebuffer into the individual chip selects and commands, also doubling as a voltage shifter from the 3.3V Teensy and generating the analog polarization control voltage.

I would just use the Parelldisplay.parellelbus to talk to the Mega BUT then there's Hardware issue #2. The Teensy 4.1 doesn't support ParallelBus because it doesn't have ports in the Arduino sense. I'll need to bitbang the interface lines so I can send a framebuffer from the Teensy to the Mega.

I could use things like Displayio.Bitmap with Adafruit_display_text.bitmap_label, and then just bitbang that bitmap buffer, but I would then end up having to reinvent a lot of wheels, for example the VT100 stuff in terminalio. It would be really nice to be able to use Groups or Tilemaps, but I need some pointers on how I can extract composited pixels from them so I can send the screen framebuffer. I realize this is not going to be great performance wise, but I did I mention the original hardware was from 1983? :) I thought the framebufferio module might be the thing, but it only seems to work with RGBMatrix (if I try to use a non-RBGMatrix bitmap buffer with it, it just crashes the Teensy hard, and I have to reinstall CircuitPython!)

Any advice would be greatly appreciated! Thanks for taking the time to read,



Posts: 4
Joined: Wed Jul 31, 2013 11:56 am

Re: Forgive Me: hoping for help on abusing Displayio for old

by tannewt on Tue Nov 30, 2021 2:25 pm

That sounds like a really neat project! I have two ideas:

1. Use FourWire (basically SPI) instead of parallelbus because the mega may be able to be a SPI device.
2. displayio.Display has `fill_row` that can be used to extract pixels: https://circuitpython.readthedocs.io/en ... y.fill_row

You can do 2 without a display by pretending there is one connected.

Posts: 2735
Joined: Thu Oct 06, 2016 8:48 pm

Re: Forgive Me: hoping for help on abusing Displayio for old

by stephenc1 on Tue Nov 30, 2021 3:47 pm

Ah, it just didn't occur to me to try #2 with a sham display! Perfect! Thank you, you are a scholar and a gent!


( I might also try the SPI route if the wiring looks like it's getting out control. )

Posts: 4
Joined: Wed Jul 31, 2013 11:56 am

Re: Forgive Me: hoping for help on abusing Displayio for old

by stephenc1 on Wed Dec 01, 2021 3:29 am

Thanks to your help, I got this working: for the sake of anyone else who might be trying similar extractions and landed here via Google, my quick and dirty but working demo code is below.

Two main notes regarding how fill_row is used in this demo:

First, as per lines 445 to 450 of Display.c, fill_row only works with display objects that are using the default 16-bit colorspace (so the sham display can't be monochrome), and the buffer fill_row writes to must be a bytearray rather than any of the other writeable buffers.

Second, if pixel x,y of the sham display is on, and pixel x,y+1 is off, when fill_row extracts the y row followed by the y+1 row, the pixel will be left on in the bytearray buffer even after the y+1 extraction, i.e. making the pixel smear into to the next (and all subsequent) row reads unless you rezero the buffer before the next fill_row extraction.

Code: Select all | TOGGLE FULL SIZE
import displayio
import board
import terminalio
from adafruit_display_text import label

LCD_width = 240
LCD_height = 64

Tandyframe = displayio.Bitmap(LCD_width, LCD_height, 1) #Set up as packed 1-bit per pixel monochrome bitmap


#create sham display with nothing actually connected to pins
spi = board.SPI()
tft_cs = board.D5
tft_dc = board.D6

while not spi.try_lock():

display_bus = displayio.FourWire(
    spi, command=tft_dc, chip_select=tft_cs, reset=board.D9

init_sequence = (b"\x00") # only need a null sequence for sham display

display = displayio.Display(display_bus,

#lets put some sample text onto the sham display
text = "Hello world!"
font = terminalio.FONT
color = 0xFFFFFF #An RGB value that makes checking if pixels are on and converting to monochrome easy even after the internal conversion to a 16-bit colorspace

text_area = label.Label(font, text=text, color=color)
text_area.x = 5
text_area.y = 5


#setup a buffer to hold each extracted row of pixels from the display
line_buffer = bytearray(LCD_width*2) #2x the screen width because our sham display has to have a 2-bytes-per-pixel colorspace for fill_row to work

#extract the pixels line by line and copy to the monochrome bitmap buffer
for y in range(0, LCD_height):
    display.fill_row(y, line_buffer)
    for x in range(0, (LCD_width*2), 2):
        if line_buffer[x] != 0: #because of our choice of RGB color, only need to check the first byte of each pixel
            Tandyframe[x//2,y] = 1
        line_buffer[x] = 0 # make sure the bytearray buffer is zeroed at that location for the next read
        line_buffer[x+1] = 0
#let's see what we've got and dump the Bitmap pixels as ASCII-style art
for y in range(0, LCD_height):
    for x in range(0, LCD_width):
        if Tandyframe[x,y] == 1:
            print("\u2588", end="", sep="")
            print(" ", end="", sep="")

Posts: 4
Joined: Wed Jul 31, 2013 11:56 am

Please be positive and constructive with your questions and comments.