Showing posts with label FastLED. Show all posts
Showing posts with label FastLED. Show all posts

Friday, May 22, 2020

ATOM Matrix: Advanced Display Usage

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);
}


Line 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.
The ATOM Matrix product description page recommends a max brightness of 20, but higher numbers seem harmless.

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.

ATOM Matrix: Using the MPU6886 Gyroscope

Reading data from the gyroscope is essentially the same as reading temperature or acceleration:

  • Declare three floats, here called gyroX, gyroY, and gyroZ
  • Pass the addresses into the getGyroData method
The results will be in degrees per second.

This post demonstrates how to read info from the gyroscope and use that info to control the brightness of the 5x5 display. The rate of rotation about the z-axis is read, and this is used to either increase or decrease the screen's brightness. A wasReleased check is performed to reset brightness to zero.

 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
/*
 * GyroscopeTest01.ino
 * 
 * By: Mike Klepper
 * Date: 7 May 2020
 * 
 * Adjust display's brightness by measuring twists along the z-axis
 * 
 * See post on patriot-geek.blogspot.com
 * for details
 */


#include "M5Atom.h"

int GRB_COLOR_WHITE = 0xffffff;
int GRB_COLOR_BLACK = 0x000000;
int GRB_COLOR_RED = 0x00ff00;
int GRB_COLOR_ORANGE = 0xa5ff00;
int GRB_COLOR_YELLOW = 0xffff00;
int GRB_COLOR_GREEN = 0xff0000;
int GRB_COLOR_BLUE = 0x0000ff;
int GRB_COLOR_PURPLE = 0x008080;

int activeColor = GRB_COLOR_PURPLE;

const int TOLERANCE = 2;
const int SCALING_DIVISOR = 20;
const int MAX_BRIGHTNESS = 60;

float gyroX = 0;
float gyroY = 0;
float gyroZ = 0;

bool IMU6886Flag = false;

int currentBrightness = 0;

void setup() 
{
    M5.begin(true, false, true);
    delay(20);
    
    IMU6886Flag = M5.IMU.Init() == 0;
  
    // Initialize display
    M5.dis.clear();
    M5.dis.setBrightness(currentBrightness);
    
    for(int i = 0; i < 25; i++)
    {
        M5.dis.drawpix(i, activeColor);
    }

    if(!IMU6886Flag)
    {
        Serial.println("Error initializing the IMU! :-(");
    }
}

void loop() 
{
    if(IMU6886Flag)
    {
        M5.IMU.getGyroData(&gyroX, &gyroY, &gyroZ);
    
        float trackedQuantity = gyroZ;
    
        if(abs(trackedQuantity) > TOLERANCE)
        {
            currentBrightness += trackedQuantity / SCALING_DIVISOR;
             
            if(currentBrightness > MAX_BRIGHTNESS)
                currentBrightness = MAX_BRIGHTNESS;
      
            if(currentBrightness < 0)
                currentBrightness = 0;

            M5.dis.setBrightness(currentBrightness);
        }
    }
      
    if(M5.Btn.wasReleased())
    {
        Serial.println("wasReleased");
        currentBrightness = 0;
        M5.dis.setBrightness(currentBrightness);
    }
    
    delay(20);
    M5.update();
}

Click here to go to the table of contents for this series.

ATOM Matrix: Temperature Sensor

The temperature sensor included in the MPU6886 is not terribly useful for measuring ambient temperature. It is better used for detecting hardware overheating.

To read the temperature in Celsius, first declare a float (called tempC in the following application), and then pass-in the address of that float into the getTempData method like this:

M5.IMU.getTempData(&tempC)

For the sake of completeness, though, here is a simple application using the sensor.


Change Display Color Based on Temperature

 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
/*
 * TemperatureSensor01.ino
 * 
 * By: Mike Klepper
 * Date: 26 April 2020
 * 
 * Changes display based on internal temperature
 */

#include "M5Atom.h"

int GRB_COLOR_WHITE = 0xffffff;
int GRB_COLOR_BLACK = 0x000000;
int GRB_COLOR_RED = 0x00ff00;
int GRB_COLOR_ORANGE = 0xa5ff00;
int GRB_COLOR_YELLOW = 0xffff00;
int GRB_COLOR_GREEN = 0xff0000;
int GRB_COLOR_BLUE = 0x0000ff;
int GRB_COLOR_PURPLE = 0x008080;

float lowTempF = 103;
float highTempF = 105;

float tempC = 0;
bool IMU6886Flag = false;

void setup() 
{
    M5.begin(true, false, true);
    delay(20);
    
    IMU6886Flag = M5.IMU.Init() == 0;

    if(!IMU6886Flag)
    {
        Serial.println("Error initializing the IMU! :-(");
    }
}

void loop() 
{
    if(IMU6886Flag)
    {
        M5.IMU.getTempData(&tempC);
        Serial.printf(" Temp : %.2f C \r\n", tempC);
        float tempF = tempC * 9 / 5 + 32;
        Serial.printf(" Temp : %.2f F \r\n", tempF);
    
        if(tempF < lowTempF)
        {
            fillDisplay(GRB_COLOR_BLUE);
        }
        else if(tempF > highTempF)
        {
            fillDisplay(GRB_COLOR_RED);
        }
        else
        {
            fillDisplay(GRB_COLOR_GREEN);
        }
        
        delay(500);
        M5.update();
    }
}

void fillDisplay(int fillColor)
{
    for(int i = 0; i < 25; i++)
    {
        M5.dis.drawpix(i, fillColor);
    }
}

The delay(20) found on line 30 is necessary, as the MPU6886 is sometimes not found without it.

Click here to go to the table of contents for this series.

ATOM Matrix: Using the MPU6886 Accelerometer

The ATOM Matrix includes an accelerometer and gyroscope in its MPU6886. The MPU6886 also includes an internal temperature sensor.


Reading and Displaying Values

The MPU6886 must first be initialized using M5.IMU.Init() which will return 0 if the initialization was successful. Reading from the IMU then awalys follows the following steps:

  • Declare floating-point variables
  • Pass the memory location of those variables into the appropriate method
All this is demonstrated in the following application.

 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
/*
 * MPU6886.ino
 * 
 * By: Mike Klepper
 * Date: 26 April 2020
 * 
 * Basically the same as the MPU6886 example sketch but with C -> F conversion
 */

#include "M5Atom.h"

float accX = 0, accY = 0, accZ = 0;
float gyroX = 0, gyroY = 0, gyroZ = 0;
float tempC = 0;
bool IMU6886Flag = false;

void setup()
{
    M5.begin(true, false, true);
    delay(20);
    
    IMU6886Flag = M5.IMU.Init() == 0;
  
    if(!IMU6886Flag)
    {
        Serial.println("Error initializing the IMU! :-(");
    }
}

void loop()
{
    if(IMU6886Flag)
    {
        M5.IMU.getGyroData(&gyroX, &gyroY, &gyroZ);
        M5.IMU.getAccelData(&accX, &accY, &accZ);
        M5.IMU.getTempData(&tempC);
    
        float tempF = 9*tempC/5 + 32;
    
        Serial.printf("Gyroscope: %.2f,%.2f,%.2f o/s \r\n", gyroX, gyroY, gyroZ);
        Serial.printf("Accelerometer: %.2f,%.2f,%.2f mg\r\n", accX * 1000, accY * 1000, accZ * 1000);
        Serial.printf("Temperature: %.2f C \r\n", tempC);
        Serial.printf("Temperature: %.2f F \r\n", tempF);
        Serial.println("");
    }
    
    delay(500);
    M5.update();
}


Showing Device Orientation

The accelerometer data can be used to have the ATOM take different actions based on its orientation. Using the above code, we can perform experiments to see what values accX, accY, and accZ take on as we hold the device in different positions. Those values are included in comments in the code.

  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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
/*
 * AccelerometerTest02.ino
 * 
 * By: Mike Klepper
 * Date: 26 April 2020
 * 
 * Displays arrow indicating device orientation based on info from the MPU6886
 */


#include "M5Atom.h"

int GRB_COLOR_WHITE = 0xffffff;
int GRB_COLOR_BLACK = 0x000000;
int GRB_COLOR_RED = 0x00ff00;
int GRB_COLOR_ORANGE = 0xa5ff00;
int GRB_COLOR_YELLOW = 0xffff00;
int GRB_COLOR_GREEN = 0xff0000;
int GRB_COLOR_BLUE = 0x0000ff;
int GRB_COLOR_PURPLE = 0x008080;

int upArrow[25] = 
{
    0,0,1,0,0,
    0,1,1,1,0,
    1,0,1,0,1,
    0,0,1,0,0,
    0,0,1,0,0
};

int downArrow[25] = 
{
    0,0,1,0,0,
    0,0,1,0,0,
    1,0,1,0,1,
    0,1,1,1,0,
    0,0,1,0,0
};

int leftArrow[25] = 
{
    0,0,1,0,0,
    0,1,0,0,0,
    1,1,1,1,1,
    0,1,0,0,0,
    0,0,1,0,0
};

int rightArrow[25] = 
{
    0,0,1,0,0,
    0,0,0,1,0,
    1,1,1,1,1,
    0,0,0,1,0,
    0,0,1,0,0
};

int roundShape[25] = 
{
    0,1,1,1,0,
    1,0,0,0,1,
    1,0,0,0,1,
    1,0,0,0,1,
    0,1,1,1,0
};

int circleWithX[25] = 
{
    0,1,1,1,0,
    1,0,2,0,1,
    1,2,2,2,1,
    1,0,2,0,1,
    0,1,1,1,0
};


int delayAmt = 1000;

int colorList[] = {GRB_COLOR_BLACK, GRB_COLOR_PURPLE, GRB_COLOR_YELLOW};

float accX = 0;
float accY = 0;
float accZ = 0;

bool IMU6886Flag = false;

/*
 * Screen Up: 
 * |accX| < LOW_TOL -15, |accY| < LOW_TOL, accZ ~ -980
 * 
 * Screen Down: 
 * |accX| < LOW_TOL -7, |accY| < LOW_TOL 3, accZ ~ 1020
 * 
 * Note Up: 
 * |accX| < LOW_TOL -7, accY ~ 1000, |accZ| < LOW_TOL
 * 
 * Note Down: 
 * |accX| < LOW_TOL -7, accY ~ -1000, |accZ| < LOW_TOL
 * 
 * Reset Up: 
 * accX ~ 990, |accY| < LOW_TOL, |accZ| < LOW_TOL
 * 
 * Reset Down: 
 * accX ~ -1000, |accY| < LOW_TOL - 10, |accZ| < LOW_TOL, -20
 */

float LOW_TOL = 100;
float HIGH_TOL = 900;

float scaledAccX = 0;
float scaledAccY = 0;
float scaledAccZ = 0;

void setup() 
{
    M5.begin(true, false, true);
    delay(20);
    
    IMU6886Flag = M5.IMU.Init() == 0;
  
    if(!IMU6886Flag)
    {
        Serial.println("Error initializing the IMU! :-(");
    }
}

void loop() 
{
    if(IMU6886Flag)
    {
        M5.IMU.getAccelData(&accX, &accY, &accZ);

        Serial.printf("Accel: %.2f, %.2f, %.2f mg\r\n", accX * 1000, accY * 1000, accZ * 1000);
        
        scaledAccX = accX * 1000;
        scaledAccY = accY * 1000;
        scaledAccZ = accZ * 1000;

        if(abs(scaledAccX) < LOW_TOL && abs(scaledAccY) < LOW_TOL && abs(scaledAccZ) > HIGH_TOL && scaledAccZ > 0)
        {
            drawArray(roundShape, colorList);
        }

        else if(abs(scaledAccX) < LOW_TOL && abs(scaledAccY) < LOW_TOL && abs(scaledAccZ) > HIGH_TOL && scaledAccZ < 0)
        {
            drawArray(circleWithX, colorList);
        }
        
        else if(abs(scaledAccX) < LOW_TOL && abs(scaledAccY) > HIGH_TOL && abs(scaledAccZ) < LOW_TOL && scaledAccY > 0)
        {
            drawArray(upArrow, colorList);
        }

        else if(abs(scaledAccX) < LOW_TOL && abs(scaledAccY) > HIGH_TOL && abs(scaledAccZ) < LOW_TOL && scaledAccY < 0)
        {
            drawArray(downArrow, colorList);
        }

        else if(abs(scaledAccX) > HIGH_TOL && abs(scaledAccY) < LOW_TOL && abs(scaledAccZ) < LOW_TOL && scaledAccX > 0)
        {
            drawArray(leftArrow, colorList);
        }

        else if(abs(scaledAccX) > HIGH_TOL && abs(scaledAccY) < LOW_TOL && abs(scaledAccZ) < LOW_TOL && scaledAccX < 0)
        {
            drawArray(rightArrow, colorList);
        }
        else
        {
            M5.dis.clear();
        }
    }

    delay(250);
    M5.update();
}


void drawArray(int arr[], int colors[])
{
    for(int i = 0; i < 25; i++)
    {
        M5.dis.drawpix(i, colors[arr[i]]);
    }
}


Detect a Shake and Roll a Die

The final project revisits the 6-sided die roller from a previous blog post. Again, to determine what constitutes a shake, experiment with the first the application in this post.

  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
/*
 * AccelerometerTest03.ino
 * 
 * By: Mike Klepper
 * Date: 26 April 2020
 * 
 * Display a random integer between 1 and 6 inclusive when the 
 * M5 ATOM Matrix is shook
 */

#include "M5Atom.h"

int GRB_COLOR_WHITE = 0xffffff;
int GRB_COLOR_BLACK = 0x000000;
int GRB_COLOR_RED = 0x00ff00;
int GRB_COLOR_ORANGE = 0xa5ff00;
int GRB_COLOR_YELLOW = 0xffff00;
int GRB_COLOR_GREEN = 0xff0000;
int GRB_COLOR_BLUE = 0x0000ff;
int GRB_COLOR_PURPLE = 0x008080;

int activeColor = GRB_COLOR_RED;

int colorList[] = {GRB_COLOR_BLACK, activeColor};

int one[25] = 
{
    0,0,1,0,0,
    0,1,1,0,0,
    0,0,1,0,0,
    0,0,1,0,0,
    0,1,1,1,0
};

int two[25] = 
{
    0,1,1,1,0,
    0,0,0,0,1,
    0,0,1,1,0,
    0,1,0,0,0,
    0,1,1,1,1
};

int three[25] = 
{
    0,1,1,1,0,
    0,0,0,0,1,
    0,0,1,1,0,
    0,0,0,0,1,
    0,1,1,1,0
};

int four[25] = 
{
    0,0,0,1,0,
    0,1,0,1,0,
    0,1,1,1,1,
    0,0,0,1,0,
    0,0,0,1,0
};

int five[25] = 
{
    0,1,1,1,1,
    0,1,0,0,0,
    0,1,1,1,0,
    0,0,0,0,1,
    0,1,1,1,0
};

int six[25] = 
{
    0,0,1,1,0,
    0,1,0,0,0,
    0,1,1,1,0,
    0,1,0,0,1,
    0,0,1,1,0
};

int *displayNumbers[6] = { one, two, three, four, five, six };


float accX = 0, accY = 0, accZ = 0;

float accTolerance = 3;

bool IMU6886Flag = false;

void setup() 
{
    M5.begin(true, false, true);
    delay(20);
    
    IMU6886Flag = M5.IMU.Init() == 0;
  
    if(!IMU6886Flag)
    {
        Serial.println("Error initializing the IMU! :-(");
    }

    randomSeed(analogRead(0));
    
    showRandomNumber();
}

void loop() 
{
    if(IMU6886Flag)
    {
        while(1) 
        {
            M5.IMU.getAccelData(&accX, &accY, &accZ);
            
            if(abs(accX) > accTolerance || abs(accY) > accTolerance) 
            {
                break;
            }
        }
    
        Serial.println("Shake Detected!");
        M5.dis.clear();
        showRandomNumber();
        Serial.printf("Accel: %.2f, %.2f, %.2f \r\n", accX, accY, accZ);
    }

    delay(250);
    M5.update();
}

void showRandomNumber()
{
    int numberToShow = random(0, 6);
  
    Serial.println(numberToShow + 1);
  
    drawArray(displayNumbers[numberToShow], colorList);
}

void drawArray(int arr[], int colors[])
{
    for(int i = 0; i < 25; i++)
    {
        M5.dis.drawpix(i, colors[arr[i]]);
    }
}

Click here to go to the table of contents for this series.

ATOM Matrix: Introduction

M5Stack manufacturers a series of development boards, and the next several blog posts are devoted to one of their products, the M5 ATOM Matrix. The first part of this series is concerned with hardware aspects other than wifi or bluetooth connectivity; those aspects will be examined later in the series.

While these tutorials are framed in terms of this one product, the code can be adapted to other ESP-32 boards.


ATOM Matrix and ATOM Lite

There is a related product, called the M5 ATOM Lite, that does not have the ATOM Matrix's 5 x 5 RGB LED display. Instead, it has only one RGB pixel. In other aspects it is the same as the ATOM Matrix. One major difference between these products and the majority of M5Stack's products is that the Matrix and Lite do not include an internal battery. The ATOM Lite, the ATOM Matrix, a M5Stick-C, and one of the ordinary M5Stack units are shown here, left to right.


ATOM Matrix Hardware Overview:

  • ESP32-PICO-D4
  • 4MB integrated SPI flash memory
  • 5 x 5 RGB LED matrix
  • One user button
  • One reset button
  • IMU sensor (MPU6886)
  • Temperature sensor (part of MPU6886)
  • Infrared LED
  • Grove PH2.0 interface
  • GPIO pins on the back - but only 6 (see following photo)
  • USB-C port


Driver and Arduino IDE Setup

The ATOM Matrix uses Silicon Lab's CP2201x driver to make the board appear as a USB port. The driver can be downloaded from this URL:
https://www.silabs.com/products/development-tools/software/usb-to-uart-bridge-vcp-drivers

NOTE: it is essential to un-zip the download from Silicon Labs first! Mac OSX allows you to inspect zip files, but attempting to install the driver directly from the zip file will silently fail!

To install ESP-32 support in the Arduino IDE, do the following:

  • Open the Preferences dialog (which is under the Arduino menu on the Mac)
  • Click the button to the right of the "Additional Boards Manager URLs" label
  • Enter the following URL to the list: https://dl.espressif.com/dl/package_esp32_index.json
  • Then close the dialogs.
  • Under the Tools menu, choose the Board option, then the Boards Manager
  • Search for "esp32", and install the package from Espressif Systems (the manufacturer of the ESP-32)
  • Close that dialog

After this is done, various M5 boards should be listed in the Boards menu. We want to use the one called "M5Stick-C".

NOTE: If, after connecting the board to your computer using a USB cable, the board doesn't show under the Arduino Boards menu, here are two things to check:

  1. Is the USB cable power-only, or can it carry data as well?
  2. Is the driver properly installed?

Driver problems are difficult to diagnose, and the only advice I can offer is to visit the M5Stack Forums found at https://forum.m5stack.com/

There are other ways of developing software for the ESP-32 MicroPython, CircuitPython, and UIFlow. The latter is mostly specific to M5Stack boards.


Table of Contents: (will be updated as more posts go online)