M5UnitUnified 0.6.4 git rev:53b220c
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
442inline bool beginUartSerial(HardwareSerial& serial, const uint32_t baud, const uint32_t cfg, const int8_t rx,
443 const int8_t tx, const size_t rx_buffer_size)
444{
445 serial.end();
446 // HardwareSerial resizes its RX buffer only while it is stopped
447 if (rx_buffer_size && serial.setRxBufferSize(rx_buffer_size) != rx_buffer_size) {
448 M5_LIB_LOGE("wiring: UART rx_buffer_size=%u rejected", (unsigned)rx_buffer_size);
449 return false;
450 }
451 serial.begin(baud, cfg, rx, tx);
452 return true;
453}
454
465inline bool addUART(UnitUnified& units, Component& unit, const uint32_t baud = 115200,
466 const UartConfig config = UartConfig::Default, const size_t rx_buffer_size = 0)
467{
468 HardwareSerial& serial = defaultUartSerial();
469 const uint32_t cfg = toArduinoSerialConfig(config);
470 const auto p = uartPins();
471 if (p.fallback_a) {
472 const auto b = M5.getBoard();
473 if (b == m5::board_t::board_M5NanoC6 || b == m5::board_t::board_M5NanoH2) {
474 // M5.begin() calls Ex_I2C.setPort() but NOT begin(). m5gfx::i2c::release() is gated
475 // by the internal `initialized` flag, so release() alone is a no-op. Workaround:
476 // begin() flips initialized=true, then release() actually detaches + restores pins.
477 M5.Ex_I2C.begin();
478 M5.Ex_I2C.release();
479 }
480 }
481 M5_LIB_LOGI("wiring: UART rx=%d tx=%d baud=%lu config=0x%lx fallback_a=%d", (int)p.rx, (int)p.tx,
482 (unsigned long)baud, (unsigned long)cfg, (int)p.fallback_a);
483 return beginUartSerial(serial, baud, cfg, p.rx, p.tx, rx_buffer_size) && units.add(unit, serial);
484}
485
487inline bool spiBus(UnitUnified& units, Component& unit, SPIClass& spi, const SPISettings& settings)
488{
489 M5_LIB_LOGI("wiring: SPI");
490 return units.add(unit, spi, settings);
491}
492
500inline bool addSPI(UnitUnified& units, Component& unit, const uint32_t clock_hz, const uint8_t mode = 0,
501 const uint8_t bit_order = 0)
502{
503 SPISettings settings{clock_hz, static_cast<uint8_t>((bit_order == 0) ? MSBFIRST : LSBFIRST), mode};
504 const auto p = spiPins();
505 M5_LIB_LOGI("wiring: addSPI sclk=%d miso=%d mosi=%d clock=%lu mode=%u bit_order=%u", (int)p.sclk, (int)p.miso,
506 (int)p.mosi, (unsigned long)clock_hz, (unsigned)mode, (unsigned)bit_order);
507 if (!SPI.bus()) {
508 SPI.begin(p.sclk, p.miso, p.mosi);
509 }
510 return spiBus(units, unit, SPI, settings);
511}
512
514
520inline bool addHatI2C(UnitUnified& units, Component& unit, const uint32_t clock = 0)
521{
522 const auto p = hatI2CPins();
523 if (p.sda < 0 || p.scl < 0) {
524 M5_LIB_LOGE("wiring: Hat I2C unsupported board=0x%02x", (int)M5.getBoard());
525 return false;
526 }
527 // clock == 0: use the unit's own configured clock; clock > 0: override it.
528 if (clock != 0) {
529 auto cfg = unit.component_config();
530 cfg.clock = clock;
531 unit.component_config(cfg);
532 }
533 const uint32_t eff_clock = unit.component_config().clock;
534#if M5_UNIT_UNIFIED_WIRING_HAS_WIRE1
535 TwoWire& wire = p.useWire1 ? Wire1 : Wire;
536#else
537 // Arduino-ESP32 declares Wire only (e.g. ESP32-C3); Wire1 is absent.
538 if (p.useWire1) {
539 M5_LIB_LOGE("wiring: addHatI2C NessoN1 Hat needs Wire1, but Wire1 is not available");
540 return false;
541 }
542 TwoWire& wire = Wire;
543#endif
544 M5_LIB_LOGI("wiring: addHatI2C board=0x%02x sda=%d scl=%d clock=%lu wire=%s", (int)M5.getBoard(), (int)p.sda,
545 (int)p.scl, (unsigned long)eff_clock, p.useWire1 ? "Wire1" : "Wire");
546 return i2cWire(units, unit, wire, p.sda, p.scl, eff_clock);
547}
548
553inline bool addHatGPIO(UnitUnified& units, Component& unit, const GpioRole role = GpioRole::Both)
554{
555 auto p = hatGPIOPins();
556 if (p.rx < 0 || p.tx < 0) {
557 M5_LIB_LOGE("wiring: Hat GPIO unsupported board=0x%02x", (int)M5.getBoard());
558 return false;
559 }
560 if (role == GpioRole::OutOnly) p.rx = -1;
561 if (role == GpioRole::InOnly) p.tx = -1;
562 M5_LIB_LOGI("wiring: addHatGPIO board=0x%02x rx=%d tx=%d role=%d", (int)M5.getBoard(), (int)p.rx, (int)p.tx,
563 (int)role);
564 return units.add(unit, p.rx, p.tx);
565}
566
574inline bool addHatUART(UnitUnified& units, Component& unit, const uint32_t baud = 115200,
575 const UartConfig config = UartConfig::Default, const size_t rx_buffer_size = 0)
576{
577 HardwareSerial& serial = defaultUartSerial();
578 const uint32_t cfg = toArduinoSerialConfig(config);
579 const auto p = hatUARTPins();
580 if (p.rx < 0 || p.tx < 0) {
581 M5_LIB_LOGE("wiring: Hat UART unsupported board=0x%02x", (int)M5.getBoard());
582 return false;
583 }
584 M5_LIB_LOGI("wiring: addHatUART board=0x%02x rx=%d tx=%d baud=%lu config=0x%lx", (int)M5.getBoard(), (int)p.rx,
585 (int)p.tx, (unsigned long)baud, (unsigned long)cfg);
586 return beginUartSerial(serial, baud, cfg, p.rx, p.tx, rx_buffer_size) && units.add(unit, serial);
587}
588#endif // ARDUINO
589
592#if !defined(ARDUINO) && defined(ESP_PLATFORM)
593
595inline uart_port_t defaultUartPort()
596{
597#if M5_UNIT_UNIFIED_WIRING_HP_UART_NUM > 2
598 return UART_NUM_2;
599#elif M5_UNIT_UNIFIED_WIRING_HP_UART_NUM > 1
600 return UART_NUM_1;
601#else
602#error "Not enough UART"
603#endif
604}
605
607struct IdfUartParams {
608 uart_word_length_t word_length;
609 uart_parity_t parity;
610 uart_stop_bits_t stop_bits;
611};
612inline IdfUartParams toIdfUartParams(const UartConfig c)
613{
614 switch (c) {
615 case UartConfig::_8N2:
616 return {UART_DATA_8_BITS, UART_PARITY_DISABLE, UART_STOP_BITS_2};
617 case UartConfig::_8E1:
618 return {UART_DATA_8_BITS, UART_PARITY_EVEN, UART_STOP_BITS_1};
619 case UartConfig::_8O1:
620 return {UART_DATA_8_BITS, UART_PARITY_ODD, UART_STOP_BITS_1};
621 case UartConfig::_7N1:
622 return {UART_DATA_7_BITS, UART_PARITY_DISABLE, UART_STOP_BITS_1};
623 case UartConfig::_7E1:
624 return {UART_DATA_7_BITS, UART_PARITY_EVEN, UART_STOP_BITS_1};
625 case UartConfig::_7O1:
626 return {UART_DATA_7_BITS, UART_PARITY_ODD, UART_STOP_BITS_1};
627 case UartConfig::Default:
628 case UartConfig::_8N1:
629 default:
630 return {UART_DATA_8_BITS, UART_PARITY_DISABLE, UART_STOP_BITS_1};
631 }
632}
633
634namespace detail {
635
636constexpr size_t kI2CBusCacheSize = 4;
637constexpr size_t kUARTPortCacheSize = 4;
638constexpr size_t kSPIHostCacheSize = 2; // SPI2_HOST / SPI3_HOST
639constexpr size_t kSPIDevCacheSize = 4;
640
641#if __has_include(<driver/i2c_master.h>)
642struct I2CCacheEntry {
643 uint32_t key;
644 i2c_master_bus_handle_t handle;
645};
646#endif
647
648constexpr size_t uart_default_rx_buffer_size = 256;
649
650struct UARTCacheEntry {
651 uint32_t key;
652 uart_port_t port;
653 size_t rx_buffer_size;
654};
655
656struct SPIHostEntry {
657 spi_host_device_t host;
658 bool initialized;
659};
660
661struct SPIDevEntry {
662 uint32_t key;
663 spi_device_handle_t handle;
664};
665
666#if __has_include(<driver/i2c_master.h>)
668inline i2c_master_bus_handle_t ensureI2CBus(const i2c_port_t port, const gpio_num_t sda, const gpio_num_t scl,
669 const uint32_t /*clock - not used to key, single bus per pins*/)
670{
671 const uint32_t key = (static_cast<uint32_t>(static_cast<uint8_t>(port)) << 24) |
672 (static_cast<uint32_t>(static_cast<uint8_t>(sda)) << 16) |
673 (static_cast<uint32_t>(static_cast<uint8_t>(scl)) << 8);
674 static I2CCacheEntry cache[kI2CBusCacheSize]{};
675 static size_t count{};
676 for (size_t i = 0; i < count; ++i) {
677 if (cache[i].key == key) return cache[i].handle;
678 }
679 if (count >= kI2CBusCacheSize) {
680 M5_LIB_LOGE("wiring: I2C bus cache full (max %zu)", kI2CBusCacheSize);
681 return nullptr;
682 }
683 i2c_master_bus_config_t cfg{};
684 cfg.i2c_port = port;
685 cfg.sda_io_num = sda;
686 cfg.scl_io_num = scl;
687#if SOC_LP_I2C_SUPPORTED
688 if (port == LP_I2C_NUM_0) {
689#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
690 // LP I2C uses RTC_FAST clock (union with clk_source, mutually exclusive).
691 cfg.lp_source_clk = LP_I2C_SCLK_DEFAULT;
692#else
693 // i2c_master_bus_config_t has no lp_source_clk before ESP-IDF 5.3; LP I2C cannot be opened
694 M5_LIB_LOGE("wiring: LP I2C needs ESP-IDF >= 5.3");
695 return nullptr;
696#endif
697 } else
698#endif
699 {
700 cfg.clk_source = I2C_CLK_SRC_DEFAULT;
701 }
702 cfg.glitch_ignore_cnt = 7;
703 cfg.flags.enable_internal_pullup = true;
704 if (i2c_new_master_bus(&cfg, &cache[count].handle) != ESP_OK) {
705 M5_LIB_LOGE("wiring: i2c_new_master_bus failed port=%d sda=%d scl=%d", (int)port, (int)sda, (int)scl);
706 return nullptr;
707 }
708 cache[count].key = key;
709 return cache[count++].handle;
710}
711#else // legacy driver/i2c.h (IDF 5.0 / 5.1)
713inline bool ensureI2CLegacyDriver(const i2c_port_t port, const gpio_num_t sda, const gpio_num_t scl,
714 const uint32_t clock)
715{
716 const uint32_t key = (static_cast<uint32_t>(static_cast<uint8_t>(port)) << 24) |
717 (static_cast<uint32_t>(static_cast<uint8_t>(sda)) << 16) |
718 (static_cast<uint32_t>(static_cast<uint8_t>(scl)) << 8);
719 static uint32_t cache[kI2CBusCacheSize]{};
720 static size_t count{};
721 for (size_t i = 0; i < count; ++i) {
722 if (cache[i] == key) return true;
723 }
724 if (count >= kI2CBusCacheSize) {
725 M5_LIB_LOGE("wiring: I2C bus cache full (max %zu)", kI2CBusCacheSize);
726 return false;
727 }
728 // i2c_driver_install returns ESP_FAIL for several distinct reasons in IDF 5.0/5.1 (already
729 // installed by another owner / OOM / slave-setup failure). Probe i2c_get_period to disambiguate:
730 // it writes high/low only when the driver object exists (p_i2c_obj != NULL), so high>0 && low>0
731 // means the port is genuinely installed and can be borrowed.
732 const esp_err_t install_err = i2c_driver_install(port, I2C_MODE_MASTER, 0, 0, 0);
733 if (install_err == ESP_OK) {
734 // Freshly installed by us: configure pins and timing.
735 i2c_config_t conf{};
736 conf.mode = I2C_MODE_MASTER;
737 conf.sda_io_num = sda;
738 conf.scl_io_num = scl;
739 conf.sda_pullup_en = true;
740 conf.scl_pullup_en = true;
741 conf.master.clk_speed = clock;
742 if (i2c_param_config(port, &conf) != ESP_OK) {
743 M5_LIB_LOGE("wiring: legacy i2c param_config failed port=%d sda=%d scl=%d", (int)port, (int)sda, (int)scl);
744 i2c_driver_delete(port);
745 return false;
746 }
747 M5_LIB_LOGI("wiring: legacy i2c INSTALLED(new) port=%d sda=%d scl=%d clock=%u", (int)port, (int)sda, (int)scl,
748 (unsigned)clock);
749 } else if (install_err == ESP_FAIL) {
750 // Already installed by another owner -> borrow it (keep the owner's pins/timing; do NOT
751 // re-run i2c_param_config and override them). Confirm the driver really exists first.
752 int high = 0, low = 0;
753 i2c_get_period(port, &high, &low);
754 if (!(high > 0 && low > 0)) {
755 M5_LIB_LOGE("wiring: legacy i2c driver install failed port=%d sda=%d scl=%d", (int)port, (int)sda,
756 (int)scl);
757 return false;
758 }
759 M5_LIB_LOGI("wiring: legacy i2c BORROW(already) port=%d sda=%d scl=%d high=%d low=%d", (int)port, (int)sda,
760 (int)scl, high, low);
761 } else {
762 M5_LIB_LOGE("wiring: legacy i2c driver install error=0x%x port=%d sda=%d scl=%d", (int)install_err, (int)port,
763 (int)sda, (int)scl);
764 return false;
765 }
766 cache[count++] = key;
767 return true;
768}
769#endif
770
772inline uart_port_t ensureUARTPort(const uart_port_t port, const gpio_num_t rx, const gpio_num_t tx, const uint32_t baud,
773 const UartConfig config, const size_t rx_buffer_size)
774{
775 const size_t rx_size = rx_buffer_size ? rx_buffer_size : uart_default_rx_buffer_size;
776 const uint32_t key = (static_cast<uint32_t>(static_cast<uint8_t>(port)) << 24) |
777 (static_cast<uint32_t>(static_cast<uint8_t>(rx)) << 16) |
778 (static_cast<uint32_t>(static_cast<uint8_t>(tx)) << 8);
779 static UARTCacheEntry cache[kUARTPortCacheSize]{};
780 static size_t count{};
781 for (size_t i = 0; i < count; ++i) {
782 if (cache[i].key == key) {
783 // The driver keeps the buffer it was installed with
784 if (rx_buffer_size && cache[i].rx_buffer_size && cache[i].rx_buffer_size != rx_size) {
785 M5_LIB_LOGW("wiring: UART port=%d already installed with rx_buffer_size=%u (requested %u)", (int)port,
786 (unsigned)cache[i].rx_buffer_size, (unsigned)rx_size);
787 }
788 return cache[i].port;
789 }
790 }
791 if (count >= kUARTPortCacheSize) {
792 M5_LIB_LOGE("wiring: UART port cache full (max %zu)", kUARTPortCacheSize);
793 return UART_NUM_MAX;
794 }
795 if (uart_is_driver_installed(port)) {
796 cache[count] = {key, port, 0};
797 return cache[count++].port;
798 }
799 const auto p = toIdfUartParams(config);
800 uart_config_t uc{};
801 uc.baud_rate = static_cast<int>(baud);
802 uc.data_bits = p.word_length;
803 uc.parity = p.parity;
804 uc.stop_bits = p.stop_bits;
805 uc.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
806 uc.source_clk = UART_SCLK_DEFAULT;
807 if (uart_param_config(port, &uc) != ESP_OK ||
808 uart_set_pin(port, tx, rx, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE) != ESP_OK ||
809 uart_driver_install(port, static_cast<int>(rx_size), 0, 0, nullptr, 0) != ESP_OK) {
810 M5_LIB_LOGE("wiring: uart setup failed port=%d rx_buffer_size=%u", (int)port, (unsigned)rx_size);
811 return UART_NUM_MAX;
812 }
813 cache[count] = {key, port, rx_size};
814 return cache[count++].port;
815}
816
819inline spi_device_handle_t ensureSPIDevice(const spi_host_device_t host, const gpio_num_t mosi, const gpio_num_t miso,
820 const gpio_num_t sck, const uint32_t clock_hz, const uint8_t mode,
821 const uint8_t bit_order)
822{
823 static SPIHostEntry host_cache[kSPIHostCacheSize]{};
824 static SPIDevEntry dev_cache[kSPIDevCacheSize]{};
825 static size_t host_count{};
826 static size_t dev_count{};
827
828 bool host_init = false;
829 for (size_t i = 0; i < host_count; ++i) {
830 if (host_cache[i].host == host) {
831 host_init = host_cache[i].initialized;
832 break;
833 }
834 }
835 if (!host_init) {
836 spi_bus_config_t bc{};
837 bc.mosi_io_num = mosi;
838 bc.miso_io_num = miso;
839 bc.sclk_io_num = sck;
840 bc.quadwp_io_num = -1;
841 bc.quadhd_io_num = -1;
842 bc.max_transfer_sz = 4096;
843 if (spi_bus_initialize(host, &bc, SPI_DMA_CH_AUTO) != ESP_OK) {
844 M5_LIB_LOGE("wiring: spi_bus_initialize failed host=%d", (int)host);
845 return nullptr;
846 }
847 if (host_count < kSPIHostCacheSize) host_cache[host_count++] = {host, true};
848 }
849
850 const uint32_t key = (static_cast<uint32_t>(static_cast<uint8_t>(host)) << 24) |
851 (static_cast<uint32_t>(clock_hz / 1000U) << 8) | (static_cast<uint32_t>(mode) << 4) |
852 static_cast<uint32_t>(bit_order);
853 for (size_t i = 0; i < dev_count; ++i) {
854 if (dev_cache[i].key == key) return dev_cache[i].handle;
855 }
856 if (dev_count >= kSPIDevCacheSize) {
857 M5_LIB_LOGE("wiring: SPI device cache full (max %zu)", kSPIDevCacheSize);
858 return nullptr;
859 }
860 spi_device_interface_config_t dc{};
861 dc.clock_speed_hz = static_cast<int>(clock_hz);
862 dc.mode = mode;
863 dc.spics_io_num = GPIO_NUM_NC; // manual CS control by adapter
864 dc.queue_size = 1;
865 if (bit_order == 1) dc.flags |= SPI_DEVICE_BIT_LSBFIRST;
866 if (spi_bus_add_device(host, &dc, &dev_cache[dev_count].handle) != ESP_OK) {
867 M5_LIB_LOGE("wiring: spi_bus_add_device failed host=%d", (int)host);
868 return nullptr;
869 }
870 dev_cache[dev_count].key = key;
871 return dev_cache[dev_count++].handle;
872}
873
874} // namespace detail
875
876#if __has_include(<driver/i2c_master.h>)
882inline i2c_master_bus_handle_t i2cBusHandle(const i2c_port_t port, const gpio_num_t sda, const gpio_num_t scl,
883 const uint32_t clock = 100000)
884{
885 return detail::ensureI2CBus(port, sda, scl, clock);
886}
887#endif
888
895inline uart_port_t uartPortHandle(const uart_port_t port, const gpio_num_t rx, const gpio_num_t tx, const uint32_t baud,
896 const UartConfig config = UartConfig::Default, const size_t rx_buffer_size = 0)
897{
898 return detail::ensureUARTPort(port, rx, tx, baud, config, rx_buffer_size);
899}
900
906inline spi_device_handle_t spiDeviceHandle(const spi_host_device_t host, const gpio_num_t mosi, const gpio_num_t miso,
907 const gpio_num_t sck, const uint32_t clock_hz, const uint8_t mode = 0,
908 const uint8_t bit_order = 0)
909{
910 return detail::ensureSPIDevice(host, mosi, miso, sck, clock_hz, mode, bit_order);
911}
912
913//
914// === ESP-IDF native generic helpers (Phase C) — same signatures as Arduino ===
915//
916
917#if defined(__M5UNIFIED_HPP__)
926inline bool addI2C(UnitUnified& units, Component& unit, const uint32_t clock = 0,
927 const NessoPort nesso = NessoPort::PortB)
928{
929 // clock == 0: use the unit's own configured clock; clock > 0: override it. component_config().clock
930 // is the single source of truth applied per transaction (Wire / I2C_Class / master device / legacy).
931 if (clock != 0) {
932 auto cfg = unit.component_config();
933 cfg.clock = clock;
934 unit.component_config(cfg);
935 }
936 const uint32_t eff_clock = unit.component_config().clock;
937 const auto pins = i2cPins(nesso);
938 const auto board = M5.getBoard();
939 M5_LIB_LOGI("wiring(ESP-IDF): addI2C board=0x%02x nesso=%d sda=%d scl=%d clock=%u backend=%d", (int)board,
940 (int)nesso, (int)pins.sda, (int)pins.scl, (unsigned)eff_clock, (int)pins.backend);
941 switch (pins.backend) {
943#if defined(M5_HAL_HPP)
944 return i2cSoftware(units, unit, pins.sda, pins.scl);
945#else
946 M5_LIB_LOGE("wiring: SoftwareI2C backend requires M5HAL");
947 return false;
948#endif
950 if (board == m5::board_t::board_ArduinoNessoN1) {
951 // NessoN1 PortA (QWIIC) -> Wire == M5.In_I2C (NessoN1 maps Wire to In_I2C).
952 // M5Unified already holds I2C_NUM_0 here, so borrow instead of installing.
953 return i2cClass(units, unit, M5.In_I2C);
954 } else {
955 // Stick / S3 etc: M5Unified does not call Wire.begin(), so install ourselves
956 // (Core is routed to ExI2C by i2cPins() and never reaches this branch)
957#if __has_include(<driver/i2c_master.h>)
958 auto bus = i2cBusHandle(I2C_NUM_0, (gpio_num_t)pins.sda, (gpio_num_t)pins.scl, eff_clock);
959 if (!bus) return false;
960 return units.add(unit, bus, I2C_NUM_0, (gpio_num_t)pins.sda, (gpio_num_t)pins.scl);
961#else // legacy driver (IDF 5.0 / 5.1): install legacy driver, then add by port
962 if (!detail::ensureI2CLegacyDriver(I2C_NUM_0, (gpio_num_t)pins.sda, (gpio_num_t)pins.scl, eff_clock)) {
963 return false;
964 }
965 return units.add(unit, I2C_NUM_0, (gpio_num_t)pins.sda, (gpio_num_t)pins.scl);
966#endif
967 }
969 // Core / NanoC6 / NanoH2: M5Unified manages Ex_I2C (Core shares it with In_I2C) -> borrow
970 return i2cClass(units, unit, M5.Ex_I2C);
971 }
972 return false;
973}
974
976inline bool addGPIO(UnitUnified& units, Component& unit, const GpioRole role = GpioRole::Both)
977{
978 auto p = gpioPins(role);
979 if (role == GpioRole::OutOnly) p.rx = -1;
980 if (role == GpioRole::InOnly) p.tx = -1;
981 M5_LIB_LOGI("wiring(ESP-IDF): GPIO rx=%d tx=%d role=%d fallback_a=%d", (int)p.rx, (int)p.tx, (int)role,
982 (int)p.fallback_a);
983 return units.add(unit, p.rx, p.tx);
984}
985
991inline bool addUART(UnitUnified& units, Component& unit, const uint32_t baud = 115200,
992 const UartConfig config = UartConfig::Default, const size_t rx_buffer_size = 0)
993{
994 const auto pins = uartPins();
995 M5_LIB_LOGI("wiring(ESP-IDF): UART rx=%d tx=%d baud=%u fallback_a=%d", (int)pins.rx, (int)pins.tx, (unsigned)baud,
996 (int)pins.fallback_a);
997 auto port =
998 uartPortHandle(defaultUartPort(), (gpio_num_t)pins.rx, (gpio_num_t)pins.tx, baud, config, rx_buffer_size);
999 if (port == UART_NUM_MAX) return false;
1000 return units.add(unit, port);
1001}
1002
1004inline bool addSPI(UnitUnified& units, Component& unit, const uint32_t clock_hz, const uint8_t mode = 0,
1005 const uint8_t bit_order = 0)
1006{
1007 const auto pins = spiPins();
1008 M5_LIB_LOGI("wiring(ESP-IDF): addSPI sclk=%d miso=%d mosi=%d clock=%u mode=%u bit_order=%u", (int)pins.sclk,
1009 (int)pins.miso, (int)pins.mosi, (unsigned)clock_hz, (unsigned)mode, (unsigned)bit_order);
1010 auto dev = spiDeviceHandle(SPI2_HOST, (gpio_num_t)pins.mosi, (gpio_num_t)pins.miso, (gpio_num_t)pins.sclk, clock_hz,
1011 mode, bit_order);
1012 if (!dev) return false;
1013 return units.add(unit, dev); // cs omitted -> Component::assign uses address() as CS
1014}
1015
1017inline bool addHatI2C(UnitUnified& units, Component& unit, const uint32_t clock = 0)
1018{
1019 const auto p = hatI2CPins();
1020 if (p.sda < 0 || p.scl < 0) {
1021 M5_LIB_LOGE("wiring: Hat I2C unsupported board=0x%02x", (int)M5.getBoard());
1022 return false;
1023 }
1024 // clock == 0: use the unit's own configured clock; clock > 0: override it.
1025 if (clock != 0) {
1026 auto cfg = unit.component_config();
1027 cfg.clock = clock;
1028 unit.component_config(cfg);
1029 }
1030 const uint32_t eff_clock = unit.component_config().clock;
1031 i2c_port_t port;
1032 if (p.useWire1) {
1033#if M5_UNIT_UNIFIED_WIRING_HP_I2C_NUM >= 2
1034 port = I2C_NUM_1; // HP I2C #1 (ESP32 / S2 / S3 / P4 / H2 etc.)
1035#elif SOC_LP_I2C_SUPPORTED
1036 port = LP_I2C_NUM_0; // C6: 2nd I2C is LP (= Arduino Wire1)
1037#else
1038 M5_LIB_LOGE("wiring: addHatI2C needs a 2nd I2C port, none available board=0x%02x", (int)M5.getBoard());
1039 return false;
1040#endif
1041 } else {
1042 port = I2C_NUM_0;
1043 }
1044 M5_LIB_LOGI("wiring(ESP-IDF): addHatI2C port=%d sda=%d scl=%d clock=%u", (int)port, (int)p.sda, (int)p.scl,
1045 (unsigned)eff_clock);
1046#if __has_include(<driver/i2c_master.h>)
1047 auto bus = i2cBusHandle(port, (gpio_num_t)p.sda, (gpio_num_t)p.scl, eff_clock);
1048 if (!bus) return false;
1049 return units.add(unit, bus, port, (gpio_num_t)p.sda, (gpio_num_t)p.scl);
1050#else // legacy driver (IDF 5.0 / 5.1)
1051 if (!detail::ensureI2CLegacyDriver(port, (gpio_num_t)p.sda, (gpio_num_t)p.scl, eff_clock)) return false;
1052 return units.add(unit, port, (gpio_num_t)p.sda, (gpio_num_t)p.scl);
1053#endif
1054}
1055
1057inline bool addHatGPIO(UnitUnified& units, Component& unit, const GpioRole role = GpioRole::Both)
1058{
1059 auto p = hatGPIOPins();
1060 if (p.rx < 0 || p.tx < 0) {
1061 M5_LIB_LOGE("wiring: Hat GPIO unsupported board=0x%02x", (int)M5.getBoard());
1062 return false;
1063 }
1064 if (role == GpioRole::OutOnly) p.rx = -1;
1065 if (role == GpioRole::InOnly) p.tx = -1;
1066 M5_LIB_LOGI("wiring(ESP-IDF): addHatGPIO board=0x%02x rx=%d tx=%d role=%d", (int)M5.getBoard(), (int)p.rx,
1067 (int)p.tx, (int)role);
1068 return units.add(unit, p.rx, p.tx);
1069}
1070
1075inline bool addHatUART(UnitUnified& units, Component& unit, const uint32_t baud = 115200,
1076 const UartConfig config = UartConfig::Default, const size_t rx_buffer_size = 0)
1077{
1078 const auto p = hatUARTPins();
1079 if (p.rx < 0 || p.tx < 0) {
1080 M5_LIB_LOGE("wiring: Hat UART unsupported board=0x%02x", (int)M5.getBoard());
1081 return false;
1082 }
1083 M5_LIB_LOGI("wiring(ESP-IDF): addHatUART board=0x%02x rx=%d tx=%d baud=%u", (int)M5.getBoard(), (int)p.rx,
1084 (int)p.tx, (unsigned)baud);
1085 auto port = uartPortHandle(defaultUartPort(), (gpio_num_t)p.rx, (gpio_num_t)p.tx, baud, config, rx_buffer_size);
1086 if (port == UART_NUM_MAX) return false;
1087 return units.add(unit, port);
1088}
1089#endif // __M5UNIFIED_HPP__
1090
1091#endif // !ARDUINO && ESP_PLATFORM
1093
1096#if defined(__M5UNIFIED_HPP__)
1098[[noreturn]] inline void failStop()
1099{
1100 M5.Display.fillScreen(0xF800 /* TFT_RED */);
1101 while (true) {
1102 m5::utility::delay(10000);
1103 }
1104}
1105#endif
1107
1108} // namespace wiring
1109} // namespace unit
1110} // namespace m5
1111#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