From 08146daacd882ede38a6fadaa8a301d8a1b875a2 Mon Sep 17 00:00:00 2001 From: Winford Date: Tue, 17 Dec 2024 01:58:44 +0000 Subject: [PATCH 1/4] Remove unnecessary GPIO atoms from ESP32 platform_defaultatoms.def Reduce the number of GPIO related atoms created at boot by using more efficient matching to atom string where possible. Removes `static const char *const` declarations for atoms, replacing with `ATOM_STR()` in place for better readability. Handles the creations of atoms used by the driver in a safer way, checking their validity where necessary. Signed-off-by: Winford --- .../components/avm_builtins/gpio_driver.c | 77 ++++++++----------- .../avm_sys/include/platform_defaultatoms.def | 4 - 2 files changed, 33 insertions(+), 48 deletions(-) diff --git a/src/platforms/esp32/components/avm_builtins/gpio_driver.c b/src/platforms/esp32/components/avm_builtins/gpio_driver.c index ef0279044..d3cac494c 100644 --- a/src/platforms/esp32/components/avm_builtins/gpio_driver.c +++ b/src/platforms/esp32/components/avm_builtins/gpio_driver.c @@ -68,11 +68,6 @@ static void gpio_isr_handler(void *arg); static Context *gpio_driver_create_port(GlobalContext *global, term opts); #endif -#ifdef CONFIG_AVM_ENABLE_GPIO_PORT_DRIVER -static const char *const gpio_atom = "\x4" "gpio"; -static const char *const gpio_driver_atom = "\xB" "gpio_driver"; -#endif - static const AtomStringIntPair pin_mode_table[] = { { ATOM_STR("\x5", "input"), GPIO_MODE_INPUT }, { ATOM_STR("\x6", "output"), GPIO_MODE_OUTPUT }, @@ -101,6 +96,16 @@ static const AtomStringIntPair pin_level_table[] = { SELECT_INT_DEFAULT(GPIOPinInvalid) }; +static const AtomStringIntPair int_trigger_table[] = { + { ATOM_STR("\x4", "none"), GPIO_INTR_DISABLE }, + { ATOM_STR("\x6", "rising"), GPIO_INTR_POSEDGE }, + { ATOM_STR("\x7", "falling"), GPIO_INTR_NEGEDGE }, + { ATOM_STR("\x4", "both"), GPIO_INTR_ANYEDGE }, + { ATOM_STR("\x3", "low"), GPIO_INTR_LOW_LEVEL }, + { ATOM_STR("\x4", "high"), GPIO_INTR_HIGH_LEVEL }, + SELECT_INT_DEFAULT(GPIO_INTR_MAX) +}; + enum gpio_cmd { GPIOInvalidCmd = 0, @@ -286,15 +291,19 @@ static inline term gpio_digital_read(term gpio_num_term) avm_int_t level = gpio_get_level(gpio_num); - return level ? HIGH_ATOM : LOW_ATOM; + return level ? globalcontext_make_atom(glb, high_atom) : globalcontext_make_atom(glb, low_atom); } #ifdef CONFIG_AVM_ENABLE_GPIO_PORT_DRIVER void gpio_driver_init(GlobalContext *global) { - int index = globalcontext_insert_atom(global, gpio_driver_atom); - gpio_driver = term_from_atom_index(index); + int index = globalcontext_insert_atom(global, ATOM_STR("\xB", "gpio_driver")); + if (UNLIKELY(index < 0 )){ + ESP_LOGE(TAG, "Failed to initialize gpio_driver"); + } else { + gpio_driver = term_from_atom_index(index); + } } Context *gpio_driver_create_port(GlobalContext *global, term opts) @@ -312,12 +321,18 @@ Context *gpio_driver_create_port(GlobalContext *global, term opts) ctx->native_handler = consume_gpio_mailbox; ctx->platform_data = gpio_data; - term reg_name_term = globalcontext_make_atom(global, gpio_atom); - int atom_index = term_to_atom_index(reg_name_term); - - if (UNLIKELY(!globalcontext_register_process(ctx->global, atom_index, ctx->process_id))) { + int gpio_atom_index = globalcontext_insert_atom(global, ATOM_STR("\x4", "gpio")); + if (UNLIKELY(gpio_atom_index < 0)) { + ESP_LOGE(TAG, "Failed to create 'gpio' atom to register the driver!"); + free(gpio_data); scheduler_terminate(ctx); + return NULL; + } + + if (UNLIKELY(!globalcontext_register_process(ctx->global, gpio_atom_index, ctx->process_id))) { ESP_LOGE(TAG, "Only a single GPIO driver can be opened."); + free(gpio_data); + scheduler_terminate(ctx); return NULL; } @@ -327,7 +342,7 @@ Context *gpio_driver_create_port(GlobalContext *global, term opts) static term gpiodriver_close(Context *ctx) { GlobalContext *glb = ctx->global; - int gpio_atom_index = atom_table_ensure_atom(glb->atom_table, gpio_atom, AtomTableNoOpts); + int gpio_atom_index = atom_table_ensure_atom(glb->atom_table, ATOM_STR("\x4", "gpio"), AtomTableNoOpts); if (UNLIKELY(!globalcontext_get_registered_process(glb, gpio_atom_index))) { return ERROR_ATOM; } @@ -482,36 +497,9 @@ static term gpiodriver_set_int(Context *ctx, int32_t target_pid, term cmd) target_local_pid = target_pid; } - - /* TODO: GPIO specific atoms should be removed from platform_defaultatoms and constructed within this driver */ - gpio_int_type_t interrupt_type; - switch (trigger) { - case NONE_ATOM: - interrupt_type = GPIO_INTR_DISABLE; - break; - - case RISING_ATOM: - interrupt_type = GPIO_INTR_POSEDGE; - break; - - case FALLING_ATOM: - interrupt_type = GPIO_INTR_NEGEDGE; - break; - - case BOTH_ATOM: - interrupt_type = GPIO_INTR_ANYEDGE; - break; - - case LOW_ATOM: - interrupt_type = GPIO_INTR_LOW_LEVEL; - break; - - case HIGH_ATOM: - interrupt_type = GPIO_INTR_HIGH_LEVEL; - break; - - default: - return ERROR_ATOM; + gpio_int_type_t interrupt_type = interop_atom_term_select_int(int_trigger_table, trigger, ctx->global); + if(UNLIKELY(interrupt_type == GPIO_INTR_MAX)) { + return BADARG_ATOM; } if (trigger != NONE_ATOM) { @@ -570,6 +558,7 @@ static term gpiodriver_remove_int(Context *ctx, term cmd) return ERROR_ATOM; } + return unregister_interrupt_listener(ctx, gpio_num); } @@ -749,7 +738,7 @@ static term nif_gpio_digital_write(Context *ctx, int argc, term argv[]) static term nif_gpio_digital_read(Context *ctx, int argc, term argv[]) { - return gpio_digital_read(argv[0]); + return gpio_digital_read(ctx, argv[0]); } static const struct Nif gpio_init_nif = diff --git a/src/platforms/esp32/components/avm_sys/include/platform_defaultatoms.def b/src/platforms/esp32/components/avm_sys/include/platform_defaultatoms.def index 654bff095..5d83cdc99 100644 --- a/src/platforms/esp32/components/avm_sys/include/platform_defaultatoms.def +++ b/src/platforms/esp32/components/avm_sys/include/platform_defaultatoms.def @@ -18,11 +18,7 @@ * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later */ -X(READ_ATOM, "\x4", "read") X(GPIO_INTERRUPT_ATOM, "\xE", "gpio_interrupt") -X(RISING_ATOM, "\x6", "rising") -X(FALLING_ATOM, "\x7", "falling") -X(BOTH_ATOM, "\x4", "both") X(LOW_ATOM, "\x3", "low") X(HIGH_ATOM, "\x4", "high") From 36f83fb72229468368aabaa74e5285f15d8d9b4b Mon Sep 17 00:00:00 2001 From: Winford Date: Mon, 30 Dec 2024 01:26:09 +0000 Subject: [PATCH 2/4] Add `internal_error` atom to defaultatoms.def Adds the `internal_error` atom from OTP for returning errors for "thing that shouldn't happen". There are rare occasions where user inputs may be valid, but an internal operation fails for some reason (potentially an internal bug to the VM, not the users application code). For example, this may be an invalid internal state when setting the direction for a gpio pin. If the users inputs are valid `internal_error` should be returned rather than `badarg`, which could mislead application developers into chasing a bug in their application that isn't there. Signed-off-by: Winford --- src/libAtomVM/defaultatoms.def | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libAtomVM/defaultatoms.def b/src/libAtomVM/defaultatoms.def index 9e768a355..978dbb4fb 100644 --- a/src/libAtomVM/defaultatoms.def +++ b/src/libAtomVM/defaultatoms.def @@ -82,6 +82,7 @@ X(EXIT_ATOM, "\x4", "EXIT") X(BADMAP_ATOM, "\x6", "badmap") X(BADKEY_ATOM, "\x6", "badkey") X(NONE_ATOM, "\x4", "none") +X(INTERNAL_ERROR_ATOM, "\xE", "internal_error") X(IO_REQUEST_ATOM, "\xA", "io_request") X(IO_REPLY_ATOM, "\x8", "io_reply") From 0f80e19435c5f89b1488fc29ed41cb5b7835ace9 Mon Sep 17 00:00:00 2001 From: Winford Date: Sat, 21 Dec 2024 07:16:45 +0000 Subject: [PATCH 3/4] Fix ESP32 gpio driver error returns to match spec Changes error returns to match the spec and ofther platforms. Nifs now raise any errors, and port function errors return `{error, Reason}`. Updates gpio.erl to reflect the correct returns, and make it more clear that errors for nifs will be raised. Closes #894 Signed-off-by: Winford --- CHANGELOG.md | 3 + libs/eavmlib/src/gpio.erl | 58 ++--- .../components/avm_builtins/gpio_driver.c | 210 ++++++++++++------ 3 files changed, 179 insertions(+), 92 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b48dd8748..4ffbd36b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - ESP32: improved sntp sync speed from a cold boot. - Utilize reserved `phy_init` partition on ESP32 to store wifi calibration for faster connections. +### Changed +- ESP32 GPIO driver now matches spec for error returns, nifs raise `Error`, port functions return `{error, Reason}` + ## [0.6.6] - Unreleased ### Added diff --git a/libs/eavmlib/src/gpio.erl b/libs/eavmlib/src/gpio.erl index d0fac085a..b027387b5 100644 --- a/libs/eavmlib/src/gpio.erl +++ b/libs/eavmlib/src/gpio.erl @@ -82,7 +82,7 @@ %% Event type that will trigger a `gpio_interrupt'. STM32 only supports `rising', `falling', or `both'. %%----------------------------------------------------------------------------- -%% @returns Pid | error | {error, Reason} +%% @returns Pid | {error, Reason} %% @doc Start the GPIO driver port %% %% Returns the pid of the active GPIO port driver, otherwise the GPIO @@ -103,7 +103,7 @@ start() -> end. %%----------------------------------------------------------------------------- -%% @returns Pid | error | {error, Reason} +%% @returns Pid | {error, Reason} %% @doc Start the GPIO driver port %% %% The GPIO port driver will be stared and registered as `gpio'. If the @@ -121,7 +121,7 @@ open() -> %%----------------------------------------------------------------------------- %% @param GPIO pid that was returned from gpio:start/0 -%% @returns ok | error | {error, Reason} +%% @returns ok | {error, Reason} %% @doc Stop the GPIO interrupt port %% %% This function disables any interrupts that are set, stops @@ -135,7 +135,7 @@ close(GPIO) -> port:call(GPIO, {close}). %%----------------------------------------------------------------------------- -%% @returns ok | error | {error, Reason} +%% @returns ok | {error, Reason} %% @doc Stop the GPIO interrupt port %% %% This function disables any interrupts that are set, stops @@ -151,7 +151,7 @@ stop() -> %%----------------------------------------------------------------------------- %% @param GPIO pid that was returned from gpio:start/0 %% @param Pin number of the pin to read -%% @returns high | low | error | {error, Reason} +%% @returns high | low | {error, Reason} %% @doc Read the digital state of a GPIO pin %% %% Read if an input pin state is `high' or `low'. @@ -169,7 +169,7 @@ read(GPIO, Pin) -> %% @param GPIO pid that was returned from `gpio:start/0' %% @param Pin number of the pin to configure %% @param Direction is `input', `output', or `output_od' -%% @returns ok | error | {error, Reason} +%% @returns ok | {error, Reason} %% @doc Set the operational mode of a pin %% %% Pins can be used for input, output, or output with open drain. @@ -199,7 +199,7 @@ set_direction(GPIO, Pin, Direction) -> %% @param GPIO pid that was returned from `gpio:start/0' %% @param Pin number of the pin to write %% @param Level the desired output level to set -%% @returns ok | error | {error, Reason} +%% @returns ok | {error, Reason} %% @doc Set GPIO digital output level %% %% Set a pin to `high' (1) or `low' (0). @@ -233,7 +233,7 @@ set_level(GPIO, Pin, Level) -> %% @param GPIO pid that was returned from `gpio:start/0' %% @param Pin number of the pin to set the interrupt on %% @param Trigger is the state that will trigger an interrupt -%% @returns ok | error | {error, Reason} +%% @returns ok | {error, Reason} %% @doc Set a GPIO interrupt %% %% Available triggers are `none' (which is the same as disabling an @@ -257,7 +257,7 @@ set_int(GPIO, Pin, Trigger) -> %% @param Pin number of the pin to set the interrupt on %% @param Trigger is the state that will trigger an interrupt %% @param Pid is the process that will receive the interrupt message -%% @returns ok | error | {error, Reason} +%% @returns ok | {error, Reason} %% @doc Set a GPIO interrupt %% %% Available triggers are `none' (which is the same as disabling an @@ -280,7 +280,7 @@ set_int(GPIO, Pin, Trigger, Pid) -> %%----------------------------------------------------------------------------- %% @param GPIO pid that was returned from `gpio:start/0' %% @param Pin number of the pin to remove the interrupt -%% @returns ok | error | {error, Reason} +%% @returns ok | {error, Reason} %% @doc Remove a GPIO interrupt %% %% Removes an interrupt from the specified pin. @@ -288,13 +288,13 @@ set_int(GPIO, Pin, Trigger, Pid) -> %% The rp2040 (Pico) port does not support gpio interrupts at this time. %% @end %%----------------------------------------------------------------------------- --spec remove_int(GPIO :: gpio(), Pin :: pin()) -> ok | {error, Reason :: atom()} | error. +-spec remove_int(GPIO :: gpio(), Pin :: pin()) -> ok | {error, Reason :: atom()}. remove_int(GPIO, Pin) -> port:call(GPIO, {remove_int, Pin}). %%----------------------------------------------------------------------------- %% @param Pin number to initialize -%% @returns ok +%% @returns ok | raise(Error) %% @doc Initialize a pin to be used as GPIO. %% This is required on RP2040 and for some pins on ESP32. %% @end @@ -305,7 +305,7 @@ init(_Pin) -> %%----------------------------------------------------------------------------- %% @param Pin number to deinitialize -%% @returns ok +%% @returns ok | raise(Error) %% @doc Reset a pin back to the NULL function. %% Currently only implemented for RP2040 (Pico). %% @end @@ -317,7 +317,7 @@ deinit(_Pin) -> %%----------------------------------------------------------------------------- %% @param Pin number to set operational mode %% @param Direction is `input', `output', or `output_od' -%% @returns ok | error | {error, Reason} +%% @returns ok | raise(Error) %% @doc Set the operational mode of a pin %% %% Pins can be used for input, output, or output with open drain. @@ -336,14 +336,14 @@ deinit(_Pin) -> %% @end %%----------------------------------------------------------------------------- -spec set_pin_mode(Pin :: pin(), Direction :: direction()) -> - ok | {error, Reason :: atom()} | error. + ok. set_pin_mode(_Pin, _Mode) -> erlang:nif_error(undefined). %%----------------------------------------------------------------------------- %% @param Pin number to set internal resistor direction %% @param Pull is the internal resistor state -%% @returns ok | error +%% @returns ok | raise(Error) %% @doc Set the internal resistor of a pin %% %% Pins can be internally pulled `up', `down', `up_down' (pulled in @@ -354,13 +354,13 @@ set_pin_mode(_Pin, _Mode) -> %% or `set_pin_mode/2'. %% @end %%----------------------------------------------------------------------------- --spec set_pin_pull(Pin :: pin(), Pull :: pull()) -> ok | error. +-spec set_pin_pull(Pin :: pin(), Pull :: pull()) -> ok. set_pin_pull(_Pin, _Pull) -> erlang:nif_error(undefined). %%----------------------------------------------------------------------------- %% @param Pin number of the pin to be held -%% @returns ok | error +%% @returns ok | raise(Error) %% @doc Hold the state of a pin %% %% The gpio pad hold function works in both input and output modes, @@ -380,13 +380,13 @@ set_pin_pull(_Pin, _Pull) -> %% This function is only supported on ESP32. %% @end %%----------------------------------------------------------------------------- --spec hold_en(Pin :: pin()) -> ok | error. +-spec hold_en(Pin :: pin()) -> ok. hold_en(_Pin) -> erlang:nif_error(undefined). %%----------------------------------------------------------------------------- %% @param Pin number of the pin to be released -%% @returns ok | error +%% @returns ok | raise(Error) %% @doc Release a pin from a hold state. %% %% When the chip is woken up from Deep-sleep, the gpio will be set to @@ -402,7 +402,7 @@ hold_en(_Pin) -> %% This function is only supported on ESP32. %% @end %%----------------------------------------------------------------------------- --spec hold_dis(Pin :: pin()) -> ok | error. +-spec hold_dis(Pin :: pin()) -> ok. hold_dis(_Pin) -> erlang:nif_error(undefined). @@ -445,7 +445,7 @@ deep_sleep_hold_dis() -> %%----------------------------------------------------------------------------- %% @param Pin number of the pin to write %% @param Level the desired output level to set -%% @returns ok | error | {error, Reason} +%% @returns ok | raise(Error) %% @doc Set GPIO digital output level %% %% Set a pin to `high' (1) or `low' (0). @@ -469,13 +469,13 @@ deep_sleep_hold_dis() -> %% require or accept `set_pin_mode' or `set_pin_pull' before use. %% @end %%----------------------------------------------------------------------------- --spec digital_write(Pin :: pin(), Level :: level()) -> ok | {error, Reason :: atom()} | error. +-spec digital_write(Pin :: pin(), Level :: level()) -> ok. digital_write(_Pin, _Level) -> erlang:nif_error(undefined). %%----------------------------------------------------------------------------- %% @param Pin number of the pin to read -%% @returns high | low | error | {error, Reason} +%% @returns high | low | raise(Error) %% @doc Read the digital state of a GPIO pin %% %% Read if an input pin state is high or low. @@ -486,14 +486,14 @@ digital_write(_Pin, _Level) -> %% and does not require or accept `set_pin_mode' or `set_pin_pull' before use. %% @end %%----------------------------------------------------------------------------- --spec digital_read(Pin :: pin()) -> high | low | {error, Reason :: atom()} | error. +-spec digital_read(Pin :: pin()) -> high | low. digital_read(_Pin) -> erlang:nif_error(undefined). %%----------------------------------------------------------------------------- %% @param Pin number of the pin to set the interrupt on %% @param Trigger is the state that will trigger an interrupt -%% @returns ok | error | {error, Reason} +%% @returns ok | {error, Reason} %% @doc Convenience function for `gpio:set_int/3' %% %% This is a convenience function for `gpio:set_int/3' that allows an @@ -509,13 +509,13 @@ digital_read(_Pin) -> %% @end %%----------------------------------------------------------------------------- -spec attach_interrupt(Pin :: pin(), Trigger :: trigger()) -> - ok | {error, Reason :: atom()} | error. + ok | {error, Reason :: atom()}. attach_interrupt(Pin, Trigger) -> set_int(start(), Pin, Trigger). %----------------------------------------------------------------------------- %% @param Pin number of the pin to remove the interrupt -%% @returns ok | error | {error, Reason} +%% @returns ok | {error, Reason} %% @doc Convenience function for `gpio:remove_int/2' %% %% This is a convenience function for `gpio:remove_int/2' that allows an @@ -527,6 +527,6 @@ attach_interrupt(Pin, Trigger) -> %% The rp2040 (Pico) port does not support gpio interrupts at this time. %% @end %%----------------------------------------------------------------------------- --spec detach_interrupt(Pin :: pin()) -> ok | {error, Reason :: atom()} | error. +-spec detach_interrupt(Pin :: pin()) -> ok | {error, Reason :: atom()}. detach_interrupt(Pin) -> remove_int(whereis(gpio), Pin). diff --git a/src/platforms/esp32/components/avm_builtins/gpio_driver.c b/src/platforms/esp32/components/avm_builtins/gpio_driver.c index d3cac494c..b319dc976 100644 --- a/src/platforms/esp32/components/avm_builtins/gpio_driver.c +++ b/src/platforms/esp32/components/avm_builtins/gpio_driver.c @@ -59,12 +59,8 @@ static const struct Nif *gpio_nif_get_nif(const char *nifname); #ifdef CONFIG_AVM_ENABLE_GPIO_PORT_DRIVER static void gpio_driver_init(GlobalContext *global); -#endif - -#ifdef CONFIG_AVM_ENABLE_GPIO_PORT_DRIVER static NativeHandlerResult consume_gpio_mailbox(Context *ctx); static void gpio_isr_handler(void *arg); - static Context *gpio_driver_create_port(GlobalContext *global, term opts); #endif @@ -144,7 +140,29 @@ struct GPIOData struct ListHead gpio_listeners; }; -/* TODO: Change error returns to {error, Reason} (See: https://github.com/atomvm/AtomVM/issues/894) */ +static bool term_is_logic_level(term level) +{ + bool result = false; + if (level == LOW_ATOM) { + result = true; + } + if (level == HIGH_ATOM) { + result = true; + } + if (term_is_integer(level)) { + switch (term_to_int(level)) { + case 0: + result = true; + break; + case 1: + result = true; + break; + default: + result = false; + } + } + return result; +} static inline term gpio_set_pin_mode(Context *ctx, term gpio_num_term, term mode_term) { @@ -152,21 +170,21 @@ static inline term gpio_set_pin_mode(Context *ctx, term gpio_num_term, term mode if (LIKELY(term_is_integer(gpio_num_term))) { avm_int_t pin_int = term_to_int32(gpio_num_term); if (UNLIKELY((pin_int < 0) || (pin_int >= GPIO_NUM_MAX))) { - return ERROR_ATOM; + return BADARG_ATOM; } gpio_num = (gpio_num_t) pin_int; } else { - return ERROR_ATOM; + return BADARG_ATOM; } avm_int_t mode = interop_atom_term_select_int(pin_mode_table, mode_term, ctx->global); if (UNLIKELY(mode < 0)) { - return ERROR_ATOM; + return BADARG_ATOM; } esp_err_t result = gpio_set_direction(gpio_num, (gpio_mode_t) mode); if (UNLIKELY(result != ESP_OK)) { - return ERROR_ATOM; + return BADARG_ATOM; } return OK_ATOM; @@ -183,21 +201,21 @@ static inline term set_pin_pull_mode(Context *ctx, term gpio_num_term, term pull if (LIKELY(term_is_integer(gpio_num_term))) { avm_int_t pin_int = term_to_int32(gpio_num_term); if (UNLIKELY((pin_int < 0) || (pin_int >= GPIO_NUM_MAX))) { - return ERROR_ATOM; + return BADARG_ATOM; } gpio_num = (gpio_num_t) pin_int; } else { - return ERROR_ATOM; + return BADARG_ATOM; } avm_int_t pull_mode = get_pull_mode(ctx, pull); if (UNLIKELY(pull_mode < 0)) { - return ERROR_ATOM; + return BADARG_ATOM; } esp_err_t result = gpio_set_pull_mode(gpio_num, (gpio_pull_mode_t) pull_mode); if (UNLIKELY(result != ESP_OK)) { - return ERROR_ATOM; + return BADARG_ATOM; } return OK_ATOM; } @@ -208,16 +226,16 @@ static inline term hold_en(term gpio_num_term) if (LIKELY(term_is_integer(gpio_num_term))) { avm_int_t pin_int = term_to_int32(gpio_num_term); if (UNLIKELY((pin_int < 0) || (pin_int >= GPIO_NUM_MAX))) { - return ERROR_ATOM; + return BADARG_ATOM; } gpio_num = (gpio_num_t) pin_int; } else { - return ERROR_ATOM; + return BADARG_ATOM; } esp_err_t result = gpio_hold_en(gpio_num); if (UNLIKELY(result != ESP_OK)) { - return ERROR_ATOM; + return BADARG_ATOM; } return OK_ATOM; } @@ -228,16 +246,16 @@ static inline term hold_dis(term gpio_num_term) if (LIKELY(term_is_integer(gpio_num_term))) { avm_int_t pin_int = term_to_int32(gpio_num_term); if (UNLIKELY((pin_int < 0) || (pin_int >= GPIO_NUM_MAX))) { - return ERROR_ATOM; + return BADARG_ATOM; } gpio_num = (gpio_num_t) pin_int; } else { - return ERROR_ATOM; + return BADARG_ATOM; } esp_err_t result = gpio_hold_dis(gpio_num); if (UNLIKELY(result != ESP_OK)) { - return ERROR_ATOM; + return BADARG_ATOM; } return OK_ATOM; } @@ -248,50 +266,47 @@ static inline term gpio_digital_write(Context *ctx, term gpio_num_term, term lev if (LIKELY(term_is_integer(gpio_num_term))) { avm_int_t pin_int = term_to_int32(gpio_num_term); if (UNLIKELY((pin_int < 0) || (pin_int >= GPIO_NUM_MAX))) { - return ERROR_ATOM; + return BADARG_ATOM; } gpio_num = (gpio_num_t) pin_int; } else { - return ERROR_ATOM; + return BADARG_ATOM; } int level; + if (UNLIKELY(!term_is_logic_level(level_term))) { + return BADARG_ATOM; + } if (term_is_integer(level_term)) { level = term_to_int32(level_term); - if (UNLIKELY((level != 0) && (level != 1))) { - return ERROR_ATOM; - } } else { level = interop_atom_term_select_int(pin_level_table, level_term, ctx->global); - if (UNLIKELY(level < 0)) { - return ERROR_ATOM; - } } esp_err_t result = gpio_set_level(gpio_num, level); if (UNLIKELY(result != ESP_OK)) { - return ERROR_ATOM; + return BADARG_ATOM; } return OK_ATOM; } -static inline term gpio_digital_read(term gpio_num_term) +static inline term gpio_digital_read(Context *ctx, term gpio_num_term) { gpio_num_t gpio_num; if (LIKELY(term_is_integer(gpio_num_term))) { avm_int_t pin_int = term_to_int32(gpio_num_term); if (UNLIKELY((pin_int < 0) || (pin_int >= GPIO_NUM_MAX))) { - return ERROR_ATOM; + return BADARG_ATOM; } gpio_num = (gpio_num_t) pin_int; } else { - return ERROR_ATOM; + return BADARG_ATOM; } avm_int_t level = gpio_get_level(gpio_num); - return level ? globalcontext_make_atom(glb, high_atom) : globalcontext_make_atom(glb, low_atom); + return level ? HIGH_ATOM : LOW_ATOM; } #ifdef CONFIG_AVM_ENABLE_GPIO_PORT_DRIVER @@ -344,7 +359,8 @@ static term gpiodriver_close(Context *ctx) GlobalContext *glb = ctx->global; int gpio_atom_index = atom_table_ensure_atom(glb->atom_table, ATOM_STR("\x4", "gpio"), AtomTableNoOpts); if (UNLIKELY(!globalcontext_get_registered_process(glb, gpio_atom_index))) { - return ERROR_ATOM; + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, NOPROC_ATOM); } struct GPIOData *gpio_data = ctx->platform_data; @@ -401,7 +417,12 @@ static term gpiodriver_set_level(Context *ctx, term cmd) term gpio_num = term_get_tuple_element(cmd, 1); term level = term_get_tuple_element(cmd, 2); - return gpio_digital_write(ctx, gpio_num, level); + term result = gpio_digital_write(ctx, gpio_num, level); + if (UNLIKELY(result != OK_ATOM)) { + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, result); + } + return result; } static term gpiodriver_set_direction(Context *ctx, term cmd) @@ -409,13 +430,23 @@ static term gpiodriver_set_direction(Context *ctx, term cmd) term gpio_num = term_get_tuple_element(cmd, 1); term direction = term_get_tuple_element(cmd, 2); - return gpio_set_pin_mode(ctx, gpio_num, direction); + term result = gpio_set_pin_mode(ctx, gpio_num, direction); + if (UNLIKELY(result != OK_ATOM)) { + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, result); + } + return result; } -static term gpiodriver_read(term cmd) +static term gpiodriver_read(Context *ctx, term cmd) { term gpio_num = term_get_tuple_element(cmd, 1); - return gpio_digital_read(gpio_num); + term result = gpio_digital_read(ctx, gpio_num); + if (UNLIKELY(!term_is_logic_level(result))) { + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, result); + } + return result; } static bool gpiodriver_is_gpio_attached(struct GPIOData *gpio_data, int gpio_num) @@ -453,7 +484,8 @@ static term unregister_interrupt_listener(Context *ctx, gpio_num_t gpio_num) } } - return ERROR_ATOM; + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, BADARG_ATOM); } static term gpiodriver_set_int(Context *ctx, int32_t target_pid, term cmd) @@ -468,11 +500,13 @@ static term gpiodriver_set_int(Context *ctx, int32_t target_pid, term cmd) if (LIKELY(term_is_integer(gpio_num_term))) { int32_t pin_int = term_to_int32(gpio_num_term); if (UNLIKELY((pin_int < 0) || (pin_int >= GPIO_NUM_MAX))) { - return ERROR_ATOM; + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, BADARG_ATOM); } gpio_num = (gpio_num_t) pin_int; } else { - return ERROR_ATOM; + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, BADARG_ATOM); } term trigger = term_get_tuple_element(cmd, 2); @@ -480,7 +514,8 @@ static term gpiodriver_set_int(Context *ctx, int32_t target_pid, term cmd) term pid = term_get_tuple_element(cmd, 3); if (UNLIKELY(!term_is_pid(pid) && !term_is_atom(pid))) { ESP_LOGE(TAG, "Invalid listener parameter, must be a pid() or registered process!"); - return ERROR_ATOM; + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, BADARG_ATOM); } if (term_is_pid(pid)) { target_local_pid = term_to_local_process_id(pid); @@ -489,7 +524,8 @@ static term gpiodriver_set_int(Context *ctx, int32_t target_pid, term cmd) int32_t registered_process = (int32_t) globalcontext_get_registered_process(ctx->global, pid_atom_index); if (UNLIKELY(registered_process == 0)) { ESP_LOGE(TAG, "Invalid listener parameter, atom() is not a registered process name!"); - return ERROR_ATOM; + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, BADARG_ATOM); } target_local_pid = registered_process; } @@ -499,12 +535,15 @@ static term gpiodriver_set_int(Context *ctx, int32_t target_pid, term cmd) gpio_int_type_t interrupt_type = interop_atom_term_select_int(int_trigger_table, trigger, ctx->global); if(UNLIKELY(interrupt_type == GPIO_INTR_MAX)) { - return BADARG_ATOM; + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, BADARG_ATOM); } if (trigger != NONE_ATOM) { if (UNLIKELY(gpiodriver_is_gpio_attached(gpio_data, gpio_num))) { - return ERROR_ATOM; + ESP_LOGE(TAG, "Pin %i already attached to interrupt", gpio_num); + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, BADARG_ATOM); } TRACE("going to install interrupt for %i.\n", gpio_num); @@ -517,7 +556,8 @@ static term gpiodriver_set_int(Context *ctx, int32_t target_pid, term cmd) struct GPIOListenerData *data = malloc(sizeof(struct GPIOListenerData)); if (IS_NULL_PTR(data)) { ESP_LOGE(TAG, "gpiodriver_set_int: Failed to ensure free heap space."); - AVM_ABORT(); + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, OUT_OF_MEMORY_ATOM); } list_append(&gpio_data->gpio_listeners, &data->gpio_listener_list_head); data->gpio = gpio_num; @@ -526,16 +566,29 @@ static term gpiodriver_set_int(Context *ctx, int32_t target_pid, term cmd) data->listener.sender = data; data->listener.handler = gpio_interrupt_callback; - gpio_set_direction(gpio_num, GPIO_MODE_INPUT); - gpio_set_intr_type(gpio_num, interrupt_type); - - esp_err_t ret = gpio_isr_handler_add(gpio_num, gpio_isr_handler, data); + // These inputs have already been validated, so any errors indicate a bad state or internal error. + esp_err_t ret = gpio_set_direction(gpio_num, GPIO_MODE_INPUT); if (UNLIKELY(ret != ESP_OK)) { - return ERROR_ATOM; + ESP_LOGE(TAG, "Internal error setting direction on pin %i", gpio_num); + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, INTERNAL_ERROR_ATOM); + } + ret = gpio_set_intr_type(gpio_num, interrupt_type); + if (UNLIKELY(ret != ESP_OK)) { + ESP_LOGE(TAG, "Internal error setting interrupt type %i on pin %i", (int) interrupt_type, gpio_num); + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, INTERNAL_ERROR_ATOM); + } + ret = gpio_isr_handler_add(gpio_num, gpio_isr_handler, data); + if (UNLIKELY(ret != ESP_OK)) { + ESP_LOGE(TAG, "Internal error adding isr handler for pin %i", gpio_num); + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, INTERNAL_ERROR_ATOM); } } else { if (UNLIKELY(!gpiodriver_is_gpio_attached(gpio_data, gpio_num))) { - return ERROR_ATOM; + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, BADARG_ATOM); } gpio_set_intr_type(gpio_num, interrupt_type); return unregister_interrupt_listener(ctx, gpio_num); @@ -551,11 +604,13 @@ static term gpiodriver_remove_int(Context *ctx, term cmd) if (LIKELY(term_is_integer(gpio_num_term))) { int32_t pin_int = term_to_int32(gpio_num_term); if (UNLIKELY((pin_int < 0) || (pin_int >= GPIO_NUM_MAX))) { - return ERROR_ATOM; + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, BADARG_ATOM); } gpio_num = (gpio_num_t) pin_int; } else { - return ERROR_ATOM; + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, BADARG_ATOM); } @@ -598,7 +653,7 @@ static NativeHandlerResult consume_gpio_mailbox(Context *ctx) break; case GPIOReadCmd: - ret = gpiodriver_read(gen_message.req); + ret = gpiodriver_read(ctx, gen_message.req); break; case GPIOSetIntCmd: @@ -615,7 +670,8 @@ static NativeHandlerResult consume_gpio_mailbox(Context *ctx) default: ESP_LOGW(TAG, "Unrecognized command"); - ret = ERROR_ATOM; + port_ensure_available(ctx, TUPLE_SIZE(2)); + ret = port_create_error_tuple(ctx, BADARG_ATOM); } term ret_msg; @@ -646,6 +702,7 @@ REGISTER_PORT_DRIVER(gpio, gpio_driver_init, NULL, gpio_driver_create_port) #ifdef CONFIG_AVM_ENABLE_GPIO_NIFS + static term nif_gpio_init(Context *ctx, int argc, term argv[]) { UNUSED(argc); @@ -677,20 +734,28 @@ static term nif_gpio_init(Context *ctx, int argc, term argv[]) return OK_ATOM; } -/* TODO: in the case of {error, Return} we should RAISE_ERROR(Reason) */ - static term nif_gpio_set_pin_mode(Context *ctx, int argc, term argv[]) { UNUSED(argc); - return gpio_set_pin_mode(ctx, argv[0], argv[1]); + term result = gpio_set_pin_mode(ctx, argv[0], argv[1]); + if (UNLIKELY(result != OK_ATOM)) { + RAISE_ERROR(result); + } + + return result; } static term nif_gpio_set_pin_pull(Context *ctx, int argc, term argv[]) { UNUSED(argc); - return set_pin_pull_mode(ctx, argv[0], argv[1]); + term result = set_pin_pull_mode(ctx, argv[0], argv[1]); + if (UNLIKELY(result != OK_ATOM)) { + RAISE_ERROR(result); + } + + return result; } static term nif_gpio_hold_en(Context *ctx, int argc, term argv[]) @@ -698,7 +763,12 @@ static term nif_gpio_hold_en(Context *ctx, int argc, term argv[]) UNUSED(ctx); UNUSED(argc); - return hold_en(argv[0]); + term result = hold_en(argv[0]); + if (UNLIKELY(result != OK_ATOM)) { + RAISE_ERROR(result); + } + + return result; } static term nif_gpio_hold_dis(Context *ctx, int argc, term argv[]) @@ -706,7 +776,12 @@ static term nif_gpio_hold_dis(Context *ctx, int argc, term argv[]) UNUSED(ctx); UNUSED(argc); - return hold_dis(argv[0]); + term result = hold_dis(argv[0]); + if (UNLIKELY(result != OK_ATOM)) { + RAISE_ERROR(result); + } + + return result; } #if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 2) && SOC_GPIO_SUPPORT_HOLD_IO_IN_DSLP && !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP) || (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 2) && !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP) @@ -733,12 +808,21 @@ static term nif_gpio_deep_sleep_hold_dis(Context *ctx, int argc, term argv[]) static term nif_gpio_digital_write(Context *ctx, int argc, term argv[]) { - return gpio_digital_write(ctx, argv[0], argv[1]); + term result = gpio_digital_write(ctx, argv[0], argv[1]); + if (UNLIKELY(result != OK_ATOM)) { + RAISE_ERROR(result); + } + return result; } static term nif_gpio_digital_read(Context *ctx, int argc, term argv[]) { - return gpio_digital_read(ctx, argv[0]); + term result = gpio_digital_read(ctx, argv[0]); + if (LIKELY(term_is_logic_level(result))) { + return result; + } else { + RAISE_ERROR(result); + } } static const struct Nif gpio_init_nif = From 3590ef7e16289565136e1d5ef17c1da782ba727e Mon Sep 17 00:00:00 2001 From: Winford Date: Wed, 8 Jan 2025 01:24:35 +0000 Subject: [PATCH 4/4] Add test_gpio.erl to ESP32 test suite Adds a test to run on hardware with a jumper wire, or in the wokwi simulator to test the basic esp32 gpio driver funtionality and error handling, with tests for all boards when full_sim_test is run. The sim-test for P4 depends on PR #1438 being merged to be able to select the correct pins for the device for the the GPIO tests. Signed-off-by: Winford --- .github/workflows/esp32-simtest.yaml | 2 +- CHANGELOG.md | 1 + src/platforms/esp32/test/CMakeLists.txt | 5 + src/platforms/esp32/test/README.md | 6 +- src/platforms/esp32/test/main/CMakeLists.txt | 4 + .../test/main/test_erl_sources/CMakeLists.txt | 3 + .../test/main/test_erl_sources/test_gpio.erl | 288 ++++++++++++++++++ src/platforms/esp32/test/main/test_main.c | 9 + .../esp32/test/sim_boards/diagram.esp32.json | 4 +- .../test/sim_boards/diagram.esp32c3.json | 3 +- .../test/sim_boards/diagram.esp32c6.json | 6 +- .../test/sim_boards/diagram.esp32h2.json | 6 +- .../test/sim_boards/diagram.esp32p4.json | 4 +- .../test/sim_boards/diagram.esp32s2.json | 6 +- .../test/sim_boards/diagram.esp32s3.json | 6 +- 15 files changed, 340 insertions(+), 13 deletions(-) create mode 100644 src/platforms/esp32/test/main/test_erl_sources/test_gpio.erl diff --git a/.github/workflows/esp32-simtest.yaml b/.github/workflows/esp32-simtest.yaml index 724775ee5..8bab148ca 100644 --- a/.github/workflows/esp32-simtest.yaml +++ b/.github/workflows/esp32-simtest.yaml @@ -124,7 +124,7 @@ jobs: export PATH=${PATH}:${HOME}/.cache/rebar3/bin . $IDF_PATH/export.sh idf.py -DSDKCONFIG_DEFAULTS='sdkconfig.ci.wokwi' set-target ${{matrix.esp-idf-target}} - idf.py build + idf.py -DAVM_TEST_PERIPHERALS='y' build - name: Configure Wokwi environment run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ffbd36b6..93d00b1c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for `ets:update_counter/3` and `ets:update_counter/4`. - Added `erlang:+/1` - Added `lists:append/1` and `lists:append/2` +- Added test_gpio.erl to esp32 test suite. ### Fixed - ESP32: improved sntp sync speed from a cold boot. diff --git a/src/platforms/esp32/test/CMakeLists.txt b/src/platforms/esp32/test/CMakeLists.txt index 14a2221f8..262724374 100644 --- a/src/platforms/esp32/test/CMakeLists.txt +++ b/src/platforms/esp32/test/CMakeLists.txt @@ -57,6 +57,11 @@ set(AVM_SELECT_IN_TASK ON) project(atomvm-esp32-test) +option(AVM_TEST_PERIPHERALS "Enable extra peripheral tests." OFF) +if (AVM_TEST_PERIPHERALS) + message("\n** NOTICE: Including additional peripheral tests that will fail on QEMU!\n") +endif() + # esp-idf does not use compile_feature but instead sets version in # c_compile_options # Ensure project is compiled with at least C11 diff --git a/src/platforms/esp32/test/README.md b/src/platforms/esp32/test/README.md index c326c84d1..3c07a9033 100644 --- a/src/platforms/esp32/test/README.md +++ b/src/platforms/esp32/test/README.md @@ -65,5 +65,9 @@ The `WOKWI_CLI_TOKEN` needs to be set in your `Repository secrets` Settings -> A 3. Now we run `idf.py build` and run the CI: ```shell - idf.py build -DSDKCONFIG_DEFAULTS='sdkconfig.ci.wokwi' && pytest --embedded-services=idf,wokwi --wokwi-timeout=90000 --target=${IDF_TARGET} --wokwi-diagram=sim_boards/diagram.${IDF_TARGET}.json -s -W ignore::DeprecationWarning + idf.py build -DSDKCONFIG_DEFAULTS='sdkconfig.ci.wokwi' -DAVM_TEST_PERIPHERALS='y' && \ + pytest --embedded-services=idf,wokwi --wokwi-timeout=90000 --target=${IDF_TARGET} \ + --wokwi-diagram=sim_boards/diagram.${IDF_TARGET}.json -s -W ignore::DeprecationWarning ``` + +>Note: Configuring with the optional AVM_TEST_PERIPHERALS setting enabled (i.e.: `-DAVM_TEST_PERIPHERALS=ON`) will enable extra hardware peripheral tests that are normally omitted from the test suite because QEMU lacks full hardware support. diff --git a/src/platforms/esp32/test/main/CMakeLists.txt b/src/platforms/esp32/test/main/CMakeLists.txt index f759f703b..bae6c704e 100644 --- a/src/platforms/esp32/test/main/CMakeLists.txt +++ b/src/platforms/esp32/test/main/CMakeLists.txt @@ -21,5 +21,9 @@ idf_component_register(SRCS "test_main.c" INCLUDE_DIRS ".") +if (AVM_TEST_PERIPHERALS) + add_compile_definitions(TEST_PERIPHERALS) +endif() + add_subdirectory(test_erl_sources) target_link_libraries(${COMPONENT_LIB} esp32_test_modules) 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 fe93ae418..75d068028 100644 --- a/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt +++ b/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt @@ -53,6 +53,7 @@ compile_erlang(test_ssl) compile_erlang(test_time_and_processes) compile_erlang(test_twdt) compile_erlang(test_tz) +compile_erlang(test_gpio) add_custom_command( OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/esp32_test_modules.avm" @@ -74,6 +75,7 @@ add_custom_command( test_time_and_processes.beam test_twdt.beam test_tz.beam + test_gpio.beam DEPENDS HostAtomVM "${CMAKE_CURRENT_BINARY_DIR}/test_esp_partition.beam" @@ -92,6 +94,7 @@ add_custom_command( "${CMAKE_CURRENT_BINARY_DIR}/test_time_and_processes.beam" "${CMAKE_CURRENT_BINARY_DIR}/test_twdt.beam" "${CMAKE_CURRENT_BINARY_DIR}/test_tz.beam" + "${CMAKE_CURRENT_BINARY_DIR}/test_gpio.beam" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} VERBATIM ) diff --git a/src/platforms/esp32/test/main/test_erl_sources/test_gpio.erl b/src/platforms/esp32/test/main/test_erl_sources/test_gpio.erl new file mode 100644 index 000000000..a8f0ab9f4 --- /dev/null +++ b/src/platforms/esp32/test/main/test_erl_sources/test_gpio.erl @@ -0,0 +1,288 @@ +% +% This file is part of AtomVM. +% +% Copyright 2025 Winford +% +% 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_gpio). + +-export([start/0, test_nifs/2, test_ports/2]). + +%% Bad argument and error reason raised if it is accepted +-define(BADPINS, [ + {-1, accepted_neg_pin_number}, + {<<0>>, accepted_binary_pin}, + {"GPIO_NUM_0", accepted_pin_string}, + {button, accepted_pin_atom}, + {{a, 0}, accepted_pin_tuple}, + {[0, 1, 2], accepted_pin_list}, + {2048, accepted_too_large}, + {2.0, accepted_float}, + {#{}, accepted_map} +]). +-define(BADMODES, [ + {up, accepted_badarg}, + {1, accepted_int_mode}, + {<<0>>, accepted_binary_mode}, + {<<"input">>, accepted_binary_string_mode}, + {"input", accepted_mode_string}, + {[input, up], accepted_mode_list}, + {{input, up}, accepted_mode_tuple}, + {2.0, accepted_float}, + {#{}, accepted_map} +]). +-define(BADPULL, [ + {1, accepted_int_pull}, + {<<1>>, accepted_binary_pull}, + {<<"up">>, accepted_binary_string_pull}, + {"up", accepted_pull_string}, + {{up, hold}, accepted_pull_tuple}, + {[up, foo, bar], accepted_pull_list}, + {sideways, accepted_invalid_atom}, + {2.0, accepted_float}, + {#{}, accepted_map} +]). +-define(BADLEVEL, [ + {medium, accepted_bad_atom}, + {-1, accepted_neg_level_number}, + {10, accepted_badarg_number}, + {<<1>>, accepted_binary_level}, + {<<"high">>, accepted_binary_level}, + {"high", accepted_level_string}, + {{0, high}, accepted_level_tuple}, + {[1], accepted_level_list}, + {1.0, accepted_float}, + {#{}, accepted_map} +]). + +start() -> + {Pin0, Pin1} = get_board_pins(maps:get(model, erlang:system_info(esp32_chip_info))), + io:format( + "Starting GPIO test, this board should have a jumper wire between pins ~p and ~p.~n", [ + Pin0, Pin1 + ] + ), + ok = test_nifs(Pin0, Pin1), + %% test ports with the pins reversed so we be sure to test reconfiguring an input to an output and vice versa + test_ports(Pin1, Pin0). + +test_nifs(Input, Output) -> + io:format("Testing nifs raise errors for badargs... "), + ok = test_nif_badargs(Input), + io:format("passed.~n"), + io:format("Testing set_pin_mode/2, set_pin_pull/2, digital_write/2 & digital_read/1... "), + ok = gpio:set_pin_mode(Input, input), + ok = gpio:set_pin_pull(Input, up), + ok = gpio:set_pin_mode(Output, output), + ok = gpio:set_pin_pull(Output, floating), + ok = gpio:digital_write(Output, high), + high = gpio:digital_read(Input), + ok = gpio:digital_write(Output, low), + low = gpio:digital_read(Input), + ok = gpio:digital_write(Output, 1), + high = gpio:digital_read(Input), + ok = gpio:digital_write(Output, 0), + low = gpio:digital_read(Input), + io:format("passed.~n"). + +test_ports(Input, Output) -> + io:format("Testing ports return {error, Reason} for badargs... "), + GPIO = gpio:start(), + ok = test_port_bardargs(GPIO, Input), + io:format("passed.~n"), + io:format("Testing set_direction/3, set_level/3 & read/2... "), + ok = gpio:set_direction(GPIO, Input, input), + ok = gpio:set_pin_pull(Input, up), + ok = gpio:set_direction(GPIO, Output, output), + ok = gpio:set_pin_pull(Output, floating), + ok = gpio:set_level(GPIO, Output, low), + low = gpio:read(GPIO, Input), + ok = gpio:set_level(GPIO, Output, high), + high = gpio:read(GPIO, Input), + ok = gpio:set_level(GPIO, Output, 0), + low = gpio:read(GPIO, Input), + ok = gpio:set_level(GPIO, Output, 1), + io:format("passed.~n"), + io:format("Testing GPIO interrupt... "), + Self = self(), + Listener = erlang:spawn(fun() -> interrupt_listener(Input, Self) end), + erlang:spawn(fun() -> interrupt_after(1000, GPIO, Output, 0) end), + ok = gpio:set_int(GPIO, Input, falling, Listener), + receive + {ok, interrupt} -> ok; + Error -> throw(Error) + after 5000 -> + io:format("No interrupt after 5000 ms giving up"), + throw(timeout_no_interrupt) + end, + erlang:spawn(fun() -> interrupt_after(1000, GPIO, Output, 0) end), + ok = gpio:set_int(GPIO, Input, falling), + receive + {gpio_interrupt, Input} -> ok; + Other -> throw(Other) + after 5000 -> + io:format("No interrupt after 5000 ms giving up"), + throw(timeout_no_interrupt) + end, + io:format("passed.~n"). + +test_nif_badargs(Pin) -> + Badpin_funs1 = [digital_read, hold_en, hold_dis], + Badpin_funs2 = [{set_pin_mode, output}, {set_pin_pull, floating}, {digital_write, low}], + Fun_args = [{set_pin_mode, ?BADMODES}, {set_pin_pull, ?BADPULL}, {digital_write, ?BADLEVEL}], + + lists:foreach( + fun(TestFun) -> + lists:foreach( + fun({Badpin, Err}) -> ok = want_catch_throw(TestFun, Badpin, badarg, Err) end, + ?BADPINS + ) + end, + Badpin_funs1 + ), + + lists:foreach( + fun({TestFun, Arg}) -> + lists:foreach( + fun({Badpin, Err}) -> ok = want_catch_throw(TestFun, Badpin, Arg, badarg, Err) end, + ?BADPINS + ) + end, + Badpin_funs2 + ), + + lists:foreach( + fun({TestFun, BadArgs}) -> + lists:foreach( + fun({Badarg, Err}) -> ok = want_catch_throw(TestFun, Pin, Badarg, badarg, Err) end, + BadArgs + ) + end, + Fun_args + ), + ok. + +test_port_bardargs(GPIO, Pin) -> + Badpin_funs2 = [read, remove_int], + Badpin_funs3 = [{set_direction, input}, {set_level, low}, {set_int, low}], + Fun_args = [{set_direction, ?BADMODES}, {set_level, ?BADLEVEL}, {set_int, ?BADLEVEL}], + + lists:foreach( + fun(TestFun) -> + lists:foreach( + fun({Badpin, Err}) -> ok = want_error_tuple(TestFun, GPIO, Badpin, badarg, Err) end, + ?BADPINS + ) + end, + Badpin_funs2 + ), + + lists:foreach( + fun({TestFun, Arg}) -> + lists:foreach( + fun({Badpin, Err}) -> + ok = want_error_tuple(TestFun, GPIO, Badpin, Arg, badarg, Err) + end, + ?BADPINS + ) + end, + Badpin_funs3 + ), + + lists:foreach( + fun({TestFun, TestArgs}) -> + lists:foreach( + fun({Badarg, Err}) -> + ok = want_error_tuple(TestFun, GPIO, Pin, Badarg, badarg, Err) + end, + TestArgs + ) + end, + Fun_args + ), + ok. + +want_catch_throw(Fun, Pin, Catch, ErrorAtom) -> + try gpio:Fun(Pin) of + ok -> + throw({Fun, ErrorAtom}); + Any -> + throw({Fun, Any}) + catch + _:Catch:_ -> + ok + end. + +want_catch_throw(Fun, Pin, Arg, Catch, ErrorAtom) -> + try gpio:Fun(Pin, Arg) of + ok -> + throw({Fun, ErrorAtom}); + Any -> + throw({Fun, Any}) + catch + _:Catch:_ -> + ok + end. + +want_error_tuple(Fun, GPIO, Pin, Reason, ErrorAtom) -> + try gpio:Fun(GPIO, Pin) of + ok -> + throw({Fun, ErrorAtom}); + {error, Reason} -> + ok + catch + Error -> + throw({Fun, {caught, Error}}) + end. + +want_error_tuple(Fun, GPIO, Pin, Arg, Reason, ErrorAtom) -> + try gpio:Fun(GPIO, Pin, Arg) of + ok -> + throw({Fun, ErrorAtom}); + {error, Reason} -> + ok + catch + Error -> + throw({Fun, {caught, Error}}) + end. + +interrupt_after(Delay, GPIO, Pin, Level) -> + timer:sleep(Delay), + ok = gpio:set_level(GPIO, Pin, Level), + ok = gpio:set_level(GPIO, Pin, 1 - Level). + +interrupt_listener(Pin, ReplyTo) -> + receive + {gpio_interrupt, Pin} -> + ok = gpio:remove_int(whereis(gpio), Pin), + ReplyTo ! {ok, interrupt}; + Any -> + throw({interrupt_listener, {received, Any}}) + end, + interrupt_listener(Pin, ReplyTo). + +get_board_pins(Chipset) -> + case (Chipset) of + esp32 -> {25, 26}; + esp32_c3 -> {4, 5}; + esp32_c6 -> {6, 7}; + esp32_h2 -> {0, 1}; + esp32_p4 -> {4, 5}; + esp32_s2 -> {3, 4}; + esp32_s3 -> {4, 5}; + _ -> throw(unsupported_chipset) + end. diff --git a/src/platforms/esp32/test/main/test_main.c b/src/platforms/esp32/test/main/test_main.c index 1ad0d6cfd..73ec1b1eb 100644 --- a/src/platforms/esp32/test/main/test_main.c +++ b/src/platforms/esp32/test/main/test_main.c @@ -603,6 +603,15 @@ TEST_CASE("test_twdt", "[test_run]") } #endif +// test_gpio may only be used by Wokwi Sim and real hardware +#ifdef TEST_PERIPHERALS +TEST_CASE("test_gpio", "[test_run]") +{ + term ret_value = avm_test_case("test_gpio.beam"); + TEST_ASSERT(ret_value == OK_ATOM); +} +#endif + void app_main(void) { UNITY_BEGIN(); diff --git a/src/platforms/esp32/test/sim_boards/diagram.esp32.json b/src/platforms/esp32/test/sim_boards/diagram.esp32.json index 64b53c72a..9ae87e8f8 100644 --- a/src/platforms/esp32/test/sim_boards/diagram.esp32.json +++ b/src/platforms/esp32/test/sim_boards/diagram.esp32.json @@ -36,7 +36,9 @@ ["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"]], + [ "esp:25", "esp:26", "blue", [ "v0", "h-12", "v9", "h10" ] ] ], "dependencies": {} } + diff --git a/src/platforms/esp32/test/sim_boards/diagram.esp32c3.json b/src/platforms/esp32/test/sim_boards/diagram.esp32c3.json index f7d41f75b..f2b347ae7 100644 --- a/src/platforms/esp32/test/sim_boards/diagram.esp32c3.json +++ b/src/platforms/esp32/test/sim_boards/diagram.esp32c3.json @@ -18,7 +18,8 @@ [ "esp:RX", "$serialMonitor:TX", "", [] ], [ "esp:3", "pot1:SIG", "green", [ "h0" ] ], [ "esp:3V3.2", "pot1:VCC", "red", [ "h-22.98", "v38.1" ] ], - [ "esp:GND.1", "pot1:GND", "black", [ "v0", "h-211.2" ] ] + [ "esp:GND.1", "pot1:GND", "black", [ "v0", "h-211.2" ] ], + [ "esp:4", "esp:5", "blue", [ "v0", "h12", "v-10" ] ] ], "dependencies": {} } \ No newline at end of file diff --git a/src/platforms/esp32/test/sim_boards/diagram.esp32c6.json b/src/platforms/esp32/test/sim_boards/diagram.esp32c6.json index 072ccd7aa..1e3d3f085 100644 --- a/src/platforms/esp32/test/sim_boards/diagram.esp32c6.json +++ b/src/platforms/esp32/test/sim_boards/diagram.esp32c6.json @@ -18,7 +18,9 @@ [ "esp:RX", "$serialMonitor:TX", "", [] ], [ "esp:GND.1", "pot1:GND", "black", [ "h-28.8", "v48", "h-211.2" ] ], [ "esp:3V3", "pot1:VCC", "red", [ "v0", "h-38.4", "v144" ] ], - [ "esp:3", "pot1:SIG", "green", [ "h0" ] ] + [ "esp:3", "pot1:SIG", "green", [ "h0" ] ], + [ "esp:6", "esp:7", "green", [ "h-12", "v-0" ] ] ], "dependencies": {} -} \ No newline at end of file +} + diff --git a/src/platforms/esp32/test/sim_boards/diagram.esp32h2.json b/src/platforms/esp32/test/sim_boards/diagram.esp32h2.json index 7b4d540bd..6759a56b6 100644 --- a/src/platforms/esp32/test/sim_boards/diagram.esp32h2.json +++ b/src/platforms/esp32/test/sim_boards/diagram.esp32h2.json @@ -18,7 +18,9 @@ [ "esp:RX", "$serialMonitor:TX", "", [] ], [ "esp:GND.2", "pot1:GND", "black", [ "h0" ] ], [ "esp:3V3", "pot1:VCC", "red", [ "v0", "h-19.2", "v105.6" ] ], - [ "esp:4", "pot1:SIG", "green", [ "h0" ] ] + [ "esp:4", "pot1:SIG", "green", [ "h0" ] ], + [ "esp:0", "esp:1", "blue", [ "h-12", "v12" ] ] ], "dependencies": {} -} \ No newline at end of file +} + diff --git a/src/platforms/esp32/test/sim_boards/diagram.esp32p4.json b/src/platforms/esp32/test/sim_boards/diagram.esp32p4.json index 6047b433b..c6c0ab6af 100644 --- a/src/platforms/esp32/test/sim_boards/diagram.esp32p4.json +++ b/src/platforms/esp32/test/sim_boards/diagram.esp32p4.json @@ -13,8 +13,10 @@ ], "connections": [ ["esp:37", "$serialMonitor:RX", "", []], - ["esp:38", "$serialMonitor:TX", "", []] + ["esp:38", "$serialMonitor:TX", "", []], + [ "esp:5", "esp:4", "blue", [ "v-12", "h0", "v-10", "h10" ] ] ], "serialMonitor": { "display": "terminal" }, "dependencies": {} } + diff --git a/src/platforms/esp32/test/sim_boards/diagram.esp32s2.json b/src/platforms/esp32/test/sim_boards/diagram.esp32s2.json index 9ebfe2722..d2ef4c2ab 100644 --- a/src/platforms/esp32/test/sim_boards/diagram.esp32s2.json +++ b/src/platforms/esp32/test/sim_boards/diagram.esp32s2.json @@ -18,7 +18,9 @@ [ "esp:RX", "$serialMonitor:TX", "", [] ], [ "pot1:SIG", "esp:11", "green", [ "h14.8", "v5.6" ] ], [ "esp:GND.1", "pot1:GND", "black", [ "h0" ] ], - [ "pot1:VCC", "esp:3V3", "red", [ "h24.4", "v-125.29" ] ] + [ "pot1:VCC", "esp:3V3", "red", [ "h24.4", "v-125.29" ] ], + [ "esp:3", "esp:4", "blue", [ "h-12", "v12" ] ] ], "dependencies": {} -} \ No newline at end of file +} + diff --git a/src/platforms/esp32/test/sim_boards/diagram.esp32s3.json b/src/platforms/esp32/test/sim_boards/diagram.esp32s3.json index 3c278bea3..9d1fa23b6 100644 --- a/src/platforms/esp32/test/sim_boards/diagram.esp32s3.json +++ b/src/platforms/esp32/test/sim_boards/diagram.esp32s3.json @@ -18,7 +18,9 @@ [ "esp:RX", "$serialMonitor:TX", "", [] ], [ "esp:GND.1", "pot1:GND", "black", [ "h0" ] ], [ "esp:3V3.2", "pot1:VCC", "red", [ "h-24.28", "v115.02" ] ], - [ "esp:11", "pot1:SIG", "green", [ "h-14.68", "v-48.18" ] ] + [ "esp:11", "pot1:SIG", "green", [ "h-14.68", "v-48.18" ] ], + [ "esp:4", "esp:5", "green", [ "h-12", "v10" ] ] ], "dependencies": {} -} \ No newline at end of file +} +