adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
MIT License
3.96k stars 1.16k forks source link

displayio: Impossible to have more than one display on the same bus #1760

Open deshipu opened 5 years ago

deshipu commented 5 years ago

In theory, I could have several SPI displays connected to the same bus, and displayio has provisions for that — locking of the SPI bus, toggling of the CS pin, even re-setting of the SPI frequency, polarity and phase. However, it's not possible to create two (or more) FourWire objects sharing the same DC and/or reset pins, but with different CS pins — because we will get a "Pin in use" error when the FourWire object tries to create them.

bablokb commented 3 years ago

Works fine if all screens display the same content. I tried to create multiple displayio.Display, but failed because I could not create the necessary bus (displayio.FourWire).

ladyada commented 3 years ago

ok btw you can use them as 'raw' RGB tft displays, you dont get displayio but you can always draw to em. https://github.com/adafruit/Adafruit_CircuitPython_RGB_Display

tannewt commented 3 years ago

I want to attach 12 (!) displays to a pico, using two SPI-busses. Although the pico is a pin-monster, this does not work out if I have a dedicated dc-pin for every display. (I'm building an advent Calendar with two picos and 24 displays in total).

We don't support 12 displays out of the box anyway. So, you'll need to compile it yourself regardless. Since you are doing that, you can comment out the pin check too: https://github.com/adafruit/circuitpython/blob/main/shared-bindings/displayio/FourWire.c#L77 (Just do MP_OBJ_TO_PTR instead.)

"Catching misuse" is fine as long as the check is for real misuse and not for non-standard but valid usage. Since this is no priority on your side, I will see if I fork the library or switch to C.

You can always modify CircuitPython and propose we merge those changes back. It's open source. :-)

Wouldn't it be better to accept a DigitalInOut object instead of a pin object, so you can use the same instance in both places to avoid the error?

Ya, we could do that. We'd need to make sure it's a native object though. We wouldn't be able to call into DigitalInOut-like objects since display code interlaces with the VM.

Can displayio drive multiple physical screens as one displayio.Display?

Not as different portions of the same display.

bablokb commented 3 years ago

ok btw you can use them as 'raw' RGB tft displays, you dont get displayio but you can always draw to em. https://github.com/adafruit/Adafruit_CircuitPython_RGB_Display

This looks promising, I will give it a try, thanks.

knox1138 commented 3 years ago

Ok, I'm new to all things programming and microcontrollers, but after beating my head trying to get multiple spi displays to work with a pico on circuitpython I want to make sure I understand this correctly. There is an artificial limit in circuitpython, that isn't mentioned anywhere, that limits the number of displays you can use to 1. This limit is not mentioned anywhere unless you stumble across this page, and the only way to change it is to compile a custom version of Circuitpython which cannot be done using windows or raspberry pi os unless you add a virtual machine and learn or already know Linux, is this correct? I dug myself into this hole so I'm trying to figure out my way out of it. So in theory I would edit the circuitpy_mpconfig to add more displays, and comment out the displayio pin check. There is bo mpconfigboard.h for the pico that I can find. Lastly, when compiling the circuitpython, do I need to make it a .uf2 file? Does it do that automatically? Oh, and I dunno if this helps but I'm using ssd1331 oled displays, and use case (i think that's the term) is putting a displays on top of each switch in a 3x3 matrix to make a diy stream deck, or a diy version of the NKK Smarth Switches.

tannewt commented 3 years ago

Ok, I'm new to all things programming and microcontrollers, but after beating my head trying to get multiple spi displays to work with a pico on circuitpython I want to make sure I understand this correctly. There is an artificial limit in circuitpython, that isn't mentioned anywhere, that limits the number of displays you can use to 1. This limit is not mentioned anywhere unless you stumble across this page, and the only way to change it is to compile a custom version of Circuitpython which cannot be done using windows or raspberry pi os unless you add a virtual machine and learn or already know Linux, is this correct? I dug myself into this hole so I'm trying to figure out my way out of it.

Displays are statically allocated because they live longer than the VM. They also take space even when not used. So, I wouldn't call it artificial, it is a trade-off.

Yes, increasing this requires a rebuild. A VM is only suggested on Windows. On Linux you don't need a VM.

So in theory I would edit the circuitpy_mpconfig to add more displays, and comment out the displayio pin check. There is bo mpconfigboard.h for the pico that I can find. Lastly, when compiling the circuitpython, do I need to make it a .uf2 file? Does it do that automatically?

I don't think you'll need to comment out the pin check if they share the same bus. The build will make the uf2 automatically.

Oh, and I dunno if this helps but I'm using ssd1331 oled displays, and use case (i think that's the term) is putting a displays on top of each switch in a 3x3 matrix to make a diy stream deck, or a diy version of the NKK Smarth Switches.

Note that the streamdeck is a single display, not a bunch of small ones: https://www.youtube.com/watch?v=9apO--Qpz58

We have a learn guide for a DIY version here: https://learn.adafruit.com/touch-deck-diy-tft-customized-control-pad

knox1138 commented 3 years ago

Ok, I don't understand how there's a VM using circuitpython on a pico but there is, and displays outlive it, and because they take space when not in use there is a tradeoff. I try to figure out what that actually means somewhere else, but it means there is a reason.

I thought I would have to comment out the pin check to have 1 command and 1 reset pin for all displays, but maybe I mis-understand what was said or how it works.

Lastly, yep, the stream deck is a single display. I still want to use multiple displays in a grid pattern, and in the use case if a streamdeck i'll keep in mind there are easier ways to accomplish that goal.

Thank you very much.

ladyada commented 3 years ago

if you happen to be using a display with drivers supported by https://github.com/adafruit/Adafruit_CircuitPython_RGB_Display you can use it WITHOUT displayio as just a 'raw RGB' display. it will not be nearly as capable or as fast, but you can draw basic shapes https://github.com/adafruit/Adafruit_CircuitPython_GFX

knox1138 commented 3 years ago

I tried that last night with my ssd1331 since it is an option and had no luck. I'll try again tonight. Thank you.

kevinjwalters commented 3 years ago

@tannewt Couldn't you just have the concept of a primary display that has that additional functionality of persisting and extra ones being instatiated as non-primary ones with a different life cycle?

tannewt commented 3 years ago

@tannewt Couldn't you just have the concept of a primary display that has that additional functionality of persisting and extra ones being instatiated as non-primary ones with a different life cycle?

You could but it's not implemented that way. Having things behave differently is tricky.

kevinjwalters commented 2 years ago

I only discovered the twin display Adafruit Monster Mask today. It was mentioned previously in the comments but it's also in the very recent A CircuitPython Board Tour with Ladyada #CircuitPythonDay2021 (YouTube).

dgoadby commented 2 years ago

I want to drive two 1.28 inch displays on a RPi Pico. The controllers are GCA901 so I need to run two SPI devices with, presumably, device selection using Chip Select. How do I configure disaplyio to work with two displays on a common bus? I see a reference to the configuration definition CIRCUITPY_DISPLAY_LIMIT - what is the current default value?

FoamyGuy commented 2 years ago

I want to drive two 1.28 inch displays on a RPi Pico. The controllers are GCA901 so I need to run two SPI devices with, presumably, device selection using Chip Select. How do I configure disaplyio to work with two displays on a common bus? I see a reference to the configuration definition CIRCUITPY_DISPLAY_LIMIT - what is the current default value?

I believe the default setting on almost all devices is 1. I think the only device with limit of 2 is the Monster M4sk https://github.com/adafruit/circuitpython/blob/main/ports/atmel-samd/boards/monster_m4sk/mpconfigboard.h

dastels commented 2 years ago

I believe the default setting on almost all devices is 1. I think the only device with limit of 2 is the Monster M4sk https://github.com/adafruit/circuitpython/blob/main/ports/atmel-samd/boards/monster_m4sk/mpconfigboard.h

That was my finding as well.

knox1138 commented 2 years ago

The current limit is 1 display. I have been told you can re-compile it and change the value to 2, but because circuitpython is basically running a virtual machine it doesn't work so great ( atleast that's how I understand it, I might not be explaining it correctly). You can use multiple displays using rgb565. I dunno if the particular display driver you mentioned supports it.

On Mon, May 9, 2022, 8:53 PM David Goadby @.***> wrote:

I want to drive two 1.28 inch displays on a RPi Pico. The controllers are GCA901 so I need to run two SPI devices with, presumably, device selection using Chip Select. How do I configure disaplyio to work with two displays on a common bus? I see a reference to the configuration definition CIRCUITPY_DISPLAY_LIMIT - what is the current default value?

— Reply to this email directly, view it on GitHub https://github.com/adafruit/circuitpython/issues/1760#issuecomment-1121740203, or unsubscribe https://github.com/notifications/unsubscribe-auth/AUDDR4FBRBKZRRW3FYUDVI3VJGXRTANCNFSM4HEA2WFA . You are receiving this because you commented.Message ID: @.***>

dgoadby commented 2 years ago

Thanks for the information. I have chosen to use an extra XIAO RP2040 board to drive the second display and use I2C to take the data from the main processor. The RP2040 is a lot cheaper than many hours of my time trying to get dual display to work. Again, thanks for your help.

rgrizzell commented 2 months ago

I've been experimenting with multi-display support on the LILYGO T-Keyboard-S3, and was able to initialize and render a displayio.Group on each screen. I can also confirm that it is very unstable! CircuitPython will crash on reloads, so any file changes will require a manual reset of the device. 🙃

However, with as niche as this feature would be, I wanted to contribute in the hopes that we can push this forward.

LILYGO T-Keyboard-S3 with 4 individually rendered displays

For the T-Keyboard-S3, the DC, RST pins are shared, and each display is controlled through individual CS pins. To get this working the FourWire library needs to be "patched" to share pins. This was relatively simple to do, just don't bother to check if the pin is in-use!

--- a/shared-bindings/fourwire/FourWire.c
+++ b/shared-bindings/fourwire/FourWire.c
@@ -88,9 +88,9 @@ STATIC mp_obj_t fourwire_fourwire_make_new(const mp_obj_type_t *type, size_t n_a
     mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
     mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

-    const mcu_pin_obj_t *command = validate_obj_is_free_pin_or_none(args[ARG_command].u_obj, MP_QSTR_command);
+    const mcu_pin_obj_t *command = validate_obj_is_pin_or_none(args[ARG_command].u_obj, MP_QSTR_command);
     const mcu_pin_obj_t *chip_select = validate_obj_is_free_pin_or_none(args[ARG_chip_select].u_obj, MP_QSTR_chip_select);
-    const mcu_pin_obj_t *reset = validate_obj_is_free_pin_or_none(args[ARG_reset].u_obj, MP_QSTR_reset);
+    const mcu_pin_obj_t *reset = validate_obj_is_pin_or_none(args[ARG_reset].u_obj, MP_QSTR_reset);

     mp_obj_t spi = mp_arg_validate_type(args[ARG_spi_bus].u_obj, &busio_spi_type, MP_QSTR_spi_bus);

#define CIRCUITPY_DISPLAY_LIMIT (4) was also added to the board configuration.

As for testing, I used the latest version of CircuitPython at the time: 9.1.0-beta1. For the displays, I used adafruit_st7789 from the Community Bundle. The T-Keyboard-S3 has the GC9107 chip for its 128x128 screens, but the ST7789 initialization sequence works just fine.

I also tried to configure CircuitPython to start all 4 displays at boot, but that didn't pan out. It sort of worked, but one the next display was initialized the previous one was de-initialized. However, the following script does work as I exepcted so, I'm willing to bet I'm missing a crucial detail or two.

import board
import busio
import displayio
import time
import adafruit_st7789
from adafruit_display_shapes.rect import Rect
from adafruit_display_shapes.circle import Circle

_WIDTH = 128
_HEIGHT = 128

tft_sck = board.IO47
tft_mosi = board.IO48
tft_blk = board.IO39
tft_dc = board.IO45
tft_rst = board.IO38
tft_cs1 = board.IO12
tft_cs2 = board.IO13
tft_cs3 = board.IO14
tft_cs4 = board.IO21

displayio.release_displays()
tft_spi = busio.SPI(tft_sck, tft_mosi)
tft_bus_1 = displayio.FourWire(spi_bus=tft_spi, command=tft_dc, reset=tft_rst, chip_select=tft_cs1)
tft_bus_2 = displayio.FourWire(spi_bus=tft_spi, command=tft_dc, reset=tft_rst, chip_select=tft_cs2)
tft_bus_3 = displayio.FourWire(spi_bus=tft_spi, command=tft_dc, reset=tft_rst, chip_select=tft_cs3)
tft_bus_4 = displayio.FourWire(spi_bus=tft_spi, command=tft_dc, reset=tft_rst, chip_select=tft_cs4)
display_1 = adafruit_st7789.ST7789(bus=tft_bus_1, backlight_pin=tft_blk, rotation=0, width=_WIDTH, height=_HEIGHT)
display_2 = adafruit_st7789.ST7789(bus=tft_bus_2, backlight_pin=None, rotation=0, width=_WIDTH, height=_HEIGHT)
display_3 = adafruit_st7789.ST7789(bus=tft_bus_3, backlight_pin=None, rotation=0, width=_WIDTH, height=_HEIGHT)
display_4 = adafruit_st7789.ST7789(bus=tft_bus_4, backlight_pin=None, rotation=0, width=_WIDTH, height=_HEIGHT)

red_group = displayio.Group()
red_group.append(Rect(x=0, y=0, width=128, height=128, fill=0xFFFFFF))
red_group.append(Circle(x0=64, y0=64, r=32, fill=0xFF0000))
display_1.root_group = red_group

blue_group = displayio.Group()
blue_group.append(Rect(x=0, y=0, width=128, height=128, fill=0xFFFFFF))
blue_group.append(Circle(x0=64, y0=64, r=32, fill=0x0000FF))
display_2.root_group = blue_group

green_group = displayio.Group()
green_group.append(Rect(x=0, y=0, width=128, height=128, fill=0xFFFFFF))
green_group.append(Circle(x0=64, y0=64, r=32, fill=0x00FF00))
display_3.root_group = green_group

black_group = displayio.Group()
black_group.append(Rect(x=0, y=0, width=128, height=128, fill=0xFFFFFF))
black_group.append(Circle(x0=64, y0=64, r=32, fill=0x000000))
display_4.root_group = black_group

while True:
    time.sleep(1)