Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added USB CDC port drivers for ESP32, RP2, and STM32 platforms

### Changed
- ESP32: migrated both the `i2c` port driver and the `i2c` NIF-resource driver from the legacy,
now end-of-life (and slated for removal in IDF v7.0) `driver/i2c.h` API to the new
`driver/i2c_master.h` API. This is an internal implementation change with no expected impact
on the Erlang-level `i2c` API; `{error, esp_fail}` is still returned for NACK'd transactions
on all supported IDF versions
- Updated network type db() to dbm() to reflect the actual representation of the type
- Use ES6 modules for emscripten port, using .mjs suffix
- `ahttp_client` now returns `{error, {parser, incomplete_response}}` when a socket closes mid-response
Expand Down
8 changes: 8 additions & 0 deletions src/platforms/esp32/components/avm_builtins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
set(AVM_BUILTIN_COMPONENT_SRCS
"dac_driver.c"
"gpio_driver.c"
"i2c_bus_manager.c"
"i2c_driver.c"
"i2c_resource.c"
"ledc_nif.c"
Expand Down Expand Up @@ -74,6 +75,13 @@ if(CONFIG_AVM_ENABLE_USB_CDC_PORT_DRIVER)
endif()
endif()

# The (new, non-legacy) I2C master driver (driver/i2c_master.h) is provided by the "driver"
# component up to IDF 5.x, but starting with IDF 6.0 the legacy "driver" component no longer
# has a public dependency on it, so it must be linked explicitly.
if((CONFIG_AVM_ENABLE_I2C_PORT_DRIVER OR CONFIG_AVM_ENABLE_I2C_RESOURCE_NIFS) AND IDF_VERSION_MAJOR GREATER_EQUAL 6)
target_link_libraries(${COMPONENT_LIB} PRIVATE idf::esp_driver_i2c)
endif()

if (IDF_VERSION_MAJOR EQUAL 4)
idf_build_set_property(
LINK_OPTIONS "-Wl,--whole-archive ${CMAKE_CURRENT_BINARY_DIR}/lib${COMPONENT_NAME}.a -Wl,--no-whole-archive"
Expand Down
212 changes: 212 additions & 0 deletions src/platforms/esp32/components/avm_builtins/i2c_bus_manager.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
* This file is part of AtomVM.
*
* Copyright 2026 The AtomVM Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
*/

#include "include/i2c_bus_manager.h"

#include <stdlib.h>
#include <string.h>

#include <esp_log.h>

#include "utils.h"

#define TAG "i2c_bus_manager"

#define I2C_TX_BUFFER_INITIAL_CAP 16

esp_err_t i2c_bus_manager_open(
int i2c_port, int scl_io_num, int sda_io_num, i2c_master_bus_handle_t *out_bus_handle)
{
i2c_master_bus_config_t bus_config;
memset(&bus_config, 0, sizeof(bus_config));
bus_config.i2c_port = i2c_port;
bus_config.scl_io_num = scl_io_num;
bus_config.sda_io_num = sda_io_num;
bus_config.clk_source = I2C_CLK_SRC_DEFAULT;
// Matches the glitch filter length recommended by Espressif examples and
// used by other in-tree/vendored I2C master bus users.
bus_config.glitch_ignore_cnt = 7;
bus_config.flags.enable_internal_pullup = true;

return i2c_new_master_bus(&bus_config, out_bus_handle);
}

esp_err_t i2c_bus_manager_close(i2c_master_bus_handle_t bus_handle)
{
return i2c_del_master_bus(bus_handle);
}

static esp_err_t add_device(i2c_master_bus_handle_t bus_handle, uint16_t address,
uint32_t clock_speed_hz, i2c_master_dev_handle_t *out_dev_handle)
{
i2c_device_config_t dev_config;
memset(&dev_config, 0, sizeof(dev_config));
dev_config.dev_addr_length = I2C_ADDR_BIT_LEN_7;
dev_config.device_address = address;
dev_config.scl_speed_hz = clock_speed_hz;

return i2c_master_bus_add_device(bus_handle, &dev_config, out_dev_handle);
}

// The legacy `driver/i2c.h` API (via `i2c_master_cmd_begin()`) always reported
// a NACK'd transaction as `ESP_FAIL`. The new I2C master driver reports the
// very same condition as `ESP_ERR_INVALID_STATE` on IDF <= 5.5, and as
// `ESP_ERR_INVALID_RESPONSE` starting with IDF 6.0 (see the "I2C Master
// Driver Updates" section of the IDF 6.0 migration guide). To preserve a
// single, version-independent `{error, esp_fail}` contract for existing
// AtomVM applications (and the `i2c` NACK test suite) across all supported
// IDF versions, both codes are normalized to `ESP_FAIL` here.
//
// NOTE: because each transaction owns a freshly-added, then removed, device
// handle (see `add_device`/`remove_device`), a NACK is by far the most likely
// cause of either of these two codes in this driver's usage pattern.
static esp_err_t normalize_transaction_error(esp_err_t err)
{
if (err == ESP_ERR_INVALID_STATE || err == ESP_ERR_INVALID_RESPONSE) {
return ESP_FAIL;
}
return err;
}

static esp_err_t remove_device(i2c_master_dev_handle_t dev_handle, esp_err_t transaction_result)
{
esp_err_t err = i2c_master_bus_rm_device(dev_handle);
if (UNLIKELY(err != ESP_OK)) {
ESP_LOGW(TAG, "Failed to remove transient I2C device handle: err=%i", err);
}
// Prefer surfacing the transaction error (if any) over a remove-device
// error, since the former is more useful/actionable to the caller.
return (transaction_result != ESP_OK) ? transaction_result : err;
}

// `i2c_master_transmit()` rejects zero-length write buffers with
// `ESP_ERR_INVALID_ARG` (unlike the legacy driver, which happily emitted a
// START/address/STOP sequence with no data bytes in between). AtomVM's i2c
// API allows this (e.g. `i2c:write_bytes(I2C, Addr, <<>>)`, or an empty
// `begin_transmission/end_transmission` pair), commonly used as a
// device-presence/ACK probe. To preserve that behavior, a zero-length
// write is instead performed as an address-only probe via
// `i2c_master_probe()`, and its result is mapped onto the same
// `{ok, error}`/`{error, esp_fail}` contract a real transmit would have
// produced: `ESP_OK` on ACK, `ESP_FAIL` on NACK (`ESP_ERR_NOT_FOUND`).
static esp_err_t probe_address(
i2c_master_bus_handle_t bus_handle, uint16_t address, int xfer_timeout_ms)
{
esp_err_t err = i2c_master_probe(bus_handle, address, xfer_timeout_ms);
if (err == ESP_ERR_NOT_FOUND) {
return ESP_FAIL;
}
return err;
}

esp_err_t i2c_bus_manager_transmit(i2c_master_bus_handle_t bus_handle, uint16_t address,
uint32_t clock_speed_hz, const uint8_t *write_buffer, size_t write_size, int xfer_timeout_ms)
{
if (write_size == 0) {
return probe_address(bus_handle, address, xfer_timeout_ms);
}

i2c_master_dev_handle_t dev_handle;
esp_err_t err = add_device(bus_handle, address, clock_speed_hz, &dev_handle);
if (UNLIKELY(err != ESP_OK)) {
return err;
}

err = normalize_transaction_error(
i2c_master_transmit(dev_handle, write_buffer, write_size, xfer_timeout_ms));

return remove_device(dev_handle, err);
}

esp_err_t i2c_bus_manager_receive(i2c_master_bus_handle_t bus_handle, uint16_t address,
uint32_t clock_speed_hz, uint8_t *read_buffer, size_t read_size, int xfer_timeout_ms)
{
i2c_master_dev_handle_t dev_handle;
esp_err_t err = add_device(bus_handle, address, clock_speed_hz, &dev_handle);
if (UNLIKELY(err != ESP_OK)) {
return err;
}

err = normalize_transaction_error(
i2c_master_receive(dev_handle, read_buffer, read_size, xfer_timeout_ms));

return remove_device(dev_handle, err);
}

esp_err_t i2c_bus_manager_transmit_receive(i2c_master_bus_handle_t bus_handle, uint16_t address,
uint32_t clock_speed_hz, const uint8_t *write_buffer, size_t write_size,
uint8_t *read_buffer, size_t read_size, int xfer_timeout_ms)
{
i2c_master_dev_handle_t dev_handle;
esp_err_t err = add_device(bus_handle, address, clock_speed_hz, &dev_handle);
if (UNLIKELY(err != ESP_OK)) {
return err;
}

err = normalize_transaction_error(i2c_master_transmit_receive(
dev_handle, write_buffer, write_size, read_buffer, read_size, xfer_timeout_ms));

return remove_device(dev_handle, err);
}

void i2c_tx_buffer_init(struct I2CTxBuffer *buf)
{
buf->data = NULL;
buf->len = 0;
buf->cap = 0;
}

void i2c_tx_buffer_reset(struct I2CTxBuffer *buf)
{
free(buf->data);
buf->data = NULL;
buf->len = 0;
buf->cap = 0;
}

esp_err_t i2c_tx_buffer_append(struct I2CTxBuffer *buf, const uint8_t *data, size_t len)
{
if (len == 0) {
return ESP_OK;
}

if (buf->len + len > buf->cap) {
size_t new_cap = buf->cap == 0 ? I2C_TX_BUFFER_INITIAL_CAP : buf->cap * 2;
while (new_cap < buf->len + len) {
new_cap *= 2;
}
uint8_t *new_data = realloc(buf->data, new_cap);
if (IS_NULL_PTR(new_data)) {
return ESP_ERR_NO_MEM;
}
buf->data = new_data;
buf->cap = new_cap;
}

memcpy(buf->data + buf->len, data, len);
buf->len += len;

return ESP_OK;
}

esp_err_t i2c_tx_buffer_append_byte(struct I2CTxBuffer *buf, uint8_t byte)
{
return i2c_tx_buffer_append(buf, &byte, 1);
}
Loading
Loading