From 8fb5ba43398511a5dfa4736f79cf4a15f80a2cde Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 22 Jan 2025 14:49:00 -0600 Subject: [PATCH 1/5] Export RGB565 framebuffer type --- src/pymain.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/pymain.cpp b/src/pymain.cpp index 998bdfa..5e7ea6e 100644 --- a/src/pymain.cpp +++ b/src/pymain.cpp @@ -153,6 +153,14 @@ update the data actually displayed on the panel. Internally, the data is triple-buffered to prevent tearing. )pbdoc"); + m.def("AdafruitMatrixBonnetRGB565", + make_piomatter, + py::arg("buffer"), py::arg("geometry")) + //.doc() = "Drive panels connected to an Adafruit Matrix Bonnet using + // the RGB565 memory layout (4 bytes per pixel)" + ; + m.def("AdafruitMatrixBonnetRGB888", make_piomatter, From a532a9105ec9862d703f08f430d53ff55b46a899 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 24 Jan 2025 10:23:55 -0600 Subject: [PATCH 2/5] Reduce PIO clock speed, remove CLOCKS_PER_DATA workaround we now understand why the fudge factor was needed to achieve a plausible brightness ramp. --- src/include/piomatter/piomatter.h | 9 ++++++++- src/include/piomatter/render.h | 4 +--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/include/piomatter/piomatter.h b/src/include/piomatter/piomatter.h index 4ae67a0..062c5d4 100644 --- a/src/include/piomatter/piomatter.h +++ b/src/include/piomatter/piomatter.h @@ -114,7 +114,14 @@ struct piomatter : piomatter_base { sm_config_set_out_shift(&c, /* shift_right= */ false, /* auto_pull = */ true, 32); sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); - sm_config_set_clkdiv(&c, 1.0); + // Due to https://github.com/raspberrypi/utils/issues/116 it's not + // possible to keep the RP1 state machine fed at high rates. This target + // frequency is approximately the best sustainable clock with current + // FW & kernel. + constexpr double target_freq = + 2700000; // 2.7MHz PIO clock -> 1.35MHz pixel clock + double div = clock_get_hz(clk_sys) / target_freq; + sm_config_set_clkdiv(&c, div); sm_config_set_out_pins(&c, 0, 28); pio_sm_init(pio, sm, offset, &c); pio_sm_set_enabled(pio, sm, true); diff --git a/src/include/piomatter/render.h b/src/include/piomatter/render.h index fda6264..4e8abe8 100644 --- a/src/include/piomatter/render.h +++ b/src/include/piomatter/render.h @@ -8,9 +8,7 @@ namespace piomatter { constexpr unsigned DATA_OVERHEAD = 3; -// this is ... flatly wrong!? but it's the number that makes the ramp intensity -// correct to my eye -constexpr unsigned CLOCKS_PER_DATA = 128; +constexpr unsigned CLOCKS_PER_DATA = 2; constexpr unsigned DELAY_OVERHEAD = 5; constexpr unsigned CLOCKS_PER_DELAY = 1; From 3d6b58f74dc1f5c7fdf45445e6d713c2ab10b889 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 24 Jan 2025 11:17:44 -0600 Subject: [PATCH 3/5] Double the pixel clock rate .. by moving the "assert CLK after each data" into the PIO program, cutting the amount of PIO data in half. --- src/assemble.py | 25 ++++++-------- src/include/piomatter/piomatter.h | 5 ++- src/include/piomatter/protomatter.pio.h | 13 ++++--- src/include/piomatter/render.h | 45 +++++++++---------------- src/protomatter.pio | 7 ++-- 5 files changed, 44 insertions(+), 51 deletions(-) diff --git a/src/assemble.py b/src/assemble.py index 645d656..400898d 100644 --- a/src/assemble.py +++ b/src/assemble.py @@ -1,29 +1,26 @@ -import sys -from contextlib import contextmanager +import io +import pathlib +from contextlib import redirect_stdout import adafruit_pioasm import click -@contextmanager -def temporary_stdout(filename): - old_stdout = sys.stdout - try: - with open(filename, "w", encoding="utf-8") as sys.stdout: - yield sys.stdout - finally: - sys.stdout = old_stdout - @click.command @click.argument("infile") @click.argument("outfile") def main(infile, outfile): - program_name = infile.rpartition("/")[2].partition(".")[0] - print(program_name) + program_name = pathlib.Path(infile).stem program = adafruit_pioasm.Program.from_file(infile, build_debuginfo=True) - with temporary_stdout(outfile): + c_program = io.StringIO() + with redirect_stdout(c_program): program.print_c_program(program_name) + with open(outfile, "w", encoding="utf-8") as out: + print("#pragma once", file=out) + print("", file=out) + print(c_program.getvalue().rstrip().replace("True", "true"), file=out) + if __name__ == '__main__': main() diff --git a/src/include/piomatter/piomatter.h b/src/include/piomatter/piomatter.h index 062c5d4..0f63e4d 100644 --- a/src/include/piomatter/piomatter.h +++ b/src/include/piomatter/piomatter.h @@ -111,6 +111,8 @@ struct piomatter : piomatter_base { pio_sm_config c = pio_get_default_sm_config(); sm_config_set_wrap(&c, offset + protomatter_wrap_target, offset + protomatter_wrap); + // 1 side-set pin + sm_config_set_sideset(&c, 2, true, false); sm_config_set_out_shift(&c, /* shift_right= */ false, /* auto_pull = */ true, 32); sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); @@ -119,10 +121,11 @@ struct piomatter : piomatter_base { // frequency is approximately the best sustainable clock with current // FW & kernel. constexpr double target_freq = - 2700000; // 2.7MHz PIO clock -> 1.35MHz pixel clock + 2700000 * 2; // 2.7MHz pixel clock, 2 PIO cycles per pixel double div = clock_get_hz(clk_sys) / target_freq; sm_config_set_clkdiv(&c, div); sm_config_set_out_pins(&c, 0, 28); + sm_config_set_sideset_pins(&c, pinout::PIN_CLK); pio_sm_init(pio, sm, offset, &c); pio_sm_set_enabled(pio, sm, true); diff --git a/src/include/piomatter/protomatter.pio.h b/src/include/piomatter/protomatter.pio.h index 0a2d84b..73485e2 100644 --- a/src/include/piomatter/protomatter.pio.h +++ b/src/include/piomatter/protomatter.pio.h @@ -2,24 +2,27 @@ const int protomatter_wrap = 4; const int protomatter_wrap_target = 0; -const int protomatter_sideset_pin_count = 0; -const bool protomatter_sideset_enable = 0; +const int protomatter_sideset_pin_count = 1; +const bool protomatter_sideset_enable = true; const uint16_t protomatter[] = { // ; data format (out-shift-right): // ; MSB ... LSB // ; 0 ddd......ddd: 31-bit delay // ; 1 ccc......ccc: 31 bit data count + // .side_set 1 opt // .wrap_target // top: 0x6021, // out x, 1 0x605f, // out y, 31 - 0x0025, // jmp !x delay_loop + 0x0025, // jmp !x do_delay // data_loop: 0x6000, // out pins, 32 - 0x0083, // jmp y--, data_loop + 0x1883, // jmp y--, data_loop side 1 ; assert clk bit // .wrap + // do_delay: + 0x6000, // out pins, 32 // delay_loop: - 0x0085, // jmp y--, delay_loop + 0x0086, // jmp y--, delay_loop 0x0000, // jmp top // ;; fill program out to 32 instructions so nothing else can load 0xa042, // nop diff --git a/src/include/piomatter/render.h b/src/include/piomatter/render.h index 4e8abe8..12c49c4 100644 --- a/src/include/piomatter/render.h +++ b/src/include/piomatter/render.h @@ -7,10 +7,10 @@ namespace piomatter { -constexpr unsigned DATA_OVERHEAD = 3; -constexpr unsigned CLOCKS_PER_DATA = 2; -constexpr unsigned DELAY_OVERHEAD = 5; -constexpr unsigned CLOCKS_PER_DELAY = 1; +constexpr int DATA_OVERHEAD = 3; +constexpr int CLOCKS_PER_DATA = 2; +constexpr int DELAY_OVERHEAD = 5; +constexpr int CLOCKS_PER_DELAY = 1; constexpr uint32_t command_data = 1u << 31; constexpr uint32_t command_delay = 0; @@ -137,12 +137,12 @@ void protomatter_render_rgb10(std::vector &result, int data_count = 0; - auto do_delay = [&](uint32_t delay) { - if (delay == 0) - return; + auto do_data_delay = [&](uint32_t data, int32_t delay) { + delay = std::max((delay / CLOCKS_PER_DELAY) - DELAY_OVERHEAD, 1); assert(delay < 1000000); assert(!data_count); result.push_back(command_delay | (delay ? delay - 1 : 0)); + result.push_back(data); }; auto prep_data = [&data_count, &result](uint32_t n) { @@ -153,27 +153,15 @@ void protomatter_render_rgb10(std::vector &result, data_count = n; }; - auto do_data = [&](uint32_t d) { - assert(data_count); - data_count--; - result.push_back(d); - }; - int32_t active_time; - auto do_data_active = [&active_time, &do_data](uint32_t d) { + auto do_data_clk_active = [&active_time, &data_count, &result](uint32_t d) { bool active = active_time > 0; active_time--; d |= active ? pinout::oe_active : pinout::oe_inactive; - do_data(d); - }; - - auto do_data_delay = [&prep_data, &do_data, &do_delay](uint32_t d, - int32_t delay) { - prep_data(1); - do_data(d); - if (delay > 0) - do_delay(delay); + assert(data_count); + data_count--; + result.push_back(d); }; auto calc_addr_bits = [](int addr) { @@ -195,9 +183,9 @@ void protomatter_render_rgb10(std::vector &result, return data; }; - auto add_pixels = [&do_data_active, &result](uint32_t addr_bits, bool r0, - bool g0, bool b0, bool r1, - bool g1, bool b1) { + auto add_pixels = [&do_data_clk_active, + &result](uint32_t addr_bits, bool r0, bool g0, bool b0, + bool r1, bool g1, bool b1) { uint32_t data = addr_bits; if (r0) data |= (1 << pinout::PIN_RGB[0]); @@ -212,8 +200,7 @@ void protomatter_render_rgb10(std::vector &result, if (b1) data |= (1 << pinout::PIN_RGB[5]); - do_data_active(data); - do_data_active(data | pinout::clk_bit); + do_data_clk_active(data); }; int last_bit = 0; @@ -243,7 +230,7 @@ void protomatter_render_rgb10(std::vector &result, active_time = 1 << last_bit; last_bit = bit; - prep_data(2 * pixels_across); + prep_data(pixels_across); auto mapiter = matrixmap.map.begin() + 2 * addr * pixels_across; for (size_t x = 0; x < pixels_across; x++) { assert(mapiter != matrixmap.map.end()); diff --git a/src/protomatter.pio b/src/protomatter.pio index c4f082a..1aef1f2 100644 --- a/src/protomatter.pio +++ b/src/protomatter.pio @@ -3,17 +3,20 @@ ; 0 ddd......ddd: 31-bit delay ; 1 ccc......ccc: 31 bit data count +.side_set 1 opt .wrap_target top: out x, 1 out y, 31 - jmp !x delay_loop + jmp !x do_delay data_loop: out pins, 32 - jmp y--, data_loop + jmp y--, data_loop side 1 ; assert clk bit .wrap +do_delay: + out pins, 32 delay_loop: jmp y--, delay_loop jmp top From b149f31bf31c9098ec950c72fe81e5c510ecda1e Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 24 Jan 2025 11:37:59 -0600 Subject: [PATCH 4/5] Add `fps` property to PioMatter objects --- examples/playframes.py | 2 +- src/include/piomatter/piomatter.h | 15 +++++++++++++++ src/protodemo.c | 1 - src/pymain.cpp | 4 ++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/examples/playframes.py b/examples/playframes.py index 7609b2b..f3af2f3 100644 --- a/examples/playframes.py +++ b/examples/playframes.py @@ -32,4 +32,4 @@ t1 = time.monotonic() dt = t1 - t0 fps = nimages/dt - print(f"{nimages} frames in {dt}s, {fps}fps") + print(f"{nimages} frames in {dt}s, {fps}fps [{matrix.fps}]") diff --git a/src/include/piomatter/piomatter.h b/src/include/piomatter/piomatter.h index 0f63e4d..4f91fc3 100644 --- a/src/include/piomatter/piomatter.h +++ b/src/include/piomatter/piomatter.h @@ -12,6 +12,12 @@ namespace piomatter { +static uint64_t monotonicns64() { + struct timespec tp; + clock_gettime(CLOCK_MONOTONIC, &tp); + return tp.tv_sec * UINT64_C(1000000000) + tp.tv_nsec; +} + constexpr size_t MAX_XFER = 65532; void pio_sm_xfer_data_large(PIO pio, int sm, int direction, size_t size, @@ -35,6 +41,8 @@ struct piomatter_base { virtual ~piomatter_base() {} virtual void show() = 0; + + double fps; }; template matter; void show() { matter->show(); } + double fps() const { return matter->fps; } }; template @@ -151,6 +152,9 @@ Update the displayed image After modifying the content of the framebuffer, call this method to update the data actually displayed on the panel. Internally, the data is triple-buffered to prevent tearing. +)pbdoc") + .def_property_readonly("fps", &PyPiomatter::fps, R"pbdoc( +The approximate number of matrix refreshes per second. )pbdoc"); m.def("AdafruitMatrixBonnetRGB565", From feb64621adf561cad9f951b9cadeacc4e9250ce2 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 24 Jan 2025 12:03:13 -0600 Subject: [PATCH 5/5] new framebuffer mirroring examples --- examples/fbmirror.py | 43 +++++++++++++++++++++++++++ examples/fbmirror_scaled.py | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 examples/fbmirror.py create mode 100644 examples/fbmirror_scaled.py diff --git a/examples/fbmirror.py b/examples/fbmirror.py new file mode 100644 index 0000000..20a4fea --- /dev/null +++ b/examples/fbmirror.py @@ -0,0 +1,43 @@ +#!/usr/bin/python3 +""" +Mirror a scaled copy of the framebuffer to a 64x32 matrix + +The upper left corner of the framebuffer is displayed until the user hits ctrl-c. + +The `/dev/fb0` special file will exist if a monitor is plugged in at boot time, +or if `/boot/firmware/cmdline.txt` specifies a resolution such as +`... video=HDMI-A-1:640x480M@60D`. +""" + + +import adafruit_raspberry_pi5_piomatter +import numpy as np + +yoffset = 0 +xoffset = 0 + +with open("/sys/class/graphics/fb0/virtual_size") as f: + screenx, screeny = [int(word) for word in f.read().split(",")] + +with open("/sys/class/graphics/fb0/bits_per_pixel") as f: + bits_per_pixel = int(f.read()) + +assert bits_per_pixel == 16 + +bytes_per_pixel = bits_per_pixel // 8 +dtype = {2: np.uint16, 4: np.uint32}[bytes_per_pixel] + +with open("/sys/class/graphics/fb0/stride") as f: + stride = int(f.read()) + +linux_framebuffer = np.memmap('/dev/fb0',mode='r', shape=(screeny, stride // bytes_per_pixel), dtype=dtype) + +width = 64 +height = 32 +geometry = adafruit_raspberry_pi5_piomatter.Geometry(width=width, height=height, n_addr_lines=4, rotation=adafruit_raspberry_pi5_piomatter.Orientation.Normal) +matrix_framebuffer = np.zeros(shape=(geometry.height, geometry.width), dtype=dtype) +matrix = adafruit_raspberry_pi5_piomatter.AdafruitMatrixBonnetRGB565(matrix_framebuffer, geometry) + +while True: + matrix_framebuffer[:,:] = linux_framebuffer[yoffset:yoffset+height, xoffset:xoffset+width] + matrix.show() diff --git a/examples/fbmirror_scaled.py b/examples/fbmirror_scaled.py new file mode 100644 index 0000000..aee0832 --- /dev/null +++ b/examples/fbmirror_scaled.py @@ -0,0 +1,58 @@ +#!/usr/bin/python3 +""" +Mirror a scaled copy of the framebuffer to a 64x32 matrix + +The upper left corner of the framebuffer is displayed until the user hits ctrl-c. + +The `/dev/fb0` special file will exist if a monitor is plugged in at boot time, +or if `/boot/firmware/cmdline.txt` specifies a resolution such as +`... video=HDMI-A-1:640x480M@60D`. +""" + + +import adafruit_raspberry_pi5_piomatter +import numpy as np +import PIL.Image as Image + +with open("/sys/class/graphics/fb0/virtual_size") as f: + screenx, screeny = [int(word) for word in f.read().split(",")] + +with open("/sys/class/graphics/fb0/bits_per_pixel") as f: + bits_per_pixel = int(f.read()) + +assert bits_per_pixel == 16 + +bytes_per_pixel = bits_per_pixel // 8 +dtype = {2: np.uint16, 4: np.uint32}[bytes_per_pixel] + +with open("/sys/class/graphics/fb0/stride") as f: + stride = int(f.read()) + +linux_framebuffer = np.memmap('/dev/fb0',mode='r', shape=(screeny, stride // bytes_per_pixel), dtype=dtype) + +xoffset = 0 +yoffset = 0 +width = 64 +height = 32 +scale = 3 + +geometry = adafruit_raspberry_pi5_piomatter.Geometry(width=width, height=height, n_addr_lines=4, rotation=adafruit_raspberry_pi5_piomatter.Orientation.Normal) +matrix_framebuffer = np.zeros(shape=(geometry.height, geometry.width, 3), dtype=np.uint8) +matrix = adafruit_raspberry_pi5_piomatter.AdafruitMatrixBonnetRGB888Packed(matrix_framebuffer, geometry) + +while True: + tmp = linux_framebuffer[yoffset:yoffset+height*scale, xoffset:xoffset+width*scale] + # Convert the RGB565 framebuffer into RGB888Packed (so that we can use PIL image operations to rescale it) + r = (tmp & 0xf800) >> 8 + r = r | (r >> 5) + r = r.astype(np.uint8) + g = (tmp & 0x07e0) >> 3 + g = g | (g >> 6) + g = g.astype(np.uint8) + b = (tmp & 0x001f) << 3 + b = b | (b >> 5) + b = b.astype(np.uint8) + img = Image.fromarray(np.stack([r, g, b], -1)) + img = img.resize((width, height)) + matrix_framebuffer[:,:] = np.asarray(img) + matrix.show()