Can you access FrameBuffer in Pimoroni PicoGraphics Micropython W

In some examples on the web I see that you can create a FrameBuffer memory array (to match the size of a graphic file), bit blast (load) a graphic file into the array then display the array on the LCD.

Does the new Pimoroni PicoGraphics version of Micropython W have a framebuffer or similar option to allow moving a block of data into it?

Thanks

This might help, some things may have changed Pico Graphics wise since then.
Trying to run dual independent displays on a Pi PICO - Support - Pimoroni Buccaneers

EDIT: I think this may be the way to go, these days.
pimoroni-pico/micropython/modules/picographics at main · pimoroni/pimoroni-pico (github.com)

pimoroni-pico/micropython/modules/picographics at main · pimoroni/pimoroni-pico (github.com)

Thanks alphanumeric for your assistance. I have been using the new Pimoroni UF2 with the graphics infomation and the jpeg routine but unfortunately it is too slow to repeatedly load and redraw the screen for what I need. It does not seem to have an option to directly load in an rgb332 or rgb565 file directly into the screen buffer memory (which would be faster).

In alternative UF2s I have seen that they have a Framebuffer option, into which you can load in a file then display it, seen at
https://docs.micropython.org/en/latest/library/framebuf.html

You first create a block of the correct size, load in a rgb565 file then update the display, which I hope would be quicker that doing repeated jpeg conversions. An example can be seen here:

I have listed the main points:

import framebuf
TH = bytearray()
fb = framebuf.FrameBuffer(TH,64,64, framebuf.MONO_HLSB) # THIS defines the file to load
oled.fill(0)
oled.blit(fb,32,0) # THIS IS GETTING FILE DATA onto display at x, y
oled.show()

What I am looking for is something similar to the above, hence my post to ask if the new Pimoroni UF2 has this option anywhere or can it be simulated, knowing that speed is of the essence. It is probably there and I have missed it.

I have resurrected my old PC program I created to convert BMP files to rgb332/565 files and want to load them into the pico and display as quickly as possible.
Thanks

PicoGraphics also use something what you could call a “framebuffer”, but not in the sense you quote in the example from Tom’s hardware. Currently, there is no official API to load raw data into the data-structures.

But you could have a look on how they integrated the jpeg-decoder into their library. I could imagine that you can skip the decoding part if you have your pre-decoded images already on disk. And why not implement it and create a pull-request?

Can you tell us which display you are using because you may be able to use a different driver which allows you to do what you want.
Why redraw the whole screen? Just update the small section which has changed values. You can do this two ways. Rewrite with background colour or draw a background rectangle over it. Then write the new bit.

You can get more info here:

1 Like

@bablokb, you mention

“PicoGraphics also use something what you could call a ‘framebuffer’, but not in the sense you quote in the example from Tom’s hardware. Currently, there is no official API to load raw data into the data-structures.”

And that is what I am looking for where I can access the screen memory/buffer directly, write into specific areas, read specific locations, overwrite specific sections. For the past 20 years I did this with my PIC projects when mimicing real-time analog gauges/sensors connected to machinery.

Unfortunately I do not know how to implement the API as I have not yet been able to set up my PC (Windows 11 not Linux) to compile for the pico. I keep following the web tutorials but still come unstuck, hence me using Python for the time being.

@Tonygo2 the display would be the Pimoroni Pico Display 2 - 320x240 SPI LCD. I did not originally say I wanted to keep redrawing the whole screen, which is the case with the current jpeg routines. Sorry if my wording was clumsy. What I want to do is access to the memory buffer to allow me to drop in and redraw just the bits I want. For max speed, on my existing PIC units, I overdraw a needle over an analog meter graphic (like a speedo) and store the pixel values underneath the pointer before actually drawing the pointer. When the value changes I replace the saved pixels and redraw the pointer in the new position (first saving the new position pointer), all of which requires direct access to the buffers.

If anyone has a W11 PC setup to directly compile the pico SDKs in C, which I could look at, please let me know!
Thanks for looking

I use MicroPython.

You may find this helpful as it uses the standard frame buffer. This was written for a Waveshare display but will probably work.

# WS 320x240 display example
from machine import Pin,SPI,PWM
import framebuf
import utime
import os

import gc
import micropython

def report():
    gc.collect()
    micropython.mem_info()
    print('-----------------------------')
    print('Initial free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
    def func():
        a = bytearray(100) # 10000
    gc.collect()
    print('Func definition: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
    func()
    print('Func run free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
    gc.collect()
    print('Garbage collect free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
    print('-----------------------------')

print("\nFirst - After loading libraries")
report()
print()
BL = 13
DC = 8
RST = 12
MOSI = 11
SCK = 10
CS = 9

class LCD_1inch3(framebuf.FrameBuffer): # For 320x240 display
    def __init__(self):
        self.width = 320
        self.height = 240
        
        self.cs = Pin(CS,Pin.OUT)
        self.rst = Pin(RST,Pin.OUT)
        
        self.cs(1)
        self.spi = SPI(1)
        self.spi = SPI(1,1000_000)
        self.spi = SPI(1,100000_000,polarity=0, phase=0,sck=Pin(SCK),mosi=Pin(MOSI),miso=None)
        self.dc = Pin(DC,Pin.OUT)
        self.dc(1)
        self.buffer = bytearray(self.height * self.width * 2)
        super().__init__(self.buffer, self.width, self.height, framebuf.RGB565)
        self.init_display()
        
        self.RED   =   0x07E0
        self.GREEN =   0x001f
        self.BLUE  =   0xf800
        self.WHITE =   0xffff
        self.BALCK =   0x0000
        
    def write_cmd(self, cmd):
        self.cs(1)
        self.dc(0)
        self.cs(0)
        self.spi.write(bytearray([cmd]))
        self.cs(1)

    def write_data(self, buf):
        self.cs(1)
        self.dc(1)
        self.cs(0)
        self.spi.write(bytearray([buf]))
        self.cs(1)

    def init_display(self):
        """Initialize display"""  
        self.rst(1)
        self.rst(0)
        self.rst(1)
        
        self.write_cmd(0x36)
        self.write_data(0x70)

        self.write_cmd(0x3A) 
        self.write_data(0x05)

        self.write_cmd(0xB2)
        self.write_data(0x0C)
        self.write_data(0x0C)
        self.write_data(0x00)
        self.write_data(0x33)
        self.write_data(0x33)

        self.write_cmd(0xB7)
        self.write_data(0x35) 

        self.write_cmd(0xBB)
        self.write_data(0x19)

        self.write_cmd(0xC0)
        self.write_data(0x2C)

        self.write_cmd(0xC2)
        self.write_data(0x01)

        self.write_cmd(0xC3)
        self.write_data(0x12)   

        self.write_cmd(0xC4)
        self.write_data(0x20)

        self.write_cmd(0xC6)
        self.write_data(0x0F) 

        self.write_cmd(0xD0)
        self.write_data(0xA4)
        self.write_data(0xA1)

        self.write_cmd(0xE0)
        self.write_data(0xD0)
        self.write_data(0x04)
        self.write_data(0x0D)
        self.write_data(0x11)
        self.write_data(0x13)
        self.write_data(0x2B)
        self.write_data(0x3F)
        self.write_data(0x54)
        self.write_data(0x4C)
        self.write_data(0x18)
        self.write_data(0x0D)
        self.write_data(0x0B)
        self.write_data(0x1F)
        self.write_data(0x23)

        self.write_cmd(0xE1)
        self.write_data(0xD0)
        self.write_data(0x04)
        self.write_data(0x0C)
        self.write_data(0x11)
        self.write_data(0x13)
        self.write_data(0x2C)
        self.write_data(0x3F)
        self.write_data(0x44)
        self.write_data(0x51)
        self.write_data(0x2F)
        self.write_data(0x1F)
        self.write_data(0x1F)
        self.write_data(0x20)
        self.write_data(0x23)
        
        self.write_cmd(0x21)

        self.write_cmd(0x11)

        self.write_cmd(0x29)

    def show(self):
        self.write_cmd(0x2A)
        self.write_data(0x00)
        self.write_data(0x00)
        self.write_data(0x01)
        self.write_data(0x3f)
        
        self.write_cmd(0x2B)
        self.write_data(0x00)
        self.write_data(0x00)
        self.write_data(0x00)
        self.write_data(0xEF)
        
        self.write_cmd(0x2C)
        
        self.cs(1)
        self.dc(1)
        self.cs(0)
        self.spi.write(self.buffer)
        self.cs(1)
        

# Colour Mixing Routine
def colour(R,G,B): # Compact method!
    mix1 = ((R&0xF8)*256) + ((G&0xFC)*8) + ((B&0xF8)>>3)
    return  (mix1 & 0xFF) *256  + int((mix1 & 0xFF00) /256) # low nibble first

# ==== Main ====  
pwm = PWM(Pin(BL))
pwm.freq(1000)
pwm.duty_u16(32768)#max 65535

print("\nSecond - before initialising the display")
report()
LCD = LCD_1inch3()
#color BRG
LCD.fill(LCD.WHITE)
LCD.show()

print("\nThird - After initialising and clearing the display")
report()

utime.sleep(0.1)
LCD.fill_rect(0,0,320,24,LCD.RED)
LCD.rect(0,0,320,24,LCD.RED)
LCD.text("Raspberry Pi Pico",2,8,LCD.WHITE)
utime.sleep(0.1)
LCD.show()
LCD.fill_rect(0,24,320,24,LCD.BLUE)
LCD.rect(0,24,320,24,LCD.BLUE)
LCD.text("PicoGo",2,32,LCD.WHITE)
utime.sleep(0.1)
LCD.show()
LCD.fill_rect(0,48,320,24,LCD.GREEN)
LCD.rect(0,48,320,24,LCD.GREEN)
LCD.text("Pico-LCD-2",2,54,LCD.WHITE)
utime.sleep(0.1)
LCD.show()
LCD.fill_rect(0,72,320,24,0X07FF)
LCD.rect(0,72,320,24,0X07FF)
utime.sleep(0.1)
LCD.show()
LCD.fill_rect(0,96,320,24,0xF81F)
LCD.rect(0,96,320,24,0xF81F)
utime.sleep(0.1)
LCD.show()
LCD.fill_rect(0,120,320,24,0x7FFF)
LCD.rect(0,120,320,24,0x7FFF)
utime.sleep(0.1)
LCD.show()
LCD.fill_rect(0,144,320,24,0xFFE0)
LCD.rect(0,144,320,24,0xFFE0)
utime.sleep(0.1)
LCD.show()
LCD.fill_rect(0,168,320,24,0XBC40)
LCD.rect(0,168,320,24,0XBC40)
utime.sleep(0.1)
LCD.show()
LCD.fill_rect(0,192,320,24,0XFC07)
LCD.rect(0,192,320,24,0XFC07)
utime.sleep(0.1)
LCD.show()
LCD.fill_rect(0,216,320,24,0X8430)
LCD.rect(0,216,320,24,0X8430)
utime.sleep(0.1)
LCD.show()
LCD.fill(0xFFFF)
utime.sleep(0.1)
LCD.show()
utime.sleep(0.1)
LCD.fill(0)


LCD.rect(0,0,319,239,colour(0,0,255)) # Blue Frame
LCD.text("WaveShare", 44,10,colour(255,0,0))
LCD.text('Pico Display 1.8"', 10,24,colour(255,255,0))
LCD.text("320x240 SPI", 38,37,colour(0,255,0))
LCD.text("Tony Goodhew", 30,48,colour(100,100,100))
LCD.show()

LCD.pixel(0,0,0xFFFF)     # Left Top - OK
LCD.pixel(0,239,0xFFFF)   # Left Bottom - OK
LCD.pixel(319,0,0xFFFF)   # Right Top - OK
LCD.pixel(319,239,0xFFFF) # Right Bottom - OK
LCD.show()
utime.sleep(20)

Best of luck
Tony

I would love to see your PIC-code. Because I did once port the Adafruit-ST7735 lib to the PIC - but this does not implement reading from the display just writing. I’m only curious to see it.

From what you write I assume that you read and write directly to the internal buffer of the driver, using the SPI-commands provided by the chip. PicoGraphics is an abstraction layer, and as far as I know they always refresh the complete display (i.e. write the internal buffer to the driver-chip). Looking at their source could verifiy this assumption.

Regarding the pico SDK: have you followed the tutorials? It is plain C/C++ with cmake, so the platform should not matter. The getting started guide has a section on the setup on Windows, and also has a link to here: GitHub - ndabas/pico-setup-windows: Quickly get started with Raspberry Pi Pico/RP2040 on Windows. The project seems active and claims to be a simple installer for the toolchain and SDK.

@Tonygo2 and @bablokb many thanks. I was due out 30mins ago! and so will look late tonight or tomorrow. I will get back to you both.
Cheers

@bablokb my PIC routines were for the small Densitron RGB COLOUR OLED 160x128 DISPLAY, DD-160128FC-2B, Driver IC = SEPS525
I prefer the OLED displays as you can see them better in daylight.
If this chip is what you use, if you PM (can this be done on this forum as I am fairly new), I will email the driver part I did.

A month ago I followed the getting started guide setting up on Windows PC you mention, but I kept coming unstuck - my fault as I can easily get confused. I use Visual Studio 2019 (not Visual Studio Code, sounds similar but are two different IDEs) and there seemed to be a conflict and so I gave up. Also, some of the steps in the guide seemed to have been updated and changed from when the guide was originally written and so I decided it was best to leave it for awhile and revisit it when it may have settled and I have more time.
Thanks

@Tonygo2 thanks for sending me the Waveshare python code, which looks very interesting. I have tried it and it naturally does not function, which is to be expected, until I edit the numerous errors to try and adapt if for the Pimoroni Display 2, 320 x 240.

Example errors are
self.buffer = bytearray(self.height * self.width * 2)

self.buffer - which I think is where I the versions of micropython differ between Pimoroni and the one used by waveshare. That was why I was hoping that the Pimoroni version had it hidden inside somewhere.

It would be excellent if I could get it working as it is uses the relevant framebuffer as you said.

Thanks

ps if someone who is far more clever than I can ever be has converted it, please share!

Both displays are ST7789s.

The main problem with these 320x240 displays is the size of the buffer. If you use 2-bytes per pixel for the colours there is very little room left in a Pico for code.

What errors did you get?

Did you remove all the Pimoroni ‘extras’ and use the basic MicroPython Pico UF2 for your test?

What I ended up doing was
Writing to the buffer only what was to be displayed on display one.
Update display one (buffer to display one).
Clear Buffer
Writing to the buffer only what was to be displayed on display two.
Update display two (buffer to display two).
Clear Buffer
Wash, rinse and repeat.

The code from @Tonygo2 won’t help you: look at the show()-method: this method writes out the complete “framebuffer” to the display, updating all pixels. To be clear: this “framebuffer” is only a memory area in the RAM of the MCU, it has nothing to do with the buffer in the display.

For what you want to do, you need to keep track of dirty pixels, and then instead of dumping the whole buffer you would only dump the necessary dirty area. Have a look at the ST7789 datasheet. Depending on the way the display is initialized (all those bytes sent at the beginning), you have to send the correct row-select and col-select bytes and then the correct bytes in row or column order.

Sounds like a long and complicated endeavor to me.

Yes, @bablokb, that was the way I did it in my PIC units which I mentioned in my posts. It looks like using micropython to get at that low level of control is a non starter and so I will in the future look at writing the specific code in C.
Thanks everyone for looking.

BTW: in CircuitPython, the display-architecture automatically keeps track of dirty regions and tries to send only areas that have changed. So this might be an easier route to go, maybe at least for prototyping. C will always be faster, but most of the low-level stuff in CP is already in C.

Excellent info @bablokb. To allow me to look-up and read about such things and to get a full list of CircuitPython’s commands, could you please let me know from where you got this info.
Thanks

I studied the source code. You can find everything about CP here: circuitpython.org

Thanks, I will download the info.

Is this the sort of thing you are talking about?

I’ve just done this in pure MicroPython on a 240x 240 circular display using the basic frame buffer. Main code is shown below.
I’ve not included the support procedures for circles and different character sizes but am happy to share if anyone would like it.

    
# ==== Board now setup ========== MAIN BELOW====================
clear(0)

# Draw the dial - once only
LCD.fill(colour(255,0,0))                    # Fill screen red
circle(120,120,100,colour(0,0,255))          # Blue circle
LCD.fill_rect(0,121,240,120,colour(255,0,0)) # Overwrite lower half blue circle with red
r = 103                                      # Tick outer radius
for p in range(0,101,10):                    # White Ticks at 10 % intervals
    theta = p * 1.8                          # Angle above horizontal in degrees
    theta_rad = math.radians(theta)          # Angle in radians
    yn = -int(r * math.sin(theta_rad))       # Calculate outer tick coordinates
    xn = -int(r * math.cos(theta_rad))
    LCD.line(120,120,120+xn,120+yn,colour(255,255,255)) # Draw the tick from centre
circle(120,120,75,colour(255,0,0))           # Overwrite inner tick lines
LCD.show()

# Counting up in fives
r = 74 # Length of hand
old_xn = 0
old_yn = 0
for p in range(0,101,5): # Percentages at 5 % interval
    theta = p * 1.8
    LCD.line(120,120,120+old_xn,120+old_yn,colour(255,0,0)) # Overwrite the old hand
    LCD.fill_rect(0,121,240,120,colour(255,0,0))            # Clear text area
    theta_rad = math.radians(theta)               
    cntr_st(str(p) +" %",130,3,255,255,255)                 # Percentage value as text
    yn = -int(r * math.sin(theta_rad))
    xn = -int(r * math.cos(theta_rad))
    LCD.line(120,120,120+xn,120+yn,colour(0,0,255))         # Draw the new hand
    LCD.show()                                              # Update screen
    time.sleep(0.1)                                         # Delay
    old_xn = xn                                             # Store current hand end corordinates
    old_yn = yn                                             #  for overwriting in next loop pass
    
# Random values
for c in range(25):
    p = random.randint(0,100)
    theta = p * 1.8
    LCD.line(120,120,120+old_xn,120+old_yn,colour(255,0,0)) # Overwrite the old hand
    LCD.fill_rect(0,121,240,120,colour(255,0,0))            # Clear text area
    theta_rad = math.radians(theta)   
    cntr_st(str(p) +" %",130,3,255,255,255)                 # Centres text on line y
    yn = -int(r * math.sin(theta_rad))
    xn = -int(r * math.cos(theta_rad))
    LCD.line(120,120,120+xn,120+yn,colour(0,255,0))         # Draw the new hand
    LCD.show()                                              # Update screen
    time.sleep(0.3)                                         # Delay
    old_xn = xn                                             # Store current hand end corordinates
    old_yn = yn                                             #  for overwriting in next loop pass    
    

It works plenty fast enough and no flicker.

1 Like