You've already forked M5Unit-EXTIO
mirror of
https://github.com/m5stack/M5Unit-EXTIO.git
synced 2026-05-20 11:36:04 -07:00
291 lines
8.4 KiB
C++
291 lines
8.4 KiB
C++
/*
|
|
* SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD
|
|
*
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
/*
|
|
Analog/Digital input example using M5UnitUnified for UnitExtIO2
|
|
|
|
Operation:
|
|
Click BtnA : Change input mode
|
|
*/
|
|
#include <M5Unified.h>
|
|
#include <M5UnitUnified.h>
|
|
#include <M5UnitUnifiedEXTIO.h>
|
|
#include <M5Utility.h>
|
|
#include <M5HAL.hpp>
|
|
|
|
using namespace m5::unit::extio2;
|
|
|
|
namespace {
|
|
auto& lcd = M5.Display;
|
|
m5::unit::UnitUnified Units;
|
|
m5::unit::UnitExtIO2 unit;
|
|
|
|
enum class Operation : uint8_t { Analog8, Analog12, Digital };
|
|
Operation& operator++(Operation& op)
|
|
{
|
|
op = static_cast<Operation>((m5::stl::to_underlying(op) + 1) % 3);
|
|
return op;
|
|
}
|
|
Operation operation{Operation::Analog8};
|
|
unsigned long latest{};
|
|
|
|
constexpr uint32_t interval{20}; // Interval for reading values (ms)
|
|
|
|
// Bits of the target pin (LSB:pin0 ... pin7:MSB)
|
|
// e.g.
|
|
// 0X01 Pin 0
|
|
// 0X9C Pin 2,3,4,7
|
|
// 0XFF All pins
|
|
constexpr uint8_t target_pins{0xFF};
|
|
static_assert(target_pins != 0, "Specify at least one");
|
|
|
|
// Layout parameters (set in setup based on screen size)
|
|
bool can_render{}; // false if no LCD or e-paper
|
|
bool small_screen{};
|
|
int16_t font_w{8}, font_h{16};
|
|
int16_t title_y{}, data_y{}, col_w{}, col_x0{};
|
|
|
|
void setup_layout()
|
|
{
|
|
can_render = (lcd.width() > 0 && lcd.height() > 0 && !lcd.isEPD());
|
|
if (!can_render) {
|
|
return;
|
|
}
|
|
small_screen = (lcd.width() < 200);
|
|
if (small_screen) {
|
|
lcd.setFont(&fonts::Font0);
|
|
font_w = 6;
|
|
font_h = 8;
|
|
} else {
|
|
lcd.setFont(&fonts::AsciiFont8x16);
|
|
font_w = 8;
|
|
font_h = 16;
|
|
}
|
|
title_y = 2;
|
|
data_y = title_y + font_h + 2;
|
|
col_x0 = 2;
|
|
col_w = lcd.width() / 2;
|
|
}
|
|
|
|
void change_operation(const Operation op)
|
|
{
|
|
latest = 0;
|
|
switch (op) {
|
|
case Operation::Digital:
|
|
unit.writePinBitsMode(target_pins, Mode::DigitalInput);
|
|
break;
|
|
case Operation::Analog8:
|
|
case Operation::Analog12:
|
|
unit.writePinBitsMode(target_pins, Mode::ADCInput);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if (!can_render) {
|
|
return;
|
|
}
|
|
lcd.fillScreen(TFT_BLACK);
|
|
lcd.setCursor(col_x0, title_y);
|
|
switch (op) {
|
|
case Operation::Digital:
|
|
lcd.printf("%s", small_screen ? "DIG IN" : "DIGITAL INPUT");
|
|
break;
|
|
case Operation::Analog8:
|
|
lcd.printf("%s", small_screen ? "ANA8" : "ANALOG 8 INPUT");
|
|
break;
|
|
case Operation::Analog12:
|
|
lcd.printf("%s", small_screen ? "ANA12" : "ANALOG 12 INPUT");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
using m5::unit::UnitExtIO2;
|
|
|
|
void render_status(const uint16_t v[UnitExtIO2::NUMBER_OF_PINS])
|
|
{
|
|
if (!can_render) {
|
|
return;
|
|
}
|
|
for (uint8_t pin = 0; pin < UnitExtIO2::NUMBER_OF_PINS; ++pin) {
|
|
if (unit.mode(pin) == Mode::ADCInput) {
|
|
int16_t x = col_x0 + col_w * (pin & 1);
|
|
int16_t y = data_y + font_h * (pin / 2);
|
|
lcd.setCursor(x, y);
|
|
if (small_screen) {
|
|
lcd.printf("%u:%4u", pin, v[pin]);
|
|
} else {
|
|
lcd.printf("Pin%u:%4u", pin, v[pin]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void render_status(const uint8_t high_bits)
|
|
{
|
|
if (!can_render) {
|
|
return;
|
|
}
|
|
for (uint8_t pin = 0; pin < UnitExtIO2::NUMBER_OF_PINS; ++pin) {
|
|
if (unit.mode(pin) == Mode::DigitalInput) {
|
|
int16_t x = col_x0 + col_w * (pin & 1);
|
|
int16_t y = data_y + font_h * (pin / 2);
|
|
bool h = (1U << pin) & high_bits;
|
|
lcd.setCursor(x, y);
|
|
if (small_screen) {
|
|
lcd.printf("%u:", pin);
|
|
lcd.setTextColor(h ? TFT_BLUE : TFT_YELLOW, TFT_BLACK);
|
|
lcd.printf("%s", h ? "H" : "L");
|
|
} else {
|
|
lcd.printf("Pin%u:", pin);
|
|
lcd.setCursor(x + font_w * 5, y);
|
|
lcd.setTextColor(h ? TFT_BLUE : TFT_YELLOW, TFT_BLACK);
|
|
lcd.printf("%s", h ? "HIGH" : "LOW ");
|
|
}
|
|
lcd.setTextColor(TFT_WHITE, TFT_BLACK);
|
|
}
|
|
}
|
|
}
|
|
|
|
void analog8_function()
|
|
{
|
|
uint16_t v[UnitExtIO2::NUMBER_OF_PINS]{};
|
|
auto at = m5::utility::millis();
|
|
if (!latest || at >= latest + interval) {
|
|
if (!unit.readPinBitsAnalogInput8(v, target_pins)) {
|
|
M5_LOGE("Failed to read");
|
|
return;
|
|
}
|
|
latest = at;
|
|
lcd.startWrite();
|
|
render_status(v);
|
|
lcd.endWrite();
|
|
M5.Log.printf("ANA8:%3u,%3u,%3u,%3u,%3u,%3u,%3u,%3u\n", v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7]);
|
|
}
|
|
}
|
|
|
|
void analog12_function()
|
|
{
|
|
uint16_t v[UnitExtIO2::NUMBER_OF_PINS]{};
|
|
|
|
auto at = m5::utility::millis();
|
|
if (!latest || at >= latest + interval) {
|
|
if (!unit.readPinBitsAnalogInput12(v, target_pins)) {
|
|
M5_LOGE("Failed to read");
|
|
return;
|
|
}
|
|
latest = at;
|
|
lcd.startWrite();
|
|
render_status(v);
|
|
lcd.endWrite();
|
|
M5.Log.printf("ANA12:%4u,%4u,%4u,%4u,%4u,%4u,%4u,%4u\n", v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7]);
|
|
}
|
|
}
|
|
|
|
void digital_function()
|
|
{
|
|
uint8_t high_bits{};
|
|
|
|
auto at = m5::utility::millis();
|
|
if (!latest || at >= latest + interval) {
|
|
if (!unit.readPinBitsDigitalInput(high_bits, target_pins)) {
|
|
M5_LOGE("Failed to read");
|
|
return;
|
|
}
|
|
latest = at;
|
|
lcd.startWrite();
|
|
render_status(high_bits);
|
|
lcd.endWrite();
|
|
M5.Log.printf("DIG:%X\n", high_bits);
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
void setup()
|
|
{
|
|
M5.begin();
|
|
M5.setTouchButtonHeightByRatio(100);
|
|
|
|
// The screen shall be in landscape mode
|
|
if (lcd.height() > lcd.width()) {
|
|
lcd.setRotation(1);
|
|
}
|
|
|
|
auto board = M5.getBoard();
|
|
|
|
// NessoN1: Arduino Wire (I2C_NUM_0) cannot be used for GROVE port.
|
|
// Wire is used by M5Unified In_I2C for internal devices (IOExpander etc.).
|
|
// Wire1 exists but is reserved for HatPort — cannot be used for GROVE.
|
|
// Reconfiguring Wire to GROVE pins breaks In_I2C, causing ESP_ERR_INVALID_STATE in M5.update().
|
|
// Solution: Use SoftwareI2C via M5HAL (bit-banging) for the GROVE port.
|
|
// NanoC6: Wire.begin() on GROVE pins conflicts with m5::I2C_Class registered by Ex_I2C.setPort()
|
|
// on the same I2C_NUM_0, causing sporadic NACK errors.
|
|
// Solution: Use M5.Ex_I2C (m5::I2C_Class) directly instead of Arduino Wire.
|
|
bool unit_ready{};
|
|
if (board == m5::board_t::board_ArduinoNessoN1) {
|
|
// NessoN1: GROVE is on port_b (GPIO 5/4), not port_a (which maps to Wire pins 8/10)
|
|
auto pin_num_sda = M5.getPin(m5::pin_name_t::port_b_out);
|
|
auto pin_num_scl = M5.getPin(m5::pin_name_t::port_b_in);
|
|
M5_LOGI("getPin(M5HAL): SDA:%u SCL:%u", pin_num_sda, pin_num_scl);
|
|
m5::hal::bus::I2CBusConfig i2c_cfg;
|
|
i2c_cfg.pin_sda = m5::hal::gpio::getPin(pin_num_sda);
|
|
i2c_cfg.pin_scl = m5::hal::gpio::getPin(pin_num_scl);
|
|
auto i2c_bus = m5::hal::bus::i2c::getBus(i2c_cfg);
|
|
M5_LOGI("Bus:%d", i2c_bus.has_value());
|
|
unit_ready = Units.add(unit, i2c_bus ? i2c_bus.value() : nullptr) && Units.begin();
|
|
} else if (board == m5::board_t::board_M5NanoC6) {
|
|
// NanoC6: Use M5.Ex_I2C (m5::I2C_Class, not Arduino Wire)
|
|
M5_LOGI("Using M5.Ex_I2C");
|
|
unit_ready = Units.add(unit, M5.Ex_I2C) && Units.begin();
|
|
} else {
|
|
auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda);
|
|
auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl);
|
|
M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl);
|
|
Wire.end();
|
|
Wire.begin(pin_num_sda, pin_num_scl, 100 * 1000U);
|
|
unit_ready = Units.add(unit, Wire) && Units.begin();
|
|
}
|
|
if (!unit_ready) {
|
|
M5_LOGE("Failed to begin");
|
|
lcd.fillScreen(TFT_RED);
|
|
while (true) {
|
|
m5::utility::delay(10000);
|
|
}
|
|
}
|
|
|
|
M5_LOGI("M5UnitUnified has been begun");
|
|
M5_LOGI("%s", Units.debugInfo().c_str());
|
|
|
|
setup_layout();
|
|
|
|
change_operation(operation);
|
|
}
|
|
|
|
void loop()
|
|
{
|
|
M5.update();
|
|
|
|
// Change ANA8 -> ANA12 -> DIG -> ANA8 ...
|
|
if (M5.BtnA.wasClicked()) {
|
|
change_operation(++operation);
|
|
return;
|
|
}
|
|
|
|
switch (operation) {
|
|
case Operation::Digital:
|
|
digital_function();
|
|
break;
|
|
case Operation::Analog8:
|
|
analog8_function();
|
|
break;
|
|
case Operation::Analog12:
|
|
analog12_function();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|