From 42233fa80d0c898a0f80874da5bc607db5393200 Mon Sep 17 00:00:00 2001 From: Peter M Date: Tue, 12 May 2026 12:37:22 +0200 Subject: [PATCH 1/2] esp32: fix I2C NIF resource cleanup and error paths Track open/closed I2C NIF resource state, clean up command handles on error paths, and reject closes while a transmission is active. Ensure driver-delete failures do not leave the resource half-closed, and document the ESP32 I2C fixes in the changelog. Signed-off-by: Peter M --- CHANGELOG.md | 1 + .../components/avm_builtins/i2c_resource.c | 234 ++++++++++++++---- 2 files changed, 189 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2129405f44..fed9f8dd1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a bug in `supervisor` handling of failing child - Fixed two bugs related to closing fds in `atomvm:subprocess/4` - Fixed `erlang:localtime/1` memory leak, use-after-free, and TZ restore bugs on newlib/picolibc +- Fixed ESP32 I2C driver resource leaks, half-closed state, and close-during-transmission errors ## [0.7.0-alpha.1] - 2026-04-06 diff --git a/src/platforms/esp32/components/avm_builtins/i2c_resource.c b/src/platforms/esp32/components/avm_builtins/i2c_resource.c index 66e0af358a..c8f906c85e 100644 --- a/src/platforms/esp32/components/avm_builtins/i2c_resource.c +++ b/src/platforms/esp32/components/avm_builtins/i2c_resource.c @@ -85,6 +85,35 @@ static term create_error_tuple(Context *ctx, term reason) return create_pair(ctx, ERROR_ATOM, reason); } +static bool is_i2c_resource_open(const struct I2CResource *rsrc_obj) +{ + return rsrc_obj->i2c_num != I2C_NUM_MAX; +} + +static void reset_i2c_transmission_state(struct I2CResource *rsrc_obj) +{ + if (rsrc_obj->cmd != NULL) { + i2c_cmd_link_delete(rsrc_obj->cmd); + rsrc_obj->cmd = NULL; + } + rsrc_obj->transmitting_pid = term_invalid_term(); +} + +static esp_err_t close_i2c_resource(struct I2CResource *rsrc_obj) +{ + if (!is_i2c_resource_open(rsrc_obj)) { + return ESP_OK; + } + + esp_err_t err = i2c_driver_delete(rsrc_obj->i2c_num); + if (err == ESP_OK) { + rsrc_obj->i2c_num = I2C_NUM_MAX; + reset_i2c_transmission_state(rsrc_obj); + } + + return err; +} + static bool is_i2c_resource(GlobalContext *global, term t) { bool ret = term_is_tuple(t) @@ -110,6 +139,15 @@ static bool to_i2c_resource(term i2c_resource, struct I2CResource **rsrc_obj, Co return true; } +static bool to_open_i2c_resource(term i2c_resource, struct I2CResource **rsrc_obj, Context *ctx) +{ + if (!to_i2c_resource(i2c_resource, rsrc_obj, ctx)) { + return false; + } + + return is_i2c_resource_open(*rsrc_obj); +} + // // i2c:open_nif/1 // @@ -214,10 +252,11 @@ static term nif_i2c_open(Context *ctx, int argc, term argv[]) } rsrc_obj->transmitting_pid = term_invalid_term(); rsrc_obj->i2c_num = i2c_num; + rsrc_obj->cmd = NULL; rsrc_obj->send_timeout_ms = send_timeout_ms_val; if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { - i2c_driver_delete(i2c_num); + close_i2c_resource(rsrc_obj); enif_release_resource(rsrc_obj); ESP_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -232,7 +271,7 @@ static term nif_i2c_open(Context *ctx, int argc, term argv[]) // {'$i2c', Resource :: resource(), Ref :: reference()} :: i2c() size_t requested_size = TUPLE_SIZE(3) + REF_SIZE; if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, &obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - i2c_driver_delete(i2c_num); + close_i2c_resource(rsrc_obj); ESP_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -255,20 +294,40 @@ static term nif_i2c_close(Context *ctx, int argc, term argv[]) { TRACE("nif_close\n"); UNUSED(argc); + GlobalContext *global = ctx->global; // // extract the resource // term i2c_resource = argv[0]; struct I2CResource *rsrc_obj; - if (UNLIKELY(!to_i2c_resource(i2c_resource, &rsrc_obj, ctx))) { + if (UNLIKELY(!to_open_i2c_resource(i2c_resource, &rsrc_obj, ctx))) { ESP_LOGE(TAG, "Failed to convert i2c_resource"); RAISE_ERROR(BADARG_ATOM); } + // + // reject close if another process has an active transmission + // + if (UNLIKELY(!term_is_invalid_term(rsrc_obj->transmitting_pid))) { + ESP_LOGE(TAG, "nif_close: A process is in the process of transmitting."); + + // {error, {einprogress, Pid :: pid()}} + if (UNLIKELY(memory_ensure_free(ctx, 2 * TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + ESP_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); + return OUT_OF_MEMORY_ATOM; + } + term reason = create_pair( + ctx, + globalcontext_make_atom(global, EINPROGRESS_ATOMSTR), + rsrc_obj->transmitting_pid + ); + return create_error_tuple(ctx, reason); + } + esp_err_t err; - err = i2c_driver_delete(rsrc_obj->i2c_num); + err = close_i2c_resource(rsrc_obj); CHECK_ERROR(ctx, err, "nif_close; Failed to delete driver"); return OK_ATOM; @@ -289,7 +348,7 @@ static term nif_i2c_write_bytes(Context *ctx, int argc, term argv[]) // term i2c_resource = argv[0]; struct I2CResource *rsrc_obj; - if (UNLIKELY(!to_i2c_resource(i2c_resource, &rsrc_obj, ctx))) { + if (UNLIKELY(!to_open_i2c_resource(i2c_resource, &rsrc_obj, ctx))) { ESP_LOGE(TAG, "Failed to convert i2c_resource"); RAISE_ERROR(BADARG_ATOM); } @@ -351,27 +410,49 @@ static term nif_i2c_write_bytes(Context *ctx, int argc, term argv[]) esp_err_t err; i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - { - err = i2c_master_start(cmd); - CHECK_ERROR(ctx, err, "nif_write_bytes; enqueue start bit"); - err = i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, ACK_ENABLE); - CHECK_ERROR(ctx, err, "nif_write_bytes; enqueue I2C bus address"); + err = i2c_master_start(cmd); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "nif_write_bytes; enqueue start bit: err: %i.", err); + goto cleanup_write; + } - if (!term_is_invalid_term(register_)) { - err = i2c_master_write_byte(cmd, register_address, ACK_ENABLE); - CHECK_ERROR(ctx, err, "nif_write_bytes; enqueue register address"); + err = i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, ACK_ENABLE); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "nif_write_bytes; enqueue I2C bus address: err: %i.", err); + goto cleanup_write; + } + + if (!term_is_invalid_term(register_)) { + err = i2c_master_write_byte(cmd, register_address, ACK_ENABLE); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "nif_write_bytes; enqueue register address: err: %i.", err); + goto cleanup_write; } + } - err = i2c_master_write(cmd, buf, data_len, ACK_ENABLE); - CHECK_ERROR(ctx, err, "nif_write_bytes; enqueue write data"); + err = i2c_master_write(cmd, buf, data_len, ACK_ENABLE); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "nif_write_bytes; enqueue write data: err: %i.", err); + goto cleanup_write; + } - err = i2c_master_stop(cmd); - CHECK_ERROR(ctx, err, "nif_write_bytes; enqueue stop bit"); + err = i2c_master_stop(cmd); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "nif_write_bytes; enqueue stop bit: err: %i.", err); + goto cleanup_write; } + err = i2c_master_cmd_begin(rsrc_obj->i2c_num, cmd, MS_TO_TICKS(rsrc_obj->send_timeout_ms)); + +cleanup_write: i2c_cmd_link_delete(cmd); - CHECK_ERROR(ctx, err, "nif_write_bytes; run the command"); + if (UNLIKELY(err != ESP_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + return OUT_OF_MEMORY_ATOM; + } + return create_error_tuple(ctx, esp_err_to_term(ctx->global, err)); + } // // return result @@ -395,7 +476,7 @@ static term nif_i2c_read_bytes(Context *ctx, int argc, term argv[]) // term i2c_resource = argv[0]; struct I2CResource *rsrc_obj; - if (UNLIKELY(!to_i2c_resource(i2c_resource, &rsrc_obj, ctx))) { + if (UNLIKELY(!to_open_i2c_resource(i2c_resource, &rsrc_obj, ctx))) { ESP_LOGE(TAG, "Failed to convert i2c_resource"); RAISE_ERROR(BADARG_ATOM); } @@ -456,28 +537,59 @@ static term nif_i2c_read_bytes(Context *ctx, int argc, term argv[]) esp_err_t err; i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - { - err = i2c_master_start(cmd); - CHECK_ERROR(ctx, err, "nif_read_bytes; enqueue start bit"); - if (!term_is_invalid_term(register_)) { - err = i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, ACK_ENABLE); - err = i2c_master_write_byte(cmd, register_address, ACK_ENABLE); - err = i2c_master_start(cmd); + err = i2c_master_start(cmd); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "nif_read_bytes; enqueue start bit: err: %i.", err); + goto cleanup_read; + } + + if (!term_is_invalid_term(register_)) { + err = i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, ACK_ENABLE); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "nif_read_bytes; enqueue write address: err: %i.", err); + goto cleanup_read; } + err = i2c_master_write_byte(cmd, register_address, ACK_ENABLE); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "nif_read_bytes; enqueue register address: err: %i.", err); + goto cleanup_read; + } + err = i2c_master_start(cmd); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "nif_read_bytes; enqueue repeated start: err: %i.", err); + goto cleanup_read; + } + } - err = i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_READ, ACK_ENABLE); - CHECK_ERROR(ctx, err, "nif_read_bytes; enqueue I2C bus address"); + err = i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_READ, ACK_ENABLE); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "nif_read_bytes; enqueue I2C bus address: err: %i.", err); + goto cleanup_read; + } - err = i2c_master_read(cmd, buf, read_count, I2C_MASTER_LAST_NACK); - CHECK_ERROR(ctx, err, "nif_read_bytes; enqueue write data"); + err = i2c_master_read(cmd, buf, read_count, I2C_MASTER_LAST_NACK); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "nif_read_bytes; enqueue read data: err: %i.", err); + goto cleanup_read; + } - err = i2c_master_stop(cmd); - CHECK_ERROR(ctx, err, "nif_write_bytes; enqueue stop bit"); + err = i2c_master_stop(cmd); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "nif_read_bytes; enqueue stop bit: err: %i.", err); + goto cleanup_read; } + err = i2c_master_cmd_begin(rsrc_obj->i2c_num, cmd, MS_TO_TICKS(rsrc_obj->send_timeout_ms)); + +cleanup_read: i2c_cmd_link_delete(cmd); - CHECK_ERROR(ctx, err, "nif_write_bytes; run the command"); + if (UNLIKELY(err != ESP_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + return OUT_OF_MEMORY_ATOM; + } + return create_error_tuple(ctx, esp_err_to_term(ctx->global, err)); + } // // return result @@ -502,7 +614,7 @@ static term nif_i2c_begin_transmission(Context *ctx, int argc, term argv[]) // term i2c_resource = argv[0]; struct I2CResource *rsrc_obj; - if (UNLIKELY(!to_i2c_resource(i2c_resource, &rsrc_obj, ctx))) { + if (UNLIKELY(!to_open_i2c_resource(i2c_resource, &rsrc_obj, ctx))) { ESP_LOGE(TAG, "Failed to convert i2c_resource"); RAISE_ERROR(BADARG_ATOM); } @@ -539,17 +651,30 @@ static term nif_i2c_begin_transmission(Context *ctx, int argc, term argv[]) esp_err_t err; i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - { - err = i2c_master_start(cmd); - CHECK_ERROR(ctx, err, "nif_write_bytes; enqueue start bit"); - err = i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, ACK_ENABLE); - CHECK_ERROR(ctx, err, "nif_write_bytes; enqueue I2C bus address"); + err = i2c_master_start(cmd); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "nif_begin_transmission; enqueue start bit: err: %i.", err); + goto cleanup_begin; } + + err = i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, ACK_ENABLE); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "nif_begin_transmission; enqueue I2C bus address: err: %i.", err); + goto cleanup_begin; + } + rsrc_obj->transmitting_pid = term_from_local_process_id(ctx->process_id); rsrc_obj->cmd = cmd; return OK_ATOM; + +cleanup_begin: + i2c_cmd_link_delete(cmd); + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + return OUT_OF_MEMORY_ATOM; + } + return create_error_tuple(ctx, esp_err_to_term(ctx->global, err)); } // @@ -567,7 +692,7 @@ static term nif_i2c_enqueue_write_bytes(Context *ctx, int argc, term argv[]) // term i2c_resource = argv[0]; struct I2CResource *rsrc_obj; - if (UNLIKELY(!to_i2c_resource(i2c_resource, &rsrc_obj, ctx))) { + if (UNLIKELY(!to_open_i2c_resource(i2c_resource, &rsrc_obj, ctx))) { ESP_LOGE(TAG, "Failed to convert i2c_resource"); RAISE_ERROR(BADARG_ATOM); } @@ -599,7 +724,14 @@ static term nif_i2c_enqueue_write_bytes(Context *ctx, int argc, term argv[]) esp_err_t err; for (size_t i = 0; i < len; ++i) { err = i2c_master_write_byte(rsrc_obj->cmd, buf[i], ACK_ENABLE); - CHECK_ERROR(ctx, err, "nif_enqueue_write_bytes; enqueue i2c_master_write_byte"); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "nif_enqueue_write_bytes; enqueue i2c_master_write_byte: err: %i.", err); + reset_i2c_transmission_state(rsrc_obj); + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + return OUT_OF_MEMORY_ATOM; + } + return create_error_tuple(ctx, esp_err_to_term(ctx->global, err)); + } } return OK_ATOM; @@ -620,7 +752,7 @@ static term nif_i2c_end_transmission(Context *ctx, int argc, term argv[]) // term i2c_resource = argv[0]; struct I2CResource *rsrc_obj; - if (UNLIKELY(!to_i2c_resource(i2c_resource, &rsrc_obj, ctx))) { + if (UNLIKELY(!to_open_i2c_resource(i2c_resource, &rsrc_obj, ctx))) { ESP_LOGE(TAG, "Failed to convert i2c_resource"); RAISE_ERROR(BADARG_ATOM); } @@ -651,13 +783,23 @@ static term nif_i2c_end_transmission(Context *ctx, int argc, term argv[]) esp_err_t err; err = i2c_master_stop(rsrc_obj->cmd); - CHECK_ERROR(ctx, err, "nif_write_bytes; enqueue stop bit"); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "nif_end_transmission; enqueue stop bit: err: %i.", err); + goto cleanup_end; + } err = i2c_master_cmd_begin(rsrc_obj->i2c_num, rsrc_obj->cmd, MS_TO_TICKS(rsrc_obj->send_timeout_ms)); - i2c_cmd_link_delete(rsrc_obj->cmd); - CHECK_ERROR(ctx, err, "nif_write_bytes; run the command"); +cleanup_end: + i2c_cmd_link_delete(rsrc_obj->cmd); + rsrc_obj->cmd = NULL; rsrc_obj->transmitting_pid = term_invalid_term(); + if (UNLIKELY(err != ESP_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + return OUT_OF_MEMORY_ATOM; + } + return create_error_tuple(ctx, esp_err_to_term(ctx->global, err)); + } return OK_ATOM; } @@ -671,7 +813,7 @@ static void i2c_resource_dtor(ErlNifEnv *caller_env, void *obj) UNUSED(caller_env); struct I2CResource *rsrc_obj = (struct I2CResource *) obj; - esp_err_t err = i2c_driver_delete(rsrc_obj->i2c_num); + esp_err_t err = close_i2c_resource(rsrc_obj); if (UNLIKELY(err != ESP_OK)) { ESP_LOGW(TAG, "Failed to delete driver in resource d'tor. err=%i", err); } From 9a4b01b25edbddab38820272e848bca9c781b30e Mon Sep 17 00:00:00 2001 From: Peter M Date: Tue, 12 May 2026 12:38:34 +0200 Subject: [PATCH 2/2] test: add BMP180-backed ESP32 I2C simulator coverage Wire a BMP180 into the ESP32 Wokwi simulator and add an Erlang test that exercises I2C through both port-driver and NIF paths. Cover successful sensor reads, NACK recovery, split transactions, and ownership-contention errors. Signed-off-by: Peter M --- .../test/main/test_erl_sources/CMakeLists.txt | 2 + .../test/main/test_erl_sources/test_i2c.erl | 303 ++++++++++++++++++ src/platforms/esp32/test/main/test_main.c | 7 + .../esp32/test/sim_boards/diagram.esp32.json | 13 +- 4 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 src/platforms/esp32/test/main/test_erl_sources/test_i2c.erl diff --git a/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt b/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt index 43b2c44cb6..d528ea2eff 100644 --- a/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt +++ b/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt @@ -61,6 +61,7 @@ endfunction() compile_erlang(test_esp_partition) compile_erlang(test_esp_timer_get_time) compile_erlang(test_file) +compile_erlang(test_i2c) compile_erlang(test_wifi_example) compile_erlang(test_wifi_managed) compile_erlang(test_list_to_atom) @@ -85,6 +86,7 @@ set(erlang_test_beams test_esp_partition.beam test_esp_timer_get_time.beam test_file.beam + test_i2c.beam test_wifi_example.beam test_wifi_managed.beam test_list_to_atom.beam diff --git a/src/platforms/esp32/test/main/test_erl_sources/test_i2c.erl b/src/platforms/esp32/test/main/test_erl_sources/test_i2c.erl new file mode 100644 index 0000000000..88264599f6 --- /dev/null +++ b/src/platforms/esp32/test/main/test_erl_sources/test_i2c.erl @@ -0,0 +1,303 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Peter M +% +% 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 +% + +-module(test_i2c). + +-export([start/0]). + +-define(BMP180_ADDRESS, 16#77). +-define(NACK_ADDRESS, 16#76). +-define(BMP180_CHIP_ID_REGISTER, 16#D0). +-define(BMP180_CHIP_ID, 16#55). +-define(BMP180_CALIBRATION_REGISTER, 16#AA). +-define(BMP180_CALIBRATION_SIZE, 22). +-define(BMP180_CONTROL_REGISTER, 16#F4). +-define(BMP180_DATA_REGISTER, 16#F6). +-define(BMP180_TEMPERATURE_COMMAND, 16#2E). +-define(BMP180_PRESSURE_COMMAND_OSS0, 16#34). + +start() -> + ok = run_case(false), + ok = run_case(true), + ok. + +run_case(UseNif) -> + I2C = i2c:open(config(UseNif)), + try + ok = test_einprogress_errors(I2C), + ok = test_nack_errors(I2C), + ok = test_chip_id(I2C), + Calibration = test_calibration(I2C), + SplitWriteTemp = test_split_transaction_temperature_read(I2C), + DirectWriteTemp = test_direct_write_temperature_read(I2C), + true = erlang:abs(SplitWriteTemp - DirectWriteTemp) =< 2, + Pressure = test_register_write_pressure_read(I2C), + ok = test_compensated_values(Calibration, DirectWriteTemp, Pressure) + after + ok = i2c:close(I2C) + end. + +config(UseNif) -> + CommonConfig = [ + {sda, 21}, + {scl, 22}, + {clock_speed_hz, 100000}, + {peripheral, "i2c0"} + ], + case UseNif of + true -> + [{use_nif, true}, {send_timeout_ms, 1000} | CommonConfig]; + false -> + CommonConfig + end. + +test_einprogress_errors(I2C) -> + {OwnerPid, OwnerRef} = start_transaction_holder(I2C), + try + {error, {einprogress, OwnerPid}} = i2c:begin_transmission(I2C, ?BMP180_ADDRESS), + {error, {einprogress, OwnerPid}} = i2c:write_byte(I2C, 16#00), + {error, {einprogress, OwnerPid}} = i2c:write_bytes(I2C, <<16#00>>), + {error, {einprogress, OwnerPid}} = i2c:end_transmission(I2C), + {error, {einprogress, OwnerPid}} = i2c:read_bytes(I2C, ?BMP180_ADDRESS, 1), + {error, {einprogress, OwnerPid}} = i2c:read_bytes( + I2C, ?BMP180_ADDRESS, ?BMP180_CHIP_ID_REGISTER, 1 + ), + {error, {einprogress, OwnerPid}} = i2c:write_bytes(I2C, ?BMP180_ADDRESS, <<16#00>>), + {error, {einprogress, OwnerPid}} = + i2c:write_bytes( + I2C, ?BMP180_ADDRESS, ?BMP180_CONTROL_REGISTER, ?BMP180_TEMPERATURE_COMMAND + ), + ok + after + ok = stop_transaction_holder(OwnerPid, OwnerRef) + end. + +start_transaction_holder(I2C) -> + Parent = self(), + {Pid, Ref} = erlang:spawn_monitor(fun() -> transaction_holder(Parent, I2C) end), + receive + {Pid, ready} -> + {Pid, Ref}; + {'DOWN', Ref, process, Pid, Reason} -> + erlang:error({transaction_holder_failed, Reason}) + after 5000 -> + erlang:error(transaction_holder_start_timeout) + end. + +stop_transaction_holder(Pid, Ref) -> + Pid ! {self(), finish}, + receive + {Pid, finished} -> + receive + {'DOWN', Ref, process, Pid, normal} -> + ok + after 5000 -> + erlang:error(transaction_holder_stop_timeout) + end; + {'DOWN', Ref, process, Pid, normal} -> + receive + {Pid, finished} -> + ok + after 0 -> + ok + end; + {'DOWN', Ref, process, Pid, Reason} -> + receive + {Pid, finished} -> + ok + after 0 -> + ok + end, + erlang:error({transaction_holder_failed, Reason}) + after 5000 -> + erlang:error(transaction_holder_stop_timeout) + end. + +transaction_holder(Parent, I2C) -> + ok = i2c:begin_transmission(I2C, ?BMP180_ADDRESS), + ok = i2c:write_byte(I2C, ?BMP180_CHIP_ID_REGISTER), + Parent ! {self(), ready}, + receive + {Parent, finish} -> + ok = i2c:end_transmission(I2C), + Parent ! {self(), finished} + after 5000 -> + erlang:error(transaction_holder_finish_timeout) + end. + +test_nack_errors(I2C) -> + % 0x76 is adjacent to the BMP180's 0x77 address and is unused in the + % current simulator diagram, so transactions there should NACK. + {error, esp_fail} = i2c:read_bytes(I2C, ?NACK_ADDRESS, 1), + ok = assert_chip_id_via_register_read(I2C), + {error, esp_fail} = i2c:write_bytes(I2C, ?NACK_ADDRESS, <<16#00>>), + ok = assert_chip_id_via_register_read(I2C), + ok = i2c:begin_transmission(I2C, ?NACK_ADDRESS), + ok = i2c:write_byte(I2C, 16#00), + {error, esp_fail} = i2c:end_transmission(I2C), + ok = assert_chip_id_via_pointer_read(I2C), + ok. + +test_chip_id(I2C) -> + ok = assert_chip_id_via_register_read(I2C), + ok = assert_chip_id_via_pointer_read(I2C), + ok. + +assert_chip_id_via_register_read(I2C) -> + {ok, <>} = i2c:read_bytes(I2C, ?BMP180_ADDRESS, ?BMP180_CHIP_ID_REGISTER, 1), + ok. + +assert_chip_id_via_pointer_read(I2C) -> + ok = set_register_pointer(I2C, ?BMP180_CHIP_ID_REGISTER), + {ok, <>} = i2c:read_bytes(I2C, ?BMP180_ADDRESS, 1), + ok. + +test_calibration(I2C) -> + {ok, Calibration} = + i2c:read_bytes( + I2C, ?BMP180_ADDRESS, ?BMP180_CALIBRATION_REGISTER, ?BMP180_CALIBRATION_SIZE + ), + true = byte_size(Calibration) =:= ?BMP180_CALIBRATION_SIZE, + true = Calibration =/= <<0:(?BMP180_CALIBRATION_SIZE * 8)>>, + decode_calibration(Calibration). + +test_split_transaction_temperature_read(I2C) -> + ok = i2c:begin_transmission(I2C, ?BMP180_ADDRESS), + ok = i2c:write_byte(I2C, ?BMP180_CONTROL_REGISTER), + ok = i2c:write_bytes(I2C, <>), + ok = i2c:end_transmission(I2C), + timer:sleep(10), + {ok, <>} = i2c:read_bytes( + I2C, ?BMP180_ADDRESS, ?BMP180_DATA_REGISTER, 2 + ), + true = Temp > 0, + Temp. + +test_direct_write_temperature_read(I2C) -> + ok = i2c:write_bytes( + I2C, ?BMP180_ADDRESS, <> + ), + timer:sleep(10), + ok = set_register_pointer(I2C, ?BMP180_DATA_REGISTER), + {ok, <>} = i2c:read_bytes(I2C, ?BMP180_ADDRESS, 2), + true = Temp > 0, + Temp. + +test_register_write_pressure_read(I2C) -> + ok = i2c:write_bytes( + I2C, ?BMP180_ADDRESS, ?BMP180_CONTROL_REGISTER, ?BMP180_PRESSURE_COMMAND_OSS0 + ), + timer:sleep(10), + {ok, <>} = i2c:read_bytes(I2C, ?BMP180_ADDRESS, ?BMP180_DATA_REGISTER, 3), + Pressure = ((Msb bsl 16) bor (Lsb bsl 8) bor Xlsb) bsr 8, + true = Pressure > 0, + Pressure. + +set_register_pointer(I2C, Register) -> + ok = i2c:begin_transmission(I2C, ?BMP180_ADDRESS), + ok = i2c:write_byte(I2C, Register), + ok = i2c:end_transmission(I2C). + +decode_calibration( + << + AC1:16/signed-big, + AC2:16/signed-big, + AC3:16/signed-big, + AC4:16/unsigned-big, + AC5:16/unsigned-big, + AC6:16/unsigned-big, + B1:16/signed-big, + B2:16/signed-big, + MB:16/signed-big, + MC:16/signed-big, + MD:16/signed-big + >> +) -> + #{ + ac1 => AC1, + ac2 => AC2, + ac3 => AC3, + ac4 => AC4, + ac5 => AC5, + ac6 => AC6, + b1 => B1, + b2 => B2, + mb => MB, + mc => MC, + md => MD + }. + +test_compensated_values(Calibration, TemperatureRaw, PressureRaw) -> + Temperature = compensate_temperature(Calibration, TemperatureRaw), + Pressure = compensate_pressure(Calibration, TemperatureRaw, PressureRaw), + true = Temperature >= 200, + true = Temperature =< 280, + true = Pressure >= 100000, + true = Pressure =< 103000, + ok. + +compensate_temperature(#{ac5 := AC5, ac6 := AC6, mc := MC, md := MD}, TemperatureRaw) -> + X1 = ((TemperatureRaw - AC6) * AC5) bsr 15, + X2 = (MC bsl 11) div (X1 + MD), + B5 = X1 + X2, + (B5 + 8) bsr 4. + +compensate_pressure(Calibration, TemperatureRaw, PressureRaw) -> + #{ + ac1 := AC1, + ac2 := AC2, + ac3 := AC3, + ac4 := AC4, + ac5 := AC5, + ac6 := AC6, + b1 := B1, + b2 := B2, + mc := MC, + md := MD + } = Calibration, + Oss = 0, + + X1 = ((TemperatureRaw - AC6) * AC5) bsr 15, + X2 = (MC bsl 11) div (X1 + MD), + B5 = X1 + X2, + B6 = B5 - 4000, + + X1_1 = (B2 * ((B6 * B6) bsr 12)) bsr 11, + X2_1 = (AC2 * B6) bsr 11, + X3_1 = X1_1 + X2_1, + B3 = ((((AC1 * 4) + X3_1) bsl Oss) + 2) div 4, + + X1_2 = (AC3 * B6) bsr 13, + X2_2 = (B1 * ((B6 * B6) bsr 12)) bsr 16, + X3_2 = (X1_2 + X2_2 + 2) bsr 2, + B4 = (AC4 * (X3_2 + 32768)) bsr 15, + B7 = (PressureRaw - B3) * (50000 bsr Oss), + + Pressure0 = + case B7 < 16#80000000 of + true -> + (B7 * 2) div B4; + false -> + (B7 div B4) * 2 + end, + + X1_3 = ((Pressure0 bsr 8) * (Pressure0 bsr 8) * 3038) bsr 16, + X2_3 = (-7357 * Pressure0) bsr 16, + Pressure0 + ((X1_3 + X2_3 + 3791) bsr 4). diff --git a/src/platforms/esp32/test/main/test_main.c b/src/platforms/esp32/test/main/test_main.c index fe5ac38b0e..f38ed2c66f 100644 --- a/src/platforms/esp32/test/main/test_main.c +++ b/src/platforms/esp32/test/main/test_main.c @@ -235,6 +235,13 @@ TEST_CASE("test_file", "[test_run]") // SPI SD CARD, is configured for esp32 simulator in diagram.esp32.json #if (!CONFIG_ETH_USE_OPENETH && CONFIG_IDF_TARGET_ESP32) + +TEST_CASE("test_i2c", "[test_run]") +{ + term ret_value = avm_test_case("test_i2c.beam"); + TEST_ASSERT(ret_value == OK_ATOM); +} + TEST_CASE("test_file", "[test_run]") { esp_vfs_fat_sdmmc_mount_config_t mount_config = { diff --git a/src/platforms/esp32/test/sim_boards/diagram.esp32.json b/src/platforms/esp32/test/sim_boards/diagram.esp32.json index 32191b7f13..a9964ba34d 100644 --- a/src/platforms/esp32/test/sim_boards/diagram.esp32.json +++ b/src/platforms/esp32/test/sim_boards/diagram.esp32.json @@ -23,6 +23,13 @@ "top": 33.8, "left": 162.2, "attrs": { "travelLength": "30", "value": "512" } + }, + { + "type": "board-bmp180", + "id": "bmp180", + "top": 42.42, + "left": 142.94, + "attrs": {} } ], "connections": [ @@ -36,7 +43,11 @@ ["sd1:DI", "esp:23", "magenta", ["h38.4", "v-96.09", "h-139.93", "v77.03"]], ["pot1:VCC", "esp:3V3", "red", ["h-19.2", "v-105.6", "h-139.39"]], ["pot1:GND", "esp:GND.2", "black", ["v0"]], - ["pot1:SIG", "esp:4", "green", ["h-38.4", "v47.2"]] + ["pot1:SIG", "esp:4", "green", ["h-38.4", "v47.2"]], + ["bmp180:VCC", "esp:3V3", "red", ["h0", "v-105.6", "h-119.74"]], + ["bmp180:GND", "esp:GND.2", "black", ["h0", "v-19.2", "h-89.74"]], + ["bmp180:SCL", "esp:22", "yellow", ["h0", "v86.4", "h-128.14"]], + ["bmp180:SDA", "esp:21", "green", ["h0", "v105.6", "h-137.74"]] ], "dependencies": {} }