Skip to content

Commit cffd8ff

Browse files
committed
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 <[email protected]>
1 parent 4d6d312 commit cffd8ff

15 files changed

+340
-13
lines changed

.github/workflows/esp32-simtest.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ jobs:
118118
export PATH=${PATH}:${HOME}/.cache/rebar3/bin
119119
. $IDF_PATH/export.sh
120120
idf.py -DSDKCONFIG_DEFAULTS='sdkconfig.ci.wokwi' set-target ${{matrix.esp-idf-target}}
121-
idf.py build
121+
idf.py -DAVM_TEST_PERIPHERALS='y' build
122122
123123
- name: Run ESP32-sim tests using Wokwi CI
124124
working-directory: ./src/platforms/esp32/test/

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- Added `net:gethostname/0` on platforms with gethostname(3).
1616
- Added `socket:getopt/2`
1717
- Added `supervisor:terminate_child/2`, `supervisor:restart_child/2` and `supervisor:delete_child/2`
18+
- Added test_gpio.erl to esp32 test suite.
1819

1920
### Fixed
2021
- ESP32: improved sntp sync speed from a cold boot.

src/platforms/esp32/test/CMakeLists.txt

+5
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ set(AVM_SELECT_IN_TASK ON)
5757

5858
project(atomvm-esp32-test)
5959

60+
option(AVM_TEST_PERIPHERALS "Enable extra peripheral tests." OFF)
61+
if (AVM_TEST_PERIPHERALS)
62+
message("\n** NOTICE: Including additional peripheral tests that will fail on QEMU!\n")
63+
endif()
64+
6065
# esp-idf does not use compile_feature but instead sets version in
6166
# c_compile_options
6267
# Ensure project is compiled with at least C11

src/platforms/esp32/test/README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,9 @@ The `WOKWI_CLI_TOKEN` needs to be set in your `Repository secrets` Settings -> A
6565
3. Now we run `idf.py build` and run the CI:
6666

6767
```shell
68-
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
68+
idf.py build -DSDKCONFIG_DEFAULTS='sdkconfig.ci.wokwi' -DAVM_TEST_PERIPHERALS='y' && \
69+
pytest --embedded-services=idf,wokwi --wokwi-timeout=90000 --target=${IDF_TARGET} \
70+
--wokwi-diagram=sim_boards/diagram.${IDF_TARGET}.json -s -W ignore::DeprecationWarning
6971
```
72+
73+
>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.

src/platforms/esp32/test/main/CMakeLists.txt

+4
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,9 @@
2121
idf_component_register(SRCS "test_main.c"
2222
INCLUDE_DIRS ".")
2323

24+
if (AVM_TEST_PERIPHERALS)
25+
add_compile_definitions(TEST_PERIPHERALS)
26+
endif()
27+
2428
add_subdirectory(test_erl_sources)
2529
target_link_libraries(${COMPONENT_LIB} esp32_test_modules)

src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt

+3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ compile_erlang(test_ssl)
5353
compile_erlang(test_time_and_processes)
5454
compile_erlang(test_twdt)
5555
compile_erlang(test_tz)
56+
compile_erlang(test_gpio)
5657

5758
add_custom_command(
5859
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/esp32_test_modules.avm"
@@ -74,6 +75,7 @@ add_custom_command(
7475
test_time_and_processes.beam
7576
test_twdt.beam
7677
test_tz.beam
78+
test_gpio.beam
7779
DEPENDS
7880
HostAtomVM
7981
"${CMAKE_CURRENT_BINARY_DIR}/test_esp_partition.beam"
@@ -92,6 +94,7 @@ add_custom_command(
9294
"${CMAKE_CURRENT_BINARY_DIR}/test_time_and_processes.beam"
9395
"${CMAKE_CURRENT_BINARY_DIR}/test_twdt.beam"
9496
"${CMAKE_CURRENT_BINARY_DIR}/test_tz.beam"
97+
"${CMAKE_CURRENT_BINARY_DIR}/test_gpio.beam"
9598
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
9699
VERBATIM
97100
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
%
2+
% This file is part of AtomVM.
3+
%
4+
% Copyright 2025 Winford <[email protected]>
5+
%
6+
% Licensed under the Apache License, Version 2.0 (the "License");
7+
% you may not use this file except in compliance with the License.
8+
% You may obtain a copy of the License at
9+
%
10+
% http://www.apache.org/licenses/LICENSE-2.0
11+
%
12+
% Unless required by applicable law or agreed to in writing, software
13+
% distributed under the License is distributed on an "AS IS" BASIS,
14+
% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
% See the License for the specific language governing permissions and
16+
% limitations under the License.
17+
%
18+
% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
19+
%
20+
21+
-module(test_gpio).
22+
23+
-export([start/0, test_nifs/2, test_ports/2]).
24+
25+
%% Bad argument and error reason raised if it is accepted
26+
-define(BADPINS, [
27+
{-1, accepted_neg_pin_number},
28+
{<<0>>, accepted_binary_pin},
29+
{"GPIO_NUM_0", accepted_pin_string},
30+
{button, accepted_pin_atom},
31+
{{a, 0}, accepted_pin_tuple},
32+
{[0, 1, 2], accepted_pin_list},
33+
{2048, accepted_too_large},
34+
{2.0, accepted_float},
35+
{#{}, accepted_map}
36+
]).
37+
-define(BADMODES, [
38+
{up, accepted_badarg},
39+
{1, accepted_int_mode},
40+
{<<0>>, accepted_binary_mode},
41+
{<<"input">>, accepted_binary_string_mode},
42+
{"input", accepted_mode_string},
43+
{[input, up], accepted_mode_list},
44+
{{input, up}, accepted_mode_tuple},
45+
{2.0, accepted_float},
46+
{#{}, accepted_map}
47+
]).
48+
-define(BADPULL, [
49+
{1, accepted_int_pull},
50+
{<<1>>, accepted_binary_pull},
51+
{<<"up">>, accepted_binary_string_pull},
52+
{"up", accepted_pull_string},
53+
{{up, hold}, accepted_pull_tuple},
54+
{[up, foo, bar], accepted_pull_list},
55+
{sideways, accepted_invalid_atom},
56+
{2.0, accepted_float},
57+
{#{}, accepted_map}
58+
]).
59+
-define(BADLEVEL, [
60+
{medium, accepted_bad_atom},
61+
{-1, accepted_neg_level_number},
62+
{10, accepted_badarg_number},
63+
{<<1>>, accepted_binary_level},
64+
{<<"high">>, accepted_binary_level},
65+
{"high", accepted_level_string},
66+
{{0, high}, accepted_level_tuple},
67+
{[1], accepted_level_list},
68+
{1.0, accepted_float},
69+
{#{}, accepted_map}
70+
]).
71+
72+
start() ->
73+
{Pin0, Pin1} = get_board_pins(maps:get(model, erlang:system_info(esp32_chip_info))),
74+
io:format(
75+
"Starting GPIO test, this board should have a jumper wire between pins ~p and ~p.~n", [
76+
Pin0, Pin1
77+
]
78+
),
79+
ok = test_nifs(Pin0, Pin1),
80+
%% test ports with the pins reversed so we be sure to test reconfiguring an input to an output and vice versa
81+
test_ports(Pin1, Pin0).
82+
83+
test_nifs(Input, Output) ->
84+
io:format("Testing nifs raise errors for badargs... "),
85+
ok = test_nif_badargs(Input),
86+
io:format("passed.~n"),
87+
io:format("Testing set_pin_mode/2, set_pin_pull/2, digital_write/2 & digital_read/1... "),
88+
ok = gpio:set_pin_mode(Input, input),
89+
ok = gpio:set_pin_pull(Input, up),
90+
ok = gpio:set_pin_mode(Output, output),
91+
ok = gpio:set_pin_pull(Output, floating),
92+
ok = gpio:digital_write(Output, high),
93+
high = gpio:digital_read(Input),
94+
ok = gpio:digital_write(Output, low),
95+
low = gpio:digital_read(Input),
96+
ok = gpio:digital_write(Output, 1),
97+
high = gpio:digital_read(Input),
98+
ok = gpio:digital_write(Output, 0),
99+
low = gpio:digital_read(Input),
100+
io:format("passed.~n").
101+
102+
test_ports(Input, Output) ->
103+
io:format("Testing ports return {error, Reason} for badargs... "),
104+
GPIO = gpio:start(),
105+
ok = test_port_bardargs(GPIO, Input),
106+
io:format("passed.~n"),
107+
io:format("Testing set_direction/3, set_level/3 & read/2... "),
108+
ok = gpio:set_direction(GPIO, Input, input),
109+
ok = gpio:set_pin_pull(Input, up),
110+
ok = gpio:set_direction(GPIO, Output, output),
111+
ok = gpio:set_pin_pull(Output, floating),
112+
ok = gpio:set_level(GPIO, Output, low),
113+
low = gpio:read(GPIO, Input),
114+
ok = gpio:set_level(GPIO, Output, high),
115+
high = gpio:read(GPIO, Input),
116+
ok = gpio:set_level(GPIO, Output, 0),
117+
low = gpio:read(GPIO, Input),
118+
ok = gpio:set_level(GPIO, Output, 1),
119+
io:format("passed.~n"),
120+
io:format("Testing GPIO interrupt... "),
121+
Self = self(),
122+
Listener = erlang:spawn(fun() -> interrupt_listener(Input, Self) end),
123+
erlang:spawn(fun() -> interrupt_after(1000, GPIO, Output, 0) end),
124+
ok = gpio:set_int(GPIO, Input, falling, Listener),
125+
receive
126+
{ok, interrupt} -> ok;
127+
Error -> throw(Error)
128+
after 5000 ->
129+
io:format("No interrupt after 5000 ms giving up"),
130+
throw(timeout_no_interrupt)
131+
end,
132+
erlang:spawn(fun() -> interrupt_after(1000, GPIO, Output, 0) end),
133+
ok = gpio:set_int(GPIO, Input, falling),
134+
receive
135+
{gpio_interrupt, Input} -> ok;
136+
Other -> throw(Other)
137+
after 5000 ->
138+
io:format("No interrupt after 5000 ms giving up"),
139+
throw(timeout_no_interrupt)
140+
end,
141+
io:format("passed.~n").
142+
143+
test_nif_badargs(Pin) ->
144+
Badpin_funs1 = [digital_read, hold_en, hold_dis],
145+
Badpin_funs2 = [{set_pin_mode, output}, {set_pin_pull, floating}, {digital_write, low}],
146+
Fun_args = [{set_pin_mode, ?BADMODES}, {set_pin_pull, ?BADPULL}, {digital_write, ?BADLEVEL}],
147+
148+
lists:foreach(
149+
fun(TestFun) ->
150+
lists:foreach(
151+
fun({Badpin, Err}) -> ok = want_catch_throw(TestFun, Badpin, badarg, Err) end,
152+
?BADPINS
153+
)
154+
end,
155+
Badpin_funs1
156+
),
157+
158+
lists:foreach(
159+
fun({TestFun, Arg}) ->
160+
lists:foreach(
161+
fun({Badpin, Err}) -> ok = want_catch_throw(TestFun, Badpin, Arg, badarg, Err) end,
162+
?BADPINS
163+
)
164+
end,
165+
Badpin_funs2
166+
),
167+
168+
lists:foreach(
169+
fun({TestFun, BadArgs}) ->
170+
lists:foreach(
171+
fun({Badarg, Err}) -> ok = want_catch_throw(TestFun, Pin, Badarg, badarg, Err) end,
172+
BadArgs
173+
)
174+
end,
175+
Fun_args
176+
),
177+
ok.
178+
179+
test_port_bardargs(GPIO, Pin) ->
180+
Badpin_funs2 = [read, remove_int],
181+
Badpin_funs3 = [{set_direction, input}, {set_level, low}, {set_int, low}],
182+
Fun_args = [{set_direction, ?BADMODES}, {set_level, ?BADLEVEL}, {set_int, ?BADLEVEL}],
183+
184+
lists:foreach(
185+
fun(TestFun) ->
186+
lists:foreach(
187+
fun({Badpin, Err}) -> ok = want_error_tuple(TestFun, GPIO, Badpin, badarg, Err) end,
188+
?BADPINS
189+
)
190+
end,
191+
Badpin_funs2
192+
),
193+
194+
lists:foreach(
195+
fun({TestFun, Arg}) ->
196+
lists:foreach(
197+
fun({Badpin, Err}) ->
198+
ok = want_error_tuple(TestFun, GPIO, Badpin, Arg, badarg, Err)
199+
end,
200+
?BADPINS
201+
)
202+
end,
203+
Badpin_funs3
204+
),
205+
206+
lists:foreach(
207+
fun({TestFun, TestArgs}) ->
208+
lists:foreach(
209+
fun({Badarg, Err}) ->
210+
ok = want_error_tuple(TestFun, GPIO, Pin, Badarg, badarg, Err)
211+
end,
212+
TestArgs
213+
)
214+
end,
215+
Fun_args
216+
),
217+
ok.
218+
219+
want_catch_throw(Fun, Pin, Catch, ErrorAtom) ->
220+
try gpio:Fun(Pin) of
221+
ok ->
222+
throw({Fun, ErrorAtom});
223+
Any ->
224+
throw({Fun, Any})
225+
catch
226+
_:Catch:_ ->
227+
ok
228+
end.
229+
230+
want_catch_throw(Fun, Pin, Arg, Catch, ErrorAtom) ->
231+
try gpio:Fun(Pin, Arg) of
232+
ok ->
233+
throw({Fun, ErrorAtom});
234+
Any ->
235+
throw({Fun, Any})
236+
catch
237+
_:Catch:_ ->
238+
ok
239+
end.
240+
241+
want_error_tuple(Fun, GPIO, Pin, Reason, ErrorAtom) ->
242+
try gpio:Fun(GPIO, Pin) of
243+
ok ->
244+
throw({Fun, ErrorAtom});
245+
{error, Reason} ->
246+
ok
247+
catch
248+
Error ->
249+
throw({Fun, {caught, Error}})
250+
end.
251+
252+
want_error_tuple(Fun, GPIO, Pin, Arg, Reason, ErrorAtom) ->
253+
try gpio:Fun(GPIO, Pin, Arg) of
254+
ok ->
255+
throw({Fun, ErrorAtom});
256+
{error, Reason} ->
257+
ok
258+
catch
259+
Error ->
260+
throw({Fun, {caught, Error}})
261+
end.
262+
263+
interrupt_after(Delay, GPIO, Pin, Level) ->
264+
timer:sleep(Delay),
265+
ok = gpio:set_level(GPIO, Pin, Level),
266+
ok = gpio:set_level(GPIO, Pin, 1 - Level).
267+
268+
interrupt_listener(Pin, ReplyTo) ->
269+
receive
270+
{gpio_interrupt, Pin} ->
271+
ok = gpio:remove_int(whereis(gpio), Pin),
272+
ReplyTo ! {ok, interrupt};
273+
Any ->
274+
throw({interrupt_listener, {received, Any}})
275+
end,
276+
interrupt_listener(Pin, ReplyTo).
277+
278+
get_board_pins(Chipset) ->
279+
case (Chipset) of
280+
esp32 -> {25, 26};
281+
esp32_c3 -> {4, 5};
282+
esp32_c6 -> {6, 7};
283+
esp32_h2 -> {0, 1};
284+
esp32_p4 -> {4, 5};
285+
esp32_s2 -> {3, 4};
286+
esp32_s3 -> {4, 5};
287+
_ -> throw(unsupported_chipset)
288+
end.

src/platforms/esp32/test/main/test_main.c

+9
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,15 @@ TEST_CASE("test_twdt", "[test_run]")
603603
}
604604
#endif
605605

606+
// test_gpio may only be used by Wokwi Sim and real hardware
607+
#ifdef TEST_PERIPHERALS
608+
TEST_CASE("test_gpio", "[test_run]")
609+
{
610+
term ret_value = avm_test_case("test_gpio.beam");
611+
TEST_ASSERT(ret_value == OK_ATOM);
612+
}
613+
#endif
614+
606615
void app_main(void)
607616
{
608617
UNITY_BEGIN();

src/platforms/esp32/test/sim_boards/diagram.esp32.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
["sd1:DI", "esp:23", "magenta", ["h38.4", "v-96.09", "h-139.93", "v77.03"]],
3737
["pot1:VCC", "esp:3V3", "red", ["h-19.2", "v-105.6", "h-139.39"]],
3838
["pot1:GND", "esp:GND.2", "black", ["v0"]],
39-
["pot1:SIG", "esp:4", "green", ["h-38.4", "v47.2"]]
39+
["pot1:SIG", "esp:4", "green", ["h-38.4", "v47.2"]],
40+
[ "esp:25", "esp:26", "blue", [ "v0", "h-12", "v9", "h10" ] ]
4041
],
4142
"dependencies": {}
4243
}
44+

0 commit comments

Comments
 (0)