106г. GSM modem SIM800L over UART: AT commands, SMS, calls and TCP over GPRS — non-blocking response parsing
In 106а–106в the device went online by wire: Ethernet, its own stack, lwIP. Where there is no wire and Wi-Fi does not reach — at a country house, in a pump shed, in a car — the cellular network remains. The cheapest way to it is the SIM800L module: a GSM modem with GPRS controlled by text AT commands over a UART. In 37.1 such modules were described as simple to look at and treacherous to implement. Here is a driver that survives that treachery.
What is inside: what to power the module from and how to match the levels; the first commands and turning the signal level into dBm; a driver that never waits in a loop — a receive ring, line assembly, telling the reply to a command from unsolicited messages; SMS in Latin and in Cyrillic; an incoming call; GPRS and TCP with reconnection; resetting the modem and a watchdog that notices that the modem went silent or restarted.
Sources: DocID13587 Rev 17 (STM32F103x8/xB): PA9, PA10 are USART1_TX,
USART1_RX with 5 V tolerance, p. 31; the input high threshold is 0.65·VDD,
table 35, p. 62. The Keil pack FreeRTOS Cellular Interface 1.1.0 (FreeRTOS
code 1.4.0, files cellular_common.c, cellular_types.h, cellular_3gpp_api.c):
turning +CSQ into dBm, the <stat> registration values and the difference between the reply
to AT+CREG? and an unsolicited +CREG:. The ESP8266 driver from the CMSIS-Driver
2.10.0 pack (ESP8266.c) handles the > prompt separately from lines.
The project has no SIMCom documents on the SIM800L and no 3GPP standards: the command set,
the form of replies, the pin timings and the supply figures are recalled from them and
marked as unverified. Everything the section calls the run is the code
_verify/gsm106e/gsm106e.c on a computer (gcc 12.2.0) with a modem model in
Python 3.11; the firmware is built by armclang 6.24 for the Cortex-M3. Phone numbers,
the access point and addresses are placeholders.
What it is called in the documentation
Original Meaning AT command, final result code ( OK,ERROR)AT command, final reply information response data line before the final reply unsolicited result code (URC) message the modem sends on its own command echo ( ATE0,ATE1)echo: the modem repeats the characters it receives autobauding automatic speed detection received signal strength indication ( +CSQ)received signal level network registration status ( +CREG)network registration state SMS text mode ( +CMGF=1), PDU modeSMS text mode, PDU mode TE character set ( +CSCS), UCS2character set of the exchange, UCS2 — 16 bits per character data coding scheme (DCS) SMS text coding scheme calling line identification presentation ( +CLIP)caller number display PDP context, APN data session, access point name power key ( PWRKEY)modem power-on pin
106г.1 The SIM800L module: supply, levels, antenna, network
The supply comes first. The modem transmits in bursts, and in each burst the current jumps many times above the average. The range most often quoted is 3.4–4.4 V with current pulses up to 2 A during transmission — recalled from the SIMCom document, which the project does not have, the figures are not verified. The conclusion is simple: the module does not live from the board's 3.3 V line or from a USB port. It needs its own source able to deliver the pulse — a lithium cell or a step-down converter (58) with current margin — short thick wires and a large capacitor right at the module. Otherwise during transmission the voltage sags and the modem restarts — silently, in the middle of a command (106г.6).
Levels. The modem talks over the UART at the levels of its internal supply, and they must be matched with the STM32 in both directions (16.7). The F103 receive input is PA10: it is 5 V tolerant (DocID13587, p. 31), and it is guaranteed to see a "one" from 0.65·VDD (table 35, p. 62) — at 3.3 V that is 2.145 V. The modem's output high level must be compared with this threshold using the modem document. In the other direction — the PA9 output at 3.3 V into the modem input: what level it tolerates is again in the SIMCom document; many module boards have a divider or a transistor stage for this. The ground is common, the TX of one goes to the RX of the other.
Control pins. PWRKEY turns the modem on and off when held
at ground; RST restarts it. Many small modules do not bring out
PWRKEY — the modem turns on by itself when power is applied (recalled, not
verified), and then RST is left for resets. The driver of this section
handles both: a pulse on PWRKEY at start, RST when the modem stopped
answering. The pulse lengths in the driver (1.2 s and 0.2 s) are chosen with
margin; what the SIMCom document requires is not verified.
Antenna and network. Without an antenna the modem registers poorly or not at all, and transmitting without a load harms the transmitter. The SIM800L works only in 2G networks (GSM and GPRS). In a number of countries operators are switching 2G networks off or have already done so — before designing the module into a product, check whether 2G will exist where the device is installed, and for how long; otherwise an LTE modem is needed (similar AT commands, other subtleties).
The wiring and the structure of the driver are in the figure.
106г.2 AT commands: echo, final reply, first questions to the modem
A command is an AT… line ended by CR. The reply is zero or
more data lines and a final reply: OK, ERROR or a detailed error
+CME ERROR: n (for SMS — +CMS ERROR: n). Each reply line is framed
by CR LF. By default the modem also repeats everything it receives —
the echo, and the echo ends with a single CR, without LF. So the first setting is
ATE0.
The first questions without which there is no going further:
| Command | Reply in the run | Meaning |
|---|---|---|
AT |
OK |
the modem is alive and caught the speed |
AT+CPIN? |
+CPIN: READY |
the SIM card is ready |
AT+CSQ |
+CSQ: 20,0 |
signal level (0…31, 99 — unknown) and errors |
AT+CREG? |
+CREG: 0,1 |
the second number is registration: 1 — home, 5 — roaming |
Signal level in dBm. The FreeRTOS code from the Keil pack turns the first
number of +CSQ into −113 + 2 × rssi for 0…31, and 99 is "unknown"
(cellular_common.c). The same conversion in the driver:
int16_t gsm_csq_dbm(int16_t rssi)
{
return rssi >= 0 && rssi <= 31 ? (int16_t)(-113 + 2 * rssi) : 0; /* 99 — неизвестно: 0 */
}
The run checks it against the formula:
| rssi | 0 | 1 | 2 | 10 | 15 | 20 | 25 | 30 | 31 | 99 |
|---|---|---|---|---|---|---|---|---|---|---|
| dBm | −113 | −111 | −109 | −93 | −83 | −73 | −63 | −53 | −51 | unknown (0) |
The step is 2 dB, the whole span is from −113 to −51 dBm. The same FreeRTOS code turns the GSM signal into level "bars" by the thresholds −104, −98, −89 and −80 dBm — a ready scale for an indicator on the device.
Registration. The <stat> values are (cellular_types.h): 0 — not
searching, 1 — registered in the home network, 2 — searching, 3 — denied, 4 —
unknown, 5 — roaming. Work is possible at 1 and 5. A subtlety from the
comment in cellular_3gpp_api.c: in the reply to AT+CREG? the first number is
the mode and the second is the state (+CREG: 2,1), while in an unsolicited +CREG:,
which the modem sends by itself after AT+CREG=1, the first number is already the state
(+CREG: 1). A parser that takes "the second number" goes wrong on the unsolicited
line. The driver does not enable unsolicited +CREG: and asks
itself.
Autobauding. As recalled from the SIMCom document, out of the factory the SIM800L
picks the speed from the first AT characters and stays silent until it has.
Hence the rule: send AT several times with a short pause until it
answers. In the model autobauding needs to see AT twice: the first AT
is lost, and in the run the modem answered on the second attempt. The driver sends AT
every 0.3 s up to 40 times, then resets the modem.
Start in the run. PWRKEY at ground for 1200 ms, another 3000 ms the model
boots, the modem answered the second AT — the driver moved on to setup
at 4532 ms. Then ATE0, AT+CLIP=1, AT+CMGF=1, AT+CNMI=2,1,0,0,0,
AT+CIPHEAD=1; +CPIN: READY — on the third attempt (twice
+CME ERROR: 14: in the model the SIM is ready 1.5 s after boot), and 11 polls of
AT+CREG? once a second until the model "found the network" (11821 ms
after boot, the time is random). Ready at 17008 ms. The first +CSQ
gave 99 — no network yet, the last before ready gave 20, that is −73 dBm.
106г.3 A non-blocking driver: ring, lines, reply and URC
The modem answers slowly by processor standards: at 115 200 baud a byte takes 86.8 µs, and the modem thinks over a command from milliseconds to tens of seconds. Waiting in a loop is not an option. The driver has three layers, and none of them waits.
Ring. The RXNE interrupt puts a byte into a 512-byte ring — and that is all
(like the DMA ring from 16б, only byte by byte). The main loop calls
gsm_poll(&G, ms): it takes everything that has arrived, checks the timeout
of the current command and makes a step of the state machine. In the run gsm_poll was called once every
1 ms, and the ring never held more than 12 bytes; nothing was lost.
Lines. A line is assembled byte by byte. The end is CR; LF
is skipped; empty lines are dropped. This cuts the same way both
replies CR LF text CR LF and the echo text CR. Two things are recognised without waiting for
the end of the line: the > prompt and the TCP data header +IPD,n::
static void on_byte(gsm_t *g, uint8_t b)
{
if (g->ipd_left) { /* данные TCP: длина известна, CR и LF — тоже данные */
g->ipd[g->ipd_len++] = b;
if (--g->ipd_left == 0 || g->ipd_len == GSM_DATA) {
emit(g, GSM_EV_TCP_DATA, (const char *)g->ipd, g->ipd_len);
g->ipd_len = 0;
}
return;
}
if (b == '\n')
return;
if (b == '\r') { /* конец строки; эхо кончается одним CR */
if (g->len) {
g->line[g->len] = 0;
on_line(g);
}
g->len = 0;
return;
}
if (g->len < GSM_LINE - 1u)
g->line[g->len++] = (char)b;
g->line[g->len] = 0;
if (g->want_prompt && g->len == 2 && g->line[0] == '>' && g->line[1] == ' ') {
g->len = 0; /* «> » без перевода строки */
on_prompt(g);
} else if (b == ':' && g->gp == GSM_GP_UP && g->len > 5 && starts(g->line, "+IPD,")) {
g->ipd_left = (uint16_t)atou(g->line + 5);
g->ipd_len = 0;
g->len = 0;
}
}
Whose line. This is the key place of the driver. While a command is running,
the line is checked in order: the echo of our command — drop it; a final reply —
end the command; a line that starts like the reply of the current
command (+CSQ: for AT+CSQ) — into the reply; everything else is an unsolicited
message, even if it came in the middle of the reply:
static void on_line(gsm_t *g)
{
const char *s = g->line;
if (g->busy) {
if (!strcmp(s, g->cmd)) { /* эхо своей команды */
g->n_echo++;
if (!g->echo_on && g->st >= GSM_ST_SIM)
g->rebooted = 1; /* эхо вернулось само: модем перезапустился */
return;
}
uint8_t f = final(g, s);
if (f) {
if (g->fin == GSM_FIN_IP && f == GSM_RES_OK)
resp_add(g, s);
finish(g, f);
return;
}
if (g->prefix[0] && starts(s, g->prefix)) {
resp_add(g, s);
g->take_next = starts(s, "+CMGR:"); /* за заголовком SMS — строка текста */
return;
}
if (g->take_next && !is_urc(s)) {
g->take_next = 0;
resp_add(g, s);
return;
}
}
urc(g, s);
}
The final reply also depends on the command: for ordinary ones it is OK; for AT+CIPSEND —
SEND OK; for AT+CIPSHUT — SHUT OK; for AT+CIPSTART the first OK
is not yet a connection, the wait is for CONNECT OK; and AT+CIFSR has no final reply
at all — the reply ends with the address line.
One command at a time. A command is sent, its deadline is stored, and until
the final reply or the deadline there are no new commands. The state machine (figure) makes one step per
call: power-on, AT until a reply, setup, SIM, registration, and in the
READY state — jobs by priority: a call, reading SMS from the +CMTI
queue, our own SMS, a TCP packet, GPRS connection, an application command, and
every 20 s — polling AT+CSQ and AT+CREG?.
Test under load. The model replied with a delay of 2…30 ms,
left up to 5 ms between reply lines and with probability 0.1 inserted
an unsolicited message before each line; besides, every 300…1000
ms came +CMTI (a new SMS) or RING with +CLIP (a call from an unknown
number). The application sent AT+CCLK? (each reply is unique — the model
writes milliseconds into it), and the reply was compared with what the model
sent. For comparison the bench parsed the same bytes "naively": the reply is
the first non-empty line after the command.
| echo off | echo on | |
|---|---|---|
| commands | 1000 | 1000 |
| URC in the middle of a reply | 279 | 351 |
+CMTI received / SMS read and deleted |
217 / 217 | 296 / 296 |
| calls from an unknown number rejected | 121 | 124 |
| driver: correct replies | 1000 | 1000 |
| "naive" parsing: correct replies | 885 | 0 |
| of the errors: URC instead of the reply / echo instead of the reply | 114 / 0 | 5 / 994 |
With echo the "naive" parsing never hits: the first line is the repeated
command. Without echo it is wrong on every eighth or ninth command — where
RING or +CMTI got in before the reply.
106г.4 SMS and calls
SMS in Latin. Text mode (AT+CMGF=1), the GSM character set,
coding scheme 0. The command AT+CMGS="number", in reply — > without
a line break: the modem waits for the text. The text ends with Ctrl+Z
(0x1A); Esc (0x1B) cancels. Then +CMGS: n — the message number — and OK.
A line parser that waits for CR LF after > will wait forever: the modem
is silent until it gets the text. So > is recognised by two characters
(106г.3), and the text is sent right at the moment of the prompt:
static void on_prompt(gsm_t *g)
{
g->want_prompt = 0;
g->n_prompt++;
gsm_hw_tx(g->out, g->out_len);
if (g->ctrlz) {
static const uint8_t z = 0x1Au; /* Ctrl+Z — конец текста SMS */
gsm_hw_tx(&z, 1);
}
}
In the run 0.7 ms passed from the last byte of the prompt to the first byte of the text
— until the next call of gsm_poll. The whole sending of Test 1 took three
commands (AT+CSCS="GSM", AT+CSMP=17,167,0,0, AT+CMGS) and 2772 ms, of
which almost everything is the model "network" (1.5…3 s per message).
Cyrillic means UCS2. The GSM character set has no Cyrillic, so the text is
written in UCS2: each character is four hex digits of its
Unicode number. AT+CSCS="UCS2" switches all exchange strings to UCS2,
AT+CSMP=17,167,0,8 sets coding scheme 8 (the AT+CSMP parameters are
recalled from 3GPP TS 27.005, not verified). The driver converts UTF-8 to UCS2
itself; the run checks it against the Python encoder:
| Text | Characters | UTF-8 bytes | UCS2, hex digits |
|---|---|---|---|
Привет, мир! |
12 | 21 | 48: 041F04400438043204350442002C0020043C043804400021 |
Темп. 21,5 °C |
13 | 18 | 52 |
SIM800L |
7 | 7 | 28 |
A trap: in UCS2 mode the number in AT+CMGS is written in UCS2 too —
+70000000000 turns into 48 digits 002B0037…0030. The model rejects a number in Latin
(+CMS ERROR), as, from memory, does the real modem. Here is
the piece of the driver that builds the command:
p = cat(c, "AT+CMGS=\"", e); /* в режиме UCS2 и номер — в UCS2 */
if (g->sms_ucs)
p += gsm_utf8_to_ucs2hex(g->sms_num, p, (uint16_t)(e - p - 1));
else
p = cat(p, g->sms_num, e);
cat(p, "\"", e);
cmd_ex(g, c, "+CMGS:", GSM_FIN_OK, 30000, g->sms_text, g->sms_text_len, 1);
Sending Привет, мир! takes 2661 ms, the text is 48 bytes after >. The text
buffer in the driver is 280 bytes, exactly 70 UCS2 characters: that is how many, as recalled from
3GPP TS 23.038, fit into one message (in Latin with the 7-bit coding —
160); a long text is split into parts — not done here.
Receiving SMS. AT+CNMI=2,1,0,0,0 tells the modem to report a new SMS
with the line +CMTI: "SM",n — n is the slot number on the SIM. The driver puts the number into a
queue and reads it in READY: AT+CSCS="UCS2", AT+CMGR=n, AT+CMGD=n.
The reply to AT+CMGR is two lines: the header +CMGR: "REC UNREAD","<number>",… and
the text on a separate line that has no prefix of its own. So after the
header the driver takes the next line as the text — unless it is a URC
(the model inserts unsolicited messages there too). In the run the SMS "Temperature 21.5 °C" (in Russian) from
the number +70000000001 reached the application 56 ms after +CMTI and was
deleted from the SIM by the next command. The number queue holds 8; if it is full,
the number is lost and the SMS stays on the SIM — that is what the n_sms_drop counter is for, in the
run it is 0.
Calls. An incoming call is a repeating RING, and after AT+CLIP=1 each one is
followed by +CLIP: "number",145,…. The driver decides once per call:
it asks the application (on_call), and in READY the call job sends ATA —
answer — or ATH — reject. The end of a call is NO CARRIER (or ATH
from our side). In the run our own number got ATA, the model replied
OK 341 ms after the first RING (of which 200…600 ms the model "picks up
the receiver"); 3 s later the application hung up — ATH and OK in 27 ms.
For an unknown number ATH reached the modem 4 ms after RING. Repeated RINGs of the same
call do not change the decision.
106г.5 GPRS and TCP: connecting, data, reconnecting
The command order is recalled from the SIMCom document, not verified; the model
requires exactly this order and answers ERROR to a violation:
| Command | Final reply | What it does |
|---|---|---|
AT+CIPSHUT |
SHUT OK |
close everything, return to the initial state |
AT+CSTT="internet" |
OK |
access point (APN) |
AT+CIICR |
OK |
bring up the data session — seconds |
AT+CIFSR |
address line | own address; without this command CIPSTART fails |
AT+CIPSTART="TCP","192.0.2.1",7 |
OK, then CONNECT OK |
connection to the server |
AT+CIPSEND=n |
>, n bytes, SEND OK |
send n bytes |
Each one is a step of the GPRS job; an error in any of them means a pause and starting over with
AT+CIPSHUT. In the run the model handed out the address 198.51.100.23, the connection
came up in 3111 ms (of which 1…2.5 s is AT+CIICR in the model).
Data from the server with AT+CIPHEAD=1 arrive as +IPD,n: and n bytes
without a line break at the end. The length in the header is the only reliable
way to find the end: the data may contain CR LF, OK and RING.
After : the driver switches to data mode and counts n bytes
(106г.3). The model echo server returned all packets byte for byte:
| Packet | Bytes | To SEND OK, ms |
To the echo, ms |
|---|---|---|---|
hello |
5 | 206 | 352 |
CR LF OK CR LF |
6 | 139 | 335 |
+IPD,3:abc CR LF RING CR LF |
18 | 125 | 392 |
| random bytes | 200 | 151 | 305 |
Reconnection. CLOSED (the server closed the connection) and +PDP: DEACT
(the network dropped the session) are unsolicited messages; the driver marks TCP
as down and sets a pause by the rule of 198и.6: 1, 2, 4 … 32 s plus a random
addition of up to a quarter. TCP retransmission itself plays no part here — it is inside the modem
(106.6). A packet that did not go out is not thrown away: gsm_tcp_send
keeps it, and it goes out after the connection.
static void tcp_down(gsm_t *g, const char *why)
{
if (g->gp == GSM_GP_UP)
emit(g, GSM_EV_TCP_DOWN, why, 0);
g->gp = GSM_GP_DOWN;
g->ipd_left = 0;
if (g->job != GSM_J_GPRS) /* идущее подключение само решит, когда повторить */
g->gp_wait = g->now + next_pause(g);
}
| Event in the model | What happened | Online again |
|---|---|---|
| the server closed the connection | one pause and the full command cycle | after 3658 ms |
| the network was gone for 20 s | 3 failed AT+CIICR, pauses 2427, 4763 and 9329 ms |
after 26187 ms, 6187 ms after the network returned |
The pauses between attempts are from the failure to the next AT+CIPSHUT, with
the addition. The delay after the network returned is the price of the growing pause, as in
198и.6.
106г.6 Supply, reset and watchdog
The modem is a separate computer with its own failures (37.1). The driver tells two cases apart.
The modem went silent. Any timeout in READY — and the next command is a short
AT with a 1 s deadline; a reply clears the alarm. Timeouts in a row are counted, and
on the third the driver holds RST at ground for 0.2 s, waits 3 s for boot and
starts with AT:
static void finish(gsm_t *g, uint8_t res)
{
g->busy = 0;
g->want_prompt = 0;
g->take_next = 0;
g->result = res;
g->have = 1;
if (res == GSM_RES_TIMEOUT) {
g->n_tmo++;
if (g->st >= GSM_ST_SETUP)
g->tmo_row++;
if (g->st == GSM_ST_READY)
g->probe = 1; /* жив ли модем — спросить коротким AT */
} else {
g->tmo_row = 0;
if (res == GSM_RES_OK && (!strcmp(g->cmd, "ATE0") || !strcmp(g->cmd, "ATE1")))
g->echo_on = g->cmd[3] == '1';
}
}
if (g->tmo_row >= GSM_TMO_MAX) { /* модем молчит: сброс выводом RST */
g->n_reset++;
emit(g, GSM_EV_RESET, g->cmd, (uint16_t)g->n_reset);
reset_all(g);
set_st(g, GSM_ST_RESET);
r = GSM_RES_NONE;
} else if (g->rebooted) { /* модем жив, но забыл настройки: заново с ATE0 */
g->n_reboot++;
emit(g, GSM_EV_RESET, "ECHO", (uint16_t)g->n_reboot);
reset_all(g);
set_st(g, GSM_ST_SETUP);
r = GSM_RES_NONE;
}
In the run the model "hung" with TCP up and stayed silent until RST
was pulled. The periodic poll noticed it: after 22563 ms — up to 20 s until the
poll and three timeouts (AT+CSQ 2 s, two AT of 1 s each). After the reset — the
same road as at power-on, and TCP came up again 34616 ms after the
hang.
The modem restarted on its own. This is the supply sag from 106г.1: in
the model it happened at the moment a packet was sent (AT+CIPSEND). After the
restart the modem forgot everything: the speed, ATE0, the access point, the connection. With autobauding it sends
neither RDY nor any other message — it is silent. After
10 s the AT+CIPSEND deadline expired, the driver asked AT: the first was lost in
autobauding, the second one the modem answered with echo. Echo while echo
is off is a sure sign of a restart; the driver went back to setup without
RST. Noticed after 11309 ms, TCP up again after 13593 ms, and the
same packet that stayed in the driver came back as an echo after 13914 ms.
The firmware is built the same way as the bench: the USART interrupt puts a byte into
the driver ring, transmission goes through its own ring on TXE, the main loop
calls gsm_poll and sleeps until the next interrupt:
void USART1_IRQHandler(void)
{
uint32_t sr = USART1->SR;
if (sr & (USART_SR_RXNE | USART_SR_ORE)) /* чтение DR снимает и RXNE, и ORE */
gsm_rx_isr(&G, (uint8_t)USART1->DR);
if ((USART1->CR1 & USART_CR1_TXEIE) && (sr & USART_SR_TXE)) {
if (tx_tail != tx_head) {
USART1->DR = txq[tx_tail];
tx_tail = (uint16_t)((tx_tail + 1u) % TXQ);
} else {
USART1->CR1 &= ~USART_CR1_TXEIE; /* очередь пуста */
}
}
}
/* ── платформа драйвера ───────────────────────────────────────────────── */
void gsm_hw_tx(const uint8_t *p, uint16_t n)
{
for (uint16_t i = 0; i < n; i++) {
uint16_t nx = (uint16_t)((tx_head + 1u) % TXQ);
if (nx == tx_tail)
break; /* очередь полна — драйвер больше 512 байт за раз не шлёт */
txq[tx_head] = p[i];
tx_head = nx;
}
USART1->CR1 |= USART_CR1_TXEIE;
}
The driver structure with all buffers takes 2592 bytes of RAM. The microcontroller watchdog (12.2, 33а) does not replace this: it restarts the firmware, not the modem; but it will catch a loop hung waiting for the modem's reply — this driver has no such loop.
106г.7 Pitfalls
The module restarts when sending an SMS or connecting GPRS. Supply: a sag during transmission. Its own source with current margin, thick short wires, a capacitor at the module; in the firmware — the restart sign (the echo came back) and setting up again.
The reply to a command is the command itself. Echo is on. The first
command is ATE0; the parser must drop a line equal to its own
command. With echo the "naive" parsing was wrong in 994 cases out of 1000.
RING or +CMTI came instead of the reply. An unsolicited message
in the middle of a reply. The reply is identified by the start of the line (+CSQ:), not by
order; everything else is a URC. Without echo the "naive" parsing was wrong 115 times out of
1000.
AT+CMGS or AT+CIPSEND hangs until the timeout. The parser waits for the end of
the line after >, and there is none. The prompt is recognised by two characters.
An SMS in Cyrillic goes out empty or with an error. The UCS2 character set is not enabled or the number is left in Latin. In UCS2 mode the number is in UCS2 too.
TCP data breaks replies. A packet containing CR LF or OK was parsed
as lines. The length from +IPD,n: — and n bytes without parsing.
New SMS pile up on the SIM. The +CMTI queue overflowed, the number
was lost. A queue with margin, a loss counter; on a loss — reread the
whole storage (not done here).
AT+CIPSTART answers ERROR at once. AT+CIFSR was skipped or
AT+CIPSHUT was not done after a drop — the modem is in the wrong state (from memory,
so in the model).
The device does not register, +CSQ: 99,99. No antenna, no 2G network in
this place, or no money or services on the SIM card; +CREG: 0,3 — the network
refused.
106г.8 Summary
- The SIM800L is a 2G modem: its own supply with margin for the transmission pulses, UART levels checked against the documents (on the F103 the PA10 input is 5 V tolerant, the "1" threshold is 0.65·VDD, 2.145 V at 3.3 V). 2G networks are being switched off in a number of countries.
- A reply is lines in CR LF and a final
OK/ERROR; the echo ends with a single CR;>comes without a line break;AT+CIFSRanswers withoutOK. +CSQ: −113 + 2 × rssi dBm, from −113 to −51, 99 — unknown;+CREG: work at 1 and 5.- A driver without waits: the interrupt — into a 512-byte ring,
gsm_poll— lines, the reply by the start of the line, everything else is a URC; one command at a time with its own deadline. Under load — 2000 of 2000 replies correct; "naive" parsing — 885 of 1000 without echo and 0 of 1000 with echo. - SMS in Cyrillic means UCS2: 4 digits per character,
Привет, мир!is 48 digits, and the number is in UCS2 too. - TCP:
CIPSHUT→CSTT→CIICR→CIFSR→CIPSTART→CIPSEND; read+IPD,n:data by length; afterCLOSEDand+PDP: DEACT— a growing pause, the packet is not lost. - Modem watchdog: a timeout → a short
AT, three timeouts in a row →RSTfor 0.2 s; echo while echo is off → the modem restarted, set it up again. The hang was noticed in 22.6 s, the restart in 11.3 s. - The whole driver is 2592 bytes of RAM and about 6.5 KB of code on the Cortex-M3.
Full sources
gsm106e.h — driver: buffers, states, events and prototypes.
/* gsm106e.h — раздел 106г: драйвер GSM-модема SIM800L по UART без блокировки.
* Байты приёма кладёт прерывание USART (gsm_rx_isr) в кольцо; всё остальное — в gsm_poll из главного цикла:
* сборка строк, ответ на текущую команду, незапрошенные сообщения (URC), автомат модема, SMS, звонок, GPRS и TCP.
* Платформа даёт две функции: gsm_hw_tx (отправить байты в USART) и gsm_hw_pin (PWRKEY и RST модема).
* Один и тот же код собирается в прошивку (fw106e.c) и в стенд на компьютере (host106e.c). */
#ifndef GSM106E_H
#define GSM106E_H
#include <stdint.h>
#define GSM_RXQ 512u /* кольцо приёма */
#define GSM_LINE 200u /* самая длинная строка от модема */
#define GSM_RESP 320u /* строки ответа на одну команду */
#define GSM_CMD 96u /* текст команды — для сверки с эхом */
#define GSM_DATA 280u /* данные после «> »: 70 знаков SMS в UCS2 (hex) или пакет TCP */
#define GSM_SMSQ 8u /* номера SMS из +CMTI, ещё не прочитанные */
#define GSM_TMO_MAX 3u /* столько таймаутов подряд — и модем сбрасывается выводом RST */
enum { GSM_PIN_PWRKEY, GSM_PIN_RST };
enum { GSM_RES_NONE, GSM_RES_OK, GSM_RES_ERROR, GSM_RES_TIMEOUT };
/* чем кончается ответ: OK, SEND OK, SHUT OK, CONNECT OK или строкой адреса (у AT+CIFSR нет OK) */
enum { GSM_FIN_OK, GSM_FIN_SEND, GSM_FIN_SHUT, GSM_FIN_CONNECT, GSM_FIN_IP };
enum { GSM_ST_OFF, GSM_ST_PWRKEY, GSM_ST_SYNC, GSM_ST_SETUP, GSM_ST_SIM, GSM_ST_REG, GSM_ST_READY, GSM_ST_RESET };
enum { GSM_GP_DOWN, GSM_GP_UP };
enum { GSM_J_NONE, GSM_J_PROBE, GSM_J_CALL, GSM_J_SMSR, GSM_J_SMSS, GSM_J_USER, GSM_J_GPRS, GSM_J_SEND, GSM_J_CHECK };
enum { GSM_EV_STATE, GSM_EV_SMS, GSM_EV_SMS_SENT, GSM_EV_CALL, GSM_EV_CALL_END, GSM_EV_TCP_UP, GSM_EV_TCP_DOWN,
GSM_EV_TCP_DATA, GSM_EV_TCP_SENT, GSM_EV_RESET, GSM_EV_USER, GSM_EV_CSQ };
typedef struct gsm gsm_t;
typedef void (*gsm_event_fn)(gsm_t *g, uint8_t ev, const char *s, uint16_t n);
typedef uint8_t (*gsm_call_fn)(gsm_t *g, const char *number); /* 1 — ответить, 0 — сбросить */
struct gsm {
/* кольцо приёма: head пишет прерывание, tail — gsm_poll */
uint8_t rx[GSM_RXQ];
volatile uint16_t rx_head;
uint16_t rx_tail, rx_hiwater;
volatile uint16_t rx_lost;
/* сборка строки и данных +IPD */
char line[GSM_LINE];
uint16_t len, ipd_left, ipd_len;
uint8_t ipd[GSM_DATA];
/* текущая команда */
char cmd[GSM_CMD];
char resp[GSM_RESP];
uint16_t resp_len;
const char *prefix; /* строки с этим началом — ответ команде, остальное — URC */
uint8_t busy, fin, result, have, want_prompt, take_next, ctrlz;
uint8_t out[GSM_DATA]; /* что отправить по приглашению «> » */
uint16_t out_len;
uint32_t now, t_sent, deadline, wait;
/* автомат модема */
uint8_t st, sub, job, js, tmo_row, probe;
uint8_t echo_on, rebooted; /* эхо включено (как у модема после старта); эхо при выключенном — модем перезагрузился */
uint32_t next_check;
int16_t rssi; /* из AT+CSQ, 0…31, 99 — неизвестно */
uint8_t creg; /* <stat> из AT+CREG? */
/* SMS */
uint8_t smsq[GSM_SMSQ], sms_n;
char sms_num[20];
uint8_t sms_text[GSM_DATA];
uint16_t sms_text_len;
uint8_t sms_pending, sms_ucs;
/* звонок */
uint8_t call, call_answer, call_pending, hang_req;
char call_num[20];
/* GPRS и TCP */
uint8_t gp, want_tcp, tx_pending;
uint32_t gp_wait, pause_ms, rnd;
uint8_t tx[GSM_DATA];
uint16_t tx_len;
char ip[16];
/* команда приложения (для стенда и отладки) */
char user[GSM_CMD];
const char *user_prefix;
uint8_t user_pending;
/* настройки */
const char *apn, *host;
uint16_t port;
gsm_event_fn on_event;
gsm_call_fn on_call;
/* статистика */
uint32_t n_cmd, n_tmo, n_echo, n_urc, n_prompt, n_reset, n_reboot, n_sms_drop;
};
/* платформа */
void gsm_hw_tx(const uint8_t *p, uint16_t n);
void gsm_hw_pin(uint8_t pin, uint8_t level);
void gsm_init(gsm_t *g, const char *apn, const char *host, uint16_t port, gsm_event_fn ev, gsm_call_fn call);
void gsm_rx_isr(gsm_t *g, uint8_t b);
void gsm_poll(gsm_t *g, uint32_t now_ms);
int gsm_sms_send(gsm_t *g, const char *number, const char *utf8);
void gsm_hangup(gsm_t *g);
void gsm_tcp_open(gsm_t *g);
int gsm_tcp_send(gsm_t *g, const uint8_t *p, uint16_t n);
int gsm_user(gsm_t *g, const char *cmd, const char *prefix);
int16_t gsm_csq_dbm(int16_t rssi);
uint16_t gsm_utf8_to_ucs2hex(const char *s, char *out, uint16_t cap);
uint16_t gsm_ucs2hex_to_utf8(const char *hex, char *out, uint16_t cap);
#endif
gsm106e.c — SIM800L driver, shared by the firmware and the bench.
/* gsm106e.c — раздел 106г: драйвер SIM800L. Ни одного ожидания в цикле: gsm_poll разбирает то, что уже
* пришло, проверяет таймаут текущей команды и, если модем свободен, делает следующий шаг автомата. */
#include <string.h>
#include "gsm106e.h"
#define PAUSE_MAX_MS 32000u
static const char *const ST_NAME[] = {"OFF", "PWRKEY", "SYNC", "SETUP", "SIM", "REG", "READY", "RESET"};
static const char *const SETUP[] = {"ATE0", "AT+CLIP=1", "AT+CMGF=1", "AT+CNMI=2,1,0,0,0", "AT+CIPHEAD=1"};
#define N_SETUP (sizeof SETUP / sizeof SETUP[0])
/* незапрошенные сообщения: всё, что модем может прислать без команды */
static const char *const URC[] = {"RING", "+CLIP:", "+CMTI:", "NO CARRIER", "BUSY", "NO ANSWER", "CLOSED",
"+PDP: DEACT", "RDY", "+CFUN:", "+CPIN:", "Call Ready", "SMS Ready", "+CREG:"};
/* ── мелочи без printf ────────────────────────────────────────────────── */
static int starts(const char *s, const char *p)
{
while (*p)
if (*s++ != *p++)
return 0;
return 1;
}
static char *cat(char *d, const char *s, const char *end)
{
while (*s && d < end - 1)
*d++ = *s++;
*d = 0;
return d;
}
static char *catu(char *d, uint32_t v, const char *end)
{
char t[10];
int n = 0;
do
t[n++] = (char)('0' + v % 10u);
while (v /= 10u);
while (n && d < end - 1)
*d++ = t[--n];
*d = 0;
return d;
}
static uint32_t atou(const char *s)
{
uint32_t v = 0;
while (*s == ' ')
s++;
while (*s >= '0' && *s <= '9')
v = v * 10u + (uint32_t)(*s++ - '0');
return v;
}
/* k-е поле в кавычках (с нуля) — в out */
static int quoted(const char *s, uint8_t k, char *out, uint16_t cap)
{
for (;;) {
s = strchr(s, '"');
if (!s)
return 0;
const char *e = strchr(s + 1, '"');
if (!e)
return 0;
if (k-- == 0) {
uint16_t n = (uint16_t)(e - s - 1);
if (n >= cap)
n = (uint16_t)(cap - 1u);
memcpy(out, s + 1, n);
out[n] = 0;
return 1;
}
s = e + 1;
}
}
static int is_urc(const char *s)
{
for (uint32_t i = 0; i < sizeof URC / sizeof URC[0]; i++)
if (starts(s, URC[i]))
return 1;
return 0;
}
static int is_ip(const char *s)
{
int dots = 0;
if (!*s)
return 0;
for (; *s; s++) {
if (*s == '.')
dots++;
else if (*s < '0' || *s > '9')
return 0;
}
return dots == 3;
}
static int after(uint32_t now, uint32_t t)
{
return (int32_t)(now - t) >= 0;
}
static void emit(gsm_t *g, uint8_t ev, const char *s, uint16_t n)
{
if (g->on_event)
g->on_event(g, ev, s, n);
}
/* ── кодировки и уровень сигнала ─────────────────────────────────────── */
int16_t gsm_csq_dbm(int16_t rssi)
{
return rssi >= 0 && rssi <= 31 ? (int16_t)(-113 + 2 * rssi) : 0; /* 99 — неизвестно: 0 */
}
uint16_t gsm_utf8_to_ucs2hex(const char *s, char *out, uint16_t cap)
{
static const char hx[] = "0123456789ABCDEF";
const uint8_t *p = (const uint8_t *)s;
uint16_t n = 0;
while (*p && n + 4u < cap) {
uint32_t c = *p++;
if (c >= 0xE0u && p[0] && p[1]) { /* три байта: U+0800…U+FFFF */
c = (c & 0x0Fu) << 12 | (uint32_t)(p[0] & 0x3Fu) << 6 | (p[1] & 0x3Fu);
p += 2;
} else if (c >= 0xC0u && p[0]) { /* два байта: кириллица U+0400…U+04FF */
c = (c & 0x1Fu) << 6 | (p[0] & 0x3Fu);
p += 1;
} else if (c >= 0x80u) {
c = '?'; /* четыре байта и обрывки — вне UCS2 */
}
for (int k = 12; k >= 0; k -= 4)
out[n++] = hx[(c >> k) & 15u];
}
out[n] = 0;
return n;
}
static int hexv(char c)
{
return c >= '0' && c <= '9' ? c - '0' : c >= 'A' && c <= 'F' ? c - 'A' + 10 : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1;
}
uint16_t gsm_ucs2hex_to_utf8(const char *h, char *out, uint16_t cap)
{
uint16_t n = 0;
while (h[0] && h[1] && h[2] && h[3]) {
uint32_t c = 0;
for (int k = 0; k < 4; k++) {
int v = hexv(h[k]);
if (v < 0)
goto end;
c = c << 4 | (uint32_t)v;
}
h += 4;
if (c < 0x80u && n + 1u < cap) {
out[n++] = (char)c;
} else if (c < 0x800u && n + 2u < cap) {
out[n++] = (char)(0xC0u | c >> 6);
out[n++] = (char)(0x80u | (c & 0x3Fu));
} else if (c >= 0x800u && n + 3u < cap) {
out[n++] = (char)(0xE0u | c >> 12);
out[n++] = (char)(0x80u | (c >> 6 & 0x3Fu));
out[n++] = (char)(0x80u | (c & 0x3Fu));
} else {
break;
}
}
end:
out[n] = 0;
return n;
}
/* ── команда: отправить и ждать конца ответа ─────────────────────────── */
static void cmd_ex(gsm_t *g, const char *c, const char *prefix, uint8_t fin, uint32_t tmo,
const uint8_t *data, uint16_t n, uint8_t ctrlz)
{
cat(g->cmd, c, g->cmd + GSM_CMD);
g->prefix = prefix;
g->fin = fin;
g->busy = 1;
g->have = 0;
g->result = GSM_RES_NONE;
g->resp_len = 0;
g->resp[0] = 0;
g->take_next = 0;
g->want_prompt = data != 0;
if (data) {
memcpy(g->out, data, n);
g->out_len = n;
g->ctrlz = ctrlz;
}
g->t_sent = g->now;
g->deadline = g->now + tmo;
g->n_cmd++;
gsm_hw_tx((const uint8_t *)g->cmd, (uint16_t)strlen(g->cmd));
gsm_hw_tx((const uint8_t *)"\r", 1);
}
static void cmd(gsm_t *g, const char *c, const char *prefix, uint8_t fin, uint32_t tmo)
{
cmd_ex(g, c, prefix, fin, tmo, 0, 0, 0);
}
static void finish(gsm_t *g, uint8_t res)
{
g->busy = 0;
g->want_prompt = 0;
g->take_next = 0;
g->result = res;
g->have = 1;
if (res == GSM_RES_TIMEOUT) {
g->n_tmo++;
if (g->st >= GSM_ST_SETUP)
g->tmo_row++;
if (g->st == GSM_ST_READY)
g->probe = 1; /* жив ли модем — спросить коротким AT */
} else {
g->tmo_row = 0;
if (res == GSM_RES_OK && (!strcmp(g->cmd, "ATE0") || !strcmp(g->cmd, "ATE1")))
g->echo_on = g->cmd[3] == '1';
}
}
static void resp_add(gsm_t *g, const char *s)
{
char *e = cat(g->resp + g->resp_len, s, g->resp + GSM_RESP - 1);
*e++ = '\n';
*e = 0;
g->resp_len = (uint16_t)(e - g->resp);
}
static uint8_t final(const gsm_t *g, const char *s)
{
if (!strcmp(s, "ERROR") || starts(s, "+CME ERROR:") || starts(s, "+CMS ERROR:"))
return GSM_RES_ERROR;
switch (g->fin) {
case GSM_FIN_SEND:
return !strcmp(s, "SEND OK") ? GSM_RES_OK : !strcmp(s, "SEND FAIL") ? GSM_RES_ERROR : GSM_RES_NONE;
case GSM_FIN_SHUT:
return !strcmp(s, "SHUT OK") ? GSM_RES_OK : GSM_RES_NONE;
case GSM_FIN_CONNECT: /* первое OK у CIPSTART — ещё не соединение */
if (!strcmp(s, "CONNECT OK") || !strcmp(s, "ALREADY CONNECT"))
return GSM_RES_OK;
return !strcmp(s, "CONNECT FAIL") ? GSM_RES_ERROR : GSM_RES_NONE;
case GSM_FIN_IP:
return is_ip(s) ? GSM_RES_OK : GSM_RES_NONE;
default:
return !strcmp(s, "OK") ? GSM_RES_OK : GSM_RES_NONE;
}
}
/* ── незапрошенные сообщения ─────────────────────────────────────────── */
static uint32_t next_pause(gsm_t *g)
{
uint32_t p = g->pause_ms;
g->rnd ^= g->rnd << 13;
g->rnd ^= g->rnd >> 17;
g->rnd ^= g->rnd << 5;
g->pause_ms = g->pause_ms * 2u > PAUSE_MAX_MS ? PAUSE_MAX_MS : g->pause_ms * 2u;
return p + g->rnd % (p / 4u + 1u);
}
static void tcp_down(gsm_t *g, const char *why)
{
if (g->gp == GSM_GP_UP)
emit(g, GSM_EV_TCP_DOWN, why, 0);
g->gp = GSM_GP_DOWN;
g->ipd_left = 0;
if (g->job != GSM_J_GPRS) /* идущее подключение само решит, когда повторить */
g->gp_wait = g->now + next_pause(g);
}
static void urc(gsm_t *g, const char *s)
{
if (!is_urc(s))
return; /* чужая строка (первое OK у CIPSTART и т. п.) */
g->n_urc++;
if (starts(s, "+CLIP:")) {
if (!g->call) { /* RING повторяется — решаем один раз за звонок */
quoted(s, 0, g->call_num, sizeof g->call_num);
g->call = 1;
g->call_answer = g->on_call ? g->on_call(g, g->call_num) : 0;
g->call_pending = 1;
emit(g, GSM_EV_CALL, g->call_num, g->call_answer);
}
} else if (starts(s, "+CMTI:")) {
const char *c = strchr(s, ',');
if (c && g->sms_n < GSM_SMSQ)
g->smsq[g->sms_n++] = (uint8_t)atou(c + 1);
else
g->n_sms_drop++;
} else if (!strcmp(s, "NO CARRIER") || !strcmp(s, "BUSY") || !strcmp(s, "NO ANSWER")) {
if (g->call) {
g->call = 0;
g->call_pending = 0;
emit(g, GSM_EV_CALL_END, s, 0);
}
} else if (!strcmp(s, "CLOSED") || starts(s, "+PDP: DEACT")) {
tcp_down(g, s);
}
}
static void on_line(gsm_t *g)
{
const char *s = g->line;
if (g->busy) {
if (!strcmp(s, g->cmd)) { /* эхо своей команды */
g->n_echo++;
if (!g->echo_on && g->st >= GSM_ST_SIM)
g->rebooted = 1; /* эхо вернулось само: модем перезапустился */
return;
}
uint8_t f = final(g, s);
if (f) {
if (g->fin == GSM_FIN_IP && f == GSM_RES_OK)
resp_add(g, s);
finish(g, f);
return;
}
if (g->prefix[0] && starts(s, g->prefix)) {
resp_add(g, s);
g->take_next = starts(s, "+CMGR:"); /* за заголовком SMS — строка текста */
return;
}
if (g->take_next && !is_urc(s)) {
g->take_next = 0;
resp_add(g, s);
return;
}
}
urc(g, s);
}
static void on_prompt(gsm_t *g)
{
g->want_prompt = 0;
g->n_prompt++;
gsm_hw_tx(g->out, g->out_len);
if (g->ctrlz) {
static const uint8_t z = 0x1Au; /* Ctrl+Z — конец текста SMS */
gsm_hw_tx(&z, 1);
}
}
static void on_byte(gsm_t *g, uint8_t b)
{
if (g->ipd_left) { /* данные TCP: длина известна, CR и LF — тоже данные */
g->ipd[g->ipd_len++] = b;
if (--g->ipd_left == 0 || g->ipd_len == GSM_DATA) {
emit(g, GSM_EV_TCP_DATA, (const char *)g->ipd, g->ipd_len);
g->ipd_len = 0;
}
return;
}
if (b == '\n')
return;
if (b == '\r') { /* конец строки; эхо кончается одним CR */
if (g->len) {
g->line[g->len] = 0;
on_line(g);
}
g->len = 0;
return;
}
if (g->len < GSM_LINE - 1u)
g->line[g->len++] = (char)b;
g->line[g->len] = 0;
if (g->want_prompt && g->len == 2 && g->line[0] == '>' && g->line[1] == ' ') {
g->len = 0; /* «> » без перевода строки */
on_prompt(g);
} else if (b == ':' && g->gp == GSM_GP_UP && g->len > 5 && starts(g->line, "+IPD,")) {
g->ipd_left = (uint16_t)atou(g->line + 5);
g->ipd_len = 0;
g->len = 0;
}
}
/* ── задания в состоянии READY ───────────────────────────────────────── */
static void set_st(gsm_t *g, uint8_t st)
{
emit(g, GSM_EV_STATE, ST_NAME[st], g->sub);
g->st = st;
g->sub = 0;
g->job = GSM_J_NONE;
g->js = 0;
}
static void done(gsm_t *g)
{
g->job = GSM_J_NONE;
g->js = 0;
}
static void parse_csq(gsm_t *g)
{
const char *c = strchr(g->resp, ':');
if (c) {
g->rssi = (int16_t)atou(c + 1);
emit(g, GSM_EV_CSQ, g->resp, (uint16_t)g->rssi);
}
}
/* +CREG: <n>,<stat>[,…] — ответ на AT+CREG?; у незапрошенного +CREG: только <stat> */
static void parse_creg(gsm_t *g)
{
const char *c = strchr(g->resp, ':');
if (!c)
return;
const char *k = strchr(c, ',');
g->creg = (uint8_t)atou(k ? k + 1 : c + 1);
}
static int registered(const gsm_t *g)
{
return g->creg == 1u || g->creg == 5u; /* 1 — домашняя сеть, 5 — роуминг */
}
static void gp_fail(gsm_t *g)
{
emit(g, GSM_EV_TCP_DOWN, g->cmd, g->result);
g->gp_wait = g->now + next_pause(g);
done(g);
}
static void pick(gsm_t *g)
{
g->js = 0;
if (g->probe)
g->job = GSM_J_PROBE;
else if (g->call_pending || g->hang_req)
g->job = GSM_J_CALL;
else if (g->sms_n)
g->job = GSM_J_SMSR;
else if (g->sms_pending)
g->job = GSM_J_SMSS;
else if (g->gp == GSM_GP_UP && g->tx_pending)
g->job = GSM_J_SEND;
else if (g->want_tcp && g->gp == GSM_GP_DOWN && after(g->now, g->gp_wait))
g->job = GSM_J_GPRS;
else if (g->user_pending)
g->job = GSM_J_USER;
else if (after(g->now, g->next_check))
g->job = GSM_J_CHECK;
}
static void job(gsm_t *g, uint8_t r)
{
char c[GSM_CMD], *e = c + GSM_CMD, *p;
switch (g->job) {
case GSM_J_PROBE:
if (g->js++ == 0) {
cmd(g, "AT", "", GSM_FIN_OK, 1000);
return;
}
if (r != GSM_RES_TIMEOUT)
g->probe = 0;
done(g);
return;
case GSM_J_CALL:
if (g->js == 0) {
uint8_t ans = g->call_answer && !g->hang_req;
g->js = ans ? 2 : 1;
cmd(g, ans ? "ATA" : "ATH", "", GSM_FIN_OK, ans ? 10000 : 5000);
return;
}
if (g->js == 1) { /* сброшен */
g->hang_req = 0;
g->call_pending = 0;
if (g->call) {
g->call = 0;
emit(g, GSM_EV_CALL_END, "ATH", r);
}
} else { /* отвечен */
g->call_pending = 0;
emit(g, GSM_EV_CALL, "ATA", r);
}
done(g);
return;
case GSM_J_SMSR:
if (g->js == 0) {
g->js = 1;
cmd(g, "AT+CSCS=\"UCS2\"", "", GSM_FIN_OK, 2000);
return;
}
if (g->js == 1) {
if (r != GSM_RES_OK) {
done(g);
return;
}
g->js = 2;
p = cat(c, "AT+CMGR=", e);
catu(p, g->smsq[0], e);
cmd(g, c, "+CMGR:", GSM_FIN_OK, 5000);
return;
}
if (g->js == 2) {
if (r == GSM_RES_TIMEOUT) {
done(g);
return;
}
if (r == GSM_RES_OK) { /* «+CMGR: "REC UNREAD","<номер>",…» и текст — всё в UCS2 */
char num[64], txt[GSM_DATA];
const char *body = strchr(g->resp, '\n');
uint16_t k = 0;
if (quoted(g->resp, 1, num, sizeof num))
k = gsm_ucs2hex_to_utf8(num, txt, 24);
txt[k++] = '\t';
if (body && body[1]) {
char *nl = strchr(body + 1, '\n');
if (nl)
*nl = 0;
k = (uint16_t)(k + gsm_ucs2hex_to_utf8(body + 1, txt + k, (uint16_t)(sizeof txt - k)));
}
txt[k] = 0;
emit(g, GSM_EV_SMS, txt, g->smsq[0]);
}
g->js = 3;
p = cat(c, "AT+CMGD=", e);
catu(p, g->smsq[0], e);
cmd(g, c, "", GSM_FIN_OK, 5000);
return;
}
if (r != GSM_RES_TIMEOUT) {
memmove(g->smsq, g->smsq + 1, --g->sms_n);
}
done(g);
return;
case GSM_J_SMSS:
if (g->js == 0) {
g->js = 1;
cmd(g, g->sms_ucs ? "AT+CSCS=\"UCS2\"" : "AT+CSCS=\"GSM\"", "", GSM_FIN_OK, 2000);
return;
}
if (g->js == 1 || g->js == 2) {
if (r != GSM_RES_OK) {
if (r == GSM_RES_ERROR) {
g->sms_pending = 0;
emit(g, GSM_EV_SMS_SENT, g->cmd, r);
}
done(g);
return;
}
if (g->js++ == 1) {
cmd(g, g->sms_ucs ? "AT+CSMP=17,167,0,8" : "AT+CSMP=17,167,0,0", "", GSM_FIN_OK, 2000);
return;
}
p = cat(c, "AT+CMGS=\"", e); /* в режиме UCS2 и номер — в UCS2 */
if (g->sms_ucs)
p += gsm_utf8_to_ucs2hex(g->sms_num, p, (uint16_t)(e - p - 1));
else
p = cat(p, g->sms_num, e);
cat(p, "\"", e);
cmd_ex(g, c, "+CMGS:", GSM_FIN_OK, 30000, g->sms_text, g->sms_text_len, 1);
return;
}
if (r != GSM_RES_TIMEOUT) { /* таймаут — повторить позже, остальное — итог */
g->sms_pending = 0;
emit(g, GSM_EV_SMS_SENT, g->resp, r);
}
done(g);
return;
case GSM_J_USER:
if (g->js++ == 0) {
cmd(g, g->user, g->user_prefix, GSM_FIN_OK, 2000);
return;
}
g->user_pending = 0;
emit(g, GSM_EV_USER, g->resp, r);
done(g);
return;
case GSM_J_CHECK:
if (g->js == 0) {
g->js = 1;
cmd(g, "AT+CSQ", "+CSQ:", GSM_FIN_OK, 2000);
return;
}
if (g->js == 1) {
if (r == GSM_RES_OK)
parse_csq(g);
g->js = 2;
cmd(g, "AT+CREG?", "+CREG:", GSM_FIN_OK, 2000);
return;
}
g->next_check = g->now + 20000u;
if (r == GSM_RES_OK) {
parse_creg(g);
if (!registered(g)) {
tcp_down(g, "CREG");
set_st(g, GSM_ST_REG);
return;
}
}
done(g);
return;
case GSM_J_GPRS:
if (g->js > 0 && r != GSM_RES_OK) {
gp_fail(g);
return;
}
switch (g->js++) {
case 0:
cmd(g, "AT+CIPSHUT", "", GSM_FIN_SHUT, 10000);
return;
case 1:
p = cat(c, "AT+CSTT=\"", e);
p = cat(p, g->apn, e);
cat(p, "\"", e);
cmd(g, c, "", GSM_FIN_OK, 5000);
return;
case 2:
cmd(g, "AT+CIICR", "", GSM_FIN_OK, 30000);
return;
case 3:
cmd(g, "AT+CIFSR", "", GSM_FIN_IP, 5000);
return;
case 4:
cat(g->ip, g->resp, g->ip + sizeof g->ip);
if ((p = strchr(g->ip, '\n')) != 0)
*p = 0;
p = cat(c, "AT+CIPSTART=\"TCP\",\"", e);
p = cat(p, g->host, e);
p = cat(p, "\",", e);
catu(p, g->port, e);
cmd(g, c, "", GSM_FIN_CONNECT, 30000);
return;
default:
g->gp = GSM_GP_UP;
g->pause_ms = 1000u;
emit(g, GSM_EV_TCP_UP, g->ip, 0);
done(g);
return;
}
case GSM_J_SEND:
if (g->js++ == 0) {
p = cat(c, "AT+CIPSEND=", e);
catu(p, g->tx_len, e);
cmd_ex(g, c, "", GSM_FIN_SEND, 10000, g->tx, g->tx_len, 0);
return;
}
if (r == GSM_RES_OK) {
g->tx_pending = 0;
emit(g, GSM_EV_TCP_SENT, g->cmd, g->tx_len);
} else {
tcp_down(g, g->cmd); /* пакет остаётся: уйдёт после переподключения */
}
done(g);
return;
default:
done(g);
return;
}
}
static void reset_all(gsm_t *g)
{
tcp_down(g, "RESET");
g->busy = g->want_prompt = g->take_next = g->have = 0;
g->ipd_left = g->len = 0;
g->call = g->call_pending = g->hang_req = g->probe = 0;
g->tmo_row = 0;
g->echo_on = 1;
g->rebooted = 0;
}
/* ── автомат модема ──────────────────────────────────────────────────── */
static void machine(gsm_t *g)
{
uint8_t r = g->have ? g->result : GSM_RES_NONE;
g->have = 0;
if (g->tmo_row >= GSM_TMO_MAX) { /* модем молчит: сброс выводом RST */
g->n_reset++;
emit(g, GSM_EV_RESET, g->cmd, (uint16_t)g->n_reset);
reset_all(g);
set_st(g, GSM_ST_RESET);
r = GSM_RES_NONE;
} else if (g->rebooted) { /* модем жив, но забыл настройки: заново с ATE0 */
g->n_reboot++;
emit(g, GSM_EV_RESET, "ECHO", (uint16_t)g->n_reboot);
reset_all(g);
set_st(g, GSM_ST_SETUP);
r = GSM_RES_NONE;
}
if (!after(g->now, g->wait))
return;
switch (g->st) {
case GSM_ST_OFF: /* включение: PWRKEY к земле */
gsm_hw_pin(GSM_PIN_PWRKEY, 0);
g->wait = g->now + 1200u;
g->st = GSM_ST_PWRKEY;
break;
case GSM_ST_PWRKEY:
gsm_hw_pin(GSM_PIN_PWRKEY, 1);
g->wait = g->now + 3000u;
set_st(g, GSM_ST_SYNC);
break;
case GSM_ST_RESET:
if (g->sub++ == 0) {
gsm_hw_pin(GSM_PIN_RST, 0);
g->wait = g->now + 200u;
} else {
gsm_hw_pin(GSM_PIN_RST, 1);
g->wait = g->now + 3000u;
set_st(g, GSM_ST_SYNC);
}
break;
case GSM_ST_SYNC: /* автоскорость: AT, пока не ответит */
if (r == GSM_RES_OK || r == GSM_RES_ERROR) {
set_st(g, GSM_ST_SETUP);
break;
}
if (g->sub >= 40u) {
g->n_reset++;
emit(g, GSM_EV_RESET, "SYNC", (uint16_t)g->n_reset);
set_st(g, GSM_ST_RESET);
break;
}
g->sub++;
cmd(g, "AT", "", GSM_FIN_OK, 300);
break;
case GSM_ST_SETUP:
if (r == GSM_RES_OK) {
g->sub++;
} else if (r != GSM_RES_NONE) {
g->wait = g->now + 500u;
break;
}
if (g->sub >= N_SETUP) {
set_st(g, GSM_ST_SIM);
break;
}
cmd(g, SETUP[g->sub], "", GSM_FIN_OK, 1000);
break;
case GSM_ST_SIM:
if (r == GSM_RES_OK && strstr(g->resp, "READY")) {
set_st(g, GSM_ST_REG);
break;
}
if (r != GSM_RES_NONE) {
g->sub++;
g->wait = g->now + 1000u;
break;
}
cmd(g, "AT+CPIN?", "+CPIN:", GSM_FIN_OK, 2000);
break;
case GSM_ST_REG:
if (r != GSM_RES_NONE) {
if (g->js == 0) {
if (r == GSM_RES_OK)
parse_csq(g);
g->js = 1;
} else {
g->js = 0;
g->sub++;
if (r == GSM_RES_OK)
parse_creg(g);
if (registered(g)) {
g->next_check = g->now + 20000u;
set_st(g, GSM_ST_READY);
break;
}
g->wait = g->now + 1000u;
break;
}
}
if (g->js == 0)
cmd(g, "AT+CSQ", "+CSQ:", GSM_FIN_OK, 2000);
else
cmd(g, "AT+CREG?", "+CREG:", GSM_FIN_OK, 2000);
break;
case GSM_ST_READY:
if (g->job == GSM_J_NONE) {
pick(g);
r = GSM_RES_NONE;
}
if (g->job != GSM_J_NONE)
job(g, r);
break;
default:
break;
}
}
/* ── то, что зовёт платформа и приложение ────────────────────────────── */
void gsm_init(gsm_t *g, const char *apn, const char *host, uint16_t port, gsm_event_fn ev, gsm_call_fn call)
{
memset(g, 0, sizeof *g);
g->apn = apn;
g->host = host;
g->port = port;
g->on_event = ev;
g->on_call = call;
g->rssi = 99;
g->pause_ms = 1000u;
g->rnd = 0x2545F491u;
g->prefix = "";
g->echo_on = 1;
g->st = GSM_ST_OFF;
}
void gsm_rx_isr(gsm_t *g, uint8_t b)
{
uint16_t h = g->rx_head, n = (uint16_t)((h + 1u) % GSM_RXQ);
if (n == g->rx_tail) { /* кольцо полно: байт теряется, счёт — для отладки */
g->rx_lost++;
return;
}
g->rx[h] = b;
g->rx_head = n;
}
void gsm_poll(gsm_t *g, uint32_t now_ms)
{
g->now = now_ms;
uint16_t h = g->rx_head;
uint16_t used = (uint16_t)((h + GSM_RXQ - g->rx_tail) % GSM_RXQ);
if (used > g->rx_hiwater)
g->rx_hiwater = used;
while (g->rx_tail != h) {
uint8_t b = g->rx[g->rx_tail];
g->rx_tail = (uint16_t)((g->rx_tail + 1u) % GSM_RXQ);
on_byte(g, b);
}
if (g->busy && after(now_ms, g->deadline))
finish(g, GSM_RES_TIMEOUT);
if (!g->busy)
machine(g);
}
int gsm_sms_send(gsm_t *g, const char *number, const char *utf8)
{
if (g->sms_pending)
return -1;
const uint8_t *p = (const uint8_t *)utf8;
g->sms_ucs = 0;
while (*p)
if (*p++ >= 0x80u)
g->sms_ucs = 1; /* не ASCII — текст в UCS2 */
cat(g->sms_num, number, g->sms_num + sizeof g->sms_num);
if (g->sms_ucs)
g->sms_text_len = gsm_utf8_to_ucs2hex(utf8, (char *)g->sms_text, GSM_DATA);
else
g->sms_text_len = (uint16_t)(cat((char *)g->sms_text, utf8, (char *)g->sms_text + GSM_DATA)
- (char *)g->sms_text);
g->sms_pending = 1;
return 0;
}
void gsm_hangup(gsm_t *g)
{
g->hang_req = 1;
}
void gsm_tcp_open(gsm_t *g)
{
g->want_tcp = 1;
}
int gsm_tcp_send(gsm_t *g, const uint8_t *p, uint16_t n)
{
if (g->tx_pending || n == 0 || n > GSM_DATA)
return -1;
memcpy(g->tx, p, n);
g->tx_len = n;
g->tx_pending = 1;
return 0;
}
int gsm_user(gsm_t *g, const char *c, const char *prefix)
{
if (g->user_pending)
return -1;
cat(g->user, c, g->user + GSM_CMD);
g->user_prefix = prefix;
g->user_pending = 1;
return 0;
}
fw106e.c — STM32F103 firmware: USART1 with interrupts, PWRKEY and RST, the main loop.
/* fw106e.c — раздел 106г: модем SIM800L на STM32F103 (BluePill), 72 МГц.
* USART1: PA9 — TX к RXD модема, PA10 — RX от TXD модема (вывод с допуском 5 В), 115 200 бод, 8N1.
* Приём — прерывание RXNE кладёт байт в кольцо драйвера; передача — своё кольцо и прерывание TXE.
* PB0 — PWRKEY, PB1 — RST модема (через ключ — уровни инвертировать в gsm_hw_pin). PC13 — светодиод.
* SysTick раз в 1 мс — только счёт времени; gsm_poll — в главном цикле, ничто не ждёт ответа модема.
* Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется); USART1 на APB2 = 72 МГц. */
#include <string.h>
#include "stm32f10x.h"
#include "gsm106e.h"
#define CPU_HZ 72000000u
#define BAUD 115200u
#define TXQ 512u
#define OWN "+70000000001" /* условный номер владельца */
_Static_assert(CPU_HZ / BAUD == 625u, "USART1: BRR = 625 — 115 200 бод");
static gsm_t G;
static volatile uint32_t ms;
static uint8_t txq[TXQ];
static volatile uint16_t tx_head, tx_tail;
static uint32_t call_at, last_tm;
void SysTick_Handler(void)
{
ms++;
}
void USART1_IRQHandler(void)
{
uint32_t sr = USART1->SR;
if (sr & (USART_SR_RXNE | USART_SR_ORE)) /* чтение DR снимает и RXNE, и ORE */
gsm_rx_isr(&G, (uint8_t)USART1->DR);
if ((USART1->CR1 & USART_CR1_TXEIE) && (sr & USART_SR_TXE)) {
if (tx_tail != tx_head) {
USART1->DR = txq[tx_tail];
tx_tail = (uint16_t)((tx_tail + 1u) % TXQ);
} else {
USART1->CR1 &= ~USART_CR1_TXEIE; /* очередь пуста */
}
}
}
/* ── платформа драйвера ───────────────────────────────────────────────── */
void gsm_hw_tx(const uint8_t *p, uint16_t n)
{
for (uint16_t i = 0; i < n; i++) {
uint16_t nx = (uint16_t)((tx_head + 1u) % TXQ);
if (nx == tx_tail)
break; /* очередь полна — драйвер больше 512 байт за раз не шлёт */
txq[tx_head] = p[i];
tx_head = nx;
}
USART1->CR1 |= USART_CR1_TXEIE;
}
void gsm_hw_pin(uint8_t pin, uint8_t level)
{
uint32_t bit = pin == GSM_PIN_PWRKEY ? 1u << 0 : 1u << 1;
GPIOB->BSRR = level ? bit : bit << 16;
}
/* ── приложение ───────────────────────────────────────────────────────── */
static void on_event(gsm_t *g, uint8_t ev, const char *s, uint16_t n)
{
if (ev == GSM_EV_STATE && !strcmp(s, "READY")) {
gsm_sms_send(g, OWN, "Прибор на связи");
gsm_tcp_open(g);
} else if (ev == GSM_EV_SMS && !strncmp(s, OWN "\t", sizeof OWN)) {
gsm_sms_send(g, OWN, g->creg == 1u ? "Сеть: домашняя" : "Сеть: роуминг");
} else if (ev == GSM_EV_CALL && !strcmp(s, "ATA") && n == GSM_RES_OK) {
call_at = ms; /* отвечен: через 5 с положить трубку */
} else if (ev == GSM_EV_TCP_DATA) {
GPIOC->ODR ^= 1u << 13; /* ответ сервера — мигнуть */
}
}
static uint8_t on_call(gsm_t *g, const char *number)
{
(void)g;
return strcmp(number, OWN) == 0;
}
static void board_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_USART1EN;
GPIOB->BSRR = (1u << 0) | (1u << 1); /* PWRKEY и RST отпущены (высокий уровень) */
GPIOB->CRL = (GPIOB->CRL & ~0xFFu) | 0x22u; /* PB0, PB1 — выходы 2 МГц */
GPIOA->CRH = (GPIOA->CRH & ~0xFF0u)
| 0xBu << 4 /* PA9 — USART1_TX, альтернативная функция */
| 0x4u << 8; /* PA10 — USART1_RX, вход без подтяжки */
GPIOC->CRH = (GPIOC->CRH & ~(0xFu << 20)) | 0x2u << 20; /* PC13 — светодиод платы */
USART1->BRR = CPU_HZ / BAUD;
USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE | USART_CR1_RXNEIE;
NVIC_EnableIRQ(USART1_IRQn);
SysTick_Config(CPU_HZ / 1000u);
}
int main(void)
{
board_init();
gsm_init(&G, "internet", "192.0.2.1", 7, on_event, on_call);
for (;;) {
uint32_t now = ms;
gsm_poll(&G, now);
if (call_at && now - call_at >= 5000u) {
call_at = 0;
gsm_hangup(&G);
}
if (G.gp == GSM_GP_UP && now - last_tm >= 60000u) { /* раз в минуту — телеметрия */
static const char msg[] = "t=21.5\n";
if (gsm_tcp_send(&G, (const uint8_t *)msg, sizeof msg - 1u) == 0)
last_tm = now;
}
__WFI(); /* до следующего прерывания: байта или тика */
}
}
Next: section 107 "What happens when you type an address: DNS, DHCP, HTTP, TLS" — what the stack does on top of these frames.