Skip to content

Commit 8a95b0c

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 0d8184c commit 8a95b0c

File tree

9 files changed

+1296
-3
lines changed

9 files changed

+1296
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Added
1010
- Added Erlang distribution over serial (uart)
11+
- Added UART API to rp2 platform
1112

1213
## [0.7.0-alpha.1] - 2026-04-06
1314

@@ -22,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2223
- Added RISC-V 64-bit (RV64IMAC) JIT backend
2324
- Added arm32 JIT backend
2425
- Added DWARF debug information support for JIT-compiled code
25-
- Added I2C and SPI APIs to rp2 platform
26+
- Added I2C, SPI APIs to rp2 platform
2627
- Added `code:get_object_code/1`
2728
- Added `erlang:display_string/1` and `erlang:display_string/2`
2829
- Added Thumb-2 support to armv6m JIT backend, optimizing code for ARMv7-M and later cores

examples/erlang/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ pack_runnable(i2c_scanner i2c_scanner eavmlib estdlib DIALYZE_AGAINST avm_esp32
4848
pack_runnable(i2c_lis3dh i2c_lis3dh eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32)
4949
pack_runnable(spi_flash spi_flash eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2)
5050
pack_runnable(spi_lis3dh spi_lis3dh eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2)
51+
pack_runnable(sim800l sim800l eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2)

examples/erlang/rp2/CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ add_custom_command(
7070
add_custom_target(spi_lis3dh_uf2 ALL DEPENDS spi_lis3dh.uf2)
7171
add_dependencies(spi_lis3dh_uf2 spi_lis3dh)
7272

73+
set(SIM800L_AVM ${CMAKE_BINARY_DIR}/examples/erlang/sim800l.avm)
74+
add_custom_command(
75+
OUTPUT sim800l.uf2
76+
DEPENDS ${SIM800L_AVM} UF2Tool
77+
COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o sim800l.uf2 -f universal -s 0x10180000 ${SIM800L_AVM}
78+
COMMENT "Creating UF2 file sim800l.uf2"
79+
VERBATIM
80+
)
81+
add_custom_target(sim800l_uf2 ALL DEPENDS sim800l.uf2)
82+
add_dependencies(sim800l_uf2 sim800l)
7383
pack_uf2(picow_blink picow_blink)
7484
pack_uf2(picow_wifi_sta picow_wifi_sta)
7585
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)