Skip to content

Commit

Permalink
Add test_gpio.erl to ESP32 test suite
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
UncleGrumpy committed Jan 9, 2025
1 parent 2966462 commit fd4ebdc
Show file tree
Hide file tree
Showing 15 changed files with 341 additions and 13 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/esp32-simtest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,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: Run ESP32-sim tests using Wokwi CI
working-directory: ./src/platforms/esp32/test/
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added support for `registered_name` in `erlang:process_info/2` and `Process.info/2`
- Added `net:gethostname/0` on platforms with gethostname(3).
- Added `socket:getopt/2`
- Added `supervisor:terminate_child/2`, `supervisor:restart_child/2` and `supervisor:delete_child/2`
- Added test_gpio.erl to esp32 test suite.

### Fixed
- ESP32: improved sntp sync speed from a cold boot.
Expand Down
5 changes: 5 additions & 0 deletions src/platforms/esp32/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/platforms/esp32/test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 4 additions & 0 deletions src/platforms/esp32/test/main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
3 changes: 3 additions & 0 deletions src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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
)
Expand Down
288 changes: 288 additions & 0 deletions src/platforms/esp32/test/main/test_erl_sources/test_gpio.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
%
% This file is part of AtomVM.
%
% Copyright 2025 Winford <[email protected]>
%
% 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.
9 changes: 9 additions & 0 deletions src/platforms/esp32/test/main/test_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 3 additions & 1 deletion src/platforms/esp32/test/sim_boards/diagram.esp32.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}

Loading

0 comments on commit fd4ebdc

Please sign in to comment.