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-команды, но другие тонкости).
Схема подключения и устройство драйвера — на рисунке.
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 Итоги
- SIM800L — модем 2G: свой источник питания с запасом на импульсы передачи, уровни UART сверить по документам (у F103 вход PA10 — с допуском 5 В, порог «1» — 0,65·VDD, 2,145 В при 3,3 В). Сети 2G в ряде стран отключают.
- Ответ — строки в CR LF и итог
OK/ERROR; эхо кончается одним CR;>приходит без перевода строки;AT+CIFSRотвечает безOK. +CSQ: −113 + 2 × rssi дБм, от −113 до −51, 99 — неизвестно;+CREG: работать при 1 и 5.- Драйвер без ожиданий: прерывание — в кольцо на 512 байт,
gsm_poll— строки, ответ по началу строки, всё прочее — URC; одна команда за раз со своим сроком. Под нагрузкой — 2000 из 2000 ответов верно; «наивный» разбор — 885 из 1000 без эха и 0 из 1000 с эхом. - SMS кириллицей — UCS2: 4 цифры на знак,
Привет, мир!— 48 цифр, и номер тоже в UCS2. - TCP:
CIPSHUT→CSTT→CIICR→CIFSR→CIPSTART→CIPSEND; данные+IPD,n:читать по длине; послеCLOSEDи+PDP: DEACT— нарастающая пауза, пакет не теряется. - Сторож модема: таймаут → короткий
AT, три таймаута подряд →RST0,2 с; эхо при выключенном эхе → модем перезапустился, настроить заново. Зависание замечено за 22,6 с, перезапуск — за 11,3 с. - Весь драйвер — 2592 байта ОЗУ и около 6,5 КБ кода на Cortex-M3.
Полные исходники
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» — что стек делает поверх этих кадров.