ESP-IDF 6.0 did not remove one thing. It removed four different things.

Ask anyone what changed in ESP-IDF 6.0 and you get "they removed the legacy drivers." That sentence is doing far too much work. It covers four different fates, and the difference between them is the difference between a five-minute fix and a rewrite.
I went through the migration guides and the v6.0 tree to sort them out.
Four fates, one word
Removed. The header is gone. The build fails. You port the code.
End-of-life but still shipping. The header is still there, still compiles, and prints a warning telling you the clock is running.
Moved out of the tree with its API unchanged. The component left ESP-IDF for the Component Registry. You add a dependency line.
Moved with code changes, renamed, or refactored. The peripheral is fine, but a component name, symbol, enum member, or implicit include changed underneath you.
Three of those four can break a build, and each demands a different fix. Treating them as one bucket is how a half-day migration turns into a week.

The nine that are actually gone
v6.0 shipped on 20 March 2026.1 These legacy drivers are removed outright:
ADC (driver/adc.h), DAC (driver/dac.h), I2S (driver/i2s.h), timer group
(driver/timer.h), PCNT (driver/pcnt.h), MCPWM (driver/mcpwm.h), RMT
(driver/rmt.h), sigma-delta (driver/sigmadelta.h), and temperature sensor
(driver/temp_sensor.h).2
The pattern is identical for each. The peripherals migration guide on ADC:
The legacy ADC driver
driver/adc.his deprecated since version 5.0 [...]. Starting from version 6.0, the legacy driver is completely removed.2
None of this was a surprise. Every one of the nine was deprecated in 5.0 and
carried a warning for a full major version before it went. The replacements are
already documented: esp_adc/adc_oneshot.h for ADC, driver/gptimer.h for the
timer group, driver/pulse_cnt.h for PCNT, driver/sdm.h for sigma-delta, and
driver/temperature_sensor.h for the temperature sensor.2
Six years of Stack Overflow answers still use the old ones.
I2C is the one everyone gets wrong
Legacy I2C was not removed in 6.0. It is the odd one out, and it is the claim I see stated backwards most often.
driver/i2c.h is still in the v6.0 tree at
components/driver/i2c/include/driver/i2c.h. It still compiles. What it does
now is shout at you, via a #pragma message guarded by a Kconfig option:
#if !CONFIG_I2C_SUPPRESS_DEPRECATE_WARN
#pragma message("\n" \
"================================ CRITICAL WARNING ================================\n" \
"This legacy I2C driver (driver/i2c.h) is officially END-OF-LIFE (EOL) as of ESP-IDF v6.0.\n" \
"\n" \
"1. NO SUPPORT: ESP-IDF will not provide updates, bug fixes, or security patches timely.\n" \
"2. PLANNED REMOVAL: This driver WILL BE REMOVED in ESP-IDF v7.0 (next major release).\n" \
"3. ACTION REQUIRED: Migrate to 'driver/i2c_master.h' or 'driver/i2c_slave.h' immediately.\n" \
That is verbatim from the v6.0 header.3 The migration guide says the same thing in calmer language: marked end-of-life in v6.0, "scheduled for removal in v7.0."2
So legacy I2C code builds against 6.0. It is also on a countdown, and it stops receiving fixes now rather than at removal. The old I2C slave driver is a separate story — that one was redesigned in 5.4 and the old version is gone.2
What the new I2C actually looks like
Here is the pair, both from Espressif's own examples, same sensor, same function name. Legacy, from the v5.1 tag:
static esp_err_t i2c_master_init(void)
{
int i2c_master_port = I2C_MASTER_NUM;
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MASTER_SDA_IO,
.scl_io_num = I2C_MASTER_SCL_IO,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = I2C_MASTER_FREQ_HZ,
};
i2c_param_config(i2c_master_port, &conf);
return i2c_driver_install(i2c_master_port, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0);
}
That is examples/peripherals/i2c/i2c_simple in v5.1.4 The same job in
examples/peripherals/i2c/i2c_basic at the v6.0 tag:
static void i2c_master_init(i2c_master_bus_handle_t *bus_handle, i2c_master_dev_handle_t *dev_handle)
{
i2c_master_bus_config_t bus_config = {
.i2c_port = I2C_MASTER_NUM,
.sda_io_num = I2C_MASTER_SDA_IO,
.scl_io_num = I2C_MASTER_SCL_IO,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, bus_handle));
i2c_device_config_t dev_config = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = MPU9250_SENSOR_ADDR,
.scl_speed_hz = I2C_MASTER_FREQ_HZ,
};
ESP_ERROR_CHECK(i2c_master_bus_add_device(*bus_handle, &dev_config, dev_handle));
}
Also verbatim.5 This is not a rename. The old model is a port number plus a
config struct, with the device address passed on every transfer. The new model
is a bus handle, then device handles hung off it, with clock speed and address
length as per-device properties. Reads go from
i2c_master_write_read_device(port, addr, ...) to
i2c_master_transmit_receive(dev_handle, ...).
Anything that mechanically swaps header names produces code that does not compile. The handles have to be threaded through every function that touches the bus.

Moved is not removed
This is the other half of the confusion, and it goes the opposite way: people see a component vanish from the tree and start rewriting code that is completely fine.
ESP-MQTT left ESP-IDF for the Component Registry. The protocols migration guide is explicit about what that costs you:
Include headers and APIs remain the same (
mqtt_client.h), but the component is fetched via the Component Manager.6
You run idf.py add-dependency espressif/mqtt and carry on. cJSON went the same
way, and the guide's heading for it is literally "No Code Changes Required."6
wifi_provisioning moved out and was renamed network_provisioning.1 The
Ethernet PHY and SPI module drivers moved to a separate repository, and that one
does remove APIs — esp_eth_phy_new_ip101() and friends are gone from the
core tree.7
Same word, "removed", covering a dependency line in one case and deleted functions in another.
The fourth fate is the one that ambushes you
The last category has no migration section of its own because it is scattered through everything. A symbol moves and takes an unrelated build down with it.
Issue 18410, filed ten days after v6.0 shipped: PERIPH_I2S1_MODULE is no
longer in soc/periph_defs.h. The reporter notes it "was well defined and
working fine in v5.5.3."8 Their I2S peripheral logic was not the problem. A
constant they passed to periph_module_enable() stopped existing, as fallout
from the legacy I2S removal.
Elsewhere in the same guide: sdm_channel_set_duty() became
sdm_channel_set_pulse_density(), the io_od_mode member disappeared from RMT
and MCPWM configs in favor of calling gpio_od_enable() yourself, LCD GPIO
numbers went from int to gpio_num_t, and driver headers stopped implicitly
including FreeRTOS headers — so code that relied on that inclusion now needs the
include it always should have had.2
Espressif's own examples were not immune. Issue 18379 reports
lp_core_build_system_example failing to build on v6.0 for the ESP32-S3 with
ulp_lp_core.h: No such file or directory.9 When the in-tree examples have
gaps at a major release, the code out in the world has more.
Why this is worse for an agent than for you
When you hit driver/adc.h: No such file or directory, you search, you find the
migration guide, you notice it is scoped to 6.0, and you port. You carry the
version in your head the whole time.
A model working from its training corpus does not have that anchor by default.
The volume of ESP-IDF code written against 5.x and earlier is enormous and has
been accumulating since 2016. v6.0 shipped in March. Many older tutorials,
forum answers and vendor samples still use i2c_param_config and
i2c_driver_install. Weight of material alone favors the old API, and much of
that material is not labeled with a version at the point where it matters.
I have not measured agent failure rates on v6.0 migration tasks. The closest receipt is IoT-SkillsBench, where LLM-written skills dropped the ESP-IDF score from 31/42 to 27/42 and the authors found that synthesized skills can reinforce incorrect assumptions about ESP-IDF-specific behavior.10 That was measured before v6.0 added another version boundary.
The mitigation is not clever prompting. It is the same one as always: make the loop close on something real. A build against the pinned IDF version answers "does this API exist" in seconds and does not care what the corpus thinks. That is cheaper than any amount of context stuffing, and it is the only step that catches the fourth fate, where nothing about the code looks wrong until the linker disagrees.
Compilation is the floor, not the ceiling. Getting past the compiler on ESP32 still leaves you with peripherals a simulator will not model — Espressif's own QEMU matrix already marks I2C, I2S, RMT and WiFi unsupported — and substituting mocks for the driver only tests your call contract, not the chip.
The actual checklist
Before you touch a line: figure out which of the four fates each of your build errors belongs to. Removed means port. End-of-life means schedule it, and note that legacy I2C buys you until v7.0 and no longer. A component moved with its API unchanged usually means adding a dependency. A move with API changes, a rename or a refactor means reading that component's migration section instead of guessing from the release-note verb.
Then pin your IDF version somewhere a tool can read it. After March, "what does the header look like" stopped being a question with one answer.
Sources
Footnotes
-
ESP-IDF Release v6.0, published 2026-03-20. https://github.com/espressif/esp-idf/releases/tag/v6.0 ↩ ↩2
-
ESP-IDF v6.0 documentation, "Migration from 5.x to 6.0 — Peripherals." https://docs.espressif.com/projects/esp-idf/en/v6.0/esp32/migration-guides/release-6.x/6.0/peripherals.html ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
ESP-IDF v6.0,
components/driver/i2c/include/driver/i2c.h. https://github.com/espressif/esp-idf/blob/v6.0/components/driver/i2c/include/driver/i2c.h ↩ -
ESP-IDF v5.1,
examples/peripherals/i2c/i2c_simple/main/i2c_simple_main.c. https://github.com/espressif/esp-idf/blob/v5.1/examples/peripherals/i2c/i2c_simple/main/i2c_simple_main.c ↩ -
ESP-IDF v6.0,
examples/peripherals/i2c/i2c_basic/main/i2c_basic_example_main.c. https://github.com/espressif/esp-idf/blob/v6.0/examples/peripherals/i2c/i2c_basic/main/i2c_basic_example_main.c ↩ -
ESP-IDF v6.0 documentation, "Migration from 5.x to 6.0 — Protocols." https://docs.espressif.com/projects/esp-idf/en/v6.0/esp32/migration-guides/release-6.x/6.0/protocols.html ↩ ↩2
-
ESP-IDF v6.0 documentation, "Migration from 5.x to 6.0 — Networking." https://docs.espressif.com/projects/esp-idf/en/v6.0/esp32/migration-guides/release-6.x/6.0/networking.html ↩
-
ESP-IDF issue #18410, "In V6.0.0: PERIPH_I2S1_MODULE disappeared from the components/soc/esp32/include/soc/periph_defs.h," opened 2026-03-30. https://github.com/espressif/esp-idf/issues/18410 ↩
-
ESP-IDF issue #18379, on
lp_core_build_system_examplefailing to build for ESP-IDF v6.0 on esp32-s3 with error "ulp_lp_core.h: No such file or directory," opened 2026-03-22. https://github.com/espressif/esp-idf/issues/18379 ↩ -
Li et al., "Skilled AI Agents for Embedded and IoT Systems Development," arXiv:2603.19583, 2026-03-20. https://arxiv.org/abs/2603.19583 ↩