M5UnitUnified 0.5.5 git rev:bf711f3
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#else // 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
55namespace m5 {
56namespace unit {
57namespace wiring {
58
62enum class NessoPort { PortB, PortA };
63
65enum class GpioRole {
66 Both,
67 InOnly,
68 OutOnly
69};
70
72enum class UartConfig : uint32_t {
73 Default = 0,
74 _8N1 = 1,
75 _8N2 = 2,
76 _8E1 = 3,
77 _8O1 = 4,
78 _7N1 = 5,
79 _7E1 = 6,
80 _7O1 = 7,
81};
83
87struct I2CPins {
88 int8_t sda;
89 int8_t scl;
90 enum class Backend : uint8_t {
91 Wire,
93 ExI2C
94 } backend;
95};
96
99 int8_t rx;
100 int8_t tx;
102};
103
106 int8_t rx;
107 int8_t tx;
109};
110
112struct SpiPins {
113 int8_t sclk;
114 int8_t miso;
115 int8_t mosi;
116};
117
120 int8_t sda;
121 int8_t scl;
122 bool useWire1;
123};
124
127 int8_t rx;
128 int8_t tx;
129};
131
134#if defined(__M5UNIFIED_HPP__) // M5Unified.h must be included before this header
143inline I2CPins i2cPins(const NessoPort nesso = NessoPort::PortB)
144{
145 const auto board = M5.getBoard();
146 if (board == m5::board_t::board_ArduinoNessoN1) {
147 if (nesso == NessoPort::PortB) {
148 return {static_cast<int8_t>(M5.getPin(m5::pin_name_t::port_b_out)),
149 static_cast<int8_t>(M5.getPin(m5::pin_name_t::port_b_in)), I2CPins::Backend::SoftwareI2C};
150 }
151 return {static_cast<int8_t>(M5.getPin(m5::pin_name_t::port_a_sda)),
152 static_cast<int8_t>(M5.getPin(m5::pin_name_t::port_a_scl)), I2CPins::Backend::Wire};
153 }
154 if (board == m5::board_t::board_M5NanoC6 || board == m5::board_t::board_M5NanoH2) {
155 return {static_cast<int8_t>(M5.getPin(m5::pin_name_t::ex_i2c_sda)),
156 static_cast<int8_t>(M5.getPin(m5::pin_name_t::ex_i2c_scl)), I2CPins::Backend::ExI2C};
157 }
158 if (board == m5::board_t::board_M5Stack) {
159 // Core (Basic/Gray/Go/Fire): In_I2C and Ex_I2C are the same bus (I2C_NUM_0 / GPIO21,22).
160 // M5.begin() already installs that bus via In_I2C, so opening a new Wire / native master bus
161 // on the same port would collide. Borrow M5.Ex_I2C instead (ExI2C backend -> i2cClass).
162 return {static_cast<int8_t>(M5.getPin(m5::pin_name_t::ex_i2c_sda)),
163 static_cast<int8_t>(M5.getPin(m5::pin_name_t::ex_i2c_scl)), I2CPins::Backend::ExI2C};
164 }
165 return {static_cast<int8_t>(M5.getPin(m5::pin_name_t::port_a_sda)),
166 static_cast<int8_t>(M5.getPin(m5::pin_name_t::port_a_scl)), I2CPins::Backend::Wire};
167}
168
176inline GpioPinPair gpioPins(const GpioRole role = GpioRole::Both)
177{
178 int rx = (role == GpioRole::OutOnly) ? -1 : M5.getPin(m5::pin_name_t::port_b_in);
179 int tx = (role == GpioRole::InOnly) ? -1 : M5.getPin(m5::pin_name_t::port_b_out);
180 const bool need_rx_fb = (role != GpioRole::OutOnly && rx < 0);
181 const bool need_tx_fb = (role != GpioRole::InOnly && tx < 0);
182 bool fb = false;
183 if (need_rx_fb || need_tx_fb) {
184 fb = true;
185 rx = (role == GpioRole::OutOnly) ? -1 : M5.getPin(m5::pin_name_t::port_a_pin1);
186 tx = (role == GpioRole::InOnly) ? -1 : M5.getPin(m5::pin_name_t::port_a_pin2);
187 }
188 return {static_cast<int8_t>(rx), static_cast<int8_t>(tx), fb};
189}
190
197inline UartPinPair uartPins()
198{
199 int rx = M5.getPin(m5::pin_name_t::port_c_rxd);
200 int tx = M5.getPin(m5::pin_name_t::port_c_txd);
201 bool fb = false;
202 if (rx < 0 || tx < 0) {
203 fb = true;
204 rx = M5.getPin(m5::pin_name_t::port_a_pin1);
205 tx = M5.getPin(m5::pin_name_t::port_a_pin2);
206 }
207 return {static_cast<int8_t>(rx), static_cast<int8_t>(tx), fb};
208}
209
214inline SpiPins spiPins()
215{
216 return {static_cast<int8_t>(M5.getPin(m5::pin_name_t::sd_spi_sclk)),
217 static_cast<int8_t>(M5.getPin(m5::pin_name_t::sd_spi_miso)),
218 static_cast<int8_t>(M5.getPin(m5::pin_name_t::sd_spi_mosi))};
219}
220
225inline HatI2CPins hatI2CPins()
226{
227 const auto board = M5.getBoard();
228 switch (board) {
229 case m5::board_t::board_M5StickC:
230 case m5::board_t::board_M5StickCPlus:
231 case m5::board_t::board_M5StickCPlus2:
232 return {0, 26, false};
233 case m5::board_t::board_M5StickS3:
234 return {8, 0, false};
235 case m5::board_t::board_M5StackCoreInk:
236 return {25, 26, false};
237 case m5::board_t::board_ArduinoNessoN1:
238 return {6, 7, true}; // useWire1
239 default:
240 return {-1, -1, false};
241 }
242}
243
248inline HatPinPair hatGPIOPins()
249{
250 const auto board = M5.getBoard();
251 switch (board) {
252 case m5::board_t::board_M5StickC:
253 case m5::board_t::board_M5StickCPlus:
254 case m5::board_t::board_M5StickCPlus2:
255 return {26, 0};
256 case m5::board_t::board_M5StickS3:
257 return {0, 8};
258 case m5::board_t::board_M5StackCoreInk:
259 return {26, 25};
260 case m5::board_t::board_ArduinoNessoN1:
261 return {7, 6};
262 default:
263 return {-1, -1};
264 }
265}
266
270inline HatPinPair hatUARTPins()
271{
272 const auto board = M5.getBoard();
273 switch (board) {
274 case m5::board_t::board_M5StickCPlus:
275 case m5::board_t::board_M5StickCPlus2:
276 return {26, 0};
277 case m5::board_t::board_M5StickS3:
278 return {0, 8};
279 case m5::board_t::board_M5StackCoreInk:
280 return {26, 25};
281 case m5::board_t::board_ArduinoNessoN1:
282 return {7, 6};
283 default:
284 return {-1, -1};
285 }
286}
287#endif // __M5UNIFIED_HPP__
289
292#if defined(ARDUINO)
294inline bool i2cWire(UnitUnified& units, Component& unit, TwoWire& wire, const int sda, const int scl,
295 const uint32_t clock)
296{
297 M5_LIB_LOGI("wiring: I2C(Wire) sda=%d scl=%d clock=%lu", sda, scl, (unsigned long)clock);
298 wire.end();
299 wire.begin(sda, scl, clock);
300 return units.add(unit, wire);
301}
302#endif
303
304#if defined(__M5_I2C_CLASS_H__)
306inline bool i2cClass(UnitUnified& units, Component& unit, m5::I2C_Class& i2c)
307{
308 M5_LIB_LOGI("wiring: I2C(I2C_Class)");
309 i2c.begin();
310 return units.add(unit, i2c);
311}
312#endif
313
314#if defined(M5_HAL_HPP)
316inline bool i2cSoftware(UnitUnified& units, Component& unit, const int sda, const int scl)
317{
318 M5_LIB_LOGI("wiring: I2C(Software/M5HAL) sda=%d scl=%d", sda, scl);
319 m5::hal::bus::I2CBusConfig cfg;
320 cfg.pin_sda = m5::hal::gpio::getPin(sda);
321 cfg.pin_scl = m5::hal::gpio::getPin(scl);
322 auto bus = m5::hal::bus::i2c::getBus(cfg);
323 return units.add(unit, bus ? bus.value() : nullptr);
324}
325#endif
327
330#if defined(ARDUINO)
336inline bool addI2C(UnitUnified& units, Component& unit, const uint32_t clock = 0,
337 const NessoPort nesso = NessoPort::PortB)
338{
339 // clock == 0: use the unit's own configured clock; clock > 0: override it. Either way the unit's
340 // component_config().clock is the single source of truth applied per transaction by every backend.
341 if (clock != 0) {
342 auto cfg = unit.component_config();
343 cfg.clock = clock;
344 unit.component_config(cfg);
345 }
346 const uint32_t eff_clock = unit.component_config().clock;
347 const auto p = i2cPins(nesso);
348 M5_LIB_LOGI("wiring: addI2C board=0x%02x nesso=%d backend=%d sda=%d scl=%d clock=%lu", (int)M5.getBoard(),
349 (int)nesso, (int)p.backend, (int)p.sda, (int)p.scl, (unsigned long)eff_clock);
350 switch (p.backend) {
352 return i2cSoftware(units, unit, p.sda, p.scl);
354 return i2cWire(units, unit, Wire, p.sda, p.scl, eff_clock);
356 return i2cClass(units, unit, M5.Ex_I2C);
357 }
358 return false;
359}
360
367inline bool addGPIO(UnitUnified& units, Component& unit, const GpioRole role = GpioRole::Both)
368{
369 const auto p = gpioPins(role);
370 if (p.fallback_a) {
371 Wire.end();
372 }
373 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);
374 return units.add(unit, p.rx, p.tx);
375}
376
378inline uint32_t toArduinoSerialConfig(const UartConfig c)
379{
380 switch (c) {
381 case UartConfig::_8N2:
382 return SERIAL_8N2;
383 case UartConfig::_8E1:
384 return SERIAL_8E1;
385 case UartConfig::_8O1:
386 return SERIAL_8O1;
387 case UartConfig::_7N1:
388 return SERIAL_7N1;
389 case UartConfig::_7E1:
390 return SERIAL_7E1;
391 case UartConfig::_7O1:
392 return SERIAL_7O1;
393 case UartConfig::Default:
394 case UartConfig::_8N1:
395 default:
396 return SERIAL_8N1;
397 }
398}
399
401inline HardwareSerial& defaultUartSerial()
402{
403#if defined(CONFIG_IDF_TARGET_ESP32C6)
404 return Serial1;
405#elif SOC_UART_NUM > 2
406 return Serial2;
407#elif SOC_UART_NUM > 1
408 return Serial1;
409#else
410#error "Not enough Serial"
411#endif
412}
413
422inline bool addUART(UnitUnified& units, Component& unit, const uint32_t baud = 115200,
423 const UartConfig config = UartConfig::Default)
424{
425 HardwareSerial& serial = defaultUartSerial();
426 const uint32_t cfg = toArduinoSerialConfig(config);
427 const auto p = uartPins();
428 if (p.fallback_a) {
429 const auto b = M5.getBoard();
430 if (b == m5::board_t::board_M5NanoC6 || b == m5::board_t::board_M5NanoH2) {
431 // M5.begin() calls Ex_I2C.setPort() but NOT begin(). m5gfx::i2c::release() is gated
432 // by the internal `initialized` flag, so release() alone is a no-op. Workaround:
433 // begin() flips initialized=true, then release() actually detaches + restores pins.
434 M5.Ex_I2C.begin();
435 M5.Ex_I2C.release();
436 }
437 }
438 M5_LIB_LOGI("wiring: UART rx=%d tx=%d baud=%lu config=0x%lx fallback_a=%d", (int)p.rx, (int)p.tx,
439 (unsigned long)baud, (unsigned long)cfg, (int)p.fallback_a);
440 serial.end();
441 serial.begin(baud, cfg, p.rx, p.tx);
442 return units.add(unit, serial);
443}
444
446inline bool spiBus(UnitUnified& units, Component& unit, SPIClass& spi, const SPISettings& settings)
447{
448 M5_LIB_LOGI("wiring: SPI");
449 return units.add(unit, spi, settings);
450}
451
459inline bool addSPI(UnitUnified& units, Component& unit, const uint32_t clock_hz, const uint8_t mode = 0,
460 const uint8_t bit_order = 0)
461{
462 SPISettings settings{clock_hz, static_cast<uint8_t>((bit_order == 0) ? MSBFIRST : LSBFIRST), mode};
463 const auto p = spiPins();
464 M5_LIB_LOGI("wiring: addSPI sclk=%d miso=%d mosi=%d clock=%lu mode=%u bit_order=%u", (int)p.sclk, (int)p.miso,
465 (int)p.mosi, (unsigned long)clock_hz, (unsigned)mode, (unsigned)bit_order);
466 if (!SPI.bus()) {
467 SPI.begin(p.sclk, p.miso, p.mosi);
468 }
469 return spiBus(units, unit, SPI, settings);
470}
471
473
479inline bool addHatI2C(UnitUnified& units, Component& unit, const uint32_t clock = 0)
480{
481 const auto p = hatI2CPins();
482 if (p.sda < 0 || p.scl < 0) {
483 M5_LIB_LOGE("wiring: Hat I2C unsupported board=0x%02x", (int)M5.getBoard());
484 return false;
485 }
486 // clock == 0: use the unit's own configured clock; clock > 0: override it.
487 if (clock != 0) {
488 auto cfg = unit.component_config();
489 cfg.clock = clock;
490 unit.component_config(cfg);
491 }
492 const uint32_t eff_clock = unit.component_config().clock;
493#if SOC_I2C_NUM > 1
494 TwoWire& wire = p.useWire1 ? Wire1 : Wire;
495#else
496 // SOC_I2C_NUM == 1 (ESP32-C3/C6/H2): Arduino-ESP32 declares Wire only; Wire1 is absent.
497 if (p.useWire1) {
498 M5_LIB_LOGE("wiring: addHatI2C NessoN1 Hat needs Wire1, but SOC_I2C_NUM==1");
499 return false;
500 }
501 TwoWire& wire = Wire;
502#endif
503 M5_LIB_LOGI("wiring: addHatI2C board=0x%02x sda=%d scl=%d clock=%lu wire=%s", (int)M5.getBoard(), (int)p.sda,
504 (int)p.scl, (unsigned long)eff_clock, p.useWire1 ? "Wire1" : "Wire");
505 return i2cWire(units, unit, wire, p.sda, p.scl, eff_clock);
506}
507
512inline bool addHatGPIO(UnitUnified& units, Component& unit, const GpioRole role = GpioRole::Both)
513{
514 auto p = hatGPIOPins();
515 if (p.rx < 0 || p.tx < 0) {
516 M5_LIB_LOGE("wiring: Hat GPIO unsupported board=0x%02x", (int)M5.getBoard());
517 return false;
518 }
519 if (role == GpioRole::OutOnly) p.rx = -1;
520 if (role == GpioRole::InOnly) p.tx = -1;
521 M5_LIB_LOGI("wiring: addHatGPIO board=0x%02x rx=%d tx=%d role=%d", (int)M5.getBoard(), (int)p.rx, (int)p.tx,
522 (int)role);
523 return units.add(unit, p.rx, p.tx);
524}
525
532inline bool addHatUART(UnitUnified& units, Component& unit, const uint32_t baud = 115200,
533 const UartConfig config = UartConfig::Default)
534{
535 HardwareSerial& serial = defaultUartSerial();
536 const uint32_t cfg = toArduinoSerialConfig(config);
537 const auto p = hatUARTPins();
538 if (p.rx < 0 || p.tx < 0) {
539 M5_LIB_LOGE("wiring: Hat UART unsupported board=0x%02x", (int)M5.getBoard());
540 return false;
541 }
542 M5_LIB_LOGI("wiring: addHatUART board=0x%02x rx=%d tx=%d baud=%lu config=0x%lx", (int)M5.getBoard(), (int)p.rx,
543 (int)p.tx, (unsigned long)baud, (unsigned long)cfg);
544 serial.end();
545 serial.begin(baud, cfg, p.rx, p.tx);
546 return units.add(unit, serial);
547}
548#endif // ARDUINO
549
552#if !defined(ARDUINO)
553
555inline uart_port_t defaultUartPort()
556{
557#if defined(CONFIG_IDF_TARGET_ESP32C6)
558 return UART_NUM_1;
559#elif SOC_UART_NUM > 2
560 return UART_NUM_2;
561#elif SOC_UART_NUM > 1
562 return UART_NUM_1;
563#else
564#error "Not enough UART"
565#endif
566}
567
570 uart_word_length_t word_length;
571 uart_parity_t parity;
572 uart_stop_bits_t stop_bits;
573};
575{
576 switch (c) {
577 case UartConfig::_8N2:
578 return {UART_DATA_8_BITS, UART_PARITY_DISABLE, UART_STOP_BITS_2};
579 case UartConfig::_8E1:
580 return {UART_DATA_8_BITS, UART_PARITY_EVEN, UART_STOP_BITS_1};
581 case UartConfig::_8O1:
582 return {UART_DATA_8_BITS, UART_PARITY_ODD, UART_STOP_BITS_1};
583 case UartConfig::_7N1:
584 return {UART_DATA_7_BITS, UART_PARITY_DISABLE, UART_STOP_BITS_1};
585 case UartConfig::_7E1:
586 return {UART_DATA_7_BITS, UART_PARITY_EVEN, UART_STOP_BITS_1};
587 case UartConfig::_7O1:
588 return {UART_DATA_7_BITS, UART_PARITY_ODD, UART_STOP_BITS_1};
589 case UartConfig::Default:
590 case UartConfig::_8N1:
591 default:
592 return {UART_DATA_8_BITS, UART_PARITY_DISABLE, UART_STOP_BITS_1};
593 }
594}
595
596namespace detail {
597
598constexpr size_t kI2CBusCacheSize = 4;
599constexpr size_t kUARTPortCacheSize = 4;
600constexpr size_t kSPIHostCacheSize = 2; // SPI2_HOST / SPI3_HOST
601constexpr size_t kSPIDevCacheSize = 4;
602
603#if __has_include(<driver/i2c_master.h>)
604struct I2CCacheEntry {
605 uint32_t key;
606 i2c_master_bus_handle_t handle;
607};
608#endif
609
611 uint32_t key;
612 uart_port_t port;
613};
614
616 spi_host_device_t host;
617 bool initialized;
618};
619
621 uint32_t key;
622 spi_device_handle_t handle;
623};
624
625#if __has_include(<driver/i2c_master.h>)
627inline i2c_master_bus_handle_t ensureI2CBus(const i2c_port_t port, const gpio_num_t sda, const gpio_num_t scl,
628 const uint32_t /*clock - not used to key, single bus per pins*/)
629{
630 const uint32_t key = (static_cast<uint32_t>(static_cast<uint8_t>(port)) << 24) |
631 (static_cast<uint32_t>(static_cast<uint8_t>(sda)) << 16) |
632 (static_cast<uint32_t>(static_cast<uint8_t>(scl)) << 8);
633 static I2CCacheEntry cache[kI2CBusCacheSize]{};
634 static size_t count{};
635 for (size_t i = 0; i < count; ++i) {
636 if (cache[i].key == key) return cache[i].handle;
637 }
638 if (count >= kI2CBusCacheSize) {
639 M5_LIB_LOGE("wiring: I2C bus cache full (max %zu)", kI2CBusCacheSize);
640 return nullptr;
641 }
642 i2c_master_bus_config_t cfg{};
643 cfg.i2c_port = port;
644 cfg.sda_io_num = sda;
645 cfg.scl_io_num = scl;
646#if SOC_LP_I2C_SUPPORTED
647 if (port == LP_I2C_NUM_0) {
648 // LP I2C uses RTC_FAST clock (union with clk_source, mutually exclusive).
649 cfg.lp_source_clk = LP_I2C_SCLK_DEFAULT;
650 } else
651#endif
652 {
653 cfg.clk_source = I2C_CLK_SRC_DEFAULT;
654 }
655 cfg.glitch_ignore_cnt = 7;
656 cfg.flags.enable_internal_pullup = true;
657 if (i2c_new_master_bus(&cfg, &cache[count].handle) != ESP_OK) {
658 M5_LIB_LOGE("wiring: i2c_new_master_bus failed port=%d sda=%d scl=%d", (int)port, (int)sda, (int)scl);
659 return nullptr;
660 }
661 cache[count].key = key;
662 return cache[count++].handle;
663}
664#else // legacy driver/i2c.h (IDF 5.0 / 5.1)
666inline bool ensureI2CLegacyDriver(const i2c_port_t port, const gpio_num_t sda, const gpio_num_t scl,
667 const uint32_t clock)
668{
669 const uint32_t key = (static_cast<uint32_t>(static_cast<uint8_t>(port)) << 24) |
670 (static_cast<uint32_t>(static_cast<uint8_t>(sda)) << 16) |
671 (static_cast<uint32_t>(static_cast<uint8_t>(scl)) << 8);
672 static uint32_t cache[kI2CBusCacheSize]{};
673 static size_t count{};
674 for (size_t i = 0; i < count; ++i) {
675 if (cache[i] == key) return true;
676 }
677 if (count >= kI2CBusCacheSize) {
678 M5_LIB_LOGE("wiring: I2C bus cache full (max %zu)", kI2CBusCacheSize);
679 return false;
680 }
681 // i2c_driver_install returns ESP_FAIL for several distinct reasons in IDF 5.0/5.1 (already
682 // installed by another owner / OOM / slave-setup failure). Probe i2c_get_period to disambiguate:
683 // it writes high/low only when the driver object exists (p_i2c_obj != NULL), so high>0 && low>0
684 // means the port is genuinely installed and can be borrowed.
685 const esp_err_t install_err = i2c_driver_install(port, I2C_MODE_MASTER, 0, 0, 0);
686 if (install_err == ESP_OK) {
687 // Freshly installed by us: configure pins and timing.
688 i2c_config_t conf{};
689 conf.mode = I2C_MODE_MASTER;
690 conf.sda_io_num = sda;
691 conf.scl_io_num = scl;
692 conf.sda_pullup_en = true;
693 conf.scl_pullup_en = true;
694 conf.master.clk_speed = clock;
695 if (i2c_param_config(port, &conf) != ESP_OK) {
696 M5_LIB_LOGE("wiring: legacy i2c param_config failed port=%d sda=%d scl=%d", (int)port, (int)sda, (int)scl);
697 i2c_driver_delete(port);
698 return false;
699 }
700 M5_LIB_LOGI("wiring: legacy i2c INSTALLED(new) port=%d sda=%d scl=%d clock=%u", (int)port, (int)sda, (int)scl,
701 (unsigned)clock);
702 } else if (install_err == ESP_FAIL) {
703 // Already installed by another owner -> borrow it (keep the owner's pins/timing; do NOT
704 // re-run i2c_param_config and override them). Confirm the driver really exists first.
705 int high = 0, low = 0;
706 i2c_get_period(port, &high, &low);
707 if (!(high > 0 && low > 0)) {
708 M5_LIB_LOGE("wiring: legacy i2c driver install failed port=%d sda=%d scl=%d", (int)port, (int)sda,
709 (int)scl);
710 return false;
711 }
712 M5_LIB_LOGI("wiring: legacy i2c BORROW(already) port=%d sda=%d scl=%d high=%d low=%d", (int)port, (int)sda,
713 (int)scl, high, low);
714 } else {
715 M5_LIB_LOGE("wiring: legacy i2c driver install error=0x%x port=%d sda=%d scl=%d", (int)install_err, (int)port,
716 (int)sda, (int)scl);
717 return false;
718 }
719 cache[count++] = key;
720 return true;
721}
722#endif
723
725inline uart_port_t ensureUARTPort(const uart_port_t port, const gpio_num_t rx, const gpio_num_t tx, const uint32_t baud,
726 const UartConfig config)
727{
728 const uint32_t key = (static_cast<uint32_t>(static_cast<uint8_t>(port)) << 24) |
729 (static_cast<uint32_t>(static_cast<uint8_t>(rx)) << 16) |
730 (static_cast<uint32_t>(static_cast<uint8_t>(tx)) << 8);
731 static UARTCacheEntry cache[kUARTPortCacheSize]{};
732 static size_t count{};
733 for (size_t i = 0; i < count; ++i) {
734 if (cache[i].key == key) return cache[i].port;
735 }
736 if (count >= kUARTPortCacheSize) {
737 M5_LIB_LOGE("wiring: UART port cache full (max %zu)", kUARTPortCacheSize);
738 return UART_NUM_MAX;
739 }
740 if (uart_is_driver_installed(port)) {
741 cache[count] = {key, port};
742 return cache[count++].port;
743 }
744 const auto p = toIdfUartParams(config);
745 uart_config_t uc{};
746 uc.baud_rate = static_cast<int>(baud);
747 uc.data_bits = p.word_length;
748 uc.parity = p.parity;
749 uc.stop_bits = p.stop_bits;
750 uc.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
751 uc.source_clk = UART_SCLK_DEFAULT;
752 if (uart_param_config(port, &uc) != ESP_OK ||
753 uart_set_pin(port, tx, rx, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE) != ESP_OK ||
754 uart_driver_install(port, 256, 0, 0, nullptr, 0) != ESP_OK) {
755 M5_LIB_LOGE("wiring: uart setup failed port=%d", (int)port);
756 return UART_NUM_MAX;
757 }
758 cache[count] = {key, port};
759 return cache[count++].port;
760}
761
764inline spi_device_handle_t ensureSPIDevice(const spi_host_device_t host, const gpio_num_t mosi, const gpio_num_t miso,
765 const gpio_num_t sck, const uint32_t clock_hz, const uint8_t mode,
766 const uint8_t bit_order)
767{
768 static SPIHostEntry host_cache[kSPIHostCacheSize]{};
769 static SPIDevEntry dev_cache[kSPIDevCacheSize]{};
770 static size_t host_count{};
771 static size_t dev_count{};
772
773 bool host_init = false;
774 for (size_t i = 0; i < host_count; ++i) {
775 if (host_cache[i].host == host) {
776 host_init = host_cache[i].initialized;
777 break;
778 }
779 }
780 if (!host_init) {
781 spi_bus_config_t bc{};
782 bc.mosi_io_num = mosi;
783 bc.miso_io_num = miso;
784 bc.sclk_io_num = sck;
785 bc.quadwp_io_num = -1;
786 bc.quadhd_io_num = -1;
787 bc.max_transfer_sz = 4096;
788 if (spi_bus_initialize(host, &bc, SPI_DMA_CH_AUTO) != ESP_OK) {
789 M5_LIB_LOGE("wiring: spi_bus_initialize failed host=%d", (int)host);
790 return nullptr;
791 }
792 if (host_count < kSPIHostCacheSize) host_cache[host_count++] = {host, true};
793 }
794
795 const uint32_t key = (static_cast<uint32_t>(static_cast<uint8_t>(host)) << 24) |
796 (static_cast<uint32_t>(clock_hz / 1000U) << 8) | (static_cast<uint32_t>(mode) << 4) |
797 static_cast<uint32_t>(bit_order);
798 for (size_t i = 0; i < dev_count; ++i) {
799 if (dev_cache[i].key == key) return dev_cache[i].handle;
800 }
801 if (dev_count >= kSPIDevCacheSize) {
802 M5_LIB_LOGE("wiring: SPI device cache full (max %zu)", kSPIDevCacheSize);
803 return nullptr;
804 }
805 spi_device_interface_config_t dc{};
806 dc.clock_speed_hz = static_cast<int>(clock_hz);
807 dc.mode = mode;
808 dc.spics_io_num = GPIO_NUM_NC; // manual CS control by adapter
809 dc.queue_size = 1;
810 if (bit_order == 1) dc.flags |= SPI_DEVICE_BIT_LSBFIRST;
811 if (spi_bus_add_device(host, &dc, &dev_cache[dev_count].handle) != ESP_OK) {
812 M5_LIB_LOGE("wiring: spi_bus_add_device failed host=%d", (int)host);
813 return nullptr;
814 }
815 dev_cache[dev_count].key = key;
816 return dev_cache[dev_count++].handle;
817}
818
819} // namespace detail
820
821#if __has_include(<driver/i2c_master.h>)
827inline i2c_master_bus_handle_t i2cBusHandle(const i2c_port_t port, const gpio_num_t sda, const gpio_num_t scl,
828 const uint32_t clock = 100000)
829{
830 return detail::ensureI2CBus(port, sda, scl, clock);
831}
832#endif
833
838inline uart_port_t uartPortHandle(const uart_port_t port, const gpio_num_t rx, const gpio_num_t tx, const uint32_t baud,
839 const UartConfig config = UartConfig::Default)
840{
841 return detail::ensureUARTPort(port, rx, tx, baud, config);
842}
843
849inline spi_device_handle_t spiDeviceHandle(const spi_host_device_t host, const gpio_num_t mosi, const gpio_num_t miso,
850 const gpio_num_t sck, const uint32_t clock_hz, const uint8_t mode = 0,
851 const uint8_t bit_order = 0)
852{
853 return detail::ensureSPIDevice(host, mosi, miso, sck, clock_hz, mode, bit_order);
854}
855
856//
857// === ESP-IDF native generic helpers (Phase C) — same signatures as Arduino ===
858//
859
860#if defined(__M5UNIFIED_HPP__)
869inline bool addI2C(UnitUnified& units, Component& unit, const uint32_t clock = 0,
870 const NessoPort nesso = NessoPort::PortB)
871{
872 // clock == 0: use the unit's own configured clock; clock > 0: override it. component_config().clock
873 // is the single source of truth applied per transaction (Wire / I2C_Class / master device / legacy).
874 if (clock != 0) {
875 auto cfg = unit.component_config();
876 cfg.clock = clock;
877 unit.component_config(cfg);
878 }
879 const uint32_t eff_clock = unit.component_config().clock;
880 const auto pins = i2cPins(nesso);
881 const auto board = M5.getBoard();
882 M5_LIB_LOGI("wiring(ESP-IDF): addI2C board=0x%02x nesso=%d sda=%d scl=%d clock=%u backend=%d", (int)board,
883 (int)nesso, (int)pins.sda, (int)pins.scl, (unsigned)eff_clock, (int)pins.backend);
884 switch (pins.backend) {
886#if defined(M5_HAL_HPP)
887 return i2cSoftware(units, unit, pins.sda, pins.scl);
888#else
889 M5_LIB_LOGE("wiring: SoftwareI2C backend requires M5HAL");
890 return false;
891#endif
893 if (board == m5::board_t::board_ArduinoNessoN1) {
894 // NessoN1 PortA (QWIIC) -> Wire == M5.In_I2C (NessoN1 maps Wire to In_I2C).
895 // M5Unified already holds I2C_NUM_0 here, so borrow instead of installing.
896 return i2cClass(units, unit, M5.In_I2C);
897 } else {
898 // Stick / S3 etc: M5Unified does not call Wire.begin(), so install ourselves
899 // (Core is routed to ExI2C by i2cPins() and never reaches this branch)
900#if __has_include(<driver/i2c_master.h>)
901 auto bus = i2cBusHandle(I2C_NUM_0, (gpio_num_t)pins.sda, (gpio_num_t)pins.scl, eff_clock);
902 if (!bus) return false;
903 return units.add(unit, bus);
904#else // legacy driver (IDF 5.0 / 5.1): install legacy driver, then add by port
905 if (!detail::ensureI2CLegacyDriver(I2C_NUM_0, (gpio_num_t)pins.sda, (gpio_num_t)pins.scl, eff_clock)) {
906 return false;
907 }
908 return units.add(unit, I2C_NUM_0, (gpio_num_t)pins.sda, (gpio_num_t)pins.scl);
909#endif
910 }
912 // Core / NanoC6 / NanoH2: M5Unified manages Ex_I2C (Core shares it with In_I2C) -> borrow
913 return i2cClass(units, unit, M5.Ex_I2C);
914 }
915 return false;
916}
917
919inline bool addGPIO(UnitUnified& units, Component& unit, const GpioRole role = GpioRole::Both)
920{
921 auto p = gpioPins(role);
922 if (role == GpioRole::OutOnly) p.rx = -1;
923 if (role == GpioRole::InOnly) p.tx = -1;
924 M5_LIB_LOGI("wiring(ESP-IDF): GPIO rx=%d tx=%d role=%d fallback_a=%d", (int)p.rx, (int)p.tx, (int)role,
925 (int)p.fallback_a);
926 return units.add(unit, p.rx, p.tx);
927}
928
930inline bool addUART(UnitUnified& units, Component& unit, const uint32_t baud = 115200,
931 const UartConfig config = UartConfig::Default)
932{
933 const auto pins = uartPins();
934 M5_LIB_LOGI("wiring(ESP-IDF): UART rx=%d tx=%d baud=%u fallback_a=%d", (int)pins.rx, (int)pins.tx, (unsigned)baud,
935 (int)pins.fallback_a);
936 auto port = uartPortHandle(defaultUartPort(), (gpio_num_t)pins.rx, (gpio_num_t)pins.tx, baud, config);
937 if (port == UART_NUM_MAX) return false;
938 return units.add(unit, port);
939}
940
942inline bool addSPI(UnitUnified& units, Component& unit, const uint32_t clock_hz, const uint8_t mode = 0,
943 const uint8_t bit_order = 0)
944{
945 const auto pins = spiPins();
946 M5_LIB_LOGI("wiring(ESP-IDF): addSPI sclk=%d miso=%d mosi=%d clock=%u mode=%u bit_order=%u", (int)pins.sclk,
947 (int)pins.miso, (int)pins.mosi, (unsigned)clock_hz, (unsigned)mode, (unsigned)bit_order);
948 auto dev = spiDeviceHandle(SPI2_HOST, (gpio_num_t)pins.mosi, (gpio_num_t)pins.miso, (gpio_num_t)pins.sclk, clock_hz,
949 mode, bit_order);
950 if (!dev) return false;
951 return units.add(unit, dev); // cs omitted -> Component::assign uses address() as CS
952}
953
955inline bool addHatI2C(UnitUnified& units, Component& unit, const uint32_t clock = 0)
956{
957 const auto p = hatI2CPins();
958 if (p.sda < 0 || p.scl < 0) {
959 M5_LIB_LOGE("wiring: Hat I2C unsupported board=0x%02x", (int)M5.getBoard());
960 return false;
961 }
962 // clock == 0: use the unit's own configured clock; clock > 0: override it.
963 if (clock != 0) {
964 auto cfg = unit.component_config();
965 cfg.clock = clock;
966 unit.component_config(cfg);
967 }
968 const uint32_t eff_clock = unit.component_config().clock;
969 i2c_port_t port;
970 if (p.useWire1) {
971#if SOC_HP_I2C_NUM >= 2
972 port = I2C_NUM_1; // HP I2C #1 (ESP32 / S2 / S3 / P4 / H2 etc.)
973#elif SOC_LP_I2C_SUPPORTED
974 port = LP_I2C_NUM_0; // C6: 2nd I2C is LP (= Arduino Wire1)
975#else
976 M5_LIB_LOGE("wiring: addHatI2C needs a 2nd I2C port, none available board=0x%02x", (int)M5.getBoard());
977 return false;
978#endif
979 } else {
980 port = I2C_NUM_0;
981 }
982 M5_LIB_LOGI("wiring(ESP-IDF): addHatI2C port=%d sda=%d scl=%d clock=%u", (int)port, (int)p.sda, (int)p.scl,
983 (unsigned)eff_clock);
984#if __has_include(<driver/i2c_master.h>)
985 auto bus = i2cBusHandle(port, (gpio_num_t)p.sda, (gpio_num_t)p.scl, eff_clock);
986 if (!bus) return false;
987 return units.add(unit, bus);
988#else // legacy driver (IDF 5.0 / 5.1)
989 if (!detail::ensureI2CLegacyDriver(port, (gpio_num_t)p.sda, (gpio_num_t)p.scl, eff_clock)) return false;
990 return units.add(unit, port, (gpio_num_t)p.sda, (gpio_num_t)p.scl);
991#endif
992}
993
995inline bool addHatGPIO(UnitUnified& units, Component& unit, const GpioRole role = GpioRole::Both)
996{
997 auto p = hatGPIOPins();
998 if (p.rx < 0 || p.tx < 0) {
999 M5_LIB_LOGE("wiring: Hat GPIO unsupported board=0x%02x", (int)M5.getBoard());
1000 return false;
1001 }
1002 if (role == GpioRole::OutOnly) p.rx = -1;
1003 if (role == GpioRole::InOnly) p.tx = -1;
1004 M5_LIB_LOGI("wiring(ESP-IDF): addHatGPIO board=0x%02x rx=%d tx=%d role=%d", (int)M5.getBoard(), (int)p.rx,
1005 (int)p.tx, (int)role);
1006 return units.add(unit, p.rx, p.tx);
1007}
1008
1010inline bool addHatUART(UnitUnified& units, Component& unit, const uint32_t baud = 115200,
1011 const UartConfig config = UartConfig::Default)
1012{
1013 const auto p = hatUARTPins();
1014 if (p.rx < 0 || p.tx < 0) {
1015 M5_LIB_LOGE("wiring: Hat UART unsupported board=0x%02x", (int)M5.getBoard());
1016 return false;
1017 }
1018 M5_LIB_LOGI("wiring(ESP-IDF): addHatUART board=0x%02x rx=%d tx=%d baud=%u", (int)M5.getBoard(), (int)p.rx,
1019 (int)p.tx, (unsigned)baud);
1020 auto port = uartPortHandle(defaultUartPort(), (gpio_num_t)p.rx, (gpio_num_t)p.tx, baud, config);
1021 if (port == UART_NUM_MAX) return false;
1022 return units.add(unit, port);
1023}
1024#endif // __M5UNIFIED_HPP__
1025
1026#endif // !ARDUINO
1028
1031#if defined(__M5UNIFIED_HPP__)
1033[[noreturn]] inline void failStop()
1034{
1035 M5.Display.fillScreen(0xF800 /* TFT_RED */);
1036 while (true) {
1037 m5::utility::delay(10000);
1038 }
1039}
1040#endif
1042
1043} // namespace wiring
1044} // namespace unit
1045} // namespace m5
1046#endif // M5_UNIT_UNIFIED_WIRING_HPP
Main header of M5UnitUnified.
Base class of unit component.
For managing and leading units.
bool add(Component &u, TwoWire &wire)
Add unit to be managed (I2C via TwoWire)
UartConfig
UART config (word length / parity / stop bits). Common across Arduino and ESP-IDF native.
Definition m5_unit_unified_wiring.hpp:72
spi_device_handle_t spiDeviceHandle(const spi_host_device_t host, const gpio_num_t mosi, const gpio_num_t miso, const gpio_num_t sck, const uint32_t clock_hz, const uint8_t mode=0, const uint8_t bit_order=0)
Get or initialize a SPI device on the given host (cached by {host, clock_hz, mode,...
Definition m5_unit_unified_wiring.hpp:849
spi_device_handle_t ensureSPIDevice(const spi_host_device_t host, const gpio_num_t mosi, const gpio_num_t miso, const gpio_num_t sck, const uint32_t clock_hz, const uint8_t mode, const uint8_t bit_order)
Get or initialize a SPI device, cached by {host, clock_hz, mode, bit_order}.
Definition m5_unit_unified_wiring.hpp:764
GpioRole
GPIO usage role: how many pins of the port the unit actually uses.
Definition m5_unit_unified_wiring.hpp:65
@ 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:62
uart_port_t defaultUartPort()
The board's default UART port for a Port unit, chosen by SoC UART count (ESP-IDF native)
Definition m5_unit_unified_wiring.hpp:555
uart_port_t uartPortHandle(const uart_port_t port, const gpio_num_t rx, const gpio_num_t tx, const uint32_t baud, const UartConfig config=UartConfig::Default)
Get or install a UART port (cached by {port, rx, tx})
Definition m5_unit_unified_wiring.hpp:838
uart_port_t ensureUARTPort(const uart_port_t port, const gpio_num_t rx, const gpio_num_t tx, const uint32_t baud, const UartConfig config)
Get or install a UART port, cached by {port, rx, tx}.
Definition m5_unit_unified_wiring.hpp:725
IdfUartParams toIdfUartParams(const UartConfig c)
The board's default UART port for a Port unit, chosen by SoC UART count (ESP-IDF native)
Definition m5_unit_unified_wiring.hpp:574
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:98
bool fallback_a
True when PortB unavailable and PortA was chosen.
Definition m5_unit_unified_wiring.hpp:101
Hat I2C pin pair + bus selection; sda=scl=-1 if board has no Hat header.
Definition m5_unit_unified_wiring.hpp:119
bool useWire1
True if the Hat uses Wire1 (NessoN1); else use Wire.
Definition m5_unit_unified_wiring.hpp:122
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:126
I2C backend selection + pin numbers (pure getter result, no side effects)
Definition m5_unit_unified_wiring.hpp:87
Backend
Definition m5_unit_unified_wiring.hpp:90
@ 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.
Map UartConfig to ESP-IDF native uart_config_t fields.
Definition m5_unit_unified_wiring.hpp:569
SPI pin set (sclk / miso / mosi) on the board's shared SD/SPI bus.
Definition m5_unit_unified_wiring.hpp:112
UART rx/tx pin pair; fallback_a indicates port_a fallback was used.
Definition m5_unit_unified_wiring.hpp:105
bool fallback_a
True when PortC unavailable and PortA was chosen.
Definition m5_unit_unified_wiring.hpp:108
Definition m5_unit_unified_wiring.hpp:620
Definition m5_unit_unified_wiring.hpp:615
Definition m5_unit_unified_wiring.hpp:610