M5Stack 8ANGLE Potentiometer and LED Module - Arduino C++
The 8ANGLE module has 8 x potentiometers, 9 x RGB LEDS, and a slider toggle switch, with an I2C connection. It has been very useful for manually fine-tuning the parameters for PID loops, and many other things. The multi-coloured LEDs look nice too.
Pretty pictures
This is what I originally used for manual PID tuning, needing three analogue inputs. The 8ANGLE is more fun.
The 8ANGLE module uses I2C communications, with default I2C address 0x43, but a different address can be programmed if necessary.
The connector is a standard 4-pin "Grove" connector. Connect to the same pins on the Arduino board.
1
SCL
I2C clock
Open collector, may need pull-ups to 3.3V or 5V
2
SDA
I2C data
Open collector, may need pull-ups to 3.3V or 5V
3
VCC
5V supply
Max. 50mA with all LEDs white at full brightness
4
GND
Ground
It uses a 5V supply (max. 50mA), but has an onboard 3.3V regulator. Its internal STM32 microcontroller is powered by 3.3V, and has 5V-tolerant inputs, so they can be used with 3.3V or 5V boards without problems - no level shifters are needed.
You may need pull-ups to 3.3V or 5V (depending on your board's VCC) on the open-collector SCL and SDA lines, if your board does not already have them. The value depends on the speed and how many I2C devices are connected, 4K7 is usually ok. But it seems to work well without them, maybe the internal pull-ups are being used.
The module has 256 x 8-bit 'registers', but not all of them are used. The potentiometer values are 16 bits, so you must read 2 bytes. The LED values are 32 bits, so you must read/write 4 bytes. It fails if you don't read 2 or 4 bytes.
The module cannot be accessed too fast, else it misses messages, especially on the fast boards like the Zero. My first version used Wire.write(buffer, length) to write multiple bytes to the 8ANGLE. This sometimes caused it to hang. But if each byte is written separately in a loop, then it works.
All methods return true on success, false on failure, but in practice you only need to check the return value of begin() to determine if the 8ANGLE module is connected.
Code
This code has been tested on the Arduino Uno R3 and the Arduino Zero, but should work on them all.
// Class for M5Stack's "8ANGLE" module
// Copyright (C) muman.ch, 2022.02.22
// info@muman.ch
// All rights reversed
/*
Manufacturer documentation
https://docs.m5stack.com/en/unit/8Angle
The 8ANGLE module has 8 x potentiometers, 9 x RGB LEDS, and a slider
toggle switch. It uses I2C communications, with default I2C address 0x43,
but a different address can be programmed if necessary.
The connector is a standard 4-pin "Grove" connector. Connect to the same
pins on the Arduino board.
1 SCL I2C clock } open collector, may need pullups to 3.3V or 5V
2 SDA I2C data }
3 VCC 5V Supply max. 50mA with all LEDs white at full brightness
4 GND
The 8ANGLE module uses a 5V supply (max. 50mA), but contains an onboard
3.3V regulator. Its STM32 microcontroller is powered by 3.3V, and has
5V-tolerant inputs, so they can be used with 3.3V or 5V boards without
problems - no level shifters are needed.
You may need pullups to 3.3V or 5V (depending on your board's VCC) on
the open-collector SCL and SDA lines, if your board does not already
have them. The value depends on the speed and how many I2C devices
are connected, 4K7 is usually ok. But it seems to work well without
them, maybe the internal pullups are being used.
The module has 256 x 8-bit 'registers'. The potentiometer values are
16 bits, so you must read 2 bytes. The LED values are 32 bits, so you
must read/write 4 bytes. It fails if you don't read 2 or 4 bytes.
All methods return true on success, false on failure, but in practice
you only need to check the return value of begin() to determine if the
8ANGLE module is connected.
COMPATIBILITY
This code has been tested on the Arduino Uno R3 and the Arduino Zero,
but should work on them all.
Note: On 8 and 16-bit microcontrollers, an 'int' is 16 bits and 'long'
is 32 bits, so we need 'long' for the RGB values. On 32-bit devices
'long' and 'int' are both 32 bits. It's most efficient to use the
native 'int' instead of 'uint8_t' or 'int16_t'.
*/
#include "M5Stack8Angle.h"
bool M5Stack8Angle::begin(int i2cAddress)
{
i2cAdds = i2cAddress;
Wire.begin();
delay(100);
return isConnected();
}
bool M5Stack8Angle::isConnected()
{
Wire.beginTransmission(i2cAdds);
return Wire.endTransmission() == 0;
}
// Use the standard LED RGB colours defined in M5Stack8Angle.h
// 32-bit rgbColour = 0x00rrggbb
bool M5Stack8Angle::setLed(int channel, long rgbColour, int brightness)
{
if ((unsigned int)channel > 8)
return false;
byte data[4];
data[0] = rgbColour >> 16; // red
data[1] = rgbColour >> 8; // green
data[2] = rgbColour; // blue
data[3] = brightness; // 0..100
return writeBytes(0x30 + channel * 4, data, 4);
}
bool M5Stack8Angle::setAllLeds(long rgbColour, int brightness)
{
for (int channel = 0; channel < 8; ++channel) {
if (!setLed(channel, rgbColour, brightness))
return false;
}
return true;
}
// Read potentiometer as 12-bit value, 0..4095
bool M5Stack8Angle::getAnalogInput12(int channel, int* value)
{
if ((unsigned int)channel > 7)
return false;
byte data[2];
if (!readBytes(channel * 2, data, 2))
return false;
*value = (data[1] << 8) | data[0];
return true;
}
// Read potentiometer as 8-bit value, 0..255
bool M5Stack8Angle::getAnalogInput8(int channel, int* value)
{
if ((unsigned int)channel > 7)
return false;
byte b;
if (!readBytes(0x10 + channel, &b, 1))
return false;
*value = b;
return true;
}
// Slider switch, 0=off, 1=on
bool M5Stack8Angle::getSwitchPosition(int* position)
{
byte b;
if (!readBytes(0x20, &b, 1))
return false;
*position = b & 1;
return true;
}
// Gets a potentiometer value as rotary switch position
// with a given number of steps
// returns position 0 .. steps - 1
bool M5Stack8Angle::getRotaryPosition(int channel, int steps, int* position)
{
if (steps > 4096)
steps = 4096;
int value;
if (!getAnalogInput12(channel, &value))
return false;
*position = (value * steps) >> 12;
return true;
}
bool M5Stack8Angle::getFwVersion(int* version)
{
byte b;
if (!readBytes(0xfe, &b, 1)) {
*version = -1;
return false;
}
*version = b;
return true;
}
// Internal methods
// Take care, make a note of the addresss!
bool M5Stack8Angle::setDeviceI2cAddress(int i2cAddress)
{
byte b = i2cAddress;
if (!writeBytes(0xff, &b, 1))
return false;
i2cAdds = i2cAddress;
return true;
}
bool M5Stack8Angle::readBytes(int reg, byte* buffer, int length)
{
Wire.beginTransmission(i2cAdds);
Wire.write(reg);
Wire.endTransmission();
if (Wire.requestFrom(i2cAdds, length) != length)
return false;
for (int i = 0; i < length; ++i)
buffer[i] = Wire.read();
return true;
}
bool M5Stack8Angle::writeBytes(int reg, byte* buffer, int length)
{
Wire.beginTransmission(i2cAdds);
Wire.write(reg);
//note: do not do this, it sends data too fast and can hang the 8Angle
//Wire.write(buffer, length);
//instead, write one byte at a time
for (int i = 0; i < length; ++i)
Wire.write(buffer[i]);
return Wire.endTransmission() == 0;
}
// Demo sketch for the M5Stack 8ANGLE library
// info@muman.ch, 2024.02.22
#include "M5Stack8Angle.h"
M5Stack8Angle angle8;
const long rgbColors[8] =
{
angle8.Off, angle8.Red, angle8.Green, angle8.Blue, angle8.Cyan,
angle8.Magenta, angle8.Yellow, angle8.White
};
void setup()
{
// use default I2C address 0x43
if (!angle8.begin()) {
// module not connected
}
int version;
angle8.getFwVersion(&version);
}
void loop()
{
// brightness is controlled by the slider switch, 1=bright, 0=dim
int swpos;
angle8.getSwitchPosition(&swpos);
int brightness = swpos ? 100 : 20;
// sequence the led above the slider to show it's running
static int swledColorIndex = 1;
angle8.setLed(8, rgbColors[swledColorIndex], brightness);
if (++swledColorIndex > 7)
swledColorIndex = 1;
// set the potentiometer's led color according to the potentiometer position
for (int i = 0; i < 8; ++i) {
int value;
angle8.getAnalogInput12(i, &value);
int colorIndex = (value * 8) >> 12;
angle8.setLed(i, rgbColors[colorIndex], brightness);
}
delay(300);
}
Note: On 8 and 16-bit microcontrollers, an 'int' is 16 bits and 'long' is 32 bits, so we need 'long' for the 24-bit RGB values. On 32-bit devices 'long' and 'int' are both 32 bits. It's often more efficient to use the native 'int' instead of 'uint8_t' or 'int16_t', because the compiler usually converts them to 'int' for arithmetic operations.
M5Stack 8ENCODER Rotary Encoder Module
M5Stack also has a similar module which has rotary encoders instead of potentiometers. Each encoder has 30 steps, with half-step in between = 60 counts per revolution.