Embedded HandbookПриложения
Все разделы › XII. Сети и DevOps для инженера
STM32F103 · уровень средний · 50 мин чтения
Открыть в интерактивном справочнике

106г. GSM-модем SIM800L по UART: AT-команды, SMS, звонок и TCP через GPRS — разбор ответов без блокировки

В 106а–106в прибор выходил в сеть проводом: Ethernet, свой стек, lwIP. Там, где провода нет, а Wi-Fi не дотягивается, — на даче, в будке насоса, в машине — остаётся сотовая сеть. Самый дешёвый путь к ней — модуль SIM800L: модем GSM с GPRS, которым управляют текстовыми AT-командами по UART. В 37.1 про такие модули сказано: просто на вид и коварно в реализации. Здесь — драйвер, который это коварство переживает.

Что внутри: чем питать модуль и как согласовать уровни; первые команды и перевод уровня сигнала в дБм; драйвер, который никогда не ждёт в цикле, — кольцо приёма, сборка строк, отделение ответа на команду от незапрошенных сообщений; SMS латиницей и кириллицей; входящий звонок; GPRS и TCP с переподключением; сброс модема и сторож, который замечает, что модем замолчал или перезапустился.

Опоры: DocID13587 Rev 17 (STM32F103x8/xB): PA9, PA10 — USART1_TX, USART1_RX с допуском 5 В, с. 31; порог высокого уровня входа 0,65·VDD, таблица 35, с. 62. Пак Keil FreeRTOS Cellular Interface 1.1.0 (код FreeRTOS 1.4.0, файлы cellular_common.c, cellular_types.h, cellular_3gpp_api.c): перевод +CSQ в дБм, значения <stat> регистрации и разница между ответом на AT+CREG? и незапрошенным +CREG:. Драйвер ESP8266 из пака CMSIS-Driver 2.10.0 (ESP8266.c) — приглашение > разбирается отдельно от строк. Документов SIMCom на SIM800L и стандартов 3GPP в проекте нет: набор команд, вид ответов, тайминги выводов и числа питания взяты из памяти о них и помечены как непроверенные. Всё, что в разделе названо прогоном, — код _verify/gsm106e/gsm106e.c на компьютере (gcc 12.2.0) с моделью модема на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3. Номера телефонов, точка доступа и адреса — условные.

Как это называется в документации

Оригинал Перевод
AT command, final result code (OK, ERROR) AT-команда, итоговый ответ
information response строка с данными перед итогом
unsolicited result code (URC) незапрошенное сообщение модема
command echo (ATE0, ATE1) эхо: модем повторяет принятые символы
autobauding автоматическое определение скорости
received signal strength indication (+CSQ) уровень принимаемого сигнала
network registration status (+CREG) состояние регистрации в сети
SMS text mode (+CMGF=1), PDU mode текстовый режим SMS, режим PDU
TE character set (+CSCS), UCS2 кодировка строк обмена, UCS2 — 16 бит на знак
data coding scheme (DCS) схема кодирования текста SMS
calling line identification presentation (+CLIP) показ номера звонящего
PDP context, APN сеанс передачи данных, имя точки доступа
power key (PWRKEY) вывод включения модема

106г.1 Модуль SIM800L: питание, уровни, антенна, сеть

Питание — главное. Модем передаёт пачками, и в каждой пачке ток скачет во много раз против среднего. Чаще всего называют диапазон 3,4–4,4 В и импульсы тока до 2 А во время передачи — это по памяти о документе SIMCom, которого в проекте нет, числа не проверены. Вывод один: от линии 3,3 В платы и от USB-порта модуль не живёт. Ему нужен свой источник, способный отдать импульс, — литиевый аккумулятор или понижающий преобразователь (58) с запасом по току, — короткие толстые провода и конденсатор большой ёмкости у самого модуля. Иначе в момент передачи напряжение проседает, и модем перезапускается — молча, в середине команды (106г.6).

Уровни. Модем говорит по UART с уровнями своего внутреннего питания, и их надо сверить с STM32 в обе стороны (16.7). Вход приёма F103 — PA10: у него допуск 5 В (DocID13587, с. 31), а «единицу» он гарантированно видит от 0,65·VDD (таблица 35, с. 62) — при 3,3 В это 2,145 В. Высокий уровень выхода модема надо сравнить с этим порогом по документу модема. В другую сторону — выход PA9 на 3,3 В в вход модема: какой уровень тот выдерживает, тоже говорит документ SIMCom; на многих платах-модулях для этого стоит делитель или транзисторный каскад. Земля — общая, TX одного — на RX другого.

Выводы управления. PWRKEY включает и выключает модем, если подержать его на земле; RST перезапускает. У многих маленьких модулей вывода PWRKEY нет — модем включается сам при подаче питания (по памяти, не проверено), и тогда для сброса остаётся RST. Драйвер этого раздела умеет оба: импульс на PWRKEY при старте, RST — когда модем перестал отвечать. Длительности импульсов в драйвере (1,2 с и 0,2 с) выбраны с запасом; что требует документ SIMCom — не проверено.

Антенна и сеть. Без антенны модем регистрируется плохо или не регистрируется вовсе, а передача без нагрузки вредна передатчику. SIM800L работает только в сетях 2G (GSM и GPRS). В ряде стран операторы сети 2G отключают или уже отключили — перед тем как закладывать модуль в изделие, проверьте, будет ли 2G там, где прибор встанет, и на какой срок; иначе нужен модем LTE (у них похожие AT-команды, но другие тонкости).

Схема подключения и устройство драйвера — на рисунке.

Слева — STM32F103 и модуль SIM800L: PA9 TX к RXD модема, PA10 RX с допуском 5 В от TXD, PB0 на PWRKEY, PB1 на RST, общая земля, UART 115 200 бод 8N1 с перекрещенными линиями; модуль питается от своего источника, который должен выдерживать провалы при передаче, к модулю подключена антенна; порог «1» у F103 — 0,65·VDD, уровни и ток модема — по документу SIMCom, которого в проекте нет. Справа — путь байта: прерывание RXNE кладёт его в кольцо на 512 байт, gsm_poll собирает строку — CR конец, LF пропуск, приглашение «> » и заголовок +IPD,n: распознаются сразу, — потом решает, чья строка: эхо своей команды отбрасывается, итог OK, ERROR, SEND OK завершает команду, строка с началом текущей команды идёт в ответ, остальное — незапрошенные сообщения +CMTI, RING, +CLIP, CLOSED, даже посреди ответа. Внизу — автомат модема: OFF, PWRKEY на земле 1,2 с, SYNC — AT раз в 0,3 с, SETUP — ATE0 и настройки, SIM — AT+CPIN?, REG — CSQ и CREG? раз в 1 с, READY — задания; 3 таймаута подряд ведут в RESET — RST на земле 0,2 с и снова SYNC; эхо при выключенном эхо значит, что модем перезапустился, — назад в SETUP; в READY опрос сети раз в 20 с, после таймаута — короткий AT


106г.2 AT-команды: эхо, итог, первые вопросы модему

Команда — строка AT…, оконченная символом CR. Ответ — ноль или несколько строк с данными и итог: OK, ERROR или подробная ошибка +CME ERROR: n (у SMS — +CMS ERROR: n). Каждая строка ответа обрамлена парой CR LF. По умолчанию модем ещё и повторяет всё, что принял, — эхо, и эхо кончается одним CR, без LF. Поэтому первая настройка — ATE0.

Первые вопросы, без которых дальше идти нельзя:

Команда Ответ в прогоне Что значит
AT OK модем жив и понял скорость
AT+CPIN? +CPIN: READY SIM-карта готова
AT+CSQ +CSQ: 20,0 уровень сигнала (0…31, 99 — неизвестно) и ошибки
AT+CREG? +CREG: 0,1 второе число — регистрация: 1 — дома, 5 — роуминг

Уровень сигнала в дБм. Код FreeRTOS из пака Keil переводит первое число +CSQ так: −113 + 2 × rssi для 0…31, а 99 — «неизвестно» (cellular_common.c). Тот же перевод в драйвере:

int16_t gsm_csq_dbm(int16_t rssi)
{
    return rssi >= 0 && rssi <= 31 ? (int16_t)(-113 + 2 * rssi) : 0;   /* 99 — неизвестно: 0 */
}

Прогон сверяет его с формулой:

rssi 0 1 2 10 15 20 25 30 31 99
дБм −113 −111 −109 −93 −83 −73 −63 −53 −51 неизвестно (0)

Шаг — 2 дБ, весь размах — от −113 до −51 дБм. В том же коде FreeRTOS сигнал GSM переводится в «палочки» уровня по порогам −104, −98, −89 и −80 дБм — для индикатора на приборе это готовая шкала.

Регистрация. У <stat> значения такие (cellular_types.h): 0 — не ищет, 1 — зарегистрирован в домашней сети, 2 — ищет, 3 — отказано, 4 — неизвестно, 5 — роуминг. Работать можно при 1 и 5. Тонкость из комментария в cellular_3gpp_api.c: в ответе на AT+CREG? первое число — режим, второе — состояние (+CREG: 2,1), а в незапрошенном +CREG:, который модем шлёт сам после AT+CREG=1, первое число — уже состояние (+CREG: 1). Разбор, который берёт «второе число», на незапрошенной строке ошибётся. Драйвер незапрошенных +CREG: не включает и спрашивает сам.

Автоскорость. По памяти о документе SIMCom, с завода SIM800L сам подбирает скорость по первым символам AT и, пока не подобрал, молчит. Отсюда правило: слать AT несколько раз с короткой паузой, пока не ответит. В модели автоскорости нужно увидеть AT дважды: первый AT теряется, и в прогоне модем ответил со второй попытки. Драйвер шлёт AT раз в 0,3 с до 40 раз, потом сбрасывает модем.

Старт в прогоне. PWRKEY на земле 1200 мс, ещё 3000 мс модель загружается, со второго AT модем ответил — драйвер перешёл к настройке на 4532 мс. Дальше ATE0, AT+CLIP=1, AT+CMGF=1, AT+CNMI=2,1,0,0,0, AT+CIPHEAD=1; +CPIN: READY — с третьей попытки (дважды +CME ERROR: 14: в модели SIM готова через 1,5 с после загрузки), и 11 опросов AT+CREG? раз в секунду, пока модель не «нашла сеть» (через 11 821 мс после загрузки, время случайное). Готов — на 17 008 мс. Первый +CSQ дал 99 — сети ещё нет, последний перед готовностью — 20, то есть −73 дБм.


106г.3 Драйвер без блокировки: кольцо, строки, ответ и URC

Ответ модема идёт долго по меркам процессора: на 115 200 бод байт — 86,8 мкс, а модем думает над командой от миллисекунд до десятков секунд. Ждать в цикле нельзя. Драйвер устроен в три слоя, и ни один не ждёт.

Кольцо. Прерывание RXNE кладёт байт в кольцо на 512 байт — и всё (так же, как кольцо DMA из 16б, только по байту). Главный цикл зовёт gsm_poll(&G, ms): она забирает всё, что пришло, проверяет таймаут текущей команды и делает шаг автомата. В прогоне gsm_poll звалась раз в 1 мс, и в кольце не лежало больше 12 байт; потерь нет.

Строки. Байт за байтом собирается строка. Конец — CR; LF пропускается; пустые строки отбрасываются. Так одинаково режутся и ответы CR LF текст CR LF, и эхо текст CR. Две вещи узнаются, не дожидаясь конца строки: приглашение > и заголовок данных TCP +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;
    }
}

Чья строка. Это главное место драйвера. Пока команда в работе, строка проверяется по порядку: эхо своей команды — выбросить; итог — закончить команду; строка, начинающаяся так же, как ответ текущей команды (+CSQ: для AT+CSQ), — в ответ; всё остальное — незапрошенное сообщение, даже если оно пришло посреди ответа:

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);
}

Итог тоже зависит от команды: для обычных — OK; для AT+CIPSEND — SEND OK; для AT+CIPSHUT — SHUT OK; для AT+CIPSTART первое OK ещё не соединение, ждать надо CONNECT OK; а у AT+CIFSR итога нет вовсе — ответ кончается строкой адреса.

Одна команда за раз. Команда отправляется, запоминается срок, и до итога или срока новых команд нет. Автомат (рисунок) делает по шагу на вызов: включение, AT до ответа, настройка, SIM, регистрация, а в состоянии READY — задания по старшинству: звонок, чтение SMS из очереди +CMTI, своя SMS, пакет TCP, подключение GPRS, команда приложения, и раз в 20 с — опрос AT+CSQ и AT+CREG?.

Проверка под нагрузкой. Модель отвечала с задержкой 2…30 мс, выдерживала до 5 мс между строками ответа и с вероятностью 0,1 вставляла незапрошенное сообщение перед каждой строкой; кроме того, раз в 300…1000 мс приходили +CMTI (новая SMS) или RING с +CLIP (звонок с чужого номера). Приложение задавало AT+CCLK? (ответ у каждой команды свой — модель вписывает в него миллисекунды), и ответ сверялся с тем, что модель отправила. Для сравнения стенд разбирал те же байты «наивно»: ответ — первая непустая строка после команды.

эхо выключено эхо включено
команд 1000 1000
URC посреди ответа 279 351
пришло +CMTI / прочитано и удалено SMS 217 / 217 296 / 296
звонков с чужого номера сброшено 121 124
драйвер: верных ответов 1000 1000
«наивный» разбор: верных ответов 885 0
из них ошибок: URC вместо ответа / эхо вместо ответа 114 / 0 5 / 994

С эхом «наивный» разбор не попадает никогда: первая строка — повтор команды. Без эха ошибается на каждой восьмой-девятой команде — там, где перед ответом вклинилось RING или +CMTI.


106г.4 SMS и звонок

SMS латиницей. Текстовый режим (AT+CMGF=1), кодировка GSM, схема кодирования 0. Команда AT+CMGS="номер", в ответ — > без перевода строки: модем ждёт текст. Текст кончается символом Ctrl+Z (0x1A); Esc (0x1B) отменяет. Потом +CMGS: n — номер сообщения — и OK. Строковый разбор, ждущий CR LF после >, не дождётся никогда: модем молчит, пока не получит текст. Поэтому > узнаётся по двум символам (106г.3), а отправка текста — сразу в момент приглашения:

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);
    }
}

В прогоне от последнего байта приглашения до первого байта текста прошло 0,7 мс — до ближайшего вызова gsm_poll. Вся отправка Test 1 — три команды (AT+CSCS="GSM", AT+CSMP=17,167,0,0, AT+CMGS) и 2772 мс, из которых почти всё — модельная «сеть» (1,5…3 с на отправку).

Кириллица — UCS2. В кодировке GSM кириллицы нет, поэтому текст пишется в UCS2: каждый знак — четыре шестнадцатеричные цифры своего номера в Юникоде. AT+CSCS="UCS2" переводит в UCS2 все строки обмена, AT+CSMP=17,167,0,8 — схема кодирования 8 (параметры AT+CSMP — по памяти о 3GPP TS 27.005, не проверено). Драйвер переводит UTF-8 в UCS2 сам; прогон сверяет с кодировщиком Python:

Текст Знаков Байт UTF-8 UCS2, шестнадцатеричных цифр
Привет, мир! 12 21 48: 041F04400438043204350442002C0020043C043804400021
Темп. 21,5 °C 13 18 52
SIM800L 7 7 28

Ловушка: в режиме UCS2 и номер в AT+CMGS пишется в UCS2 — +70000000000 превращается в 48 цифр 002B0037…0030. Номер латиницей модель отвергает (+CMS ERROR), как, по памяти, и настоящий модем. Вот кусок драйвера, который собирает команду:

            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);

Отправка Привет, мир! — 2661 мс, текст — 48 байт после >. Буфер текста в драйвере — 280 байт, ровно 70 знаков UCS2: столько, по памяти о 3GPP TS 23.038, входит в одно сообщение (латиницей в 7-битной кодировке — 160); длинный текст делят на части — здесь этого нет.

Приём SMS. AT+CNMI=2,1,0,0,0 велит модему сообщать о новой SMS строкой +CMTI: "SM",n — n — номер ячейки на SIM. Драйвер кладёт номер в очередь, а в READY читает: AT+CSCS="UCS2", AT+CMGR=n, AT+CMGD=n. Ответ AT+CMGR — две строки: заголовок +CMGR: "REC UNREAD","<номер>",… и текст отдельной строкой, у которой нет своего начала. Поэтому после заголовка драйвер берёт следующую строку как текст — если это не URC (модель вставляет незапрошенные сообщения и туда). В прогоне SMS «Температура 21,5 °C» с номера +70000000001 дошла до приложения через 56 мс после +CMTI и удалена с SIM следующей командой. Очередь номеров — 8; если она полна, номер теряется и SMS остаётся на SIM, — для этого счётчик n_sms_drop, в прогоне он 0.

Звонок. Входящий звонок — повторяющееся RING, после AT+CLIP=1 за каждым — +CLIP: "номер",145,…. Драйвер решает один раз за звонок: спрашивает приложение (on_call), и в READY задание звонка шлёт ATA — ответить — или ATH — сбросить. Конец звонка — NO CARRIER (или ATH с нашей стороны). В прогоне свой номер получил ATA, модель ответила OK через 341 мс после первого RING (из них 200…600 мс модель «снимает трубку»); через 3 с приложение положило трубку — ATH и OK за 27 мс. С чужого номера ATH дошёл до модема через 4 мс после RING. Повторные RING того же звонка решения не меняют.


106г.5 GPRS и TCP: подключение, данные, переподключение

Порядок команд — по памяти о документе SIMCom, не проверено; модель требует именно его и отвечает ERROR на нарушение:

Команда Итог Что делает
AT+CIPSHUT SHUT OK закрыть всё, вернуться в начальное состояние
AT+CSTT="internet" OK точка доступа (APN)
AT+CIICR OK поднять сеанс передачи данных — секунды
AT+CIFSR строка адреса свой адрес; без этой команды CIPSTART не пройдёт
AT+CIPSTART="TCP","192.0.2.1",7 OK, потом CONNECT OK соединение с сервером
AT+CIPSEND=n >, n байт, SEND OK отправить n байт

Каждая — шаг задания GPRS; ошибка любого — пауза и всё сначала с AT+CIPSHUT. В прогоне модель выдала адрес 198.51.100.23, соединение поднялось за 3111 мс (из них 1…2,5 с — AT+CIICR в модели).

Данные от сервера с AT+CIPHEAD=1 приходят как +IPD,n: и n байт без перевода строки в конце. Длина в заголовке — единственный надёжный способ найти конец: в данных могут быть и CR LF, и OK, и RING. Драйвер после : переключается в режим данных и отсчитывает n байт (106г.3). Сервер-эхо модели вернул все пакеты байт в байт:

Пакет Байт До SEND OK, мс До эха, мс
hello 5 206 352
CR LF OK CR LF 6 139 335
+IPD,3:abc CR LF RING CR LF 18 125 392
случайные байты 200 151 305

Переподключение. CLOSED (сервер закрыл соединение) и +PDP: DEACT (сеть сняла сеанс) — незапрошенные сообщения; драйвер помечает TCP упавшим и ставит паузу по правилу 198и.6: 1, 2, 4 … 32 с плюс случайная добавка до четверти. Сам повтор TCP здесь ни при чём — он внутри модема (106.6). Пакет, который не ушёл, не выбрасывается: gsm_tcp_send оставляет его, и он уходит после подключения.

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);
}
Событие в модели Что было Снова на связи
сервер закрыл соединение одна пауза и полный цикл команд через 3658 мс
сеть пропала на 20 с 3 неудачных AT+CIICR, паузы 2427, 4763 и 9329 мс через 26 187 мс, через 6187 мс после возврата сети

Паузы между попытками — от неудачи до следующего AT+CIPSHUT, с добавкой. Задержка после возврата сети — цена нарастающей паузы, как и в 198и.6.


106г.6 Питание, сброс и сторож

Модем — отдельный компьютер со своими сбоями (37.1). Драйвер различает два случая.

Модем замолчал. Любой таймаут в READY — и следующей идёт короткая AT со сроком 1 с; ответ снимает тревогу. Таймауты подряд считаются, и на третьем драйвер держит RST на земле 0,2 с, ждёт загрузки 3 с и начинает с 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;
    }

В прогоне модель «зависла» при поднятом TCP и молчала, пока не дёрнут RST. Заметил это периодический опрос: через 22 563 мс — до 20 с до опроса и три таймаута (AT+CSQ 2 с, два AT по 1 с). После сброса — та же дорога, что при включении, и TCP снова поднялся через 34 616 мс после зависания.

Модем перезапустился сам. Это и есть провал питания из 106г.1: в модели он случился в момент передачи пакета (AT+CIPSEND). Модем после перезапуска забыл всё: скорость, ATE0, точку доступа, соединение. Ни RDY, ни другого сообщения при автоскорости он не шлёт — он молчит. Через 10 с истёк срок AT+CIPSEND, драйвер спросил AT: первый потерялся в автоскорости, на второй модем ответил с эхом. Эхо при выключенном эхе — верный признак перезапуска; драйвер вернулся к настройке без RST. Заметил — через 11 309 мс, TCP снова поднят через 13 593 мс, и тот же пакет, оставшийся в драйвере, вернулся эхом через 13 914 мс.

Прошивка устроена так же, как стенд: прерывание USART кладёт байт в кольцо драйвера, передача идёт через своё кольцо по TXE, главный цикл зовёт gsm_poll и засыпает до следующего прерывания:

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;
}

Структура драйвера со всеми буферами — 2592 байта ОЗУ. Сторож микроконтроллера (12.2, 33а) этого не заменяет: он перезапускает прошивку, а не модем; зато зависший в ожидании ответа модема цикл он поймает — в этом драйвере такого цикла нет.


106г.7 Грабли

Модуль перезапускается при отправке SMS или подключении GPRS. Питание: провал во время передачи. Свой источник с запасом по току, толстые короткие провода, конденсатор у модуля; в прошивке — признак перезапуска (эхо вернулось) и повторная настройка.

Ответ на команду — повтор самой команды. Эхо включено. Первой командой — ATE0; разбор должен выбрасывать строку, равную своей команде. С эхом «наивный» разбор ошибся в 994 случаях из 1000.

Вместо ответа пришло RING или +CMTI. Незапрошенное сообщение посреди ответа. Ответ определяется по началу строки (+CSQ:), а не по порядку; всё остальное — URC. Без эха «наивный» разбор ошибся 115 раз из 1000.

AT+CMGS или AT+CIPSEND висит до таймаута. Разбор ждёт конца строки после >, а его нет. Приглашение узнаётся по двум символам.

SMS кириллицей уходит пустой или с ошибкой. Не включена кодировка UCS2 или номер оставлен латиницей. В режиме UCS2 и номер — в UCS2.

Данные TCP режут ответы. Пакет, содержащий CR LF или OK, разобран как строки. Длина из +IPD,n: — и n байт без разбора.

Новые SMS копятся на SIM. Очередь +CMTI переполнилась, номер потерян. Очередь с запасом, счётчик потерь; при потере — перечитать хранилище целиком (здесь не сделано).

AT+CIPSTART отвечает ERROR сразу. Пропущен AT+CIFSR или не сделан AT+CIPSHUT после обрыва — модем не в том состоянии (по памяти, в модели так).

Прибор не регистрируется, +CSQ: 99,99. Нет антенны, нет сети 2G в этом месте или нет денег и услуг на SIM-карте; +CREG: 0,3 — сеть отказала.


106г.8 Итоги

Полные исходники

gsm106e.h — драйвер: буферы, состояния, события и прототипы.

/* 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 — общий для прошивки и стенда.

/* 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: USART1 с прерываниями, PWRKEY и RST, главный цикл.

/* 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();                                /* до следующего прерывания: байта или тика */
    }
}

Дальше: раздел 107 «Что происходит, когда вводишь адрес: DNS, DHCP, HTTP, TLS» — что стек делает поверх этих кадров.

Открыть в интерактивном справочнике