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 b48dd8748..93d00b1c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,11 +22,15 @@ 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. - 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/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") diff --git a/src/platforms/esp32/components/avm_builtins/gpio_driver.c b/src/platforms/esp32/components/avm_builtins/gpio_driver.c index ef0279044..b319dc976 100644 --- a/src/platforms/esp32/components/avm_builtins/gpio_driver.c +++ b/src/platforms/esp32/components/avm_builtins/gpio_driver.c @@ -59,20 +59,11 @@ 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 -#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 +92,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, @@ -139,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) { @@ -147,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; @@ -178,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; } @@ -203,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; } @@ -223,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; } @@ -243,45 +266,42 @@ 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); @@ -293,8 +313,12 @@ static inline term gpio_digital_read(term gpio_num_term) 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 +336,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,9 +357,10 @@ 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; + port_ensure_available(ctx, TUPLE_SIZE(2)); + return port_create_error_tuple(ctx, NOPROC_ATOM); } struct GPIOData *gpio_data = ctx->platform_data; @@ -386,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) @@ -394,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) @@ -438,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) @@ -453,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); @@ -465,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); @@ -474,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; } @@ -482,41 +533,17 @@ 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)) { + 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); @@ -529,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; @@ -538,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)) { + 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)) { - return ERROR_ATOM; + 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); @@ -563,13 +604,16 @@ 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); } + return unregister_interrupt_listener(ctx, gpio_num); } @@ -609,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: @@ -626,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; @@ -657,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); @@ -688,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[]) @@ -709,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[]) @@ -717,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) @@ -744,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(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 = 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") 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 +} +