top of page
Search

SIMCOM SIM7070G with STM32: A Practical Path to Stable NB-IoT and HTTP Operation

  • Writer: CircuitCopper
    CircuitCopper
  • Jul 11
  • 10 min read

The SIMCOM SIM7070G initially appears to be a simple device: connect it over UART, send AT commands, register on the network, and transmit data to a server.

On a laboratory bench, the first HTTP request can often be completed relatively quickly.

The real problems begin later, when the device must operate autonomously for days or weeks. The network may disappear temporarily, the modem may send asynchronous URC messages, an HTTP session may close unexpectedly, and commands such as SHCONN, SHDISC, or SHREQ may return ERROR or +CME ERROR.

In the SmartMotorActuator project, we used the SIM7070G together with an STM32 microcontroller to send telemetry to ThingsBoard over HTTP. MQTT was not used in this implementation.

This article covers:

  • SIM7070G initialization;

  • registration on an Israeli NB-IoT network;

  • UART receive architecture;

  • processing asynchronous modem responses;

  • HTTP finite state machine design;

  • protection against infinite waiting states;

  • recovery after SHCONN, SHDISC, and SHREQ failures;

  • practical experience with Italian and Israeli SIM cards.

The SIM Card Problem: When the Modem Works but the Network Does Not

During the first development stage, we used a SIM card supplied from Italy.

The modem detected the card correctly, and the command:

AT+CPIN?

returned:

+CPIN: READY

However, in Israel the modem could not register reliably on an NB-IoT network or activate a PDP context.

We tested different network modes, RAT configurations, APN settings, and automatic operator selection. However, firmware cannot compensate for the absence of a suitable NB-IoT roaming agreement between operators.

This does not mean that every Italian SIM card will fail in Israel. It means that the specific SIM card used in our project did not work on the Israeli NB-IoT network.

We therefore switched to a local NB-IoT SIM card from Pelephone.

The following APN was used:

After the correct configuration, the modem registered successfully on the Pelephone network:

+CEREG: 1
+COPS: 1,0,"Pelephone",9
+CPSI: LTE NB-IOT,Online,425-03,...,EUTRAN-BAND28

PDP context activation also completed successfully:

+CNACT: 0,1,"84.xxx.xxx.xxx"

The practical conclusion is simple:

Before spending significant time debugging firmware, verify that the SIM card actually supports NB-IoT in the target country, including the required roaming agreements.

Driver Architecture

A reliable SIM7070G driver should contain at least four layers:

  1. UART RX ring buffer.

  2. Line-based parser for responses and URCs.

  3. Functions for individual AT transactions.

  4. A high-level finite state machine for network and HTTP operation.

A common implementation mistake is to transmit a command and immediately search the UART buffer for OK.

The SIM7070G operates asynchronously. Between a command and its final response, the modem may send messages such as:

+CEREG
+CPIN
SMS Ready
*PSUTTZ
+CNACT

The parser must therefore process the incoming stream continuously. The result of the current AT transaction should be stored separately from the general network state.

In the main application loop, UART processing should usually run first:

while (1)
{
    SIM7070_ProcessRx();
    SIM7070_NetworkTask();
    SIM7070_HttpFsmTask();
    ApplicationTask();
}

Basic AT Command Function

Before transmitting a new command, the response state from the previous transaction must be cleared.

Otherwise, an old OK that remains in the receive buffer may be incorrectly interpreted as the response to a new command.

typedef enum
{
    SIM_RSP_NONE = 0,
    SIM_RSP_OK,
    SIM_RSP_ERROR,
    SIM_RSP_CME_ERROR
} SimResponse_t;

static volatile SimResponse_t g_sim_response;
static volatile int g_sim_cme_code;

bool SIM7070_SendCommand(const char *command, uint32_t timeout_ms)
{
    uint32_t start;

    g_sim_response = SIM_RSP_NONE;
    g_sim_cme_code = 0;

    SIM_UART_SendString(command);
    SIM_UART_SendString("\r\n");

    start = HAL_GetTick();

    while ((HAL_GetTick() - start) < timeout_ms)
    {
        SIM7070_ProcessRx();

        if (g_sim_response == SIM_RSP_OK)
        {
            return true;
        }

        if ((g_sim_response == SIM_RSP_ERROR) ||
            (g_sim_response == SIM_RSP_CME_ERROR))
        {
            return false;
        }
    }

    return false;
}

This is a blocking helper function, so it should only be used for operations with strictly limited execution time.

Long network operations should be handled by a finite state machine.

Practical Initialization Function

The following example is a reduced initialization function that can be used as a starting point.

Every command should be checked. Commands should not simply be transmitted one after another without verifying their results.

#define SIM_APN       "sphone.pelephone.net.il"
#define SIM_APN_USER  "<APN_USER>"
#define SIM_APN_PASS  "<APN_PASSWORD>"

bool SIM7070_Init(void)
{
    char command[160];

    /* Synchronize UART communication with the modem */
    for (uint8_t retry = 0; retry < 10; retry++)
    {
        if (SIM7070_SendCommand("AT", 1000))
        {
            break;
        }

        HAL_Delay(500);

        if (retry == 9)
        {
            return false;
        }
    }

    if (!SIM7070_SendCommand("ATE0", 1000))
        return false;

    if (!SIM7070_SendCommand("ATV1", 1000))
        return false;

    if (!SIM7070_SendCommand("ATQ0", 1000))
        return false;

    if (!SIM7070_SendCommand("AT+CMEE=1", 1000))
        return false;

    /* Check SIM card status */
    if (!SIM7070_WaitForSimReady(10000))
        return false;

    /* Disable power-saving modes during debugging */
    SIM7070_SendCommand("AT+CPSMS=0", 2000);
    SIM7070_SendCommand("AT+CEDRXS=0", 2000);

    /* Enable extended registration information */
    if (!SIM7070_SendCommand("AT+CEREG=2", 2000))
        return false;

    snprintf(command, sizeof(command),
             "AT+CGDCONT=1,\"IP\",\"%s\",\"0.0.0.0\",0,0",
             SIM_APN);

    if (!SIM7070_SendCommand(command, 3000))
        return false;

    snprintf(command, sizeof(command),
             "AT+CNCFG=0,1,\"%s\",\"%s\",\"%s\",1",
             SIM_APN,
             SIM_APN_USER,
             SIM_APN_PASS);

    if (!SIM7070_SendCommand(command, 3000))
        return false;

    /*
     * CNMP=2: automatic network mode.
     * CMNB=3: LPWA profile selected according to
     * the modem firmware and operator configuration.
     */
    if (!SIM7070_SendCommand("AT+CFUN=0", 5000))
        return false;

    if (!SIM7070_SendCommand("AT+CNMP=2", 2000))
        return false;

    if (!SIM7070_SendCommand("AT+CMNB=3", 2000))
        return false;

    if (!SIM7070_SendCommand("AT+CFUN=1", 5000))
        return false;

    if (!SIM7070_WaitForRegistration(120000))
        return false;

    if (!SIM7070_SendCommand("AT+CGATT=1", 30000))
        return false;

    if (!SIM7070_SendCommand("AT+CNACT=0,1", 30000))
        return false;

    if (!SIM7070_WaitForPdpActive(30000))
        return false;

    return true;
}

The response to AT+CNACT=0,1 should not be treated as final proof that the modem is online.

The firmware should query AT+CNACT? and verify that the PDP context is active and that the modem received an IP address.

Processing Responses and URCs

The parser must recognize both standard command responses and asynchronous network messages.

Correct parsing of the HTTP request result is particularly important.

The SIM7070G returns the HTTP method inside quotation marks:

+SHREQ: "GET",200,48
+SHREQ: "POST",200,0

An incorrect parser may expect only numeric fields:

sscanf(line, "+SHREQ: %d,%d,%d", ...);

In that case, the HTTP completion flag will never be set.

A correct parser can be implemented as follows:

static volatile bool g_shreq_seen;
static volatile int g_http_code;
static volatile int g_http_data_len;
static char g_http_method[8];

void SIM7070_ParseLine(const char *line)
{
    char method[8];
    int code;
    int length;
    int cme_code;

    if (strcmp(line, "OK") == 0)
    {
        g_sim_response = SIM_RSP_OK;
        return;
    }

    if (strcmp(line, "ERROR") == 0)
    {
        g_sim_response = SIM_RSP_ERROR;
        return;
    }

    if (sscanf(line, "+CME ERROR: %d", &cme_code) == 1)
    {
        g_sim_cme_code = cme_code;
        g_sim_response = SIM_RSP_CME_ERROR;
        return;
    }

    if (sscanf(line,
               "+SHREQ: \"%7[^\"]\",%d,%d",
               method,
               &code,
               &length) == 3)
    {
        strncpy(g_http_method, method, sizeof(g_http_method) - 1);
        g_http_method[sizeof(g_http_method) - 1] = '\0';

        g_http_code = code;
        g_http_data_len = length;
        g_shreq_seen = true;
        return;
    }

    if (SIM7070_ParseCereg(line))
        return;

    if (SIM7070_ParseCnact(line))
        return;

    SIM7070_OnLogLine(line);
}

The > prompt used by AT+SHBOD may arrive without a normal line terminator.

It should therefore be detected at the byte-processing level or by a dedicated prompt parser instead of being processed as a standard text line.

Opening an HTTP Session

For every new HTTP session, we applied the required configuration before executing AT+SHCONN.

bool SIM7070_HTTP_Open(void)
{
    /*
     * SHDISC is used as a best-effort cleanup operation.
     * ERROR does not always indicate a critical failure:
     * there may simply be no active HTTP session.
     */
    SIM7070_SendCommand("AT+SHDISC", 5000);

    if (!SIM7070_SendCommand(
            "AT+SHCONF=\"URL\",\"http://demo.thingsboard.io\"",
            3000))
        return false;

    if (!SIM7070_SendCommand(
            "AT+SHCONF=\"BODYLEN\",1024",
            3000))
        return false;

    if (!SIM7070_SendCommand(
            "AT+SHCONF=\"HEADERLEN\",350",
            3000))
        return false;

    if (!SIM7070_SendCommand(
            "AT+SHCONF=\"TIMEOUT\",180",
            3000))
        return false;

    if (!SIM7070_SendCommand(
            "AT+SHCONF=\"POLLCNT\",7",
            3000))
        return false;

    if (!SIM7070_SendCommand("AT+SHCONN", 30000))
        return false;

    return true;
}

One of the problems discovered during development was that the return values of the SHCONF commands were ignored.

The firmware continued to execute SHCONN even when one of the preceding configuration commands had already failed.

After a full modem reinitialization, the complete SHCONF sequence should be executed again.

The firmware must not assume that HTTP configuration remains valid after CFUN, a software reset, or network recovery.

Sending JSON Data

The telemetry transmission sequence was:

AT+SHCHEAD
AT+SHAHEAD="Content-Type","application/json"
AT+SHBOD=<length>,5000
>
<JSON>
OK
AT+SHREQ="/api/v1/<DEVICE_TOKEN>/telemetry",3
OK
+SHREQ: "POST",200,0

The function that starts a POST request should not wait indefinitely for the final +SHREQ message.

Its responsibility is to transfer the body correctly and start the HTTP request.

Waiting for the final result should be performed by a separate FSM state.

bool SIM7070_HTTP_PostJsonStart(const char *path,
                                const char *json)
{
    char command[192];
    size_t length = strlen(json);

    if (!SIM7070_SendCommand("AT+SHCHEAD", 3000))
        return false;

    if (!SIM7070_SendCommand(
            "AT+SHAHEAD=\"Content-Type\",\"application/json\"",
            3000))
        return false;

    g_body_prompt_seen = false;
    g_sim_response = SIM_RSP_NONE;

    snprintf(command, sizeof(command),
             "AT+SHBOD=%lu,5000",
             (unsigned long)length);

    SIM_UART_SendString(command);
    SIM_UART_SendString("\r\n");

    if (!SIM7070_WaitForBodyPrompt(5000))
        return false;

    /*
     * Clear the response again before sending the body
     * to avoid accepting an old OK from SHBOD.
     */
    g_sim_response = SIM_RSP_NONE;

    SIM_UART_SendData((const uint8_t *)json, length);

    if (!SIM7070_WaitForResponse(5000))
        return false;

    g_shreq_seen = false;
    g_http_code = 0;
    g_http_data_len = 0;

    snprintf(command, sizeof(command),
             "AT+SHREQ=\"%s\",3",
             path);

    SIM_UART_SendString(command);
    SIM_UART_SendString("\r\n");

    return true;
}

HTTP Finite State Machine

Every waiting state in the FSM must have a timeout.

States such as WAIT_GET or WAIT_POST without time limits are not acceptable in an autonomous embedded device.

typedef enum
{
    HTTP_IDLE = 0,
    HTTP_OPEN,
    HTTP_GET_START,
    HTTP_WAIT_GET,
    HTTP_READ_BODY,
    HTTP_POST_START,
    HTTP_WAIT_POST,
    HTTP_CLOSE,
    HTTP_ERROR,
    HTTP_RETRY_DELAY
} HttpState_t;

static HttpState_t http_state = HTTP_IDLE;
static uint32_t http_state_started;
static uint8_t http_retry_count;

static void HTTP_SetState(HttpState_t new_state)
{
    http_state = new_state;
    http_state_started = HAL_GetTick();
}

static bool HTTP_StateTimeout(uint32_t timeout_ms)
{
    return (HAL_GetTick() - http_state_started) >= timeout_ms;
}

The main FSM task can be implemented as follows:

void SIM7070_HttpFsmTask(void)
{
    switch (http_state)
    {
        case HTTP_IDLE:
            if (TelemetryPeriodElapsed())
            {
                HTTP_SetState(HTTP_OPEN);
            }
            break;

        case HTTP_OPEN:
            if (SIM7070_HTTP_Open())
            {
                HTTP_SetState(HTTP_GET_START);
            }
            else
            {
                HTTP_SetState(HTTP_ERROR);
            }
            break;

        case HTTP_GET_START:
            g_shreq_seen = false;

            if (SIM7070_HTTP_GetAttributesStart())
            {
                HTTP_SetState(HTTP_WAIT_GET);
            }
            else
            {
                HTTP_SetState(HTTP_ERROR);
            }
            break;

        case HTTP_WAIT_GET:
            if (g_shreq_seen)
            {
                g_shreq_seen = false;

                if ((g_http_code >= 200) &&
                    (g_http_code < 300))
                {
                    if (g_http_data_len > 0)
                        HTTP_SetState(HTTP_READ_BODY);
                    else
                        HTTP_SetState(HTTP_POST_START);
                }
                else
                {
                    HTTP_SetState(HTTP_ERROR);
                }
            }
            else if (HTTP_StateTimeout(20000))
            {
                HTTP_SetState(HTTP_ERROR);
            }
            break;

        case HTTP_READ_BODY:
            if (SIM7070_HTTP_ReadBody(
                    g_http_data_len,
                    g_attribute_json,
                    sizeof(g_attribute_json)))
            {
                ProcessSharedAttributes(g_attribute_json);
                HTTP_SetState(HTTP_POST_START);
            }
            else
            {
                HTTP_SetState(HTTP_ERROR);
            }
            break;

        case HTTP_POST_START:
            BuildTelemetryJson(g_telemetry_json,
                               sizeof(g_telemetry_json));

            if (SIM7070_HTTP_PostJsonStart(
                    "/api/v1/<DEVICE_TOKEN>/telemetry",
                    g_telemetry_json))
            {
                HTTP_SetState(HTTP_WAIT_POST);
            }
            else
            {
                /*
                 * Do not enter WAIT_POST if the request
                 * was not successfully started.
                 */
                HTTP_SetState(HTTP_ERROR);
            }
            break;

        case HTTP_WAIT_POST:
            if (g_shreq_seen)
            {
                g_shreq_seen = false;

                if ((g_http_code >= 200) &&
                    (g_http_code < 300))
                {
                    /*
                     * Clear pending events only after
                     * a confirmed successful POST.
                     */
                    ConfirmTelemetryDelivered();
                    http_retry_count = 0;
                    HTTP_SetState(HTTP_CLOSE);
                }
                else
                {
                    HTTP_SetState(HTTP_ERROR);
                }
            }
            else if (HTTP_StateTimeout(20000))
            {
                HTTP_SetState(HTTP_ERROR);
            }
            break;

        case HTTP_CLOSE:
            SIM7070_SendCommand("AT+SHDISC", 5000);
            HTTP_SetState(HTTP_IDLE);
            break;

        case HTTP_ERROR:
            SIM7070_SendCommand("AT+SHDISC", 5000);

            if (http_retry_count < 10)
                http_retry_count++;

            HTTP_SetState(HTTP_RETRY_DELAY);
            break;

        case HTTP_RETRY_DELAY:
            if (HTTP_StateTimeout(
                    SIM7070_GetRetryDelay(http_retry_count)))
            {
                if (!SIM7070_IsPdpActive())
                {
                    SIM7070_RequestNetworkRecovery();
                }

                HTTP_SetState(HTTP_OPEN);
            }
            break;

        default:
            HTTP_SetState(HTTP_ERROR);
            break;
    }
}

How the Infinite Lockup Occurred

One of the most important defects was located between the POST function and the HTTP state machine.

The original SIM7070_HTTP_PostJson() function waited for:

+SHREQ: "POST",...

for approximately 15 seconds.

When the expected line did not arrive, the function returned an error.

However, the calling code ignored the return value and unconditionally executed:

http_state = HTTP_WAIT_POST;

The HTTP_WAIT_POST state could only be exited when g_shreq_seen became true.

There was no timeout.

If +SHREQ never arrived, the system could remain in this state for several hours.

The correct logic is:

if (SIM7070_HTTP_PostJsonStart(path, json))
{
    HTTP_SetState(HTTP_WAIT_POST);
}
else
{
    HTTP_SetState(HTTP_ERROR);
}

Even when the request starts successfully, HTTP_WAIT_POST must still have its own timeout.

This leads to an important general embedded-system rule:

No state that depends on an external device, network response, interrupt, or asynchronous event should have an unlimited waiting period.

Handling SHCONN and SHDISC Errors

Real logs contained sequences such as:

AT+SHCONN
+CME ERROR: 3

AT+SHDISC
+CME ERROR: 3

The firmware then immediately repeated the same commands, creating a tight retry loop:

SHCONN
SHDISC
SHCONN
SHDISC
...

This type of loop does not give the modem enough time to recover its internal state.

We changed the recovery strategy as follows:

  1. Limited the number of immediate retries.

  2. Added a delay between complete HTTP session attempts.

  3. Verified the PDP context state.

  4. Repeated the full SHCONF sequence after modem reinitialization.

  5. Performed network recovery after persistent failures instead of repeating SHCONN indefinitely.

An error returned by SHDISC is not always critical.

It may simply mean that no active HTTP session exists.

In the recovery path, SHDISC can therefore be treated as a best-effort cleanup operation. However, the firmware must still restore a consistent internal software state after the cleanup attempt.

Why the Response Body Should Be Read for HTTP 400, 404, and 500

Initially, the firmware inspected only the HTTP status code:

+SHREQ: "POST",500,123

After receiving this result, it immediately started recovery.

However, the response length of 123 indicates that the server returned a diagnostic body.

That body may contain the actual cause of the failure, for example:

  • an incorrect endpoint;

  • invalid JSON;

  • an incorrect device token;

  • an unsupported data format;

  • an internal server error.

For any response with a non-zero data length, including HTTP 400, 404, and 500, it is useful to execute AT+SHREAD, store the body in the diagnostic log, and only then close the HTTP session.

Changes That Produced the Largest Improvement

The most important improvements were not related to one specific AT command. They were architectural:

  • one owner for the UART RX ring buffer;

  • continuous execution of SIM7070_ProcessRx();

  • separation of regular command responses and URCs;

  • clearing response flags before every new transaction;

  • correct parsing of +SHREQ with the method in quotation marks;

  • separate processing of the > prompt;

  • verification of every SHCONF, SHCHEAD, and SHAHEAD command;

  • a timeout in every waiting state;

  • entering WAIT_POST only after the POST request was successfully started;

  • controlled delays between recovery attempts;

  • complete HTTP reconfiguration after modem reinitialization;

  • clearing pending telemetry events only after confirmed HTTP 2xx delivery;

  • reading the response body after server-side errors;

  • complete TX, RX, and FSM logs with timestamps.

A dedicated diagnostic UART was especially useful.

The log should contain not only modem responses but also FSM state transitions:

[125000] >> AT+SHREQ="/api/v1/.../telemetry",3
[125015] << OK
[145020] TIMEOUT waiting POST +SHREQ
[145021] FSM HTTP_WAIT_POST -> HTTP_ERROR
[150025] FSM HTTP_RETRY_DELAY -> HTTP_OPEN

Without timestamps, it is difficult to determine whether a real timeout occurred or whether the firmware simply stopped processing UART data.

Conclusion

The SIM7070G should not be treated as a synchronous peripheral that always returns OK immediately after receiving an AT command.

It is a network modem with its own internal state, asynchronous events, long-running operations, and the ability to lose an HTTP session independently of the STM32 application state.

A reliable implementation requires:

  • a strict finite state machine;

  • bounded timeouts;

  • correct URC parsing;

  • verification of every transaction stage;

  • recovery of consistency between the firmware state and the modem state;

  • a SIM card with confirmed NB-IoT support in the target country.

In our project in Israel, we used an NB-IoT SIM card from Pelephone with the APN:

The Italian SIM card used during the initial development stage did not work in our configuration on the Israeli NB-IoT network.

Embedded developers, communication engineers, and SIMCOM users are invited to join the discussion.

It would be especially useful to compare experience with long-term SIM7070G operation, uncommon CME ERROR cases, HTTP recovery strategies, and NB-IoT roaming behavior in different countries.

I am also interested in collaboration on further development, code review, and debugging of a production-ready SIM7070G driver for STM32.

Real modem logs, discovered corner cases, and code contributions could help create a more reliable implementation for other embedded and IoT projects.

 
 
 

Comments


bottom of page