Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for 16x16 LED Matrix #956

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
3 changes: 2 additions & 1 deletion drivers/hub75/hub75.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Hub75::Hub75(uint width, uint height, Pixel *buffer, PanelType panel_type, bool
if (width >= 128) brightness = 2;
if (width >= 160) brightness = 1;
}

}

void Hub75::set_color(uint x, uint y, Pixel c) {
Expand Down Expand Up @@ -275,4 +276,4 @@ void Hub75::update(PicoGraphics *graphics) {
}
}
}
}
}
307 changes: 307 additions & 0 deletions examples/interstate75/interstate75_hello_world.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
#include <stdio.h>
#include <math.h>
#include <cstdint>
#include <cstring>

#include "pico/stdlib.h"
#include "pico/multicore.h"
#include "hardware/vreg.h"

#include "common/pimoroni_common.hpp"

using namespace pimoroni;

/* Interstate 75 - HUB75 from basic principles.

While trying to get our I75 running I soon realised that I couldn't find any documentation or basics on the HUB75 protocol.

Yes there were libraries and tidbits that were seemingly loosely related, but nothing really laying out clear steps to update the 32x32 display on my desk.

So I wrote this.

This code may not be technically correct, but the results are visually appealing and fast enough.

More importantly, the code is legible and doesn't hide any implementation details beneath the inscrutable veneer of PIO.

No interpolators, no DMA, no PIO here- if you want a beautifully optimised library for HUB75 output then this... isn't it.

Just pure C. Pure CPU. Running on RP2040s Core1.

*/

// Display size in pixels
// Should be either 64x64 or 32x32 but perhaps 64x32 an other sizes will work.
// Note: this example uses only 5 address lines so it's limited to 32*2 pixels.
const uint8_t WIDTH = 64;
const uint8_t HEIGHT = 64;

// Settings below are correct for I76, change them to suit your setup:

// Top half of display - 16 rows on a 32x32 panel
const uint PIN_R0 = 0;
const uint PIN_G0 = 1;
const uint PIN_B0 = 2;

// Bottom half of display - 16 rows on a 64x64 panel
const uint PIN_R1 = 3;
const uint PIN_G1 = 4;
const uint PIN_B1 = 5;

// Address pins, 5 lines = 2^5 = 32 values (max 64x64 display)
const uint PIN_ROW_A = 6;
const uint PIN_ROW_B = 7;
const uint PIN_ROW_C = 8;
const uint PIN_ROW_D = 9;
const uint PIN_ROW_E = 10;

// Sundry things
const uint PIN_CLK = 11; // Clock
const uint PIN_STB = 12; // Strobe/Latch
const uint PIN_OE = 13; // Output Enable

const bool CLK_POLARITY = 1;
const bool STB_POLARITY = 1;
const bool OE_POLARITY = 0;

// User buttons and status LED
const uint PIN_SW_A = 14;
const uint PIN_SW_USER = 23;

const uint PIN_LED_R = 16;
const uint PIN_LED_G = 17;
const uint PIN_LED_B = 18;

// This gamma table is used to correct our 8-bit (0-255) colours up to 11-bit,
// allowing us to gamma correct without losing dynamic range.
const uint16_t GAMMA[256] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 47, 50,
52, 54, 57, 59, 62, 65, 67, 70, 73, 76, 79, 82, 85, 88, 91, 94,
98, 101, 105, 108, 112, 115, 119, 123, 127, 131, 135, 139, 143, 147, 151, 155,
160, 164, 169, 173, 178, 183, 187, 192, 197, 202, 207, 212, 217, 223, 228, 233,
239, 244, 250, 255, 261, 267, 273, 279, 285, 291, 297, 303, 309, 316, 322, 328,
335, 342, 348, 355, 362, 369, 376, 383, 390, 397, 404, 412, 419, 427, 434, 442,
449, 457, 465, 473, 481, 489, 497, 505, 513, 522, 530, 539, 547, 556, 565, 573,
582, 591, 600, 609, 618, 628, 637, 646, 656, 665, 675, 685, 694, 704, 714, 724,
734, 744, 755, 765, 775, 786, 796, 807, 817, 828, 839, 850, 861, 872, 883, 894,
905, 917, 928, 940, 951, 963, 975, 987, 998, 1010, 1022, 1035, 1047, 1059, 1071, 1084,
1096, 1109, 1122, 1135, 1147, 1160, 1173, 1186, 1199, 1213, 1226, 1239, 1253, 1266, 1280, 1294,
1308, 1321, 1335, 1349, 1364, 1378, 1392, 1406, 1421, 1435, 1450, 1465, 1479, 1494, 1509, 1524,
1539, 1554, 1570, 1585, 1600, 1616, 1631, 1647, 1663, 1678, 1694, 1710, 1726, 1743, 1759, 1775,
1791, 1808, 1824, 1841, 1858, 1875, 1891, 1908, 1925, 1943, 1960, 1977, 1994, 2012, 2029, 2047};


// We don't *need* to make Pixel a fancy struct with RGB values, but it helps.
#pragma pack(push, 1)
struct alignas(4) Pixel {
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t _;
constexpr Pixel() : r(0), g(0), b(0), _(0) {};
constexpr Pixel(uint8_t r, uint8_t g, uint8_t b) : r(r), g(g), b(b), _(0) {};
constexpr Pixel(uint r, uint g, uint b) : r(r), g(g), b(b), _(0) {};
constexpr Pixel(float r, float g, float b) : r((uint8_t)(r * 255.0f)), g((uint8_t)(g * 255.0f)), b((uint8_t)(b * 255.0f)), _(0) {};
};
#pragma pack(pop)

// Create our front and back buffers.
// We'll draw into the frontbuffer and then copy everything into the backbuffer which will be used to refresh the screen.
// Double-buffering the display avoids screen tearing with fast animations or slow update rates.
Pixel backbuffer[WIDTH][HEIGHT];
Pixel frontbuffer[WIDTH][HEIGHT];

// Basic function to convert Hue, Saturation and Value to an RGB colour
Pixel hsv_to_rgb(float h, float s, float v) {
if(h < 0.0f) {
h = 1.0f + fmod(h, 1.0f);
}

int i = int(h * 6);
float f = h * 6 - i;

v = v * 255.0f;

float sv = s * v;
float fsv = f * sv;

auto p = uint8_t(-sv + v);
auto q = uint8_t(-fsv + v);
auto t = uint8_t(fsv - sv + v);

uint8_t bv = uint8_t(v);

switch (i % 6) {
default:
case 0: return Pixel(bv, t, p);
case 1: return Pixel(q, bv, p);
case 2: return Pixel(p, bv, t);
case 3: return Pixel(p, q, bv);
case 4: return Pixel(t, p, bv);
case 5: return Pixel(bv, p, q);
}
}

// Required for FM6126A-based displays which need some register config/init to work properly
void FM6126A_write_register(uint16_t value, uint8_t position) {
uint8_t threshold = WIDTH - position;
for(auto i = 0u; i < WIDTH; i++) {
auto j = i % 16;
bool b = value & (1 << j);
gpio_put(PIN_R0, b);
gpio_put(PIN_G0, b);
gpio_put(PIN_B0, b);
gpio_put(PIN_R1, b);
gpio_put(PIN_G1, b);
gpio_put(PIN_B1, b);

// Assert strobe/latch if i > threshold
// This somehow indicates to the FM6126A which register we want to write :|
gpio_put(PIN_STB, i > threshold);
gpio_put(PIN_CLK, CLK_POLARITY);
sleep_us(10);
gpio_put(PIN_CLK, !CLK_POLARITY);
}
}

void hub75_display_update() {
// Ridiculous register write nonsense for the FM6126A-based 64x64 matrix
FM6126A_write_register(0b1111111111111110, 12);
FM6126A_write_register(0b0000001000000000, 13);

while (true) {
// 0. Copy the contents of the front buffer into our backbuffer for output to the display.
// This uses another whole backbuffer worth of memory, but prevents visual tearing at low frequencies.
memcpy((uint8_t *)backbuffer, (uint8_t *)frontbuffer, WIDTH * HEIGHT * sizeof(Pixel));

// Step through 0b00000001, 0b00000010, 0b00000100 etc
for(auto bit = 1u; bit < 1 << 11; bit <<= 1) {
// Since the display is in split into two equal halves, we step through y from 0 to HEIGHT / 2
for(auto y = 0u; y < HEIGHT / 2; y++) {

// 1. Shift out pixel data
// Shift out WIDTH pixels to the top and bottom half of the display
for(auto x = 0u; x < WIDTH; x++) {
// Get the current pixel for top/bottom half
// This is easy since we just need the pixels at X/Y and X/Y+HEIGHT/2
Pixel pixel_top = backbuffer[x][y];
Pixel pixel_bottom = backbuffer[x][y + HEIGHT / 2];

// Gamma correct the colour values from 8-bit to 11-bit
uint16_t pixel_top_b = GAMMA[pixel_top.b];
uint16_t pixel_top_g = GAMMA[pixel_top.g];
uint16_t pixel_top_r = GAMMA[pixel_top.r];

uint16_t pixel_bottom_b = GAMMA[pixel_bottom.b];
uint16_t pixel_bottom_g = GAMMA[pixel_bottom.g];
uint16_t pixel_bottom_r = GAMMA[pixel_bottom.r];

// Set the clock low while we set up the data pins
gpio_put(PIN_CLK, !CLK_POLARITY);

// Top half
gpio_put(PIN_R0, (bool)(pixel_top_r & bit));
gpio_put(PIN_G0, (bool)(pixel_top_g & bit));
gpio_put(PIN_B0, (bool)(pixel_top_b & bit));

// Bottom half
gpio_put(PIN_R1, (bool)(pixel_bottom_r & bit));
gpio_put(PIN_G1, (bool)(pixel_bottom_g & bit));
gpio_put(PIN_B1, (bool)(pixel_bottom_b & bit));

// Wiggle the clock
// The gamma correction above will ensure our clock stays asserted
// for some small amount of time, avoiding the need for an explicit delay.
gpio_put(PIN_CLK, CLK_POLARITY);
}


// 2. Set address pins
// Set the address pins to reflect the row to light up: 0 through 15 for 32x32 pixel panels
// We decode our 5-bit row address out onto the 5 GPIO pins by masking each bit in turn.
gpio_put_masked(0b11111 << PIN_ROW_A, y << PIN_ROW_A);

// 3. Assert latch/strobe signal (STB)
// This latches all the values we've just clocked into the column shift registers.
// The values will appear on the output pins, ready for the display to be driven.
gpio_put(PIN_STB, STB_POLARITY);

// 4. Asset the output-enable signal (OE)
// This turns on the display for a brief period to light the selected rows/columns.
gpio_put(PIN_OE, OE_POLARITY);

// 4. Delay
// Delay for a period of time coressponding to "bit"'s significance
for(auto s = 0u; s < bit; ++s) {
// The basic premise here is that "bit" will step through the values:
// 1, 2, 4, 8, 16, 32, 64, etc in sequence.
// If we plug this number into a delay loop, we'll get different magnitudes
// of delay which correspond exactly to the significance of each bit.
// The longer we delay here, the slower the overall panel refresh rate will be.
// But we need to delay *just enough* that we're not under-driving the panel and
// losing out on brightness.
asm volatile("nop \nnop"); // Batman!
}

// 5. De-assert latch/strobe signal (STB) + output-enable signal (OE)
// Ready to go again!
gpio_put(PIN_STB, !STB_POLARITY);
gpio_put(PIN_OE, !OE_POLARITY);

// 6. GOTO 1.
}
sleep_us(1);
}
}
}

int main() {
// 1.3v allows overclock to ~280000-300000 but YMMV. Faster clock = faster screen update rate!
// vreg_set_voltage(VREG_VOLTAGE_1_30);
// sleep_ms(100);

// 200MHz is roughly about the lower limit for driving a 64x64 display smoothly.
// Just don't look at it out of the corner of your eye.
set_sys_clock_khz(200000, false);

// Set up allllll the GPIO
gpio_init(PIN_R0); gpio_set_function(PIN_R0, GPIO_FUNC_SIO); gpio_set_dir(PIN_R0, true);
gpio_init(PIN_G0); gpio_set_function(PIN_G0, GPIO_FUNC_SIO); gpio_set_dir(PIN_G0, true);
gpio_init(PIN_B0); gpio_set_function(PIN_B0, GPIO_FUNC_SIO); gpio_set_dir(PIN_B0, true);

gpio_init(PIN_R1); gpio_set_function(PIN_R1, GPIO_FUNC_SIO); gpio_set_dir(PIN_R1, true);
gpio_init(PIN_G1); gpio_set_function(PIN_G1, GPIO_FUNC_SIO); gpio_set_dir(PIN_G1, true);
gpio_init(PIN_B1); gpio_set_function(PIN_B1, GPIO_FUNC_SIO); gpio_set_dir(PIN_B1, true);

gpio_init(PIN_ROW_A); gpio_set_function(PIN_ROW_A, GPIO_FUNC_SIO); gpio_set_dir(PIN_ROW_A, true);
gpio_init(PIN_ROW_B); gpio_set_function(PIN_ROW_B, GPIO_FUNC_SIO); gpio_set_dir(PIN_ROW_B, true);
gpio_init(PIN_ROW_C); gpio_set_function(PIN_ROW_C, GPIO_FUNC_SIO); gpio_set_dir(PIN_ROW_C, true);
gpio_init(PIN_ROW_D); gpio_set_function(PIN_ROW_D, GPIO_FUNC_SIO); gpio_set_dir(PIN_ROW_D, true);
gpio_init(PIN_ROW_E); gpio_set_function(PIN_ROW_E, GPIO_FUNC_SIO); gpio_set_dir(PIN_ROW_E, true);

gpio_init(PIN_CLK); gpio_set_function(PIN_CLK, GPIO_FUNC_SIO); gpio_set_dir(PIN_CLK, true);
gpio_init(PIN_STB); gpio_set_function(PIN_STB, GPIO_FUNC_SIO); gpio_set_dir(PIN_STB, true);
gpio_init(PIN_OE); gpio_set_function(PIN_OE, GPIO_FUNC_SIO); gpio_set_dir(PIN_OE, true);

// Launch the display update routine on Core 1, it's hungry for cycles!
multicore_launch_core1(hub75_display_update);

// Basic loop to draw something to the screen.
// This gets the distance from the middle of the display and uses it to paint a circular colour cycle.
while (true) {
float offset = millis() / 10000.0f;
for(auto x = 0u; x < WIDTH; x++) {
for(auto y = 0u; y < HEIGHT; y++) {
// Center our rainbow circles
float x1 = ((int)x - WIDTH / 2);
float y1 = ((int)y - HEIGHT / 2);
// Get hue as the distance from the display center as float from 0.0 to 1.0f.
float h = float(x1*x1 + y1*y1) / float(WIDTH*WIDTH + HEIGHT*HEIGHT);
// Offset our hue to animate the effect
h -= offset;
frontbuffer[x][y] = hsv_to_rgb(h, 1.0f, 1.0f);
}
}
}
}
1 change: 1 addition & 0 deletions micropython/modules/picographics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Both the Interstate75 and Interstate75W support lots of different sizes of HUB75

The available display settings are listed here:

* 32 x 32 Matrix - `DISPLAY_INTERSTATE75_16X16`
* 32 x 32 Matrix - `DISPLAY_INTERSTATE75_32X32`
* 64 x 32 Matrix - `DISPLAY_INTERSTATE75_64X32`
* 96 x 32 Matrix - `DISPLAY_INTERSTATE75_96X32`
Expand Down
1 change: 1 addition & 0 deletions micropython/modules/picographics/picographics.c
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ static const mp_map_elem_t picographics_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR_DISPLAY_INKY_FRAME_4), MP_ROM_INT(DISPLAY_INKY_FRAME_4) },
{ MP_ROM_QSTR(MP_QSTR_DISPLAY_GALACTIC_UNICORN), MP_ROM_INT(DISPLAY_GALACTIC_UNICORN) },
{ MP_ROM_QSTR(MP_QSTR_DISPLAY_GFX_PACK), MP_ROM_INT(DISPLAY_GFX_PACK) },
{ MP_ROM_QSTR(MP_QSTR_DISPLAY_INTERSTATE75_16X16), MP_ROM_INT(DISPLAY_INTERSTATE75_16X16) },
{ MP_ROM_QSTR(MP_QSTR_DISPLAY_INTERSTATE75_32X32), MP_ROM_INT(DISPLAY_INTERSTATE75_32X32) },
{ MP_ROM_QSTR(MP_QSTR_DISPLAY_INTERSTATE75_64X32), MP_ROM_INT(DISPLAY_INTERSTATE75_64X32) },
{ MP_ROM_QSTR(MP_QSTR_DISPLAY_INTERSTATE75_96X32), MP_ROM_INT(DISPLAY_INTERSTATE75_96X32) },
Expand Down
8 changes: 8 additions & 0 deletions micropython/modules/picographics/picographics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ bool get_display_settings(PicoGraphicsDisplay display, int &width, int &height,
if(rotate == -1) rotate = (int)Rotation::ROTATE_0;
if(pen_type == -1) pen_type = PEN_1BIT;
break;
case DISPLAY_INTERSTATE75_16X16:
width = 16;
height = 16;
bus_type = BUS_PIO;
// Portrait to match labelling
if(rotate == -1) rotate = (int)Rotation::ROTATE_0;
if(pen_type == -1) pen_type = PEN_RGB888;
break;
case DISPLAY_INTERSTATE75_32X32:
width = 32;
height = 32;
Expand Down
1 change: 1 addition & 0 deletions micropython/modules/picographics/picographics.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ enum PicoGraphicsDisplay {
DISPLAY_INKY_FRAME_4,
DISPLAY_GALACTIC_UNICORN,
DISPLAY_GFX_PACK,
DISPLAY_INTERSTATE75_16X16,
DISPLAY_INTERSTATE75_32X32,
DISPLAY_INTERSTATE75_64X32,
DISPLAY_INTERSTATE75_96X32,
Expand Down
3 changes: 2 additions & 1 deletion micropython/modules_py/interstate75.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ The version of Intersate75 you're using should be automatically detected. Check
You can choose the HUB75 matrix display size that you wish to use by defining `display=` as one of the following:

```python
DISPLAY_INTERSTATE75_16X16
DISPLAY_INTERSTATE75_32X32
DISPLAY_INTERSTATE75_64X32
DISPLAY_INTERSTATE75_96X32
Expand Down Expand Up @@ -98,4 +99,4 @@ display.text("Hello World!", 0, 0)
display.line(0, 0, 128, 64)
board.update() # Update display with the above items
```
All the picographics functions can be found [Here](../modules/picographics/README.md)
All the picographics functions can be found [Here](../modules/picographics/README.md)
3 changes: 2 additions & 1 deletion micropython/modules_py/interstate75.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pimoroni import RGBLED, Button
from picographics import PicoGraphics, DISPLAY_INTERSTATE75_32X32, DISPLAY_INTERSTATE75_64X32, DISPLAY_INTERSTATE75_96X32, DISPLAY_INTERSTATE75_96X48, DISPLAY_INTERSTATE75_128X32, DISPLAY_INTERSTATE75_64X64, DISPLAY_INTERSTATE75_128X64, DISPLAY_INTERSTATE75_192X64, DISPLAY_INTERSTATE75_256X64
from picographics import PicoGraphics, DISPLAY_INTERSTATE75_16X16, DISPLAY_INTERSTATE75_32X32, DISPLAY_INTERSTATE75_64X32, DISPLAY_INTERSTATE75_96X32, DISPLAY_INTERSTATE75_96X48, DISPLAY_INTERSTATE75_128X32, DISPLAY_INTERSTATE75_64X64, DISPLAY_INTERSTATE75_128X64, DISPLAY_INTERSTATE75_192X64, DISPLAY_INTERSTATE75_256X64
from pimoroni_i2c import PimoroniI2C
import hub75
import sys
Expand All @@ -20,6 +20,7 @@ class Interstate75:
LED_B_PIN = 18

# Display Types
DISPLAY_INTERSTATE75_16X16 = DISPLAY_INTERSTATE75_16X16
DISPLAY_INTERSTATE75_32X32 = DISPLAY_INTERSTATE75_32X32
DISPLAY_INTERSTATE75_64X32 = DISPLAY_INTERSTATE75_64X32
DISPLAY_INTERSTATE75_96X32 = DISPLAY_INTERSTATE75_96X32
Expand Down