As we saw in a previous tutorial, the ATOM Matrix's display is nothing but an array of WS2812B LEDs, commonly called NeoPixels. As such, we can use various libraries developed by Adafruit.
First, install the following libraries using the Arduino IDE's Library Manager:
- Adafruit GFX Library - foundation of the other graphics libraries
- Adafruit NeoPixel - for controlling NeoPixel strips
- Adafruit NeoMatrix - for controlling NeoPixel grids
- FastLED - includes various effect and noise functions for making interesting animations
These libraries all come with examples, especially the FastLED library. Before examining those examples, here is an example of displaying strings.
Scrolling Text Messages
To display text messages, we must first have a font! The Adafruit GFX library includes many fonts (found in the Adafruit_GFX/Fonts folder, which on the Mac is at ~/Documents/Arduino/libraries/Adafruit_GFX/Fonts), unfortunately they are all of size 8 x 8. A 5 x 5 font can be found in Lucasmaximus89's Github page at https://github.com/lukasmaximus89/M5Atom-Resources. The specific URL for the font is https://github.com/lukasmaximus89/M5Atom-Resources/blob/master/glcdfont.c. This font has character definitions for uppercase letters and digits, plus other characters - but those other characters are mostly unreadable.
Download this font and move it into the Adafruit_GFX/Fonts folder. By default, the GFX library looks for a file called "glcfont.c" for the fonts, so there is no need to specify that file in our code.
Note: when updating the GFX library, the glcfont.c file will be overwritten with the default 8 x 8 font!
Documentation for the three Adafruit libraries can be found at:
The methods in the NeoMatrix library we'll be using are mostly self explanatory, the exception being the constructor, which looks like this:
Adafruit_NeoMatrix(width, height, dataPin, matrixType, neoPixelType)
- width and height
- size of display, so in our case those numbers will be 5 and 5
- dataPin
- the pin that the NeoPixel display uses for data, which is 27 for the ATOM Matrix
- matrixType
- what is the physical position of the pixel at (0, 0), and are the subsequent pixels arranged in rows or columns
- neoPixelType
- the color order (GRB) and data rate
As we'll see, the library contains constants that make specifying the last two arguments easy.
The whole trick is to imagine the display to be a "window" showing part of the the text. The scrolling results by moving the window, not moving the message. Here is the code, with explanation below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | /* * Text-Display-01.ino * * By: Mike Klepper * Date: 26 April 2020 * * Displays text on the M5 ATOM Matrix using the Adafruit GFX library */ #include <Adafruit_GFX.h> #include <Adafruit_NeoMatrix.h> #include <Adafruit_NeoPixel.h> #define DISPLAY_PIN 27 String msg = F("MAKE AMERICA GREAT AGAIN"); int msgLength = msg.length(); int charWidth = 5; int numTrailingSpaces = 3; int maxLeftPosition = (msgLength + numTrailingSpaces) * (charWidth + 1); int scrollDelay = 100; // Horizontal, left to right with bottom closest to USB-C port int directionAndOrientation = NEO_MATRIX_TOP + NEO_MATRIX_LEFT + NEO_MATRIX_ROWS; // Vertical, top to bottom with top closest to reset button // int directionAndOrientation = NEO_MATRIX_TOP + NEO_MATRIX_RIGHT + NEO_MATRIX_COLUMNS; int pixelType = NEO_GRB + NEO_KHZ800; Adafruit_NeoMatrix matrix = Adafruit_NeoMatrix(5, 5, DISPLAY_PIN, directionAndOrientation + NEO_MATRIX_PROGRESSIVE, pixelType); int xPos = matrix.width(); void setup() { matrix.begin(); matrix.setTextWrap(false); matrix.setBrightness(40); matrix.setTextColor(matrix.Color(80, 0, 80)); } void loop() { matrix.fillScreen(0); matrix.setCursor(xPos, 0); matrix.print(msg); if(--xPos < -maxLeftPosition) { xPos = matrix.width(); } matrix.show(); delay(scrollDelay); } |
Comment | |
---|---|
14 | The dataPin, which will be third argument of the constructor (line 30) |
16 | The message that will be displayed |
17 | Number of characters in the message |
18 | How wide each character in our fixed-width font is |
19 | These trailing spaces result in a delay between the end of the message and the start of the next cycle |
20 | The total width of the message (including trailing spaces) in pixels |
25 | (0, 0) is located at the top-left, and the subsequent pixels are arranged in rows |
28 | Uncomment this line (and comment line 25) for a different scroll direction |
30 | The NeoPixels use GRB color order, with data rate of 800 KHz |
32 | The constructor! |
33 | NEO_MATRIX_PROGRESSIVE means that the pixels in the rows all have same order (as opposed to zig-zag) |
36 | How much the "window" is offset |
38 | Start of the setup function; all this is self explanatory, except... |
43 | The matrix.Color method takes values in RGB order, so in this case the text will be purple |
49 | Set the position of the "window" |
50 | Draw the message |
52 | Move the window to the left by one; if the window has moved pass the end of the message... |
54 | Reset the window to the right edge of the display |
Animation Effects
The FastLED library contains numerous examples that were, I believe, originally intended for NeoPixel strips instead of matrices. They still look good, however!
To use the examples, the following three changes must be made to the code:
- Set the data pin to 27
- The number of LEDs must be set to 25
- The brightness should be set to a lower value, like 60, for example.
Here is one of the examples, called "Fire2012WithPalette," with the above three modifications made. Comments from the original are retained.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | #include <FastLED.h> #define LED_PIN 27 #define COLOR_ORDER GRB #define CHIPSET WS2811 #define NUM_LEDS 25 #define BRIGHTNESS 60 #define FRAMES_PER_SECOND 60 bool gReverseDirection = false; CRGB leds[NUM_LEDS]; // Fire2012 with programmable Color Palette // // This code is the same fire simulation as the original "Fire2012", // but each heat cell's temperature is translated to color through a FastLED // programmable color palette, instead of through the "HeatColor(...)" function. // // Four different static color palettes are provided here, plus one dynamic one. // // The three static ones are: // 1. the FastLED built-in HeatColors_p -- this is the default, and it looks // pretty much exactly like the original Fire2012. // // To use any of the other palettes below, just "uncomment" the corresponding code. // // 2. a gradient from black to red to yellow to white, which is // visually similar to the HeatColors_p, and helps to illustrate // what the 'heat colors' palette is actually doing, // 3. a similar gradient, but in blue colors rather than red ones, // i.e. from black to blue to aqua to white, which results in // an "icy blue" fire effect, // 4. a simplified three-step gradient, from black to red to white, just to show // that these gradients need not have four components; two or // three are possible, too, even if they don't look quite as nice for fire. // // The dynamic palette shows how you can change the basic 'hue' of the // color palette every time through the loop, producing "rainbow fire". CRGBPalette16 gPal; void setup() { delay(3000); // sanity delay FastLED.addLeds<CHIPSET, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection( TypicalLEDStrip ); FastLED.setBrightness( BRIGHTNESS ); // This first palette is the basic 'black body radiation' colors, // which run from black to red to bright yellow to white. gPal = HeatColors_p; // These are other ways to set up the color palette for the 'fire'. // First, a gradient from black to red to yellow to white -- similar to HeatColors_p // gPal = CRGBPalette16( CRGB::Black, CRGB::Red, CRGB::Yellow, CRGB::White); // Second, this palette is like the heat colors, but blue/aqua instead of red/yellow // gPal = CRGBPalette16( CRGB::Black, CRGB::Blue, CRGB::Aqua, CRGB::White); // Third, here's a simpler, three-step gradient, from black to red to white // gPal = CRGBPalette16( CRGB::Black, CRGB::Red, CRGB::White); } void loop() { // Add entropy to random number generator; we use a lot of it. random16_add_entropy( rand()); // Fourth, the most sophisticated: this one sets up a new palette every // time through the loop, based on a hue that changes every time. // The palette is a gradient from black, to a dark color based on the hue, // to a light color based on the hue, to white. // // static uint8_t hue = 0; // hue++; // CRGB darkcolor = CHSV(hue,255,192); // pure hue, three-quarters brightness // CRGB lightcolor = CHSV(hue,128,255); // half 'whitened', full brightness // gPal = CRGBPalette16( CRGB::Black, darkcolor, lightcolor, CRGB::White); Fire2012WithPalette(); // run simulation frame, using palette colors FastLED.show(); // display this frame FastLED.delay(1000 / FRAMES_PER_SECOND); } // Fire2012 by Mark Kriegsman, July 2012 // as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY //// // This basic one-dimensional 'fire' simulation works roughly as follows: // There's a underlying array of 'heat' cells, that model the temperature // at each point along the line. Every cycle through the simulation, // four steps are performed: // 1) All cells cool down a little bit, losing heat to the air // 2) The heat from each cell drifts 'up' and diffuses a little // 3) Sometimes randomly new 'sparks' of heat are added at the bottom // 4) The heat from each cell is rendered as a color into the leds array // The heat-to-color mapping uses a black-body radiation approximation. // // Temperature is in arbitrary units from 0 (cold black) to 255 (white hot). // // This simulation scales it self a bit depending on NUM_LEDS; it should look // "OK" on anywhere from 20 to 100 LEDs without too much tweaking. // // I recommend running this simulation at anywhere from 30-100 frames per second, // meaning an interframe delay of about 10-35 milliseconds. // // Looks best on a high-density LED setup (60+ pixels/meter). // // // There are two main parameters you can play with to control the look and // feel of your fire: COOLING (used in step 1 above), and SPARKING (used // in step 3 above). // // COOLING: How much does the air cool as it rises? // Less cooling = taller flames. More cooling = shorter flames. // Default 55, suggested range 20-100 #define COOLING 55 // SPARKING: What chance (out of 255) is there that a new spark will be lit? // Higher chance = more roaring fire. Lower chance = more flickery fire. // Default 120, suggested range 50-200. #define SPARKING 120 void Fire2012WithPalette() { // Array of temperature readings at each simulation cell static byte heat[NUM_LEDS]; // Step 1. Cool down every cell a little for( int i = 0; i < NUM_LEDS; i++) { heat[i] = qsub8( heat[i], random8(0, ((COOLING * 10) / NUM_LEDS) + 2)); } // Step 2. Heat from each cell drifts 'up' and diffuses a little for( int k= NUM_LEDS - 1; k >= 2; k--) { heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2] ) / 3; } // Step 3. Randomly ignite new 'sparks' of heat near the bottom if( random8() < SPARKING ) { int y = random8(7); heat[y] = qadd8( heat[y], random8(160,255) ); } // Step 4. Map from heat cells to LED colors for( int j = 0; j < NUM_LEDS; j++) { // Scale the heat value from 0-255 down to 0-240 // for best results with color palettes. byte colorindex = scale8( heat[j], 240); CRGB color = ColorFromPalette( gPal, colorindex); int pixelnumber; if( gReverseDirection ) { pixelnumber = (NUM_LEDS-1) - j; } else { pixelnumber = j; } leds[pixelnumber] = color; } } |
Application: Controlling a NeoPixel Strip
The same FireLED examples can be used to control an external NeoPixel strip.
In order to connect a strip to the Atom's Grove port, Dupont jumper wires cannot be used - the pins are too close! Instead, use the type with the thinner ends. Connect ground to ground, power to power, and (for example) G32 to the data line on the NeoPixel strip.
The spacing between the pins on the back of the ATOM Matrix is sufficient to use ordinary Dupont jumpers.
Ordinary NeoPixel strips can operate at full brightness (255), but the other two changes (dataPin and number of pixels) must still be made. Here is a FastLED example called "Pride2015," with original comments in place.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | #include "FastLED.h" // Pride2015 // Animated, ever-changing rainbows. // by Mark Kriegsman #if FASTLED_VERSION < 3001000 #error "Requires FastLED 3.1 or later; check github for latest code." #endif #define DATA_PIN 32 //#define CLK_PIN 4 #define LED_TYPE WS2811 #define COLOR_ORDER GRB #define NUM_LEDS 30 #define BRIGHTNESS 255 CRGB leds[NUM_LEDS]; void setup() { delay(3000); // 3 second delay for recovery // tell FastLED about the LED strip configuration FastLED.addLeds<LED_TYPE,DATA_PIN,COLOR_ORDER>(leds, NUM_LEDS) .setCorrection(TypicalLEDStrip) .setDither(BRIGHTNESS < 255); // set master brightness control FastLED.setBrightness(BRIGHTNESS); } void loop() { pride(); FastLED.show(); } // This function draws rainbows with an ever-changing, // widely-varying set of parameters. void pride() { static uint16_t sPseudotime = 0; static uint16_t sLastMillis = 0; static uint16_t sHue16 = 0; uint8_t sat8 = beatsin88( 87, 220, 250); uint8_t brightdepth = beatsin88( 341, 96, 224); uint16_t brightnessthetainc16 = beatsin88( 203, (25 * 256), (40 * 256)); uint8_t msmultiplier = beatsin88(147, 23, 60); uint16_t hue16 = sHue16;//gHue * 256; uint16_t hueinc16 = beatsin88(113, 1, 3000); uint16_t ms = millis(); uint16_t deltams = ms - sLastMillis ; sLastMillis = ms; sPseudotime += deltams * msmultiplier; sHue16 += deltams * beatsin88( 400, 5,9); uint16_t brightnesstheta16 = sPseudotime; for( uint16_t i = 0 ; i < NUM_LEDS; i++) { hue16 += hueinc16; uint8_t hue8 = hue16 / 256; brightnesstheta16 += brightnessthetainc16; uint16_t b16 = sin16( brightnesstheta16 ) + 32768; uint16_t bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536; bri8 += (255 - brightdepth); CRGB newcolor = CHSV( hue8, sat8, bri8); uint16_t pixelnumber = i; pixelnumber = (NUM_LEDS-1) - pixelnumber; nblend( leds[pixelnumber], newcolor, 64); } } |
Click here to go to the table of contents for this series.
Mike. Can't thank you enough fr putting all this stuff on the ATOM together. Super helpful.
ReplyDeleteQuestion: In the "Scrolling Text Messages" example, Line 16 is
String msg = F("MAKE AMERICA GREAT AGAIN");
Looking to clarify the syntax. "String" is the type? Why the capital "S" in String? What does the "F" preceding ("MAKE AMERICA GREAT AGAIN") do?
Thanks again.
Hey Eye,
DeleteThank you for taking time to read over these ATOM tutorials - they were absolute blast to put together!
You're right, String is indeed the type of that variable, just like int or boolean or char.
On the String vs string issue...
Here's some of the differences:
1) The String type (capital S) is declared in a separate library (though it is part of Arduino core), so there's the overhead
2) The string type (lowercase s) is a null-terminated array of characters - hence no separate library
3) The String type was introduced into Arduino for performing string manipulations (concatenating, finding substrings, etc)
4) String (capital S) variables are dynamic variables, meaning that their addresses in the heap can change as the program is running. So as the Strings are used (concatenated, split apart, etc) and the addresses change, this leaves gaps in the heap. This is called "heap fragmentation," and those fragments may be unavailable for later use.
So, which one to use here, String or string? Since I'm not doing any interesting string manipulation, and we should avoid the overhead of the String library, it would be better to use the string (lowercase s) or arrays of characters.
For the F macro...
Most microprocessors store instructions and data in two separate parts of memory (this scheme is called "Harvard architecture") as opposed to storing them in the same place ("Von Neumann architecture"). The advantage of Harvard architecture is the speed - accessing code is faster than accessing data.
The F macro forces the compiler to put the string into instruction memory (flash memory), where ordinarily it would be put into data memory (RAM), thus giving you a little bit better performance while not consuming RAM.
So, should you use the F macro here? The ESP32 has large amounts of RAM, and it runs faster than most microprocessors, so it should be safe to remove it!
Again, thanks for the questions! When I fix the code to address the issues you raised, I'll be sure to give you credit!
- Mike
BTW, if you're on a Mac, be careful when upgrading to macOS Big Sur - it breaks the ESP-SDK, which is an SDK from Espressif Systems, the makers of the ESP32. Hence, no recent posts regarding the ATOM. Once I get that working (or switch to a Windows or Linux computer) there will be more ESP32-related posts!
DeleteAh! Figured it. New to Arduino. String() is their class of handlers. The preceding "F" puts the message into non volatile memory.
ReplyDeleteThat's right!
DeleteHi,
ReplyDeleteThanks for the informations.
But where is the "default" fonts used with the 8x8 matrix ?
I fond nothing into the gfxfont.h ???
Thanks
Hey S1244,
DeleteSome fonts are included with the Adafruit GFX library, in the Fonts folder. You can also use fontconvert to make 8x8 fonts out of TTF files. There is also a web-based converter here:
https://tchapi.github.io/Adafruit-GFX-Font-Customiser/
The ATOM Matrix uses 5x5 fonts, so you really can't "downsample" 8x8.
Let me know how this goes!
Thanks,
Mike K
Hi Mike
ReplyDeleteMaybe I was not clear. If you set:
matrix.setFont(&muMatrix8ptRegular);
You will see your text with this new font.
But now I want to write a new text using the default library, how to do:
matrix.setFont(& ??????);
Thanks!
ReplyDeleteThanks for this neat tutorial, Mike! I am not sure if something changed in the recent Adafruit libraries, but now they default to the "glcdfont.c" font in the \libraries\Adafruit_GFX_Library root folder. So after downloading the "glcfont.c", i had to rename it to "glcdfont.c" and copy it to that folder - otherwise Arduino would ignore it.
ReplyDeleteP.S. I tried the 11.10.11 and 11.10.12 versions of the Adafruit GFX Library
DeleteAnd my 5c contribution: the font in the link only supports letters and numbers, with no special characters. If you want to add a dot "." (i.e. for showing temperature with decimal digits), search and replace "0x00, 0x00, 0x60, 0x60, 0x00" with "0x00, 0x00, 0x10, 0x00, 0x00" in the font file
DeleteHi Mikhail, thanks for looking into the glcfont.c issue, and for reading this blog!
DeleteYea, it would be nice to have some stability with the libraries, but that's not to be. When I get some time to revisit IoT dev, I'll see if there is some way around this.
Again, thanks!
- Mike