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
- Memory Management: Dynamic allocation should be done cautiously, ensuring memory is released to prevent leaks.
- Priority Scheduling: High-priority commands should be sent first, suitable for urgent tasks.
- URC Handling: Complex URCs require clearly defined parsing rules.
- Error Retransmission: Adjust retry counts and timeout durations based on module response speeds.
- 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.