Merge pull request #1 from m5stack/dev

init
This commit is contained in:
IAMLIUBO
2026-04-23 10:09:08 +08:00
committed by GitHub
59 changed files with 6241 additions and 10 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ body:
label: Issue checklist
description: Please double-check that you have done each of the following things before submitting the issue.
options:
- label: I searched for previous reports in [the issue tracker](https://github.com/m5stack/M5Stack/issues?q=)
- label: I searched for previous reports in [the issue tracker](https://github.com/m5stack/StackChan-BSP/issues?q=)
required: true
- label: My report contains all necessary details
required: true
+6 -6
View File
@@ -39,12 +39,12 @@ jobs:
strategy:
matrix:
path:
- check: './' # path to include
exclude: '' # path to exclude
#- check: 'src'
# exclude: '(Fonts)' # Exclude file paths containing "Fonts"
#- check: 'examples'
# exclude: ''
# - check: './' # path to include
# exclude: '' # path to exclude
- check: 'src'
exclude: 'src/(utils|drivers)'
- check: 'examples'
exclude: ''
steps:
- name: Checkout
+5 -3
View File
@@ -1,8 +1,8 @@
# Product Name
# StackChan
## Overview
### SKU:xxx
### SKU:K151
Description of the product
@@ -12,7 +12,9 @@ Description of the product
## Required Libraries:
- [Adafruit_BMP280_Library](https://github.com/adafruit/Required_Libraries_Link)
- [M5Unified](https://github.com/m5stack/M5Unified)
- [IRremoteESP8266](https://github.com/crankyoldgit/irremoteesp8266)
- [M5Unit-NFC](https://github.com/m5stack/M5Unit-NFC)
## License
+29
View File
@@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include <Arduino.h>
#include <M5StackChan.h>
void setup()
{
/* Init StackChan */
M5StackChan.begin();
/* Setup display */
M5StackChan.Display().setTextSize(2);
M5StackChan.Display().setTextColor(TFT_GREENYELLOW);
M5StackChan.Display().setTextScroll(true);
}
void loop()
{
/* Get battery info from INA226 */
float voltage = M5StackChan.getBatteryVoltage();
float current = M5StackChan.getBatteryCurrent() * 1000;
M5StackChan.Display().printf("> Bat: %0.2fV %0.2fmA\n", voltage, current);
delay(1000);
}
+77
View File
@@ -0,0 +1,77 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
// More examples on: https://github.com/crankyoldgit/IRremoteESP8266/tree/master/examples
#include <Arduino.h>
#include <M5StackChan.h>
#include <assert.h>
#include <IRrecv.h>
#include <IRremoteESP8266.h>
#include <IRac.h>
#include <IRtext.h>
#include <IRutils.h>
const uint16_t kRecvPin = 10; // IR rx pin
const uint16_t kCaptureBufferSize = 1024;
#if DECODE_AC
// Some A/C units have gaps in their protocols of ~40ms. e.g. Kelvinator
// A value this large may swallow repeats of some protocols
const uint8_t kTimeout = 50;
#else // DECODE_AC
// Suits most messages, while not swallowing many repeats.
const uint8_t kTimeout = 15;
#endif // DECODE_AC
const uint16_t kMinUnknownSize = 12;
const uint8_t kTolerancePercentage = kTolerance; // kTolerance is normally 25%
IRrecv irrecv(kRecvPin, kCaptureBufferSize, kTimeout, true);
decode_results results;
void setup()
{
M5StackChan.begin();
Serial.begin(115200);
// Perform a low level sanity checks that the compiler performs bit field
// packing as we expect and Endianness is as we expect.
assert(irutils::lowLevelSanityCheck() == 0);
#if DECODE_HASH
// Ignore messages with less than minimum on or off pulses.
irrecv.setUnknownThreshold(kMinUnknownSize);
#endif // DECODE_HASH
irrecv.setTolerance(kTolerancePercentage); // Override the default tolerance.
irrecv.enableIRIn(); // Start the receiver
}
void loop()
{
// Check if the IR code has been received.
if (irrecv.decode(&results)) {
// Display a crude timestamp.
uint32_t now = millis();
Serial.printf(D_STR_TIMESTAMP " : %06u.%03u\n", now / 1000, now % 1000);
// Check if we got an IR message that was to big for our capture buffer.
if (results.overflow) Serial.printf(D_WARN_BUFFERFULL "\n", kCaptureBufferSize);
// Display the library version the message was captured with.
Serial.println(D_STR_LIBRARY " : v" _IRREMOTEESP8266_VERSION_STR "\n");
// Display the tolerance percentage if it has been change from the default.
if (kTolerancePercentage != kTolerance) Serial.printf(D_STR_TOLERANCE " : %d%%\n", kTolerancePercentage);
// Display the basic output of what we found.
Serial.print(resultToHumanReadableBasic(&results));
// Display any extra A/C info if we have it.
String description = IRAcUtils::resultAcToString(&results);
if (description.length()) Serial.println(D_STR_MESGDESC ": " + description);
yield(); // Feed the WDT as the text output can take a while to print.
// Output the results as source code
Serial.println(resultToSourceCode(&results));
Serial.println(); // Blank line between entries
yield(); // Feed the WDT (again)
}
delay(50);
}
+47
View File
@@ -0,0 +1,47 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
// More examples on: https://github.com/crankyoldgit/IRremoteESP8266/tree/master/examples
#include <Arduino.h>
#include <M5StackChan.h>
#include <IRremoteESP8266.h>
#include <IRsend.h>
const uint16_t kIrLed = 5; // IR tx pin
IRsend irsend(kIrLed); // Set the GPIO to be used to sending the message.
// Example of data captured by IRrecvDumpV2.ino
uint16_t rawData[67] = {9000, 4500, 650, 550, 650, 1650, 600, 550, 650, 550, 600, 1650, 650, 550,
600, 1650, 650, 1650, 650, 1650, 600, 550, 650, 1650, 650, 1650, 650, 550,
600, 1650, 650, 1650, 650, 550, 650, 550, 650, 1650, 650, 550, 650, 550,
650, 550, 600, 550, 650, 550, 650, 550, 650, 1650, 600, 550, 650, 1650,
650, 1650, 650, 1650, 650, 1650, 650, 1650, 650, 1650, 600};
// Example Samsung A/C state captured from IRrecvDumpV2.ino
uint8_t samsungState[kSamsungAcStateLength] = {0x02, 0x92, 0x0F, 0x00, 0x00, 0x00, 0xF0,
0x01, 0xE2, 0xFE, 0x71, 0x40, 0x11, 0xF0};
void setup()
{
M5StackChan.begin();
Serial.begin(115200);
irsend.begin();
}
void loop()
{
Serial.println("NEC");
irsend.sendNEC(0x00FFE01FUL);
delay(2000);
Serial.println("Sony");
irsend.sendSony(0xa90, 12, 2); // 12 bits & 2 repeats
delay(2000);
Serial.println("a rawData capture from IRrecvDumpV2");
irsend.sendRaw(rawData, 67, 38); // Send a raw data capture at 38kHz.
delay(2000);
Serial.println("a Samsung A/C state from IRrecvDumpV2");
irsend.sendSamsungAC(samsungState);
delay(2000);
}
+75
View File
@@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
// More examples on: https://github.com/m5stack/M5Unit-NFC/tree/main/examples
#include <Arduino.h>
#include <M5StackChan.h>
#include <M5UnitUnified.h>
#include <M5UnitUnifiedNFC.h>
#include <M5Utility.h>
#include <vector>
using namespace m5::nfc::a;
namespace {
auto& lcd = M5.Display;
m5::unit::UnitUnified Units;
m5::unit::UnitNFC unit{}; // I2C
m5::nfc::NFCLayerA nfc_a{unit};
} // namespace
void setup()
{
M5StackChan.begin();
if (!Units.add(unit, M5.In_I2C) || !Units.begin()) {
M5_LOGE("Failed to begin");
lcd.clear(TFT_RED);
while (true) {
m5::utility::delay(10000);
}
}
M5_LOGI("M5UnitUnified has been begun");
M5_LOGI("%s", Units.debugInfo().c_str());
if (lcd.width() < lcd.height()) {
lcd.setRotation(1);
}
lcd.setFont(&fonts::Font0);
lcd.fillScreen(0);
lcd.setCursor(0, 0);
}
void loop()
{
M5StackChan.update();
Units.update();
std::vector<PICC> piccs;
if (nfc_a.detect(piccs)) {
lcd.fillScreen(0);
lcd.setCursor(0, 0);
uint16_t idx{};
for (auto&& u : piccs) {
M5.Speaker.tone(6000, 5);
// detect only performs a provisional classification based on sak, so further identification is required
if (nfc_a.identify(u)) {
M5.Log.printf("PICC:%s %s %04X/%02X %u/%u\n", u.uidAsString().c_str(), u.typeAsString().c_str(), u.atqa,
u.sak, u.userAreaSize(), u.totalSize());
lcd.printf("[%2u]:PICC:<%s> %s\n", idx, u.uidAsString().c_str(), u.typeAsString().c_str());
++idx;
} else {
M5_LOGW("Failed to identify %s %s %04X/%02X %u/%u", u.uidAsString().c_str(), u.typeAsString().c_str(),
u.atqa, u.sak, u.userAreaSize(), u.totalSize());
}
}
if (idx) {
M5.Speaker.tone(3000, 10);
lcd.printf("==> %u PICC\n", idx);
M5.Log.printf("==> %u PICC\n", idx);
}
nfc_a.deactivate();
}
}
+133
View File
@@ -0,0 +1,133 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
// More examples on: https://github.com/m5stack/M5Unit-NFC/tree/main/examples
#include <Arduino.h>
#include <M5StackChan.h>
#include <M5UnitUnified.h>
#include <M5UnitUnifiedNFC.h>
#include <M5Utility.h>
#include <vector>
using namespace m5::nfc;
using namespace m5::nfc::a;
using namespace m5::nfc::a::mifare;
using namespace m5::nfc::a::mifare::classic;
namespace {
auto& lcd = M5.Display;
m5::unit::UnitUnified Units;
m5::unit::UnitNFC unit{}; // I2C
m5::nfc::EmulationLayerA emu_a{unit};
PICC picc{};
constexpr Type type{Type::MIFARE_Ultralight};
constexpr uint8_t uid[] = {0x04, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE};
uint8_t picc_memory[] = {
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0xA3, 0x00, 0x00, //
0xE1, 0x10, 0x06, 0x00, //
0x03, 0x25, 0x91, 0x01, //
0x0D, 0x55, 0x04, 0x6D, //
0x35, 0x73, 0x74, 0x61, //
0x63, 0x6B, 0x2E, 0x63, //
0x6F, 0x6D, 0x2F, 0x51, //
0x01, 0x10, 0x54, 0x02, //
0x65, 0x6E, 0x48, 0x65, //
0x6C, 0x6C, 0x6F, 0x20, //
0x4D, 0x35, 0x53, 0x74, //
0x61, 0x63, 0x6B, 0xFE, //
0x44, 0x45, 0x46, 0x00, //
0x44, 0x45, 0x46, 0x00, //
};
uint8_t bcc8(const uint8_t* p, const uint8_t len, const uint8_t init = 0)
{
uint8_t v = init;
for (uint_fast8_t i = 0; i < len; ++i) {
v ^= p[i];
}
return v;
}
// Correctly embed the Ultralight and NTAG UIDs into memory
void embed_uid(uint8_t mem[9], const uint8_t uid[7])
{
memcpy(mem, uid, 3);
mem[3] = bcc8(uid, 3, 0x88 /* CT */);
memcpy(mem + 4, uid + 3, 4);
mem[8] = bcc8(uid + 3, 4);
}
constexpr uint16_t color_table[] = {
// None, Off, Idle, Ready, Active, Halt };
TFT_BLACK, TFT_RED, TFT_BLUE, TFT_YELLOW, TFT_GREEN, TFT_MAGENTA};
constexpr const char* state_table[] = {"-", "O", "I", "R", "A", "H"};
} // namespace
void setup()
{
M5StackChan.begin();
// Emulation settings
auto cfg = unit.config();
cfg.emulation = true;
cfg.mode = NFC::A;
unit.config(cfg);
if (!Units.add(unit, M5.In_I2C) || !Units.begin()) {
M5_LOGE("Failed to begin");
lcd.clear(TFT_RED);
while (true) {
m5::utility::delay(10000);
}
}
M5_LOGI("M5UnitUnified has been begun");
M5_LOGI("%s", Units.debugInfo().c_str());
if (lcd.width() < lcd.height()) {
lcd.setRotation(1);
}
lcd.setFont(&fonts::Font2);
//
lcd.startWrite();
lcd.fillScreen(TFT_RED);
if (picc.emulate(type, uid, sizeof(uid))) {
embed_uid(picc_memory, uid);
if (emu_a.begin(picc, picc_memory, sizeof(picc_memory))) {
lcd.fillScreen(TFT_DARKGREEN);
lcd.setCursor(0, 16);
const auto& e_picc = emu_a.emulatePICC();
M5.Log.printf("Emulation:%s %s ATQA:%04X SAK:%u\n", e_picc.typeAsString().c_str(),
e_picc.uidAsString().c_str(), e_picc.atqa, e_picc.sak);
lcd.printf("%s\n%s\nATQA:%04X SAK:%u", e_picc.typeAsString().c_str(), e_picc.uidAsString().c_str(),
e_picc.atqa, e_picc.sak);
}
}
lcd.fillRect(0, 0, 32, 16, color_table[0]);
lcd.drawString(state_table[0], 0, 0);
lcd.endWrite();
}
void loop()
{
M5StackChan.update();
Units.update();
emu_a.update(); // Need call in loop
static EmulationLayerA::State latest{};
auto state = emu_a.state();
if (latest != state) {
latest = state;
lcd.startWrite();
lcd.fillRect(0, 0, 32, 16, color_table[m5::stl::to_underlying(state)]);
lcd.drawString(state_table[m5::stl::to_underlying(state)], 0, 0);
lcd.endWrite();
}
}
+35
View File
@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include <Arduino.h>
#include <M5StackChan.h>
struct Color_t {
uint8_t r = 0;
uint8_t g = 0;
uint8_t b = 0;
};
static std::vector<Color_t> colors = {
{0, 0, 0}, {168, 0, 0}, {0, 168, 0}, {0, 0, 168}, {168, 168, 0}, {168, 0, 168}, {0, 168, 168}, {168, 168, 168},
};
void setup()
{
/* Init StackChan */
M5StackChan.begin();
}
void loop()
{
/* There are 12 RGB LEDs, index 0-5 are on the left, 6-11 are on the right */
for (int color_index = 0; color_index < colors.size(); color_index++) {
for (int led_index = 0; led_index < 12; led_index++) {
M5StackChan.setRgbColor(led_index, colors[color_index].r, colors[color_index].g, colors[color_index].b);
M5StackChan.refreshRgb();
delay(1000 / 24);
}
}
}
@@ -0,0 +1,138 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include <Arduino.h>
#include <M5StackChan.h>
void setup()
{
/* Init StackChan */
M5StackChan.begin();
/* Setup display */
M5StackChan.Display().setTextSize(2);
M5StackChan.Display().setTextScroll(true);
M5StackChan.Display().setTextColor(TFT_YELLOW);
M5StackChan.Display().printf("> Touch the top to start\n");
M5StackChan.Display().setTextColor(TFT_GREENYELLOW);
// Set to false if high-frequency updates are needed
// M5StackChan.Motion.setAutoAngleSyncEnabled(false);
}
void loop()
{
M5StackChan.update();
if (M5StackChan.TouchSensor.wasPressed()) {
delay(200);
/* Angle unit: 10 = 1 degrees, Speed range: 0~1000 */
/* Range X: -1280 ~ 1280 (-128° ~ 128°), Range Y: 0 ~ 900 (0° ~ 90°) */
/* Move to home position (0, 0) */
M5StackChan.Motion.goHome();
M5StackChan.Display().printf("> Go home\n");
delay(2000);
/* Move X servo to 100° */
M5StackChan.Motion.moveX(1000, 200);
M5StackChan.Display().printf("> Turn Left (Slow: 200)\n");
delay(2000);
/* Move X servo to -100° */
M5StackChan.Motion.moveX(-1000, 200);
M5StackChan.Display().printf("> Turn Right (Slow: 200)\n");
delay(2000);
/* Move X servo to 100° */
M5StackChan.Motion.moveX(1000, 800);
M5StackChan.Display().printf("> Turn Left (Fast: 800)\n");
delay(2000);
/* Move X servo to -100° */
M5StackChan.Motion.moveX(-1000, 800);
M5StackChan.Display().printf("> Turn Right (Fast: 800)\n");
delay(2000);
M5StackChan.Motion.goHome();
M5StackChan.Display().printf("> Go home\n");
delay(2000);
/* Move Y servo to 90° */
M5StackChan.Motion.moveY(900, 200);
M5StackChan.Display().printf("> Look Up (Slow: 200)\n");
delay(2000);
/* Move Y servo to 0° */
M5StackChan.Motion.moveY(0, 200);
M5StackChan.Display().printf("> Look Down (Slow: 200)\n");
delay(2000);
/* Move Y servo to 90° */
M5StackChan.Motion.moveY(900, 800);
M5StackChan.Display().printf("> Look Up (Fast: 800)\n");
delay(2000);
/* Move Y servo to 0° */
M5StackChan.Motion.moveY(0, 800);
M5StackChan.Display().printf("> Look Down (Fast: 800)\n");
delay(2000);
/* Move X servo to 60°, Y servo to 70° */
M5StackChan.Motion.move(600, 700);
M5StackChan.Display().printf("> Top Left\n");
delay(2000);
M5StackChan.Motion.goHome();
M5StackChan.Display().printf("> Go home\n");
delay(2000);
/* Move X servo to -60°, Y servo to 70° */
M5StackChan.Motion.move(-600, 700);
M5StackChan.Display().printf("> Top Right\n");
delay(2000);
M5StackChan.Motion.goHome();
M5StackChan.Display().printf("> Go home\n");
delay(2000);
/* Only X axis supports continuous 360° rotation. Y axis does not. */
/* Velocity range: -1000 ~ 1000 (Negative: CW, Positive: CCW) */
/* Rotate clockwise */
M5StackChan.Motion.rotateX(-300);
M5StackChan.Display().printf("> Rotate clockwise (Slow: 300)\n");
delay(3000);
/* Rotate clockwise */
M5StackChan.Motion.rotateX(-800);
M5StackChan.Display().printf("> Rotate clockwise (Fast: 800)\n");
delay(3000);
M5StackChan.Motion.goHome();
M5StackChan.Display().printf("> Go home\n");
delay(2000);
/* Rotate counter-clockwise */
M5StackChan.Motion.rotateX(300);
M5StackChan.Display().printf("> Rotate counter-clockwise (Slow: 300)\n");
delay(3000);
/* Rotate counter-clockwise */
M5StackChan.Motion.rotateX(800);
M5StackChan.Display().printf("> Rotate counter-clockwise (Fast: 800)\n");
delay(3000);
M5StackChan.Motion.goHome();
M5StackChan.Display().printf("> Go home\n");
delay(2000);
M5StackChan.Display().setTextColor(TFT_YELLOW);
M5StackChan.Display().printf("> Touch the top to start\n");
M5StackChan.Display().setTextColor(TFT_GREENYELLOW);
}
delay(100);
}
+111
View File
@@ -0,0 +1,111 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include <Arduino.h>
#include <M5StackChan.h>
/**
* @brief
* Angle unit: 10 = 1 degrees, Speed range: 0~1000
* Range X: -1280 ~ 1280 (-128° ~ 128°), Range Y: 0 ~ 900 (0° ~ 90°)
*
*/
struct Keyframe_t {
int x = 0;
int y = 0;
int speed = 500;
uint32_t interval = 0;
};
/* Move around */
std::vector<Keyframe_t> dance_1 = {
{0, 0, 500, 1000}, // Home, 1s
{600, 200, 800, 500}, // Left, fast
{-600, 200, 800, 500}, // Right, fast
{600, 200, 800, 500}, // Left, fast
{-600, 200, 800, 500}, // Right, fast
{0, 800, 900, 400}, // Look Up
{0, 0, 900, 400}, // Look Down
{0, 800, 900, 400}, // Look Up
{0, 0, 900, 400}, // Look Down
{800, 700, 700, 500}, // Top Left
{-800, 700, 700, 500}, // Top Right
{800, 700, 700, 500}, // Top Left
{-800, 700, 700, 500}, // Top Right
{0, 0, 500, 1000} // Back Home
};
/* Shake */
std::vector<Keyframe_t> dance_2 = {
{0, 0, 500, 1000}, // Home
{300, 0, 750, 250}, // Shake Left
{-300, 0, 750, 250}, // Shake Right
{300, 0, 750, 250}, // Shake Left
{-300, 0, 750, 250}, // Shake Right
{300, 0, 750, 250}, // Shake Left
{-300, 0, 750, 250}, // Shake Right
{0, 0, 500, 1000} // Back Home
};
/* Nod */
std::vector<Keyframe_t> dance_3 = {
{0, 0, 500, 1000}, // Home
{0, 350, 900, 250}, // Nod Up
{0, 0, 900, 250}, // Nod Down
{0, 350, 900, 250}, // Nod Up
{0, 0, 900, 250}, // Nod Down
{0, 350, 900, 250}, // Nod Up
{0, 0, 900, 250}, // Nod Down
{0, 0, 500, 500} // Back Home
};
std::vector<std::vector<Keyframe_t>*> dances = {
&dance_1,
&dance_2,
&dance_3,
};
static int dance_index = 0;
void setup()
{
/* Init StackChan */
M5StackChan.begin();
/* Setup display */
M5StackChan.Display().setTextSize(2);
M5StackChan.Display().setTextScroll(true);
M5StackChan.Display().setTextColor(TFT_YELLOW);
M5StackChan.Display().printf("> Touch the top to start\n");
M5StackChan.Display().setTextColor(TFT_GREENYELLOW);
// Disable auto angle sync for smooth and continuous movement
M5StackChan.Motion.setAutoAngleSyncEnabled(false);
}
void loop()
{
M5StackChan.update();
if (M5StackChan.TouchSensor.wasPressed()) {
delay(200);
/* Perform dance */
M5StackChan.Display().printf("> Dance #%d!\n", dance_index + 1);
for (const auto& frame : *dances[dance_index]) {
M5StackChan.Motion.move(frame.x, frame.y, frame.speed);
delay(frame.interval);
}
M5StackChan.Display().printf("> Finished!\n");
/* Next dance */
dance_index = (dance_index + 1) % dances.size();
M5StackChan.Display().setTextColor(TFT_YELLOW);
M5StackChan.Display().printf("> Touch the top to start\n");
M5StackChan.Display().setTextColor(TFT_GREENYELLOW);
}
delay(100);
}
@@ -0,0 +1,111 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include <Arduino.h>
#include <M5StackChan.h>
namespace {
constexpr uint16_t kBackgroundColor = TFT_BLACK;
constexpr uint16_t kBorderColor = TFT_WHITE;
constexpr uint16_t kTopButtonColor = 0x39C7;
constexpr uint16_t kTopButtonPressedColor = 0x2204;
constexpr uint16_t kBottomButtonColor = 0x03EF;
constexpr uint16_t kBottomButtonPressedColor = 0x01E8;
constexpr uint16_t kTextColor = TFT_WHITE;
enum class ButtonZone {
None,
SetHome,
GoHome,
};
ButtonZone pressed_zone = ButtonZone::None;
ButtonZone getButtonZone(const int16_t y, const int16_t height)
{
return y < (height / 2) ? ButtonZone::SetHome : ButtonZone::GoHome;
}
void drawButton(const int16_t x, const int16_t y, const int16_t w, const int16_t h, const uint16_t color,
const char* line_1, const char* line_2)
{
auto& display = M5StackChan.Display();
display.fillRect(x, y, w, h, color);
display.drawRect(x, y, w, h, kBorderColor);
display.setTextDatum(middle_center);
display.setTextColor(kTextColor, color);
display.setTextSize(2);
display.drawString(line_1, x + w / 2, y + h / 2 - 12);
display.drawString(line_2, x + w / 2, y + h / 2 + 12);
}
void drawUi(ButtonZone active_zone)
{
auto& display = M5StackChan.Display();
const int16_t width = display.width();
const int16_t height = display.height();
const int16_t gap = 8;
const int16_t button_x = 8;
const int16_t button_w = width - button_x * 2;
const int16_t half_h = (height - gap) / 2;
display.startWrite();
display.fillScreen(kBackgroundColor);
display.fillRect(0, half_h, width, gap, kBackgroundColor);
drawButton(button_x, 8, button_w, half_h - 12,
active_zone == ButtonZone::SetHome ? kTopButtonPressedColor : kTopButtonColor, "set current postion",
"as home");
drawButton(button_x, half_h + gap + 4, button_w, height - (half_h + gap + 12),
active_zone == ButtonZone::GoHome ? kBottomButtonPressedColor : kBottomButtonColor, "move to", "home");
display.endWrite();
}
} // namespace
void setup()
{
/* Init StackChan */
M5StackChan.begin();
/* Setup display */
M5StackChan.Display().setTextScroll(false);
drawUi(ButtonZone::None);
}
void loop()
{
M5StackChan.update();
auto& display = M5StackChan.Display();
const int16_t screen_height = display.height();
int16_t touch_x = 0;
int16_t touch_y = 0;
const bool touching = display.getTouch(&touch_x, &touch_y);
if (touching) {
const ButtonZone current_zone = getButtonZone(touch_y, screen_height);
if (current_zone != pressed_zone) {
pressed_zone = current_zone;
drawUi(pressed_zone);
}
} else if (pressed_zone != ButtonZone::None) {
const ButtonZone released_zone = pressed_zone;
pressed_zone = ButtonZone::None;
drawUi(ButtonZone::None);
if (released_zone == ButtonZone::SetHome) {
M5StackChan.Motion.setCurrentPostionAsHome();
} else if (released_zone == ButtonZone::GoHome) {
M5StackChan.Motion.goHome();
}
}
delay(16);
}
+42
View File
@@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include <Arduino.h>
#include <M5StackChan.h>
void setup()
{
/* Init StackChan */
M5StackChan.begin();
/* Setup display */
M5StackChan.Display().setTextSize(2);
M5StackChan.Display().setTextScroll(true);
M5StackChan.Display().setTextColor(TFT_YELLOW);
M5StackChan.Display().printf("> Touch or swipe the top\n");
M5StackChan.Display().setTextColor(TFT_GREENYELLOW);
}
void loop()
{
/* Update touch sensor */
M5StackChan.update();
auto& ts = M5StackChan.TouchSensor;
if (ts.wasClicked()) {
M5StackChan.Display().printf("> Was clicked\n");
}
if (ts.wasSwipedForward()) {
M5StackChan.Display().printf("> Was swiped forward\n");
}
if (ts.wasSwipedBackward()) {
M5StackChan.Display().printf("> Was swiped backward\n");
}
delay(50);
}
+21
View File
@@ -0,0 +1,21 @@
{
"name": "M5StackChan",
"description": "Library for M5StackChan",
"keywords": "M5Stack,M5StackChan",
"authors": {
"name": "M5Stack"
},
"repository": {
"type": "git",
"url": "https://github.com/M5Stack/M5StackChan.git"
},
"dependencies": {
"M5Unified": "*",
"M5GFX": "*",
"IRremoteESP8266": "*",
"M5Unit-NFC": "*"
},
"version": "1.0.0",
"frameworks": "arduino",
"platforms": "espressif32"
}
+11
View File
@@ -0,0 +1,11 @@
name=M5StackChan
version=1.0.0
author=M5Stack
maintainer=M5Stack
sentence=M5StackChan is a library for M5StackChan
paragraph=
category=Device Control
url=https://github.com/m5stack/M5StackChan.git
architectures=esp32
includes=src,src/utils
depends=M5Unified,IRremoteESP8266,M5Unit-NFC
+323
View File
@@ -0,0 +1,323 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include "M5StackChan.h"
#include "drivers/PY32IOExpander/PY32IOExpander.hpp"
#include "drivers/SCServo_lib/src/SCSCL.h"
#include "utils/compat/make_unique.h"
#include "utils/settings/settings.h"
#include "utility/power/INA226_Class.hpp"
#include <esp_log.h>
using namespace m5;
M5StackChan_Class M5StackChan;
static const char* TAG = "M5StackChan";
void M5StackChan_Class::begin()
{
M5.begin();
TouchSensor.begin();
io_expander_init();
servo_init();
ina226_init();
}
void M5StackChan_Class::update()
{
M5.update();
TouchSensor.update();
}
/* -------------------------------------------------------------------------- */
/* IO Expander */
/* -------------------------------------------------------------------------- */
std::unique_ptr<PY32IOExpander_Class> _io_expander;
void M5StackChan_Class::io_expander_init()
{
_io_expander = std::make_unique<m5::PY32IOExpander_Class>();
// PY32 IO Expander may boot slowly, wait for it
uint32_t start_tick = millis();
while (1) {
delay(200);
if (millis() - start_tick > 1200) {
ESP_LOGE(TAG, "IO expander init timeout");
_io_expander.reset();
break;
}
if (_io_expander->begin()) {
break;
}
}
if (_io_expander) {
// VM EN
_io_expander->setDirection(0, true); // Output
_io_expander->setPullMode(0, true); // Pull-up
setServoPowerEnabled(true);
delay(200);
// RGB
_io_expander->setDirection(13, true); // Output
_io_expander->setPullMode(13, true); // Pull-up
_io_expander->setDriveMode(13, false); // Push-pull
_io_expander->setLedCount(12);
delay(200);
showRgbColor(0, 0, 0);
delay(50);
showRgbColor(0, 0, 0);
}
}
void M5StackChan_Class::setServoPowerEnabled(bool enabled)
{
if (!_io_expander) {
return;
}
_io_expander->digitalWrite(0, enabled ? true : false);
}
void M5StackChan_Class::setRgbColor(uint8_t index, uint8_t r, uint8_t g, uint8_t b)
{
if (!_io_expander) {
return;
}
_io_expander->setLedColor(index, r, g, b);
}
void M5StackChan_Class::refreshRgb()
{
if (!_io_expander) {
return;
}
_io_expander->refreshLeds();
}
void M5StackChan_Class::showRgbColor(uint8_t r, uint8_t g, uint8_t b)
{
for (int i = 0; i < 12; i++) {
setRgbColor(i, r, g, b);
}
refreshRgb();
}
/* -------------------------------------------------------------------------- */
/* Servo */
/* -------------------------------------------------------------------------- */
static SCSCL _scs_bus;
struct ServoConfig_t {
int id = -1;
int defaultZeroPos = 0;
uitk_intl::Vector2i angleLimit;
uitk_intl::Vector2i rawPosLimit;
std::string settingNs;
std::string settingZeroPositionKey;
bool enablePwmMode = false;
};
/**
* @brief Servo class implement
*
*/
class ScsServo : public stackchan::motion::Servo {
public:
static inline const std::string _tag = "ScsServo";
ScsServo(const ServoConfig_t& config) : _config(config)
{
}
void init() override
{
set_angle_limit(_config.angleLimit);
get_zero_pos_from_nvs();
Servo::init();
}
void get_zero_pos_from_nvs()
{
_zero_pos = _config.defaultZeroPos;
bool is_valid = false;
{
Settings settings(_config.settingNs, false);
int nvs_zero_pos = settings.GetInt(_config.settingZeroPositionKey, -1);
// Limit check
if (nvs_zero_pos >= _config.rawPosLimit.x && nvs_zero_pos <= _config.rawPosLimit.y) {
_zero_pos = nvs_zero_pos;
is_valid = true;
ESP_LOGI(TAG, "Servo ID: %d get zero pos: %d from settings", _config.id, _zero_pos);
} else {
is_valid = false;
ESP_LOGW(TAG, "Servo ID: %d get invalid zero pos: %d from settings", _config.id, nvs_zero_pos);
}
}
if (!is_valid) {
_zero_pos = _config.defaultZeroPos;
ESP_LOGI(TAG, "Servo ID: %d override zero pos to default: %d", _config.id, _zero_pos);
Settings settings(_config.settingNs, true);
settings.SetInt(_config.settingZeroPositionKey, _zero_pos);
}
}
void set_angle_impl(int angle) override
{
int mapped_angle = _zero_pos + angle * 16 / 5 / 10; // 一步对应 0.3125度, 0.3125 = 5/16
mapped_angle = uitk_intl::clamp(mapped_angle, _config.rawPosLimit.x, _config.rawPosLimit.y);
// ESP_LOGI(TAG, "Servo ID: %d mapped angle: %d", _config.id, mapped_angle);
check_mode(Mode::Position);
_scs_bus.WritePos(_config.id, mapped_angle, 20, 0);
}
int getCurrentAngle() override
{
int current_pos = _scs_bus.ReadPos(_config.id);
int angle = (current_pos - _zero_pos) * 5 * 10 / 16;
angle = uitk_intl::clamp(angle, getAngleLimit().x, getAngleLimit().y);
// ESP_LOGI(TAG, "Servo ID: %d current pos: %d angle: %d", _id, current_pos, angle);
return angle;
}
bool is_moving_impl() override
{
int moving = _scs_bus.ReadMove(_config.id);
// ESP_LOGI(TAG, "Servo ID: %d moving: %d", _id, moving);
return moving != 0;
}
void setTorqueEnabled(bool enabled) override
{
Servo::setTorqueEnabled(enabled);
_scs_bus.EnableTorque(_config.id, enabled ? 1 : 0);
// ESP_LOGI(TAG, "Servo ID: %d set torque: %d", _id, enabled);
}
bool getTorqueEnabled() override
{
int torque_enable = _scs_bus.ReadToqueEnable(_config.id);
// ESP_LOGI(TAG, "Servo ID: %d torque enable: %d", _id, torque_enable);
return torque_enable > 0;
}
void setCurrentAngleAsZero() override
{
_zero_pos = _scs_bus.ReadPos(_config.id);
Settings settings(_config.settingNs, true);
settings.SetInt(_config.settingZeroPositionKey, _zero_pos);
ESP_LOGI(TAG, "Servo ID: %d set zero pos: %d to settings", _config.id, _zero_pos);
}
void rotate(int velocity) override
{
velocity = uitk_intl::clamp(velocity, -1000, 1000);
if (!_config.enablePwmMode) {
return;
}
int mapped_velocity = uitk_intl::map_range(velocity, 0, 1000, 0, 1023);
check_mode(Mode::PWM);
_scs_bus.WritePWM(_config.id, mapped_velocity);
}
private:
enum class Mode { Position = 0, PWM = 1 };
ServoConfig_t _config;
int _zero_pos = 0;
Mode _current_mode = Mode::Position;
void check_mode(Mode targetMode)
{
if (targetMode == _current_mode) {
return;
}
_scs_bus.SwitchMode(_config.id, static_cast<uint8_t>(targetMode));
_current_mode = targetMode;
}
};
void M5StackChan_Class::servo_init()
{
_scs_bus.begin(UART_NUM_1, 1000000, 6, 7);
uitk_intl::ui_hal::on_delay([](uint32_t ms) { delay(ms); });
uitk_intl::ui_hal::on_get_tick([]() { return millis(); });
ServoConfig_t yaw_servo_config;
yaw_servo_config.id = 1;
yaw_servo_config.defaultZeroPos = 460;
yaw_servo_config.angleLimit = uitk_intl::Vector2i(-1280, 1280);
yaw_servo_config.rawPosLimit = uitk_intl::Vector2i(0, 1000);
yaw_servo_config.settingNs = "servo";
yaw_servo_config.settingZeroPositionKey = "zero_pos_1";
yaw_servo_config.enablePwmMode = true;
ServoConfig_t pitch_servo_config;
pitch_servo_config.id = 2;
pitch_servo_config.defaultZeroPos = 620;
pitch_servo_config.angleLimit = uitk_intl::Vector2i(0, 900);
pitch_servo_config.rawPosLimit = uitk_intl::Vector2i(0, 1000);
pitch_servo_config.settingNs = "servo";
pitch_servo_config.settingZeroPositionKey = "zero_pos_2";
auto yaw_servo = std::make_unique<ScsServo>(yaw_servo_config);
auto pitch_servo = std::make_unique<ScsServo>(pitch_servo_config);
Motion.init(std::move(yaw_servo), std::move(pitch_servo));
}
/* -------------------------------------------------------------------------- */
/* INA226 */
/* -------------------------------------------------------------------------- */
std::unique_ptr<m5::INA226_Class> _ina226;
void M5StackChan_Class::ina226_init()
{
_ina226 = std::make_unique<m5::INA226_Class>(0x41);
m5::INA226_Class::config_t config;
config.shunt_res = 0.01;
config.max_expected_current = 8.19;
_ina226->config(config);
if (!_ina226->begin()) {
ESP_LOGE(TAG, "INA226 init failed");
_ina226.reset();
}
}
float M5StackChan_Class::getBatteryVoltage()
{
float result = 0.0f;
if (_ina226) {
result = _ina226->getBusVoltage();
}
return result;
}
float M5StackChan_Class::getBatteryCurrent()
{
float result = 0.0f;
if (_ina226) {
result = _ina226->getShuntCurrent();
}
return result;
}
+87
View File
@@ -0,0 +1,87 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include "utils/touch_sensor/touch_sensor.h"
#include "utils/motion/motion.h"
#include <M5GFX.h>
#include <M5Unified.hpp>
#include <memory>
namespace m5 {
class M5StackChan_Class {
public:
void begin();
void update();
inline LGFX_Device& Display()
{
return M5.Display;
}
inline LGFX_Device& Lcd()
{
return M5.Lcd;
}
TouchSensor_Class TouchSensor;
stackchan::motion::Motion Motion;
/**
* @brief Enable or disable servo power.
*
* @param enabled
*/
void setServoPowerEnabled(bool enabled);
/**
* @brief Set the Rgb Color object.
* There are 12 RGB LEDs, 0-5 are on the left, 6-11 are on the right.
*
* @param index
* @param r
* @param g
* @param b
*/
void setRgbColor(uint8_t index, uint8_t r, uint8_t g, uint8_t b);
/**
* @brief Update the RGB LEDs with the set colors.
*
*/
void refreshRgb();
/**
* @brief Set all RGB LEDs to the specified color.
*
* @param r
* @param g
* @param b
*/
void showRgbColor(uint8_t r, uint8_t g, uint8_t b);
/**
* @brief Get battery voltage.
*
* @return float
*/
float getBatteryVoltage();
/**
* @brief Get battery current.
* Positive when discharging, negative when charging.
*
* @return float
*/
float getBatteryCurrent();
protected:
void io_expander_init();
void servo_init();
void ina226_init();
};
} // namespace m5
extern m5::M5StackChan_Class M5StackChan;
@@ -0,0 +1,280 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include "PY32IOExpander.hpp"
namespace m5 {
// Register definitions
static constexpr uint8_t REG_UID_L = 0x00;
static constexpr uint8_t REG_UID_H = 0x01;
static constexpr uint8_t REG_VERSION = 0x02;
static constexpr uint8_t REG_GPIO_M_L = 0x03;
static constexpr uint8_t REG_GPIO_M_H = 0x04;
static constexpr uint8_t REG_GPIO_O_L = 0x05;
static constexpr uint8_t REG_GPIO_O_H = 0x06;
static constexpr uint8_t REG_GPIO_I_L = 0x07;
static constexpr uint8_t REG_GPIO_I_H = 0x08;
static constexpr uint8_t REG_GPIO_PU_L = 0x09;
static constexpr uint8_t REG_GPIO_PU_H = 0x0A;
static constexpr uint8_t REG_GPIO_PD_L = 0x0B;
static constexpr uint8_t REG_GPIO_PD_H = 0x0C;
static constexpr uint8_t REG_GPIO_IE_L = 0x0D;
static constexpr uint8_t REG_GPIO_IE_H = 0x0E;
static constexpr uint8_t REG_GPIO_IT_L = 0x0F;
static constexpr uint8_t REG_GPIO_IT_H = 0x10;
static constexpr uint8_t REG_GPIO_IS_L = 0x11;
static constexpr uint8_t REG_GPIO_IS_H = 0x12;
static constexpr uint8_t REG_GPIO_DRV_L = 0x13;
static constexpr uint8_t REG_GPIO_DRV_H = 0x14;
static constexpr uint8_t REG_ADC_CTRL = 0x15;
static constexpr uint8_t REG_ADC_D_L = 0x16;
static constexpr uint8_t REG_ADC_D_H = 0x17;
static constexpr uint8_t REG_PWM_FREQ_L = 0x25;
static constexpr uint8_t REG_PWM_FREQ_H = 0x26;
static constexpr uint8_t REG_LED_CFG = 0x24;
static constexpr uint8_t REG_LED_RAM_START = 0x30;
// PWM Duty Registers
static constexpr uint8_t REG_PWM1_DUTY_L = 0x1B;
static constexpr uint8_t REG_PWM1_DUTY_H = 0x1C;
static constexpr uint8_t REG_PWM2_DUTY_L = 0x1D;
static constexpr uint8_t REG_PWM2_DUTY_H = 0x1E;
static constexpr uint8_t REG_PWM3_DUTY_L = 0x1F;
static constexpr uint8_t REG_PWM3_DUTY_H = 0x20;
static constexpr uint8_t REG_PWM4_DUTY_L = 0x21;
static constexpr uint8_t REG_PWM4_DUTY_H = 0x22;
void PY32IOExpander_Class::_writeBit(uint8_t reg_l, uint8_t reg_h, uint8_t pin, bool value)
{
if (pin < 8) {
if (value)
bitOn(reg_l, 1 << pin);
else
bitOff(reg_l, 1 << pin);
} else {
if (value)
bitOn(reg_h, 1 << (pin - 8));
else
bitOff(reg_h, 1 << (pin - 8));
}
}
bool PY32IOExpander_Class::_readBit(uint8_t reg_l, uint8_t reg_h, uint8_t pin)
{
if (pin < 8) {
return (readRegister8(reg_l) & (1 << pin)) != 0;
} else {
return (readRegister8(reg_h) & (1 << (pin - 8))) != 0;
}
}
bool PY32IOExpander_Class::begin()
{
uint8_t version = readRegister8(REG_VERSION);
if (version == 0 || version == 0xFF) return false;
return true;
}
void PY32IOExpander_Class::setDirection(uint8_t pin, bool direction)
{
// direction: false=input (0), true=output (1)
_writeBit(REG_GPIO_M_L, REG_GPIO_M_H, pin, direction);
}
void PY32IOExpander_Class::enablePull(uint8_t pin, bool enablePull)
{
if (enablePull) {
// Enable Pull Up by default if neither is set
bool pu = _readBit(REG_GPIO_PU_L, REG_GPIO_PU_H, pin);
bool pd = _readBit(REG_GPIO_PD_L, REG_GPIO_PD_H, pin);
if (!pu && !pd) {
_writeBit(REG_GPIO_PU_L, REG_GPIO_PU_H, pin, true);
}
// If one is already set, leave it.
} else {
// Disable both
_writeBit(REG_GPIO_PU_L, REG_GPIO_PU_H, pin, false);
_writeBit(REG_GPIO_PD_L, REG_GPIO_PD_H, pin, false);
}
}
void PY32IOExpander_Class::setPullMode(uint8_t pin, bool mode)
{
// mode: false=down, true=up
if (mode) {
// Pull Up
_writeBit(REG_GPIO_PD_L, REG_GPIO_PD_H, pin, false);
_writeBit(REG_GPIO_PU_L, REG_GPIO_PU_H, pin, true);
} else {
// Pull Down
_writeBit(REG_GPIO_PU_L, REG_GPIO_PU_H, pin, false);
_writeBit(REG_GPIO_PD_L, REG_GPIO_PD_H, pin, true);
}
}
void PY32IOExpander_Class::setDriveMode(uint8_t pin, bool openDrain)
{
// openDrain: false=push-pull (0), true=open-drain (1)
_writeBit(REG_GPIO_DRV_L, REG_GPIO_DRV_H, pin, openDrain);
}
void PY32IOExpander_Class::setHighImpedance(uint8_t pin, bool enable)
{
if (enable) {
// Input mode
setDirection(pin, false);
// Disable pulls
enablePull(pin, false);
}
}
bool PY32IOExpander_Class::getWriteValue(uint8_t pin)
{
return _readBit(REG_GPIO_O_L, REG_GPIO_O_H, pin);
}
void PY32IOExpander_Class::digitalWrite(uint8_t pin, bool level)
{
_writeBit(REG_GPIO_O_L, REG_GPIO_O_H, pin, level);
}
bool PY32IOExpander_Class::digitalRead(uint8_t pin)
{
return _readBit(REG_GPIO_I_L, REG_GPIO_I_H, pin);
}
void PY32IOExpander_Class::resetIrq()
{
// Clear all interrupts by writing 1s to IS registers
writeRegister8(REG_GPIO_IS_L, 0xFF);
writeRegister8(REG_GPIO_IS_H, 0xFF); // Only bits 0-5 used for high byte (pins 8-13)
}
void PY32IOExpander_Class::disableIrq()
{
// Disable all interrupts
writeRegister8(REG_GPIO_IE_L, 0x00);
writeRegister8(REG_GPIO_IE_H, 0x00);
}
void PY32IOExpander_Class::enableIrq()
{
// Enable all interrupts
writeRegister8(REG_GPIO_IE_L, 0xFF);
writeRegister8(REG_GPIO_IE_H, 0x3F); // Pins 8-13
}
uint16_t PY32IOExpander_Class::readDeviceUID()
{
uint8_t l = readRegister8(REG_UID_L);
uint8_t h = readRegister8(REG_UID_H);
return (h << 8) | l;
}
uint8_t PY32IOExpander_Class::readVersion()
{
return readRegister8(REG_VERSION);
}
uint16_t PY32IOExpander_Class::analogRead(uint8_t channel)
{
if (channel < 1 || channel > 4) return 0;
// Start conversion
// REG_ADC_CTRL: [7:Busy] [6:Start] [2:0:Channel]
// Channel mapping: 1->1, 2->2, 3->3, 4->4
writeRegister8(REG_ADC_CTRL, (1 << 6) | (channel & 0x07));
// Wait for busy bit to clear
// Simple polling with timeout
for (int i = 0; i < 100; i++) {
uint8_t ctrl = readRegister8(REG_ADC_CTRL);
if (!(ctrl & (1 << 7))) {
break;
}
// delay?
}
uint8_t l = readRegister8(REG_ADC_D_L);
uint8_t h = readRegister8(REG_ADC_D_H);
return (h << 8) | l;
}
void PY32IOExpander_Class::setPwmDuty(uint8_t channel, uint8_t duty)
{
if (channel > 3) return;
// Calculate register address
// Channel 0 -> PWM1 (0x1B)
// Channel 1 -> PWM2 (0x1D)
// Channel 2 -> PWM3 (0x1F)
// Channel 3 -> PWM4 (0x21)
uint8_t reg_l = REG_PWM1_DUTY_L + (channel * 2);
uint8_t reg_h = reg_l + 1;
// Duty is 8-bit percentage (0-100)? Or 0-255?
// m5_io_py32ioexpander uses percentage (0-100) or 12-bit raw.
// Let's assume 0-255 for standard Arduino style, but map to 12-bit (0-4095).
// 255 -> 4095. val * 4095 / 255 = val * 16 approx.
uint16_t duty12 = (uint16_t)duty * 16;
if (duty12 > 4095) duty12 = 4095;
// High byte contains Enable(7) and Polarity(6) bits.
// We need to preserve them or set defaults.
// Let's enable by default, polarity normal (0).
uint8_t h_val = (duty12 >> 8) & 0x0F;
h_val |= (1 << 7); // Enable
writeRegister8(reg_l, duty12 & 0xFF);
writeRegister8(reg_h, h_val);
}
void PY32IOExpander_Class::setPwmFrequency(uint16_t freq)
{
writeRegister8(REG_PWM_FREQ_L, freq & 0xFF);
writeRegister8(REG_PWM_FREQ_H, (freq >> 8) & 0xFF);
}
void PY32IOExpander_Class::setLedCount(uint8_t count)
{
if (count > 32) count = 32;
writeRegister8(REG_LED_CFG, count & 0x3F);
}
void PY32IOExpander_Class::setLedColor(uint8_t index, uint16_t color565)
{
if (index >= 32) return;
uint8_t data[2] = {(uint8_t)(color565 & 0xFF), (uint8_t)((color565 >> 8) & 0xFF)};
writeRegister(REG_LED_RAM_START + index * 2, data, 2);
}
void PY32IOExpander_Class::setLedColor(uint8_t index, uint8_t r, uint8_t g, uint8_t b)
{
// RGB888 to RGB565: RRRRRGGG GGGBBBBB
uint16_t val = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
setLedColor(index, val);
}
void PY32IOExpander_Class::setLedColor(uint8_t index, uint32_t color)
{
setLedColor(index, (uint8_t)((color >> 16) & 0xFF), (uint8_t)((color >> 8) & 0xFF), (uint8_t)(color & 0xFF));
}
void PY32IOExpander_Class::setLedData(const uint8_t* data, size_t len)
{
if (!data || len == 0) return;
if (len > 64) len = 64; // Max 32 LEDs * 2 bytes
writeRegister(REG_LED_RAM_START, (uint8_t*)data, len);
}
void PY32IOExpander_Class::refreshLeds()
{
uint8_t val = readRegister8(REG_LED_CFG);
writeRegister8(REG_LED_CFG, val | (1 << 6));
}
} // namespace m5
@@ -0,0 +1,77 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#ifndef __M5_PY32IOEXPANDER_CLASS_H__
#define __M5_PY32IOEXPANDER_CLASS_H__
#include <M5Unified.hpp>
namespace m5 {
class PY32IOExpander_Class : public IOExpander_Base {
public:
static constexpr std::uint8_t DEFAULT_ADDRESS = 0x6F;
PY32IOExpander_Class(std::uint8_t i2c_addr = DEFAULT_ADDRESS, std::uint32_t freq = 100000,
m5::I2C_Class* i2c = &m5::In_I2C)
: IOExpander_Base(i2c_addr, freq, i2c)
{
}
bool begin();
// IOExpander_Base overrides
// false input, true output
void setDirection(uint8_t pin, bool direction) override;
void enablePull(uint8_t pin, bool enablePull) override;
// false down, true up
void setPullMode(uint8_t pin, bool mode) override;
// false push-pull, true open-drain
void setDriveMode(uint8_t pin, bool openDrain);
void setHighImpedance(uint8_t pin, bool enable) override;
bool getWriteValue(uint8_t pin) override;
void digitalWrite(uint8_t pin, bool level) override;
bool digitalRead(uint8_t pin) override;
void resetIrq() override;
void disableIrq() override;
void enableIrq() override;
// Extended functionality
uint16_t readDeviceUID();
uint8_t readVersion();
// ADC
// channel: 1-4
uint16_t analogRead(uint8_t channel);
// PWM
// channel: 0-3
void setPwmDuty(uint8_t channel, uint8_t duty);
void setPwmFrequency(uint16_t freq);
// LED
void setLedCount(uint8_t count);
void setLedColor(uint8_t index, uint16_t color565);
void setLedColor(uint8_t index, uint8_t r, uint8_t g, uint8_t b);
void setLedColor(uint8_t index, uint32_t color);
void setLedData(const uint8_t* data, size_t len);
void refreshLeds();
private:
void _writeBit(uint8_t reg_l, uint8_t reg_h, uint8_t pin, bool value);
bool _readBit(uint8_t reg_l, uint8_t reg_h, uint8_t pin);
};
} // namespace m5
#endif
+12
View File
@@ -0,0 +1,12 @@
idf_component_register(
SRCS
"src/SCS.cpp"
"src/SCSCL.cpp"
"src/SCSerial.cpp"
INCLUDE_DIRS
"src"
REQUIRES
driver
esp_timer
)

Some files were not shown because too many files have changed in this diff Show More