M5UnitUnified 0.6.3 git rev:e38fc73
Loading...
Searching...
No Matches
m5_unit_unified_wiring.hpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
3 *
4 * SPDX-License-Identifier: MIT
5 */
32#ifndef M5_UNIT_UNIFIED_WIRING_HPP
33#define M5_UNIT_UNIFIED_WIRING_HPP
34
35#include "../M5UnitUnified.hpp" // core (UnitUnified / Component, brings in M5HAL)
36#if defined(ARDUINO)
37#include <Wire.h>
38#include <SPI.h>
39#include <HardwareSerial.h>
40#elif defined(ESP_PLATFORM) // ESP-IDF native
41// driver/i2c_master.h (new I2C master driver) exists only on IDF >= 5.2. On IDF 5.0/5.1 fall back to
42// the legacy driver/i2c.h so the I2C wiring helpers keep working across the whole IDF >= 5.0 range.
43#if __has_include(<driver/i2c_master.h>)
44#include <driver/i2c_master.h>
45#else
46#include <driver/i2c.h>
47#endif
48#include <driver/uart.h>
49#include <driver/spi_master.h>
50#include <hal/gpio_types.h>
51#include <soc/soc_caps.h>
52#endif
53// NOTE: M5Unified.h / M5GFX are NOT included; caller must include them BEFORE this header.
54
55#if defined(ESP_PLATFORM)
56#include <esp_idf_version.h>
57// Number of HP I2C controllers. SOC_HP_I2C_NUM exists from ESP-IDF 5.3; before that SOC_I2C_NUM counts HP only.
58#if defined(SOC_HP_I2C_NUM)
59#define M5_UNIT_UNIFIED_WIRING_HP_I2C_NUM SOC_HP_I2C_NUM
60#else
61#define M5_UNIT_UNIFIED_WIRING_HP_I2C_NUM SOC_I2C_NUM
62#endif
63// Whether Arduino-ESP32 declares Wire1 (mirrors its Wire.h):
64// 2.x / 3.0: SOC_I2C_NUM > 1, 3.1: SOC_HP_I2C_NUM > 1, 3.2+ on ESP-IDF >= 5.4: SOC_I2C_NUM > 1 (LP I2C included)
65#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0)
66#define M5_UNIT_UNIFIED_WIRING_HAS_WIRE1 (SOC_I2C_NUM > 1)
67#else
68#define M5_UNIT_UNIFIED_WIRING_HAS_WIRE1 (M5_UNIT_UNIFIED_WIRING_HP_I2C_NUM > 1)
69#endif
70// Number of HP UARTs. SOC_UART_HP_NUM exists from ESP-IDF 5.2; before that SOC_UART_NUM counts HP only.
71// (SOC_UART_NUM includes the LP UART on C6 / C5 / P4, which cannot be routed to the GROVE ports.)
72#if defined(SOC_UART_HP_NUM)
73#define M5_UNIT_UNIFIED_WIRING_HP_UART_NUM SOC_UART_HP_NUM
74#else
75#define M5_UNIT_UNIFIED_WIRING_HP_UART_NUM SOC_UART_NUM
76#endif
77#endif
78
79namespace m5 {
80namespace unit {
81namespace wiring {
82
86enum class NessoPort { PortB, PortA };
87
89enum class GpioRole {
90 Both,
91 InOnly,
92 OutOnly
93};
94
96enum class UartConfig : uint32_t {
97 Default = 0,
98 _8N1 = 1,
99 _8N2 = 2,
100 _8E1 = 3,
101 _8O1 = 4,
102 _7N1 = 5,
103 _7E1 = 6,
104 _7O1 = 7,
105};
107
111struct I2CPins {
112 int8_t sda;
113 int8_t scl;
114 enum class Backend : uint8_t {
115 Wire,
117 ExI2C
118 } backend;
119};
120
123 int8_t rx;
124 int8_t tx;
126};
127
130 int8_t rx;
131 int8_t tx;
133};
134
136struct SpiPins {
137 int8_t sclk;
138 int8_t miso;
139 int8_t mosi;
140};
141
144 int8_t sda;
145 int8_t scl;
146 bool useWire1;
147};
148
151 int8_t rx;
152 int8_t tx;
153};
155
158#if defined(__M5UNIFIED_HPP__) // M5Unified.h must be included before this header
168inline I2CPins i2cPins(const NessoPort nesso = NessoPort::PortB)
169{
170 const auto board = M5.getBoard();
171 if (board == m5::board_t::board_ArduinoNessoN1) {
172 if (nesso == NessoPort::PortB) {
173 return {static_cast<int8_t>(M5.getPin(m5::pin_name_t::port_b_out)),
174 static_cast<int8_t>(M5.getPin(m5::pin_name_t::port_b_in)), I2CPins::Backend::SoftwareI2C};
175 }
176 return {static_cast<int8_t>(M5.getPin(m5::pin_name_t::port_a_sda)),
177 static_cast<int8_t>(M5.getPin(m5::pin_name_t::port_a_scl)), I2CPins::Backend::Wire};
178 }
179 if (board == m5::board_t::board_M5NanoC6 || board == m5::board_t::board_M5NanoH2) {
180 return {static_cast<int8_t>(M5.getPin(m5::pin_name_t::ex_i2c_sda)),
181 static_cast<int8_t>(M5.getPin(m5::pin_name_t::ex_i2c_scl)), I2CPins::Backend::ExI2C};
182 }
183 // In_I2C and Ex_I2C on the same pins are one bus that M5.begin() already installed via In_I2C
184 // (Core Basic/Gray/Go/Fire, ToughC5, CoreMatrix, PaperDIY...). Opening a new Wire / native master bus
185 // on it would collide, so borrow M5.Ex_I2C instead (ExI2C backend -> i2cClass).
186 const int8_t ex_sda = M5.getPin(m5::pin_name_t::ex_i2c_sda);
187 const int8_t ex_scl = M5.getPin(m5::pin_name_t::ex_i2c_scl);
188 if (ex_sda >= 0 && ex_scl >= 0 && ex_sda == M5.getPin(m5::pin_name_t::in_i2c_sda) &&
189 ex_scl == M5.getPin(m5::pin_name_t::in_i2c_scl)) {
190 return {ex_sda, ex_scl, I2CPins::Backend::ExI2C};
191 }
192 return {static_cast<int8_t>(M5.getPin(m5::pin_name_t::port_a_sda)),
193 static_cast<int8_t>(M5.getPin(m5::pin_name_t::port_a_scl)), I2CPins::Backend::Wire};
194}
195
203inline GpioPinPair gpioPins(const GpioRole role = GpioRole::Both)
204{
205 int rx = (role == GpioRole::OutOnly) ? -1 : M5.getPin(m5::pin_name_t::port_b_in);
206 int tx = (role == GpioRole::InOnly) ? -1 : M5.getPin(m5::pin_name_t::port_b_out);
207 const bool need_rx_fb = (role != GpioRole::OutOnly && rx < 0);
208 const bool need_tx_fb = (role != GpioRole::InOnly && tx < 0);
209 bool fb = false;
210 if (need_rx_fb || need_tx_fb) {
211 fb = true;
212 rx = (role == GpioRole::OutOnly) ? -1 : M5.getPin(m5::pin_name_t::port_a_pin1);
213 tx = (role == GpioRole::InOnly) ? -1 : M5.getPin(m5::pin_name_t::port_a_pin2);
214 }
215 return {static_cast<int8_t>(rx), static_cast<int8_t>(tx), fb};
216}
217
224inline UartPinPair uartPins()
225{
226 int rx = M5.getPin(m5::pin_name_t::port_c_rxd);
227 int tx = M5.getPin(m5::pin_name_t::port_c_txd);
228 bool fb = false;
229 if (rx < 0 || tx < 0) {
230 fb = true;
231 rx = M5.getPin(m5::pin_name_t::port_a_pin1);
232 tx = M5.getPin(m5::pin_name_t::port_a_pin2);
233 }
234 return {static_cast<int8_t>(rx), static_cast<int8_t>(tx), fb};
235}
236
241inline SpiPins spiPins()
242{
243 return {static_cast<int8_t>(M5.getPin(m5::pin_name_t::sd_spi_sclk)),
244 static_cast<int8_t>(M5.getPin(m5::pin_name_t::sd_spi_miso)),
245 static_cast<int8_t>(M5.getPin(m5::pin_name_t::sd_spi_mosi))};
246}
247
252inline HatI2CPins hatI2CPins()
253{
254 const auto board = M5.getBoard();
255 switch (board) {
256 case m5::board_t::board_M5StickC:
257 case m5::board_t::board_M5StickCPlus:
258 case m5::board_t::board_M5StickCPlus2:
259 return {0, 26, false};
260 case m5::board_t::board_M5StickS3:
261 // SCL is GPIO0 (strapping pin): a Hat that pulls it to its own unpowered supply at power-on (e.g. Hat DAC)
262 // makes the board boot into download mode
263 return {8, 0, false};
264 case m5::board_t::board_M5StackCoreInk:
265 return {25, 26, false};
266 case m5::board_t::board_ArduinoNessoN1:
267 return {6, 7, true}; // useWire1
268 default:
269 return {-1, -1, false};
270 }
271}
272
277inline HatPinPair hatGPIOPins()
278{
279 const auto board = M5.getBoard();
280 switch (board) {
281 case m5::board_t::board_M5StickC:
282 case m5::board_t::board_M5StickCPlus:
283 case m5::board_t::board_M5StickCPlus2:
284 return {26, 0};
285 case m5::board_t::board_M5StickS3:
286 return {0, 8};
287 case m5::board_t::board_M5StackCoreInk:
288 return {26, 25};
289 case m5::board_t::board_ArduinoNessoN1:
290 return {7, 6};
291 default:
292 return {-1, -1};
293 }
294}
295
299inline HatPinPair hatUARTPins()
300{
301 const auto board = M5.getBoard();
302 switch (board) {
303 case m5::board_t::board_M5StickCPlus:
304 case m5::board_t::board_M5StickCPlus2:
305 return {26, 0};
306 case m5::board_t::board_M5StickS3:
307 return {0, 8};
308 case m5::board_t::board_M5StackCoreInk:
309 return {26, 25};
310 case m5::board_t::board_ArduinoNessoN1:
311 return {7, 6};
312 default:
313 return {-1, -1};
314 }
315}
316#endif // __M5UNIFIED_HPP__
318
321#if defined(ARDUINO)
323inline bool i2cWire(UnitUnified& units, Component& unit, TwoWire& wire, const int sda, const int scl,
324 const uint32_t clock)
325{
326 M5_LIB_LOGI("wiring: I2C(Wire) sda=%d scl=%d clock=%lu", sda, scl, (unsigned long)clock);
327 wire.end();
328 wire.begin(sda, scl, clock);
329 return units.add(unit, wire);
330}
331#endif
332
333#if defined(__M5_I2C_CLASS_H__)
335inline bool i2cClass(UnitUnified& units, Component& unit, m5::I2C_Class& i2c)
336{
337 M5_LIB_LOGI("wiring: I2C(I2C_Class)");
338 i2c.begin();
339 return units.add(unit, i2c);
340}
341#endif
342
343#if defined(M5_HAL_HPP) && defined(ESP_PLATFORM)
345inline bool i2cSoftware(UnitUnified& units, Component& unit, const int sda, const int scl)
346{
347 M5_LIB_LOGI("wiring: I2C(Software/M5HAL) sda=%d scl=%d", sda, scl);
348 m5::hal::bus::I2CBusConfig cfg;
349 cfg.pin_sda = m5::hal::gpio::getPin(sda);
350 cfg.pin_scl = m5::hal::gpio::getPin(scl);
351 auto bus = m5::hal::bus::i2c::getBus(cfg);
352 return units.add(unit, bus ? bus.value() : nullptr);
353}
354#endif
356
359#if defined(ARDUINO)
365inline bool addI2C(UnitUnified& units, Component& unit, const uint32_t clock = 0,
366 const NessoPort nesso = NessoPort::PortB)
367{
368 // clock == 0: use the unit's own configured clock; clock > 0: override it. Either way the unit's
369 // component_config().clock is the single source of truth applied per transaction by every backend.
370 if (clock != 0) {
371 auto cfg = unit.component_config();
372 cfg.clock = clock;
373 unit.component_config(cfg);
374 }
375 const uint32_t eff_clock = unit.component_config().clock;
376 const auto p = i2cPins(nesso);
377 M5_LIB_LOGI("wiring: addI2C board=0x%02x nesso=%d backend=%d sda=%d scl=%d clock=%lu", (int)M5.getBoard(),
378 (int)nesso, (int)p.backend, (int)p.sda, (int)p.scl, (unsigned long)eff_clock);
379 switch (p.backend) {
381 return i2cSoftware(units, unit, p.sda, p.scl);
383 return i2cWire(units, unit, Wire, p.sda, p.scl, eff_clock);
385 return i2cClass(units, unit, M5.Ex_I2C);
386 }
387 return false;
388}
389
396inline bool addGPIO(UnitUnified& units, Component& unit, const GpioRole role = GpioRole::Both)
397{
398 const auto p = gpioPins(role);
399 if (p.fallback_a) {
400 Wire.end();
401 }
402 M5_LIB_LOGI("wiring: GPIO rx=%d tx=%d role=%d fallback_a=%d", (int)p.rx, (int)p.tx, (int)role, (int)p.fallback_a);
403 return units.add(unit, p.rx, p.tx);
404}
405
407inline uint32_t toArduinoSerialConfig(const UartConfig c)
408{
409 switch (c) {
410 case UartConfig::_8N2:
411 return SERIAL_8N2;
412 case UartConfig::_8E1:
413 return SERIAL_8E1;
414 case UartConfig::_8O1:
415 return SERIAL_8O1;
416 case UartConfig::_7N1:
417 return SERIAL_7N1;
418 case UartConfig::_7E1:
419 return SERIAL_7E1;
420 case UartConfig::_7O1:
421 return SERIAL_7O1;
422 case UartConfig::Default:
423 case UartConfig::_8N1:
424 default:
425 return SERIAL_8N1;
426 }
427}
428
430inline HardwareSerial& defaultUartSerial()
431{
432#if M5_UNIT_UNIFIED_WIRING_HP_UART_NUM > 2
433 return Serial2;
434#elif M5_UNIT_UNIFIED_WIRING_HP_UART_NUM > 1
435 return Serial1;
436#else
437#error "Not enough Serial"
438#endif
439}
440
449inline bool addUART(UnitUnified& units, Component& unit, const uint32_t baud = 115200,
450 const UartConfig config = UartConfig::Default)
451{
452 HardwareSerial& serial = defaultUartSerial();
453 const uint32_t cfg = toArduinoSerialConfig(config);
454 const auto p = uartPins();
455 if (p.fallback_a) {
456 const auto b = M5.getBoard();
457 if (b == m5::board_t::board_M5NanoC6 || b == m5::board_t::board_M5NanoH2) {
458 // M5.begin() calls Ex_I2C.setPort() but NOT begin(). m5gfx::i2c::release() is gated
459 // by the internal `initialized` flag, so release() alone is a no-op. Workaround:
460 // begin() flips initialized=true, then release() actually detaches + restores pins.
461 M5.Ex_I2C.begin();
462 M5.Ex_I2C.release();
463 }
464 }
465 M5_LIB_LOGI("wiring: UART rx=%d tx=%d baud=%lu config=0x%lx fallback_a=%d", (int)p.rx, (int)p.tx,
466 (unsigned long)baud, (unsigned long)cfg, (int)p.fallback_a);
467 serial.end();
468 serial.begin(baud, cfg, p.rx, p.tx);
469 return units.add(unit, serial);
470}
471
473inline bool spiBus(UnitUnified& units, Component& unit, SPIClass& spi, const SPISettings& settings)
474{
475 M5_LIB_LOGI("wiring: SPI");
476 return units.add(unit, spi, settings);
477}
478
486inline bool addSPI(UnitUnified& units, Component& unit, const uint32_t clock_hz, const uint8_t mode = 0,
487 const uint8_t bit_order = 0)
488{
489 SPISettings settings{clock_hz, static_cast<uint8_t>((bit_order == 0) ? MSBFIRST : LSBFIRST), mode};
490 const auto p = spiPins();
491 M5_LIB_LOGI("wiring: addSPI sclk=%d miso=%d mosi=%d clock=%lu mode=%u bit_order=%u", (int)p.sclk, (int)p.miso,
492 (int)p.mosi, (unsigned long)clock_hz, (unsigned)mode, (unsigned)bit_order);
493 if (!SPI.bus()) {
494 SPI.begin(p.sclk, p.miso, p.mosi);
495 }
496 return spiBus(units, unit, SPI, settings);
497}
498
500
506inline bool addHatI2C(UnitUnified& units, Component& unit, const uint32_t clock = 0)
507{
508 const auto p = hatI2CPins();
509 if (p.sda < 0 || p.scl < 0) {
510 M5_LIB_LOGE("wiring: Hat I2C unsupported board=0x%02x", (int)M5.getBoard());
511 return false;
512 }
513 // clock == 0: use the unit's own configured clock; clock > 0: override it.
514 if (clock != 0) {
515 auto cfg = unit.component_config();
516 cfg.clock = clock;
517 unit.component_config(cfg);
518 }
519 const uint32_t eff_clock = unit.component_config().clock;
520#if M5_UNIT_UNIFIED_WIRING_HAS_WIRE1
521 TwoWire& wire = p.useWire1 ? Wire1 : Wire;
522#else
523 // Arduino-ESP32 declares Wire only (e.g. ESP32-C3); Wire1 is absent.
524 if (p.useWire1) {
525 M5_LIB_LOGE("wiring: addHatI2C NessoN1 Hat needs Wire1, but Wire1 is not available");
526 return false;
527 }
528 TwoWire& wire = Wire;
529#endif
530 M5_LIB_LOGI("wiring: addHatI2C board=0x%02x sda=%d scl=%d clock=%lu wire=%s", (int)M5.getBoard(), (int)p.sda,
531 (int)p.scl, (unsigned long)eff_clock, p.useWire1 ? "Wire1" : "Wire");
532 return i2cWire(units, unit, wire, p.sda, p.scl, eff_clock);
533}
534
539inline bool addHatGPIO(UnitUnified& units, Component& unit, const GpioRole role = GpioRole::Both)
540{
541 auto p = hatGPIOPins();
542 if (p.rx < 0 || p.tx < 0) {
543 M5_LIB_LOGE("wiring: Hat GPIO unsupported board=0x%02x", (int)M5.getBoard());
544 return false;
545 }
546 if (role == GpioRole::OutOnly) p.rx = -1;
547 if (role == GpioRole::InOnly) p.tx = -1;
548 M5_LIB_LOGI("wiring: addHatGPIO board=0x%02x rx=%d tx=%d role=%d", (int)M5.getBoard(), (int)p.rx, (int)p.tx,
549 (int)role);
550 return units.add(unit, p.rx, p.tx);
551}
552
559inline bool addHatUART(UnitUnified& units, Component& unit, const uint32_t baud = 115200,
560 const UartConfig config = UartConfig::Default)
561{
562 HardwareSerial& serial = defaultUartSerial();
563 const uint32_t cfg = toArduinoSerialConfig(config);
564 const auto p = hatUARTPins();
565 if (p.rx < 0 || p.tx < 0) {
566 M5_LIB_LOGE("wiring: Hat UART unsupported board=0x%02x", (int)M5.getBoard());
567 return false;
568 }
569 M5_LIB_LOGI("wiring: addHatUART board=0x%02x rx=%d tx=%d baud=%lu config=0x%lx", (int)M5.getBoard(), (int)p.rx,
570 (int)p.tx, (unsigned long)baud, (unsigned long)cfg);
571 serial.end();
572 serial.begin(baud, cfg, p.rx, p.tx);
573 return units.add(unit, serial);
574}
575#endif // ARDUINO
576
579#if !defined(ARDUINO) && defined(ESP_PLATFORM)
580
582inline uart_port_t defaultUartPort()
583{
584#if M5_UNIT_UNIFIED_WIRING_HP_UART_NUM > 2
585 return UART_NUM_2;
586#elif M5_UNIT_UNIFIED_WIRING_HP_UART_NUM > 1
587 return UART_NUM_1;
588#else
589#error "Not enough UART"
590#endif
591}
592
594struct IdfUartParams {
595 uart_word_length_t word_length;
596 uart_parity_t parity;
597 uart_stop_bits_t stop_bits;
598};
599inline IdfUartParams toIdfUartParams(const UartConfig c)
600{
601 switch (c) {
602 case UartConfig::_8N2:
603 return {UART_DATA_8_BITS, UART_PARITY_DISABLE, UART_STOP_BITS_2};
604 case UartConfig::_8E1:
605 return {UART_DATA_8_BITS, UART_PARITY_EVEN, UART_STOP_BITS_1};
606 case UartConfig::_8O1:
607 return {UART_DATA_8_BITS, UART_PARITY_ODD, UART_STOP_BITS_1};
608 case UartConfig::_7N1:
609 return {UART_DATA_7_BITS, UART_PARITY_DISABLE, UART_STOP_BITS_1};
610 case UartConfig::_7E1:
611 return {UART_DATA_7_BITS, UART_PARITY_EVEN, UART_STOP_BITS_1};
612 case UartConfig::_7O1:
613 return {UART_DATA_7_BITS, UART_PARITY_ODD, UART_STOP_BITS_1};
614 case UartConfig::Default:
615 case UartConfig::_8N1:
616 default:
617 return {UART_DATA_8_BITS, UART_PARITY_DISABLE, UART_STOP_BITS_1};
618 }
619}
620
621namespace detail {
622
623constexpr size_t kI2CBusCacheSize = 4;
624constexpr size_t kUARTPortCacheSize = 4;
625constexpr size_t kSPIHostCacheSize = 2; // SPI2_HOST / SPI3_HOST
626constexpr size_t kSPIDevCacheSize = 4;
627
628#if __has_include(<driver/i2c_master.h>)
629struct I2CCacheEntry {
630 uint32_t key;
631 i2c_master_bus_handle_t handle;
632};
633#endif
634
635struct UARTCacheEntry {
636 uint32_t key;
637 uart_port_t port;
638};
639
640struct SPIHostEntry {
641 spi_host_device_t host;
642 bool initialized;
643};
644
645struct SPIDevEntry {
646 uint32_t key;
647 spi_device_handle_t handle;
648};
649
650#if __has_include(<driver/i2c_master.h>)
652inline i2c_master_bus_handle_t ensureI2CBus(const i2c_port_t port, const gpio_num_t sda, const gpio_num_t scl,
653 const uint32_t /*clock - not used to key, single bus per pins*/)
654{
655 const uint32_t key = (static_cast<uint32_t>(static_cast<uint8_t>(port)) << 24) |
656 (static_cast<uint32_t>(static_cast<uint8_t>(sda)) << 16) |
657 (static_cast<uint32_t>(static_cast<uint8_t>(scl)) << 8);
658 static I2CCacheEntry cache[kI2CBusCacheSize]{};
659 static size_t count{};
660 for (size_t i = 0; i < count; ++i) {
661 if (cache[i].key == key) return cache[i].handle;
662 }
663 if (count >= kI2CBusCacheSize) {
664 M5_LIB_LOGE("wiring: I2C bus cache full (max %zu)", kI2CBusCacheSize);
665 return nullptr;
666 }
667 i2c_master_bus_config_t cfg{};
668 cfg.i2c_port = port;
669 cfg.sda_io_num = sda;
670 cfg.scl_io_num = scl;
671#if SOC_LP_I2C_SUPPORTED
672 if (port == LP_I2C_NUM_0) {
673#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
674 // LP I2C uses RTC_FAST clock (union with clk_source, mutually exclusive).
675 cfg.lp_source_clk = LP_I2C_SCLK_DEFAULT;
676#else
677 // i2c_master_bus_config_t has no lp_source_clk before ESP-IDF 5.3; LP I2C cannot be opened
678 M5_LIB_LOGE("wiring: LP I2C needs ESP-IDF >= 5.3");
679 return nullptr;
680#endif
681 } else
682#endif
683 {
684 cfg.clk_source = I2C_CLK_SRC_DEFAULT;
685 }
686 cfg.glitch_ignore_cnt = 7;
687 cfg.flags.enable_internal_pullup = true;
688 if (i2c_new_master_bus(&cfg, &cache[count].handle) != ESP_OK) {
689 M5_LIB_LOGE("wiring: i2c_new_master_bus failed port=%d sda=%d scl=%d", (int)port, (int)sda, (int)scl);
690 return nullptr;
691 }
692 cache[count].key = key;
693 return cache[count++].handle;
694}
695#else // legacy driver/i2c.h (IDF 5.0 / 5.1)
697inline bool ensureI2CLegacyDriver(const i2c_port_t port, const gpio_num_t sda, const gpio_num_t scl,
698 const uint32_t clock)
699{
700 const uint32_t key = (static_cast<uint32_t>(static_cast<uint8_t>(port)) << 24) |
701 (static_cast<uint32_t>(static_cast<uint8_t>(sda)) << 16) |
702 (static_cast<uint32_t>(static_cast<uint8_t>(scl)) << 8);
703 static uint32_t cache[kI2CBusCacheSize]{};
704 static size_t count{};
705 for (size_t i = 0; i < count; ++i) {
706 if (cache[i] == key) return true;
707 }
708 if (count >= kI2CBusCacheSize) {
709 M5_LIB_LOGE("wiring: I2C bus cache full (max %zu)", kI2CBusCacheSize);
710 return false;
711 }
712 // i2c_driver_install returns ESP_FAIL for several distinct reasons in IDF 5.0/5.1 (already
713 // installed by another owner / OOM / slave-setup failure). Probe i2c_get_period to disambiguate:
714 // it writes high/low only when the driver object exists (p_i2c_obj != NULL), so high>0 && low>0
715 // means the port is genuinely installed and can be borrowed.
716 const esp_err_t install_err = i2c_driver_install(port, I2C_MODE_MASTER, 0, 0, 0);
717 if (install_err == ESP_OK) {
718 // Freshly installed by us: configure pins and timing.
719 i2c_config_t conf{};
720 conf.mode = I2C_MODE_MASTER;
721 conf.sda_io_num = sda;
722 conf.scl_io_num = scl;
723 conf.sda_pullup_en = true;
724 conf.scl_pullup_en = true;
725 conf.master.clk_speed = clock;
726 if (i2c_param_config(port, &conf) != ESP_OK) {
727 M5_LIB_LOGE("wiring: legacy i2c param_config failed port=%d sda=%d scl=%d", (int)port, (int)sda, (int)scl);
728 i2c_driver_delete(port);
729 return false;
730 }
731 M5_LIB_LOGI("wiring: legacy i2c INSTALLED(new) port=%d sda=%d scl=%d clock=%u", (int)port, (int)sda, (int)scl,
732 (unsigned)clock);
733 } else if (install_err == ESP_FAIL) {
734 // Already installed by another owner -> borrow it (keep the owner's pins/timing; do NOT
735 // re-run i2c_param_config and override them). Confirm the driver really exists first.
736 int high = 0, low = 0;
737 i2c_get_period(port, &high, &low);
738 if (!(high > 0 && low > 0)) {
739 M5_LIB_LOGE("wiring: legacy i2c driver install failed port=%d sda=%d scl=%d", (int)port, (int)sda,
740 (int)scl);
741 return false;
742 }
743 M5_LIB_LOGI("wiring: legacy i2c BORROW(already) port=%d sda=%d scl=%d high=%d low=%d", (int)port, (int)sda,
744 (int)scl, high, low);
745 } else {
746 M5_LIB_LOGE("wiring: legacy i2c driver install error=0x%x port=%d sda=%d scl=%d", (int)install_err, (int)port,
747 (int)sda, (int)scl);
748 return false;
749 }
750 cache[count++] = key;
751 return true;
752}
753#endif
754
756inline uart_port_t ensureUARTPort(const uart_port_t port, const gpio_num_t rx, const gpio_num_t tx, const uint32_t baud,
757 const UartConfig config)
758{
759 const uint32_t key = (static_cast<uint32_t>(static_cast<uint8_t>(port)) << 24) |
760 (static_cast<uint32_t>(static_cast<uint8_t>(rx)) << 16) |
761 (static_cast<uint32_t>(static_cast<uint8_t>(tx)) << 8);
762 static UARTCacheEntry cache[kUARTPortCacheSize]{};
763 static size_t count{};
764 for (size_t i = 0; i < count; ++i) {
765 if (cache[i].key == key) return cache[i].port;
766 }
767 if (count >= kUARTPortCacheSize) {
768 M5_LIB_LOGE("wiring: UART port cache full (max %zu)", kUARTPortCacheSize);
769 return UART_NUM_MAX;
770 }
771 if (uart_is_driver_installed(port)) {
772 cache[count] = {key, port};
773 return cache[count++].port;
774 }
775 const auto p = toIdfUartParams(config);
776 uart_config_t uc{};
777 uc.baud_rate = static_cast<int>(baud);
778 uc.data_bits = p.word_length;
779 uc.parity = p.parity;
780 uc.stop_bits = p.stop_bits;
781 uc.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
782 uc.source_clk = UART_SCLK_DEFAULT;
783 if (uart_param_config(port, &uc) != ESP_OK ||
784 uart_set_pin(port, tx, rx, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE) != ESP_OK ||
785 uart_driver_install(port, 256, 0, 0, nullptr, 0) != ESP_OK) {
786 M5_LIB_LOGE("wiring: uart setup failed port=%d", (int)port);
787 return UART_NUM_MAX;
788 }
789 cache[count] = {key, port};
790 return cache[count++].port;
791}
792
795inline spi_device_handle_t ensureSPIDevice(const spi_host_device_t host, const gpio_num_t mosi, const gpio_num_t miso,
796 const gpio_num_t sck, const uint32_t clock_hz, const uint8_t mode,
797 const uint8_t bit_order)
798{
799 static SPIHostEntry host_cache[kSPIHostCacheSize]{};
800 static SPIDevEntry dev_cache[kSPIDevCacheSize]{};
801 static size_t host_count{};
802 static size_t dev_count{};
803
804 bool host_init = false;
805 for (size_t i = 0; i < host_count; ++i) {
806 if (host_cache[i].host == host) {
807 host_init = host_cache[i].initialized;
808 break;
809 }
810 }
811 if (!host_init) {
812 spi_bus_config_t bc{};
813 bc.mosi_io_num = mosi;
814 bc.miso_io_num = miso;
815 bc.sclk_io_num = sck;
816 bc.quadwp_io_num = -1;
817 bc.quadhd_io_num = -1;
818 bc.max_transfer_sz = 4096;
819 if (spi_bus_initialize(host, &bc, SPI_DMA_CH_AUTO) != ESP_OK) {
820 M5_LIB_LOGE("wiring: spi_bus_initialize failed host=%d", (int)host);
821 return nullptr;
822 }
823 if (host_count < kSPIHostCacheSize) host_cache[host_count++] = {host, true};
824 }
825
826 const uint32_t key = (static_cast<uint32_t>(static_cast<uint8_t>(host)) << 24) |
827 (static_cast<uint32_t>(clock_hz / 1000U) << 8) | (static_cast<uint32_t>(mode) << 4) |
828 static_cast<uint32_t>(bit_order);
829 for (size_t i = 0; i < dev_count; ++i) {
830 if (dev_cache[i].key == key) return dev_cache[i].handle;
831 }
832 if (dev_count >= kSPIDevCacheSize) {
833 M5_LIB_LOGE("wiring: SPI device cache full (max %zu)", kSPIDevCacheSize);
834 return nullptr;
835 }
836 spi_device_interface_config_t dc{};
837 dc.clock_speed_hz = static_cast<int>(clock_hz);
838 dc.mode = mode;
839 dc.spics_io_num = GPIO_NUM_NC; // manual CS control by adapter
840 dc.queue_size = 1;
841 if (bit_order == 1) dc.flags |= SPI_DEVICE_BIT_LSBFIRST;
842 if (spi_bus_add_device(host, &dc, &dev_cache[dev_count].handle) != ESP_OK) {
843 M5_LIB_LOGE("wiring: spi_bus_add_device failed host=%d", (int)host);
844 return nullptr;
845 }
846 dev_cache[dev_count].key = key;
847 return dev_cache[dev_count++].handle;
848}
849
850} // namespace detail
851
852#if __has_include(<driver/i2c_master.h>)
858inline i2c_master_bus_handle_t i2cBusHandle(const i2c_port_t port, const gpio_num_t sda, const gpio_num_t scl,
859 const uint32_t clock = 100000)
860{
861 return detail::ensureI2CBus(port, sda, scl, clock);
862}
863#endif
864
869inline uart_port_t uartPortHandle(const uart_port_t port, const gpio_num_t rx, const gpio_num_t tx, const uint32_t baud,
870 const UartConfig config = UartConfig::Default)
871{
872 return detail::ensureUARTPort(port, rx, tx, baud, config);
873}
874
880inline spi_device_handle_t spiDeviceHandle(const spi_host_device_t host, const gpio_num_t mosi, const gpio_num_t miso,
881 const gpio_num_t sck, const uint32_t clock_hz, const uint8_t mode = 0,
882 const uint8_t bit_order = 0)
883{
884 return detail::ensureSPIDevice(host, mosi, miso, sck, clock_hz, mode, bit_order);
885}
886
887//
888// === ESP-IDF native generic helpers (Phase C) — same signatures as Arduino ===
889//
890
891#if defined(__M5UNIFIED_HPP__)
900inline bool addI2C(UnitUnified& units, Component& unit, const uint32_t clock = 0,
901 const NessoPort nesso = NessoPort::PortB)
902{
903 // clock == 0: use the unit's own configured clock; clock > 0: override it. component_config().clock
904 // is the single source of truth applied per transaction (Wire / I2C_Class / master device / legacy).
905 if (clock != 0) {
906 auto cfg = unit.component_config();
907 cfg.clock = clock;
908 unit.component_config(cfg);
909 }
910 const uint32_t eff_clock = unit.component_config().clock;
911 const auto pins = i2cPins(nesso);
912 const auto board = M5.getBoard();
913 M5_LIB_LOGI("wiring(ESP-IDF): addI2C board=0x%02x nesso=%d sda=%d scl=%d clock=%u backend=%d", (int)board,
914 (int)nesso, (int)pins.sda, (int)pins.scl, (unsigned)eff_clock, (int)pins.backend);
915 switch (pins.backend) {
917#if defined(M5_HAL_HPP)
918 return i2cSoftware(units, unit, pins.sda, pins.scl);
919#else
920 M5_LIB_LOGE("wiring: SoftwareI2C backend requires M5HAL");
921 return false;
922#endif
924 if (board == m5::board_t::board_ArduinoNessoN1) {
925 // NessoN1 PortA (QWIIC) -> Wire == M5.In_I2C (NessoN1 maps Wire to In_I2C).
926 // M5Unified already holds I2C_NUM_0 here, so borrow instead of installing.
927 return i2cClass(units, unit, M5.In_I2C);
928 } else {
929 // Stick / S3 etc: M5Unified does not call Wire.begin(), so install ourselves
930 // (Core is routed to ExI2C by i2cPins() and never reaches this branch)
931#if __has_include(<driver/i2c_master.h>)
932 auto bus = i2cBusHandle(I2C_NUM_0, (gpio_num_t)pins.sda, (gpio_num_t)pins.scl, eff_clock);
933 if (!bus) return false;
934 return units.add(unit, bus, I2C_NUM_0, (gpio_num_t)pins.sda, (gpio_num_t)pins.scl);
935#else // legacy driver (IDF 5.0 / 5.1): install legacy driver, then add by port
936 if (!detail::ensureI2CLegacyDriver(I2C_NUM_0, (gpio_num_t)pins.sda, (gpio_num_t)pins.scl, eff_clock)) {
937 return false;
938 }
939 return units.add(unit, I2C_NUM_0, (gpio_num_t)pins.sda, (gpio_num_t)pins.scl);
940#endif
941 }
943 // Core / NanoC6 / NanoH2: M5Unified manages Ex_I2C (Core shares it with In_I2C) -> borrow
944 return i2cClass(units, unit, M5.Ex_I2C);
945 }
946 return false;
947}
948
950inline bool addGPIO(UnitUnified& units, Component& unit, const GpioRole role = GpioRole::Both)
951{
952 auto p = gpioPins(role);
953 if (role == GpioRole::OutOnly) p.rx = -1;
954 if (role == GpioRole::InOnly) p.tx = -1;
955 M5_LIB_LOGI("wiring(ESP-IDF): GPIO rx=%d tx=%d role=%d fallback_a=%d", (int)p.rx, (int)p.tx, (int)role,
956 (int)p.fallback_a);
957 return units.add(unit, p.rx, p.tx);
958}
959
961inline bool addUART(UnitUnified& units, Component& unit, const uint32_t baud = 115200,
962 const UartConfig config = UartConfig::Default)
963{
964 const auto pins = uartPins();
965 M5_LIB_LOGI("wiring(ESP-IDF): UART rx=%d tx=%d baud=%u fallback_a=%d", (int)pins.rx, (int)pins.tx, (unsigned)baud,
966 (int)pins.fallback_a);
967 auto port = uartPortHandle(defaultUartPort(), (gpio_num_t)pins.rx, (gpio_num_t)pins.tx, baud, config);
968 if (port == UART_NUM_MAX) return false;
969 return units.add(unit, port);
970}
971
973inline bool addSPI(UnitUnified& units, Component& unit, const uint32_t clock_hz, const uint8_t mode = 0,
974 const uint8_t bit_order = 0)
975{
976 const auto pins = spiPins();
977 M5_LIB_LOGI("wiring(ESP-IDF): addSPI sclk=%d miso=%d mosi=%d clock=%u mode=%u bit_order=%u", (int)pins.sclk,
978 (int)pins.miso, (int)pins.mosi, (unsigned)clock_hz, (unsigned)mode, (unsigned)bit_order);
979 auto dev = spiDeviceHandle(SPI2_HOST, (gpio_num_t)pins.mosi, (gpio_num_t)pins.miso, (gpio_num_t)pins.sclk, clock_hz,
980 mode, bit_order);
981 if (!dev) return false;
982 return units.add(unit, dev); // cs omitted -> Component::assign uses address() as CS
983}
984
986inline bool addHatI2C(UnitUnified& units, Component& unit, const uint32_t clock = 0)
987{
988 const auto p = hatI2CPins();
989 if (p.sda < 0 || p.scl < 0) {
990 M5_LIB_LOGE("wiring: Hat I2C unsupported board=0x%02x", (int)M5.getBoard());
991 return false;
992 }
993 // clock == 0: use the unit's own configured clock; clock > 0: override it.
994 if (clock != 0) {
995 auto cfg = unit.component_config();
996 cfg.clock = clock;
997 unit.component_config(cfg);
998 }
999 const uint32_t eff_clock = unit.component_config().clock;
1000 i2c_port_t port;
1001 if (p.useWire1) {
1002#if M5_UNIT_UNIFIED_WIRING_HP_I2C_NUM >= 2
1003 port = I2C_NUM_1; // HP I2C #1 (ESP32 / S2 / S3 / P4 / H2 etc.)
1004#elif SOC_LP_I2C_SUPPORTED
1005 port = LP_I2C_NUM_0; // C6: 2nd I2C is LP (= Arduino Wire1)
1006#else
1007 M5_LIB_LOGE("wiring: addHatI2C needs a 2nd I2C port, none available board=0x%02x", (int)M5.getBoard());
1008 return false;
1009#endif
1010 } else {
1011 port = I2C_NUM_0;
1012 }
1013 M5_LIB_LOGI("wiring(ESP-IDF): addHatI2C port=%d sda=%d scl=%d clock=%u", (int)port, (int)p.sda, (int)p.scl,
1014 (unsigned)eff_clock);
1015#if __has_include(<driver/i2c_master.h>)
1016 auto bus = i2cBusHandle(port, (gpio_num_t)p.sda, (gpio_num_t)p.scl, eff_clock);
1017 if (!bus) return false;
1018 return units.add(unit, bus, port, (gpio_num_t)p.sda, (gpio_num_t)p.scl);
1019#else // legacy driver (IDF 5.0 / 5.1)
1020 if (!detail::ensureI2CLegacyDriver(port, (gpio_num_t)p.sda, (gpio_num_t)p.scl, eff_clock)) return false;
1021 return units.add(unit, port, (gpio_num_t)p.sda, (gpio_num_t)p.scl);
1022#endif
1023}
1024
1026inline bool addHatGPIO(UnitUnified& units, Component& unit, const GpioRole role = GpioRole::Both)
1027{
1028 auto p = hatGPIOPins();
1029 if (p.rx < 0 || p.tx < 0) {
1030 M5_LIB_LOGE("wiring: Hat GPIO unsupported board=0x%02x", (int)M5.getBoard());
1031 return false;
1032 }
1033 if (role == GpioRole::OutOnly) p.rx = -1;
1034 if (role == GpioRole::InOnly) p.tx = -1;
1035 M5_LIB_LOGI("wiring(ESP-IDF): addHatGPIO board=0x%02x rx=%d tx=%d role=%d", (int)M5.getBoard(), (int)p.rx,
1036 (int)p.tx, (int)role);
1037 return units.add(unit, p.rx, p.tx);
1038}
1039
1041inline bool addHatUART(UnitUnified& units, Component& unit, const uint32_t baud = 115200,
1042 const UartConfig config = UartConfig::Default)
1043{
1044 const auto p = hatUARTPins();
1045 if (p.rx < 0 || p.tx < 0) {
1046 M5_LIB_LOGE("wiring: Hat UART unsupported board=0x%02x", (int)M5.getBoard());
1047 return false;
1048 }
1049 M5_LIB_LOGI("wiring(ESP-IDF): addHatUART board=0x%02x rx=%d tx=%d baud=%u", (int)M5.getBoard(), (int)p.rx,
1050 (int)p.tx, (unsigned)baud);
1051 auto port = uartPortHandle(defaultUartPort(), (gpio_num_t)p.rx, (gpio_num_t)p.tx, baud, config);
1052 if (port == UART_NUM_MAX) return false;
1053 return units.add(unit, port);
1054}
1055#endif // __M5UNIFIED_HPP__
1056
1057#endif // !ARDUINO && ESP_PLATFORM
1059
1062#if defined(__M5UNIFIED_HPP__)
1064[[noreturn]] inline void failStop()
1065{
1066 M5.Display.fillScreen(0xF800 /* TFT_RED */);
1067 while (true) {
1068 m5::utility::delay(10000);
1069 }
1070}
1071#endif
1073
1074} // namespace wiring
1075} // namespace unit
1076} // namespace m5
1077#endif // M5_UNIT_UNIFIED_WIRING_HPP
Main header of M5UnitUnified.
UartConfig
UART config (word length / parity / stop bits). Common across Arduino and ESP-IDF native.
Definition m5_unit_unified_wiring.hpp:96
GpioRole
GPIO usage role: how many pins of the port the unit actually uses.
Definition m5_unit_unified_wiring.hpp:89
@ Both
Both input and output pins (e.g., IR Tx+Rx)
@ OutOnly
Output only — rx = -1, tx = port_*_out (pin2) (e.g., Flashlight, RF433 Tx, IR Tx alone)
@ InOnly
Input only — rx = port_*_in (pin1), tx = -1 (e.g., RF433 Rx, IR Rx alone)
NessoPort
NessoN1 connection choice: PortB = direct GROVE (SoftwareI2C), PortA = QWIIC / PbHub (Wire)
Definition m5_unit_unified_wiring.hpp:86
Top level namespace of M5Stack.
Definition test_helper.hpp:20
Unit-related namespace.
GPIO rx/tx pin pair; fallback_a indicates port_a fallback was used.
Definition m5_unit_unified_wiring.hpp:122
bool fallback_a
True when PortB unavailable and PortA was chosen.
Definition m5_unit_unified_wiring.hpp:125
Hat I2C pin pair + bus selection; sda=scl=-1 if board has no Hat header.
Definition m5_unit_unified_wiring.hpp:143
bool useWire1
True if the Hat uses Wire1 (NessoN1); else use Wire.
Definition m5_unit_unified_wiring.hpp:146
Hat rx/tx pin pair (shared by Hat GPIO and Hat UART); rx=tx=-1 if board has no Hat header.
Definition m5_unit_unified_wiring.hpp:150
I2C backend selection + pin numbers (pure getter result, no side effects)
Definition m5_unit_unified_wiring.hpp:111
Backend
Definition m5_unit_unified_wiring.hpp:114
@ Wire
Arduino TwoWire on (sda, scl)
@ ExI2C
M5.Ex_I2C (pin numbers are for reference; M5Unified manages them)
@ SoftwareI2C
M5HAL SoftwareI2C on (sda, scl) — NessoN1 PortB.
SPI pin set (sclk / miso / mosi) on the board's shared SD/SPI bus.
Definition m5_unit_unified_wiring.hpp:136
UART rx/tx pin pair; fallback_a indicates port_a fallback was used.
Definition m5_unit_unified_wiring.hpp:129
bool fallback_a
True when PortC unavailable and PortA was chosen.
Definition m5_unit_unified_wiring.hpp:132