Practical Application of an Asynchronous AT Command Framework for Embedded Systems

In embedded system development, AT commands are widely used to control peripheral modules such as cellular communication modules, Wi-Fi modules, and Bluetooth devices due to their simple structure, strong readability, and ease of debugging. As system complexity increases, traditional synchronous blocking AT command processing methods can no longer meet the real-time demands of multitasking and multi-channel operations.

The moluo-tech open-source AT-Command project on Gitee provides a fully functional asynchronous AT command processing framework that can run without an operating system. This project supports single-line commands, batch commands, variable parameter commands, and custom commands, featuring advanced capabilities such as command timeout retransmission, priority management, URC capture, memory monitoring, and multi-device communication management, offering a high-performance and high-reliability solution for embedded AT command processing.

1. Overview of Framework Design

The “moluo-tech/AT-Command” provides a lightweight AT command framework suitable for bare-metal environments, implementing command transmission and reception based on an asynchronous mechanism. The framework supports the following features:

  • Asynchronous Processing: All command requests are executed asynchronously to avoid blocking the main loop.
  • Command Types: Supports single-line, batch, variable parameter, and custom AT commands.
  • Robustness: Supports response timeouts, error retransmissions, and priority management.
  • URC Capture: Handles unsolicited responses of variable length.
  • Multi-Device Support: Manages multiple AT devices (e.g., multiple serial modules).
  • Memory Management: Monitors memory usage to prevent overflow.
  • Lifecycle Management: Tracks command status in real-time.
  • Command Passthrough: Allows direct sending of raw AT commands.

The following implementation of this framework is based on STM32F103 (HAL library), assuming UART1 and UART2 are used to connect two AT devices (e.g., ESP8266 and SIM800).

2. Core Implementation of Asynchronous AT Commands

To achieve asynchronous processing, a linked list is used to manage the command queue, combined with interrupt data reception. Below is the core data structure:

#include "stm32f1xx_hal.h"
#include <string.h>
#include <stdlib.h>

#define MAX_CMD_LEN 128
#define MAX_QUEUE_SIZE 10

typedef enum { IDLE, PENDING, SUCCESS, TIMEOUT, ERROR } CmdState;

typedef struct {
    char cmd[MAX_CMD_LEN]; // Command string
    uint8_t priority;      // Priority (0 highest)
    CmdState state;        // Command state
    uint32_t timeout_ms;   // Timeout duration
    uint32_t start_time;   // Send time
    uint8_t retries;       // Retry count
    UART_HandleTypeDef *huart; // UART handle
    void (*callback)(char *response); // Response callback
} ATCommand;

typedef struct CmdNode {
    ATCommand cmd;
    struct CmdNode *next;
} CmdNode;

CmdNode *cmd_queue = NULL;
uint32_t used_memory = 0; // Memory usage

2.1 Single-Line and Batch Commands

Single-line commands are sent directly, while batch commands are processed one by one through the queue. To add a command to the queue:

void AT_AddCommand(char *cmd, uint8_t priority, uint32_t timeout_ms, UART_HandleTypeDef *huart, void (*callback)(char *response)) {
    if (used_memory + sizeof(CmdNode) > 1024) return; // Memory limit 1KB
    CmdNode *node = (CmdNode *)malloc(sizeof(CmdNode));
    if (!node) return;
    used_memory += sizeof(CmdNode);

    strncpy(node->cmd.cmd, cmd, MAX_CMD_LEN - 1);
    node->cmd.priority = priority;
    node->cmd.state = IDLE;
    node->cmd.timeout_ms = timeout_ms;
    node->cmd.start_time = 0;
    node->cmd.retries = 3; // Default retry 3 times
    node->cmd.huart = huart;
    node->cmd.callback = callback;
    node->next = NULL;

    // Insert into queue by priority
    if (!cmd_queue || priority < cmd_queue->cmd.priority) {
        node->next = cmd_queue;
        cmd_queue = node;
    } else {
        CmdNode *current = cmd_queue;
        while (current->next && current->next->cmd.priority <= priority) {
            current = current->next;
        }
        node->next = current->next;
        current->next = node;
    }
}

2.2 Variable Parameter and Custom Commands

Variable parameter commands are implemented through formatted strings, while custom commands are concatenated directly. For example:

void AT_SetWiFi(char *ssid, char *password, UART_HandleTypeDef *huart) {
    char cmd[MAX_CMD_LEN];
    snprintf(cmd, MAX_CMD_LEN, "AT+CWJAP=\"%s\",\"%s\"\r\n", ssid, password);
    AT_AddCommand(cmd, 0, 5000, huart, NULL);
}

2.3 Command Sending and Timeout Management

Use a timer or the main loop to check for timeouts and send commands:

void AT_ProcessQueue(void) {
    if (!cmd_queue || cmd_queue->cmd.state != IDLE) return;
    CmdNode *node = cmd_queue;
    node->cmd.state = PENDING;
    node->cmd.start_time = HAL_GetTick();
    HAL_UART_Transmit(node->cmd.huart, (uint8_t *)node->cmd.cmd, strlen(node->cmd.cmd), 100);
}

Timeout and retransmission:

void AT_CheckTimeout(void) {
    CmdNode *current = cmd_queue;
    while (current) {
        if (current->cmd.state == PENDING && (HAL_GetTick() - current->cmd.start_time) > current->cmd.timeout_ms) {
            if (current->cmd.retries > 0) {
                current->cmd.retries--;
                current->cmd.state = IDLE; // Retry
            } else {
                current->cmd.state = TIMEOUT;
            }
        }
        current = current->next;
    }
}

2.4 URC Message Capture

URC messages (e.g., “+IPD”) are received via UART interrupts and stored in a buffer:

#define RX_BUFFER_SIZE 256
char rx_buffer[RX_BUFFER_SIZE];
uint16_t rx_index = 0;

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    static uint8_t rx_char;
    if (rx_index < RX_BUFFER_SIZE - 1) {
        HAL_UART_Receive_IT(huart, &rx_char, 1);
        rx_buffer[rx_index++] = rx_char;
        if (rx_char == '\n') { // Assume URC ends with newline
            rx_buffer[rx_index] = '\0';
            AT_ProcessURC(rx_buffer, huart);
            rx_index = 0;
        }
    } else {
        rx_index = 0; // Prevent overflow
    }
}

void AT_ProcessURC(char *data, UART_HandleTypeDef *huart) {
    if (strstr(data, "+IPD")) {
        // Process URC, e.g., parse network data
    }
    // Check if it is a command response
    if (cmd_queue && cmd_queue->cmd.huart == huart && strstr(data, "OK")) {
        if (cmd_queue->cmd.callback) cmd_queue->cmd.callback(data);
        cmd_queue->cmd.state = SUCCESS;
        CmdNode *temp = cmd_queue;
        cmd_queue = cmd_queue->next;
        free(temp);
        used_memory -= sizeof(CmdNode);
    }
}

3. Multi-Device Communication Management

Support for multiple devices is distinguished by different UART instances, for example, UART1 connects to ESP8266, and UART2 connects to SIM800. During initialization, interrupts are enabled for each device:

void AT_Init(void) {
    HAL_UART_Receive_IT(&huart1, &rx_char, 1); // ESP8266
    HAL_UART_Receive_IT(&huart2, &rx_char, 1); // SIM800
}

When sending commands, the target device is specified through the huart parameter, and during URC processing, the source is distinguished based on huart.

4. Memory Monitoring and Limitations

Memory usage is tracked through used_memory, limiting queue size and command length. Update memory count when freeing command nodes:

void AT_FreeCommand(CmdNode *node) {
    free(node);
    used_memory -= sizeof(CmdNode);
}

5. Command Lifecycle Management

Command states (CmdState) transition from IDLE to PENDING, SUCCESS, TIMEOUT, or ERROR, monitored in real-time:

void AT_MonitorCommands(void) {
    CmdNode *current = cmd_queue;
    while (current) {
        printf("Cmd: %s, State: %d, Retries: %d\n", current->cmd.cmd, current->cmd.state, current->cmd.retries);
        current = current->next;
    }
}

6. Command Passthrough

Supports direct sending of raw AT commands, bypassing the queue:

void AT_Passthrough(char *cmd, UART_HandleTypeDef *huart) {
    HAL_UART_Transmit(huart, (uint8_t *)cmd, strlen(cmd), 100);
}

7. Main Program Example

The main loop integrates the above functionalities:

int main(void) {
    HAL_Init();
    // Initialize clock, GPIO, UART, etc. (omitted)
    AT_Init();

    // Add example commands
    AT_AddCommand("AT\r\n", 1, 1000, &huart1, NULL);
    AT_SetWiFi("MyWiFi", "12345678", &huart1);

    while (1) {
        AT_ProcessQueue();
        AT_CheckTimeout();
        AT_MonitorCommands();
        HAL_Delay(10);
    }
}

8. Considerations and Optimizations

  1. Memory Management: Dynamic allocation should be done cautiously, ensuring memory is released to prevent leaks.
  2. Priority Scheduling: High-priority commands should be sent first, suitable for urgent tasks.
  3. URC Handling: Complex URCs require clearly defined parsing rules.
  4. Error Retransmission: Adjust retry counts and timeout durations based on module response speeds.
  5. Scalability: Command templates can be added to support more custom commands.

9. Conclusion

Based on the “moluo-tech/AT-Command” framework, STM32 has implemented a fully functional AT command processing system that supports asynchronous operations, various command types, URC capture, multi-device management, and other features. The code examples demonstrate how to achieve efficient communication in a bare-metal environment, suitable for real-time data interaction in IoT devices. This framework is lightweight, flexible, and can be extended to other communication modules, providing significant practical value.

Leave a Comment