commit b6231669cc212ad6fe765049d019c902c9b1071f Author: lukas Date: Sat Dec 26 13:23:47 2020 +0100 Initial commit diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0348de1 --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +.PHONY: default all clean + +RGB_LIB_DISTRIBUTION=../../rpi-rgb-led-matrix +RGB_INCDIR=$(RGB_LIB_DISTRIBUTION)/include +RGB_LIBDIR=$(RGB_LIB_DISTRIBUTION)/lib +RGB_LIBRARY_NAME=librgbmatrix +RGB_LIBRARY=$(RGB_LIBDIR)/lib$(RGB_LIBRARY_NAME).a +LDFLAGS+=-L$(RGB_LIBDIR) -l$(RGB_LIBRARY_NAME) -lrt -lm -lpthread + +CC = gcc +CFLAGS = -Wall -pthread +LIBS = -levent -levent_pthreads -lrt -lGL -lGLEW -lglfw $(RGB_LIBRARY_NAME).a -lrt -lm -lpthread -lstdc++ +TARGET = pixelnuke + + + +default: CFLAGS += -O2 -flto +default: $(TARGET) +all: default + +debug: CFLAGS += -DDEBUG -g +debug: $(TARGET) + +OBJECTS = $(patsubst %.c, %.o, $(wildcard *.c)) +HEADERS = $(wildcard *.h) + +%.o: %.c $(HEADERS) + $(CC) $(CFLAGS) -c $< -o $@ + +% : %.o $(RGB_LIBRARY) + $(CXX) $< -o $@ $(LDFLAGS) + +%.o : %.c + $(CC) -I$(RGB_INCDIR) $(CFLAGS) -c -o $@ $< + +.PRECIOUS: $(TARGET) $(OBJECTS) + +$(TARGET): $(OBJECTS) + $(CC) $(CFLAGS) $(OBJECTS) -Wall $(LIBS) -o $@ + +clean: + -rm -f *.o $(TARGET) diff --git a/canvas.h b/canvas.h new file mode 100644 index 0000000..bbb8922 --- /dev/null +++ b/canvas.h @@ -0,0 +1,52 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2014 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +#ifndef RPI_CANVAS_H +#define RPI_CANVAS_H +#include + +namespace rgb_matrix { +// An interface for things a Canvas can do. The RGBMatrix implements this +// interface, so you can use it directly wherever a canvas is needed. +// +// This abstraction also allows you to e.g. create delegating +// implementations that do a particular transformation, e.g. re-map +// pixels (as you might lay out the physical RGB matrix in a different way), +// compose images (OR, XOR, transparecy), scale, rotate, anti-alias or +// translate coordinates in a funky way. +// +// It is a good idea to have your applications use the concept of +// a Canvas to write the content to instead of directly using the RGBMatrix. +class Canvas { +public: + virtual ~Canvas() {} + virtual int width() const = 0; // Pixels available in x direction. + virtual int height() const = 0; // Pixels available in y direction. + + // Set pixel at coordinate (x,y) with given color. Pixel (0,0) is the + // top left corner. + // Each color is 8 bit (24bpp), 0 black, 255 brightest. + virtual void SetPixel(int x, int y, + uint8_t red, uint8_t green, uint8_t blue) = 0; + + // Clear screen to be all black. + virtual void Clear() = 0; + + // Fill screen with given 24bpp color. + virtual void Fill(uint8_t red, uint8_t green, uint8_t blue) = 0; +}; + +} // namespace rgb_matrix +#endif // RPI_CANVAS_H diff --git a/canvaspixel.c b/canvaspixel.c new file mode 100644 index 0000000..526b41f --- /dev/null +++ b/canvaspixel.c @@ -0,0 +1,408 @@ +#include +#include +#include // usleep +#include +#include +#include +#include //memcpy + +#include "canvaspixel.h" + +typedef struct CanvasLayer { + GLuint size; + GLenum format; + GLuint tex; + GLuint pbo1; + GLuint pbo2; + GLubyte *data; + size_t mem; +} CanvasLayer; + +// Global state + +static int canvaspixel_display = -1; +static unsigned int canvaspixel_tex_size = 1024; +static int canvaspixel_width=0; +static int canvaspixel_height=0; +static GLFWwindow* canvaspixel_win; +static CanvasLayer *canvaspixel_base; +static CanvasLayer *canvaspixel_overlay; + +pthread_t canvaspixel_thread; + +void glfw_error_callback(int error, const char* description) { + printf("GLFW Error: %d %s", error, description); +} + +static inline int min(int a, int b) { + return a < b ? a : b; +} + +static inline int max(int a, int b) { + return a > b ? a : b; +} + +// User callbacks + +void (*canvaspixel_on_close_cb)(); +void (*canvaspixel_on_resize_cb)(); +void (*canvaspixel_on_key_cb)(int, int, int); + +static int canvaspixel_do_layout = 0; + +static CanvasLayer* canvaspixel_layer_alloc(int size, int alpha) { + CanvasLayer * layer = malloc(sizeof(CanvasLayer)); + layer->size = size; + layer->format = alpha ? GL_RGBA : GL_RGB; + layer->mem = size * size * (alpha ? 4 : 3); + layer->data = malloc(sizeof(GLubyte) * layer->mem); + memset(layer->data, 0, layer->mem); + return layer; +} + +static void canvaspixel_layer_bind(CanvasLayer* layer) { + + // Create texture object + glGenTextures(1, &(layer->tex)); + glBindTexture( GL_TEXTURE_2D, layer->tex); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glBindTexture( GL_TEXTURE_2D, 0); + + // Create two PBOs + glGenBuffers(1, &(layer->pbo1)); + glBindBuffer( GL_PIXEL_UNPACK_BUFFER, layer->pbo1); + glBufferData( GL_PIXEL_UNPACK_BUFFER, layer->mem, NULL, GL_STREAM_DRAW); + glGenBuffers(1, &(layer->pbo2)); + glBindBuffer( GL_PIXEL_UNPACK_BUFFER, layer->pbo2); + glBufferData( GL_PIXEL_UNPACK_BUFFER, layer->mem, NULL, GL_STREAM_DRAW); + glBindBuffer( GL_PIXEL_UNPACK_BUFFER, 0); +} + + +static void canvaspixel_layer_unbind(CanvasLayer * layer) { + if (layer->tex) { + glDeleteTextures(1, &(layer->tex)); + glDeleteBuffers(1, &(layer->pbo1)); + glDeleteBuffers(1, &(layer->pbo2)); + } +} + +static void canvaspixel_layer_free(CanvasLayer * layer) { + canvaspixel_layer_unbind(layer); + free(layer->data); + free(layer); +} + +static void canvaspixel_on_resize(GLFWwindow* window, int w, int h); +static void canvaspixel_on_key(GLFWwindow* window, int key, int scancode, int action, + int mods); + +static void canvaspixel_on_key(GLFWwindow* window, int key, int scancode, int action, + int mods) { + if (action == GLFW_PRESS && canvaspixel_on_key_cb) + (*canvaspixel_on_key_cb)(key, scancode, mods); +} + +static void canvaspixel_on_resize(GLFWwindow* window, int w, int h) { + canvaspixel_width = w; + canvaspixel_height = h; + + if(canvaspixel_on_resize_cb) + (*canvaspixel_on_resize_cb)(); +} + +static void canvaspixel_window_setup() { + + if(canvaspixel_win) { + glfwDestroyWindow(canvaspixel_win); + } + + glfwWindowHint(GLFW_DOUBLEBUFFER, 1); + if (canvaspixel_display >= 0) { + int mcount; + GLFWmonitor** monitors = glfwGetMonitors(&mcount); + canvaspixel_display %= mcount; + GLFWmonitor* monitor = monitors[canvaspixel_display]; + const GLFWvidmode* mode = glfwGetVideoMode(monitor); + glfwWindowHint(GLFW_RED_BITS, mode->redBits); + glfwWindowHint(GLFW_GREEN_BITS, mode->greenBits); + glfwWindowHint(GLFW_BLUE_BITS, mode->blueBits); + glfwWindowHint(GLFW_REFRESH_RATE, mode->refreshRate); + canvaspixel_win = glfwCreateWindow(mode->width, mode->height, "Pixelflut", monitor, NULL); + } else { + canvaspixel_win = glfwCreateWindow(800, 600, "Pixelflut", NULL, NULL); + } + + if (!canvaspixel_win) { + printf("Could not create OpenGL context and/or window"); + return; + } + + glfwMakeContextCurrent(canvaspixel_win); + + // TODO: Move GL stuff to better place + //glShadeModel(GL_FLAT); // shading mathod: GL_SMOOTH or GL_FLAT + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); // 4-byte pixel alignment + //glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); + //glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); + //glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST); + glDisable(GL_DEPTH_TEST); + glDisable(GL_LIGHTING); + glDisable(GL_CULL_FACE); + glEnable(GL_TEXTURE_2D); + + //glfwSetWindowUserPointer(canvaspixel_win, (void*) this); + glfwSwapInterval(1); + glfwSetKeyCallback(canvaspixel_win, &canvaspixel_on_key); + glfwSetFramebufferSizeCallback(canvaspixel_win, &canvaspixel_on_resize); + + glfwGetFramebufferSize(canvaspixel_win, &canvaspixel_width, &canvaspixel_height); + canvaspixel_on_resize(canvaspixel_win, canvaspixel_width, canvaspixel_height); + + canvaspixel_do_layout = 0; +} + +static void canvaspixel_draw_layer(CanvasLayer * layer) { + if (!layer || !layer->data) + return; + + GLuint pboNext = layer->pbo1; + GLuint pboIndex = layer->pbo2; + layer->pbo1 = pboIndex; + layer->pbo2 = pboNext; + + // Switch PBOs on each call. One is updated, one is drawn. + // Update texture from first PBO + glBindBuffer( GL_PIXEL_UNPACK_BUFFER, pboIndex); + glBindTexture( GL_TEXTURE_2D, layer->tex); + glTexImage2D( GL_TEXTURE_2D, 0, layer->format, layer->size, layer->size, 0, + layer->format, GL_UNSIGNED_BYTE, 0); + glBindTexture( GL_TEXTURE_2D, 0); + + // Update second PBO with new pixel data + glBindBuffer( GL_PIXEL_UNPACK_BUFFER, pboNext); + GLubyte *ptr = (GLubyte*) glMapBuffer( GL_PIXEL_UNPACK_BUFFER, + GL_WRITE_ONLY); + memcpy(ptr, layer->data, layer->mem); + glUnmapBuffer( GL_PIXEL_UNPACK_BUFFER); + glBindBuffer( GL_PIXEL_UNPACK_BUFFER, 0); + + //// Actually draw stuff. The texture should be updated in the meantime. + + if (layer->format == GL_RGBA) { + glEnable( GL_BLEND); + glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } else { + glDisable( GL_BLEND); + } + + glPushMatrix(); + glBindTexture( GL_TEXTURE_2D, layer->tex); + glBegin( GL_QUADS); + glTexCoord2f(0, 0); + glVertex3f(0.0f, 0.0f, 0.0f); + glTexCoord2f(0, 1); + glVertex3f(0.0f, layer->size, 0.0f); + glTexCoord2f(1, 1); + glVertex3f(layer->size, layer->size, 0.0f); + glTexCoord2f(1, 0); + glVertex3f(layer->size, 0.0f, 0.0f); + glEnd(); + glBindTexture( GL_TEXTURE_2D, 0); + glPopMatrix(); +} + +static void* canvaspixel_render_loop(void * arg) { + + glfwSetErrorCallback(glfw_error_callback); + if (!glfwInit()) { + puts("GLFW initialization failed"); + if(canvaspixel_on_close_cb) + (*canvaspixel_on_close_cb)(); + glfwTerminate(); + return NULL; + } + + canvaspixel_window_setup(); + + int err = glewInit(); + if (err != GLEW_OK) { + puts("GLEW initialization failed"); + printf("Error: %s\n", glewGetErrorString(err)); + if(canvaspixel_on_close_cb) + (*canvaspixel_on_close_cb)(); + return NULL; + } + + canvaspixel_layer_bind(canvaspixel_base); + canvaspixel_layer_bind(canvaspixel_overlay); + + double last_frame = glfwGetTime(); + + while ("pixels are coming") { + + if (canvaspixel_do_layout) { + canvaspixel_layer_unbind(canvaspixel_base); + canvaspixel_layer_unbind(canvaspixel_overlay); + canvaspixel_window_setup(); + canvaspixel_layer_bind(canvaspixel_base); + canvaspixel_layer_bind(canvaspixel_overlay); + } + + if (glfwWindowShouldClose(canvaspixel_win)) + break; + + glfwGetFramebufferSize(canvaspixel_win, &canvaspixel_width, &canvaspixel_height); + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glOrtho(0, canvaspixel_width, canvaspixel_height, 0, -1, 1); + glViewport(0, 0, (GLsizei) canvaspixel_width, (GLsizei) canvaspixel_height); + glClearColor(0, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + glPushMatrix(); + + GLuint texSize = canvaspixel_base->size; + if(canvaspixel_width > texSize || canvaspixel_height > texSize) { + float scale = ((float) max(canvaspixel_width, canvaspixel_height)) / (float) texSize; + glScalef(scale, scale, 1); + } + + canvaspixel_draw_layer(canvaspixel_base); + // TODO: Overlay is not used yet + //canvaspixel_draw_layer(canvaspixel_overlay); + + glPopMatrix(); + glfwPollEvents(); + glfwSwapBuffers(canvaspixel_win); + + double now = glfwGetTime(); + double dt = now - last_frame; + last_frame = now; + double sleep = 1.0 / 30 - dt; + if (sleep > 0) { + usleep(sleep * 1000000); + } + } + + if(canvaspixel_on_close_cb) + (*canvaspixel_on_close_cb)(); + + glfwTerminate(); + canvaspixel_layer_free(canvaspixel_base); + canvaspixel_layer_free(canvaspixel_overlay); + + return NULL; +} + +// Public functions + + + +void canvaspixel_start(unsigned int texSize, void (*on_close)()) { + + canvaspixel_on_close_cb = on_close; + canvaspixel_tex_size = texSize; + canvaspixel_base = canvaspixel_layer_alloc(canvaspixel_tex_size, 0); + canvaspixel_overlay = canvaspixel_layer_alloc(canvaspixel_tex_size, 1); + + if (pthread_create(&canvaspixel_thread, NULL, canvaspixel_render_loop, NULL)) { + puts("Failed to start render thread"); + exit(1); + } +} + +void canvaspixel_setcb_key(void (*on_key)(int key, int scancode, int mods)) { + canvaspixel_on_key_cb = on_key; +} + +void canvaspixel_setcb_resize(void (*on_resize)()) { + canvaspixel_on_resize_cb = on_resize; +} + +void canvaspixel_close() { + glfwSetWindowShouldClose(canvaspixel_win, 1); +} + +void canvaspixel_fullscreen(int display) { + canvaspixel_display = display; + canvaspixel_do_layout = 1; +} + +int canvaspixel_get_display() { + return canvaspixel_display; +} + +// Return a pointer to the GLubyte for a given pixel, or NULL for out of bound coordinates. +static inline GLubyte* canvaspixel_offset(CanvasLayer * layer, unsigned int x, + unsigned int y) { + if (x < 0 || y < 0 || x >= layer->size || y >= layer->size || layer->data == NULL) + return NULL; + return layer->data + + ((y * layer->size) + x) * (layer->format == GL_RGBA ? 4 : 3); +} + +void canvaspixel_set_px(unsigned int x, unsigned int y, uint32_t rgba) { + CanvasLayer * layer = canvaspixel_base; + GLubyte* ptr = canvaspixel_offset(layer, x, y); + + if (ptr == NULL) { + return; + } + + GLubyte r = (rgba & 0xff000000) >> 24; + GLubyte g = (rgba & 0x00ff0000) >> 16; + GLubyte b = (rgba & 0x0000ff00) >> 8; + GLubyte a = (rgba & 0x000000ff) >> 0; + + if (layer->format == GL_RGBA) { + ptr[0] = r; + ptr[1] = g; + ptr[2] = b; + ptr[3] = a; + return; + } + if (a == 0) { + return; + } + if (a < 0xff) { + GLuint na = 0xff - a; + r = (a * r + na * (ptr[0])) / 0xff; + g = (a * g + na * (ptr[1])) / 0xff; + b = (a * b + na * (ptr[2])) / 0xff; + } + ptr[0] = r; + ptr[1] = g; + ptr[2] = b; +} + +void canvaspixel_fill(uint32_t rgba) { + CanvasLayer * layer = canvaspixel_base; + for(int x=0; xsize; x++) + for(int y=0; ysize; y++) + canvaspixel_set_px(x, y, rgba); +} + + +void canvaspixel_get_px(unsigned int x, unsigned int y, uint32_t *rgba) { + CanvasLayer * layer = canvaspixel_base; + GLubyte* ptr = canvaspixel_offset(layer, x, y); + if (ptr == NULL) { + *rgba = 0x000000; + } else { + *rgba = (ptr[0] << 24) + (ptr[1] << 16) + (ptr[2] << 8) + 0xff; + } +} + +void canvaspixel_get_size(unsigned int *w, unsigned int *h) { + int texSize = canvaspixel_base->size; + if(canvaspixel_width > texSize || canvaspixel_height > texSize) { + float scale = ((float) max(canvaspixel_width, canvaspixel_height)) / texSize; + *w = min(texSize, canvaspixel_width/scale); + *h = min(texSize, canvaspixel_height/scale); + } else { + *w = canvaspixel_width; + *h = canvaspixel_height; + } +} + diff --git a/canvaspixel.h b/canvaspixel.h new file mode 100644 index 0000000..e961bf2 --- /dev/null +++ b/canvaspixel.h @@ -0,0 +1,27 @@ +#ifndef CANVAS_H_ +#define CANVAS_H_ + +#include + +// Open the canvaspixel window and start the gui loop (in a separate thread) +void canvaspixel_start(unsigned int texSize, void (*on_close)()); + +void canvaspixel_setcb_key(void (*on_key)(int key, int scancode, int mods)); +void canvaspixel_setcb_resize(void (*on_resize)()); + +// Close the canvaspixel window and free any resources and contexts +void canvaspixel_close(); + +void canvaspixel_fullscreen(int display); +int canvaspixel_get_display(); + +void canvaspixel_fill(uint32_t rgba); +void canvaspixel_set_px(unsigned int x, unsigned int y, uint32_t rgba); +void canvaspixel_get_px(unsigned int x, unsigned int y, uint32_t *rgba); + +// get the current visible canvaspixel size in pixel. +// The actual window might be bigger if scaling is enabled. +void canvaspixel_get_size(unsigned int *width, unsigned int *height); + + +#endif /* CANVAS_H_ */ diff --git a/canvaspixel.o b/canvaspixel.o new file mode 100644 index 0000000..bb0a16e Binary files /dev/null and b/canvaspixel.o differ diff --git a/content-streamer.h b/content-streamer.h new file mode 100644 index 0000000..c95c63a --- /dev/null +++ b/content-streamer.h @@ -0,0 +1,108 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// +// Abstractions to read and write FrameCanvas objects to streams. This allows +// you to create canned streams of content with minimal overhead at runtime +// to play with extreme pixel-throughput which also minimizes overheads in +// the Pi to avoid stuttering or brightness glitches. +// +// The disadvantage is, that this represents the full expanded internal +// representation of a frame, so is very large memory wise. +// +// These abstractions are used in util/led-image-viewer.cc to read and +// write such animations to disk. It is also used in util/video-viewer.cc +// to write a version to disk that then can be played with the led-image-viewer. + +#include +#include +#include +#include + +#include + +namespace rgb_matrix { +class FrameCanvas; + +// An abstraction of a data stream. +class StreamIO { +public: + virtual ~StreamIO() {} + + // Rewind stream. + virtual void Rewind() = 0; + + // Read bytes into buffer. Similar to Posix behavior that allows short reads. + virtual ssize_t Read(void *buf, size_t count) = 0; + + // Write bytes from buffer. Similar to Posix behavior that allows short + // writes. + virtual ssize_t Append(const void *buf, size_t count) = 0; +}; + +class FileStreamIO : public StreamIO { +public: + explicit FileStreamIO(int fd); + ~FileStreamIO(); + + virtual void Rewind(); + virtual ssize_t Read(void *buf, size_t count); + virtual ssize_t Append(const void *buf, size_t count); + +private: + const int fd_; +}; + +class MemStreamIO : public StreamIO { +public: + virtual void Rewind(); + virtual ssize_t Read(void *buf, size_t count); + virtual ssize_t Append(const void *buf, size_t count); + +private: + std::string buffer_; // super simplistic. + size_t pos_; +}; + +class StreamWriter { +public: + // Does not take ownership of StreamIO + StreamWriter(StreamIO *io); + + // Stream out given canvas at the given time. "hold_time_us" indicates + // for how long this frame is to be shown in microseconds. + bool Stream(const FrameCanvas &frame, uint32_t hold_time_us); + +private: + void WriteFileHeader(const FrameCanvas &frame, size_t len); + + StreamIO *const io_; + bool header_written_; +}; + +class StreamReader { +public: + // Does not take ownership of StreamIO + StreamReader(StreamIO *io); + ~StreamReader(); + + // Go back to the beginning. + void Rewind(); + + // Get next frame and its timestamp. Returns 'false' if there is an error + // or end of stream reached.. + bool GetNext(FrameCanvas *frame, uint32_t* hold_time_us); + +private: + enum State { + STREAM_AT_BEGIN, + STREAM_READING, + STREAM_ERROR, + }; + bool ReadFileHeader(const FrameCanvas &frame); + + StreamIO *io_; + size_t frame_buf_size_; + State state_; + + char *header_frame_buffer_; +}; +} diff --git a/graphics.h b/graphics.h new file mode 100644 index 0000000..2feb633 --- /dev/null +++ b/graphics.h @@ -0,0 +1,144 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Very simple graphics library to do simple things. +// +// Might be useful to consider using Cairo instead and just have an interface +// between that and the Canvas. Well, this is a quick set of things to get +// started (and nicely self-contained). +#ifndef RPI_GRAPHICS_H +#define RPI_GRAPHICS_H + +#include "canvas.h" + +#include +#include + +#include + +namespace rgb_matrix { +struct Color { + Color() : r(0), g(0), b(0) {} + Color(uint8_t rr, uint8_t gg, uint8_t bb) : r(rr), g(gg), b(bb) {} + uint8_t r; + uint8_t g; + uint8_t b; +}; + +// Font loading bdf files. If this ever becomes more types, just make virtual +// base class. +class Font { +public: + // Initialize font, but it is only usable after LoadFont() has been called. + Font(); + ~Font(); + + bool LoadFont(const char *path); + + // Return height of font in pixels. Returns -1 if font has not been loaded. + int height() const { return font_height_; } + + // Return baseline. Pixels from the topline to the baseline. + int baseline() const { return base_line_; } + + // Return width of given character, or -1 if font is not loaded or character + // does not exist. + int CharacterWidth(uint32_t unicode_codepoint) const; + + // Draws the unicode character at position "x","y" + // with "color" on "background_color" (background_color can be NULL for + // transparency. + // The "y" position is the baseline of the font. + // If we don't have it in the font, draws the replacement character "�" if + // available. + // Returns how much we advance on the screen, which is the width of the + // character or 0 if we didn't draw any chracter. + int DrawGlyph(Canvas *c, int x, int y, + const Color &color, const Color *background_color, + uint32_t unicode_codepoint) const; + + // Same without background. Deprecated, use the one above instead. + int DrawGlyph(Canvas *c, int x, int y, const Color &color, + uint32_t unicode_codepoint) const; + + // Create a new font derived from this font, which represents an outline + // of the original font, essentially pixels tracing around the original + // letter. + // This can be used in situations in which it is desirable to frame a letter + // in a different color to increase contrast. + // The ownership of the returned pointer is passed to the caller. + Font *CreateOutlineFont() const; + +private: + Font(const Font& x); // No copy constructor. Use references or pointer instead. + + struct Glyph; + typedef std::map CodepointGlyphMap; + + const Glyph *FindGlyph(uint32_t codepoint) const; + + int font_height_; + int base_line_; + CodepointGlyphMap glyphs_; +}; + +// -- Some utility functions. + +// Utility function: set an image from the given buffer containting pixels. +// +// Draw image of size "image_width" and "image_height" from pixel at +// canvas-offset "canvas_offset_x", "canvas_offset_y". Image will be shown +// cropped on the edges if needed. +// +// The canvas offset can be negative, i.e. the image start can be shifted +// outside the image frame on the left/top edge. +// +// The buffer needs to be organized as rows with columns of three bytes +// organized as rgb or bgr. Thus the size of the buffer needs to be exactly +// (3 * image_width * image_height) bytes. +// +// The "image_buffer" parameters contains the data, "buffer_size_bytes" the +// size in bytes. +// +// If "is_bgr" is true, the buffer is treated as BGR pixel arrangement instead +// of RGB. +// Returns 'true' if image was shown within canvas. +bool SetImage(Canvas *c, int canvas_offset_x, int canvas_offset_y, + const uint8_t *image_buffer, size_t buffer_size_bytes, + int image_width, int image_height, + bool is_bgr); + +// Draw text, a standard NUL terminated C-string encoded in UTF-8, +// with given "font" at "x","y" with "color". +// "color" always needs to be set (hence it is a reference), +// "background_color" is a pointer to optionally be NULL for transparency. +// "kerning_offset" allows for additional spacing between characters (can be +// negative) +// Returns how many pixels we advanced on the screen. +int DrawText(Canvas *c, const Font &font, int x, int y, + const Color &color, const Color *background_color, + const char *utf8_text, int kerning_offset = 0); + +// Same without background. Deprecated, use the one above instead. +int DrawText(Canvas *c, const Font &font, int x, int y, const Color &color, + const char *utf8_text); + +// Draw text, a standard NUL terminated C-string encoded in UTF-8, +// with given "font" at "x","y" with "color". +// Draw text as above, but vertically (top down). +// The text is a standard NUL terminated C-string encoded in UTF-8. +// "font, "x", "y", "color" and "background_color" are same as DrawText(). +// "kerning_offset" allows for additional spacing between characters (can be +// negative). +// Returns font height to advance up on the screen. +int VerticalDrawText(Canvas *c, const Font &font, int x, int y, + const Color &color, const Color *background_color, + const char *utf8_text, int kerning_offset = 0); + +// Draw a circle centered at "x", "y", with a radius of "radius" and with "color" +void DrawCircle(Canvas *c, int x, int y, int radius, const Color &color); + +// Draw a line from "x0", "y0" to "x1", "y1" and with "color" +void DrawLine(Canvas *c, int x0, int y0, int x1, int y1, const Color &color); + +} // namespace rgb_matrix + +#endif // RPI_GRAPHICS_H diff --git a/include/canvas.h b/include/canvas.h new file mode 100644 index 0000000..bbb8922 --- /dev/null +++ b/include/canvas.h @@ -0,0 +1,52 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2014 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +#ifndef RPI_CANVAS_H +#define RPI_CANVAS_H +#include + +namespace rgb_matrix { +// An interface for things a Canvas can do. The RGBMatrix implements this +// interface, so you can use it directly wherever a canvas is needed. +// +// This abstraction also allows you to e.g. create delegating +// implementations that do a particular transformation, e.g. re-map +// pixels (as you might lay out the physical RGB matrix in a different way), +// compose images (OR, XOR, transparecy), scale, rotate, anti-alias or +// translate coordinates in a funky way. +// +// It is a good idea to have your applications use the concept of +// a Canvas to write the content to instead of directly using the RGBMatrix. +class Canvas { +public: + virtual ~Canvas() {} + virtual int width() const = 0; // Pixels available in x direction. + virtual int height() const = 0; // Pixels available in y direction. + + // Set pixel at coordinate (x,y) with given color. Pixel (0,0) is the + // top left corner. + // Each color is 8 bit (24bpp), 0 black, 255 brightest. + virtual void SetPixel(int x, int y, + uint8_t red, uint8_t green, uint8_t blue) = 0; + + // Clear screen to be all black. + virtual void Clear() = 0; + + // Fill screen with given 24bpp color. + virtual void Fill(uint8_t red, uint8_t green, uint8_t blue) = 0; +}; + +} // namespace rgb_matrix +#endif // RPI_CANVAS_H diff --git a/include/content-streamer.h b/include/content-streamer.h new file mode 100644 index 0000000..c95c63a --- /dev/null +++ b/include/content-streamer.h @@ -0,0 +1,108 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// +// Abstractions to read and write FrameCanvas objects to streams. This allows +// you to create canned streams of content with minimal overhead at runtime +// to play with extreme pixel-throughput which also minimizes overheads in +// the Pi to avoid stuttering or brightness glitches. +// +// The disadvantage is, that this represents the full expanded internal +// representation of a frame, so is very large memory wise. +// +// These abstractions are used in util/led-image-viewer.cc to read and +// write such animations to disk. It is also used in util/video-viewer.cc +// to write a version to disk that then can be played with the led-image-viewer. + +#include +#include +#include +#include + +#include + +namespace rgb_matrix { +class FrameCanvas; + +// An abstraction of a data stream. +class StreamIO { +public: + virtual ~StreamIO() {} + + // Rewind stream. + virtual void Rewind() = 0; + + // Read bytes into buffer. Similar to Posix behavior that allows short reads. + virtual ssize_t Read(void *buf, size_t count) = 0; + + // Write bytes from buffer. Similar to Posix behavior that allows short + // writes. + virtual ssize_t Append(const void *buf, size_t count) = 0; +}; + +class FileStreamIO : public StreamIO { +public: + explicit FileStreamIO(int fd); + ~FileStreamIO(); + + virtual void Rewind(); + virtual ssize_t Read(void *buf, size_t count); + virtual ssize_t Append(const void *buf, size_t count); + +private: + const int fd_; +}; + +class MemStreamIO : public StreamIO { +public: + virtual void Rewind(); + virtual ssize_t Read(void *buf, size_t count); + virtual ssize_t Append(const void *buf, size_t count); + +private: + std::string buffer_; // super simplistic. + size_t pos_; +}; + +class StreamWriter { +public: + // Does not take ownership of StreamIO + StreamWriter(StreamIO *io); + + // Stream out given canvas at the given time. "hold_time_us" indicates + // for how long this frame is to be shown in microseconds. + bool Stream(const FrameCanvas &frame, uint32_t hold_time_us); + +private: + void WriteFileHeader(const FrameCanvas &frame, size_t len); + + StreamIO *const io_; + bool header_written_; +}; + +class StreamReader { +public: + // Does not take ownership of StreamIO + StreamReader(StreamIO *io); + ~StreamReader(); + + // Go back to the beginning. + void Rewind(); + + // Get next frame and its timestamp. Returns 'false' if there is an error + // or end of stream reached.. + bool GetNext(FrameCanvas *frame, uint32_t* hold_time_us); + +private: + enum State { + STREAM_AT_BEGIN, + STREAM_READING, + STREAM_ERROR, + }; + bool ReadFileHeader(const FrameCanvas &frame); + + StreamIO *io_; + size_t frame_buf_size_; + State state_; + + char *header_frame_buffer_; +}; +} diff --git a/include/graphics.h b/include/graphics.h new file mode 100644 index 0000000..2feb633 --- /dev/null +++ b/include/graphics.h @@ -0,0 +1,144 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Very simple graphics library to do simple things. +// +// Might be useful to consider using Cairo instead and just have an interface +// between that and the Canvas. Well, this is a quick set of things to get +// started (and nicely self-contained). +#ifndef RPI_GRAPHICS_H +#define RPI_GRAPHICS_H + +#include "canvas.h" + +#include +#include + +#include + +namespace rgb_matrix { +struct Color { + Color() : r(0), g(0), b(0) {} + Color(uint8_t rr, uint8_t gg, uint8_t bb) : r(rr), g(gg), b(bb) {} + uint8_t r; + uint8_t g; + uint8_t b; +}; + +// Font loading bdf files. If this ever becomes more types, just make virtual +// base class. +class Font { +public: + // Initialize font, but it is only usable after LoadFont() has been called. + Font(); + ~Font(); + + bool LoadFont(const char *path); + + // Return height of font in pixels. Returns -1 if font has not been loaded. + int height() const { return font_height_; } + + // Return baseline. Pixels from the topline to the baseline. + int baseline() const { return base_line_; } + + // Return width of given character, or -1 if font is not loaded or character + // does not exist. + int CharacterWidth(uint32_t unicode_codepoint) const; + + // Draws the unicode character at position "x","y" + // with "color" on "background_color" (background_color can be NULL for + // transparency. + // The "y" position is the baseline of the font. + // If we don't have it in the font, draws the replacement character "�" if + // available. + // Returns how much we advance on the screen, which is the width of the + // character or 0 if we didn't draw any chracter. + int DrawGlyph(Canvas *c, int x, int y, + const Color &color, const Color *background_color, + uint32_t unicode_codepoint) const; + + // Same without background. Deprecated, use the one above instead. + int DrawGlyph(Canvas *c, int x, int y, const Color &color, + uint32_t unicode_codepoint) const; + + // Create a new font derived from this font, which represents an outline + // of the original font, essentially pixels tracing around the original + // letter. + // This can be used in situations in which it is desirable to frame a letter + // in a different color to increase contrast. + // The ownership of the returned pointer is passed to the caller. + Font *CreateOutlineFont() const; + +private: + Font(const Font& x); // No copy constructor. Use references or pointer instead. + + struct Glyph; + typedef std::map CodepointGlyphMap; + + const Glyph *FindGlyph(uint32_t codepoint) const; + + int font_height_; + int base_line_; + CodepointGlyphMap glyphs_; +}; + +// -- Some utility functions. + +// Utility function: set an image from the given buffer containting pixels. +// +// Draw image of size "image_width" and "image_height" from pixel at +// canvas-offset "canvas_offset_x", "canvas_offset_y". Image will be shown +// cropped on the edges if needed. +// +// The canvas offset can be negative, i.e. the image start can be shifted +// outside the image frame on the left/top edge. +// +// The buffer needs to be organized as rows with columns of three bytes +// organized as rgb or bgr. Thus the size of the buffer needs to be exactly +// (3 * image_width * image_height) bytes. +// +// The "image_buffer" parameters contains the data, "buffer_size_bytes" the +// size in bytes. +// +// If "is_bgr" is true, the buffer is treated as BGR pixel arrangement instead +// of RGB. +// Returns 'true' if image was shown within canvas. +bool SetImage(Canvas *c, int canvas_offset_x, int canvas_offset_y, + const uint8_t *image_buffer, size_t buffer_size_bytes, + int image_width, int image_height, + bool is_bgr); + +// Draw text, a standard NUL terminated C-string encoded in UTF-8, +// with given "font" at "x","y" with "color". +// "color" always needs to be set (hence it is a reference), +// "background_color" is a pointer to optionally be NULL for transparency. +// "kerning_offset" allows for additional spacing between characters (can be +// negative) +// Returns how many pixels we advanced on the screen. +int DrawText(Canvas *c, const Font &font, int x, int y, + const Color &color, const Color *background_color, + const char *utf8_text, int kerning_offset = 0); + +// Same without background. Deprecated, use the one above instead. +int DrawText(Canvas *c, const Font &font, int x, int y, const Color &color, + const char *utf8_text); + +// Draw text, a standard NUL terminated C-string encoded in UTF-8, +// with given "font" at "x","y" with "color". +// Draw text as above, but vertically (top down). +// The text is a standard NUL terminated C-string encoded in UTF-8. +// "font, "x", "y", "color" and "background_color" are same as DrawText(). +// "kerning_offset" allows for additional spacing between characters (can be +// negative). +// Returns font height to advance up on the screen. +int VerticalDrawText(Canvas *c, const Font &font, int x, int y, + const Color &color, const Color *background_color, + const char *utf8_text, int kerning_offset = 0); + +// Draw a circle centered at "x", "y", with a radius of "radius" and with "color" +void DrawCircle(Canvas *c, int x, int y, int radius, const Color &color); + +// Draw a line from "x0", "y0" to "x1", "y1" and with "color" +void DrawLine(Canvas *c, int x0, int y0, int x1, int y1, const Color &color); + +} // namespace rgb_matrix + +#endif // RPI_GRAPHICS_H diff --git a/include/led-matrix-c.h b/include/led-matrix-c.h new file mode 100644 index 0000000..d5989bb --- /dev/null +++ b/include/led-matrix-c.h @@ -0,0 +1,394 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- + * Copyright (C) 2013 Henner Zeller + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 2. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + * Controlling 16x32 or 32x32 RGB matrixes via GPIO. It allows daisy chaining + * of a string of these, and also connecting a parallel string on newer + * Raspberry Pis with more GPIO pins available. + * + * This is a C-binding (for the C++ library) to allow easy binding and + * integration with other languages. The symbols are exported in librgbmatrix.a + * and librgbmatrix.so. You still need to call the final link with + * + * See examples-api-use/c-example.c for a usage example. + * + */ +#ifndef RPI_RGBMATRIX_C_H +#define RPI_RGBMATRIX_C_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct RGBLedMatrix; +struct LedCanvas; +struct LedFont; + +/** + * Parameters to create a new matrix. + * + * To get the defaults, non-set values have to be initialized to zero, so you + * should zero out this struct before setting anything. + */ +struct RGBLedMatrixOptions { + /* + * Name of the hardware mapping used. If passed NULL here, the default + * is used. + */ + const char *hardware_mapping; + + /* The "rows" are the number of rows supported by the display, so 32 or 16. + * Default: 32. + * Corresponding flag: --led-rows + */ + int rows; + + /* The "cols" are the number of columns per panel. Typically something + * like 32, but also 64 is possible. Sometimes even 40. + * cols * chain_length is the total length of the display, so you can + * represent a 64 wide display as cols=32, chain=2 or cols=64, chain=1; + * same thing. + * Flag: --led-cols + */ + int cols; + + /* The chain_length is the number of displays daisy-chained together + * (output of one connected to input of next). Default: 1 + * Corresponding flag: --led-chain + */ + int chain_length; + + /* The number of parallel chains connected to the Pi; in old Pis with 26 + * GPIO pins, that is 1, in newer Pis with 40 interfaces pins, that can + * also be 2 or 3. The effective number of pixels in vertical direction is + * then thus rows * parallel. Default: 1 + * Corresponding flag: --led-parallel + */ + int parallel; + + /* Set PWM bits used for output. Default is 11, but if you only deal with + * limited comic-colors, 1 might be sufficient. Lower require less CPU and + * increases refresh-rate. + * Corresponding flag: --led-pwm-bits + */ + int pwm_bits; + + /* Change the base time-unit for the on-time in the lowest + * significant bit in nanoseconds. + * Higher numbers provide better quality (more accurate color, less + * ghosting), but have a negative impact on the frame rate. + * Corresponding flag: --led-pwm-lsb-nanoseconds + */ + int pwm_lsb_nanoseconds; + + /* The lower bits can be time-dithered for higher refresh rate. + * Corresponding flag: --led-pwm-dither-bits + */ + int pwm_dither_bits; + + /* The initial brightness of the panel in percent. Valid range is 1..100 + * Corresponding flag: --led-brightness + */ + int brightness; + + /* Scan mode: 0=progressive, 1=interlaced + * Corresponding flag: --led-scan-mode + */ + int scan_mode; + + /* Default row address type is 0, corresponding to direct setting of the + * row, while row address type 1 is used for panels that only have A/B, + * typically some 64x64 panels + */ + int row_address_type; /* Corresponding flag: --led-row-addr-type */ + + /* Type of multiplexing. 0 = direct, 1 = stripe, 2 = checker (typical 1:8) + */ + int multiplexing; + + /* In case the internal sequence of mapping is not "RGB", this contains the + * real mapping. Some panels mix up these colors. + */ + const char *led_rgb_sequence; /* Corresponding flag: --led-rgb-sequence */ + + /* A string describing a sequence of pixel mappers that should be applied + * to this matrix. A semicolon-separated list of pixel-mappers with optional + * parameter. + */ + const char *pixel_mapper_config; /* Corresponding flag: --led-pixel-mapper */ + + /* + * Panel type. Typically just NULL, but certain panels (FM6126) require + * an initialization sequence + */ + const char *panel_type; /* Corresponding flag: --led-panel-type */ + + /** The following are boolean flags, all off by default **/ + + /* Allow to use the hardware subsystem to create pulses. This won't do + * anything if output enable is not connected to GPIO 18. + * Corresponding flag: --led-hardware-pulse + */ + char disable_hardware_pulsing; + char show_refresh_rate; /* Corresponding flag: --led-show-refresh */ + char inverse_colors; /* Corresponding flag: --led-inverse */ + + /* Limit refresh rate of LED panel. This will help on a loaded system + * to keep a constant refresh rate. <= 0 for no limit. + */ + int limit_refresh_rate_hz; /* Corresponding flag: --led-limit-refresh */ +}; + +/** + * Runtime options to simplify doing common things for many programs such as + * dropping privileges and becoming a daemon. + */ +struct RGBLedRuntimeOptions { + int gpio_slowdown; // 0 = no slowdown. Flag: --led-slowdown-gpio + + // ---------- + // If the following options are set to disabled with -1, they are not + // even offered via the command line flags. + // ---------- + + // Thre are three possible values here + // -1 : don't leave choise of becoming daemon to the command line parsing. + // If set to -1, the --led-daemon option is not offered. + // 0 : do not becoma a daemon, run in forgreound (default value) + // 1 : become a daemon, run in background. + // + // If daemon is disabled (= -1), the user has to call + // RGBMatrix::StartRefresh() manually once the matrix is created, to leave + // the decision to become a daemon + // after the call (which requires that no threads have been started yet). + // In the other cases (off or on), the choice is already made, so the thread + // is conveniently already started for you. + int daemon; // -1 disabled. 0=off, 1=on. Flag: --led-daemon + + // Drop privileges from 'root' to 'daemon' once the hardware is initialized. + // This is usually a good idea unless you need to stay on elevated privs. + int drop_privileges; // -1 disabled. 0=off, 1=on. flag: --led-drop-privs + + // By default, the gpio is initialized for you, but if you run on a platform + // not the Raspberry Pi, this will fail. If you don't need to access GPIO + // e.g. you want to just create a stream output (see content-streamer.h), + // set this to false. + bool do_gpio_init; +}; + +/** + * Universal way to create and initialize a matrix. + * The "options" struct (if not NULL) contains all default configuration values + * chosen by the programmer to create the matrix. + * + * If "argc" and "argv" are provided, this function also reads command line + * flags provided, that then can override any of the defaults given. + * The arguments that have been used from the command line are removed from + * the argv list (and argc is adjusted) - that way these don't mess with your + * own command line handling. + * + * The actual options used are filled back into the "options" struct if not + * NULL. + * + * Usage: + * ---------------- + * int main(int argc, char **argv) { + * struct RGBLedMatrixOptions options; + * memset(&options, 0, sizeof(options)); + * options.rows = 32; // You can set defaults if you want. + * options.chain_length = 1; + * struct RGBLedMatrix *matrix = led_matrix_create_from_options(&options, + * &argc, &argv); + * if (matrix == NULL) { + * led_matrix_print_flags(stderr); + * return 1; + * } + * // do additional commandline handling; then use matrix... + * } + * ---------------- + */ +struct RGBLedMatrix *led_matrix_create_from_options( + struct RGBLedMatrixOptions *options, int *argc, char ***argv); + +/* Same, but does not modify the argv array. */ +struct RGBLedMatrix *led_matrix_create_from_options_const_argv( + struct RGBLedMatrixOptions *options, int argc, char **argv); + +/** + * The way to completely initialize your matrix without using command line + * flags to initialize some things. + * + * The actual options used are filled back into the "options" and "rt_options" + * struct if not NULL. If they are null, the default value is used. + * + * Usage: + * ---------------- + * int main(int argc, char **argv) { + * struct RGBLedMatrixOptions options; + * struct RGBLedRuntimeOptions rt_options; + * memset(&options, 0, sizeof(options)); + * memset(&rt_options, 0, sizeof(rt_options)); + * options.rows = 32; // You can set defaults if you want. + * options.chain_length = 1; + * rt_options.gpio_slowdown = 4; + * struct RGBLedMatrix *matrix = led_matrix_create_from_options_and_rt_options(&options, &rt_options); + * if (matrix == NULL) { + * return 1; + * } + * // do additional commandline handling; then use matrix... + * } + * ---------------- + */ +struct RGBLedMatrix *led_matrix_create_from_options_and_rt_options( + struct RGBLedMatrixOptions *opts, struct RGBLedRuntimeOptions * rt_opts); + +/** + * Print available LED matrix options. + */ +void led_matrix_print_flags(FILE *out); + +/** + * Simple form of led_matrix_create_from_options() with just the few + * main options. Returns NULL if that was not possible. + * The "rows" are the number of rows supported by the display, so 32, 16 or 8. + * + * Number of "chained_display"s tells many of these are daisy-chained together + * (output of one connected to input of next). + * + * The "parallel_display" number determines if there is one or two displays + * connected in parallel to the GPIO port - this only works with newer + * Raspberry Pi that have 40 interface pins. + * + * This creates a realtime thread and requires root access to access the GPIO + * pins. + * So if you run this in a daemon, this should be called after becoming a + * daemon (as fork/exec stops threads) and before dropping privileges. + */ +struct RGBLedMatrix *led_matrix_create(int rows, int chained, int parallel); + + +/** + * Stop matrix and free memory. + * Always call before the end of the program to properly reset the hardware + */ +void led_matrix_delete(struct RGBLedMatrix *matrix); + + +/** + * Get active canvas from LED matrix for you to draw on. + * Ownership of returned pointer stays with the matrix, don't free(). + */ +struct LedCanvas *led_matrix_get_canvas(struct RGBLedMatrix *matrix); + +/** Return size of canvas. */ +void led_canvas_get_size(const struct LedCanvas *canvas, + int *width, int *height); + +/** Set pixel at (x, y) with color (r,g,b). */ +void led_canvas_set_pixel(struct LedCanvas *canvas, int x, int y, + uint8_t r, uint8_t g, uint8_t b); + +/** Clear screen (black). */ +void led_canvas_clear(struct LedCanvas *canvas); + +/** Fill matrix with given color. */ +void led_canvas_fill(struct LedCanvas *canvas, uint8_t r, uint8_t g, uint8_t b); + +/*** API to provide double-buffering. ***/ + +/** + * Create a new canvas to be used with led_matrix_swap_on_vsync() + * Ownership of returned pointer stays with the matrix, don't free(). + */ +struct LedCanvas *led_matrix_create_offscreen_canvas(struct RGBLedMatrix *matrix); + +/** + * Swap the given canvas (created with create_offscreen_canvas) with the + * currently active canvas on vsync (blocks until vsync is reached). + * Returns the previously active canvas. So with that, you can create double + * buffering: + * + * struct LedCanvas *offscreen = led_matrix_create_offscreen_canvas(...); + * led_canvas_set_pixel(offscreen, ...); // not shown until swap-on-vsync + * offscreen = led_matrix_swap_on_vsync(matrix, offscreen); + * // The returned buffer, assigned to offscreen, is now the inactive buffer + * // fill, then swap again. + */ +struct LedCanvas *led_matrix_swap_on_vsync(struct RGBLedMatrix *matrix, + struct LedCanvas *canvas); + +uint8_t led_matrix_get_brightness(struct RGBLedMatrix *matrix); +void led_matrix_set_brightness(struct RGBLedMatrix *matrix, uint8_t brightness); + +// Utility function: set an image from the given buffer containting pixels. +// +// Draw image of size "image_width" and "image_height" from pixel at +// canvas-offset "canvas_offset_x", "canvas_offset_y". Image will be shown +// cropped on the edges if needed. +// +// The canvas offset can be negative, i.e. the image start can be shifted +// outside the image frame on the left/top edge. +// +// The buffer needs to be organized as rows with columns of three bytes +// organized as rgb or bgr. Thus the size of the buffer needs to be exactly +// (3 * image_width * image_height) bytes. +// +// The "image_buffer" parameters contains the data, "buffer_size_bytes" the +// size in bytes. +// +// If "is_bgr" is 1, the buffer is treated as BGR pixel arrangement instead +// of RGB with is_bgr = 0. +void set_image(struct LedCanvas *c, int canvas_offset_x, int canvas_offset_y, + const uint8_t *image_buffer, size_t buffer_size_bytes, + int image_width, int image_height, + char is_bgr); + +// Load a font given a path to a font file containing a bdf font. +struct LedFont *load_font(const char *bdf_font_file); + +// Read the baseline of a font +int baseline_font(struct LedFont *font); + +// Read the height of a font +int height_font(struct LedFont *font); + +// Creates an outline font based on an existing font instance +struct LedFont *create_outline_font(struct LedFont *font); + +// Delete a font originally created from load_font. +void delete_font(struct LedFont *font); + +int draw_text(struct LedCanvas *c, struct LedFont *font, int x, int y, + uint8_t r, uint8_t g, uint8_t b, + const char *utf8_text, int kerning_offset); + +int vertical_draw_text(struct LedCanvas *c, struct LedFont *font, int x, int y, + uint8_t r, uint8_t g, uint8_t b, + const char *utf8_text, int kerning_offset); + +void draw_circle(struct LedCanvas *c, int x, int y, int radius, + uint8_t r, uint8_t g, uint8_t b); + +void draw_line(struct LedCanvas *c, int x0, int y0, int x1, int y1, + uint8_t r, uint8_t g, uint8_t b); + +#ifdef __cplusplus +} // extern C +#endif + +#endif diff --git a/include/pixel-mapper.h b/include/pixel-mapper.h new file mode 100644 index 0000000..6963d00 --- /dev/null +++ b/include/pixel-mapper.h @@ -0,0 +1,110 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2018 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +#ifndef RGBMATRIX_PIXEL_MAPPER +#define RGBMATRIX_PIXEL_MAPPER + +#include +#include + +namespace rgb_matrix { + +// A pixel mapper is a way for you to map pixels of LED matrixes to a different +// layout. If you have an implementation of a PixelMapper, you can give it +// to the RGBMatrix::ApplyPixelMapper(), which then presents you a canvas +// that has the new "visible_width", "visible_height". +class PixelMapper { +public: + virtual ~PixelMapper() {} + + // Get the name of this PixelMapper. Each PixelMapper needs to have a name + // so that it can be referred to with command line flags. + virtual const char *GetName() const = 0; + + // Pixel mappers receive the chain and parallel information and + // might receive optional user-parameters, e.g. from command line flags. + // + // This is a single string containing the parameters. + // You can be used from simple scalar parameters, such as the angle for + // the rotate transformer, or more complex parameters that describe a mapping + // of panels for instance. + // Keep it concise (as people will give parameters on the command line) and + // don't use semicolons in your string (as they are + // used to separate pixel mappers on the command line). + // + // For instance, the rotate transformer is invoked like this + // --led-pixel-mapper=rotate:90 + // And the parameter that is passed to SetParameter() is "90". + // + // Returns 'true' if parameter was parsed successfully. + virtual bool SetParameters(int chain, int parallel, + const char *parameter_string) { + return true; + } + + // Given a underlying matrix (width, height), returns the + // visible (width, height) after the mapping. + // E.g. a 90 degree rotation might map matrix=(64, 32) -> visible=(32, 64) + // Some multiplexing matrices will double the height and half the width. + // + // While not technically necessary, one would expect that the number of + // pixels stay the same, so + // matrix_width * matrix_height == (*visible_width) * (*visible_height); + // + // Returns boolean "true" if the mapping can be successfully done with this + // mapper. + virtual bool GetSizeMapping(int matrix_width, int matrix_height, + int *visible_width, int *visible_height) + const = 0; + + // Map where a visible pixel (x,y) is mapped to the underlying matrix (x,y). + // + // To be convienently stateless, the first parameters are the full + // matrix width and height. + // + // So for many multiplexing methods this means to map a panel to a double + // length and half height panel (32x16 -> 64x8). + // The logic_x, logic_y are output parameters and guaranteed not to be + // nullptr. + virtual void MapVisibleToMatrix(int matrix_width, int matrix_height, + int visible_x, int visible_y, + int *matrix_x, int *matrix_y) const = 0; +}; + +// This is a place to register PixelMappers globally. If you register your +// PixelMapper before calling RGBMatrix::CreateFromFlags(), the named +// PixelMapper is available in the --led-pixel-mapper options. +// +// Note, you don't _have_ to register your mapper, you can always call +// RGBMatrix::ApplyPixelMapper() directly. Registering is for convenience and +// commandline-flag support. +// +// There are a few standard mappers registered by default. +void RegisterPixelMapper(PixelMapper *mapper); + +// Get a list of the names of available pixel mappers. +std::vector GetAvailablePixelMappers(); + +// Given a name (e.g. "rotate") and a parameter (e.g. "90"), return the +// parametrized PixelMapper with that name. Returns NULL if mapper +// can not be found or parameter is invalid. +// Ownership of the returned object is _NOT_ transferred to the caller. +// Current available mappers are "U-mapper" and "Rotate". The "Rotate" +// gets a parameter denoting the angle. +const PixelMapper *FindPixelMapper(const char *name, + int chain, int parallel, + const char *parameter = NULL); +} // namespace rgb_matrix + +#endif // RGBMATRIX_PIXEL_MAPPER diff --git a/include/thread.h b/include/thread.h new file mode 100644 index 0000000..1f16795 --- /dev/null +++ b/include/thread.h @@ -0,0 +1,86 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2013 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +#ifndef RPI_THREAD_H +#define RPI_THREAD_H + +#include +#include + +namespace rgb_matrix { +// Simple thread abstraction. +class Thread { +public: + Thread(); + + // The destructor waits for Run() to return so make sure it does. + virtual ~Thread(); + + // Wait for the Run() method to return. + void WaitStopped(); + + // Start thread. If realtime_priority is > 0, then this will be a + // thread with SCHED_FIFO and the given priority. + // If cpu_affinity is set !=, chooses the given bitmask of CPUs + // this thread should have an affinity to. + // On a Raspberry Pi 1, this doesn't matter, as there is only one core, + // Raspberry Pi 2 can has 4 cores, so any combination of (1<<0) .. (1<<3) is + // valid. + virtual void Start(int realtime_priority = 0, uint32_t cpu_affinity_mask = 0); + + // Override this to do the work. + // + // This will be called in a thread once Start() has been called. You typically + // will have an endless loop doing stuff. + // + // It is a good idea to provide a way to communicate to the thread that + // it should stop (see ThreadedCanvasManipulator for an example) + virtual void Run() = 0; + +private: + static void *PthreadCallRun(void *tobject); + bool started_; + pthread_t thread_; +}; + +// Non-recursive Mutex. +class Mutex { +public: + Mutex() { pthread_mutex_init(&mutex_, NULL); } + ~Mutex() { pthread_mutex_destroy(&mutex_); } + void Lock() { pthread_mutex_lock(&mutex_); } + void Unlock() { pthread_mutex_unlock(&mutex_); } + + // Wait on condition. If "timeout_ms" is < 0, it waits forever, otherwise + // until timeout is reached. + // Returns 'true' if condition is met, 'false', if wait timed out. + bool WaitOn(pthread_cond_t *cond, long timeout_ms = -1); + +private: + pthread_mutex_t mutex_; +}; + +// Useful RAII wrapper around mutex. +class MutexLock { +public: + MutexLock(Mutex *m) : mutex_(m) { mutex_->Lock(); } + ~MutexLock() { mutex_->Unlock(); } +private: + Mutex *const mutex_; +}; + +} // end namespace rgb_matrix + +#endif // RPI_THREAD_H diff --git a/include/threaded-canvas-manipulator.h b/include/threaded-canvas-manipulator.h new file mode 100644 index 0000000..a7ca678 --- /dev/null +++ b/include/threaded-canvas-manipulator.h @@ -0,0 +1,103 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2014 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +// Utility base class for continuously updating the canvas. + +// Note: considering removing this, as real applications likely have something +// similar, but this might not be quite usable. +// Since it is just a few lines of code, it is probably better +// implemented in the application for readability. +// +// So for simplicity of the API, consider ThreadedCanvasManipulator deprecated. + +#ifndef RPI_THREADED_CANVAS_MANIPULATOR_H +#define RPI_THREADED_CANVAS_MANIPULATOR_H + +#include "thread.h" +#include "canvas.h" + +namespace rgb_matrix { +// +// Typically, your programs will crate a canvas and then updating the image +// in a loop. If you want to do stuff in parallel, then this utility class +// helps you doing that. Also a demo for how to use the Thread class. +// +// Extend it, then just implement Run(). Example: +/* + class MyCrazyDemo : public ThreadedCanvasManipulator { + public: + MyCrazyDemo(Canvas *canvas) : ThreadedCanvasManipulator(canvas) {} + virtual void Run() { + unsigned char c; + while (running()) { + // Calculate the next frame. + c++; + for (int x = 0; x < canvas()->width(); ++x) { + for (int y = 0; y < canvas()->height(); ++y) { + canvas()->SetPixel(x, y, c, c, c); + } + } + usleep(15 * 1000); + } + } + }; + + // Later, in your main method. + RGBMatrix *matrix = RGBMatrix::CreateFromOptions(...); + MyCrazyDemo *demo = new MyCrazyDemo(matrix); + demo->Start(); // Start doing things. + // This now runs in the background, you can do other things here, + // e.g. aquiring new data or simply wait. But for waiting, you wouldn't + // need a thread in the first place. + demo->Stop(); + delete demo; +*/ +class ThreadedCanvasManipulator : public Thread { +public: + ThreadedCanvasManipulator(Canvas *m) : running_(false), canvas_(m) {} + virtual ~ThreadedCanvasManipulator() { Stop(); } + + virtual void Start(int realtime_priority=0, uint32_t affinity_mask=0) { + { + MutexLock l(&mutex_); + running_ = true; + } + Thread::Start(realtime_priority, affinity_mask); + } + + // Stop the thread at the next possible time Run() checks the running_ flag. + void Stop() { + MutexLock l(&mutex_); + running_ = false; + } + + // Implement this and run while running() returns true. + virtual void Run() = 0; + +protected: + inline Canvas *canvas() { return canvas_; } + inline bool running() { + MutexLock l(&mutex_); + return running_; + } + +private: + Mutex mutex_; + bool running_; + Canvas *const canvas_; +}; +} // namespace rgb_matrix + +#endif // RPI_THREADED_CANVAS_MANIPULATOR_H diff --git a/led-matrix-c.h b/led-matrix-c.h new file mode 100644 index 0000000..d5989bb --- /dev/null +++ b/led-matrix-c.h @@ -0,0 +1,394 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- + * Copyright (C) 2013 Henner Zeller + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 2. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + * Controlling 16x32 or 32x32 RGB matrixes via GPIO. It allows daisy chaining + * of a string of these, and also connecting a parallel string on newer + * Raspberry Pis with more GPIO pins available. + * + * This is a C-binding (for the C++ library) to allow easy binding and + * integration with other languages. The symbols are exported in librgbmatrix.a + * and librgbmatrix.so. You still need to call the final link with + * + * See examples-api-use/c-example.c for a usage example. + * + */ +#ifndef RPI_RGBMATRIX_C_H +#define RPI_RGBMATRIX_C_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct RGBLedMatrix; +struct LedCanvas; +struct LedFont; + +/** + * Parameters to create a new matrix. + * + * To get the defaults, non-set values have to be initialized to zero, so you + * should zero out this struct before setting anything. + */ +struct RGBLedMatrixOptions { + /* + * Name of the hardware mapping used. If passed NULL here, the default + * is used. + */ + const char *hardware_mapping; + + /* The "rows" are the number of rows supported by the display, so 32 or 16. + * Default: 32. + * Corresponding flag: --led-rows + */ + int rows; + + /* The "cols" are the number of columns per panel. Typically something + * like 32, but also 64 is possible. Sometimes even 40. + * cols * chain_length is the total length of the display, so you can + * represent a 64 wide display as cols=32, chain=2 or cols=64, chain=1; + * same thing. + * Flag: --led-cols + */ + int cols; + + /* The chain_length is the number of displays daisy-chained together + * (output of one connected to input of next). Default: 1 + * Corresponding flag: --led-chain + */ + int chain_length; + + /* The number of parallel chains connected to the Pi; in old Pis with 26 + * GPIO pins, that is 1, in newer Pis with 40 interfaces pins, that can + * also be 2 or 3. The effective number of pixels in vertical direction is + * then thus rows * parallel. Default: 1 + * Corresponding flag: --led-parallel + */ + int parallel; + + /* Set PWM bits used for output. Default is 11, but if you only deal with + * limited comic-colors, 1 might be sufficient. Lower require less CPU and + * increases refresh-rate. + * Corresponding flag: --led-pwm-bits + */ + int pwm_bits; + + /* Change the base time-unit for the on-time in the lowest + * significant bit in nanoseconds. + * Higher numbers provide better quality (more accurate color, less + * ghosting), but have a negative impact on the frame rate. + * Corresponding flag: --led-pwm-lsb-nanoseconds + */ + int pwm_lsb_nanoseconds; + + /* The lower bits can be time-dithered for higher refresh rate. + * Corresponding flag: --led-pwm-dither-bits + */ + int pwm_dither_bits; + + /* The initial brightness of the panel in percent. Valid range is 1..100 + * Corresponding flag: --led-brightness + */ + int brightness; + + /* Scan mode: 0=progressive, 1=interlaced + * Corresponding flag: --led-scan-mode + */ + int scan_mode; + + /* Default row address type is 0, corresponding to direct setting of the + * row, while row address type 1 is used for panels that only have A/B, + * typically some 64x64 panels + */ + int row_address_type; /* Corresponding flag: --led-row-addr-type */ + + /* Type of multiplexing. 0 = direct, 1 = stripe, 2 = checker (typical 1:8) + */ + int multiplexing; + + /* In case the internal sequence of mapping is not "RGB", this contains the + * real mapping. Some panels mix up these colors. + */ + const char *led_rgb_sequence; /* Corresponding flag: --led-rgb-sequence */ + + /* A string describing a sequence of pixel mappers that should be applied + * to this matrix. A semicolon-separated list of pixel-mappers with optional + * parameter. + */ + const char *pixel_mapper_config; /* Corresponding flag: --led-pixel-mapper */ + + /* + * Panel type. Typically just NULL, but certain panels (FM6126) require + * an initialization sequence + */ + const char *panel_type; /* Corresponding flag: --led-panel-type */ + + /** The following are boolean flags, all off by default **/ + + /* Allow to use the hardware subsystem to create pulses. This won't do + * anything if output enable is not connected to GPIO 18. + * Corresponding flag: --led-hardware-pulse + */ + char disable_hardware_pulsing; + char show_refresh_rate; /* Corresponding flag: --led-show-refresh */ + char inverse_colors; /* Corresponding flag: --led-inverse */ + + /* Limit refresh rate of LED panel. This will help on a loaded system + * to keep a constant refresh rate. <= 0 for no limit. + */ + int limit_refresh_rate_hz; /* Corresponding flag: --led-limit-refresh */ +}; + +/** + * Runtime options to simplify doing common things for many programs such as + * dropping privileges and becoming a daemon. + */ +struct RGBLedRuntimeOptions { + int gpio_slowdown; // 0 = no slowdown. Flag: --led-slowdown-gpio + + // ---------- + // If the following options are set to disabled with -1, they are not + // even offered via the command line flags. + // ---------- + + // Thre are three possible values here + // -1 : don't leave choise of becoming daemon to the command line parsing. + // If set to -1, the --led-daemon option is not offered. + // 0 : do not becoma a daemon, run in forgreound (default value) + // 1 : become a daemon, run in background. + // + // If daemon is disabled (= -1), the user has to call + // RGBMatrix::StartRefresh() manually once the matrix is created, to leave + // the decision to become a daemon + // after the call (which requires that no threads have been started yet). + // In the other cases (off or on), the choice is already made, so the thread + // is conveniently already started for you. + int daemon; // -1 disabled. 0=off, 1=on. Flag: --led-daemon + + // Drop privileges from 'root' to 'daemon' once the hardware is initialized. + // This is usually a good idea unless you need to stay on elevated privs. + int drop_privileges; // -1 disabled. 0=off, 1=on. flag: --led-drop-privs + + // By default, the gpio is initialized for you, but if you run on a platform + // not the Raspberry Pi, this will fail. If you don't need to access GPIO + // e.g. you want to just create a stream output (see content-streamer.h), + // set this to false. + bool do_gpio_init; +}; + +/** + * Universal way to create and initialize a matrix. + * The "options" struct (if not NULL) contains all default configuration values + * chosen by the programmer to create the matrix. + * + * If "argc" and "argv" are provided, this function also reads command line + * flags provided, that then can override any of the defaults given. + * The arguments that have been used from the command line are removed from + * the argv list (and argc is adjusted) - that way these don't mess with your + * own command line handling. + * + * The actual options used are filled back into the "options" struct if not + * NULL. + * + * Usage: + * ---------------- + * int main(int argc, char **argv) { + * struct RGBLedMatrixOptions options; + * memset(&options, 0, sizeof(options)); + * options.rows = 32; // You can set defaults if you want. + * options.chain_length = 1; + * struct RGBLedMatrix *matrix = led_matrix_create_from_options(&options, + * &argc, &argv); + * if (matrix == NULL) { + * led_matrix_print_flags(stderr); + * return 1; + * } + * // do additional commandline handling; then use matrix... + * } + * ---------------- + */ +struct RGBLedMatrix *led_matrix_create_from_options( + struct RGBLedMatrixOptions *options, int *argc, char ***argv); + +/* Same, but does not modify the argv array. */ +struct RGBLedMatrix *led_matrix_create_from_options_const_argv( + struct RGBLedMatrixOptions *options, int argc, char **argv); + +/** + * The way to completely initialize your matrix without using command line + * flags to initialize some things. + * + * The actual options used are filled back into the "options" and "rt_options" + * struct if not NULL. If they are null, the default value is used. + * + * Usage: + * ---------------- + * int main(int argc, char **argv) { + * struct RGBLedMatrixOptions options; + * struct RGBLedRuntimeOptions rt_options; + * memset(&options, 0, sizeof(options)); + * memset(&rt_options, 0, sizeof(rt_options)); + * options.rows = 32; // You can set defaults if you want. + * options.chain_length = 1; + * rt_options.gpio_slowdown = 4; + * struct RGBLedMatrix *matrix = led_matrix_create_from_options_and_rt_options(&options, &rt_options); + * if (matrix == NULL) { + * return 1; + * } + * // do additional commandline handling; then use matrix... + * } + * ---------------- + */ +struct RGBLedMatrix *led_matrix_create_from_options_and_rt_options( + struct RGBLedMatrixOptions *opts, struct RGBLedRuntimeOptions * rt_opts); + +/** + * Print available LED matrix options. + */ +void led_matrix_print_flags(FILE *out); + +/** + * Simple form of led_matrix_create_from_options() with just the few + * main options. Returns NULL if that was not possible. + * The "rows" are the number of rows supported by the display, so 32, 16 or 8. + * + * Number of "chained_display"s tells many of these are daisy-chained together + * (output of one connected to input of next). + * + * The "parallel_display" number determines if there is one or two displays + * connected in parallel to the GPIO port - this only works with newer + * Raspberry Pi that have 40 interface pins. + * + * This creates a realtime thread and requires root access to access the GPIO + * pins. + * So if you run this in a daemon, this should be called after becoming a + * daemon (as fork/exec stops threads) and before dropping privileges. + */ +struct RGBLedMatrix *led_matrix_create(int rows, int chained, int parallel); + + +/** + * Stop matrix and free memory. + * Always call before the end of the program to properly reset the hardware + */ +void led_matrix_delete(struct RGBLedMatrix *matrix); + + +/** + * Get active canvas from LED matrix for you to draw on. + * Ownership of returned pointer stays with the matrix, don't free(). + */ +struct LedCanvas *led_matrix_get_canvas(struct RGBLedMatrix *matrix); + +/** Return size of canvas. */ +void led_canvas_get_size(const struct LedCanvas *canvas, + int *width, int *height); + +/** Set pixel at (x, y) with color (r,g,b). */ +void led_canvas_set_pixel(struct LedCanvas *canvas, int x, int y, + uint8_t r, uint8_t g, uint8_t b); + +/** Clear screen (black). */ +void led_canvas_clear(struct LedCanvas *canvas); + +/** Fill matrix with given color. */ +void led_canvas_fill(struct LedCanvas *canvas, uint8_t r, uint8_t g, uint8_t b); + +/*** API to provide double-buffering. ***/ + +/** + * Create a new canvas to be used with led_matrix_swap_on_vsync() + * Ownership of returned pointer stays with the matrix, don't free(). + */ +struct LedCanvas *led_matrix_create_offscreen_canvas(struct RGBLedMatrix *matrix); + +/** + * Swap the given canvas (created with create_offscreen_canvas) with the + * currently active canvas on vsync (blocks until vsync is reached). + * Returns the previously active canvas. So with that, you can create double + * buffering: + * + * struct LedCanvas *offscreen = led_matrix_create_offscreen_canvas(...); + * led_canvas_set_pixel(offscreen, ...); // not shown until swap-on-vsync + * offscreen = led_matrix_swap_on_vsync(matrix, offscreen); + * // The returned buffer, assigned to offscreen, is now the inactive buffer + * // fill, then swap again. + */ +struct LedCanvas *led_matrix_swap_on_vsync(struct RGBLedMatrix *matrix, + struct LedCanvas *canvas); + +uint8_t led_matrix_get_brightness(struct RGBLedMatrix *matrix); +void led_matrix_set_brightness(struct RGBLedMatrix *matrix, uint8_t brightness); + +// Utility function: set an image from the given buffer containting pixels. +// +// Draw image of size "image_width" and "image_height" from pixel at +// canvas-offset "canvas_offset_x", "canvas_offset_y". Image will be shown +// cropped on the edges if needed. +// +// The canvas offset can be negative, i.e. the image start can be shifted +// outside the image frame on the left/top edge. +// +// The buffer needs to be organized as rows with columns of three bytes +// organized as rgb or bgr. Thus the size of the buffer needs to be exactly +// (3 * image_width * image_height) bytes. +// +// The "image_buffer" parameters contains the data, "buffer_size_bytes" the +// size in bytes. +// +// If "is_bgr" is 1, the buffer is treated as BGR pixel arrangement instead +// of RGB with is_bgr = 0. +void set_image(struct LedCanvas *c, int canvas_offset_x, int canvas_offset_y, + const uint8_t *image_buffer, size_t buffer_size_bytes, + int image_width, int image_height, + char is_bgr); + +// Load a font given a path to a font file containing a bdf font. +struct LedFont *load_font(const char *bdf_font_file); + +// Read the baseline of a font +int baseline_font(struct LedFont *font); + +// Read the height of a font +int height_font(struct LedFont *font); + +// Creates an outline font based on an existing font instance +struct LedFont *create_outline_font(struct LedFont *font); + +// Delete a font originally created from load_font. +void delete_font(struct LedFont *font); + +int draw_text(struct LedCanvas *c, struct LedFont *font, int x, int y, + uint8_t r, uint8_t g, uint8_t b, + const char *utf8_text, int kerning_offset); + +int vertical_draw_text(struct LedCanvas *c, struct LedFont *font, int x, int y, + uint8_t r, uint8_t g, uint8_t b, + const char *utf8_text, int kerning_offset); + +void draw_circle(struct LedCanvas *c, int x, int y, int radius, + uint8_t r, uint8_t g, uint8_t b); + +void draw_line(struct LedCanvas *c, int x0, int y0, int x1, int y1, + uint8_t r, uint8_t g, uint8_t b); + +#ifdef __cplusplus +} // extern C +#endif + +#endif diff --git a/led-matrix.h b/led-matrix.h new file mode 100644 index 0000000..086fadd --- /dev/null +++ b/led-matrix.h @@ -0,0 +1,504 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2013 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +// Controlling 16x32 or 32x32 RGB matrixes via GPIO. It allows daisy chaining +// of a string of these, and also connecting a parallel string on newer +// Raspberry Pis with more GPIO pins available. + +#ifndef RPI_RGBMATRIX_H +#define RPI_RGBMATRIX_H + +#include +#include + +#include +#include + +#include "canvas.h" +#include "thread.h" +#include "pixel-mapper.h" + +namespace rgb_matrix { +class RGBMatrix; +class FrameCanvas; // Canvas for Double- and Multibuffering +struct RuntimeOptions; + +// The RGB matrix provides the framebuffer and the facilities to constantly +// update the LED matrix. +// +// This implement the Canvas interface that represents the display with +// (led_cols * chained_displays)x(rows * parallel_displays) pixels. +// +// If can do multi-buffering using the CreateFrameCanvas() and SwapOnVSync() +// methods. This is useful for animations and to prevent tearing. +// +// If you arrange the panels in a different way in the physical space, write +// a CanvasTransformer that does coordinate remapping and which should be added +// to the transformers, like with UArrangementTransformer in demo-main.cc. +class RGBMatrix : public Canvas { +public: + // Options to initialize the RGBMatrix. Also see the main README.md for + // detailed descriptions of the command line flags. + struct Options { + Options(); // Creates a default option set. + + // Validate the options and possibly output a message to string. If + // "err" is NULL, outputs validation problems to stderr. + // Returns 'true' if all options look good. + bool Validate(std::string *err) const; + + // Name of the hardware mapping. Something like "regular" or "adafruit-hat" + const char *hardware_mapping; + + // The "rows" are the number + // of rows supported by the display, so 32 or 16. Default: 32. + // Flag: --led-rows + int rows; + + // The "cols" are the number of columns per panel. Typically something + // like 32, but also 64 is possible. Sometimes even 40. + // cols * chain_length is the total length of the display, so you can + // represent a 64 wide display as cols=32, chain=2 or cols=64, chain=1; + // same thing, but more convenient to think of. + // Flag: --led-cols + int cols; + + // The chain_length is the number of displays daisy-chained together + // (output of one connected to input of next). Default: 1 + // Flag: --led-chain + int chain_length; + + // The number of parallel chains connected to the Pi; in old Pis with 26 + // GPIO pins, that is 1, in newer Pis with 40 interfaces pins, that can + // also be 2 or 3. The effective number of pixels in vertical direction is + // then thus rows * parallel. Default: 1 + // Flag: --led-parallel + int parallel; + + // Set PWM bits used for output. Default is 11, but if you only deal with + // limited comic-colors, 1 might be sufficient. Lower require less CPU and + // increases refresh-rate. + // Flag: --led-pwm-bits + int pwm_bits; + + // Change the base time-unit for the on-time in the lowest + // significant bit in nanoseconds. + // Higher numbers provide better quality (more accurate color, less + // ghosting), but have a negative impact on the frame rate. + // Flag: --led-pwm-lsb-nanoseconds + int pwm_lsb_nanoseconds; + + // The lower bits can be time-dithered for higher refresh rate. + // Flag: --led-pwm-dither-bits + int pwm_dither_bits; + + // The initial brightness of the panel in percent. Valid range is 1..100 + // Default: 100 + // Flag: --led-brightness + int brightness; + + // Scan mode: 0=progressive, 1=interlaced. + // Flag: --led-scan-mode + int scan_mode; + + // Default row address type is 0, corresponding to direct setting of the + // row, while row address type 1 is used for panels that only have A/B, + // typically some 64x64 panels + int row_address_type; // Flag --led-row-addr-type + + // Type of multiplexing. 0 = direct, 1 = stripe, 2 = checker,... + // Flag: --led-multiplexing + int multiplexing; + + // Disable the PWM hardware subsystem to create pulses. + // Typically, you don't want to disable hardware pulsing, this is mostly + // for debugging and figuring out if there is interference with the + // sound system. + // This won't do anything if output enable is not connected to GPIO 18 in + // non-standard wirings. + bool disable_hardware_pulsing; // Flag: --led-hardware-pulse + + // Show refresh rate on the terminal for debugging and tweaking purposes. + bool show_refresh_rate; // Flag: --led-show-refresh + + // Some panels have inversed colors. + bool inverse_colors; // Flag: --led-inverse + + // In case the internal sequence of mapping is not "RGB", this contains the + // real mapping. Some panels mix up these colors. String of length three + // which has to contain all characters R, G and B. + const char *led_rgb_sequence; // Flag: --led-rgb-sequence + + // A string describing a sequence of pixel mappers that should be applied + // to this matrix. A semicolon-separated list of pixel-mappers with optional + // parameter. + const char *pixel_mapper_config; // Flag: --led-pixel-mapper + + // Panel type. Typically an empty string or NULL, but some panels need + // a particular initialization sequence, so this is used for that. + // This can be e.g. "FM6126A" for that particular panel type. + const char *panel_type; // Flag: --led-panel-type + + // Limit refresh rate of LED panel. This will help on a loaded system + // to keep a constant refresh rate. <= 0 for no limit. + int limit_refresh_rate_hz; // Flag: --led-limit-refresh + }; + + // Factory to create a matrix. Additional functionality includes dropping + // privileges and becoming a daemon. + // Returns NULL, if there was a problem (a message then is written to stderr). + static RGBMatrix *CreateFromOptions(const Options &options, + const RuntimeOptions &runtime_options); + + // A factory that parses your main() commandline flags to read options + // meant to configure the the matrix and returns a freshly allocated matrix. + // + // Optionally, you can pass in option structs with a couple of defaults + // which are used unless overwritten on the command line. + // A matrix is created and returned; also the options structs are + // updated to reflect the values that were used and set on the command line. + // + // If you allow the user to start a daemon with --led-daemon, make sure to + // call this function before you have started any threads, so early on in + // main() (see RuntimeOptions documentation). + // + // Note, the permissions are dropped by default from 'root' to 'daemon', so + // if you are required to stay root after this, disable this option in + // the default RuntimeOptions (set drop_privileges = -1). + // Returns NULL, if there was a problem (a message then is written to stderr). + static RGBMatrix *CreateFromFlags(int *argc, char ***argv, + RGBMatrix::Options *default_options = NULL, + RuntimeOptions *default_runtime_opts = NULL, + bool remove_consumed_flags = true); + + // Stop matrix, delete all resources. + virtual ~RGBMatrix(); + + // -- Canvas interface. These write to the active FrameCanvas + // (see documentation in canvas.h) + // + // Since this is updating the canvas that is currently displayed, this + // might result in tearing. + // Prefer using a FrameCanvas and do double-buffering, see section below. + virtual int width() const; + virtual int height() const; + virtual void SetPixel(int x, int y, + uint8_t red, uint8_t green, uint8_t blue); + virtual void Clear(); + virtual void Fill(uint8_t red, uint8_t green, uint8_t blue); + + // -- Double- and Multibuffering. + + // Create a new buffer to be used for multi-buffering. The returned new + // Buffer implements a Canvas with the same size of thie RGBMatrix. + // You can use it to draw off-screen on it, then swap it with the active + // buffer using SwapOnVSync(). That would be classic double-buffering. + // + // You can also create as many FrameCanvas as you like and for instance use + // them to pre-fill scenes of an animation for fast playback later. + // + // The ownership of the created Canvases remains with the RGBMatrix, so you + // don't have to worry about deleting them (but you also don't want to create + // more than needed as this will fill up your memory as they are only deleted + // when the RGBMatrix is deleted). + FrameCanvas *CreateFrameCanvas(); + + // This method waits to the next VSync and swaps the active buffer with the + // supplied buffer. The formerly active buffer is returned. + // + // If you pass in NULL, the active buffer is returned, but it won't be + // replaced with NULL. You can use the NULL-behavior to just wait on + // VSync or to retrieve the initial buffer when preparing a multi-buffer + // animation. + // + // The optional "framerate_fraction" parameter allows to choose which + // multiple of the global frame-count to use. So it slows down your animation + // to an exact integer fraction of the refresh rate. + // Default is 1, so immediately next available frame. + // (Say you have 140Hz refresh rate, then a value of 5 would give you an + // 28Hz animation, nicely locked to the refresh-rate). + // If you combine this with Options::limit_refresh_rate_hz you can create + // time-correct animations. + FrameCanvas *SwapOnVSync(FrameCanvas *other, unsigned framerate_fraction = 1); + + // -- Setting shape and behavior of matrix. + + // Apply a pixel mapper. This is used to re-map pixels according to some + // scheme implemented by the PixelMapper. Does _not_ take ownership of the + // mapper. Mapper can be NULL, in which case nothing happens. + // Returns a boolean indicating if this was successful. + bool ApplyPixelMapper(const PixelMapper *mapper); + + // Note, there used to be ApplyStaticTransformer(), which has been deprecated + // since 2018 and changed to a compile-time option, then finally removed + // in 2020. Use PixelMapper instead, which is simpler and more intuitive. + + // Set PWM bits used for output. Default is 11, but if you only deal with + // limited comic-colors, 1 might be sufficient. Lower require less CPU and + // increases refresh-rate. + // + // Returns boolean to signify if value was within range. + // + // This sets the PWM bits for the current active FrameCanvas and future + // ones that are created with CreateFrameCanvas(). + bool SetPWMBits(uint8_t value); + uint8_t pwmbits(); // return the pwm-bits of the currently active buffer. + + // Map brightness of output linearly to input with CIE1931 profile. + void set_luminance_correct(bool on); + bool luminance_correct() const; + + // Set brightness in percent for all created FrameCanvas. 1%..100%. + // This will only affect newly set pixels. + void SetBrightness(uint8_t brightness); + uint8_t brightness(); + + //-- GPIO interaction. + // This library uses the GPIO pins to drive the matrix; this is a safe way + // to request the 'remaining' bits to be used for user purposes. + + // Request user readable GPIO bits. + // This function allows you to request pins you'd like to read with + // AwaitInputChange(). + // Only bits that are not already in use for reading or wrtiting + // by the matrix are allowed. + // Input is a bitmap of all the GPIO bits you're interested in; returns all + // the bits that are actually available. + uint64_t RequestInputs(uint64_t all_interested_bits); + + // This function will return whenever the GPIO input pins + // change (pins that are not already in use for output, that is) or the + // timeout is reached. You need to have reserved the inputs with + // matrix->RequestInputs(...) first (e.g. + // matrix->RequestInputs((1<<25)|(1<<24)); + // + // A positive timeout waits the given amount of milliseconds for a change + // (e.g. a button-press) to occur; if there is no change, it will just + // return the last value. + // If you just want to know how the pins are right now, call with zero + // timeout. + // A negative number waits forever and will only return if there is a change. + // + // This function only samples between display refreshes so polling some + // input does not generate flicker and provide a convenient change interface. + // + // Returns the bitmap of all GPIO input pins. + uint64_t AwaitInputChange(int timeout_ms); + + // Request user writable GPIO bits. + // This allows to request a bitmap of GPIO-bits to be used by the user for + // writing. + // Only bits that are not already in use for reading or wrtiting + // by the matrix are allowed. + // Returns the subset bits that are _actually_ available, + uint64_t RequestOutputs(uint64_t output_bits); + + // Set the user-settable bits according to output bits. + void OutputGPIO(uint64_t output_bits); + + // Legacy way to set gpio pins. We're not doing this anymore but need to + // be source-compatible with old calls of the form + // matrix->gpio()->RequestInputs(...) + // + // Don't use, use AwaitInputChange() directly. + RGBMatrix *gpio() __attribute__((deprecated)) { return this; } + + //-- Rarely needed + // Start the refresh thread. + // This is only needed if you chose RuntimeOptions::daemon = -1 (see below), + // otherwise the refresh thread is already started. + bool StartRefresh(); + +private: + class Impl; + + RGBMatrix(Impl *impl) : impl_(impl) {} + Impl *const impl_; +}; + +namespace internal { +class Framebuffer; +} + +class FrameCanvas : public Canvas { +public: + // Set PWM bits used for this Frame. + // Simple comic-colors, 1 might be sufficient (111 RGB, i.e. 8 colors). + // Lower require less CPU. + // Returns boolean to signify if value was within range. + bool SetPWMBits(uint8_t value); + uint8_t pwmbits(); + + // Map brightness of output linearly to input with CIE1931 profile. + void set_luminance_correct(bool on); + bool luminance_correct() const; + + void SetBrightness(uint8_t brightness); + uint8_t brightness(); + + //-- Serialize()/Deserialize() are fast ways to store and re-create a canvas. + + // Provides a pointer to a buffer of the internal representation to + // be copied out for later Deserialize(). + // + // Returns a "data" pointer and the data "len" in the given out-paramters; + // the content can be copied from there by the caller. + // + // Note, the content is not simply RGB, it is the opaque and platform + // specific representation which allows to make deserialization very fast. + // It is also bigger than just RGB; if you want to store it somewhere, + // using compression is a good idea. + void Serialize(const char **data, size_t *len) const; + + // Load data previously stored with Serialize(). Needs to be restored into + // a FrameCanvas with exactly the same settings (rows, chain, transformer,...) + // as serialized. + // Returns 'false' if size is unexpected. + // This method should only be called if FrameCanvas is off-screen. + bool Deserialize(const char *data, size_t len); + + // Copy content from other FrameCanvas owned by the same RGBMatrix. + void CopyFrom(const FrameCanvas &other); + + // -- Canvas interface. + virtual int width() const; + virtual int height() const; + virtual void SetPixel(int x, int y, + uint8_t red, uint8_t green, uint8_t blue); + virtual void Clear(); + virtual void Fill(uint8_t red, uint8_t green, uint8_t blue); + +private: + friend class RGBMatrix; + + FrameCanvas(internal::Framebuffer *frame) : frame_(frame){} + virtual ~FrameCanvas(); // Any FrameCanvas is owned by RGBMatrix. + internal::Framebuffer *framebuffer() { return frame_; } + + internal::Framebuffer *const frame_; +}; + +// Runtime options to simplify doing common things for many programs such as +// dropping privileges and becoming a daemon. +struct RuntimeOptions { + RuntimeOptions(); + + int gpio_slowdown; // 0 = no slowdown. Flag: --led-slowdown-gpio + + // ---------- + // If the following options are set to disabled with -1, they are not + // even offered via the command line flags. + // ---------- + + // Thre are three possible values here + // -1 : don't leave choise of becoming daemon to the command line parsing. + // If set to -1, the --led-daemon option is not offered. + // 0 : do not becoma a daemon, run in forgreound (default value) + // 1 : become a daemon, run in background. + // + // If daemon is disabled (= -1), the user has to call + // RGBMatrix::StartRefresh() manually once the matrix is created, to leave + // the decision to become a daemon + // after the call (which requires that no threads have been started yet). + // In the other cases (off or on), the choice is already made, so the thread + // is conveniently already started for you. + int daemon; // -1 disabled. 0=off, 1=on. Flag: --led-daemon + + // Drop privileges from 'root' to 'daemon' once the hardware is initialized. + // This is usually a good idea unless you need to stay on elevated privs. + int drop_privileges; // -1 disabled. 0=off, 1=on. flag: --led-drop-privs + + // By default, the gpio is initialized for you, but if you run on a platform + // not the Raspberry Pi, this will fail. If you don't need to access GPIO + // e.g. you want to just create a stream output (see content-streamer.h), + // set this to false. + bool do_gpio_init; +}; + +// Convenience utility functions to read standard rgb-matrix flags and create +// a RGBMatrix. Commandline flags are something like --led-rows, --led-chain, +// --led-parallel. See output of PrintMatrixFlags() for all available options +// and detailed description in +// https://github.com/hzeller/rpi-rgb-led-matrix#changing-parameters-via-command-line-flags +// +// Example use: +/* +using rgb_matrix::RGBMatrix; +int main(int argc, char **argv) { + RGBMatrix::Options led_options; + rgb_matrix::RuntimeOptions runtime; + + // Set defaults + led_options.chain_length = 3; + led_options.show_refresh_rate = true; + runtime.drop_privileges = 1; + if (!rgb_matrix::ParseOptionsFromFlags(&argc, &argv, &led_options, &runtime)) { + rgb_matrix::PrintMatrixFlags(stderr); + return 1; + } + + // Do your own command line handling with the remaining flags. + while (getopt()) {...} + + // Looks like we're ready to start + RGBMatrix *matrix = RGBMatrix::CreateFromOptions(led_options, runtime); + if (matrix == NULL) { + return 1; + } + + // .. now use matrix + + delete matrix; // Make sure to delete it in the end to switch off LEDs. + return 0; +} +*/ +// This parses the flags from argv and updates the structs with the parsed-out +// values. Structs can be NULL if you are not interested in it. +// +// The recongized flags are removed from argv if "remove_consumed_flags" is +// true; this simplifies your command line processing for the remaining options. +// +// Returns 'true' on success, 'false' if there was flag parsing problem. +bool ParseOptionsFromFlags(int *argc, char ***argv, + RGBMatrix::Options *default_options, + RuntimeOptions *rt_options, + bool remove_consumed_flags = true); + +// Show all the available options in a style that can be used in a --help +// output on the command line. +void PrintMatrixFlags(FILE *out, + const RGBMatrix::Options &defaults = RGBMatrix::Options(), + const RuntimeOptions &rt_opt = RuntimeOptions()); + +// Legacy version of RGBMatrix::CreateFromOptions() +inline RGBMatrix *CreateMatrixFromOptions( + const RGBMatrix::Options &options, + const RuntimeOptions &runtime_options) { + return RGBMatrix::CreateFromOptions(options, runtime_options); +} + +// Legacy version of RGBMatrix::CreateFromFlags() +inline RGBMatrix *CreateMatrixFromFlags( + int *argc, char ***argv, + RGBMatrix::Options *default_options = NULL, + RuntimeOptions *default_runtime_opts = NULL, + bool remove_consumed_flags = true) { + return RGBMatrix::CreateFromFlags(argc, argv, + default_options, default_runtime_opts, + remove_consumed_flags); +} + +} // end namespace rgb_matrix +#endif // RPI_RGBMATRIX_H diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 0000000..80c47cb --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1,3 @@ +compiler-flags +librgbmatrix.a +librgbmatrix.so.1 diff --git a/lib/Makefile b/lib/Makefile new file mode 100644 index 0000000..3359363 --- /dev/null +++ b/lib/Makefile @@ -0,0 +1,196 @@ +# Creating RGB matrix library +# When you link this library with your binary, you need to add -lrt -lm -lpthread +# So +# -lrgbmatrix +## +OBJECTS=gpio.o led-matrix.o options-initialize.o framebuffer.o \ + thread.o bdf-font.o graphics.o led-matrix-c.o hardware-mapping.o \ + pixel-mapper.o multiplex-mappers.o \ + content-streamer.o + +TARGET=librgbmatrix + +### +# After you change any of the following DEFINES, make sure to 'make' again. +# +# ########### NOTE ########### +# all of these options can now can be set programmatically and +# via command line flags as well. No real need to change them in the Makefile. +# (So be prepared for these to be removed at some point) +### + +# There are several different pinouts for various breakout boards that uses +# this library. If you are using the described pinout in the toplevel README.md +# or the standard active-3 breakout board, then 'regular' is the one you'd like +# to use. +# +# Adafruit also made a breakout board, if you want to use that, choose +# 'adafruit-hat' +# +# These are the choices +# regular # Following this project wiring and using these PCBs +# adafruit-hat # If you have a RGB matrix HAT from Adafruit +# adafruit-hat-pwm # If you have an Adafruit HAT with PWM hardware mod. +# regular-pi1 # If you have an old Pi1 and regular didn't work. +# classic # (deprecated) Classic Pi1/2/. Not used anymore. +# classic-pi1 # (deprecated) Classic pinout on Rasperry Pi 1 +HARDWARE_DESC?=regular + +# If you see that your display is inverse, you might have a matrix variant +# has uses inverse logic for the RGB bits. In that case: uncomment this. +# Flag: --led-inverse +#DEFINES+=-DINVERSE_RGB_DISPLAY_COLORS + +# For curiosity reasons and while tweaking values for LSB_PWM_NANOSECONDS, +# uncomment to see refresh rate in terminal. +# Flag: --led-show-refresh +#DEFINES+=-DSHOW_REFRESH_RATE + +# For low refresh rates below 100Hz (e.g. a lot of panels), the eye will notice +# some flicker. With this option enabled, the refreshed lines are interleaved, +# so it is less noticeable. But looks less pleasant with fast eye movements. +# Flag: --led-scan-mode=1 +#DEFINES+=-DRGB_SCAN_INTERLACED=1 + +# The signal can be too fast for some LED panels, in particular with newer +# (faster) Raspberry Pi 2s - in that case, the LED matrix only shows garbage. +# This allows to slow down the GPIO for these cases. +# +# Set to 1 for RPi2 or RPi3 (default below), because they are typically +# faster than the panels can digest. +# +# Set to 0 (or comment out) for RPi1, that are slow enough. +# +# Sometimes, you even have to give RGB_SLOWDOWN_GPIO=2 or even 3 for +# particularly slow panels or bad signal cable situations. If that happens, you +# typically should double check cables and add TTL level converter if you +# haven't. +# Flag: --led-slowdown-gpio +#DEFINES+=-DRGB_SLOWDOWN_GPIO=1 + +# This allows to change the base time-unit for the on-time in the lowest +# significant bit in nanoseconds. +# Higher numbers provide better quality (more accurate color, less ghosting), +# but have a negative impact on the frame rate. +# +# For the same frame-rate, displays with higher multiplexing (e.g. 1:16 or 1:32) +# require lower values. +# +# Good values for full-color display (PWM=11) are somewhere between 100 and 300. +# +# If you you use reduced bit color (e.g. PWM=1 for 8 colors like for text), +# then higher values might be good to minimize ghosting (and you can afford +# that, because lower PWM values result in higher frame-rates). +# +# How to decide ? Just leave the default if things are fine. If you see +# ghosting in high-contrast applications (e.g. text), increase the value. +# If you want to tweak, watch the framerate (-DSHOW_FRAME_RATE) while playing +# with this number and the PWM values. +# Flag: --led-pwm-lsb-nanoseconds +#DEFINES+=-DLSB_PWM_NANOSECONDS=130 + +# This is to debug problems with the hardware pulse generation. The PWM hardware +# module is also used by Raspberry Pi sound system, so there might be +# interference. Note, you typically don't want the hardware pulses disabled, as +# the image will have visible brightness glitches; but for debugging, this is +# a good choice. +# Flag: --led-no-hardware-pulses +#DEFINES+=-DDISABLE_HARDWARE_PULSES + +# This allows to fix the refresh rate to a particular refresh time in +# microseconds. +# +# This can be used to mitigate some situations in which you have a rare +# faint flicker, which can happen due to hardware events (network access) +# or other situations such as other IO or heavy memory access by other +# processes (all of which seem to break the isolation we request from the +# kernel. You did set isolcpus=3 right ?) +# You trade a slightly slower refresh rate and display brightness for less +# visible flicker situations. +# +# For this to calibrate, run your program for a while with --led-show-refresh +# and watch the line that shows the refresh time and the maximum microseconds +# for a frame observed. The maximum number is updated whenever the frame +# refresh take a little bit longer. So wait a while until that value doesn't +# change anymore (at least a minute, so that you catch tasks that happen once +# a minute). Some value might read e.g. +# 204.6Hz max: 5133usec +# Now take this maximum value you see there (here: 5133) and put in +# this define (don't forget to remove the # in front). +# +# The refresh rate will now be adapted to always have this amount of time +# between frames, so faster refreshes will be slowed down, but the occasional +# delayed frame will fit into the time-window as well, thus reducing visible +# brightness fluctuations. +# +# You can play with value a little and reduce until you find a good balance +# between refresh rate (which is reduce the higher this value is) and +# flicker suppression (which is better with higher values). +# Flag: --led-limit-refresh +#DEFINES+=-DFIXED_FRAME_MICROSECONDS=5000 + +# Enable wide 64 bit GPIO offered with the compute module. +# This will use more memory to internally represent the frame buffer, so +# caches can't be utilized as much. +# So only switch this on if you really use the compute module and use more +# than 3 parallel chains. +# (this is untested right now, waiting for hardware to arrive for testing) +#DEFINES+=-DENABLE_WIDE_GPIO_COMPUTE_MODULE + +# ---- Pinout options for hardware variants; usually no change needed here ---- + +# Uncomment if you want to use the Adafruit HAT with stable PWM timings. +# The newer version of this library allows for much more stable (less flicker) +# output, but it does not work with the Adafruit HAT unless you do a +# simple hardware hack on them: +# connect GPIO 4 (old OE) with 18 (the new OE); there are +# convenient solder holes labeled 4 and 18 on the Adafruit HAT, pretty +# close together. +# Then you can set the flag --led-gpio-mapping=adafruit-hat-pwm +# .. or uncomment the following line. +#HARDWARE_DESC=adafruit-hat-pwm + +# Typically, a Hub75 panel is split in two half displays, so that a 1:16 +# multiplexing actually multiplexes over two half displays and gives 32 lines. +# There are some other displays out there that you might experiment with +# that are internally wired to only have one sub-panel. In that case you might +# want to try this define to get a more reasonable canvas mapping. +# This option is typically _not_ needed, only use when you attempt to connect +# some oddball old (typically one-colored) display, such as Hub12. +#DEFINES+=-DONLY_SINGLE_SUB_PANEL + +# If someone gives additional values on the make commandline e.g. +# make USER_DEFINES="-DSHOW_REFRESH_RATE" +DEFINES+=$(USER_DEFINES) + +DEFINES+=-DDEFAULT_HARDWARE='"$(HARDWARE_DESC)"' +INCDIR=../include +CFLAGS=-W -Wall -Wextra -Wno-unused-parameter -O3 -g -fPIC $(DEFINES) +CXXFLAGS=$(CFLAGS) -fno-exceptions -std=c++11 + +all : $(TARGET).a $(TARGET).so.1 + +$(TARGET).a : $(OBJECTS) + $(AR) rcs $@ $^ + +$(TARGET).so.1 : $(OBJECTS) + $(CXX) -shared -Wl,-soname,$@ -o $@ $^ -lpthread -lrt -lm -lpthread + +led-matrix.o: led-matrix.cc $(INCDIR)/led-matrix.h +thread.o : thread.cc $(INCDIR)/thread.h +framebuffer.o: framebuffer.cc framebuffer-internal.h +graphics.o: graphics.cc utf8-internal.h + +%.o : %.cc compiler-flags + $(CXX) -I$(INCDIR) $(CXXFLAGS) -c -o $@ $< + +%.o : %.c compiler-flags + $(CC) -I$(INCDIR) $(CFLAGS) -c -o $@ $< + +clean: + rm -f $(OBJECTS) $(TARGET).a $(TARGET).so.1 + +compiler-flags: FORCE + @echo '$(CXX) $(CXXFLAGS)' | cmp -s - $@ || echo '$(CXX) $(CXXFLAGS)' > $@ + +.PHONY: FORCE diff --git a/lib/bdf-font.cc b/lib/bdf-font.cc new file mode 100644 index 0000000..3f70c20 --- /dev/null +++ b/lib/bdf-font.cc @@ -0,0 +1,188 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2014 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +// Some old g++ installations need this macro to be defined for PRIx64. +#ifndef __STDC_FORMAT_MACROS +# define __STDC_FORMAT_MACROS +#endif +#include + +#include "graphics.h" + +#include +#include +#include + +// The little question-mark box "�" for unknown code. +static const uint32_t kUnicodeReplacementCodepoint = 0xFFFD; + +// Bitmap for one row. This limits the number of available columns. +// Make wider if running into trouble. +typedef uint64_t rowbitmap_t; + +namespace rgb_matrix { +struct Font::Glyph { + int device_width, device_height; + int width, height; + int x_offset, y_offset; + rowbitmap_t bitmap[0]; // contains 'height' elements. +}; + +Font::Font() : font_height_(-1), base_line_(0) {} +Font::~Font() { + for (CodepointGlyphMap::iterator it = glyphs_.begin(); + it != glyphs_.end(); ++it) { + free(it->second); + } +} + +// TODO: that might not be working for all input files yet. +bool Font::LoadFont(const char *path) { + if (!path || !*path) return false; + FILE *f = fopen(path, "r"); + if (f == NULL) + return false; + uint32_t codepoint; + char buffer[1024]; + int dummy; + Glyph tmp; + Glyph *current_glyph = NULL; + int row = 0; + + int bitmap_shift = 0; + while (fgets(buffer, sizeof(buffer), f)) { + if (sscanf(buffer, "FONTBOUNDINGBOX %d %d %d %d", + &dummy, &font_height_, &dummy, &base_line_) == 4) { + base_line_ += font_height_; + } + else if (sscanf(buffer, "ENCODING %ud", &codepoint) == 1) { + // parsed. + } + else if (sscanf(buffer, "DWIDTH %d %d", &tmp.device_width, &tmp.device_height + ) == 2) { + // parsed. + } + else if (sscanf(buffer, "BBX %d %d %d %d", &tmp.width, &tmp.height, + &tmp.x_offset, &tmp.y_offset) == 4) { + current_glyph = (Glyph*) malloc(sizeof(Glyph) + + tmp.height * sizeof(rowbitmap_t)); + *current_glyph = tmp; + // We only get number of bytes large enough holding our width. We want + // it always left-aligned. + bitmap_shift = + 8 * (sizeof(rowbitmap_t) - ((current_glyph->width + 7) / 8)) + - current_glyph->x_offset; + row = -1; // let's not start yet, wait for BITMAP + } + else if (strncmp(buffer, "BITMAP", strlen("BITMAP")) == 0) { + row = 0; + } + else if (current_glyph && row >= 0 && row < current_glyph->height + && (sscanf(buffer, "%" PRIx64, ¤t_glyph->bitmap[row]) == 1)) { + current_glyph->bitmap[row] <<= bitmap_shift; + row++; + } + else if (strncmp(buffer, "ENDCHAR", strlen("ENDCHAR")) == 0) { + if (current_glyph && row == current_glyph->height) { + free(glyphs_[codepoint]); // just in case there was one. + glyphs_[codepoint] = current_glyph; + current_glyph = NULL; + } + } + } + fclose(f); + return true; +} + +Font *Font::CreateOutlineFont() const { + Font *r = new Font(); + const int kBorder = 1; + r->font_height_ = font_height_ + 2*kBorder; + r->base_line_ = base_line_ + kBorder; + for (CodepointGlyphMap::const_iterator it = glyphs_.begin(); + it != glyphs_.end(); ++it) { + const Glyph *orig = it->second; + const int height = orig->height + 2 * kBorder; + const size_t alloc_size = sizeof(Glyph) + height * sizeof(rowbitmap_t); + Glyph *const tmp_glyph = (Glyph*) calloc(1, alloc_size); + tmp_glyph->width = orig->width + 2*kBorder; + tmp_glyph->height = height; + tmp_glyph->device_width = orig->device_width + 2*kBorder; + tmp_glyph->device_height = height; + tmp_glyph->y_offset = orig->y_offset - kBorder; + // TODO: we don't really need bounding box, right ? + const rowbitmap_t fill_pattern = 0b111; + const rowbitmap_t start_mask = 0b010; + // Fill the border + for (int h = 0; h < orig->height; ++h) { + rowbitmap_t fill = fill_pattern; + rowbitmap_t orig_bitmap = orig->bitmap[h] >> kBorder; + for (rowbitmap_t m = start_mask; m; m <<= 1, fill <<= 1) { + if (orig_bitmap & m) { + tmp_glyph->bitmap[h+kBorder-1] |= fill; + tmp_glyph->bitmap[h+kBorder+0] |= fill; + tmp_glyph->bitmap[h+kBorder+1] |= fill; + } + } + } + // Remove original font again. + for (int h = 0; h < orig->height; ++h) { + rowbitmap_t orig_bitmap = orig->bitmap[h] >> kBorder; + tmp_glyph->bitmap[h+kBorder] &= ~orig_bitmap; + } + r->glyphs_[it->first] = tmp_glyph; + } + return r; +} + +const Font::Glyph *Font::FindGlyph(uint32_t unicode_codepoint) const { + CodepointGlyphMap::const_iterator found = glyphs_.find(unicode_codepoint); + if (found == glyphs_.end()) + return NULL; + return found->second; +} + +int Font::CharacterWidth(uint32_t unicode_codepoint) const { + const Glyph *g = FindGlyph(unicode_codepoint); + return g ? g->device_width : -1; +} + +int Font::DrawGlyph(Canvas *c, int x_pos, int y_pos, + const Color &color, const Color *bgcolor, + uint32_t unicode_codepoint) const { + const Glyph *g = FindGlyph(unicode_codepoint); + if (g == NULL) g = FindGlyph(kUnicodeReplacementCodepoint); + if (g == NULL) return 0; + y_pos = y_pos - g->height - g->y_offset; + for (int y = 0; y < g->height; ++y) { + const rowbitmap_t row = g->bitmap[y]; + rowbitmap_t x_mask = (1LL<<63); + for (int x = 0; x < g->device_width; ++x, x_mask >>= 1) { + if (row & x_mask) { + c->SetPixel(x_pos + x, y_pos + y, color.r, color.g, color.b); + } else if (bgcolor) { + c->SetPixel(x_pos + x, y_pos + y, bgcolor->r, bgcolor->g, bgcolor->b); + } + } + } + return g->device_width; +} + +int Font::DrawGlyph(Canvas *c, int x_pos, int y_pos, const Color &color, + uint32_t unicode_codepoint) const { + return DrawGlyph(c, x_pos, y_pos, color, NULL, unicode_codepoint); +} + +} // namespace rgb_matrix diff --git a/lib/bdf-font.o b/lib/bdf-font.o new file mode 100644 index 0000000..f7ad0d6 Binary files /dev/null and b/lib/bdf-font.o differ diff --git a/lib/content-streamer.cc b/lib/content-streamer.cc new file mode 100644 index 0000000..7dc7425 --- /dev/null +++ b/lib/content-streamer.cc @@ -0,0 +1,203 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- + +#include "content-streamer.h" +#include "led-matrix.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "gpio-bits.h" + +namespace rgb_matrix { + +// Pre-c++11 helper +#define STATIC_ASSERT(msg, c) typedef int static_assert_##msg[(c) ? 1 : -1] + +namespace { +// We write magic values as integers to automatically detect endian issues. +// Streams are stored in little-endian. This is the ARM default (running +// the Raspberry Pi, but also x86; so it is possible to create streams easily +// on a different x86 Linux PC. +static const uint32_t kFileMagicValue = 0xED0C5A48; +struct FileHeader { + uint32_t magic; // kFileMagicValue + uint32_t buf_size; + uint32_t width; + uint32_t height; + uint64_t future_use1; + uint64_t is_wide_gpio : 1; + uint64_t flags_future_use : 63; +}; +STATIC_ASSERT(file_header_size_changed, sizeof(FileHeader) == 32); + +static const uint32_t kFrameMagicValue = 0x12345678; +struct FrameHeader { + uint32_t magic; // kFrameMagic + uint32_t size; + uint32_t hold_time_us; // How long this frame lasts in usec. + uint32_t future_use1; + uint64_t future_use2; + uint64_t future_use3; +}; +STATIC_ASSERT(file_header_size_changed, sizeof(FrameHeader) == 32); +} + +FileStreamIO::FileStreamIO(int fd) : fd_(fd) { + posix_fadvise(fd_, 0, 0, POSIX_FADV_SEQUENTIAL); +} +FileStreamIO::~FileStreamIO() { close(fd_); } + +void FileStreamIO::Rewind() { lseek(fd_, 0, SEEK_SET); } + +ssize_t FileStreamIO::Read(void *buf, const size_t count) { + return read(fd_, buf, count); +} + +ssize_t FileStreamIO::Append(const void *buf, const size_t count) { + return write(fd_, buf, count); +} + +void MemStreamIO::Rewind() { pos_ = 0; } +ssize_t MemStreamIO::Read(void *buf, size_t count) { + const size_t amount = std::min(count, buffer_.size() - pos_); + memcpy(buf, buffer_.data() + pos_, amount); + pos_ += amount; + return amount; +} +ssize_t MemStreamIO::Append(const void *buf, size_t count) { + buffer_.append((const char*)buf, count); + return count; +} + +// Read exactly count bytes including retries. Returns success. +static bool FullRead(StreamIO *io, void *buf, const size_t count) { + int remaining = count; + char *char_buffer = (char*)buf; + while (remaining > 0) { + int r = io->Read(char_buffer, remaining); + if (r < 0) return false; + if (r == 0) break; // EOF. + char_buffer += r; remaining -= r; + } + return remaining == 0; +} + +// Write exactly count bytes including retries. Returns success. +static bool FullAppend(StreamIO *io, const void *buf, const size_t count) { + int remaining = count; + const char *char_buffer = (const char*) buf; + while (remaining > 0) { + int w = io->Append(char_buffer, remaining); + if (w < 0) return false; + char_buffer += w; remaining -= w; + } + return remaining == 0; +} + +StreamWriter::StreamWriter(StreamIO *io) : io_(io), header_written_(false) {} +bool StreamWriter::Stream(const FrameCanvas &frame, uint32_t hold_time_us) { + const char *data; + size_t len; + frame.Serialize(&data, &len); + + if (!header_written_) { + WriteFileHeader(frame, len); + } + FrameHeader h = {}; + h.magic = kFrameMagicValue; + h.size = len; + h.hold_time_us = hold_time_us; + FullAppend(io_, &h, sizeof(h)); + return FullAppend(io_, data, len) == (ssize_t)len; +} + +void StreamWriter::WriteFileHeader(const FrameCanvas &frame, size_t len) { + FileHeader header = {}; + header.magic = kFileMagicValue; + header.width = frame.width(); + header.height = frame.height(); + header.buf_size = len; + header.is_wide_gpio = (sizeof(gpio_bits_t) > 4); + FullAppend(io_, &header, sizeof(header)); + header_written_ = true; +} + +StreamReader::StreamReader(StreamIO *io) + : io_(io), state_(STREAM_AT_BEGIN), header_frame_buffer_(NULL) { + io_->Rewind(); +} +StreamReader::~StreamReader() { delete [] header_frame_buffer_; } + +void StreamReader::Rewind() { + io_->Rewind(); + state_ = STREAM_AT_BEGIN; +} + +bool StreamReader::GetNext(FrameCanvas *frame, uint32_t* hold_time_us) { + if (state_ == STREAM_AT_BEGIN && !ReadFileHeader(*frame)) return false; + if (state_ != STREAM_READING) return false; + + // Read header and expected buffer size. + if (!FullRead(io_, header_frame_buffer_, + sizeof(FrameHeader) + frame_buf_size_)) { + return false; + } + + const FrameHeader &h = *reinterpret_cast(header_frame_buffer_); + + // TODO: we might allow for this to be a kFileMagicValue, to allow people + // to just concatenate streams. In that case, we just would need to read + // ahead past this header (both headers are designed to be same size) + if (h.magic != kFrameMagicValue) { + state_ = STREAM_ERROR; + return false; + } + + // In the future, we might allow larger buffers (audio?), but never smaller. + // For now, we need to make sure to exactly match the size, as our assumption + // above is that we can read the full header + frame in one FullRead(). + if (h.size != frame_buf_size_) + return false; + + if (hold_time_us) *hold_time_us = h.hold_time_us; + return frame->Deserialize(header_frame_buffer_ + sizeof(FrameHeader), + frame_buf_size_); +} + +bool StreamReader::ReadFileHeader(const FrameCanvas &frame) { + FileHeader header; + FullRead(io_, &header, sizeof(header)); + if (header.magic != kFileMagicValue) { + state_ = STREAM_ERROR; + return false; + } + if ((int)header.width != frame.width() + || (int)header.height != frame.height()) { + fprintf(stderr, "This stream is for %dx%d, can't play on %dx%d. " + "Please use the same settings for record/replay\n", + header.width, header.height, frame.width(), frame.height()); + state_ = STREAM_ERROR; + return false; + } + if (header.is_wide_gpio != (sizeof(gpio_bits_t) == 8)) { + fprintf(stderr, "This stream was written with %s GPIO width support but " + "this library is compiled with %d bit GPIO width (see " + "ENABLE_WIDE_GPIO_COMPUTE_MODULE setting in lib/Makefile)\n", + header.is_wide_gpio ? "wide (64-bit)" : "narrow (32-bit)", + int(sizeof(gpio_bits_t) * 8)); + state_ = STREAM_ERROR; + return false; + } + state_ = STREAM_READING; + frame_buf_size_ = header.buf_size; + if (!header_frame_buffer_) + header_frame_buffer_ = new char [ sizeof(FrameHeader) + header.buf_size ]; + return true; +} +} // namespace rgb_matrix diff --git a/lib/content-streamer.o b/lib/content-streamer.o new file mode 100644 index 0000000..933bf91 Binary files /dev/null and b/lib/content-streamer.o differ diff --git a/lib/framebuffer-internal.h b/lib/framebuffer-internal.h new file mode 100644 index 0000000..2a5e3ef --- /dev/null +++ b/lib/framebuffer-internal.h @@ -0,0 +1,175 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2013 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +#ifndef RPI_RGBMATRIX_FRAMEBUFFER_INTERNAL_H +#define RPI_RGBMATRIX_FRAMEBUFFER_INTERNAL_H + +#include +#include + +#include "hardware-mapping.h" + +namespace rgb_matrix { +class GPIO; +class PinPulser; +namespace internal { +class RowAddressSetter; + +// An opaque type used within the framebuffer that can be used +// to copy between PixelMappers. +struct PixelDesignator { + PixelDesignator() : gpio_word(-1), r_bit(0), g_bit(0), b_bit(0), mask(~0u){} + long gpio_word; + gpio_bits_t r_bit; + gpio_bits_t g_bit; + gpio_bits_t b_bit; + gpio_bits_t mask; +}; + +class PixelDesignatorMap { +public: + PixelDesignatorMap(int width, int height, const PixelDesignator &fill_bits); + ~PixelDesignatorMap(); + + // Get a writable version of the PixelDesignator. Outside Framebuffer used + // by the RGBMatrix to re-assign mappings to new PixelDesignatorMappers. + PixelDesignator *get(int x, int y); + + inline int width() const { return width_; } + inline int height() const { return height_; } + + // All bits that set red/green/blue pixels; used for Fill(). + const PixelDesignator &GetFillColorBits() { return fill_bits_; } + +private: + const int width_; + const int height_; + const PixelDesignator fill_bits_; // Precalculated for fill. + PixelDesignator *const buffer_; +}; + +// Internal representation of the frame-buffer that as well can +// write itself to GPIO. +// Our internal memory layout mimicks as much as possible what needs to be +// written out. +class Framebuffer { +public: + // Maximum usable bitplanes. + // + // 11 bits seems to be a sweet spot in which we still get somewhat useful + // refresh rate and have good color richness. This is the default setting + // However, in low-light situations, we want to be able to scale down + // brightness more, having more bits at the bottom. + // TODO(hzeller): make the default 15 bit or so, but slide the use of + // timing to lower bits if fewer bits requested to not affect the overall + // refresh in that case. + // This needs to be balanced to not create too agressive timing however. + // To be explored in a separete commit. + // + // For now, if someone needs very low level of light, change this to + // say 13 and recompile. Run with --led-pwm-bits=13. Also, consider + // --led-pwm-dither-bits=2 to have the refresh rate not suffer too much. + static constexpr int kBitPlanes = 11; + static constexpr int kDefaultBitPlanes = 11; + + Framebuffer(int rows, int columns, int parallel, + int scan_mode, + const char* led_sequence, bool inverse_color, + PixelDesignatorMap **mapper); + ~Framebuffer(); + + // Initialize GPIO bits for output. Only call once. + static void InitHardwareMapping(const char *named_hardware); + static void InitGPIO(GPIO *io, int rows, int parallel, + bool allow_hardware_pulsing, + int pwm_lsb_nanoseconds, + int dither_bits, + int row_address_type); + static void InitializePanels(GPIO *io, const char *panel_type, int columns); + + // Set PWM bits used for output. Default is 11, but if you only deal with + // simple comic-colors, 1 might be sufficient. Lower require less CPU. + // Returns boolean to signify if value was within range. + bool SetPWMBits(uint8_t value); + uint8_t pwmbits() { return pwm_bits_; } + + // Map brightness of output linearly to input with CIE1931 profile. + void set_luminance_correct(bool on) { do_luminance_correct_ = on; } + bool luminance_correct() const { return do_luminance_correct_; } + + // Set brightness in percent; range=1..100 + // This will only affect newly set pixels. + void SetBrightness(uint8_t b) { + brightness_ = (b <= 100 ? (b != 0 ? b : 1) : 100); + } + uint8_t brightness() { return brightness_; } + + void DumpToMatrix(GPIO *io, int pwm_bits_to_show); + + void Serialize(const char **data, size_t *len) const; + bool Deserialize(const char *data, size_t len); + void CopyFrom(const Framebuffer *other); + + // Canvas-inspired methods, but we're not implementing this interface to not + // have an unnecessary vtable. + int width() const; + int height() const; + void SetPixel(int x, int y, uint8_t red, uint8_t green, uint8_t blue); + void Clear(); + void Fill(uint8_t red, uint8_t green, uint8_t blue); + +private: + static const struct HardwareMapping *hardware_mapping_; + static RowAddressSetter *row_setter_; + + // This returns the gpio-bit for given color (one of 'R', 'G', 'B'). This is + // returning the right value in case "led_sequence" is _not_ "RGB" + static gpio_bits_t GetGpioFromLedSequence(char col, const char *led_sequence, + gpio_bits_t default_r, + gpio_bits_t default_g, + gpio_bits_t default_b); + + void InitDefaultDesignator(int x, int y, const char *led_sequence, + PixelDesignator *designator); + inline void MapColors(uint8_t r, uint8_t g, uint8_t b, + uint16_t *red, uint16_t *green, uint16_t *blue); + const int rows_; // Number of rows. 16 or 32. + const int parallel_; // Parallel rows of chains. 1 or 2. + const int height_; // rows * parallel + const int columns_; // Number of columns. Number of chained boards * 32. + + const int scan_mode_; + const bool inverse_color_; + + uint8_t pwm_bits_; // PWM bits to display. + bool do_luminance_correct_; + uint8_t brightness_; + + const int double_rows_; + const size_t buffer_size_; + + // The frame-buffer is organized in bitplanes. + // Highest level (slowest to cycle through) are double rows. + // For each double-row, we store pwm-bits columns of a bitplane. + // Each bitplane-column is pre-filled IoBits, of which the colors are set. + // Of course, that means that we store unrelated bits in the frame-buffer, + // but it allows easy access in the critical section. + gpio_bits_t *bitplane_buffer_; + inline gpio_bits_t *ValueAt(int double_row, int column, int bit); + + PixelDesignatorMap **shared_mapper_; // Storage in RGBMatrix. +}; +} // namespace internal +} // namespace rgb_matrix +#endif // RPI_RGBMATRIX_FRAMEBUFFER_INTERNAL_H diff --git a/lib/framebuffer.cc b/lib/framebuffer.cc new file mode 100644 index 0000000..ab73e62 --- /dev/null +++ b/lib/framebuffer.cc @@ -0,0 +1,878 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2013 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +// The framebuffer is the workhorse: it represents the frame in some internal +// format that is friendly to be dumped to the matrix quickly. Provides methods +// to manipulate the content. + +#include "framebuffer-internal.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "gpio.h" + +namespace rgb_matrix { +namespace internal { +// We need one global instance of a timing correct pulser. There are different +// implementations depending on the context. +static PinPulser *sOutputEnablePulser = NULL; + +#ifdef ONLY_SINGLE_SUB_PANEL +# define SUB_PANELS_ 1 +#else +# define SUB_PANELS_ 2 +#endif + +PixelDesignator *PixelDesignatorMap::get(int x, int y) { + if (x < 0 || y < 0 || x >= width_ || y >= height_) + return NULL; + return buffer_ + (y*width_) + x; +} + +PixelDesignatorMap::PixelDesignatorMap(int width, int height, + const PixelDesignator &fill_bits) + : width_(width), height_(height), fill_bits_(fill_bits), + buffer_(new PixelDesignator[width * height]) { +} + +PixelDesignatorMap::~PixelDesignatorMap() { + delete [] buffer_; +} + +// Different panel types use different techniques to set the row address. +// We abstract that away with different implementations of RowAddressSetter +class RowAddressSetter { +public: + virtual ~RowAddressSetter() {} + virtual gpio_bits_t need_bits() const = 0; + virtual void SetRowAddress(GPIO *io, int row) = 0; +}; + +namespace { + +// The default DirectRowAddressSetter just sets the address in parallel +// output lines ABCDE with A the LSB and E the MSB. +class DirectRowAddressSetter : public RowAddressSetter { +public: + DirectRowAddressSetter(int double_rows, const HardwareMapping &h) + : row_mask_(0), last_row_(-1) { + assert(double_rows <= 32); // need to resize row_lookup_ + if (double_rows > 16) row_mask_ |= h.e; + if (double_rows > 8) row_mask_ |= h.d; + if (double_rows > 4) row_mask_ |= h.c; + if (double_rows > 2) row_mask_ |= h.b; + row_mask_ |= h.a; + for (int i = 0; i < double_rows; ++i) { + // To avoid the bit-fiddle in the critical path, utilize + // a lookup-table for all possible rows. + gpio_bits_t row_address = (i & 0x01) ? h.a : 0; + row_address |= (i & 0x02) ? h.b : 0; + row_address |= (i & 0x04) ? h.c : 0; + row_address |= (i & 0x08) ? h.d : 0; + row_address |= (i & 0x10) ? h.e : 0; + row_lookup_[i] = row_address; + } + } + + virtual gpio_bits_t need_bits() const { return row_mask_; } + + virtual void SetRowAddress(GPIO *io, int row) { + if (row == last_row_) return; + io->WriteMaskedBits(row_lookup_[row], row_mask_); + last_row_ = row; + } + +private: + gpio_bits_t row_mask_; + gpio_bits_t row_lookup_[32]; + int last_row_; +}; + +// The SM5266RowAddressSetter (ABC Shifter + DE direct) sets bits ABC using +// a 8 bit shifter and DE directly. The panel this works with has 8 SM5266 +// shifters (4 for the top 32 rows and 4 for the bottom 32 rows). +// DE is used to select the active shifter +// (rows 1-8/33-40, 9-16/41-48, 17-24/49-56, 25-32/57-64). +// Rows are enabled by shifting in 8 bits (high bit first) with a high bit +// enabling that row. This allows up to 8 rows per group to be active at the +// same time (if they have the same content), but that isn't implemented here. +// BK, DIN and DCK are the designations on the SM5266P datasheet. +// BK = Enable Input, DIN = Serial In, DCK = Clock +class SM5266RowAddressSetter : public RowAddressSetter { +public: + SM5266RowAddressSetter(int double_rows, const HardwareMapping &h) + : row_mask_(h.a | h.b | h.c), + last_row_(-1), + bk_(h.c), + din_(h.b), + dck_(h.a) { + assert(double_rows <= 32); // designed for up to 1/32 panel + if (double_rows > 8) row_mask_ |= h.d; + if (double_rows > 16) row_mask_ |= h.e; + for (int i = 0; i < double_rows; ++i) { + gpio_bits_t row_address = 0; + row_address |= (i & 0x08) ? h.d : 0; + row_address |= (i & 0x10) ? h.e : 0; + row_lookup_[i] = row_address; + } + } + + virtual gpio_bits_t need_bits() const { return row_mask_; } + + virtual void SetRowAddress(GPIO *io, int row) { + if (row == last_row_) return; + io->SetBits(bk_); // Enable serial input for the shifter + for (int r = 7; r >= 0; r--) { + if (row % 8 == r) { + io->SetBits(din_); + } else { + io->ClearBits(din_); + } + io->SetBits(dck_); + io->SetBits(dck_); // Longer clock time; tested with Pi3 + io->ClearBits(dck_); + } + io->ClearBits(bk_); // Disable serial input to keep unwanted bits out of the shifters + last_row_ = row; + // Set bits D and E to enable the proper shifter to display the selected + // row. + io->WriteMaskedBits(row_lookup_[row], row_mask_); + } + +private: + gpio_bits_t row_mask_; + int last_row_; + const gpio_bits_t bk_; + const gpio_bits_t din_; + const gpio_bits_t dck_; + gpio_bits_t row_lookup_[32]; +}; + +class ShiftRegisterRowAddressSetter : public RowAddressSetter { +public: + ShiftRegisterRowAddressSetter(int double_rows, const HardwareMapping &h) + : double_rows_(double_rows), + row_mask_(h.a | h.b), clock_(h.a), data_(h.b), + last_row_(-1) { + } + virtual gpio_bits_t need_bits() const { return row_mask_; } + + virtual void SetRowAddress(GPIO *io, int row) { + if (row == last_row_) return; + for (int activate = 0; activate < double_rows_; ++activate) { + io->ClearBits(clock_); + if (activate == double_rows_ - 1 - row) { + io->ClearBits(data_); + } else { + io->SetBits(data_); + } + io->SetBits(clock_); + } + io->ClearBits(clock_); + io->SetBits(clock_); + last_row_ = row; + } + +private: + const int double_rows_; + const gpio_bits_t row_mask_; + const gpio_bits_t clock_; + const gpio_bits_t data_; + int last_row_; +}; + +// Issue #823 +// An shift register row address setter that does not use B but C for the +// data. Clock is inverted. +class ABCShiftRegisterRowAddressSetter : public RowAddressSetter { +public: + ABCShiftRegisterRowAddressSetter(int double_rows, const HardwareMapping &h) + : double_rows_(double_rows), + row_mask_(h.a | h.c), + clock_(h.a), + data_(h.c), + last_row_(-1) { + } + virtual gpio_bits_t need_bits() const { return row_mask_; } + + virtual void SetRowAddress(GPIO *io, int row) { + for (int activate = 0; activate < double_rows_; ++activate) { + io->ClearBits(clock_); + if (activate == double_rows_ - 1 - row) { + io->SetBits(data_); + } else { + io->ClearBits(data_); + } + io->SetBits(clock_); + } + io->SetBits(clock_); + io->ClearBits(clock_); + last_row_ = row; + } + +private: + const int double_rows_; + const gpio_bits_t row_mask_; + const gpio_bits_t clock_; + const gpio_bits_t data_; + int last_row_; +}; + +// The DirectABCDRowAddressSetter sets the address by one of +// row pin ABCD for 32х16 matrix 1:4 multiplexing. The matrix has +// 4 addressable rows. Row is selected by a low level on the +// corresponding row address pin. Other row address pins must be in high level. +// +// Row addr| 0 | 1 | 2 | 3 +// --------+---+---+---+--- +// Line A | 0 | 1 | 1 | 1 +// Line B | 1 | 0 | 1 | 1 +// Line C | 1 | 1 | 0 | 1 +// Line D | 1 | 1 | 1 | 0 +class DirectABCDLineRowAddressSetter : public RowAddressSetter { +public: + DirectABCDLineRowAddressSetter(int double_rows, const HardwareMapping &h) + : last_row_(-1) { + row_mask_ = h.a | h.b | h.c | h.d; + + row_lines_[0] = /*h.a |*/ h.b | h.c | h.d; + row_lines_[1] = h.a /*| h.b*/ | h.c | h.d; + row_lines_[2] = h.a | h.b /*| h.c */| h.d; + row_lines_[3] = h.a | h.b | h.c /*| h.d*/; + } + + virtual gpio_bits_t need_bits() const { return row_mask_; } + + virtual void SetRowAddress(GPIO *io, int row) { + if (row == last_row_) return; + + gpio_bits_t row_address = row_lines_[row % 4]; + + io->WriteMaskedBits(row_address, row_mask_); + last_row_ = row; + } + +private: + gpio_bits_t row_lines_[4]; + gpio_bits_t row_mask_; + int last_row_; +}; + +} + +const struct HardwareMapping *Framebuffer::hardware_mapping_ = NULL; +RowAddressSetter *Framebuffer::row_setter_ = NULL; + +Framebuffer::Framebuffer(int rows, int columns, int parallel, + int scan_mode, + const char *led_sequence, bool inverse_color, + PixelDesignatorMap **mapper) + : rows_(rows), + parallel_(parallel), + height_(rows * parallel), + columns_(columns), + scan_mode_(scan_mode), + inverse_color_(inverse_color), + pwm_bits_(kBitPlanes), do_luminance_correct_(true), brightness_(100), + double_rows_(rows / SUB_PANELS_), + buffer_size_(double_rows_ * columns_ * kBitPlanes * sizeof(gpio_bits_t)), + shared_mapper_(mapper) { + assert(hardware_mapping_ != NULL); // Called InitHardwareMapping() ? + assert(shared_mapper_ != NULL); // Storage should be provided by RGBMatrix. + assert(rows_ >=4 && rows_ <= 64 && rows_ % 2 == 0); + if (parallel > hardware_mapping_->max_parallel_chains) { + fprintf(stderr, "The %s GPIO mapping only supports %d parallel chain%s, " + "but %d was requested.\n", hardware_mapping_->name, + hardware_mapping_->max_parallel_chains, + hardware_mapping_->max_parallel_chains > 1 ? "s" : "", parallel); + abort(); + } + assert(parallel >= 1 && parallel <= 6); + + bitplane_buffer_ = new gpio_bits_t[double_rows_ * columns_ * kBitPlanes]; + + // If we're the first Framebuffer created, the shared PixelMapper is + // still NULL, so create one. + // The first PixelMapper represents the physical layout of a standard matrix + // with the specific knowledge of the framebuffer, setting up PixelDesignators + // in a way that they are useful for this Framebuffer. + // + // Newly created PixelMappers then can just re-arrange PixelDesignators + // from the parent PixelMapper opaquely without having to know the details. + if (*shared_mapper_ == NULL) { + // Gather all the bits for given color for fast Fill()s and use the right + // bits according to the led sequence + const struct HardwareMapping &h = *hardware_mapping_; + gpio_bits_t r = h.p0_r1 | h.p0_r2 | h.p1_r1 | h.p1_r2 | h.p2_r1 | h.p2_r2 | h.p3_r1 | h.p3_r2 | h.p4_r1 | h.p4_r2 | h.p5_r1 | h.p5_r2; + gpio_bits_t g = h.p0_g1 | h.p0_g2 | h.p1_g1 | h.p1_g2 | h.p2_g1 | h.p2_g2 | h.p3_g1 | h.p3_g2 | h.p4_g1 | h.p4_g2 | h.p5_g1 | h.p5_g2; + gpio_bits_t b = h.p0_b1 | h.p0_b2 | h.p1_b1 | h.p1_b2 | h.p2_b1 | h.p2_b2 | h.p3_b1 | h.p3_b2 | h.p4_b1 | h.p4_b2 | h.p5_b1 | h.p5_b2; + PixelDesignator fill_bits; + fill_bits.r_bit = GetGpioFromLedSequence('R', led_sequence, r, g, b); + fill_bits.g_bit = GetGpioFromLedSequence('G', led_sequence, r, g, b); + fill_bits.b_bit = GetGpioFromLedSequence('B', led_sequence, r, g, b); + + *shared_mapper_ = new PixelDesignatorMap(columns_, height_, fill_bits); + for (int y = 0; y < height_; ++y) { + for (int x = 0; x < columns_; ++x) { + InitDefaultDesignator(x, y, led_sequence, (*shared_mapper_)->get(x, y)); + } + } + } + + Clear(); +} + +Framebuffer::~Framebuffer() { + delete [] bitplane_buffer_; +} + +// TODO: this should also be parsed from some special formatted string, e.g. +// {addr={22,23,24,25,15},oe=18,clk=17,strobe=4, p0={11,27,7,8,9,10},...} +/* static */ void Framebuffer::InitHardwareMapping(const char *named_hardware) { + if (named_hardware == NULL || *named_hardware == '\0') { + named_hardware = "regular"; + } + + struct HardwareMapping *mapping = NULL; + for (HardwareMapping *it = matrix_hardware_mappings; it->name; ++it) { + if (strcasecmp(it->name, named_hardware) == 0) { + mapping = it; + break; + } + } + + if (!mapping) { + fprintf(stderr, "There is no hardware mapping named '%s'.\nAvailable: ", + named_hardware); + for (HardwareMapping *it = matrix_hardware_mappings; it->name; ++it) { + if (it != matrix_hardware_mappings) fprintf(stderr, ", "); + fprintf(stderr, "'%s'", it->name); + } + fprintf(stderr, "\n"); + abort(); + } + + if (mapping->max_parallel_chains == 0) { + // Auto determine. + struct HardwareMapping *h = mapping; + if ((h->p0_r1 | h->p0_g1 | h->p0_g1 | h->p0_r2 | h->p0_g2 | h->p0_g2) > 0) + ++mapping->max_parallel_chains; + if ((h->p1_r1 | h->p1_g1 | h->p1_g1 | h->p1_r2 | h->p1_g2 | h->p1_g2) > 0) + ++mapping->max_parallel_chains; + if ((h->p2_r1 | h->p2_g1 | h->p2_g1 | h->p2_r2 | h->p2_g2 | h->p2_g2) > 0) + ++mapping->max_parallel_chains; + if ((h->p3_r1 | h->p3_g1 | h->p3_g1 | h->p3_r2 | h->p3_g2 | h->p3_g2) > 0) + ++mapping->max_parallel_chains; + if ((h->p4_r1 | h->p4_g1 | h->p4_g1 | h->p4_r2 | h->p4_g2 | h->p4_g2) > 0) + ++mapping->max_parallel_chains; + if ((h->p5_r1 | h->p5_g1 | h->p5_g1 | h->p5_r2 | h->p5_g2 | h->p5_g2) > 0) + ++mapping->max_parallel_chains; + } + hardware_mapping_ = mapping; +} + +/* static */ void Framebuffer::InitGPIO(GPIO *io, int rows, int parallel, + bool allow_hardware_pulsing, + int pwm_lsb_nanoseconds, + int dither_bits, + int row_address_type) { + if (sOutputEnablePulser != NULL) + return; // already initialized. + + const struct HardwareMapping &h = *hardware_mapping_; + // Tell GPIO about all bits we intend to use. + gpio_bits_t all_used_bits = 0; + + all_used_bits |= h.output_enable | h.clock | h.strobe; + + all_used_bits |= h.p0_r1 | h.p0_g1 | h.p0_b1 | h.p0_r2 | h.p0_g2 | h.p0_b2; + if (parallel >= 2) { + all_used_bits |= h.p1_r1 | h.p1_g1 | h.p1_b1 | h.p1_r2 | h.p1_g2 | h.p1_b2; + } + if (parallel >= 3) { + all_used_bits |= h.p2_r1 | h.p2_g1 | h.p2_b1 | h.p2_r2 | h.p2_g2 | h.p2_b2; + } + if (parallel >= 4) { + all_used_bits |= h.p3_r1 | h.p3_g1 | h.p3_b1 | h.p3_r2 | h.p3_g2 | h.p3_b2; + } + if (parallel >= 5) { + all_used_bits |= h.p4_r1 | h.p4_g1 | h.p4_b1 | h.p4_r2 | h.p4_g2 | h.p4_b2; + } + if (parallel >= 6) { + all_used_bits |= h.p5_r1 | h.p5_g1 | h.p5_b1 | h.p5_r2 | h.p5_g2 | h.p5_b2; + } + + const int double_rows = rows / SUB_PANELS_; + switch (row_address_type) { + case 0: + row_setter_ = new DirectRowAddressSetter(double_rows, h); + break; + case 1: + row_setter_ = new ShiftRegisterRowAddressSetter(double_rows, h); + break; + case 2: + row_setter_ = new DirectABCDLineRowAddressSetter(double_rows, h); + break; + case 3: + row_setter_ = new ABCShiftRegisterRowAddressSetter(double_rows, h); + break; + case 4: + row_setter_ = new SM5266RowAddressSetter(double_rows, h); + break; + default: + assert(0); // unexpected type. + } + + all_used_bits |= row_setter_->need_bits(); + + // Adafruit HAT identified by the same prefix. + const bool is_some_adafruit_hat = (0 == strncmp(h.name, "adafruit-hat", + strlen("adafruit-hat"))); + // Initialize outputs, make sure that all of these are supported bits. + const gpio_bits_t result = io->InitOutputs(all_used_bits, + is_some_adafruit_hat); + assert(result == all_used_bits); // Impl: all bits declared in gpio.cc ? + + std::vector bitplane_timings; + uint32_t timing_ns = pwm_lsb_nanoseconds; + for (int b = 0; b < kBitPlanes; ++b) { + bitplane_timings.push_back(timing_ns); + if (b >= dither_bits) timing_ns *= 2; + } + sOutputEnablePulser = PinPulser::Create(io, h.output_enable, + allow_hardware_pulsing, + bitplane_timings); +} + +// NOTE: first version for panel initialization sequence, need to refine +// until it is more clear how different panel types are initialized to be +// able to abstract this more. + +static void InitFM6126(GPIO *io, const struct HardwareMapping &h, int columns) { + const gpio_bits_t bits_on + = h.p0_r1 | h.p0_g1 | h.p0_b1 | h.p0_r2 | h.p0_g2 | h.p0_b2 + | h.p1_r1 | h.p1_g1 | h.p1_b1 | h.p1_r2 | h.p1_g2 | h.p1_b2 + | h.p2_r1 | h.p2_g1 | h.p2_b1 | h.p2_r2 | h.p2_g2 | h.p2_b2 + | h.p3_r1 | h.p3_g1 | h.p3_b1 | h.p3_r2 | h.p3_g2 | h.p3_b2 + | h.p4_r1 | h.p4_g1 | h.p4_b1 | h.p4_r2 | h.p4_g2 | h.p4_b2 + | h.p5_r1 | h.p5_g1 | h.p5_b1 | h.p5_r2 | h.p5_g2 | h.p5_b2 + | h.a; // Address bit 'A' is always on. + const gpio_bits_t bits_off = h.a; + const gpio_bits_t mask = bits_on | h.strobe; + + // Init bits. TODO: customize, as we can do things such as brightness here, + // which would allow more higher quality output. + static const char* init_b12 = "0111111111111111"; // full bright + static const char* init_b13 = "0000000001000000"; // panel on. + + io->ClearBits(h.clock | h.strobe); + + for (int i = 0; i < columns; ++i) { + gpio_bits_t value = init_b12[i % 16] == '0' ? bits_off : bits_on; + if (i > columns - 12) value |= h.strobe; + io->WriteMaskedBits(value, mask); + io->SetBits(h.clock); + io->ClearBits(h.clock); + } + io->ClearBits(h.strobe); + + for (int i = 0; i < columns; ++i) { + gpio_bits_t value = init_b13[i % 16] == '0' ? bits_off : bits_on; + if (i > columns - 13) value |= h.strobe; + io->WriteMaskedBits(value, mask); + io->SetBits(h.clock); + io->ClearBits(h.clock); + } + io->ClearBits(h.strobe); +} + +// The FM6217 is very similar to the FM6216. +// FM6217 adds Register 3 to allow for automatic bad pixel supression. +static void InitFM6127(GPIO *io, const struct HardwareMapping &h, int columns) { + const gpio_bits_t bits_r_on= h.p0_r1 | h.p0_r2; + const gpio_bits_t bits_g_on= h.p0_g1 | h.p0_g2; + const gpio_bits_t bits_b_on= h.p0_b1 | h.p0_b2; + const gpio_bits_t bits_on= bits_r_on | bits_g_on | bits_b_on; + const gpio_bits_t bits_off = 0; + + const gpio_bits_t mask = bits_on | h.strobe; + + static const char* init_b12 = "1111111111001110"; // register 1 + static const char* init_b13 = "1110000001100010"; // register 2. + static const char* init_b11 = "0101111100000000"; // register 3. + io->ClearBits(h.clock | h.strobe); + for (int i = 0; i < columns; ++i) { + gpio_bits_t value = init_b12[i % 16] == '0' ? bits_off : bits_on; + if (i > columns - 12) value |= h.strobe; + io->WriteMaskedBits(value, mask); + io->SetBits(h.clock); + io->ClearBits(h.clock); + } + io->ClearBits(h.strobe); + + for (int i = 0; i < columns; ++i) { + gpio_bits_t value = init_b13[i % 16] == '0' ? bits_off : bits_on; + if (i > columns - 13) value |= h.strobe; + io->WriteMaskedBits(value, mask); + io->SetBits(h.clock); + io->ClearBits(h.clock); + } + io->ClearBits(h.strobe); + + for (int i = 0; i < columns; ++i) { + gpio_bits_t value = init_b11[i % 16] == '0' ? bits_off : bits_on; + if (i > columns - 11) value |= h.strobe; + io->WriteMaskedBits(value, mask); + io->SetBits(h.clock); + io->ClearBits(h.clock); + } + io->ClearBits(h.strobe); +} + +/*static*/ void Framebuffer::InitializePanels(GPIO *io, + const char *panel_type, + int columns) { + if (!panel_type || panel_type[0] == '\0') return; + if (strncasecmp(panel_type, "fm6126", 6) == 0) { + InitFM6126(io, *hardware_mapping_, columns); + } + else if (strncasecmp(panel_type, "fm6127", 6) == 0) { + InitFM6127(io, *hardware_mapping_, columns); + } + // else if (strncasecmp(...)) // more init types + else { + fprintf(stderr, "Unknown panel type '%s'; typo ?\n", panel_type); + } +} + +bool Framebuffer::SetPWMBits(uint8_t value) { + if (value < 1 || value > kBitPlanes) + return false; + pwm_bits_ = value; + return true; +} + +inline gpio_bits_t *Framebuffer::ValueAt(int double_row, int column, int bit) { + return &bitplane_buffer_[ double_row * (columns_ * kBitPlanes) + + bit * columns_ + + column ]; +} + +void Framebuffer::Clear() { + if (inverse_color_) { + Fill(0, 0, 0); + } else { + // Cheaper. + memset(bitplane_buffer_, 0, + sizeof(*bitplane_buffer_) * double_rows_ * columns_ * kBitPlanes); + } +} + +// Do CIE1931 luminance correction and scale to output bitplanes +static uint16_t luminance_cie1931(uint8_t c, uint8_t brightness) { + float out_factor = ((1 << internal::Framebuffer::kBitPlanes) - 1); + float v = (float) c * brightness / 255.0; + return roundf(out_factor * ((v <= 8) ? v / 902.3 : pow((v + 16) / 116.0, 3))); +} + +struct ColorLookup { + uint16_t color[256]; +}; +static ColorLookup *CreateLuminanceCIE1931LookupTable() { + ColorLookup *for_brightness = new ColorLookup[100]; + for (int c = 0; c < 256; ++c) + for (int b = 0; b < 100; ++b) + for_brightness[b].color[c] = luminance_cie1931(c, b + 1); + + return for_brightness; +} + +static inline uint16_t CIEMapColor(uint8_t brightness, uint8_t c) { + static ColorLookup *luminance_lookup = CreateLuminanceCIE1931LookupTable(); + return luminance_lookup[brightness - 1].color[c]; +} + +// Non luminance correction. TODO: consider getting rid of this. +static inline uint16_t DirectMapColor(uint8_t brightness, uint8_t c) { + // simple scale down the color value + c = c * brightness / 100; + + // shift to be left aligned with top-most bits. + constexpr int shift = internal::Framebuffer::kBitPlanes - 8; + return (shift > 0) ? (c << shift) : (c >> -shift); +} + +inline void Framebuffer::MapColors( + uint8_t r, uint8_t g, uint8_t b, + uint16_t *red, uint16_t *green, uint16_t *blue) { + + if (do_luminance_correct_) { + *red = CIEMapColor(brightness_, r); + *green = CIEMapColor(brightness_, g); + *blue = CIEMapColor(brightness_, b); + } else { + *red = DirectMapColor(brightness_, r); + *green = DirectMapColor(brightness_, g); + *blue = DirectMapColor(brightness_, b); + } + + if (inverse_color_) { + *red = ~(*red); + *green = ~(*green); + *blue = ~(*blue); + } +} + +void Framebuffer::Fill(uint8_t r, uint8_t g, uint8_t b) { + uint16_t red, green, blue; + MapColors(r, g, b, &red, &green, &blue); + const PixelDesignator &fill = (*shared_mapper_)->GetFillColorBits(); + + for (int b = kBitPlanes - pwm_bits_; b < kBitPlanes; ++b) { + uint16_t mask = 1 << b; + gpio_bits_t plane_bits = 0; + plane_bits |= ((red & mask) == mask) ? fill.r_bit : 0; + plane_bits |= ((green & mask) == mask) ? fill.g_bit : 0; + plane_bits |= ((blue & mask) == mask) ? fill.b_bit : 0; + + for (int row = 0; row < double_rows_; ++row) { + gpio_bits_t *row_data = ValueAt(row, 0, b); + for (int col = 0; col < columns_; ++col) { + *row_data++ = plane_bits; + } + } + } +} + +int Framebuffer::width() const { return (*shared_mapper_)->width(); } +int Framebuffer::height() const { return (*shared_mapper_)->height(); } + +void Framebuffer::SetPixel(int x, int y, uint8_t r, uint8_t g, uint8_t b) { + const PixelDesignator *designator = (*shared_mapper_)->get(x, y); + if (designator == NULL) return; + const long pos = designator->gpio_word; + if (pos < 0) return; // non-used pixel marker. + + uint16_t red, green, blue; + MapColors(r, g, b, &red, &green, &blue); + + gpio_bits_t *bits = bitplane_buffer_ + pos; + const int min_bit_plane = kBitPlanes - pwm_bits_; + bits += (columns_ * min_bit_plane); + const gpio_bits_t r_bits = designator->r_bit; + const gpio_bits_t g_bits = designator->g_bit; + const gpio_bits_t b_bits = designator->b_bit; + const gpio_bits_t designator_mask = designator->mask; + for (uint16_t mask = 1<gpio_word = bits - bitplane_buffer_; + d->r_bit = d->g_bit = d->b_bit = 0; + if (y < rows_) { + if (y < double_rows_) { + d->r_bit = GetGpioFromLedSequence('R', seq, h.p0_r1, h.p0_g1, h.p0_b1); + d->g_bit = GetGpioFromLedSequence('G', seq, h.p0_r1, h.p0_g1, h.p0_b1); + d->b_bit = GetGpioFromLedSequence('B', seq, h.p0_r1, h.p0_g1, h.p0_b1); + } else { + d->r_bit = GetGpioFromLedSequence('R', seq, h.p0_r2, h.p0_g2, h.p0_b2); + d->g_bit = GetGpioFromLedSequence('G', seq, h.p0_r2, h.p0_g2, h.p0_b2); + d->b_bit = GetGpioFromLedSequence('B', seq, h.p0_r2, h.p0_g2, h.p0_b2); + } + } + else if (y >= rows_ && y < 2 * rows_) { + if (y - rows_ < double_rows_) { + d->r_bit = GetGpioFromLedSequence('R', seq, h.p1_r1, h.p1_g1, h.p1_b1); + d->g_bit = GetGpioFromLedSequence('G', seq, h.p1_r1, h.p1_g1, h.p1_b1); + d->b_bit = GetGpioFromLedSequence('B', seq, h.p1_r1, h.p1_g1, h.p1_b1); + } else { + d->r_bit = GetGpioFromLedSequence('R', seq, h.p1_r2, h.p1_g2, h.p1_b2); + d->g_bit = GetGpioFromLedSequence('G', seq, h.p1_r2, h.p1_g2, h.p1_b2); + d->b_bit = GetGpioFromLedSequence('B', seq, h.p1_r2, h.p1_g2, h.p1_b2); + } + } + else if (y >= 2*rows_ && y < 3 * rows_) { + if (y - 2*rows_ < double_rows_) { + d->r_bit = GetGpioFromLedSequence('R', seq, h.p2_r1, h.p2_g1, h.p2_b1); + d->g_bit = GetGpioFromLedSequence('G', seq, h.p2_r1, h.p2_g1, h.p2_b1); + d->b_bit = GetGpioFromLedSequence('B', seq, h.p2_r1, h.p2_g1, h.p2_b1); + } else { + d->r_bit = GetGpioFromLedSequence('R', seq, h.p2_r2, h.p2_g2, h.p2_b2); + d->g_bit = GetGpioFromLedSequence('G', seq, h.p2_r2, h.p2_g2, h.p2_b2); + d->b_bit = GetGpioFromLedSequence('B', seq, h.p2_r2, h.p2_g2, h.p2_b2); + } + } + else if (y >= 3*rows_ && y < 4 * rows_) { + if (y - 3*rows_ < double_rows_) { + d->r_bit = GetGpioFromLedSequence('R', seq, h.p3_r1, h.p3_g1, h.p3_b1); + d->g_bit = GetGpioFromLedSequence('G', seq, h.p3_r1, h.p3_g1, h.p3_b1); + d->b_bit = GetGpioFromLedSequence('B', seq, h.p3_r1, h.p3_g1, h.p3_b1); + } else { + d->r_bit = GetGpioFromLedSequence('R', seq, h.p3_r2, h.p3_g2, h.p3_b2); + d->g_bit = GetGpioFromLedSequence('G', seq, h.p3_r2, h.p3_g2, h.p3_b2); + d->b_bit = GetGpioFromLedSequence('B', seq, h.p3_r2, h.p3_g2, h.p3_b2); + } + } + else if (y >= 4*rows_ && y < 5 * rows_){ + if (y - 4*rows_ < double_rows_) { + d->r_bit = GetGpioFromLedSequence('R', seq, h.p4_r1, h.p4_g1, h.p4_b1); + d->g_bit = GetGpioFromLedSequence('G', seq, h.p4_r1, h.p4_g1, h.p4_b1); + d->b_bit = GetGpioFromLedSequence('B', seq, h.p4_r1, h.p4_g1, h.p4_b1); + } else { + d->r_bit = GetGpioFromLedSequence('R', seq, h.p4_r2, h.p4_g2, h.p4_b2); + d->g_bit = GetGpioFromLedSequence('G', seq, h.p4_r2, h.p4_g2, h.p4_b2); + d->b_bit = GetGpioFromLedSequence('B', seq, h.p4_r2, h.p4_g2, h.p4_b2); + } + + } + else { + if (y - 5*rows_ < double_rows_) { + d->r_bit = GetGpioFromLedSequence('R', seq, h.p5_r1, h.p5_g1, h.p5_b1); + d->g_bit = GetGpioFromLedSequence('G', seq, h.p5_r1, h.p5_g1, h.p5_b1); + d->b_bit = GetGpioFromLedSequence('B', seq, h.p5_r1, h.p5_g1, h.p5_b1); + } else { + d->r_bit = GetGpioFromLedSequence('R', seq, h.p5_r2, h.p5_g2, h.p5_b2); + d->g_bit = GetGpioFromLedSequence('G', seq, h.p5_r2, h.p5_g2, h.p5_b2); + d->b_bit = GetGpioFromLedSequence('B', seq, h.p5_r2, h.p5_g2, h.p5_b2); + } + } + + d->mask = ~(d->r_bit | d->g_bit | d->b_bit); +} + +void Framebuffer::Serialize(const char **data, size_t *len) const { + *data = reinterpret_cast(bitplane_buffer_); + *len = buffer_size_; +} + +bool Framebuffer::Deserialize(const char *data, size_t len) { + if (len != buffer_size_) return false; + memcpy(bitplane_buffer_, data, len); + return true; +} + +void Framebuffer::CopyFrom(const Framebuffer *other) { + if (other == this) return; + memcpy(bitplane_buffer_, other->bitplane_buffer_, buffer_size_); +} + +void Framebuffer::DumpToMatrix(GPIO *io, int pwm_low_bit) { + const struct HardwareMapping &h = *hardware_mapping_; + gpio_bits_t color_clk_mask = 0; // Mask of bits while clocking in. + color_clk_mask |= h.p0_r1 | h.p0_g1 | h.p0_b1 | h.p0_r2 | h.p0_g2 | h.p0_b2; + if (parallel_ >= 2) { + color_clk_mask |= h.p1_r1 | h.p1_g1 | h.p1_b1 | h.p1_r2 | h.p1_g2 | h.p1_b2; + } + if (parallel_ >= 3) { + color_clk_mask |= h.p2_r1 | h.p2_g1 | h.p2_b1 | h.p2_r2 | h.p2_g2 | h.p2_b2; + } + if (parallel_ >= 4) { + color_clk_mask |= h.p3_r1 | h.p3_g1 | h.p3_b1 | h.p3_r2 | h.p3_g2 | h.p3_b2; + } + if (parallel_ >= 5) { + color_clk_mask |= h.p4_r1 | h.p4_g1 | h.p4_b1 | h.p4_r2 | h.p4_g2 | h.p4_b2; + } + if (parallel_ >= 6) { + color_clk_mask |= h.p5_r1 | h.p5_g1 | h.p5_b1 | h.p5_r2 | h.p5_g2 | h.p5_b2; + } + + color_clk_mask |= h.clock; + + // Depending if we do dithering, we might not always show the lowest bits. + const int start_bit = std::max(pwm_low_bit, kBitPlanes - pwm_bits_); + + const uint8_t half_double = double_rows_/2; + for (uint8_t row_loop = 0; row_loop < double_rows_; ++row_loop) { + uint8_t d_row; + switch (scan_mode_) { + case 0: // progressive + default: + d_row = row_loop; + break; + + case 1: // interlaced + d_row = ((row_loop < half_double) + ? (row_loop << 1) + : ((row_loop - half_double) << 1) + 1); + } + + // Rows can't be switched very quickly without ghosting, so we do the + // full PWM of one row before switching rows. + for (int b = start_bit; b < kBitPlanes; ++b) { + gpio_bits_t *row_data = ValueAt(d_row, 0, b); + // While the output enable is still on, we can already clock in the next + // data. + for (int col = 0; col < columns_; ++col) { + const gpio_bits_t &out = *row_data++; + io->WriteMaskedBits(out, color_clk_mask); // col + reset clock + io->SetBits(h.clock); // Rising edge: clock color in. + } + io->ClearBits(color_clk_mask); // clock back to normal. + + // OE of the previous row-data must be finished before strobe. + sOutputEnablePulser->WaitPulseFinished(); + + // Setting address and strobing needs to happen in dark time. + row_setter_->SetRowAddress(io, d_row); + + io->SetBits(h.strobe); // Strobe in the previously clocked in row. + io->ClearBits(h.strobe); + + // Now switch on for the sleep time necessary for that bit-plane. + sOutputEnablePulser->SendPulse(b); + } + } +} +} // namespace internal +} // namespace rgb_matrix diff --git a/lib/framebuffer.o b/lib/framebuffer.o new file mode 100644 index 0000000..532e6e7 Binary files /dev/null and b/lib/framebuffer.o differ diff --git a/lib/gpio-bits.h b/lib/gpio-bits.h new file mode 100644 index 0000000..de9c7c8 --- /dev/null +++ b/lib/gpio-bits.h @@ -0,0 +1,28 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2013 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +// This file needs to compile in C and C++ context, so deliberately broken out. + +#ifndef RPI_GPIOBITS_H +#define RPI_GPIOBITS_H + +#include +#ifdef ENABLE_WIDE_GPIO_COMPUTE_MODULE +typedef uint64_t gpio_bits_t; +#else +typedef uint32_t gpio_bits_t; +#endif + +#endif diff --git a/lib/gpio.cc b/lib/gpio.cc new file mode 100644 index 0000000..a9f39bb --- /dev/null +++ b/lib/gpio.cc @@ -0,0 +1,787 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2013 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +#define __STDC_FORMAT_MACROS +#include + +#include "gpio.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * nanosleep() takes longer than requested because of OS jitter. + * In about 99.9% of the cases, this is <= 25 microcseconds on + * the Raspberry Pi (empirically determined with a Raspbian kernel), so + * we substract this value whenever we do nanosleep(); the remaining time + * we then busy wait to get a good accurate result. + * + * You can measure the overhead using DEBUG_SLEEP_JITTER below. + * + * Note: A higher value here will result in more CPU use because of more busy + * waiting inching towards the real value (for all the cases that nanosleep() + * actually was better than this overhead). + * + * This might be interesting to tweak in particular if you have a realtime + * kernel with different characteristics. + */ +#define EMPIRICAL_NANOSLEEP_OVERHEAD_US 12 + +/* + * In case of non-hardware pulse generation, use nanosleep if we want to wait + * longer than these given microseconds beyond the general overhead. + * Below that, just use busy wait. + */ +#define MINIMUM_NANOSLEEP_TIME_US 5 + +/* In order to determine useful values for above, set this to 1 and use the + * hardware pin-pulser. + * It will output a histogram atexit() of how much how often we were over + * the requested time. + * (The full histogram will be shifted by the EMPIRICAL_NANOSLEEP_OVERHEAD_US + * value above. To get a full histogram of OS overhead, set it to 0 first). + */ +#define DEBUG_SLEEP_JITTER 0 + +// Raspberry 1 and 2 have different base addresses for the periphery +#define BCM2708_PERI_BASE 0x20000000 +#define BCM2709_PERI_BASE 0x3F000000 +#define BCM2711_PERI_BASE 0xFE000000 + +#define GPIO_REGISTER_OFFSET 0x200000 +#define COUNTER_1Mhz_REGISTER_OFFSET 0x3000 + +#define GPIO_PWM_BASE_OFFSET (GPIO_REGISTER_OFFSET + 0xC000) +#define GPIO_CLK_BASE_OFFSET 0x101000 + +#define REGISTER_BLOCK_SIZE (4*1024) + +#define PWM_CTL (0x00 / 4) +#define PWM_STA (0x04 / 4) +#define PWM_RNG1 (0x10 / 4) +#define PWM_FIFO (0x18 / 4) + +#define PWM_CTL_CLRF1 (1<<6) // CH1 Clear Fifo (1 Clears FIFO 0 has no effect) +#define PWM_CTL_USEF1 (1<<5) // CH1 Use Fifo (0=data reg transmit 1=Fifo used for transmission) +#define PWM_CTL_POLA1 (1<<4) // CH1 Polarity (0=(0=low 1=high) 1=(1=low 0=high) +#define PWM_CTL_SBIT1 (1<<3) // CH1 Silence Bit (state of output when 0 transmission takes place) +#define PWM_CTL_MODE1 (1<<1) // CH1 Mode (0=pwm 1=serialiser mode) +#define PWM_CTL_PWEN1 (1<<0) // CH1 Enable (0=disable 1=enable) + +#define PWM_STA_EMPT1 (1<<1) +#define PWM_STA_FULL1 (1<<0) + +#define CLK_PASSWD (0x5A<<24) + +#define CLK_CTL_MASH(x)((x)<<9) +#define CLK_CTL_BUSY (1 <<7) +#define CLK_CTL_KILL (1 <<5) +#define CLK_CTL_ENAB (1 <<4) +#define CLK_CTL_SRC(x) ((x)<<0) + +#define CLK_CTL_SRC_PLLD 6 /* 500.0 MHz */ + +#define CLK_DIV_DIVI(x) ((x)<<12) +#define CLK_DIV_DIVF(x) ((x)<< 0) + +#define CLK_PWMCTL 40 +#define CLK_PWMDIV 41 + +// We want to have the last word in the fifo free +#define MAX_PWM_BIT_USE 224 +#define PWM_BASE_TIME_NS 2 + +// GPIO setup macros. Always use INP_GPIO(x) before using OUT_GPIO(x). +#define INP_GPIO(g) *(s_GPIO_registers+((g)/10)) &= ~(7ull<<(((g)%10)*3)) +#define OUT_GPIO(g) *(s_GPIO_registers+((g)/10)) |= (1ull<<(((g)%10)*3)) + +#define GPIO_SET *(gpio+7) // sets bits which are 1 ignores bits which are 0 +#define GPIO_CLR *(gpio+10) // clears bits which are 1 ignores bits which are 0 + +// We're pre-mapping all the registers on first call of GPIO::Init(), +// so that it is possible to drop privileges afterwards and still have these +// usable. +static volatile uint32_t *s_GPIO_registers = NULL; +static volatile uint32_t *s_Timer1Mhz = NULL; +static volatile uint32_t *s_PWM_registers = NULL; +static volatile uint32_t *s_CLK_registers = NULL; + +namespace rgb_matrix { +#define GPIO_BIT(x) (1ull << x) + +GPIO::GPIO() : output_bits_(0), input_bits_(0), reserved_bits_(0), + slowdown_(1) +#ifdef ENABLE_WIDE_GPIO_COMPUTE_MODULE + , uses_64_bit_(false) +#endif +{ +} + +gpio_bits_t GPIO::InitOutputs(gpio_bits_t outputs, + bool adafruit_pwm_transition_hack_needed) { + if (s_GPIO_registers == NULL) { + fprintf(stderr, "Attempt to init outputs but not yet Init()-ialized.\n"); + return 0; + } + + // Hack: for the PWM mod, the user soldered together GPIO 18 (new OE) + // with GPIO 4 (old OE). + // Since they are connected inside the HAT, want to make extra sure that, + // whatever the outside system set as pinmux, the old OE is _not_ also + // set as output so that these GPIO outputs don't fight each other. + // + // So explicitly set both of these pins as input initially, so the user + // can switch between the two modes "adafruit-hat" and "adafruit-hat-pwm" + // without trouble. + if (adafruit_pwm_transition_hack_needed) { + INP_GPIO(4); + INP_GPIO(18); + // Even with PWM enabled, GPIO4 still can not be used, because it is + // now connected to the GPIO18 and thus must stay an input. + // So reserve this bit if it is not set in outputs. + reserved_bits_ = GPIO_BIT(4) & ~outputs; + } + + outputs &= ~(output_bits_ | input_bits_ | reserved_bits_); +#ifdef ENABLE_WIDE_GPIO_COMPUTE_MODULE + const int kMaxAvailableBit = 45; + uses_64_bit_ |= (outputs >> 32) != 0; +#else + const int kMaxAvailableBit = 31; +#endif + for (int b = 0; b <= kMaxAvailableBit; ++b) { + if (outputs & GPIO_BIT(b)) { + INP_GPIO(b); // for writing, we first need to set as input. + OUT_GPIO(b); + } + } + output_bits_ |= outputs; + return outputs; +} + +gpio_bits_t GPIO::RequestInputs(gpio_bits_t inputs) { + if (s_GPIO_registers == NULL) { + fprintf(stderr, "Attempt to init inputs but not yet Init()-ialized.\n"); + return 0; + } + + inputs &= ~(output_bits_ | input_bits_ | reserved_bits_); +#ifdef ENABLE_WIDE_GPIO_COMPUTE_MODULE + const int kMaxAvailableBit = 45; + uses_64_bit_ |= (inputs >> 32) != 0; +#else + const int kMaxAvailableBit = 31; +#endif + for (int b = 0; b <= kMaxAvailableBit; ++b) { + if (inputs & GPIO_BIT(b)) { + INP_GPIO(b); + } + } + input_bits_ |= inputs; + return inputs; +} + +// We are not interested in the _exact_ model, just good enough to determine +// What to do. +enum RaspberryPiModel { + PI_MODEL_1, + PI_MODEL_2, + PI_MODEL_3, + PI_MODEL_4 +}; + +static int ReadFileToBuffer(char *buffer, size_t size, const char *filename) { + const int fd = open(filename, O_RDONLY); + if (fd < 0) return -1; + ssize_t r = read(fd, buffer, size - 1); // assume one read enough + buffer[r >= 0 ? r : 0] = '\0'; + close(fd); + return r; +} + +static RaspberryPiModel DetermineRaspberryModel() { + char buffer[4096]; + if (ReadFileToBuffer(buffer, sizeof(buffer), "/proc/cpuinfo") < 0) { + fprintf(stderr, "Reading cpuinfo: Could not determine Pi model\n"); + return PI_MODEL_3; // safe guess fallback. + } + static const char RevisionTag[] = "Revision"; + const char *revision_key; + if ((revision_key = strstr(buffer, RevisionTag)) == NULL) { + fprintf(stderr, "non-existent Revision: Could not determine Pi model\n"); + return PI_MODEL_3; + } + unsigned int pi_revision; + if (sscanf(index(revision_key, ':') + 1, "%x", &pi_revision) != 1) { + fprintf(stderr, "Unknown Revision: Could not determine Pi model\n"); + return PI_MODEL_3; + } + + // https://www.raspberrypi.org/documentation/hardware/raspberrypi/revision-codes/README.md + const unsigned pi_type = (pi_revision >> 4) & 0xff; + switch (pi_type) { + case 0x00: /* A */ + case 0x01: /* B, Compute Module 1 */ + case 0x02: /* A+ */ + case 0x03: /* B+ */ + case 0x05: /* Alpha ?*/ + case 0x06: /* Compute Module1 */ + case 0x09: /* Zero */ + case 0x0c: /* Zero W */ + return PI_MODEL_1; + + case 0x04: /* Pi 2 */ + return PI_MODEL_2; + + case 0x11: /* Pi 4 */ + return PI_MODEL_4; + + default: /* a bunch of versions represneting Pi 3 */ + return PI_MODEL_3; + } +} + +static RaspberryPiModel GetPiModel() { + static RaspberryPiModel pi_model = DetermineRaspberryModel(); + return pi_model; +} + +static int GetNumCores() { + return GetPiModel() == PI_MODEL_1 ? 1 : 4; +} + +static uint32_t *mmap_bcm_register(off_t register_offset) { + off_t base = BCM2709_PERI_BASE; // safe fallback guess. + switch (GetPiModel()) { + case PI_MODEL_1: base = BCM2708_PERI_BASE; break; + case PI_MODEL_2: base = BCM2709_PERI_BASE; break; + case PI_MODEL_3: base = BCM2709_PERI_BASE; break; + case PI_MODEL_4: base = BCM2711_PERI_BASE; break; + } + + int mem_fd; + if ((mem_fd = open("/dev/mem", O_RDWR|O_SYNC) ) < 0) { + // Try to fall back to /dev/gpiomem. Unfortunately, that device + // is implemented in a way that it _only_ supports GPIO, not the + // other registers we need, such as PWM or COUNTER_1Mhz, which means + // we only can operate with degraded performance. + // + // But, instead of failing, mmap() then silently succeeds with the + // unsupported offset. So bail out here. + if (register_offset != GPIO_REGISTER_OFFSET) + return NULL; + + mem_fd = open("/dev/gpiomem", O_RDWR|O_SYNC); + if (mem_fd < 0) return NULL; + } + + uint32_t *result = + (uint32_t*) mmap(NULL, // Any adddress in our space will do + REGISTER_BLOCK_SIZE, // Map length + PROT_READ|PROT_WRITE, // Enable r/w on GPIO registers. + MAP_SHARED, + mem_fd, // File to map + base + register_offset // Offset to bcm register + ); + close(mem_fd); + + if (result == MAP_FAILED) { + perror("mmap error: "); + fprintf(stderr, "MMapping from base 0x%lx, offset 0x%lx\n", + base, register_offset); + return NULL; + } + return result; +} + +static bool mmap_all_bcm_registers_once() { + if (s_GPIO_registers != NULL) return true; // alrady done. + + // The common GPIO registers. + s_GPIO_registers = mmap_bcm_register(GPIO_REGISTER_OFFSET); + if (s_GPIO_registers == NULL) { + return false; + } + + // Time measurement. Might fail when run as non-root. + uint32_t *timereg = mmap_bcm_register(COUNTER_1Mhz_REGISTER_OFFSET); + if (timereg != NULL) { + s_Timer1Mhz = timereg + 1; + } + + // Hardware pin-pulser. Might fail when run as non-root. + s_PWM_registers = mmap_bcm_register(GPIO_PWM_BASE_OFFSET); + s_CLK_registers = mmap_bcm_register(GPIO_CLK_BASE_OFFSET); + + return true; +} + +bool GPIO::Init(int slowdown) { + slowdown_ = slowdown; + + // Pre-mmap all bcm registers we need now and possibly in the future, as to + // allow dropping privileges after GPIO::Init() even as some of these + // registers might be needed later. + if (!mmap_all_bcm_registers_once()) + return false; + + gpio_set_bits_low_ = s_GPIO_registers + (0x1C / sizeof(uint32_t)); + gpio_clr_bits_low_ = s_GPIO_registers + (0x28 / sizeof(uint32_t)); + gpio_read_bits_low_ = s_GPIO_registers + (0x34 / sizeof(uint32_t)); + +#ifdef ENABLE_WIDE_GPIO_COMPUTE_MODULE + gpio_set_bits_high_ = s_GPIO_registers + (0x20 / sizeof(uint32_t)); + gpio_clr_bits_high_ = s_GPIO_registers + (0x2C / sizeof(uint32_t)); + gpio_read_bits_high_ = s_GPIO_registers + (0x38 / sizeof(uint32_t)); +#endif + + return true; +} + +/* + * We support also other pinouts that don't have the OE- on the hardware + * PWM output pin, so we need to provide (impefect) 'manual' timing as well. + * Hence all various busy_wait_nano() implementations depending on the hardware. + */ + +// --- PinPulser. Private implementation parts. +namespace { +// Manual timers. +class Timers { +public: + static bool Init(); + static void sleep_nanos(long t); +}; + +// Simplest of PinPulsers. Uses somewhat jittery and manual timers +// to get the timing, but not optimal. +class TimerBasedPinPulser : public PinPulser { +public: + TimerBasedPinPulser(GPIO *io, gpio_bits_t bits, + const std::vector &nano_specs) + : io_(io), bits_(bits), nano_specs_(nano_specs) { + if (!s_Timer1Mhz) { + fprintf(stderr, "FYI: not running as root which means we can't properly " + "control timing unless this is a real-time kernel. Expect color " + "degradation. Consider running as root with sudo.\n"); + } + } + + virtual void SendPulse(int time_spec_number) { + io_->ClearBits(bits_); + Timers::sleep_nanos(nano_specs_[time_spec_number]); + io_->SetBits(bits_); + } + +private: + GPIO *const io_; + const gpio_bits_t bits_; + const std::vector nano_specs_; +}; + +static bool LinuxHasModuleLoaded(const char *name) { + FILE *f = fopen("/proc/modules", "r"); + if (f == NULL) return false; // don't care. + char buf[256]; + const size_t namelen = strlen(name); + bool found = false; + while (fgets(buf, sizeof(buf), f) != NULL) { + if (strncmp(buf, name, namelen) == 0) { + found = true; + break; + } + } + fclose(f); + return found; +} + +static void busy_wait_nanos_rpi_1(long nanos); +static void busy_wait_nanos_rpi_2(long nanos); +static void busy_wait_nanos_rpi_3(long nanos); +static void busy_wait_nanos_rpi_4(long nanos); +static void (*busy_wait_impl)(long) = busy_wait_nanos_rpi_3; + +// Best effort write to file. Used to set kernel parameters. +static void WriteTo(const char *filename, const char *str) { + const int fd = open(filename, O_WRONLY); + if (fd < 0) return; + (void) write(fd, str, strlen(str)); // Best effort. Ignore return value. + close(fd); +} + +// By default, the kernel applies some throtteling for realtime +// threads to prevent starvation of non-RT threads. But we +// really want all we can get iff the machine has more cores and +// our RT-thread is locked onto one of these. +// So let's tell it not to do that. +static void DisableRealtimeThrottling() { + if (GetNumCores() == 1) return; // Not safe if we don't have > 1 core. + // We need to leave the kernel a little bit of time, as it does not like + // us to hog the kernel solidly. The default of 950000 leaves 50ms that + // can generate visible flicker, so we reduce that to 1ms. + WriteTo("/proc/sys/kernel/sched_rt_runtime_us", "999000"); +} + +bool Timers::Init() { + if (!mmap_all_bcm_registers_once()) + return false; + + // Choose the busy-wait loop that fits our Pi. + switch (GetPiModel()) { + case PI_MODEL_1: busy_wait_impl = busy_wait_nanos_rpi_1; break; + case PI_MODEL_2: busy_wait_impl = busy_wait_nanos_rpi_2; break; + case PI_MODEL_3: busy_wait_impl = busy_wait_nanos_rpi_3; break; + case PI_MODEL_4: busy_wait_impl = busy_wait_nanos_rpi_4; break; + } + + DisableRealtimeThrottling(); + // If we have it, we run the update thread on core3. No perf-compromises: + WriteTo("/sys/devices/system/cpu/cpu3/cpufreq/scaling_governor", + "performance"); + return true; +} + +static uint32_t JitterAllowanceMicroseconds() { + // If this is a Raspberry Pi with more than one core, we add a bit of + // additional overhead measured up to the 99.999%-ile: we can allow to burn + // a bit more busy-wait CPU cycles to get the timing accurate as we have + // more CPU to spare. + switch (GetPiModel()) { + case PI_MODEL_1: + return EMPIRICAL_NANOSLEEP_OVERHEAD_US; // 99.9%-ile + case PI_MODEL_2: case PI_MODEL_3: + return EMPIRICAL_NANOSLEEP_OVERHEAD_US + 35; // 99.999%-ile + case PI_MODEL_4: + return EMPIRICAL_NANOSLEEP_OVERHEAD_US + 10; // this one is fast. + } + return EMPIRICAL_NANOSLEEP_OVERHEAD_US; +} + +void Timers::sleep_nanos(long nanos) { + // For smaller durations, we go straight to busy wait. + + // For larger duration, we use nanosleep() to give the operating system + // a chance to do something else. + + // However, these timings have a lot of jitter, so if we have the 1Mhz timer + // available, we use that to accurately mesure time spent and do the + // remaining time with busy wait. If we don't have the timer available + // (not running as root), we just use nanosleep() for larger values. + + if (s_Timer1Mhz) { + static long kJitterAllowanceNanos = JitterAllowanceMicroseconds() * 1000; + if (nanos > kJitterAllowanceNanos + MINIMUM_NANOSLEEP_TIME_US*1000) { + const uint32_t before = *s_Timer1Mhz; + struct timespec sleep_time = { 0, nanos - kJitterAllowanceNanos }; + nanosleep(&sleep_time, NULL); + const uint32_t after = *s_Timer1Mhz; + const long nanoseconds_passed = 1000 * (uint32_t)(after - before); + if (nanoseconds_passed > nanos) { + return; // darn, missed it. + } else { + nanos -= nanoseconds_passed; // remaining time with busy-loop + } + } + } else { + // Not running as root, not having access to 1Mhz timer. Approximate large + // durations with nanosleep(); small durations are done with busy wait. + if (nanos > (EMPIRICAL_NANOSLEEP_OVERHEAD_US + MINIMUM_NANOSLEEP_TIME_US)*1000) { + struct timespec sleep_time + = { 0, nanos - EMPIRICAL_NANOSLEEP_OVERHEAD_US*1000 }; + nanosleep(&sleep_time, NULL); + return; + } + } + + busy_wait_impl(nanos); // Use model-specific busy-loop for remaining time. +} + +static void busy_wait_nanos_rpi_1(long nanos) { + if (nanos < 70) return; + // The following loop is determined empirically on a 700Mhz RPi + for (uint32_t i = (nanos - 70) >> 2; i != 0; --i) { + asm("nop"); + } +} + +static void busy_wait_nanos_rpi_2(long nanos) { + if (nanos < 20) return; + // The following loop is determined empirically on a 900Mhz RPi 2 + for (uint32_t i = (nanos - 20) * 100 / 110; i != 0; --i) { + asm(""); + } +} + +static void busy_wait_nanos_rpi_3(long nanos) { + if (nanos < 20) return; + for (uint32_t i = (nanos - 15) * 100 / 73; i != 0; --i) { + asm(""); + } +} + +static void busy_wait_nanos_rpi_4(long nanos) { + if (nanos < 20) return; + // Interesting, the Pi4 is _slower_ than the Pi3 ? At least for this busy loop + for (uint32_t i = (nanos - 5) * 100 / 132; i != 0; --i) { + asm(""); + } +} + +#if DEBUG_SLEEP_JITTER +static int overshoot_histogram_us[256] = {0}; +static void print_overshoot_histogram() { + fprintf(stderr, "Overshoot histogram >= empirical overhead of %dus\n" + "%6s | %7s | %7s\n", + JitterAllowanceMicroseconds(), "usec", "count", "accum"); + int total_count = 0; + for (int i = 0; i < 256; ++i) total_count += overshoot_histogram_us[i]; + int running_count = 0; + for (int us = 0; us < 256; ++us) { + const int count = overshoot_histogram_us[us]; + if (count > 0) { + running_count += count; + fprintf(stderr, "%s%3dus: %8d %7.3f%%\n", (us == 0) ? "<=" : " +", + us, count, 100.0 * running_count / total_count); + } + } +} +#endif + +// A PinPulser that uses the PWM hardware to create accurate pulses. +// It only works on GPIO-12 or 18 though. +class HardwarePinPulser : public PinPulser { +public: + static bool CanHandle(gpio_bits_t gpio_mask) { +#ifdef DISABLE_HARDWARE_PULSES + return false; +#else + const bool can_handle = gpio_mask==GPIO_BIT(18) || gpio_mask==GPIO_BIT(12); + if (can_handle && (s_PWM_registers == NULL || s_CLK_registers == NULL)) { + // Instead of silently not using the hardware pin pulser and falling back + // to timing based loops, complain loudly and request the user to make + // a choice before continuing. + fprintf(stderr, "Need root. You are configured to use the hardware pulse " + "generator " + "for\n\tsmooth color rendering, however the necessary hardware\n" + "\tregisters can't be accessed because you probably don't run\n" + "\twith root permissions or privileges have been dropped.\n" + "\tSo you either have to run as root (e.g. using sudo) or\n" + "\tsupply the --led-no-hardware-pulse command-line flag.\n\n" + "\tExiting; run as root or with --led-no-hardware-pulse\n\n"); + exit(1); + } + return can_handle; +#endif + } + + HardwarePinPulser(gpio_bits_t pins, const std::vector &specs) + : triggered_(false) { + assert(CanHandle(pins)); + assert(s_CLK_registers && s_PWM_registers && s_Timer1Mhz); + +#if DEBUG_SLEEP_JITTER + atexit(print_overshoot_histogram); +#endif + + if (LinuxHasModuleLoaded("snd_bcm2835")) { + fprintf(stderr, + "\n%s=== snd_bcm2835: found that the Pi sound module is loaded. ===%s\n" + "Don't use the built-in sound of the Pi together with this lib; it is known to be\n" + "incompatible and cause trouble and hangs (you can still use external USB sound adapters).\n\n" + "See Troubleshooting section in README how to disable the sound module.\n" + "You can also run with --led-no-hardware-pulse to avoid the incompatibility,\n" + "but you will have more flicker.\n" + "Exiting; fix the above first or use --led-no-hardware-pulse\n\n", + "\033[1;31m", "\033[0m"); + exit(1); + } + + for (size_t i = 0; i < specs.size(); ++i) { + // Hints how long to nanosleep, already corrected for system overhead. + sleep_hints_us_.push_back(specs[i]/1000 - JitterAllowanceMicroseconds()); + } + + const int base = specs[0]; + // Get relevant registers + fifo_ = s_PWM_registers + PWM_FIFO; + + if (pins == GPIO_BIT(18)) { + // set GPIO 18 to PWM0 mode (Alternative 5) + SetGPIOMode(s_GPIO_registers, 18, 2); + } else if (pins == GPIO_BIT(12)) { + // set GPIO 12 to PWM0 mode (Alternative 0) + SetGPIOMode(s_GPIO_registers, 12, 4); + } else { + assert(false); // should've been caught by CanHandle() + } + InitPWMDivider((base/2) / PWM_BASE_TIME_NS); + for (size_t i = 0; i < specs.size(); ++i) { + pwm_range_.push_back(2 * specs[i] / base); + } + } + + virtual void SendPulse(int c) { + if (pwm_range_[c] < 16) { + s_PWM_registers[PWM_RNG1] = pwm_range_[c]; + + *fifo_ = pwm_range_[c]; + } else { + // Keep the actual range as short as possible, as we have to + // wait for one full period of these in the zero phase. + // The hardware can't deal with values < 2, so only do this when + // have enough of these. + s_PWM_registers[PWM_RNG1] = pwm_range_[c] / 8; + + *fifo_ = pwm_range_[c] / 8; + *fifo_ = pwm_range_[c] / 8; + *fifo_ = pwm_range_[c] / 8; + *fifo_ = pwm_range_[c] / 8; + *fifo_ = pwm_range_[c] / 8; + *fifo_ = pwm_range_[c] / 8; + *fifo_ = pwm_range_[c] / 8; + *fifo_ = pwm_range_[c] / 8; + } + + /* + * We need one value at the end to have it go back to + * default state (otherwise it just repeats the last + * value, so will be constantly 'on'). + */ + *fifo_ = 0; // sentinel. + + /* + * For some reason, we need a second empty sentinel in the + * fifo, otherwise our way to detect the end of the pulse, + * which relies on 'is the queue empty' does not work. It is + * not entirely clear why that is from the datasheet, + * but probably there is some buffering register in which data + * elements are kept after the fifo is emptied. + */ + *fifo_ = 0; + + sleep_hint_us_ = sleep_hints_us_[c]; + start_time_ = *s_Timer1Mhz; + triggered_ = true; + s_PWM_registers[PWM_CTL] = PWM_CTL_USEF1 | PWM_CTL_PWEN1 | PWM_CTL_POLA1; + } + + virtual void WaitPulseFinished() { + if (!triggered_) return; + // Determine how long we already spent and sleep to get close to the + // actual end-time of our sleep period. + // + // TODO(hzeller): find if it is possible to get some sort of interrupt from + // the hardware once it is done with the pulse. Sounds silly that there is + // not (so far, only tested GPIO interrupt with a feedback line, but that + // is super-slow with 20μs overhead). + if (sleep_hint_us_ > 0) { + const uint32_t already_elapsed_usec = *s_Timer1Mhz - start_time_; + const int to_sleep_us = sleep_hint_us_ - already_elapsed_usec; + if (to_sleep_us > 0) { + struct timespec sleep_time = { 0, 1000 * to_sleep_us }; + nanosleep(&sleep_time, NULL); + +#if DEBUG_SLEEP_JITTER + { + // Record histogram of realtime jitter how much longer we actually + // took. + const int total_us = *s_Timer1Mhz - start_time_; + const int nanoslept_us = total_us - already_elapsed_usec; + int overshoot = nanoslept_us - (to_sleep_us + JitterAllowanceMicroseconds()); + if (overshoot < 0) overshoot = 0; + if (overshoot > 255) overshoot = 255; + overshoot_histogram_us[overshoot]++; + } +#endif + } + } + + while ((s_PWM_registers[PWM_STA] & PWM_STA_EMPT1) == 0) { + // busy wait until done. + } + s_PWM_registers[PWM_CTL] = PWM_CTL_USEF1 | PWM_CTL_POLA1 | PWM_CTL_CLRF1; + triggered_ = false; + } + +private: + void SetGPIOMode(volatile uint32_t *gpioReg, unsigned gpio, unsigned mode) { + const int reg = gpio / 10; + const int mode_pos = (gpio % 10) * 3; + gpioReg[reg] = (gpioReg[reg] & ~(7 << mode_pos)) | (mode << mode_pos); + } + + void InitPWMDivider(uint32_t divider) { + assert(divider < (1<<12)); // we only have 12 bits. + + s_PWM_registers[PWM_CTL] = PWM_CTL_USEF1 | PWM_CTL_POLA1 | PWM_CTL_CLRF1; + + // reset PWM clock + s_CLK_registers[CLK_PWMCTL] = CLK_PASSWD | CLK_CTL_KILL; + + // set PWM clock source as 500 MHz PLLD + s_CLK_registers[CLK_PWMCTL] = CLK_PASSWD | CLK_CTL_SRC(CLK_CTL_SRC_PLLD); + + // set PWM clock divider + s_CLK_registers[CLK_PWMDIV] + = CLK_PASSWD | CLK_DIV_DIVI(divider) | CLK_DIV_DIVF(0); + + // enable PWM clock + s_CLK_registers[CLK_PWMCTL] + = CLK_PASSWD | CLK_CTL_ENAB | CLK_CTL_SRC(CLK_CTL_SRC_PLLD); + } + +private: + std::vector pwm_range_; + std::vector sleep_hints_us_; + volatile uint32_t *fifo_; + uint32_t start_time_; + int sleep_hint_us_; + bool triggered_; +}; + +} // end anonymous namespace + +// Public PinPulser factory +PinPulser *PinPulser::Create(GPIO *io, gpio_bits_t gpio_mask, + bool allow_hardware_pulsing, + const std::vector &nano_wait_spec) { + if (!Timers::Init()) return NULL; + if (allow_hardware_pulsing && HardwarePinPulser::CanHandle(gpio_mask)) { + return new HardwarePinPulser(gpio_mask, nano_wait_spec); + } else { + return new TimerBasedPinPulser(io, gpio_mask, nano_wait_spec); + } +} + +// For external use, e.g. in the matrix for extra time. +uint32_t GetMicrosecondCounter() { + if (s_Timer1Mhz) return *s_Timer1Mhz; + + // When run as non-root, we can't read the timer. Fall back to slow + // operating-system ways. + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + const uint64_t micros = ts.tv_nsec / 1000; + const uint64_t epoch_usec = (uint64_t)ts.tv_sec * 1000000 + micros; + return epoch_usec & 0xFFFFFFFF; +} + +} // namespace rgb_matrix diff --git a/lib/gpio.h b/lib/gpio.h new file mode 100644 index 0000000..95330b9 --- /dev/null +++ b/lib/gpio.h @@ -0,0 +1,153 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2013 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +#ifndef RPI_GPIO_INTERNAL_H +#define RPI_GPIO_INTERNAL_H + +#include "gpio-bits.h" + +#include + +// Putting this in our namespace to not collide with other things called like +// this. +namespace rgb_matrix { +// For now, everything is initialized as output. +class GPIO { +public: + GPIO(); + + // Initialize before use. Returns 'true' if successful, 'false' otherwise + // (e.g. due to a permission problem). + bool Init(int +#if RGB_SLOWDOWN_GPIO + slowdown = RGB_SLOWDOWN_GPIO +#else + slowdown = 1 +#endif + ); + + + // Initialize outputs. + // Returns the bits that were available and could be set for output. + // (never use the optional adafruit_hack_needed parameter, it is used + // internally to this library). + gpio_bits_t InitOutputs(gpio_bits_t outputs, + bool adafruit_hack_needed = false); + + // Request given bitmap of GPIO inputs. + // Returns the bits that were available and could be reserved. + gpio_bits_t RequestInputs(gpio_bits_t inputs); + + // Set the bits that are '1' in the output. Leave the rest untouched. + inline void SetBits(gpio_bits_t value) { + if (!value) return; + WriteSetBits(value); + for (int i = 0; i < slowdown_; ++i) { + WriteSetBits(value); + } + } + + // Clear the bits that are '1' in the output. Leave the rest untouched. + inline void ClearBits(gpio_bits_t value) { + if (!value) return; + WriteClrBits(value); + for (int i = 0; i < slowdown_; ++i) { + WriteClrBits(value); + } + } + + // Write all the bits of "value" mentioned in "mask". Leave the rest untouched. + inline void WriteMaskedBits(gpio_bits_t value, gpio_bits_t mask) { + // Writing a word is two operations. The IO is actually pretty slow, so + // this should probably be unnoticable. + ClearBits(~value & mask); + SetBits(value & mask); + } + + inline gpio_bits_t Read() const { return ReadRegisters() & input_bits_; } + +private: + inline gpio_bits_t ReadRegisters() const { + return (static_cast(*gpio_read_bits_low_) +#ifdef ENABLE_WIDE_GPIO_COMPUTE_MODULE + | (static_cast(*gpio_read_bits_low_) << 32) +#endif + ); + } + + inline void WriteSetBits(gpio_bits_t value) { + *gpio_set_bits_low_ = static_cast(value & 0xFFFFFFFF); +#ifdef ENABLE_WIDE_GPIO_COMPUTE_MODULE + if (uses_64_bit_) + *gpio_set_bits_high_ = static_cast(value >> 32); +#endif + } + + inline void WriteClrBits(gpio_bits_t value) { + *gpio_clr_bits_low_ = static_cast(value & 0xFFFFFFFF); +#ifdef ENABLE_WIDE_GPIO_COMPUTE_MODULE + if (uses_64_bit_) + *gpio_clr_bits_high_ = static_cast(value >> 32); +#endif + } + +private: + gpio_bits_t output_bits_; + gpio_bits_t input_bits_; + gpio_bits_t reserved_bits_; + int slowdown_; + + volatile uint32_t *gpio_set_bits_low_; + volatile uint32_t *gpio_clr_bits_low_; + volatile uint32_t *gpio_read_bits_low_; + +#ifdef ENABLE_WIDE_GPIO_COMPUTE_MODULE + bool uses_64_bit_; + volatile uint32_t *gpio_set_bits_high_; + volatile uint32_t *gpio_clr_bits_high_; + volatile uint32_t *gpio_read_bits_high_; +#endif +}; + +// A PinPulser is a utility class that pulses a GPIO pin. There can be various +// implementations. +class PinPulser { +public: + // Factory for a PinPulser. Chooses the right implementation depending + // on the context (CPU and which pins are affected). + // "gpio_mask" is the mask that should be output (since we only + // need negative pulses, this is what it does) + // "nano_wait_spec" contains a list of time periods we'd like + // invoke later. This can be used to pre-process timings if needed. + static PinPulser *Create(GPIO *io, gpio_bits_t gpio_mask, + bool allow_hardware_pulsing, + const std::vector &nano_wait_spec); + + virtual ~PinPulser() {} + + // Send a pulse with a given length (index into nano_wait_spec array). + virtual void SendPulse(int time_spec_number) = 0; + + // If SendPulse() is asynchronously implemented, wait for pulse to finish. + virtual void WaitPulseFinished() {} +}; + +// Get rolling over microsecond counter. We get this from a hardware register +// if possible and a terrible slow fallback otherwise. +uint32_t GetMicrosecondCounter(); + +} // end namespace rgb_matrix + +#endif // RPI_GPIO_INGERNALH diff --git a/lib/gpio.o b/lib/gpio.o new file mode 100644 index 0000000..74bc733 Binary files /dev/null and b/lib/gpio.o differ diff --git a/lib/graphics.cc b/lib/graphics.cc new file mode 100644 index 0000000..7c2c16d --- /dev/null +++ b/lib/graphics.cc @@ -0,0 +1,172 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2014 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +#include "graphics.h" +#include "utf8-internal.h" + +#include +#include +#include + +namespace rgb_matrix { +bool SetImage(Canvas *c, int canvas_offset_x, int canvas_offset_y, + const uint8_t *buffer, size_t size, + const int width, const int height, + bool is_bgr) { + if (3 * width * height != (int)size) // Sanity check + return false; + + int image_display_w = width; + int image_display_h = height; + + size_t skip_start_row = 0; // Bytes to skip before each row + if (canvas_offset_x < 0) { + skip_start_row = -canvas_offset_x * 3; + image_display_w += canvas_offset_x; + if (image_display_w <= 0) return false; // Done. outside canvas. + canvas_offset_x = 0; + } + if (canvas_offset_y < 0) { + // Skip buffer to the first row we'll be showing + buffer += 3 * width * -canvas_offset_y; + image_display_h += canvas_offset_y; + if (image_display_h <= 0) return false; // Done. outside canvas. + canvas_offset_y = 0; + } + const int w = std::min(c->width(), canvas_offset_x + image_display_w); + const int h = std::min(c->height(), canvas_offset_y + image_display_h); + + // Bytes to skip for wider than canvas image at the end of a row + const size_t skip_end_row = (canvas_offset_x + image_display_w > w) + ? (canvas_offset_x + image_display_w - w) * 3 + : 0; + + // Let's make this a combined skip per row and ajust where we start. + const size_t next_row_skip = skip_start_row + skip_end_row; + buffer += skip_start_row; + + if (is_bgr) { + for (int y = canvas_offset_y; y < h; ++y) { + for (int x = canvas_offset_x; x < w; ++x) { + c->SetPixel(x, y, buffer[2], buffer[1], buffer[0]); + buffer += 3; + } + buffer += next_row_skip; + } + } else { + for (int y = canvas_offset_y; y < h; ++y) { + for (int x = canvas_offset_x; x < w; ++x) { + c->SetPixel(x, y, buffer[0], buffer[1], buffer[2]); + buffer += 3; + } + buffer += next_row_skip; + } + } + return true; +} + +int DrawText(Canvas *c, const Font &font, + int x, int y, const Color &color, + const char *utf8_text) { + return DrawText(c, font, x, y, color, NULL, utf8_text); +} + +int DrawText(Canvas *c, const Font &font, + int x, int y, const Color &color, const Color *background_color, + const char *utf8_text, int extra_spacing) { + const int start_x = x; + while (*utf8_text) { + const uint32_t cp = utf8_next_codepoint(utf8_text); + x += font.DrawGlyph(c, x, y, color, background_color, cp); + x += extra_spacing; + } + return x - start_x; +} + +// There used to be a symbol without the optional extra_spacing parameter. Let's +// define this here so that people linking against an old library will still +// have their code usable. Now: 2017-06-04; can probably be removed in a couple +// of months. +int DrawText(Canvas *c, const Font &font, + int x, int y, const Color &color, const Color *background_color, + const char *utf8_text) { + return DrawText(c, font, x, y, color, background_color, utf8_text, 0); +} + +int VerticalDrawText(Canvas *c, const Font &font, int x, int y, + const Color &color, const Color *background_color, + const char *utf8_text, int extra_spacing) { + const int start_y = y; + while (*utf8_text) { + const uint32_t cp = utf8_next_codepoint(utf8_text); + font.DrawGlyph(c, x, y, color, background_color, cp); + y += font.height() + extra_spacing; + } + return y - start_y; +} + +void DrawCircle(Canvas *c, int x0, int y0, int radius, const Color &color) { + int x = radius, y = 0; + int radiusError = 1 - x; + + while (y <= x) { + c->SetPixel(x + x0, y + y0, color.r, color.g, color.b); + c->SetPixel(y + x0, x + y0, color.r, color.g, color.b); + c->SetPixel(-x + x0, y + y0, color.r, color.g, color.b); + c->SetPixel(-y + x0, x + y0, color.r, color.g, color.b); + c->SetPixel(-x + x0, -y + y0, color.r, color.g, color.b); + c->SetPixel(-y + x0, -x + y0, color.r, color.g, color.b); + c->SetPixel(x + x0, -y + y0, color.r, color.g, color.b); + c->SetPixel(y + x0, -x + y0, color.r, color.g, color.b); + y++; + if (radiusError<0){ + radiusError += 2 * y + 1; + } else { + x--; + radiusError+= 2 * (y - x + 1); + } + } +} + +void DrawLine(Canvas *c, int x0, int y0, int x1, int y1, const Color &color) { + int dy = y1 - y0, dx = x1 - x0, gradient, x, y, shift = 0x10; + + if (abs(dx) > abs(dy)) { + // x variation is bigger than y variation + if (x1 < x0) { + std::swap(x0, x1); + std::swap(y0, y1); + } + gradient = (dy << shift) / dx ; + + for (x = x0 , y = 0x8000 + (y0 << shift); x <= x1; ++x, y += gradient) { + c->SetPixel(x, y >> shift, color.r, color.g, color.b); + } + } else if (dy != 0) { + // y variation is bigger than x variation + if (y1 < y0) { + std::swap(x0, x1); + std::swap(y0, y1); + } + gradient = (dx << shift) / dy; + for (y = y0 , x = 0x8000 + (x0 << shift); y <= y1; ++y, x += gradient) { + c->SetPixel(x >> shift, y, color.r, color.g, color.b); + } + } else { + c->SetPixel(x0, y0, color.r, color.g, color.b); + } +} + +}//namespace diff --git a/lib/graphics.o b/lib/graphics.o new file mode 100644 index 0000000..236f079 Binary files /dev/null and b/lib/graphics.o differ diff --git a/lib/hardware-mapping.c b/lib/hardware-mapping.c new file mode 100644 index 0000000..758b428 --- /dev/null +++ b/lib/hardware-mapping.c @@ -0,0 +1,287 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- + * Copyright (C) 2013, 2016 Henner Zeller + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 2. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +/* + * We do this in plain C so that we can use designated initializers. + */ +#include "hardware-mapping.h" + +#define GPIO_BIT(b) ((uint64_t)1<<(b)) + +struct HardwareMapping matrix_hardware_mappings[] = { + /* + * The regular hardware mapping described in the wiring.md and used + * by the adapter PCBs. + */ + { + .name = "regular", + + .output_enable = GPIO_BIT(18), + .clock = GPIO_BIT(17), + .strobe = GPIO_BIT(4), + + /* Address lines */ + .a = GPIO_BIT(22), + .b = GPIO_BIT(23), + .c = GPIO_BIT(24), + .d = GPIO_BIT(25), + .e = GPIO_BIT(15), /* RxD kept free unless 1:64 */ + + /* Parallel chain 0, RGB for both sub-panels */ + .p0_r1 = GPIO_BIT(11), /* masks: SPI0_SCKL */ + .p0_g1 = GPIO_BIT(27), /* Not on RPi1, Rev1; use "regular-pi1" instead */ + .p0_b1 = GPIO_BIT(7), /* masks: SPI0_CE1 */ + .p0_r2 = GPIO_BIT(8), /* masks: SPI0_CE0 */ + .p0_g2 = GPIO_BIT(9), /* masks: SPI0_MISO */ + .p0_b2 = GPIO_BIT(10), /* masks: SPI0_MOSI */ + + /* All the following are only available with 40 GPIP pins, on A+/B+/Pi2,3 */ + /* Chain 1 */ + .p1_r1 = GPIO_BIT(12), + .p1_g1 = GPIO_BIT(5), + .p1_b1 = GPIO_BIT(6), + .p1_r2 = GPIO_BIT(19), + .p1_g2 = GPIO_BIT(13), + .p1_b2 = GPIO_BIT(20), + + /* Chain 2 */ + .p2_r1 = GPIO_BIT(14), /* masks TxD when parallel=3 */ + .p2_g1 = GPIO_BIT(2), /* masks SCL when parallel=3 */ + .p2_b1 = GPIO_BIT(3), /* masks SDA when parallel=3 */ + .p2_r2 = GPIO_BIT(26), + .p2_g2 = GPIO_BIT(16), + .p2_b2 = GPIO_BIT(21), + }, + + /* + * This is used if you have an Adafruit HAT in the default configuration + */ + { + .name = "adafruit-hat", + + .output_enable = GPIO_BIT(4), + .clock = GPIO_BIT(17), + .strobe = GPIO_BIT(21), + + .a = GPIO_BIT(22), + .b = GPIO_BIT(26), + .c = GPIO_BIT(27), + .d = GPIO_BIT(20), + .e = GPIO_BIT(24), /* Needs manual wiring, see README.md */ + + .p0_r1 = GPIO_BIT(5), + .p0_g1 = GPIO_BIT(13), + .p0_b1 = GPIO_BIT(6), + .p0_r2 = GPIO_BIT(12), + .p0_g2 = GPIO_BIT(16), + .p0_b2 = GPIO_BIT(23), + }, + + /* + * An Adafruit HAT with the PWM modification + */ + { + .name = "adafruit-hat-pwm", + + .output_enable = GPIO_BIT(18), /* The only change compared to above */ + .clock = GPIO_BIT(17), + .strobe = GPIO_BIT(21), + + .a = GPIO_BIT(22), + .b = GPIO_BIT(26), + .c = GPIO_BIT(27), + .d = GPIO_BIT(20), + .e = GPIO_BIT(24), + + .p0_r1 = GPIO_BIT(5), + .p0_g1 = GPIO_BIT(13), + .p0_b1 = GPIO_BIT(6), + .p0_r2 = GPIO_BIT(12), + .p0_g2 = GPIO_BIT(16), + .p0_b2 = GPIO_BIT(23), + }, + + /* + * The regular pin-out, but for Raspberry Pi1. The very first Pi1 Rev1 uses + * the same pin for GPIO-21 as later Pis use GPIO-27. Make it work for both. + */ + { + .name = "regular-pi1", + + .output_enable = GPIO_BIT(18), + .clock = GPIO_BIT(17), + .strobe = GPIO_BIT(4), + + /* Address lines */ + .a = GPIO_BIT(22), + .b = GPIO_BIT(23), + .c = GPIO_BIT(24), + .d = GPIO_BIT(25), + .e = GPIO_BIT(15), /* RxD kept free unless 1:64 */ + + /* Parallel chain 0, RGB for both sub-panels */ + .p0_r1 = GPIO_BIT(11), /* masks: SPI0_SCKL */ + /* On Pi1 Rev1, the pin other Pis have GPIO27, these have GPIO21. So make + * this work for both Rev1 and Rev2. + */ + .p0_g1 = GPIO_BIT(21) | GPIO_BIT(27), + .p0_b1 = GPIO_BIT(7), /* masks: SPI0_CE1 */ + .p0_r2 = GPIO_BIT(8), /* masks: SPI0_CE0 */ + .p0_g2 = GPIO_BIT(9), /* masks: SPI0_MISO */ + .p0_b2 = GPIO_BIT(10), /* masks: SPI0_MOSI */ + + /* No more chains - there are not enough GPIO */ + }, + + /* + * Classic: Early forms of this library had this as default mapping, mostly + * derived from the 26 GPIO-header version so that it also can work + * on 40 Pin GPIO headers with more parallel chains. + * Not used anymore. + */ + { + .name = "classic", + + .output_enable = GPIO_BIT(27), /* Not available on RPi1, Rev 1 */ + .clock = GPIO_BIT(11), + .strobe = GPIO_BIT(4), + + .a = GPIO_BIT(7), + .b = GPIO_BIT(8), + .c = GPIO_BIT(9), + .d = GPIO_BIT(10), + + .p0_r1 = GPIO_BIT(17), + .p0_g1 = GPIO_BIT(18), + .p0_b1 = GPIO_BIT(22), + .p0_r2 = GPIO_BIT(23), + .p0_g2 = GPIO_BIT(24), + .p0_b2 = GPIO_BIT(25), + + .p1_r1 = GPIO_BIT(12), + .p1_g1 = GPIO_BIT(5), + .p1_b1 = GPIO_BIT(6), + .p1_r2 = GPIO_BIT(19), + .p1_g2 = GPIO_BIT(13), + .p1_b2 = GPIO_BIT(20), + + .p2_r1 = GPIO_BIT(14), /* masks TxD if parallel = 3 */ + .p2_g1 = GPIO_BIT(2), /* masks SDA if parallel = 3 */ + .p2_b1 = GPIO_BIT(3), /* masks SCL if parallel = 3 */ + .p2_r2 = GPIO_BIT(15), + .p2_g2 = GPIO_BIT(26), + .p2_b2 = GPIO_BIT(21), + }, + + /* + * Classic pin-out for Rev-A Raspberry Pi. + */ + { + .name = "classic-pi1", + + /* The Revision-1 and Revision-2 boards have different GPIO mappings + * on the P1-3 and P1-5. So we use both interpretations. + * To keep the I2C pins free, we avoid these in later mappings. + */ + .output_enable = GPIO_BIT(0) | GPIO_BIT(2), + .clock = GPIO_BIT(1) | GPIO_BIT(3), + .strobe = GPIO_BIT(4), + + .a = GPIO_BIT(7), + .b = GPIO_BIT(8), + .c = GPIO_BIT(9), + .d = GPIO_BIT(10), + + .p0_r1 = GPIO_BIT(17), + .p0_g1 = GPIO_BIT(18), + .p0_b1 = GPIO_BIT(22), + .p0_r2 = GPIO_BIT(23), + .p0_g2 = GPIO_BIT(24), + .p0_b2 = GPIO_BIT(25), + }, + +#ifdef ENABLE_WIDE_GPIO_COMPUTE_MODULE + /* + * Custom pin-out for compute-module + */ + { + .name = "compute-module", + + /* This GPIO mapping is made for the official I/O development + * board. No pin is left free when using 6 parallel chains. + */ + .output_enable = GPIO_BIT(18), + .clock = GPIO_BIT(16), + .strobe = GPIO_BIT(17), + + .a = GPIO_BIT(2), + .b = GPIO_BIT(3), + .c = GPIO_BIT(4), + .d = GPIO_BIT(5), + .e = GPIO_BIT(6), /* RxD kept free unless 1:64 */ + + /* Chain 0 */ + .p0_r1 = GPIO_BIT(7), + .p0_g1 = GPIO_BIT(8), + .p0_b1 = GPIO_BIT(9), + .p0_r2 = GPIO_BIT(10), + .p0_g2 = GPIO_BIT(11), + .p0_b2 = GPIO_BIT(12), + + /* Chain 1 */ + .p1_r1 = GPIO_BIT(13), + .p1_g1 = GPIO_BIT(14), + .p1_b1 = GPIO_BIT(15), + .p1_r2 = GPIO_BIT(19), + .p1_g2 = GPIO_BIT(20), + .p1_b2 = GPIO_BIT(21), + + /* Chain 2 */ + .p2_r1 = GPIO_BIT(22), + .p2_g1 = GPIO_BIT(23), + .p2_b1 = GPIO_BIT(24), + .p2_r2 = GPIO_BIT(25), + .p2_g2 = GPIO_BIT(26), + .p2_b2 = GPIO_BIT(27), + + /* Chain 3 */ + .p3_r1 = GPIO_BIT(28), + .p3_g1 = GPIO_BIT(29), + .p3_b1 = GPIO_BIT(30), + .p3_r2 = GPIO_BIT(31), + .p3_g2 = GPIO_BIT(32), + .p3_b2 = GPIO_BIT(33), + + /* Chain 4 */ + .p4_r1 = GPIO_BIT(34), + .p4_g1 = GPIO_BIT(35), + .p4_b1 = GPIO_BIT(36), + .p4_r2 = GPIO_BIT(37), + .p4_g2 = GPIO_BIT(38), + .p4_b2 = GPIO_BIT(39), + + /* Chain 5 */ + .p5_r1 = GPIO_BIT(40), + .p5_g1 = GPIO_BIT(41), + .p5_b1 = GPIO_BIT(42), + .p5_r2 = GPIO_BIT(43), + .p5_g2 = GPIO_BIT(44), + .p5_b2 = GPIO_BIT(45), + }, +#endif + + {0} +}; diff --git a/lib/hardware-mapping.h b/lib/hardware-mapping.h new file mode 100644 index 0000000..df4b440 --- /dev/null +++ b/lib/hardware-mapping.h @@ -0,0 +1,60 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- + * Copyright (C) 2013 Henner Zeller + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 2. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ +#ifndef RPI_HARDWARE_MAPPING_H +#define RPI_HARDWARE_MAPPING_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "gpio-bits.h" + +struct HardwareMapping { + const char *name; + int max_parallel_chains; + + gpio_bits_t output_enable; + gpio_bits_t clock; + gpio_bits_t strobe; + + gpio_bits_t a, b, c, d, e; + + gpio_bits_t p0_r1, p0_g1, p0_b1; + gpio_bits_t p0_r2, p0_g2, p0_b2; + + gpio_bits_t p1_r1, p1_g1, p1_b1; + gpio_bits_t p1_r2, p1_g2, p1_b2; + + gpio_bits_t p2_r1, p2_g1, p2_b1; + gpio_bits_t p2_r2, p2_g2, p2_b2; + + gpio_bits_t p3_r1, p3_g1, p3_b1; + gpio_bits_t p3_r2, p3_g2, p3_b2; + + gpio_bits_t p4_r1, p4_g1, p4_b1; + gpio_bits_t p4_r2, p4_g2, p4_b2; + + gpio_bits_t p5_r1, p5_g1, p5_b1; + gpio_bits_t p5_r2, p5_g2, p5_b2; +}; + +extern struct HardwareMapping matrix_hardware_mappings[]; + +#ifdef __cplusplus +} // extern C +#endif + +#endif diff --git a/lib/hardware-mapping.o b/lib/hardware-mapping.o new file mode 100644 index 0000000..2aeed6b Binary files /dev/null and b/lib/hardware-mapping.o differ diff --git a/lib/led-matrix-c.cc b/lib/led-matrix-c.cc new file mode 100644 index 0000000..eb98121 --- /dev/null +++ b/lib/led-matrix-c.cc @@ -0,0 +1,304 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2013 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// +// C-bridge for led matrix. +#include "led-matrix-c.h" + +#include +#include + +#include "led-matrix.h" +#include "graphics.h" + +// Make sure C++ is in sync with C +static_assert(sizeof(rgb_matrix::RGBMatrix::Options) == sizeof(RGBLedMatrixOptions)); +static_assert(sizeof(rgb_matrix::RuntimeOptions) == sizeof(RGBLedRuntimeOptions)); + +// Our opaque dummy structs to communicate with the c-world +struct RGBLedMatrix {}; +struct LedCanvas {}; +struct LedFont {}; + + +static rgb_matrix::RGBMatrix *to_matrix(struct RGBLedMatrix *matrix) { + return reinterpret_cast(matrix); +} +static struct RGBLedMatrix *from_matrix(rgb_matrix::RGBMatrix *matrix) { + return reinterpret_cast(matrix); +} + +static rgb_matrix::FrameCanvas *to_canvas(struct LedCanvas *canvas) { + return reinterpret_cast(canvas); +} +static struct LedCanvas *from_canvas(rgb_matrix::FrameCanvas *canvas) { + return reinterpret_cast(canvas); +} + +static rgb_matrix::Font *to_font(struct LedFont *font) { + return reinterpret_cast(font); +} +static struct LedFont *from_font(rgb_matrix::Font *font) { + return reinterpret_cast(font); +} + + +static struct RGBLedMatrix *led_matrix_create_from_options_optional_edit( + struct RGBLedMatrixOptions *opts, struct RGBLedRuntimeOptions *rt_opts, + int *argc, char ***argv, bool remove_consumed_flags) { + rgb_matrix::RuntimeOptions default_rt; + rgb_matrix::RGBMatrix::Options default_opts; + + if (opts) { + // Copy between C struct and C++ struct. The C++ struct already has a + // default constructor that sets some values. These we override with the + // C-struct values if available. + // We assume everything non-zero has an explicit value. +#define OPT_COPY_IF_SET(o) if (opts->o) default_opts.o = opts->o + OPT_COPY_IF_SET(hardware_mapping); + OPT_COPY_IF_SET(rows); + OPT_COPY_IF_SET(cols); + OPT_COPY_IF_SET(chain_length); + OPT_COPY_IF_SET(parallel); + OPT_COPY_IF_SET(pwm_bits); + OPT_COPY_IF_SET(pwm_lsb_nanoseconds); + OPT_COPY_IF_SET(pwm_dither_bits); + OPT_COPY_IF_SET(brightness); + OPT_COPY_IF_SET(scan_mode); + OPT_COPY_IF_SET(row_address_type); + OPT_COPY_IF_SET(multiplexing); + OPT_COPY_IF_SET(disable_hardware_pulsing); + OPT_COPY_IF_SET(show_refresh_rate); + OPT_COPY_IF_SET(inverse_colors); + OPT_COPY_IF_SET(led_rgb_sequence); + OPT_COPY_IF_SET(pixel_mapper_config); + OPT_COPY_IF_SET(panel_type); + OPT_COPY_IF_SET(limit_refresh_rate_hz); +#undef OPT_COPY_IF_SET + } + + if (rt_opts) { + // Same story as RGBMatrix::Options +#define RT_OPT_COPY_IF_SET(o) if (rt_opts->o) default_rt.o = rt_opts->o + RT_OPT_COPY_IF_SET(gpio_slowdown); + RT_OPT_COPY_IF_SET(daemon); + RT_OPT_COPY_IF_SET(drop_privileges); + RT_OPT_COPY_IF_SET(do_gpio_init); +#undef RT_OPT_COPY_IF_SET + } + + rgb_matrix::RGBMatrix::Options matrix_options = default_opts; + rgb_matrix::RuntimeOptions runtime_opt = default_rt; + if (argc != NULL && argv != NULL) { + if (!ParseOptionsFromFlags(argc, argv, &matrix_options, &runtime_opt, + remove_consumed_flags)) { + rgb_matrix::PrintMatrixFlags(stderr, default_opts, default_rt); + return NULL; + } + } + + if (opts) { +#define ACTUAL_VALUE_BACK_TO_OPT(o) opts->o = matrix_options.o + ACTUAL_VALUE_BACK_TO_OPT(hardware_mapping); + ACTUAL_VALUE_BACK_TO_OPT(rows); + ACTUAL_VALUE_BACK_TO_OPT(cols); + ACTUAL_VALUE_BACK_TO_OPT(chain_length); + ACTUAL_VALUE_BACK_TO_OPT(parallel); + ACTUAL_VALUE_BACK_TO_OPT(pwm_bits); + ACTUAL_VALUE_BACK_TO_OPT(pwm_lsb_nanoseconds); + ACTUAL_VALUE_BACK_TO_OPT(pwm_dither_bits); + ACTUAL_VALUE_BACK_TO_OPT(brightness); + ACTUAL_VALUE_BACK_TO_OPT(scan_mode); + ACTUAL_VALUE_BACK_TO_OPT(row_address_type); + ACTUAL_VALUE_BACK_TO_OPT(multiplexing); + ACTUAL_VALUE_BACK_TO_OPT(disable_hardware_pulsing); + ACTUAL_VALUE_BACK_TO_OPT(show_refresh_rate); + ACTUAL_VALUE_BACK_TO_OPT(inverse_colors); + ACTUAL_VALUE_BACK_TO_OPT(led_rgb_sequence); + ACTUAL_VALUE_BACK_TO_OPT(pixel_mapper_config); + ACTUAL_VALUE_BACK_TO_OPT(panel_type); + ACTUAL_VALUE_BACK_TO_OPT(limit_refresh_rate_hz); +#undef ACTUAL_VALUE_BACK_TO_OPT + } + + if (rt_opts) { +#define ACTUAL_VALUE_BACK_TO_RT_OPT(o) rt_opts->o = runtime_opt.o + ACTUAL_VALUE_BACK_TO_RT_OPT(gpio_slowdown); + ACTUAL_VALUE_BACK_TO_RT_OPT(daemon); + ACTUAL_VALUE_BACK_TO_RT_OPT(drop_privileges); + ACTUAL_VALUE_BACK_TO_RT_OPT(do_gpio_init); +#undef ACTUAL_VALUE_BACK_TO_RT_OPT + } + + rgb_matrix::RGBMatrix *matrix + = rgb_matrix::RGBMatrix::CreateFromOptions(matrix_options, runtime_opt); + return from_matrix(matrix); +} + +struct RGBLedMatrix *led_matrix_create_from_options( + struct RGBLedMatrixOptions *opts, int *argc, char ***argv) { + return led_matrix_create_from_options_optional_edit(opts, NULL, argc, argv, + true); +} + +struct RGBLedMatrix *led_matrix_create_from_options_const_argv( + struct RGBLedMatrixOptions *opts, int argc, char **argv) { + return led_matrix_create_from_options_optional_edit(opts, NULL, &argc, &argv, + false); +} + +struct RGBLedMatrix *led_matrix_create_from_options_and_rt_options( + struct RGBLedMatrixOptions *opts, struct RGBLedRuntimeOptions * rt_opts) { + return led_matrix_create_from_options_optional_edit(opts, rt_opts, NULL, NULL, + false); +} + +struct RGBLedMatrix *led_matrix_create(int rows, int chained, int parallel) { + struct RGBLedMatrixOptions opts; + memset(&opts, 0, sizeof(opts)); + opts.rows = rows; + opts.chain_length = chained; + opts.parallel = parallel; + return led_matrix_create_from_options(&opts, NULL, NULL); +} + +void led_matrix_print_flags(FILE *out) { + rgb_matrix::RGBMatrix::Options defaults; + rgb_matrix::RuntimeOptions rt_opt; + rt_opt.daemon = -1; + rt_opt.drop_privileges = -1; + rgb_matrix::PrintMatrixFlags(out, defaults, rt_opt); +} + +void led_matrix_delete(struct RGBLedMatrix *matrix) { + delete to_matrix(matrix); +} + +struct LedCanvas *led_matrix_get_canvas(struct RGBLedMatrix *matrix) { + return from_canvas(to_matrix(matrix)->SwapOnVSync(NULL)); +} + +struct LedCanvas *led_matrix_create_offscreen_canvas(struct RGBLedMatrix *m) { + return from_canvas(to_matrix(m)->CreateFrameCanvas()); +} + +struct LedCanvas *led_matrix_swap_on_vsync(struct RGBLedMatrix *matrix, + struct LedCanvas *canvas) { + return from_canvas(to_matrix(matrix)->SwapOnVSync(to_canvas(canvas))); +} + +void led_matrix_set_brightness(struct RGBLedMatrix *matrix, + uint8_t brightness) { + to_matrix(matrix)->SetBrightness(brightness); +} + +uint8_t led_matrix_get_brightness(struct RGBLedMatrix *matrix) { + return to_matrix(matrix)->brightness(); +} + +void led_canvas_get_size(const struct LedCanvas *canvas, + int *width, int *height) { + rgb_matrix::FrameCanvas *c = to_canvas((struct LedCanvas*)canvas); + if (c == NULL ) return; + if (width != NULL) *width = c->width(); + if (height != NULL) *height = c->height(); +} + +void led_canvas_set_pixel(struct LedCanvas *canvas, int x, int y, + uint8_t r, uint8_t g, uint8_t b) { + to_canvas(canvas)->SetPixel(x, y, r, g, b); +} + +void led_canvas_clear(struct LedCanvas *canvas) { + to_canvas(canvas)->Clear(); +} + +void led_canvas_fill(struct LedCanvas *canvas, uint8_t r, uint8_t g, uint8_t b) { + to_canvas(canvas)->Fill(r, g, b); +} + +struct LedFont *load_font(const char *bdf_font_file) { + rgb_matrix::Font* font = new rgb_matrix::Font(); + font->LoadFont(bdf_font_file); + return from_font(font); +} + +int baseline_font(struct LedFont * font) { + return to_font(font)->baseline(); +} + +int height_font(struct LedFont * font) { + return to_font(font)->height(); +} + +struct LedFont *create_outline_font(struct LedFont * font) { + rgb_matrix::Font* outlineFont = to_font(font)->CreateOutlineFont(); + return from_font(outlineFont); +} + +void delete_font(struct LedFont *font) { + delete to_font(font); +} + +// -- Some utility functions. + +void set_image(struct LedCanvas *c, int canvas_offset_x, int canvas_offset_y, + const uint8_t *image_buffer, size_t buffer_size_bytes, + int image_width, int image_height, + char is_bgr) { + SetImage(to_canvas(c), canvas_offset_x, canvas_offset_y, + image_buffer, buffer_size_bytes, + image_width, image_height, + is_bgr); +} + +// Draw text, a standard NUL terminated C-string encoded in UTF-8, +// with given "font" at "x","y" with "color". +// "color" always needs to be set (hence it is a reference), +// "background_color" is a pointer to optionally be NULL for transparency. +// "kerning_offset" allows for additional spacing between characters (can be +// negative) +// Returns how many pixels we advanced on the screen. +int draw_text(struct LedCanvas *c, struct LedFont *font, int x, int y, + uint8_t r, uint8_t g, uint8_t b, const char *utf8_text, int kerning_offset) { + const rgb_matrix::Color col = rgb_matrix::Color(r, g, b); + return DrawText(to_canvas(c), *to_font(font), x, y, col, NULL, utf8_text, kerning_offset); +} + +// Draw text, a standard NUL terminated C-string encoded in UTF-8, +// with given "font" at "x","y" with "color". +// Draw text as above, but vertically (top down). +// The text is a standard NUL terminated C-string encoded in UTF-8. +// "font, "x", "y", "color" and "background_color" are same as DrawText(). +// "kerning_offset" allows for additional spacing between characters (can be +// negative). +// Returns font height to advance up on the screen. +int vertical_draw_text(struct LedCanvas *c, struct LedFont *font, int x, int y, + uint8_t r, uint8_t g, uint8_t b, + const char *utf8_text, int kerning_offset = 0) { + const rgb_matrix::Color col = rgb_matrix::Color(r, g, b); + return VerticalDrawText(to_canvas(c), *to_font(font), x, y, col, NULL, utf8_text, kerning_offset); +} + +// Draw a circle centered at "x", "y", with a radius of "radius" and with "color" +void draw_circle(struct LedCanvas *c, int xx, int y, int radius, uint8_t r, uint8_t g, uint8_t b) { + const rgb_matrix::Color col = rgb_matrix::Color( r,g,b ); + DrawCircle(to_canvas(c), xx, y, radius, col); +} + +// Draw a line from "x0", "y0" to "x1", "y1" and with "color" +void draw_line(struct LedCanvas *c, int x0, int y0, int x1, int y1, uint8_t r, uint8_t g, uint8_t b) { + const rgb_matrix::Color col = rgb_matrix::Color(r, g, b); + DrawLine(to_canvas(c), x0, y0, x1, y1, col); +} diff --git a/lib/led-matrix-c.o b/lib/led-matrix-c.o new file mode 100644 index 0000000..be5c449 Binary files /dev/null and b/lib/led-matrix-c.o differ diff --git a/lib/led-matrix.cc b/lib/led-matrix.cc new file mode 100644 index 0000000..abc8307 --- /dev/null +++ b/lib/led-matrix.cc @@ -0,0 +1,763 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2013 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +#include "led-matrix.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gpio.h" +#include "thread.h" +#include "framebuffer-internal.h" +#include "multiplex-mappers-internal.h" + +// Leave this in here for a while. Setting things from old defines. +#if defined(ADAFRUIT_RGBMATRIX_HAT) +# error "ADAFRUIT_RGBMATRIX_HAT has long been deprecated. Please use the Options struct or --led-gpio-mapping=adafruit-hat commandline flag" +#endif + +#if defined(ADAFRUIT_RGBMATRIX_HAT_PWM) +# error "ADAFRUIT_RGBMATRIX_HAT_PWM has long been deprecated. Please use the Options struct or --led-gpio-mapping=adafruit-hat-pwm commandline flag" +#endif + +namespace rgb_matrix { +// Implementation details of RGBmatrix. +class RGBMatrix::Impl { + class UpdateThread; + friend class UpdateThread; + +public: + // Create an RGBMatrix. + // + // Needs an initialized GPIO object and configuration options from the + // RGBMatrix::Options struct. + // + // If you pass an GPIO object (which has to be Init()ialized), it will start // the internal thread to start the screen immediately. + // + // If you need finer control over when the refresh thread starts (which you + // might when you become a daemon), pass NULL here and see SetGPIO() method. + // + // The resulting canvas is (options.rows * options.parallel) high and + // (32 * options.chain_length) wide. + Impl(GPIO *io, const Options &options); + + ~Impl(); + + // Used to be there to help user delay initialization of thread starting, + // these days only used internally. + void SetGPIO(GPIO *io, bool start_thread = true); + + bool StartRefresh(); + + FrameCanvas *CreateFrameCanvas(); + FrameCanvas *SwapOnVSync(FrameCanvas *other, unsigned framerate_fraction); + bool ApplyPixelMapper(const PixelMapper *mapper); + + bool SetPWMBits(uint8_t value); + uint8_t pwmbits(); // return the pwm-bits of the currently active buffer. + + void set_luminance_correct(bool on); + bool luminance_correct() const; + + // Set brightness in percent for all created FrameCanvas. 1%..100%. + // This will only affect newly set pixels. + void SetBrightness(uint8_t brightness); + uint8_t brightness(); + + uint64_t RequestInputs(uint64_t); + uint64_t AwaitInputChange(int timeout_ms); + + uint64_t RequestOutputs(uint64_t output_bits); + void OutputGPIO(uint64_t output_bits); + + void Clear(); +private: + friend class RGBMatrix; + + // Apply pixel mappers that have been passed down via a configuration + // string. + void ApplyNamedPixelMappers(const char *pixel_mapper_config, + int chain, int parallel); + + Options params_; + bool do_luminance_correct_; + + FrameCanvas *active_; + + GPIO *io_; + Mutex active_frame_sync_; + UpdateThread *updater_; + std::vector created_frames_; + internal::PixelDesignatorMap *shared_pixel_mapper_; + uint64_t user_output_bits_; +}; + +using namespace internal; + +// Pump pixels to screen. Needs to be high priority real-time because jitter +class RGBMatrix::Impl::UpdateThread : public Thread { +public: + UpdateThread(GPIO *io, FrameCanvas *initial_frame, + int pwm_dither_bits, bool show_refresh, + int limit_refresh_hz) + : io_(io), show_refresh_(show_refresh), + target_frame_usec_(limit_refresh_hz < 1 ? 0 : 1e6/limit_refresh_hz), + running_(true), + current_frame_(initial_frame), next_frame_(NULL), + requested_frame_multiple_(1) { + pthread_cond_init(&frame_done_, NULL); + pthread_cond_init(&input_change_, NULL); + switch (pwm_dither_bits) { + case 0: + start_bit_[0] = 0; start_bit_[1] = 0; + start_bit_[2] = 0; start_bit_[3] = 0; + break; + case 1: + start_bit_[0] = 0; start_bit_[1] = 1; + start_bit_[2] = 0; start_bit_[3] = 1; + break; + case 2: + start_bit_[0] = 0; start_bit_[1] = 1; + start_bit_[2] = 2; start_bit_[3] = 2; + break; + } + } + + void Stop() { + MutexLock l(&running_mutex_); + running_ = false; + } + + virtual void Run() { + unsigned frame_count = 0; + unsigned low_bit_sequence = 0; + uint32_t largest_time = 0; + gpio_bits_t last_gpio_bits = 0; + + // Let's start measure max time only after a we were running for a few + // seconds to not pick up start-up glitches. + static const int kHoldffTimeUs = 2000 * 1000; + uint32_t initial_holdoff_start = GetMicrosecondCounter(); + bool max_measure_enabled = false; + + while (running()) { + const uint32_t start_time_us = GetMicrosecondCounter(); + + current_frame_->framebuffer() + ->DumpToMatrix(io_, start_bit_[low_bit_sequence % 4]); + + // SwapOnVSync() exchange. + { + MutexLock l(&frame_sync_); + // Do fast equality test first (likely due to frame_count reset). + if (frame_count == requested_frame_multiple_ + || frame_count % requested_frame_multiple_ == 0) { + // We reset to avoid frame hick-up every couple of weeks + // run-time iff requested_frame_multiple_ is not a factor of 2^32. + frame_count = 0; + if (next_frame_ != NULL) { + current_frame_ = next_frame_; + next_frame_ = NULL; + } + pthread_cond_signal(&frame_done_); + } + } + + // Read input bits. + const gpio_bits_t inputs = io_->Read(); + if (inputs != last_gpio_bits) { + last_gpio_bits = inputs; + MutexLock l(&input_sync_); + gpio_inputs_ = inputs; + pthread_cond_signal(&input_change_); + } + + ++frame_count; + ++low_bit_sequence; + + if (target_frame_usec_) { + while ((GetMicrosecondCounter() - start_time_us) < target_frame_usec_) { + // busy wait. We have our dedicated core, so ok to burn cycles. + } + } + + const uint32_t end_time_us = GetMicrosecondCounter(); + if (show_refresh_) { + uint32_t usec = end_time_us - start_time_us; + printf("\b\b\b\b\b\b\b\b%6.1fHz", 1e6 / usec); + if (usec > largest_time && max_measure_enabled) { + largest_time = usec; + const float lowest_hz = 1e6 / largest_time; + printf(" (lowest: %.1fHz)" + "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b", lowest_hz); + } else { + // Don't measure at startup, as times will be janky. + max_measure_enabled = (end_time_us - initial_holdoff_start) > kHoldffTimeUs; + } + } + } + } + + FrameCanvas *SwapOnVSync(FrameCanvas *other, unsigned frame_fraction) { + MutexLock l(&frame_sync_); + FrameCanvas *previous = current_frame_; + next_frame_ = other; + requested_frame_multiple_ = frame_fraction; + frame_sync_.WaitOn(&frame_done_); + return previous; + } + + gpio_bits_t AwaitInputChange(int timeout_ms) { + MutexLock l(&input_sync_); + input_sync_.WaitOn(&input_change_, timeout_ms); + return gpio_inputs_; + } + +private: + inline bool running() { + MutexLock l(&running_mutex_); + return running_; + } + + GPIO *const io_; + const bool show_refresh_; + const uint32_t target_frame_usec_; + uint32_t start_bit_[4]; + + Mutex running_mutex_; + bool running_; + + Mutex input_sync_; + pthread_cond_t input_change_; + gpio_bits_t gpio_inputs_; + + Mutex frame_sync_; + pthread_cond_t frame_done_; + FrameCanvas *current_frame_; + FrameCanvas *next_frame_; + unsigned requested_frame_multiple_; +}; + +// Some defaults. See options-initialize.cc for the command line parsing. +RGBMatrix::Options::Options() : + // Historically, we provided these options only as #defines. Make sure that + // things still behave as before if someone has set these. + // At some point: remove them from the Makefile. Later: remove them here. +#ifdef DEFAULT_HARDWARE + hardware_mapping(DEFAULT_HARDWARE), +#else + hardware_mapping("regular"), +#endif + + rows(32), cols(32), chain_length(1), parallel(1), + pwm_bits(internal::Framebuffer::kDefaultBitPlanes), + +#ifdef LSB_PWM_NANOSECONDS + pwm_lsb_nanoseconds(LSB_PWM_NANOSECONDS), +#else + pwm_lsb_nanoseconds(130), +#endif + + pwm_dither_bits(0), + brightness(100), + +#ifdef RGB_SCAN_INTERLACED + scan_mode(1), +#else + scan_mode(0), +#endif + + row_address_type(0), + multiplexing(0), + +#ifdef DISABLE_HARDWARE_PULSES + disable_hardware_pulsing(true), +#else + disable_hardware_pulsing(false), +#endif + +#ifdef SHOW_REFRESH_RATE + show_refresh_rate(true), +#else + show_refresh_rate(false), +#endif + +#ifdef INVERSE_RGB_DISPLAY_COLORS + inverse_colors(true), +#else + inverse_colors(false), +#endif + led_rgb_sequence("RGB"), + pixel_mapper_config(NULL), + panel_type(NULL), +#ifdef FIXED_FRAME_MICROSECONDS + limit_refresh_rate_hz(1e6 / FIXED_FRAME_MICROSECONDS) +#else + limit_refresh_rate_hz(0) +#endif +{ + // Nothing to see here. +} + +#define DEBUG_MATRIX_OPTIONS 0 + +#if DEBUG_MATRIX_OPTIONS +static void PrintOptions(const RGBMatrix::Options &o) { +#define P_INT(val) fprintf(stderr, "%s : %d\n", #val, o.val) +#define P_STR(val) fprintf(stderr, "%s : %s\n", #val, o.val) +#define P_BOOL(val) fprintf(stderr, "%s : %s\n", #val, o.val ? "true":"false") + P_STR(hardware_mapping); + P_INT(rows); + P_INT(cols); + P_INT(chain_length); + P_INT(parallel); + P_INT(pwm_bits); + P_INT(pwm_lsb_nanoseconds); + P_INT(pwm_dither_bits); + P_INT(brightness); + P_INT(scan_mode); + P_INT(row_address_type); + P_INT(multiplexing); + P_BOOL(disable_hardware_pulsing); + P_BOOL(show_refresh_rate); + P_BOOL(inverse_colors); + P_STR(led_rgb_sequence); + P_STR(pixel_mapper_config); + P_STR(panel_type); + P_INT(limit_refresh_rate_hz); +#undef P_INT +#undef P_STR +#undef P_BOOL +} +#endif // DEBUG_MATRIX_OPTIONS + +RGBMatrix::Impl::Impl(GPIO *io, const Options &options) + : params_(options), io_(NULL), updater_(NULL), shared_pixel_mapper_(NULL), + user_output_bits_(0) { + assert(params_.Validate(NULL)); +#if DEBUG_MATRIX_OPTIONS + PrintOptions(params_); +#endif + const MultiplexMapper *multiplex_mapper = NULL; + if (params_.multiplexing > 0) { + const MuxMapperList &multiplexers = GetRegisteredMultiplexMappers(); + if (params_.multiplexing <= (int) multiplexers.size()) { + // TODO: we could also do a find-by-name here, but not sure if worthwhile + multiplex_mapper = multiplexers[params_.multiplexing - 1]; + } + } + + if (multiplex_mapper) { + // The multiplexers might choose to have a different physical layout. + // We need to configure that first before setting up the hardware. + multiplex_mapper->EditColsRows(¶ms_.cols, ¶ms_.rows); + } + + Framebuffer::InitHardwareMapping(params_.hardware_mapping); + + active_ = CreateFrameCanvas(); + active_->Clear(); + SetGPIO(io, true); + + // We need to apply the mapping for the panels first. + ApplyPixelMapper(multiplex_mapper); + + // .. followed by higher level mappers that might arrange panels. + ApplyNamedPixelMappers(options.pixel_mapper_config, + params_.chain_length, params_.parallel); +} + +RGBMatrix::Impl::~Impl() { + if (updater_) { + updater_->Stop(); + updater_->WaitStopped(); + } + delete updater_; + + // Make sure LEDs are off. + active_->Clear(); + if (io_) active_->framebuffer()->DumpToMatrix(io_, 0); + + for (size_t i = 0; i < created_frames_.size(); ++i) { + delete created_frames_[i]; + } + delete shared_pixel_mapper_; +} + +RGBMatrix::~RGBMatrix() { + delete impl_; +} + +uint64_t RGBMatrix::Impl::RequestInputs(uint64_t bits) { + return io_->RequestInputs(bits); +} + +uint64_t RGBMatrix::Impl::RequestOutputs(uint64_t output_bits) { + uint64_t success_bits = io_->InitOutputs(output_bits); + user_output_bits_ |= success_bits; + return success_bits; +} + +void RGBMatrix::Impl::OutputGPIO(uint64_t output_bits) { + io_->WriteMaskedBits(output_bits, user_output_bits_); +} + +void RGBMatrix::Impl::ApplyNamedPixelMappers(const char *pixel_mapper_config, + int chain, int parallel) { + if (pixel_mapper_config == NULL || strlen(pixel_mapper_config) == 0) + return; + char *const writeable_copy = strdup(pixel_mapper_config); + const char *const end = writeable_copy + strlen(writeable_copy); + char *s = writeable_copy; + while (s < end) { + char *const semicolon = strchrnul(s, ';'); + *semicolon = '\0'; + char *optional_param_start = strchr(s, ':'); + if (optional_param_start) { + *optional_param_start++ = '\0'; + } + if (*s == '\0' && optional_param_start && *optional_param_start != '\0') { + fprintf(stderr, "Stray parameter ':%s' without mapper name ?\n", optional_param_start); + } + if (*s) { + ApplyPixelMapper(FindPixelMapper(s, chain, parallel, optional_param_start)); + } + s = semicolon + 1; + } + free(writeable_copy); +} + +void RGBMatrix::Impl::SetGPIO(GPIO *io, bool start_thread) { + if (io != NULL && io_ == NULL) { + io_ = io; + Framebuffer::InitGPIO(io_, params_.rows, params_.parallel, + !params_.disable_hardware_pulsing, + params_.pwm_lsb_nanoseconds, params_.pwm_dither_bits, + params_.row_address_type); + Framebuffer::InitializePanels(io_, params_.panel_type, + params_.cols * params_.chain_length); + } + if (start_thread) { + StartRefresh(); + } +} + +bool RGBMatrix::Impl::StartRefresh() { + if (updater_ == NULL && io_ != NULL) { + updater_ = new UpdateThread(io_, active_, params_.pwm_dither_bits, + params_.show_refresh_rate, + params_.limit_refresh_rate_hz); + // If we have multiple processors, the kernel + // jumps around between these, creating some global flicker. + // So let's tie it to the last CPU available. + // The Raspberry Pi2 has 4 cores, our attempt to bind it to + // core #3 will succeed. + // The Raspberry Pi1 only has one core, so this affinity + // call will simply fail and we keep using the only core. + updater_->Start(99, (1<<3)); // Prio: high. Also: put on last CPU. + } + return updater_ != NULL; +} + +FrameCanvas *RGBMatrix::Impl::CreateFrameCanvas() { + FrameCanvas *result = + new FrameCanvas(new Framebuffer(params_.rows, + params_.cols * params_.chain_length, + params_.parallel, + params_.scan_mode, + params_.led_rgb_sequence, + params_.inverse_colors, + &shared_pixel_mapper_)); + if (created_frames_.empty()) { + // First time. Get defaults from initial Framebuffer. + do_luminance_correct_ = result->framebuffer()->luminance_correct(); + } + + result->framebuffer()->SetPWMBits(params_.pwm_bits); + result->framebuffer()->set_luminance_correct(do_luminance_correct_); + result->framebuffer()->SetBrightness(params_.brightness); + + created_frames_.push_back(result); + return result; +} + +FrameCanvas *RGBMatrix::Impl::SwapOnVSync(FrameCanvas *other, + unsigned frame_fraction) { + if (frame_fraction == 0) frame_fraction = 1; // correct user error. + if (!updater_) return NULL; + FrameCanvas *const previous = updater_->SwapOnVSync(other, frame_fraction); + if (other) active_ = other; + return previous; +} + +uint64_t RGBMatrix::Impl::AwaitInputChange(int timeout_ms) { + if (!updater_) return 0; + return updater_->AwaitInputChange(timeout_ms); +} + +bool RGBMatrix::Impl::SetPWMBits(uint8_t value) { + const bool success = active_->framebuffer()->SetPWMBits(value); + if (success) { + params_.pwm_bits = value; + } + return success; +} +uint8_t RGBMatrix::Impl::pwmbits() { return params_.pwm_bits; } + +// Map brightness of output linearly to input with CIE1931 profile. +void RGBMatrix::Impl::set_luminance_correct(bool on) { + active_->framebuffer()->set_luminance_correct(on); + do_luminance_correct_ = on; +} +bool RGBMatrix::Impl::luminance_correct() const { + return do_luminance_correct_; +} + +void RGBMatrix::Impl::SetBrightness(uint8_t brightness) { + for (size_t i = 0; i < created_frames_.size(); ++i) { + created_frames_[i]->framebuffer()->SetBrightness(brightness); + } + params_.brightness = brightness; +} + +uint8_t RGBMatrix::Impl::brightness() { + return params_.brightness; +} + +bool RGBMatrix::Impl::ApplyPixelMapper(const PixelMapper *mapper) { + if (mapper == NULL) return true; + using internal::PixelDesignatorMap; + const int old_width = shared_pixel_mapper_->width(); + const int old_height = shared_pixel_mapper_->height(); + int new_width, new_height; + if (!mapper->GetSizeMapping(old_width, old_height, &new_width, &new_height)) { + return false; + } + PixelDesignatorMap *new_mapper = new PixelDesignatorMap( + new_width, new_height, shared_pixel_mapper_->GetFillColorBits()); + for (int y = 0; y < new_height; ++y) { + for (int x = 0; x < new_width; ++x) { + int orig_x = -1, orig_y = -1; + mapper->MapVisibleToMatrix(old_width, old_height, + x, y, &orig_x, &orig_y); + if (orig_x < 0 || orig_y < 0 || + orig_x >= old_width || orig_y >= old_height) { + fprintf(stderr, "Error in PixelMapper: (%d, %d) -> (%d, %d) [range: " + "%dx%d]\n", x, y, orig_x, orig_y, old_width, old_height); + continue; + } + const internal::PixelDesignator *orig_designator; + orig_designator = shared_pixel_mapper_->get(orig_x, orig_y); + *new_mapper->get(x, y) = *orig_designator; + } + } + delete shared_pixel_mapper_; + shared_pixel_mapper_ = new_mapper; + return true; +} + +// -- Public interface of RGBMatrix. Delegate everything to impl_ + +static bool drop_privs(const char *priv_user, const char *priv_group) { + uid_t ruid, euid, suid; + if (getresuid(&ruid, &euid, &suid) >= 0) { + if (euid != 0) // not root anyway. No priv dropping. + return true; + } + + struct group *g = getgrnam(priv_group); + if (g == NULL) { + perror("group lookup."); + return false; + } + if (setresgid(g->gr_gid, g->gr_gid, g->gr_gid) != 0) { + perror("setresgid()"); + return false; + } + struct passwd *p = getpwnam(priv_user); + if (p == NULL) { + perror("user lookup."); + return false; + } + if (setresuid(p->pw_uid, p->pw_uid, p->pw_uid) != 0) { + perror("setresuid()"); + return false; + } + return true; +} + +RGBMatrix *RGBMatrix::CreateFromOptions(const RGBMatrix::Options &options, + const RuntimeOptions &runtime_options) { + std::string error; + if (!options.Validate(&error)) { + fprintf(stderr, "%s\n", error.c_str()); + return NULL; + } + + // For the Pi4, we might need 2, maybe up to 4. Let's open up to 5. + if (runtime_options.gpio_slowdown < 0 || runtime_options.gpio_slowdown > 5) { + fprintf(stderr, "--led-slowdown-gpio=%d is outside usable range\n", + runtime_options.gpio_slowdown); + return NULL; + } + + static GPIO io; // This static var is a little bit icky. + if (runtime_options.do_gpio_init + && !io.Init(runtime_options.gpio_slowdown)) { + fprintf(stderr, "Must run as root to be able to access /dev/mem\n" + "Prepend 'sudo' to the command\n"); + return NULL; + } + + if (runtime_options.daemon > 0 && daemon(1, 0) != 0) { + perror("Failed to become daemon"); + } + + RGBMatrix::Impl *result = new RGBMatrix::Impl(NULL, options); + // Allowing daemon also means we are allowed to start the thread now. + const bool allow_daemon = !(runtime_options.daemon < 0); + if (runtime_options.do_gpio_init) + result->SetGPIO(&io, allow_daemon); + + // TODO(hzeller): if we disallow daemon, then we might also disallow + // drop privileges: we can't drop privileges until we have created the + // realtime thread that usually requires root to be established. + // Double check and document. + if (runtime_options.drop_privileges > 0) { + drop_privs("daemon", "daemon"); + } + + return new RGBMatrix(result); +} + +// Public interface. +RGBMatrix *RGBMatrix::CreateFromFlags(int *argc, char ***argv, + RGBMatrix::Options *m_opt_in, + RuntimeOptions *rt_opt_in, + bool remove_consumed_options) { + RGBMatrix::Options scratch_matrix; + RGBMatrix::Options *mopt = (m_opt_in != NULL) ? m_opt_in : &scratch_matrix; + + RuntimeOptions scratch_rt; + RuntimeOptions *ropt = (rt_opt_in != NULL) ? rt_opt_in : &scratch_rt; + + if (!ParseOptionsFromFlags(argc, argv, mopt, ropt, remove_consumed_options)) + return NULL; + return CreateFromOptions(*mopt, *ropt); +} + +FrameCanvas *RGBMatrix::CreateFrameCanvas() { + return impl_->CreateFrameCanvas(); +} +FrameCanvas *RGBMatrix::SwapOnVSync(FrameCanvas *other, + unsigned framerate_fraction) { + return impl_->SwapOnVSync(other, framerate_fraction); +} +bool RGBMatrix::ApplyPixelMapper(const PixelMapper *mapper) { + return impl_->ApplyPixelMapper(mapper); +} +bool RGBMatrix::SetPWMBits(uint8_t value) { return impl_->SetPWMBits(value); } +uint8_t RGBMatrix::pwmbits() { return impl_->pwmbits(); } + +void RGBMatrix::set_luminance_correct(bool on) { + return impl_->set_luminance_correct(on); +} +bool RGBMatrix::luminance_correct() const { return impl_->luminance_correct(); } + +void RGBMatrix::SetBrightness(uint8_t brightness) { + impl_->SetBrightness(brightness); +} +uint8_t RGBMatrix::brightness() { return impl_->brightness(); } + +uint64_t RGBMatrix::RequestInputs(uint64_t all_interested_bits) { + return impl_->RequestInputs(all_interested_bits); +} +uint64_t RGBMatrix::AwaitInputChange(int timeout_ms) { + return impl_->AwaitInputChange(timeout_ms); +} + +uint64_t RGBMatrix::RequestOutputs(uint64_t all_interested_bits) { + return impl_->RequestOutputs(all_interested_bits); +} +void RGBMatrix::OutputGPIO(uint64_t output_bits) { + impl_->OutputGPIO(output_bits); +} + +bool RGBMatrix::StartRefresh() { return impl_->StartRefresh(); } + +// -- Implementation of RGBMatrix Canvas: delegation to ContentBuffer +int RGBMatrix::width() const { + return impl_->active_->width(); +} + +int RGBMatrix::height() const { + return impl_->active_->height(); +} + +void RGBMatrix::SetPixel(int x, int y, uint8_t red, uint8_t green, uint8_t blue) { + impl_->active_->SetPixel(x, y, red, green, blue); +} + +void RGBMatrix::Clear() { + impl_->active_->Clear(); +} + +void RGBMatrix::Fill(uint8_t red, uint8_t green, uint8_t blue) { + impl_->active_->Fill(red, green, blue); +} + +// FrameCanvas implementation of Canvas +FrameCanvas::~FrameCanvas() { delete frame_; } +int FrameCanvas::width() const { return frame_->width(); } +int FrameCanvas::height() const { return frame_->height(); } +void FrameCanvas::SetPixel(int x, int y, + uint8_t red, uint8_t green, uint8_t blue) { + frame_->SetPixel(x, y, red, green, blue); +} +void FrameCanvas::Clear() { return frame_->Clear(); } +void FrameCanvas::Fill(uint8_t red, uint8_t green, uint8_t blue) { + frame_->Fill(red, green, blue); +} +bool FrameCanvas::SetPWMBits(uint8_t value) { return frame_->SetPWMBits(value); } +uint8_t FrameCanvas::pwmbits() { return frame_->pwmbits(); } + +// Map brightness of output linearly to input with CIE1931 profile. +void FrameCanvas::set_luminance_correct(bool on) { frame_->set_luminance_correct(on); } +bool FrameCanvas::luminance_correct() const { return frame_->luminance_correct(); } + +void FrameCanvas::SetBrightness(uint8_t brightness) { frame_->SetBrightness(brightness); } +uint8_t FrameCanvas::brightness() { return frame_->brightness(); } + +void FrameCanvas::Serialize(const char **data, size_t *len) const { + frame_->Serialize(data, len); +} +bool FrameCanvas::Deserialize(const char *data, size_t len) { + return frame_->Deserialize(data, len); +} +void FrameCanvas::CopyFrom(const FrameCanvas &other) { + frame_->CopyFrom(other.frame_); +} +} // end namespace rgb_matrix diff --git a/lib/led-matrix.o b/lib/led-matrix.o new file mode 100644 index 0000000..0407b1b Binary files /dev/null and b/lib/led-matrix.o differ diff --git a/lib/multiplex-mappers-internal.h b/lib/multiplex-mappers-internal.h new file mode 100644 index 0000000..2addce5 --- /dev/null +++ b/lib/multiplex-mappers-internal.h @@ -0,0 +1,38 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2017 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +#include + +#include "pixel-mapper.h" + +namespace rgb_matrix { +namespace internal { + +class MultiplexMapper : public PixelMapper { +public: + // Function that edits the original rows and columns of the panels + // provided by the user to the actual underlying mapping. This is called + // before we do the actual set-up of the GPIO mapping as this influences + // the hardware interface. + // This is so that the user can provide the rows/columns they see. + virtual void EditColsRows(int *cols, int *rows) const = 0; +}; + +// Returns a vector of the registered Multiplex mappers. +typedef std::vector MuxMapperList; +const MuxMapperList &GetRegisteredMultiplexMappers(); + +} // namespace internal +} // namespace rgb_matrix diff --git a/lib/multiplex-mappers.cc b/lib/multiplex-mappers.cc new file mode 100644 index 0000000..3a19542 --- /dev/null +++ b/lib/multiplex-mappers.cc @@ -0,0 +1,476 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2017 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +#include "multiplex-mappers-internal.h" + +namespace rgb_matrix { +namespace internal { +// A Pixel Mapper maps physical pixels locations to the internal logical +// mapping in a panel or panel-assembly, which depends on the wiring. +class MultiplexMapperBase : public MultiplexMapper { +public: + MultiplexMapperBase(const char *name, int stretch_factor) + : name_(name), panel_stretch_factor_(stretch_factor) {} + + // This method is const, but we sneakily remember the original size + // of the panels so that we can more easily quantize things. + // So technically, we're stateful, but let's pretend we're not changing + // state. In the context this is used, it is never accessed in multiple + // threads. + virtual void EditColsRows(int *cols, int *rows) const { + panel_rows_ = *rows; + panel_cols_ = *cols; + + *rows /= panel_stretch_factor_; + *cols *= panel_stretch_factor_; + } + + virtual bool GetSizeMapping(int matrix_width, int matrix_height, + int *visible_width, int *visible_height) const { + // Matrix width has been altered. Alter it back. + *visible_width = matrix_width / panel_stretch_factor_; + *visible_height = matrix_height * panel_stretch_factor_; + return true; + } + + virtual const char *GetName() const { return name_; } + + // The MapVisibleToMatrix() as required by PanelMatrix here breaks it + // down to the individual panel, so that derived classes only need to + // implement MapSinglePanel(). + virtual void MapVisibleToMatrix(int matrix_width, int matrix_height, + int visible_x, int visible_y, + int *matrix_x, int *matrix_y) const { + const int chained_panel = visible_x / panel_cols_; + const int parallel_panel = visible_y / panel_rows_; + + const int within_panel_x = visible_x % panel_cols_; + const int within_panel_y = visible_y % panel_rows_; + + int new_x, new_y; + MapSinglePanel(within_panel_x, within_panel_y, &new_x, &new_y); + *matrix_x = chained_panel * panel_stretch_factor_*panel_cols_ + new_x; + *matrix_y = parallel_panel * panel_rows_/panel_stretch_factor_ + new_y; + } + + // Map the coordinates for a single panel. This is to be overridden in + // derived classes. + // Input parameter is the visible position on the matrix, and this method + // should return the internal multiplexed position. + virtual void MapSinglePanel(int visible_x, int visible_y, + int *matrix_x, int *matrix_y) const = 0; + +protected: + const char *const name_; + const int panel_stretch_factor_; + + mutable int panel_cols_; + mutable int panel_rows_; +}; + + +/* ======================================================================== + * Multiplexer implementations. + * + * Extend MultiplexMapperBase and implement MapSinglePanel. You only have + * to worry about the mapping within a single panel, the overall panel + * construction with chains and parallel is already taken care of. + * + * Don't forget to register the new multiplexer sin CreateMultiplexMapperList() + * below. After that, the new mapper is available in the --led-multiplexing + * option. + */ +class StripeMultiplexMapper : public MultiplexMapperBase { +public: + StripeMultiplexMapper() : MultiplexMapperBase("Stripe", 2) {} + + void MapSinglePanel(int x, int y, int *matrix_x, int *matrix_y) const { + const bool is_top_stripe = (y % (panel_rows_/2)) < panel_rows_/4; + *matrix_x = is_top_stripe ? x + panel_cols_ : x; + *matrix_y = ((y / (panel_rows_/2)) * (panel_rows_/4) + + y % (panel_rows_/4)); + } +}; + +class FlippedStripeMultiplexMapper : public MultiplexMapperBase { +public: + FlippedStripeMultiplexMapper() : MultiplexMapperBase("FlippedStripe", 2) {} + + void MapSinglePanel(int x, int y, int *matrix_x, int *matrix_y) const { + const bool is_top_stripe = (y % (panel_rows_/2)) >= panel_rows_/4; + *matrix_x = is_top_stripe ? x + panel_cols_ : x; + *matrix_y = ((y / (panel_rows_/2)) * (panel_rows_/4) + + y % (panel_rows_/4)); + } +}; + +class CheckeredMultiplexMapper : public MultiplexMapperBase { +public: + CheckeredMultiplexMapper() : MultiplexMapperBase("Checkered", 2) {} + + void MapSinglePanel(int x, int y, int *matrix_x, int *matrix_y) const { + const bool is_top_check = (y % (panel_rows_/2)) < panel_rows_/4; + const bool is_left_check = (x < panel_cols_/2); + if (is_top_check) { + *matrix_x = is_left_check ? x+panel_cols_/2 : x+panel_cols_; + } else { + *matrix_x = is_left_check ? x : x + panel_cols_/2; + } + *matrix_y = ((y / (panel_rows_/2)) * (panel_rows_/4) + + y % (panel_rows_/4)); + } +}; + +class SpiralMultiplexMapper : public MultiplexMapperBase { +public: + SpiralMultiplexMapper() : MultiplexMapperBase("Spiral", 2) {} + + void MapSinglePanel(int x, int y, int *matrix_x, int *matrix_y) const { + const bool is_top_stripe = (y % (panel_rows_/2)) < panel_rows_/4; + const int panel_quarter = panel_cols_/4; + const int quarter = x / panel_quarter; + const int offset = x % panel_quarter; + *matrix_x = ((2*quarter*panel_quarter) + + (is_top_stripe + ? panel_quarter - 1 - offset + : panel_quarter + offset)); + *matrix_y = ((y / (panel_rows_/2)) * (panel_rows_/4) + + y % (panel_rows_/4)); + } +}; + +class ZStripeMultiplexMapper : public MultiplexMapperBase { +public: + ZStripeMultiplexMapper(const char *name, int even_vblock_offset, int odd_vblock_offset) + : MultiplexMapperBase(name, 2), + even_vblock_offset_(even_vblock_offset), + odd_vblock_offset_(odd_vblock_offset) {} + + void MapSinglePanel(int x, int y, int *matrix_x, int *matrix_y) const { + static const int tile_width = 8; + static const int tile_height = 4; + + const int vert_block_is_odd = ((y / tile_height) % 2); + + const int even_vblock_shift = (1 - vert_block_is_odd) * even_vblock_offset_; + const int odd_vblock_shitf = vert_block_is_odd * odd_vblock_offset_; + + *matrix_x = x + ((x + even_vblock_shift) / tile_width) * tile_width + odd_vblock_shitf; + *matrix_y = (y % tile_height) + tile_height * (y / (tile_height * 2)); + } + +private: + const int even_vblock_offset_; + const int odd_vblock_offset_; +}; + +class CoremanMapper : public MultiplexMapperBase { +public: + CoremanMapper() : MultiplexMapperBase("coreman", 2) {} + + void MapSinglePanel(int x, int y, int *matrix_x, int *matrix_y) const { + const bool is_left_check = (x < panel_cols_/2); + + if ((y <= 7) || ((y >= 16) && (y <= 23))){ + *matrix_x = ((x / (panel_cols_/2)) * panel_cols_) + (x % (panel_cols_/2)); + if ((y & (panel_rows_/4)) == 0) { + *matrix_y = (y / (panel_rows_/2)) * (panel_rows_/4) + (y % (panel_rows_/4)); + } + } else { + *matrix_x = is_left_check ? x + panel_cols_/2 : x + panel_cols_; + *matrix_y = (y / (panel_rows_/2)) * (panel_rows_/4) + y % (panel_rows_/4); + } + } +}; + +class Kaler2ScanMapper : public MultiplexMapperBase { +public: + Kaler2ScanMapper() : MultiplexMapperBase("Kaler2Scan", 4) {} + + void MapSinglePanel(int x, int y, int *matrix_x, int *matrix_y) const { + // Now we have a 128x4 matrix + int offset = ((y%4)/2) == 0 ? -1 : 1;// Add o substract + int deltaOffset = offset < 0 ? 7:8; + int deltaColumn = ((y%8)/4)== 0 ? 64 : 0; + + *matrix_y = (y%2+(y/8)*2); + *matrix_x = deltaColumn + (16 * (x/8)) + deltaOffset + ((x%8) * offset); + + } +}; + +class P10MapperZ : public MultiplexMapperBase { +public: + P10MapperZ() : MultiplexMapperBase("P10-128x4-Z", 4) {} + // supports this panel: https://www.aliexpress.com/item/2017-Special-Offer-P10-Outdoor-Smd-Full-Color-Led-Display-Module-320x160mm-1-2-Scan-Outdoor/32809267439.html?spm=a2g0s.9042311.0.0.Ob0jEw + // with --led-row-addr-type=2 flag + void MapSinglePanel(int x, int y, int *matrix_x, int *matrix_y) const { + int yComp = 0; + if (y == 0 || y == 1 || y == 8 || y == 9) { + yComp = 127; + } + else if (y == 2 || y == 3 || y == 10 || y == 11) { + yComp = 112; + } + else if (y == 4 || y == 5 || y == 12 || y == 13) { + yComp = 111; + } + else if (y == 6 || y == 7 || y == 14 || y == 15) { + yComp = 96; + } + + if (y == 0 || y == 1 || y == 4 || y == 5 || + y == 8 || y == 9 || y == 12 || y == 13) { + *matrix_x = yComp - x; + *matrix_x -= (24 * ((int)(x / 8))); + } + else { + *matrix_x = yComp + x; + *matrix_x -= (40 * ((int)(x / 8))); + } + + if (y == 0 || y == 2 || y == 4 || y == 6) { + *matrix_y = 3; + } + else if (y == 1 || y == 3 || y == 5 || y == 7) { + *matrix_y = 2; + } + else if (y == 8 || y == 10 || y == 12 || y == 14) { + *matrix_y = 1; + } + else if (y == 9 || y == 11 || y == 13 || y == 15) { + *matrix_y = 0; + } + } +}; + +class QiangLiQ8 : public MultiplexMapperBase { +public: + QiangLiQ8() : MultiplexMapperBase("QiangLiQ8", 2) {} + + void MapSinglePanel(int x, int y, int *matrix_x, int *matrix_y) const { + const int column = x + (4+ 4*(x/4)); + *matrix_x = column; + if ((y >= 15 && y <=19) || (y >= 5 && y <= 9)) { + const int reverseColumn = x + (4*(x/4)); + *matrix_x = reverseColumn; + } + *matrix_y = y % 5 + (y/10) *5; + } +}; + +class InversedZStripe : public MultiplexMapperBase { +public: + InversedZStripe() : MultiplexMapperBase("InversedZStripe", 2) {} + + void MapSinglePanel(int x, int y, int *matrix_x, int *matrix_y) const { + static const int tile_width = 8; + static const int tile_height = 4; + + const int vert_block_is_odd = ((y / tile_height) % 2); + const int evenOffset[8] = {7, 5, 3, 1, -1, -3, -5, -7}; + + if (vert_block_is_odd) { + *matrix_x = x + (x / tile_width) * tile_width; + } else { + *matrix_x = x + (x / tile_width) * tile_width + 8 + evenOffset[x % 8]; + } + *matrix_y = (y % tile_height) + tile_height * (y / (tile_height * 2)); + } +}; + + +/* + * Vairous P10 1R1G1B Outdoor implementations for 16x16 modules with separate + * RGB LEDs, e.g.: + * https://www.ledcontrollercard.com/english/p10-outdoor-rgb-led-module-160x160mm-dip.html + * + */ +class P10Outdoor1R1G1BMultiplexBase : public MultiplexMapperBase { +public: + P10Outdoor1R1G1BMultiplexBase(const char *name) + : MultiplexMapperBase(name, 2) {} + + void MapSinglePanel(int x, int y, int *matrix_x, int *matrix_y) const { + const int vblock_is_odd = (y / tile_height_) % 2; + const int vblock_is_even = 1 - vblock_is_odd; + const int even_vblock_shift = vblock_is_even * even_vblock_offset_; + const int odd_vblock_shift = vblock_is_odd * odd_vblock_offset_; + + MapPanel(x, y, matrix_x, matrix_y, + vblock_is_even, vblock_is_odd, + even_vblock_shift, odd_vblock_shift); + } + +protected: + virtual void MapPanel(int x, int y, int *matrix_x, int *matrix_y, + int vblock_is_even, int vblock_is_odd, + int even_vblock_shift, int odd_vblock_shift) const = 0; + + static const int tile_width_ = 8; + static const int tile_height_ = 4; + static const int even_vblock_offset_ = 0; + static const int odd_vblock_offset_ = 8; +}; + +class P10Outdoor1R1G1BMultiplexMapper1 : public P10Outdoor1R1G1BMultiplexBase { +public: + P10Outdoor1R1G1BMultiplexMapper1() + : P10Outdoor1R1G1BMultiplexBase("P10Outdoor1R1G1-1") {} + +protected: + void MapPanel(int x, int y, int *matrix_x, int *matrix_y, + const int vblock_is_even, const int vblock_is_odd, + const int even_vblock_shift, const int odd_vblock_shift) const { + *matrix_x = tile_width_ * (1 + vblock_is_even + 2 * (x / tile_width_)) + - (x % tile_width_) - 1; + *matrix_y = (y % tile_height_) + tile_height_ * (y / (tile_height_ * 2)); + } +}; + +class P10Outdoor1R1G1BMultiplexMapper2 : public P10Outdoor1R1G1BMultiplexBase { +public: + P10Outdoor1R1G1BMultiplexMapper2() + : P10Outdoor1R1G1BMultiplexBase("P10Outdoor1R1G1-2") {} + +protected: + void MapPanel(int x, int y, int *matrix_x, int *matrix_y, + const int vblock_is_even, const int vblock_is_odd, + const int even_vblock_shift, const int odd_vblock_shift) const { + *matrix_x = vblock_is_even + ? tile_width_ * (1 + 2 * (x / tile_width_)) - (x % tile_width_) - 1 + : x + ((x + even_vblock_shift) / tile_width_) * tile_width_ + odd_vblock_shift; + *matrix_y = (y % tile_height_) + tile_height_ * (y / (tile_height_ * 2)); + } +}; + +class P10Outdoor1R1G1BMultiplexMapper3 : public P10Outdoor1R1G1BMultiplexBase { +public: + P10Outdoor1R1G1BMultiplexMapper3() + : P10Outdoor1R1G1BMultiplexBase("P10Outdoor1R1G1-3") {} + +protected: + void MapPanel(int x, int y, int *matrix_x, int *matrix_y, + const int vblock_is_even, const int vblock_is_odd, + const int even_vblock_shift, const int odd_vblock_shift) const { + *matrix_x = vblock_is_odd + ? tile_width_ * (2 + 2 * (x / tile_width_)) - (x % tile_width_) - 1 + : x + ((x + even_vblock_shift) / tile_width_) * tile_width_ + odd_vblock_shift; + *matrix_y = (y % tile_height_) + tile_height_ * (y / (tile_height_ * 2)); + } +}; + +class P10CoremanMapper : public MultiplexMapperBase { +public: + P10CoremanMapper() : MultiplexMapperBase("P10CoremanMapper", 4) {} + + void MapSinglePanel(int x, int y, int *matrix_x, int *matrix_y) const { + //Row offset 8,8,8,8,0,0,0,0,8,8,8,8,0,0,0,0 + int mulY = (y & 4) > 0 ? 0 : 8; + + //Row offset 9,9,8,8,1,1,0,0,9,9,8,8,1,1,0,0 + mulY += (y & 2) > 0 ? 0 : 1; + mulY += (x >> 2) & ~1; //Drop lsb + + *matrix_x = (mulY << 3) + x % 8; + *matrix_y = (y & 1) + ((y >> 2) & ~1); + } +}; + +/* + * P8 1R1G1B Outdoor P8-5S-V3.2-HX 20x40 + */ +class P8Outdoor1R1G1BMultiplexBase : public MultiplexMapperBase { +public: + P8Outdoor1R1G1BMultiplexBase(const char *name) + : MultiplexMapperBase(name, 2) {} + + void MapSinglePanel(int x, int y, int *matrix_x, int *matrix_y) const { + const int vblock_is_odd = (y / tile_height_) % 2; + const int vblock_is_even = 1 - vblock_is_odd; + const int even_vblock_shift = vblock_is_even * even_vblock_offset_; + const int odd_vblock_shift = vblock_is_odd * odd_vblock_offset_; + + MapPanel(x, y, matrix_x, matrix_y, + vblock_is_even, vblock_is_odd, + even_vblock_shift, odd_vblock_shift); + } + +protected: + virtual void MapPanel(int x, int y, int *matrix_x, int *matrix_y, + int vblock_is_even, int vblock_is_odd, + int even_vblock_shift, int odd_vblock_shift) const = 0; + + static const int tile_width_ = 8; + static const int tile_height_ = 5; + static const int even_vblock_offset_ = 0; + static const int odd_vblock_offset_ = 8; +}; + +class P8Outdoor1R1G1BMultiplexMapper : public P8Outdoor1R1G1BMultiplexBase { +public: + P8Outdoor1R1G1BMultiplexMapper() + : P8Outdoor1R1G1BMultiplexBase("P8Outdoor1R1G1") {} + +protected: + void MapPanel(int x, int y, int *matrix_x, int *matrix_y, + const int vblock_is_even, const int vblock_is_odd, + const int even_vblock_shift, const int odd_vblock_shift) const { + + + *matrix_x = vblock_is_even + ? tile_width_ * (1 + tile_width_ - 2 * (x / tile_width_)) + tile_width_ - (x % tile_width_) - 1 + : tile_width_ * (1 + tile_width_ - 2 * (x / tile_width_)) - tile_width_ + (x % tile_width_); + + *matrix_y = (tile_height_ - y % tile_height_) + tile_height_ * (1 - y / (tile_height_ * 2)) -1; + + } +}; + +/* + * Here is where the registration happens. + * If you add an instance of the mapper here, it will automatically be + * made available in the --led-multiplexing commandline option. + */ +static MuxMapperList *CreateMultiplexMapperList() { + MuxMapperList *result = new MuxMapperList(); + + // Here, register all multiplex mappers from above. + result->push_back(new StripeMultiplexMapper()); + result->push_back(new CheckeredMultiplexMapper()); + result->push_back(new SpiralMultiplexMapper()); + result->push_back(new ZStripeMultiplexMapper("ZStripe", 0, 8)); + result->push_back(new ZStripeMultiplexMapper("ZnMirrorZStripe", 4, 4)); + result->push_back(new CoremanMapper()); + result->push_back(new Kaler2ScanMapper()); + result->push_back(new ZStripeMultiplexMapper("ZStripeUneven", 8, 0)); + result->push_back(new P10MapperZ()); + result->push_back(new QiangLiQ8()); + result->push_back(new InversedZStripe()); + result->push_back(new P10Outdoor1R1G1BMultiplexMapper1()); + result->push_back(new P10Outdoor1R1G1BMultiplexMapper2()); + result->push_back(new P10Outdoor1R1G1BMultiplexMapper3()); + result->push_back(new P10CoremanMapper()); + result->push_back(new P8Outdoor1R1G1BMultiplexMapper()); + result->push_back(new FlippedStripeMultiplexMapper()); + return result; +} + +const MuxMapperList &GetRegisteredMultiplexMappers() { + static const MuxMapperList *all_mappers = CreateMultiplexMapperList(); + return *all_mappers; +} +} // namespace internal +} // namespace rgb_matrix diff --git a/lib/multiplex-mappers.o b/lib/multiplex-mappers.o new file mode 100644 index 0000000..44c01c0 Binary files /dev/null and b/lib/multiplex-mappers.o differ diff --git a/lib/options-initialize.cc b/lib/options-initialize.cc new file mode 100644 index 0000000..89b5abb --- /dev/null +++ b/lib/options-initialize.cc @@ -0,0 +1,461 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2013, 2016 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +#include "led-matrix.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "multiplex-mappers-internal.h" +#include "framebuffer-internal.h" + +#include "gpio.h" + +namespace rgb_matrix { +RuntimeOptions::RuntimeOptions() : +#ifdef RGB_SLOWDOWN_GPIO + gpio_slowdown(RGB_SLOWDOWN_GPIO), +#else + gpio_slowdown(1), +#endif + daemon(0), // Don't become a daemon by default. + drop_privileges(1), // Encourage good practice: drop privileges by default. + do_gpio_init(true) +{ + // Nothing to see here. +} + +namespace { +typedef char** argv_iterator; + +#define OPTION_PREFIX "--led-" +#define OPTION_PREFIX_LEN strlen(OPTION_PREFIX) + +static bool ConsumeBoolFlag(const char *flag_name, const argv_iterator &pos, + bool *result_value) { + const char *option = *pos; + if (strncmp(option, OPTION_PREFIX, OPTION_PREFIX_LEN) != 0) + return false; + option += OPTION_PREFIX_LEN; + bool value_to_set = true; + if (strncmp(option, "no-", 3) == 0) { + value_to_set = false; + option += 3; + } + if (strcmp(option, flag_name) != 0) + return false; // not consumed. + *result_value = value_to_set; + return true; +} + +static bool ConsumeIntFlag(const char *flag_name, + argv_iterator &pos, const argv_iterator end, + int *result_value, int *error) { + const char *option = *pos; + if (strncmp(option, OPTION_PREFIX, OPTION_PREFIX_LEN) != 0) + return false; + option += OPTION_PREFIX_LEN; + const size_t flag_len = strlen(flag_name); + if (strncmp(option, flag_name, flag_len) != 0) + return false; // not consumed. + const char *value; + if (option[flag_len] == '=') // --option=42 # value in same arg + value = option + flag_len + 1; + else if (pos + 1 < end) { // --option 42 # value in next arg + value = *(++pos); + } else { + fprintf(stderr, "Parameter expected after %s%s\n", + OPTION_PREFIX, flag_name); + ++*error; + return true; // consumed, but error. + } + char *end_value = NULL; + int val = strtol(value, &end_value, 10); + if (!*value || *end_value) { + fprintf(stderr, "Couldn't parse parameter %s%s=%s " + "(Expected decimal number but '%s' looks funny)\n", + OPTION_PREFIX, flag_name, value, end_value); + ++*error; + return true; // consumed, but error + } + *result_value = val; + return true; // consumed. +} + +// The resulting value is allocated. +static bool ConsumeStringFlag(const char *flag_name, + argv_iterator &pos, const argv_iterator end, + const char **result_value, int *error) { + const char *option = *pos; + if (strncmp(option, OPTION_PREFIX, OPTION_PREFIX_LEN) != 0) + return false; + option += OPTION_PREFIX_LEN; + const size_t flag_len = strlen(flag_name); + if (strncmp(option, flag_name, flag_len) != 0) + return false; // not consumed. + const char *value; + if (option[flag_len] == '=') // --option=hello # value in same arg + value = option + flag_len + 1; + else if (pos + 1 < end) { // --option hello # value in next arg + value = *(++pos); + } else { + fprintf(stderr, "Parameter expected after %s%s\n", + OPTION_PREFIX, flag_name); + ++*error; + *result_value = NULL; + return true; // consumed, but error. + } + *result_value = strdup(value); // This will leak, but no big deal. + return true; +} + +static bool FlagInit(int &argc, char **&argv, + RGBMatrix::Options *mopts, + RuntimeOptions *ropts, + bool remove_consumed_options) { + argv_iterator it = &argv[0]; + argv_iterator end = it + argc; + + std::vector unused_options; + unused_options.push_back(*it++); // Not interested in program name + + bool bool_scratch; + int err = 0; + bool posix_end_option_seen = false; // end of options '--' + for (/**/; it < end; ++it) { + posix_end_option_seen |= (strcmp(*it, "--") == 0); + if (!posix_end_option_seen) { + if (ConsumeStringFlag("gpio-mapping", it, end, + &mopts->hardware_mapping, &err)) + continue; + if (ConsumeStringFlag("rgb-sequence", it, end, + &mopts->led_rgb_sequence, &err)) + continue; + if (ConsumeStringFlag("pixel-mapper", it, end, + &mopts->pixel_mapper_config, &err)) + continue; + if (ConsumeStringFlag("panel-type", it, end, + &mopts->panel_type, &err)) + continue; + if (ConsumeIntFlag("rows", it, end, &mopts->rows, &err)) + continue; + if (ConsumeIntFlag("cols", it, end, &mopts->cols, &err)) + continue; + if (ConsumeIntFlag("chain", it, end, &mopts->chain_length, &err)) + continue; + if (ConsumeIntFlag("parallel", it, end, &mopts->parallel, &err)) + continue; + if (ConsumeIntFlag("multiplexing", it, end, &mopts->multiplexing, &err)) + continue; + if (ConsumeIntFlag("brightness", it, end, &mopts->brightness, &err)) + continue; + if (ConsumeIntFlag("scan-mode", it, end, &mopts->scan_mode, &err)) + continue; + if (ConsumeIntFlag("pwm-bits", it, end, &mopts->pwm_bits, &err)) + continue; + if (ConsumeIntFlag("pwm-lsb-nanoseconds", it, end, + &mopts->pwm_lsb_nanoseconds, &err)) + continue; + if (ConsumeIntFlag("pwm-dither-bits", it, end, + &mopts->pwm_dither_bits, &err)) + continue; + if (ConsumeIntFlag("row-addr-type", it, end, + &mopts->row_address_type, &err)) + continue; + if (ConsumeIntFlag("limit-refresh", it, end, + &mopts->limit_refresh_rate_hz, &err)) + continue; + if (ConsumeBoolFlag("show-refresh", it, &mopts->show_refresh_rate)) + continue; + if (ConsumeBoolFlag("inverse", it, &mopts->inverse_colors)) + continue; + // We don't have a swap_green_blue option anymore, but we simulate the + // flag for a while. + bool swap_green_blue; + if (ConsumeBoolFlag("swap-green-blue", it, &swap_green_blue)) { + if (strlen(mopts->led_rgb_sequence) == 3) { + char *new_sequence = strdup(mopts->led_rgb_sequence); + new_sequence[0] = mopts->led_rgb_sequence[0]; + new_sequence[1] = mopts->led_rgb_sequence[2]; + new_sequence[2] = mopts->led_rgb_sequence[1]; + mopts->led_rgb_sequence = new_sequence; // leaking. Ignore. + } + continue; + } + bool allow_hardware_pulsing = !mopts->disable_hardware_pulsing; + if (ConsumeBoolFlag("hardware-pulse", it, &allow_hardware_pulsing)) { + mopts->disable_hardware_pulsing = !allow_hardware_pulsing; + continue; + } + + bool request_help = false; + if (ConsumeBoolFlag("help", it, &request_help) && request_help) { + // In that case, we pretend to have failure in parsing, which will + // trigger printing the usage(). Typically :) + return false; + } + + //-- Runtime options. + if (ConsumeIntFlag("slowdown-gpio", it, end, &ropts->gpio_slowdown, &err)) + continue; + if (ropts->daemon >= 0 && ConsumeBoolFlag("daemon", it, &bool_scratch)) { + ropts->daemon = bool_scratch ? 1 : 0; + continue; + } + if (ropts->drop_privileges >= 0 && + ConsumeBoolFlag("drop-privs", it, &bool_scratch)) { + ropts->drop_privileges = bool_scratch ? 1 : 0; + continue; + } + if (strncmp(*it, OPTION_PREFIX, OPTION_PREFIX_LEN) == 0) { + fprintf(stderr, "Option %s starts with %s but it is unknown. Typo?\n", + *it, OPTION_PREFIX); + } + } + unused_options.push_back(*it); + } + + if (err > 0) { + return false; + } + + if (remove_consumed_options) { + // Success. Re-arrange flags to only include the ones not consumed. + argc = (int) unused_options.size(); + for (int i = 0; i < argc; ++i) { + argv[i] = unused_options[i]; + } + } + return true; +} + +} // anonymous namespace + +bool ParseOptionsFromFlags(int *argc, char ***argv, + RGBMatrix::Options *m_opt_in, + RuntimeOptions *rt_opt_in, + bool remove_consumed_options) { + if (argc == NULL || argv == NULL) { + fprintf(stderr, "Called ParseOptionsFromFlags() without argc/argv\n"); + return false; + } + // Replace NULL arguments with some scratch-space. + RGBMatrix::Options scratch_matrix; + RGBMatrix::Options *mopt = (m_opt_in != NULL) ? m_opt_in : &scratch_matrix; + + RuntimeOptions scratch_rt; + RuntimeOptions *ropt = (rt_opt_in != NULL) ? rt_opt_in : &scratch_rt; + + return FlagInit(*argc, *argv, mopt, ropt, remove_consumed_options); +} + +static std::string CreateAvailableMultiplexString( + const internal::MuxMapperList &m) { + std::string result; + char buffer[256]; + for (size_t i = 0; i < m.size(); ++i) { + if (i != 0) result.append("; "); + snprintf(buffer, sizeof(buffer), "%d=%s", (int) i+1, m[i]->GetName()); + result.append(buffer); + } + return result; +} + +void PrintMatrixFlags(FILE *out, const RGBMatrix::Options &d, + const RuntimeOptions &r) { + const internal::MuxMapperList &muxers + = internal::GetRegisteredMultiplexMappers(); + + std::vector mapper_names = GetAvailablePixelMappers(); + std::string available_mappers; + for (size_t i = 0; i < mapper_names.size(); ++i) { + if (i != 0) available_mappers.append(", "); + available_mappers.append("\"").append(mapper_names[i]).append("\""); + } + + fprintf(out, + "\t--led-gpio-mapping= : Name of GPIO mapping used. Default \"%s\"\n" + "\t--led-rows= : Panel rows. Typically 8, 16, 32 or 64." + " (Default: %d).\n" + "\t--led-cols= : Panel columns. Typically 32 or 64. " + "(Default: %d).\n" + "\t--led-chain= : Number of daisy-chained panels. " + "(Default: %d).\n" + "\t--led-parallel= : Parallel chains. range=1..3 " +#ifdef ENABLE_WIDE_GPIO_COMPUTE_MODULE + "(6 for CM3) " +#endif + "(Default: %d).\n" + "\t--led-multiplexing=<0..%d> : Mux type: 0=direct; %s (Default: 0)\n" + "\t--led-pixel-mapper : Semicolon-separated list of pixel-mappers to arrange pixels.\n" + "\t Optional params after a colon e.g. \"U-mapper;Rotate:90\"\n" + "\t Available: %s. Default: \"\"\n" + "\t--led-pwm-bits=<1..%d> : PWM bits (Default: %d).\n" + "\t--led-brightness=: Brightness in percent (Default: %d).\n" + "\t--led-scan-mode=<0..1> : 0 = progressive; 1 = interlaced " + "(Default: %d).\n" + "\t--led-row-addr-type=<0..4>: 0 = default; 1 = AB-addressed panels; 2 = direct row select; 3 = ABC-addressed panels; 4 = ABC Shift + DE direct " + "(Default: 0).\n" + "\t--led-%sshow-refresh : %show refresh rate.\n" + "\t--led-limit-refresh= : Limit refresh rate to this frequency in Hz. Useful to keep a\n" + "\t constant refresh rate on loaded system. 0=no limit. Default: %d\n" + "\t--led-%sinverse " + ": Switch if your matrix has inverse colors %s.\n" + "\t--led-rgb-sequence : Switch if your matrix has led colors " + "swapped (Default: \"RGB\")\n" + "\t--led-pwm-lsb-nanoseconds : PWM Nanoseconds for LSB " + "(Default: %d)\n" + "\t--led-pwm-dither-bits=<0..2> : Time dithering of lower bits " + "(Default: 0)\n" + "\t--led-%shardware-pulse : %sse hardware pin-pulse generation.\n" + "\t--led-panel-type= : Needed to initialize special panels. Supported: 'FM6126A', 'FM6127'\n", + d.hardware_mapping, + d.rows, d.cols, d.chain_length, d.parallel, + (int) muxers.size(), CreateAvailableMultiplexString(muxers).c_str(), + available_mappers.c_str(), + internal::Framebuffer::kBitPlanes, d.pwm_bits, + d.brightness, d.scan_mode, + d.show_refresh_rate ? "no-" : "", d.show_refresh_rate ? "Don't s" : "S", + d.limit_refresh_rate_hz, + d.inverse_colors ? "no-" : "", d.inverse_colors ? "off" : "on", + d.pwm_lsb_nanoseconds, + !d.disable_hardware_pulsing ? "no-" : "", + !d.disable_hardware_pulsing ? "Don't u" : "U"); + + fprintf(out, "\t--led-slowdown-gpio=<0..4>: " + "Slowdown GPIO. Needed for faster Pis/slower panels " + "(Default: %d).\n", r.gpio_slowdown); + if (r.daemon >= 0) { + const bool on = (r.daemon > 0); + fprintf(out, + "\t--led-%sdaemon : " + "%sake the process run in the background as daemon.\n", + on ? "no-" : "", on ? "Don't m" : "M"); + } + if (r.drop_privileges >= 0) { + const bool on = (r.drop_privileges > 0); + fprintf(out, + "\t--led-%sdrop-privs : %srop privileges from 'root' " + "after initializing the hardware.\n", + on ? "no-" : "", on ? "Don't d" : "D"); + } +} + +bool RGBMatrix::Options::Validate(std::string *err_in) const { + std::string scratch; + std::string *err = err_in ? err_in : &scratch; + bool success = true; + if (rows < 8 || rows > 64 || rows % 2 != 0) { + err->append("Invalid number or rows per panel (--led-rows). " + "Should be in range of [8..64] and divisible by 2.\n"); + success = false; + } + + if (cols < 16) { + err->append("Invlid number of columns for panel (--led-cols). " + "Typically that is something like 32 or 64\n"); + success = false; + } + + if (chain_length < 1) { + err->append("Chain-length outside usable range.\n"); + success = false; + } + + const internal::MuxMapperList &muxers + = internal::GetRegisteredMultiplexMappers(); + if (multiplexing < 0 || multiplexing > (int)muxers.size()) { + err->append("Multiplexing can only be one of 0=normal; ") + .append(CreateAvailableMultiplexString(muxers)); + success = false; + } + + if (row_address_type < 0 || row_address_type > 4) { + err->append("Row address type values can be 0 (default), 1 (AB addressing), 2 (direct row select), 3 (ABC address), 4 (ABC Shift + DE direct).\n"); + success = false; + } + +#ifdef ENABLE_WIDE_GPIO_COMPUTE_MODULE + const bool is_cm = (strcmp(hardware_mapping, "compute-module") == 0); +#else + const bool is_cm = false; +#endif + if (parallel < 1 || parallel > (is_cm ? 6 : 3)) { + err->append("Parallel outside usable range (1..3 allowed" +#ifdef ENABLE_WIDE_GPIO_COMPUTE_MODULE + ", up to 6 only for CM3" +#endif + ").\n"); + success = false; + } + + if (brightness < 1 || brightness > 100) { + err->append("Brightness outside usable range (Percent 1..100 allowed).\n"); + success = false; + } + + if (pwm_bits <= 0 || pwm_bits > internal::Framebuffer::kBitPlanes) { + char buffer[256]; + snprintf(buffer, sizeof(buffer), + "Invalid range of pwm-bits (1..%d allowed).\n", + internal::Framebuffer::kBitPlanes); + err->append(buffer); + success = false; + } + + if (scan_mode < 0 || scan_mode > 1) { + err->append("Invalid scan mode (0 or 1 allowed).\n"); + success = false; + } + + if (pwm_lsb_nanoseconds < 50 || pwm_lsb_nanoseconds > 3000) { + err->append("Invalid range of pwm-lsb-nanoseconds (50..3000 allowed).\n"); + success = false; + } + + if (pwm_dither_bits < 0 || pwm_dither_bits > 2) { + err->append("Inavlid range of pwm-dither-bits (0..2 allowed).\n"); + success = false; + } + + if (led_rgb_sequence == NULL || strlen(led_rgb_sequence) != 3) { + err->append("led-sequence needs to be three characters long.\n"); + success = false; + } else { + if ((!strchr(led_rgb_sequence, 'R') && !strchr(led_rgb_sequence, 'r')) + || (!strchr(led_rgb_sequence, 'G') && !strchr(led_rgb_sequence, 'g')) + || (!strchr(led_rgb_sequence, 'B') && !strchr(led_rgb_sequence, 'b'))) { + err->append("led-sequence needs to contain all of letters 'R', 'G' " + "and 'B'\n"); + success = false; + } + } + + if (!success && !err_in) { + // If we didn't get a string to write to, we write things to stderr. + fprintf(stderr, "%s", err->c_str()); + } + + return success; +} + +} // namespace rgb_matrix diff --git a/lib/options-initialize.o b/lib/options-initialize.o new file mode 100644 index 0000000..3eb0ebf Binary files /dev/null and b/lib/options-initialize.o differ diff --git a/lib/pixel-mapper.cc b/lib/pixel-mapper.cc new file mode 100644 index 0000000..57258f1 --- /dev/null +++ b/lib/pixel-mapper.cc @@ -0,0 +1,338 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2018 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +#include "pixel-mapper.h" + +#include +#include +#include +#include + +#include + +namespace rgb_matrix { +namespace { + +class RotatePixelMapper : public PixelMapper { +public: + RotatePixelMapper() : angle_(0) {} + + virtual const char *GetName() const { return "Rotate"; } + + virtual bool SetParameters(int chain, int parallel, const char *param) { + if (param == NULL || strlen(param) == 0) { + angle_ = 0; + return true; + } + char *errpos; + const int angle = strtol(param, &errpos, 10); + if (*errpos != '\0') { + fprintf(stderr, "Invalid rotate parameter '%s'\n", param); + return false; + } + if (angle % 90 != 0) { + fprintf(stderr, "Rotation needs to be multiple of 90 degrees\n"); + return false; + } + angle_ = (angle + 360) % 360; + return true; + } + + virtual bool GetSizeMapping(int matrix_width, int matrix_height, + int *visible_width, int *visible_height) + const { + if (angle_ % 180 == 0) { + *visible_width = matrix_width; + *visible_height = matrix_height; + } else { + *visible_width = matrix_height; + *visible_height = matrix_width; + } + return true; + } + + virtual void MapVisibleToMatrix(int matrix_width, int matrix_height, + int x, int y, + int *matrix_x, int *matrix_y) const { + switch (angle_) { + case 0: + *matrix_x = x; + *matrix_y = y; + break; + case 90: + *matrix_x = matrix_width - y - 1; + *matrix_y = x; + break; + case 180: + *matrix_x = matrix_width - x - 1; + *matrix_y = matrix_height - y - 1; + break; + case 270: + *matrix_x = y; + *matrix_y = matrix_height - x - 1; + break; + } + } + +private: + int angle_; +}; + +class MirrorPixelMapper : public PixelMapper { +public: + MirrorPixelMapper() : horizontal_(true) {} + + virtual const char *GetName() const { return "Mirror"; } + + virtual bool SetParameters(int chain, int parallel, const char *param) { + if (param == NULL || strlen(param) == 0) { + horizontal_ = true; + return true; + } + if (strlen(param) != 1) { + fprintf(stderr, "Mirror parameter should be a single " + "character:'V' or 'H'\n"); + } + switch (*param) { + case 'V': + case 'v': + horizontal_ = false; + break; + case 'H': + case 'h': + horizontal_ = true; + break; + default: + fprintf(stderr, "Mirror parameter should be either 'V' or 'H'\n"); + return false; + } + return true; + } + + virtual bool GetSizeMapping(int matrix_width, int matrix_height, + int *visible_width, int *visible_height) + const { + *visible_height = matrix_height; + *visible_width = matrix_width; + return true; + } + + virtual void MapVisibleToMatrix(int matrix_width, int matrix_height, + int x, int y, + int *matrix_x, int *matrix_y) const { + if (horizontal_) { + *matrix_x = matrix_width - 1 - x; + *matrix_y = y; + } else { + *matrix_x = x; + *matrix_y = matrix_height - 1 - y; + } + } + +private: + bool horizontal_; +}; + +// If we take a long chain of panels and arrange them in a U-shape, so +// that after half the panels we bend around and continue below. This way +// we have a panel that has double the height but only uses one chain. +// A single chain display with four 32x32 panels can then be arranged in this +// 64x64 display: +// [<][<][<][<] }- Raspbery Pi connector +// +// can be arranged in this U-shape +// [<][<] }----- Raspberry Pi connector +// [>][>] +// +// This works for more than one chain as well. Here an arrangement with +// two chains with 8 panels each +// [<][<][<][<] }-- Pi connector #1 +// [>][>][>][>] +// [<][<][<][<] }--- Pi connector #2 +// [>][>][>][>] +class UArrangementMapper : public PixelMapper { +public: + UArrangementMapper() : parallel_(1) {} + + virtual const char *GetName() const { return "U-mapper"; } + + virtual bool SetParameters(int chain, int parallel, const char *param) { + if (chain < 2) { // technically, a chain of 2 would work, but somewhat pointless + fprintf(stderr, "U-mapper: need at least --led-chain=4 for useful folding\n"); + return false; + } + if (chain % 2 != 0) { + fprintf(stderr, "U-mapper: Chain (--led-chain) needs to be divisible by two\n"); + return false; + } + parallel_ = parallel; + return true; + } + + virtual bool GetSizeMapping(int matrix_width, int matrix_height, + int *visible_width, int *visible_height) + const { + *visible_width = (matrix_width / 64) * 32; // Div at 32px boundary + *visible_height = 2 * matrix_height; + if (matrix_height % parallel_ != 0) { + fprintf(stderr, "%s For parallel=%d we would expect the height=%d " + "to be divisible by %d ??\n", + GetName(), parallel_, matrix_height, parallel_); + return false; + } + return true; + } + + virtual void MapVisibleToMatrix(int matrix_width, int matrix_height, + int x, int y, + int *matrix_x, int *matrix_y) const { + const int panel_height = matrix_height / parallel_; + const int visible_width = (matrix_width / 64) * 32; + const int slab_height = 2 * panel_height; // one folded u-shape + const int base_y = (y / slab_height) * panel_height; + y %= slab_height; + if (y < panel_height) { + x += matrix_width / 2; + } else { + x = visible_width - x - 1; + y = slab_height - y - 1; + } + *matrix_x = x; + *matrix_y = base_y + y; + } + +private: + int parallel_; +}; + + + +class VerticalMapper : public PixelMapper { +public: + VerticalMapper() {} + + virtual const char *GetName() const { return "V-mapper"; } + + virtual bool SetParameters(int chain, int parallel, const char *param) { + chain_ = chain; + parallel_ = parallel; + // optional argument :Z allow for every other panel to be flipped + // upside down so that cabling can be shorter: + // [ O < I ] without Z [ O < I ] + // ,---^ <---- ^ + // [ O < I ] [ I > O ] + // ,---^ with Z ^ + // [ O < I ] ---> [ O < I ] + z_ = (param && strcasecmp(param, "Z") == 0); + return true; + } + + virtual bool GetSizeMapping(int matrix_width, int matrix_height, + int *visible_width, int *visible_height) + const { + *visible_width = matrix_width * parallel_ / chain_; + *visible_height = matrix_height * chain_ / parallel_; +#if 0 + fprintf(stderr, "%s: C:%d P:%d. Turning W:%d H:%d Physical " + "into W:%d H:%d Virtual\n", + GetName(), chain_, parallel_, + *visible_width, *visible_height, matrix_width, matrix_height); +#endif + return true; + } + + virtual void MapVisibleToMatrix(int matrix_width, int matrix_height, + int x, int y, + int *matrix_x, int *matrix_y) const { + const int panel_width = matrix_width / chain_; + const int panel_height = matrix_height / parallel_; + const int x_panel_start = y / panel_height * panel_width; + const int y_panel_start = x / panel_width * panel_height; + const int x_within_panel = x % panel_width; + const int y_within_panel = y % panel_height; + const bool needs_flipping = z_ && (y / panel_height) % 2 == 1; + *matrix_x = x_panel_start + (needs_flipping + ? panel_width - 1 - x_within_panel + : x_within_panel); + *matrix_y = y_panel_start + (needs_flipping + ? panel_height - 1 - y_within_panel + : y_within_panel); + } + +private: + bool z_; + int chain_; + int parallel_; +}; + + +typedef std::map MapperByName; +static void RegisterPixelMapperInternal(MapperByName *registry, + PixelMapper *mapper) { + assert(mapper != NULL); + std::string lower_name; + for (const char *n = mapper->GetName(); *n; n++) + lower_name.append(1, tolower(*n)); + (*registry)[lower_name] = mapper; +} + +static MapperByName *CreateMapperMap() { + MapperByName *result = new MapperByName(); + + // Register all the default PixelMappers here. + RegisterPixelMapperInternal(result, new RotatePixelMapper()); + RegisterPixelMapperInternal(result, new UArrangementMapper()); + RegisterPixelMapperInternal(result, new VerticalMapper()); + RegisterPixelMapperInternal(result, new MirrorPixelMapper()); + return result; +} + +static MapperByName *GetMapperMap() { + static MapperByName *singleton_instance = CreateMapperMap(); + return singleton_instance; +} +} // anonymous namespace + +// Public API. +void RegisterPixelMapper(PixelMapper *mapper) { + RegisterPixelMapperInternal(GetMapperMap(), mapper); +} + +std::vector GetAvailablePixelMappers() { + std::vector result; + MapperByName *m = GetMapperMap(); + for (MapperByName::const_iterator it = m->begin(); it != m->end(); ++it) { + result.push_back(it->second->GetName()); + } + return result; +} + +const PixelMapper *FindPixelMapper(const char *name, + int chain, int parallel, + const char *parameter) { + std::string lower_name; + for (const char *n = name; *n; n++) lower_name.append(1, tolower(*n)); + MapperByName::const_iterator found = GetMapperMap()->find(lower_name); + if (found == GetMapperMap()->end()) { + fprintf(stderr, "%s: no such mapper\n", name); + return NULL; + } + PixelMapper *mapper = found->second; + if (mapper == NULL) return NULL; // should not happen. + if (!mapper->SetParameters(chain, parallel, parameter)) + return NULL; // Got parameter, but couldn't deal with it. + return mapper; +} +} // namespace rgb_matrix diff --git a/lib/pixel-mapper.o b/lib/pixel-mapper.o new file mode 100644 index 0000000..cb48675 Binary files /dev/null and b/lib/pixel-mapper.o differ diff --git a/lib/thread.cc b/lib/thread.cc new file mode 100644 index 0000000..b107673 --- /dev/null +++ b/lib/thread.cc @@ -0,0 +1,100 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2013 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +#include "thread.h" + +#include +#include +#include +#include +#include +#include + +namespace rgb_matrix { +void *Thread::PthreadCallRun(void *tobject) { + reinterpret_cast(tobject)->Run(); + return NULL; +} + +Thread::Thread() : started_(false) {} +Thread::~Thread() { + WaitStopped(); +} + +void Thread::WaitStopped() { + if (!started_) return; + int result = pthread_join(thread_, NULL); + if (result != 0) { + perror("Issue joining thread"); + } + started_ = false; +} + +void Thread::Start(int priority, uint32_t affinity_mask) { + assert(!started_); // Did you call WaitStopped() ? + pthread_create(&thread_, NULL, &PthreadCallRun, this); + int err; + + if (priority > 0) { + struct sched_param p; + p.sched_priority = priority; + if ((err = pthread_setschedparam(thread_, SCHED_FIFO, &p))) { + char buffer[PATH_MAX]; + const char *bin = realpath("/proc/self/exe", buffer); // Linux specific. + fprintf(stderr, "Can't set realtime thread priority=%d: %s.\n" + "\tYou are probably not running as root ?\n" + "\tThis will seriously mess with color stability and flicker\n" + "\tof the matrix. Please run as `root` (e.g. by invoking this\n" + "\tprogram with `sudo`), or setting the capability on this\n" + "\tbinary by calling\n" + "\tsudo setcap 'cap_sys_nice=eip' %s\n", + p.sched_priority, strerror(err), bin ? bin : ""); + } + } + + if (affinity_mask != 0) { + cpu_set_t cpu_mask; + CPU_ZERO(&cpu_mask); + for (int i = 0; i < 32; ++i) { + if ((affinity_mask & (1< +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +#ifndef RPI_GRAPHICS_UTF8_H +#define RPI_GRAPHICS_UTF8_H + +#include + +// Utility function that reads UTF-8 encoded codepoints from byte iterator. +// No error checking, we assume string is UTF-8 clean. +template +uint32_t utf8_next_codepoint(byte_iterator &it) { + uint32_t cp = *it++; + if (cp < 0x80) { + return cp; // iterator already incremented. + } + else if ((cp & 0xE0) == 0xC0) { + cp = ((cp & 0x1F) << 6) + (*it & 0x3F); + } + else if ((cp & 0xF0) == 0xE0) { + cp = ((cp & 0x0F) << 12) + ((*it & 0x3F) << 6); + cp += (*++it & 0x3F); + } + else if ((cp & 0xF8) == 0xF0) { + cp = ((cp & 0x07) << 18) + ((*it & 0x3F) << 12); + cp += (*++it & 0x3F) << 6; + cp += (*++it & 0x3F); + } + else if ((cp & 0xFC) == 0xF8) { + cp = ((cp & 0x03) << 24) + ((*it & 0x3F) << 18); + cp += (*++it & 0x3F) << 12; + cp += (*++it & 0x3F) << 6; + cp += (*++it & 0x3F); + } + else if ((cp & 0xFE) == 0xFC) { + cp = ((cp & 0x01) << 30) + ((*it & 0x3F) << 24); + cp += (*++it & 0x3F) << 18; + cp += (*++it & 0x3F) << 12; + cp += (*++it & 0x3F) << 6; + cp += (*++it & 0x3F); + } + ++it; + return cp; +} + +#endif // RPI_GRAPHICS_UTF8_H diff --git a/librgbmatrix.a b/librgbmatrix.a new file mode 100644 index 0000000..2c4653b Binary files /dev/null and b/librgbmatrix.a differ diff --git a/net.c b/net.c new file mode 100644 index 0000000..d6ec324 --- /dev/null +++ b/net.c @@ -0,0 +1,253 @@ +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "net.h" + +// Lines longer than this are considered an error. +#define NET_MAX_LINE 1024 + +// The server buffers up to NET_READ_BUFFER bytes per client connection. +// Lower values allow lots of clients to draw at the same time, each with a fair share. +// Higher values increase throughput but fast clients might be able to draw large batches at once. +#define NET_MAX_BUFFER 10240 + +typedef struct NetClient { + int sock_fd; + struct bufferevent *buf_ev; + int state; + void *user; +} NetClient; + +#define NET_CSTATE_OPEN 0 +#define NET_CSTATE_CLOSING 1 + +static inline int min(int a, int b) { + return a < b ? a : b; +} + +// global state +static struct event_base *base; +static char * line_buffer; + +// User defined callbacks +static net_on_connect netcb_on_connect = NULL; +static net_on_read netcb_on_read = NULL; +static net_on_close netcb_on_close = NULL; + +// libevent callbacks + + +// Like evbuffer_readln, but read into an existing string instead of allocating a new one. +// Return 1 if a line ending was found and the line was read completely. +// Return 2 if a line ending was found but the line was read only partially (longer than maxread-1). +// Return 0 if no line ending not found within the buffer. +int net_evbuffer_readln(struct evbuffer *buffer, char *line, size_t maxread, size_t * read_out, enum evbuffer_eol_style eol_style) { + struct evbuffer_ptr it; + size_t read_len=0, eol_len=0; + + it = evbuffer_search_eol(buffer, NULL, &eol_len, eol_style); + if (it.pos < 0) + return 0; + + read_len = min(maxread-1, it.pos); + + if(read_out) + *read_out = read_len; + + evbuffer_remove(buffer, line, read_len); + line[read_len] = '\0'; + + // Eat EOL if we have read all bytes before it + if(read_len == it.pos) { + evbuffer_drain(buffer, eol_len); + return 1; + } else { + return 2; + } +} + +static void netev_on_read(struct bufferevent *bev, void *ctx) { + struct evbuffer *input; + NetClient *client = ctx; + int r = 0; + + input = bufferevent_get_input(bev); + + // Change while->if for less throughput but more fair pixel distribution across client connections. + while ((r = net_evbuffer_readln(input, line_buffer, NET_MAX_LINE, NULL, EVBUFFER_EOL_LF)) == 1) { + (*netcb_on_read)(client, line_buffer); + } + + if (r == 2) { + net_err(client, "Line to long"); + } +} + + +static void netev_on_write(struct bufferevent *bev, void *arg) { + NetClient *client = arg; + + if (client->state == NET_CSTATE_CLOSING + && evbuffer_get_length(bufferevent_get_output(bev)) == 0) { + + if (netcb_on_close) + (*netcb_on_close)(client, 0); + + bufferevent_free(bev); + free(client); + } +} + +static void netev_on_error(struct bufferevent *bev, short error, void *arg) { + NetClient *client = arg; + + // TODO: Some logging? + if (error & BEV_EVENT_EOF) { + } else if (error & BEV_EVENT_ERROR) { + } else if (error & BEV_EVENT_TIMEOUT) { + } + + client->state = NET_CSTATE_CLOSING; + if (netcb_on_close) + (*netcb_on_close)(client, error); + + bufferevent_free(bev); + free(client); +} + +static void on_accept(evutil_socket_t listener, short event, void *arg) { + struct event_base *base = arg; + struct sockaddr_storage ss; + socklen_t slen = sizeof(ss); + int fd = accept(listener, (struct sockaddr*) &ss, &slen); + if (fd < 0) { + perror("accept failed"); + } else if (fd > FD_SETSIZE) { + close(fd); // TODO: verify if this is needed. Only for select()? But libevent uses poll/kqueue? + } else { + NetClient *client = calloc(1, sizeof(NetClient)); + if (client == NULL) { + perror("client malloc failed"); + close(fd); + return; + } + + client->sock_fd = fd; + client->buf_ev = bufferevent_socket_new(base, fd, + BEV_OPT_CLOSE_ON_FREE); + + evutil_make_socket_nonblocking(fd); + bufferevent_setcb(client->buf_ev, netev_on_read, netev_on_write, + netev_on_error, client); + bufferevent_setwatermark(client->buf_ev, EV_READ, 0, NET_MAX_BUFFER); + + if (netcb_on_connect) + (*netcb_on_connect)(client); + + if (client->state == NET_CSTATE_OPEN) + bufferevent_enable(client->buf_ev, EV_READ | EV_WRITE); + } +} + +// Public functions + +void net_start(int port, net_on_connect on_connect, net_on_read on_read, + net_on_close on_close) { + evutil_socket_t listener; + struct sockaddr_in sin; +// struct sockaddr_in6 sin; + struct event *listener_event; + + line_buffer = malloc(sizeof(char)*NET_MAX_LINE); + + evthread_use_pthreads(); + + //setvbuf(stdout, NULL, _IONBF, 0); + + netcb_on_connect = on_connect; + netcb_on_read = on_read; + netcb_on_close = on_close; + + base = event_base_new(); + if (!base) + err(1, "Failed to create event_base"); + + //evthread_make_base_notifiable(base); + + sin.sin_family = AF_INET; +// sin.sin6_family = AF_INET6; + sin.sin_addr.s_addr = 0; +// sin.sin6_addr = in6addr_any; + sin.sin_port = htons(1337); +// sin.sin6_port = htons(1337); + listener = socket(AF_INET, SOCK_STREAM, 0); +// listener = socket(AF_INET6, SOCK_STREAM, 0); + evutil_make_socket_nonblocking(listener); + evutil_make_listen_socket_reuseable(listener); + + if (bind(listener, (struct sockaddr*) &sin, sizeof(sin)) < 0) { + err(1, "bind failed"); + } + + if (listen(listener, 16) < 0) { + err(1, "listen failed"); + } + + listener_event = event_new(base, listener, EV_READ | EV_PERSIST, on_accept, + (void*) base); + event_add(listener_event, NULL); + + event_base_dispatch(base); +} + +void net_stop() { + event_base_loopbreak(base); + if(line_buffer) { + free(line_buffer); + line_buffer = NULL; + } +} + +void net_send(NetClient *client, const char * msg) { + struct evbuffer *output = bufferevent_get_output(client->buf_ev); + evbuffer_add(output, msg, strlen(msg)); + evbuffer_add(output, "\n", 1); +} + +void net_close(NetClient *client) { + if (client->state == NET_CSTATE_OPEN) { + client->state = NET_CSTATE_CLOSING; + bufferevent_disable(client->buf_ev, EV_READ); + } +} + +void net_err(NetClient *client, const char * msg) { + struct evbuffer *output = bufferevent_get_output(client->buf_ev); + evbuffer_add(output, "ERROR: ", 7); + evbuffer_add(output, msg, strlen(msg)); + evbuffer_add(output, "\n", 1); + net_close(client); +} + +void net_set_user(NetClient *client, void *user) { + client->user = user; +} + +void net_get_user(NetClient *client, void **user) { + *user = client->user; +} + diff --git a/net.h b/net.h new file mode 100644 index 0000000..593ecdf --- /dev/null +++ b/net.h @@ -0,0 +1,38 @@ +#ifndef NET_H_ +#define NET_H_ + +typedef struct NetClient NetClient; + +#define NET_CSTATE_OPEN 0 +#define NET_CSTATE_CLOSING 1 + +// Callback called immediately after a client connects +typedef void (*net_on_connect)(NetClient *client); + +// Callback called for each line of input. +// The char array is null-terminated and does not include the final line break. +// It is NOT owned by the callback and freed as soon as the callback returned. +typedef void (*net_on_read)(NetClient *client, char* line); + +// Callback called after a client disconnects. +// The second parameter is 0 for a normal client-induced disconnect and != 0 on errors. +typedef void (*net_on_close)(NetClient *client, int error); + +// Start the server and block until it is closed again. +void net_start(int port, net_on_connect on_connect, net_on_read on_read, net_on_close on_close); + +// Stop the server as soon as possible +void net_stop(); + +// Send a string to the client. A newline is added automatically. +void net_send(NetClient *client, const char * msg); +// Stop reading from this clients socket, send all bytes still in the output buffer, then close the connection. +void net_close(NetClient *client); +// Send an error message to the client, then close the connection. +void net_err(NetClient *client, const char * msg); + +// Get or set the user attachment, a pointer to an arbitrary data structure or NULL +void net_set_user(NetClient *client, void *user); +void net_get_user(NetClient *client, void **user); + +#endif /* NET_H_ */ diff --git a/net.o b/net.o new file mode 100644 index 0000000..2802979 Binary files /dev/null and b/net.o differ diff --git a/pixel-mapper.h b/pixel-mapper.h new file mode 100644 index 0000000..6963d00 --- /dev/null +++ b/pixel-mapper.h @@ -0,0 +1,110 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2018 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +#ifndef RGBMATRIX_PIXEL_MAPPER +#define RGBMATRIX_PIXEL_MAPPER + +#include +#include + +namespace rgb_matrix { + +// A pixel mapper is a way for you to map pixels of LED matrixes to a different +// layout. If you have an implementation of a PixelMapper, you can give it +// to the RGBMatrix::ApplyPixelMapper(), which then presents you a canvas +// that has the new "visible_width", "visible_height". +class PixelMapper { +public: + virtual ~PixelMapper() {} + + // Get the name of this PixelMapper. Each PixelMapper needs to have a name + // so that it can be referred to with command line flags. + virtual const char *GetName() const = 0; + + // Pixel mappers receive the chain and parallel information and + // might receive optional user-parameters, e.g. from command line flags. + // + // This is a single string containing the parameters. + // You can be used from simple scalar parameters, such as the angle for + // the rotate transformer, or more complex parameters that describe a mapping + // of panels for instance. + // Keep it concise (as people will give parameters on the command line) and + // don't use semicolons in your string (as they are + // used to separate pixel mappers on the command line). + // + // For instance, the rotate transformer is invoked like this + // --led-pixel-mapper=rotate:90 + // And the parameter that is passed to SetParameter() is "90". + // + // Returns 'true' if parameter was parsed successfully. + virtual bool SetParameters(int chain, int parallel, + const char *parameter_string) { + return true; + } + + // Given a underlying matrix (width, height), returns the + // visible (width, height) after the mapping. + // E.g. a 90 degree rotation might map matrix=(64, 32) -> visible=(32, 64) + // Some multiplexing matrices will double the height and half the width. + // + // While not technically necessary, one would expect that the number of + // pixels stay the same, so + // matrix_width * matrix_height == (*visible_width) * (*visible_height); + // + // Returns boolean "true" if the mapping can be successfully done with this + // mapper. + virtual bool GetSizeMapping(int matrix_width, int matrix_height, + int *visible_width, int *visible_height) + const = 0; + + // Map where a visible pixel (x,y) is mapped to the underlying matrix (x,y). + // + // To be convienently stateless, the first parameters are the full + // matrix width and height. + // + // So for many multiplexing methods this means to map a panel to a double + // length and half height panel (32x16 -> 64x8). + // The logic_x, logic_y are output parameters and guaranteed not to be + // nullptr. + virtual void MapVisibleToMatrix(int matrix_width, int matrix_height, + int visible_x, int visible_y, + int *matrix_x, int *matrix_y) const = 0; +}; + +// This is a place to register PixelMappers globally. If you register your +// PixelMapper before calling RGBMatrix::CreateFromFlags(), the named +// PixelMapper is available in the --led-pixel-mapper options. +// +// Note, you don't _have_ to register your mapper, you can always call +// RGBMatrix::ApplyPixelMapper() directly. Registering is for convenience and +// commandline-flag support. +// +// There are a few standard mappers registered by default. +void RegisterPixelMapper(PixelMapper *mapper); + +// Get a list of the names of available pixel mappers. +std::vector GetAvailablePixelMappers(); + +// Given a name (e.g. "rotate") and a parameter (e.g. "90"), return the +// parametrized PixelMapper with that name. Returns NULL if mapper +// can not be found or parameter is invalid. +// Ownership of the returned object is _NOT_ transferred to the caller. +// Current available mappers are "U-mapper" and "Rotate". The "Rotate" +// gets a parameter denoting the angle. +const PixelMapper *FindPixelMapper(const char *name, + int chain, int parallel, + const char *parameter = NULL); +} // namespace rgb_matrix + +#endif // RGBMATRIX_PIXEL_MAPPER diff --git a/pixelnuke b/pixelnuke new file mode 100755 index 0000000..e6102a6 Binary files /dev/null and b/pixelnuke differ diff --git a/pixelnuke.c b/pixelnuke.c new file mode 100644 index 0000000..148a692 --- /dev/null +++ b/pixelnuke.c @@ -0,0 +1,235 @@ +#include "net.h" +#include "canvaspixel.h" +#include "led-matrix-c.h" + +#include +#include +#include //sprintf +#include +#include +#include + +unsigned int px_width = 192; +unsigned int px_height = 128; +unsigned int px_pixelcount = 0; +unsigned int px_clientcount = 0; +struct RGBLedMatrix *matrix; +struct LedCanvas *offscreen_canvas; + + +// User sessions +typedef struct PxSession { + +} PxSession; + +// Helper functions + +static inline int fast_str_startswith(const char* prefix, const char* str) { + char cp, cs; + while ((cp = *prefix++) == (cs = *str++)) { + if (cp == 0) + return 1; + } + return !cp; +} + +// Decimal string to unsigned int. This variant does NOT consume +, - or whitespace. +// If **endptr is not NULL, it will point to the first non-decimal character, which +// may be \0 at the end of the string. +static inline uint32_t fast_strtoul10(const char *str, const char **endptr) { + uint32_t result = 0; + unsigned char c; + for (; (c = *str - '0') <= 9; str++) + result = result * 10 + c; + if (endptr) + *endptr = str; + return result; +} + +// Same as fast_strtoul10, but for hex strings. +static inline uint32_t fast_strtoul16(const char *str, const char **endptr) { + uint32_t result = 0; + unsigned char c; + while ((c = *str - '0') <= 9 // 0-9 + || ((c -= 7) >= 10 && c <= 15) // A-F + || ((c -= 32) >= 10 && c <= 15)) { // a-f + result = result * 16 + c; + str++; + } + if (endptr) + *endptr = str; + return result; +} + +// server callbacks +void px_on_connect(NetClient *client) { + px_clientcount++; +} + +void px_on_close(NetClient *client, int error) { + px_clientcount--; +} + +void px_on_read(NetClient *client, char *line) { + if (fast_str_startswith("PX ", line)) { + const char * ptr = line + 3; + const char * endptr = ptr; + errno = 0; + + uint32_t x = fast_strtoul10(ptr, &endptr); + if (endptr == ptr) { + net_err(client, + "Invalid command (expected decimal as first parameter)"); + return; + } + if (*endptr == '\0') { + net_err(client, "Invalid command (second parameter required)"); + return; + } + + endptr++; // eat space (or whatever non-decimal is found here) + + uint32_t y = fast_strtoul10((ptr = endptr), &endptr); + if (endptr == ptr) { + net_err(client, + "Invalid command (expected decimal as second parameter)"); + return; + } + + // PX -> Get RGB color at position (x,y) or '0x000000' for out-of-range queries + if (*endptr == '\0') { + uint32_t c; + //canvaspixel_get_px(x, y, &c); + char str[64]; + sprintf(str, "PX %u %u %06X", x, y, (c >> 8)); + net_send(client, str); + return; + } + + endptr++; // eat space (or whatever non-decimal is found here) + + // PX BB|RRGGBB|RRGGBBAA + uint32_t c = fast_strtoul16((ptr = endptr), &endptr); + if (endptr == ptr) { + net_err(client, + "Third parameter missing or invalid (should be hex color)"); + return; + } + + if (endptr - ptr == 6) { + // RGB -> RGBA (most common) + c = (c << 8) + 0xff; + } else if (endptr - ptr == 8) { + // done + } else if (endptr - ptr == 2) { + // WW -> RGBA + c = (c << 24) + (c << 16) + (c << 8) + 0xff; + } else { + net_err(client, + "Color hex code must be 2, 6 or 8 characters long (WW, RGB or RGBA)"); + return; + } + + px_pixelcount++; + //canvaspixel_set_px(x, y, c); + led_canvas_set_pixel(offscreen_canvas, x, y, c >>24&255 , c>>16&255, c>>8&255); + //offscreen_canvas = led_matrix_swap_on_vsync(matrix, offscreen_canvas); + + } else if (fast_str_startswith("SIZE", line)) { + + char str[64]; + snprintf(str, 64, "SIZE %d %d", px_width, px_height); + net_send(client, str); + + } else if (fast_str_startswith("STATS", line)) { + + char str[128]; + snprintf(str, 128, "STATS px:%u conn:%u", px_pixelcount, + px_clientcount); + net_send(client, str); + + } else if (fast_str_startswith("HELP", line)) { + + net_send(client, + "\ +PX x y: Get color at position (x,y)\n\ +PX x y rrggbb(aa): Draw a pixel (with optional alpha channel)\n\ +SIZE: Get canvaspixel size\n\ +STATS: Return statistics"); + + } else { + + net_err(client, "Unknown command"); + + } +} + +void px_on_key(int key, int scancode, int mods) { + + printf("Key pressed: key:%d scancode:%d mods:%d\n", key, scancode, mods); + + if (key == 301) { // F12 + canvaspixel_fullscreen(canvaspixel_get_display() + 1); + } else if (key == 67) { // c + canvaspixel_fill(0x00000088); + } else if (key == 81 || key == 256) { // q or ESC + canvaspixel_close(); + } +} + +//void px_on_resize() { +// canvaspixel_get_size(&px_width, &px_height); +//} + +void px_on_window_close() { + printf("Window closed\n"); + net_stop(); + //led_matrix_delete(matrix); +} + +void *myThreadFun(/**struct RGBLedMatrix *matrix, struct LedCanvas *offscreen_canvas**/) { + while(true){ + offscreen_canvas = led_matrix_swap_on_vsync(matrix, offscreen_canvas); + } +} + +int main(int argc, char **argv) { + canvaspixel_setcb_key(&px_on_key); + + struct RGBLedMatrixOptions options; + struct RGBLedRuntimeOptions rt_options; + + //int width, height; + //int x, y, i; + + memset(&options, 0, sizeof(options)); + options.rows = 32; + options.cols = 64; + options.chain_length = 6; + options.parallel = 2; + options.hardware_mapping = "regular"; + options.pixel_mapper_config = "U-mapper"; + memset(&rt_options, 0, sizeof(rt_options)); + rt_options.gpio_slowdown = 4; + options.chain_length = 6; + + /* This supports all the led commandline options. Try --led-help */ + matrix = led_matrix_create_from_options_and_rt_options(&options, &rt_options); + if (matrix == NULL) + return 1; + + /* Let's do an example with double-buffering. We create one extra + * buffer onto which we draw, which is then swapped on each refresh. + * This is typically a good aproach for animations and such. + */ + offscreen_canvas = led_matrix_create_offscreen_canvas(matrix); +// canvaspixel_setcb_resize(&px_on_resize); + + //canvaspixel_start(1024, &px_on_window_close); + pthread_t thread_id; + pthread_create(&thread_id, NULL, myThreadFun, NULL); + + net_start(1337, &px_on_connect, &px_on_read, &px_on_close); + return 0; +} + diff --git a/pixelnuke.o b/pixelnuke.o new file mode 100644 index 0000000..0037dd4 Binary files /dev/null and b/pixelnuke.o differ diff --git a/thread.h b/thread.h new file mode 100644 index 0000000..1f16795 --- /dev/null +++ b/thread.h @@ -0,0 +1,86 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2013 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +#ifndef RPI_THREAD_H +#define RPI_THREAD_H + +#include +#include + +namespace rgb_matrix { +// Simple thread abstraction. +class Thread { +public: + Thread(); + + // The destructor waits for Run() to return so make sure it does. + virtual ~Thread(); + + // Wait for the Run() method to return. + void WaitStopped(); + + // Start thread. If realtime_priority is > 0, then this will be a + // thread with SCHED_FIFO and the given priority. + // If cpu_affinity is set !=, chooses the given bitmask of CPUs + // this thread should have an affinity to. + // On a Raspberry Pi 1, this doesn't matter, as there is only one core, + // Raspberry Pi 2 can has 4 cores, so any combination of (1<<0) .. (1<<3) is + // valid. + virtual void Start(int realtime_priority = 0, uint32_t cpu_affinity_mask = 0); + + // Override this to do the work. + // + // This will be called in a thread once Start() has been called. You typically + // will have an endless loop doing stuff. + // + // It is a good idea to provide a way to communicate to the thread that + // it should stop (see ThreadedCanvasManipulator for an example) + virtual void Run() = 0; + +private: + static void *PthreadCallRun(void *tobject); + bool started_; + pthread_t thread_; +}; + +// Non-recursive Mutex. +class Mutex { +public: + Mutex() { pthread_mutex_init(&mutex_, NULL); } + ~Mutex() { pthread_mutex_destroy(&mutex_); } + void Lock() { pthread_mutex_lock(&mutex_); } + void Unlock() { pthread_mutex_unlock(&mutex_); } + + // Wait on condition. If "timeout_ms" is < 0, it waits forever, otherwise + // until timeout is reached. + // Returns 'true' if condition is met, 'false', if wait timed out. + bool WaitOn(pthread_cond_t *cond, long timeout_ms = -1); + +private: + pthread_mutex_t mutex_; +}; + +// Useful RAII wrapper around mutex. +class MutexLock { +public: + MutexLock(Mutex *m) : mutex_(m) { mutex_->Lock(); } + ~MutexLock() { mutex_->Unlock(); } +private: + Mutex *const mutex_; +}; + +} // end namespace rgb_matrix + +#endif // RPI_THREAD_H diff --git a/threaded-canvas-manipulator.h b/threaded-canvas-manipulator.h new file mode 100644 index 0000000..a7ca678 --- /dev/null +++ b/threaded-canvas-manipulator.h @@ -0,0 +1,103 @@ +// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// Copyright (C) 2014 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +// Utility base class for continuously updating the canvas. + +// Note: considering removing this, as real applications likely have something +// similar, but this might not be quite usable. +// Since it is just a few lines of code, it is probably better +// implemented in the application for readability. +// +// So for simplicity of the API, consider ThreadedCanvasManipulator deprecated. + +#ifndef RPI_THREADED_CANVAS_MANIPULATOR_H +#define RPI_THREADED_CANVAS_MANIPULATOR_H + +#include "thread.h" +#include "canvas.h" + +namespace rgb_matrix { +// +// Typically, your programs will crate a canvas and then updating the image +// in a loop. If you want to do stuff in parallel, then this utility class +// helps you doing that. Also a demo for how to use the Thread class. +// +// Extend it, then just implement Run(). Example: +/* + class MyCrazyDemo : public ThreadedCanvasManipulator { + public: + MyCrazyDemo(Canvas *canvas) : ThreadedCanvasManipulator(canvas) {} + virtual void Run() { + unsigned char c; + while (running()) { + // Calculate the next frame. + c++; + for (int x = 0; x < canvas()->width(); ++x) { + for (int y = 0; y < canvas()->height(); ++y) { + canvas()->SetPixel(x, y, c, c, c); + } + } + usleep(15 * 1000); + } + } + }; + + // Later, in your main method. + RGBMatrix *matrix = RGBMatrix::CreateFromOptions(...); + MyCrazyDemo *demo = new MyCrazyDemo(matrix); + demo->Start(); // Start doing things. + // This now runs in the background, you can do other things here, + // e.g. aquiring new data or simply wait. But for waiting, you wouldn't + // need a thread in the first place. + demo->Stop(); + delete demo; +*/ +class ThreadedCanvasManipulator : public Thread { +public: + ThreadedCanvasManipulator(Canvas *m) : running_(false), canvas_(m) {} + virtual ~ThreadedCanvasManipulator() { Stop(); } + + virtual void Start(int realtime_priority=0, uint32_t affinity_mask=0) { + { + MutexLock l(&mutex_); + running_ = true; + } + Thread::Start(realtime_priority, affinity_mask); + } + + // Stop the thread at the next possible time Run() checks the running_ flag. + void Stop() { + MutexLock l(&mutex_); + running_ = false; + } + + // Implement this and run while running() returns true. + virtual void Run() = 0; + +protected: + inline Canvas *canvas() { return canvas_; } + inline bool running() { + MutexLock l(&mutex_); + return running_; + } + +private: + Mutex mutex_; + bool running_; + Canvas *const canvas_; +}; +} // namespace rgb_matrix + +#endif // RPI_THREADED_CANVAS_MANIPULATOR_H