Embedded HandbookApps
All sections › XII. Networks and DevOps for the engineer
STM32F103 · level intermediate · 50 min read
Open in the interactive handbook

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 mode SMS text mode, PDU mode
TE character set (+CSCS), UCS2 character 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.

Left — the STM32F103 and the SIM800L module: PA9 TX to the modem RXD, PA10 RX with 5 V tolerance from TXD, PB0 to PWRKEY, PB1 to RST, common ground, UART 115 200 baud 8N1 with crossed lines; the module runs from its own source, which must withstand sags during transmission, an antenna is connected to the module; the F103 "1" threshold is 0.65·VDD, the modem levels and current are in the SIMCom document, which the project does not have. Right — the path of a byte: the RXNE interrupt puts it into a 512-byte ring, gsm_poll assembles a line — CR ends it, LF is skipped, the "> " prompt and the +IPD,n: header are recognised at once — then decides whose line it is: the echo of our command is dropped, the final OK, ERROR, SEND OK ends the command, a line starting like the current command reply goes into the reply, the rest are unsolicited messages +CMTI, RING, +CLIP, CLOSED, even in the middle of a reply. Bottom — the modem state machine: OFF, PWRKEY at ground for 1.2 s, SYNC — AT every 0.3 s, SETUP — ATE0 and settings, SIM — AT+CPIN?, REG — CSQ and CREG? every 1 s, READY — jobs; 3 timeouts in a row lead to RESET — RST at ground for 0.2 s and SYNC again; echo while echo is off means the modem restarted — back to SETUP; in READY the network is polled every 20 s, after a timeout — a short AT


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

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.

Open in the interactive handbook