Este artigo mostra como utilizar as placas de desenvolvimento Arduino Mega 2560 e um modem SIM7600G em um nó IoT celular capaz de coletar dados de rede, GPS e status do SIM, empacotá-los em JSON, que futuramente enviará para qualquer back-end.
Por que um Mega 2560 + SIM7600G?
- SRAM abundante (8 kB) — gera JSON, mantém buffers e ainda sobra para lógica de aplicação; um UNO ficaria no limite.
- UART dedicada (Serial1) — o Mega oferece portas seriais extras, isolando o tráfego do modem do console USB.
- SIM7600G — cobre 2G/3G/4G e GNSS no mesmo chip, simplificando o hardware.
A pinagem utilizada para este projeto está explicada em outro post.
Estrutura do projeto
arduino-mobile-sensor/
├─ src/ // sketch principal
├─ include/ // headers próprios
├─ lib/ // bibliotecas internas
└─ .pio/ // cache do PlatformIO
Código-fonte e arquivos do projeto: https://gitlab.com/mfs.eng.br/hardware-quirks/arduino-sim7600-telemetry
Anatomia do código em C++
Inclusões mínimas
#include <Arduino.h> // HAL AVR
#include <avr/pgmspace.h> // PROGMEM
#include <ArduinoJson.h> // Única lib externa
Definições constantes
constexpr uint32_t USB_BAUD = 115200;
constexpr uint32_t MODEM_BAUD = 115200;
constexpr uint16_t TIMEOUT_MS = 1500;
#define OUTPUTDEBUG 0
/* ------------ buffer de recepção ----------------------------- */
static char respBuf[160];
static size_t respLen = 0;
constexprgarante avaliação em compile-time;- flags
#defineativam logs sem custo quando desligadas; - buffers globais (
char respBuf[160]) vivem em BSS, evitando uso de heap.
Strings AT em Flash
/* ------------ comandos em flash ------------------------------ */
const char CMD_CGSN[] PROGMEM = "AT+CGSN";
const char CMD_CIMI[] PROGMEM = "AT+CIMI";
const char CMD_CGMM[] PROGMEM = "AT+CGMM";
const char CMD_CSPN[] PROGMEM = "AT+CSPN?";
const char CMD_CSQ[] PROGMEM = "AT+CSQ";
const char CMD_CREG[] PROGMEM = "AT+CREG?";
const char CMD_COPS[] PROGMEM = "AT+COPS?";
const char CMD_CGPADDR[] PROGMEM = "AT+CGPADDR";
const char CMD_CGDCONT[] PROGMEM = "AT+CGDCONT?";
const char AT_GPSINFO[] PROGMEM = "AT+CGPSINFO";
PROGMEM poupa SRAM; cada comando moraria no Flash.
Compatibilidade Flash-print
#ifndef FPSTR
#define FPSTR(p) (reinterpret_cast<const __FlashStringHelper *>(p))
#endif
Isso permite que Serial.println() imprima strings guardadas em Flash.
Máquina de leitura/escrita dos comandos AT (sendAT)
Envio
Serial1.println(cmd); // já inclui \r\n
Coleta em bloco
// Loop principal: fica ativo enquanto o tempo decorrido for menor que 'tout'
while (millis() - t0 < tout)
{
// Loop de leitura: executa enquanto houver bytes no UART *e*
// ainda houver espaço no buffer (deixa 1 byte livre para o '\0')
while (Serial1.available() && respLen < sizeof(respBuf) - 1)
// Lê um byte da porta Serial1 e armazena na posição atual do buffer,
// depois incrementa 'respLen' para apontar para o próximo espaço livre
respBuf[respLen++] = Serial1.read();
// Coloca terminador nulo na posição atual, transformando respBuf
// em uma string C válida (necessário para usar funções de <string.h>)
respBuf[respLen] = '\0';
// Verifica se a resposta já traz um "\nOK" ou "\nERROR"
// — padrões que sinalizam fim de resposta AT.
// Se encontrar, sai imediatamente do loop externo (timeout não é mais necessário)
if (strstr(respBuf, "\nOK") || strstr(respBuf, "\nERROR"))
break;
}
A função singleLineAT
/* ---------- devolve primeira linha “útil” -------------------- */
bool singleLineAT(const __FlashStringHelper *cmd, char *dst, size_t dstSz)
{
if (!sendAT(cmd)) return false;
char *p = respBuf;
while (*p) {
/* pula CR/LF */
while (*p == '\r' || *p == '\n') ++p;
if (!*p) break;
/* encontra fim da linha */
char *q = p;
while (*q && *q != '\r' && *q != '\n') ++q;
/* copia linha para tmp */
char line[128];
size_t len = min((size_t)(q - p), sizeof line - 1);
memcpy(line, p, len); line[len] = '\0';
/* trim início */
char *s = line;
while (*s == ' ') ++s;
/* descarta eco, OK, ERROR */
if (!strncmp(s, "AT", 2) || !strcmp(s, "OK") || !strncmp(s, "ERROR", 5)) {
p = q;
continue;
}
/* achou payload */
strncpy(dst, s, dstSz - 1);
dst[dstSz - 1] = '\0';
return true;
}
return false;
}
A rotina singleLineAT() encapsula todo o trabalho de disparar um comando AT e devolver exatamente a primeira linha útil da resposta, já limpa de ecos, quebras de linha, espaços extras e tokens de controle.
- Disparo e verificação de sucesso:
if (!sendAT(cmd)) return false;reaproveitasendAT()para enviar o comando e encherrespBuf. Se ocorrerERRORou timeout, a função já devolvefalse. - Iterador
ppercorre o buffer bruto:char *p = respBuf; while (*p) { ... }varre o buffer até achar uma linha útil. - Pular quebras de linha:
while (*p == '\r' || *p == '\n') ++p;garante quepsempre aponte para o primeiro caractere real da linha corrente. - Encontrar o fim da linha corrente:
char *q = p; while (*q && *q != '\r' && *q != '\n') ++q;define o intervalo[p, q)da linha. - Copiar para buffer temporário usando
memcpy()e limitando o tamanho para evitar overflow. - Copiar o payload ao destino com
strncpy(dst, s, dstSz - 1); dst[dstSz - 1] = '\0';. - Se nenhuma linha útil aparecer, o laço termina naturalmente e a função devolve
false.
Boas práticas de embarcados — buffers fixos em vez de String
| Benefício | Consequência no campo |
|---|---|
| Zero fragmentação de heap | Uptime sem resets imprevistos |
| Tempo de execução determinístico | Sem pausas aleatórias de realloc |
Funções de conversão e limpeza
| Função | Propósito |
|---|---|
ddmmToDecimal() |
No GPS, converte coordenadas NMEA ddmm.mmm → graus decimais. |
csqToDbm() |
Converte 0-31/99 → dBm (-113 a -51). |
cregText() |
Código 0-5 → texto "registered (roaming)". |
techText() |
0/1/2/7/8 → 2G/3G/4G/CDMA. |
csvField() |
Extrai campo n de "CMD: a,b,c". |
stripQuotes() |
Remove aspas e espaços nas extremidades. |
Todas essas funções recebem ponteiros ou valores primitivos para otimização de memória.
Roteiro
Setup
- Abre USB e UART do modem.
- Habilita GPS automático (
AT+CGPSAUTO=1). - Define exibição numérica de operadora (
AT+COPS=3,2).
Loop
- Se chegar
"INFO"pelo USB, disparasendInfoJSON()e imprime o JSON. - Caso contrário, funciona como serial transparente PC ↔ modem.
A função sendInfoJSON() executa a sequência dos comandos AT, converte métricas e monta o JSON final.
Campos gerados e valor para telecom
| Campo | Uso prático |
|---|---|
imei |
Inventário de ativos, id do dispositivo (modem). |
imsi |
Inventário de ativos, id do simcard. |
spn |
Nome comercial da operadora. |
csq / rssi |
Diagnóstico de antena e qualidade de cobertura. |
reg / ran |
Estado de registro e tecnologia (2G/3G/4G). |
ip / apn |
Validação de contexto de dados (PDP/Bearer). |
location |
Georreferenciamento. |
O JSON completo cabe em ~300 bytes, ideal para transporte via MQTT.
Tricks and Tips
- Reaproveitar
respBuf— o mesmo buffer serve à detecção de fim de comando e à análise da resposta, evitando tráfego duplicado. singleLineAT()universal — limpa eco e ruído em qualquer comando que retorne uma única linha.- Strings em Flash — elimina 300+ bytes de SRAM, algo crítico em AVR.
Próximos passos sugeridos
- Publicar via MQTT/TLS.
- Gerenciar energia — desligar GPS (
AT+CGPS=0) fora das janelas de leitura. - Implementar FOTA — update de firmware remoto direto no módulo.
Conclusão
Esse é o primeiro passo: entender o diálogo “AT” e convertê-lo em dados de negócio. No próximo artigo, veremos como transmitir essa telemetria de forma segura e escalável para a nuvem.