Skip to content

Commit 8889cda

Browse files
committed
Add UART support to RP2 platform
Also add a sample code, `sim800l.erl` confirmed to work on both Pico-W and ESP32C2, using the same high level UART API. Signed-off-by: Paul Guyot <pguyot@kallisys.net>
1 parent 9c18753 commit 8889cda

File tree

7 files changed

+1004
-1
lines changed

7 files changed

+1004
-1
lines changed

examples/erlang/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,4 @@ pack_runnable(logging_example logging_example estdlib eavmlib)
4343
pack_runnable(http_client http_client estdlib eavmlib avm_network)
4444
pack_runnable(disterl disterl estdlib)
4545
pack_runnable(spi_flash spi_flash eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2)
46+
pack_runnable(sim800l sim800l eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2)

examples/erlang/rp2/CMakeLists.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ add_custom_command(
3838
)
3939
add_custom_target(spi_flash_uf2 ALL DEPENDS spi_flash.uf2)
4040
add_dependencies(spi_flash_uf2 spi_flash)
41+
42+
set(SIM800L_AVM ${CMAKE_BINARY_DIR}/examples/erlang/sim800l.avm)
43+
add_custom_command(
44+
OUTPUT sim800l.uf2
45+
DEPENDS ${SIM800L_AVM} UF2Tool
46+
COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o sim800l.uf2 -f universal -s 0x10180000 ${SIM800L_AVM}
47+
COMMENT "Creating UF2 file sim800l.uf2"
48+
VERBATIM
49+
)
50+
add_custom_target(sim800l_uf2 ALL DEPENDS sim800l.uf2)
51+
add_dependencies(sim800l_uf2 sim800l)
4152
pack_uf2(picow_blink picow_blink)
4253
pack_uf2(picow_wifi_sta picow_wifi_sta)
4354
pack_uf2(picow_wifi_ap picow_wifi_ap)

examples/erlang/sim800l.erl

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
%
2+
% This file is part of AtomVM.
3+
%
4+
% Copyright 2026 Paul Guyot <pguyot@kallisys.net>
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+
%%-----------------------------------------------------------------------------
22+
%% @doc SIM800L AT command demo.
23+
%%
24+
%% Opens a UART connection to a SIM800L GSM module and verifies it responds
25+
%% to basic AT commands. Prints firmware identification and signal quality
26+
%% every 10 seconds.
27+
%%
28+
%% Be careful: SIM800L boards can draw up to 2A and shouldn't be powered by
29+
%% the 3.3V of the usual Pico / ESP32 boards. It's ok for this demo but
30+
%% do not put a SIM card in them to avoid damaging your board.
31+
%%
32+
%% The SIM800L communicates at 115200 baud (8N1) by default.
33+
%%
34+
%% Default pins are auto-detected from the platform and chip model:
35+
%%
36+
%% Pico (UART1): TX=GP4, RX=GP5
37+
%% ESP32/S2/S3 (UART1): TX=17, RX=16
38+
%% ESP32-C3/C5 (UART1): TX=4, RX=5
39+
%% ESP32-C6/C61 (UART1): TX=4, RX=5
40+
%% @end
41+
%%-----------------------------------------------------------------------------
42+
-module(sim800l).
43+
-export([start/0]).
44+
45+
-define(AT_TIMEOUT, 2000).
46+
47+
start() ->
48+
{TX, RX} = default_pins(),
49+
io:format("Opening UART1 on TX=~B RX=~B~n", [TX, RX]),
50+
UART = uart:open("UART1", [
51+
{tx, TX},
52+
{rx, RX},
53+
{speed, 115200}
54+
]),
55+
%% SIM800L takes 3-5 seconds to boot after power-on
56+
case wait_for_module(UART, 5) of
57+
ok ->
58+
io:format("SIM800L responding to AT commands~n"),
59+
at_identify(UART),
60+
loop(UART);
61+
error ->
62+
io:format("SIM800L not responding, giving up~n"),
63+
uart:close(UART)
64+
end.
65+
66+
loop(UART) ->
67+
at_signal_quality(UART),
68+
timer:sleep(10000),
69+
loop(UART).
70+
71+
wait_for_module(_UART, 0) ->
72+
error;
73+
wait_for_module(UART, Retries) ->
74+
drain(UART),
75+
case at_command(UART, <<"AT">>) of
76+
{ok, _} ->
77+
ok;
78+
{error, _} ->
79+
timer:sleep(1000),
80+
wait_for_module(UART, Retries - 1)
81+
end.
82+
83+
at_identify(UART) ->
84+
case at_command(UART, <<"ATI">>) of
85+
{ok, Response} ->
86+
io:format("Module info: ~s~n", [Response]);
87+
{error, Reason} ->
88+
io:format("ATI failed: ~p~n", [Reason])
89+
end.
90+
91+
at_signal_quality(UART) ->
92+
case at_command(UART, <<"AT+CSQ">>) of
93+
{ok, Response} ->
94+
io:format("Signal quality: ~s~n", [Response]);
95+
{error, Reason} ->
96+
io:format("AT+CSQ failed: ~p~n", [Reason])
97+
end.
98+
99+
%%-----------------------------------------------------------------------------
100+
%% @private Send an AT command and collect the response until OK or ERROR.
101+
%%-----------------------------------------------------------------------------
102+
at_command(UART, Command) ->
103+
uart:write(UART, [Command, <<"\r\n">>]),
104+
collect_response(UART, []).
105+
106+
collect_response(UART, Acc) ->
107+
case uart:read(UART, ?AT_TIMEOUT) of
108+
{ok, Data} ->
109+
NewAcc = [Data | Acc],
110+
Combined = erlang:iolist_to_binary(lists:reverse(NewAcc)),
111+
case parse_response(Combined) of
112+
{ok, Body} -> {ok, Body};
113+
error -> {error, Combined};
114+
incomplete -> collect_response(UART, NewAcc)
115+
end;
116+
{error, timeout} when Acc =/= [] ->
117+
Combined = erlang:iolist_to_binary(lists:reverse(Acc)),
118+
case parse_response(Combined) of
119+
{ok, Body} -> {ok, Body};
120+
_ -> {error, {partial, Combined}}
121+
end;
122+
{error, timeout} ->
123+
{error, timeout}
124+
end.
125+
126+
%% Look for OK or ERROR in the accumulated response
127+
parse_response(Data) ->
128+
case binary:match(Data, <<"\r\nOK\r\n">>) of
129+
{_Pos, _Len} ->
130+
Body = strip_status(Data),
131+
{ok, Body};
132+
nomatch ->
133+
case binary:match(Data, <<"\r\nERROR\r\n">>) of
134+
{_Pos2, _Len2} -> error;
135+
nomatch -> incomplete
136+
end
137+
end.
138+
139+
%% Extract body between echo/first CRLF and final status line
140+
strip_status(Data) ->
141+
Trimmed = trim_leading_crlf(Data),
142+
case binary:match(Trimmed, <<"\r\nOK\r\n">>) of
143+
{Pos, _} -> binary:part(Trimmed, 0, Pos);
144+
nomatch -> Trimmed
145+
end.
146+
147+
trim_leading_crlf(<<"\r\n", Rest/binary>>) -> trim_leading_crlf(Rest);
148+
trim_leading_crlf(Data) -> Data.
149+
150+
%% Discard any pending data in the UART buffer
151+
drain(UART) ->
152+
case uart:read(UART, 100) of
153+
{ok, _} -> drain(UART);
154+
{error, timeout} -> ok
155+
end.
156+
157+
%%-----------------------------------------------------------------------------
158+
%% Platform-specific default pins
159+
%%-----------------------------------------------------------------------------
160+
default_pins() ->
161+
default_pins(atomvm:platform()).
162+
163+
%% {TX, RX}
164+
default_pins(pico) -> {4, 5};
165+
default_pins(esp32) -> esp32_default_pins().
166+
167+
esp32_default_pins() ->
168+
#{model := Model} = erlang:system_info(esp32_chip_info),
169+
esp32_default_pins(Model).
170+
171+
%% {TX, RX}
172+
esp32_default_pins(esp32) -> {17, 16};
173+
esp32_default_pins(esp32_s2) -> {17, 16};
174+
esp32_default_pins(esp32_s3) -> {17, 16};
175+
esp32_default_pins(esp32_c2) -> {4, 5};
176+
esp32_default_pins(esp32_c3) -> {4, 5};
177+
esp32_default_pins(esp32_c5) -> {4, 5};
178+
esp32_default_pins(esp32_c6) -> {4, 5};
179+
esp32_default_pins(esp32_c61) -> {4, 5};
180+
esp32_default_pins(_) -> {17, 16}.

libs/avm_rp2/src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ set(ERLANG_MODULES
2727
i2c
2828
pico
2929
spi
30+
uart
3031
)
3132

3233
pack_archive(avm_rp2 DEPENDS_ON eavmlib ERLC_FLAGS +warnings_as_errors MODULES ${ERLANG_MODULES})

0 commit comments

Comments
 (0)