kopia lustrzana https://github.com/pimoroni/pimoroni-pico
				
				
				
			Pure C++ HUB75 example
This code is intentionally written in C++ to illustrate the basics of driving HUB75 without being overly complicated to read and understand. Tested on a 32x32 panel and a 64x64 "FM6126A" panel, for which this code includes a magic pair of register settings.pull/193/head
							rodzic
							
								
									e249113fea
								
							
						
					
					
						commit
						babe8a7760
					
				|  | @ -37,3 +37,4 @@ add_subdirectory(pico_audio) | |||
| add_subdirectory(pico_wireless) | ||||
| 
 | ||||
| add_subdirectory(plasma2040) | ||||
| add_subdirectory(interstate75) | ||||
|  |  | |||
|  | @ -0,0 +1 @@ | |||
| include(interstate75_hello_world.cmake) | ||||
|  | @ -0,0 +1,16 @@ | |||
| set(OUTPUT_NAME interstate75_hello_world) | ||||
| add_executable(${OUTPUT_NAME} interstate75_hello_world.cpp) | ||||
| 
 | ||||
| # enable usb output | ||||
| pico_enable_stdio_usb(${OUTPUT_NAME} 1) | ||||
| 
 | ||||
| pico_add_extra_outputs(${OUTPUT_NAME}) | ||||
| 
 | ||||
| target_link_libraries(${OUTPUT_NAME} | ||||
|     pico_stdlib | ||||
|     pico_multicore | ||||
|     hardware_pio | ||||
|     hardware_dma | ||||
|     ) | ||||
| 
 | ||||
| pico_generate_pio_header(${OUTPUT_NAME} ${CMAKE_CURRENT_LIST_DIR}/hub75.pio) | ||||
|  | @ -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); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Ładowanie…
	
		Reference in New Issue
	
	 Phil Howard
						Phil Howard