Skip to content
Chiplab logo
Chiplab logo
Back to Now

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.

Diagram sorting ESP-IDF 6.0 changes into removed drivers, end-of-life drivers, components moved unchanged, and components or symbols moved or refactored

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.h is 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.

Diagram contrasting the legacy ESP-IDF I2C port-number API with the new bus-handle and device-handle model in driver/i2c_master.h

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

  1. ESP-IDF Release v6.0, published 2026-03-20. https://github.com/espressif/esp-idf/releases/tag/v6.0 2

  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

  3. 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

  4. 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

  5. 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

  6. 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

  7. 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

  8. 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

  9. ESP-IDF issue #18379, on lp_core_build_system_example failing 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

  10. Li et al., "Skilled AI Agents for Embedded and IoT Systems Development," arXiv:2603.19583, 2026-03-20. https://arxiv.org/abs/2603.19583

Daniel Frassinelli
Published Aug 14, 2026