|
| 1 | +% |
| 2 | +% This file is part of AtomVM. |
| 3 | +% |
| 4 | +% Copyright 2025 Winford <winford@object.stream> |
| 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}, {<<0>>, accepted_binary_pin}, |
| 28 | + {"GPIO_NUM_0", accepted_pin_string}, {button, accepted_pin_atom}, |
| 29 | + {{a, 0}, accepted_pin_tuple}, {[0, 1, 2], accepted_pin_list}, |
| 30 | + {2048, accepted_too_large}, {2.0, accepted_float}, {#{}, accepted_map} |
| 31 | +]). |
| 32 | +-define(BADMODES, [ |
| 33 | + {up, accepted_badarg}, {1, accepted_int_mode}, {<<0>>, accepted_binary_mode}, |
| 34 | + {<<"input">>, accepted_binary_string_mode}, {"input", accepted_mode_string}, |
| 35 | + {[input, up], accepted_mode_list}, {{input, up}, accepted_mode_tuple}, |
| 36 | + {2.0, accepted_float}, {#{}, accepted_map} |
| 37 | +]). |
| 38 | +-define(BADPULL, [ |
| 39 | + {1, accepted_int_pull}, {<<1>>, accepted_binary_pull}, |
| 40 | + {<<"up">>, accepted_binary_string_pull}, {"up", accepted_pull_string}, |
| 41 | + {{up, hold}, accepted_pull_tuple}, {[up, foo, bar], accepted_pull_list}, |
| 42 | + {sideways, accepted_invalid_atom}, {2.0, accepted_float}, {#{}, accepted_map} |
| 43 | +]). |
| 44 | +-define(BADLEVEL, [ |
| 45 | + {medium, accepted_bad_atom}, {-1, accepted_neg_level_number}, |
| 46 | + {10, accepted_badarg_number}, {<<1>>, accepted_binary_level}, |
| 47 | + {<<"high">>, accepted_binary_level}, {"high", accepted_level_string}, |
| 48 | + {{0, high}, accepted_level_tuple}, {[1], accepted_level_list}, |
| 49 | + {1.0, accepted_float}, {#{}, accepted_map} |
| 50 | +]). |
| 51 | + |
| 52 | +start() -> |
| 53 | + {Pin0, Pin1} = get_board_pins(maps:get(model, erlang:system_info(esp32_chip_info))), |
| 54 | + io:format("Starting GPIO test, this board should have a jumper wire between pins ~p and ~p.~n", [Pin0, Pin1]), |
| 55 | + ok = test_nifs(Pin0, Pin1), |
| 56 | + %% test ports with the pins reversed so we be sure to test reconfiguring an input to an output and vice versa |
| 57 | + test_ports(Pin1, Pin0). |
| 58 | + |
| 59 | +test_nifs(Input, Output) -> |
| 60 | + io:format("Testing nifs raise errors for badargs... "), |
| 61 | + ok = test_nif_badargs(Input), |
| 62 | + io:format("passed.~n"), |
| 63 | + io:format("Testing set_pin_mode/2, set_pin_pull/2, digital_write/2 & digital_read/1... "), |
| 64 | + ok = gpio:set_pin_mode(Input, input), |
| 65 | + ok = gpio:set_pin_pull(Input, up), |
| 66 | + ok = gpio:set_pin_mode(Output, output), |
| 67 | + ok = gpio:set_pin_pull(Output, floating), |
| 68 | + ok = gpio:digital_write(Output, high), |
| 69 | + high = gpio:digital_read(Input), |
| 70 | + ok = gpio:digital_write(Output, low), |
| 71 | + low = gpio:digital_read(Input), |
| 72 | + ok = gpio:digital_write(Output, 1), |
| 73 | + high = gpio:digital_read(Input), |
| 74 | + ok = gpio:digital_write(Output, 0), |
| 75 | + low = gpio:digital_read(Input), |
| 76 | + io:format("passed.~n"). |
| 77 | + |
| 78 | +test_ports(Input, Output) -> |
| 79 | + io:format("Testing ports return {error, Reason} for badargs... "), |
| 80 | + GPIO = gpio:start(), |
| 81 | + ok = test_port_bardargs(GPIO, Input), |
| 82 | + io:format("passed.~n"), |
| 83 | + io:format("Testing set_direction/3, set_level/3 & read/2... "), |
| 84 | + ok = gpio:set_direction(GPIO, Input, input), |
| 85 | + ok = gpio:set_pin_pull(Input, up), |
| 86 | + ok = gpio:set_direction(GPIO, Output, output), |
| 87 | + ok = gpio:set_pin_pull(Output, floating), |
| 88 | + ok = gpio:set_level(GPIO, Output, low), |
| 89 | + low = gpio:read(GPIO, Input), |
| 90 | + ok = gpio:set_level(GPIO, Output, high), |
| 91 | + high = gpio:read(GPIO, Input), |
| 92 | + ok = gpio:set_level(GPIO, Output, 0), |
| 93 | + low = gpio:read(GPIO, Input), |
| 94 | + ok = gpio:set_level(GPIO, Output, 1), |
| 95 | + io:format("passed.~n"), |
| 96 | + io:format("Testing GPIO interrupt... "), |
| 97 | + Self = self(), |
| 98 | + Listener = erlang:spawn(fun() -> interrupt_listener(Input, Self) end), |
| 99 | + erlang:spawn(fun() -> interrupt_after(1000, GPIO, Output, 0) end), |
| 100 | + ok = gpio:set_int(GPIO, Input, falling, Listener), |
| 101 | + receive |
| 102 | + {ok, interrupt} -> ok; |
| 103 | + Error -> throw(Error) |
| 104 | + after |
| 105 | + 5000 -> |
| 106 | + io:format("No interrupt after 5000 ms giving up"), |
| 107 | + throw(timeout_no_interrupt) |
| 108 | + end, |
| 109 | + erlang:spawn(fun() -> interrupt_after(1000, GPIO, Output, 0) end), |
| 110 | + ok = gpio:set_int(GPIO, Input, falling), |
| 111 | + receive |
| 112 | + {gpio_interrupt, Input} -> ok; |
| 113 | + Other -> throw(Other) |
| 114 | + after |
| 115 | + 5000 -> |
| 116 | + io:format("No interrupt after 5000 ms giving up"), |
| 117 | + throw(timeout_no_interrupt) |
| 118 | + end, |
| 119 | + io:format("passed.~n"). |
| 120 | + |
| 121 | +test_nif_badargs(Pin) -> |
| 122 | + Badpin_funs1 = [ digital_read, hold_en, hold_dis ], |
| 123 | + Badpin_funs2 = [ {set_pin_mode, output}, {set_pin_pull, floating}, {digital_write, low} ], |
| 124 | + Fun_args = [ {set_pin_mode, ?BADMODES}, {set_pin_pull, ?BADPULL}, {digital_write, ?BADLEVEL} ], |
| 125 | + |
| 126 | + lists:foreach(fun(TestFun) -> |
| 127 | + lists:foreach(fun({Badpin, Err}) -> ok = want_catch_throw(TestFun, Badpin, badarg, Err) end, ?BADPINS) |
| 128 | + end, Badpin_funs1), |
| 129 | + |
| 130 | + lists:foreach(fun({TestFun, Arg}) -> |
| 131 | + lists:foreach(fun({Badpin, Err}) -> ok = want_catch_throw(TestFun, Badpin, Arg, badarg, Err) end, ?BADPINS) |
| 132 | + end, Badpin_funs2), |
| 133 | + |
| 134 | + lists:foreach(fun({TestFun, BadArgs}) -> |
| 135 | + lists:foreach(fun({Badarg, Err}) -> ok = want_catch_throw(TestFun, Pin, Badarg, badarg, Err) end, BadArgs) |
| 136 | + end, Fun_args), |
| 137 | + ok. |
| 138 | + |
| 139 | +test_port_bardargs(GPIO, Pin) -> |
| 140 | + Badpin_funs2 = [ read, remove_int ], |
| 141 | + Badpin_funs3 = [ {set_direction, input}, {set_level, low}, {set_int, low} ], |
| 142 | + Fun_args = [ {set_direction, ?BADMODES}, {set_level, ?BADLEVEL}, {set_int, ?BADLEVEL} ], |
| 143 | + |
| 144 | + lists:foreach(fun(TestFun) -> |
| 145 | + lists:foreach(fun({Badpin, Err}) -> ok = want_error_tuple(TestFun, GPIO, Badpin, badarg, Err) end, ?BADPINS) |
| 146 | + end, Badpin_funs2), |
| 147 | + |
| 148 | + lists:foreach(fun({TestFun, Arg}) -> |
| 149 | + lists:foreach(fun({Badpin, Err}) -> ok = want_error_tuple(TestFun, GPIO, Badpin, Arg, badarg, Err) end, ?BADPINS) |
| 150 | + end, Badpin_funs3), |
| 151 | + |
| 152 | + lists:foreach(fun({TestFun, TestArgs}) -> |
| 153 | + lists:foreach(fun({Badarg, Err}) -> ok = want_error_tuple(TestFun, GPIO, Pin, Badarg, badarg, Err) end, TestArgs) |
| 154 | + end, Fun_args), |
| 155 | + ok. |
| 156 | + |
| 157 | +want_catch_throw(Fun, Pin, Catch, ErrorAtom) -> |
| 158 | + try gpio:Fun(Pin) of |
| 159 | + ok -> |
| 160 | + throw({Fun, ErrorAtom}); |
| 161 | + Any -> |
| 162 | + throw({Fun, Any}) |
| 163 | + catch |
| 164 | + _:Catch:_ -> |
| 165 | + ok |
| 166 | + end. |
| 167 | + |
| 168 | +want_catch_throw(Fun, Pin, Arg, Catch, ErrorAtom) -> |
| 169 | + try gpio:Fun(Pin, Arg) of |
| 170 | + ok -> |
| 171 | + throw({Fun, ErrorAtom}); |
| 172 | + Any -> |
| 173 | + throw({Fun, Any}) |
| 174 | + catch |
| 175 | + _:Catch:_ -> |
| 176 | + ok |
| 177 | + end. |
| 178 | + |
| 179 | +want_error_tuple(Fun, GPIO, Pin, Reason, ErrorAtom) -> |
| 180 | + try gpio:Fun(GPIO, Pin) of |
| 181 | + ok -> |
| 182 | + throw({Fun, ErrorAtom}); |
| 183 | + {error, Reason} -> |
| 184 | + ok |
| 185 | + catch |
| 186 | + Error -> |
| 187 | + throw({Fun, {caught, Error}}) |
| 188 | + end. |
| 189 | + |
| 190 | +want_error_tuple(Fun, GPIO, Pin, Arg, Reason, ErrorAtom) -> |
| 191 | + try gpio:Fun(GPIO, Pin, Arg) of |
| 192 | + ok -> |
| 193 | + throw({Fun, ErrorAtom}); |
| 194 | + {error, Reason} -> |
| 195 | + ok |
| 196 | + catch |
| 197 | + Error -> |
| 198 | + throw({Fun, {caught, Error}}) |
| 199 | + end. |
| 200 | + |
| 201 | +interrupt_after(Delay, GPIO, Pin, Level) -> |
| 202 | + timer:sleep(Delay), |
| 203 | + ok = gpio:set_level(GPIO, Pin, Level), |
| 204 | + ok = gpio:set_level(GPIO, Pin, 1 - Level). |
| 205 | + |
| 206 | +interrupt_listener(Pin, ReplyTo) -> |
| 207 | + receive |
| 208 | + {gpio_interrupt, Pin} -> |
| 209 | + ok = gpio:remove_int(whereis(gpio), Pin), |
| 210 | + ReplyTo ! {ok, interrupt}; |
| 211 | + Any -> |
| 212 | + throw({interrupt_listener, {received, Any}}) |
| 213 | + end, |
| 214 | + interrupt_listener(Pin, ReplyTo). |
| 215 | + |
| 216 | +get_board_pins(Chipset) -> |
| 217 | + case (Chipset) of |
| 218 | + esp32 -> {25, 26}; |
| 219 | + esp32_c3 -> {4, 5}; |
| 220 | + esp32_c6 -> {6, 7}; |
| 221 | + esp32_h2 -> {0, 1}; |
| 222 | + esp32_p4 -> {4, 5}; |
| 223 | + esp32_s2 -> {3, 4}; |
| 224 | + esp32_s3 -> {4, 5}; |
| 225 | + _ -> throw(unsupported_chipset) |
| 226 | + end. |
0 commit comments