For the person who finds this later from a web search, I figured out an explanation. By default, the pins for the external I2C bus (GPIO1 and GPIO2) are not put into a usable power state. As the error message above states, the SDA pin has a low power signal being held which prevents other things from using this bus to communicate.
The simplest way for me to ensure the pins (and other things) are initialized is to import the m5stack/M5Unified library from PlatformIO, and the external component board_m5cores3 from https://github.com/m5stack/M5CoreS3-Esphome, then declare board_m5cores3: in the ESPHome YAML config:
esphome:
name: ${hostname}
friendly_name: ${friendlyname}
platformio_options:
board_build.flash_mode: dio
board_build.mcu: esp32s3
board_build.f_cpu: 240000000L
libraries:
- m5stack/M5Unified
external_components:
- source:
type: git
url: https://github.com/m5stack/M5CoreS3-Esphome
refresh: 0s
components: [board_m5cores3]
board_m5cores3:
This results in M5.begin() being called. The M5 namespace on which it's calling begin() is provided by the m5stack/M5Unified C++ library. This ensures physical buttons and hardware are initialized properly for this particular board.
The begin method is defined here. On L349-343:
auto board = _check_boardtype(Display.getBoard());
if (board == board_t::board_unknown) { board = cfg.fallback_board; }
_board = board;
_setup_pinmap(board);
_setup_i2c(board);
That call _setup_i2c is calling this and this from deeper within the M5Unified's I2C library. That seems to cause SDA to not be held in LOW state.