1. STM32F03C8T6通过AT指令获取天气API实战解析
作为一名长期从事嵌入式开发的工程师,我最近在STM32F03C8T6上实现通过AT指令获取天气API的功能时踩了不少坑。这个看似简单的任务实际上涉及硬件通信、数据解析和嵌入式编程的多个关键点。本文将详细分享我从失败到成功的完整调试过程,特别适合正在学习STM32网络通信开发的同行参考。
2. 项目背景与硬件环境搭建
2.1 硬件选型与连接
我使用的硬件配置如下:
- 主控芯片:STM32F03C8T6(Cortex-M0内核,64KB Flash,8KB RAM)
- WiFi模块:ESP8266-01S(支持AT指令集)
- 开发环境:Keil MDK-ARM 5.30
- 串口工具:USB-TTL转换器(CH340G芯片)
硬件连接示意图:
code复制STM32F03C8T6 <--> ESP8266-01S
PA9(TX) <--> URXD
PA10(RX) <--> UTXD
3.3V <--> VCC
GND <--> GND
注意:ESP8266模块对电源质量敏感,务必确保3.3V电源稳定,建议在VCC和GND之间并联100μF和0.1μF电容各一个。
2.2 软件框架设计
整个系统的工作流程分为三个主要阶段:
- 初始化阶段:配置USART串口、初始化ESP8266模块
- 网络通信阶段:通过AT指令连接WiFi和服务器
- 数据处理阶段:接收并解析天气API返回的JSON数据
3. AT指令通信实现细节
3.1 基础AT指令封装
首先需要实现基本的AT指令发送和接收功能。我封装了以下核心函数:
c复制#define ESP8266_RX_BUFFER_SIZE 1024
char ESP8266_RxBuffer[ESP8266_RX_BUFFER_SIZE];
void ESP8266_SendCmd(const char *cmd) {
USART_SendString(USART1, cmd);
USART_SendString(USART1, "\r\n");
}
uint8_t ESP8266_WaitResponse(const char *expect, uint16_t timeout) {
uint32_t start = HAL_GetTick();
uint16_t len = 0;
while((HAL_GetTick() - start) < timeout) {
if(USART_ReceiveAvailable(USART1)) {
char c = USART_ReceiveChar(USART1);
if(len < ESP8266_RX_BUFFER_SIZE - 1) {
ESP8266_RxBuffer[len++] = c;
ESP8266_RxBuffer[len] = '\0';
if(strstr(ESP8266_RxBuffer, expect)) {
return 1; // 成功匹配
}
}
}
}
return 0; // 超时未匹配
}
3.2 HTTP请求构造与发送
获取天气数据的核心是构造正确的HTTP请求。我使用的是心知天气的API,请求格式如下:
c复制void Weather_GetData(const char *city) {
char cmd[256];
sprintf(cmd, "AT+CIPSTART=\"TCP\",\"api.seniverse.com\",80");
ESP8266_SendCmd(cmd);
if(!ESP8266_WaitResponse("OK", 2000)) {
printf("[Error] Connection failed\r\n");
return;
}
// 计算HTTP请求长度
const char *api_key = "your_api_key_here";
char http_request[512];
sprintf(http_request,
"GET /v3/weather/now.json?key=%s&location=%s&language=en&unit=c HTTP/1.1\r\n"
"Host: api.seniverse.com\r\n"
"Connection: close\r\n"
"\r\n", api_key, city);
// 发送数据长度声明
sprintf(cmd, "AT+CIPSEND=%d", strlen(http_request));
ESP8266_SendCmd(cmd);
ESP8266_WaitResponse(">", 1000);
// 发送实际HTTP请求
USART_SendString(USART1, http_request);
ESP8266_WaitResponse("SEND OK", 2000);
}
4. JSON数据解析方案对比与优化
4.1 初始方案:cJSON库的问题
最初我尝试使用cJSON库来解析返回的JSON数据,但遇到了以下问题:
- 内存不足:STM32F03C8T6仅有8KB RAM,cJSON解析复杂JSON时容易耗尽内存
- 格式敏感:HTTP响应头与JSON混合时,cJSON无法直接解析
- 性能问题:解析过程耗时较长,影响系统实时性
典型错误现象:
c复制cJSON *root = cJSON_Parse(ESP8266_RxBuffer);
if(!root) {
printf("Parse error: %s\r\n", cJSON_GetErrorPtr());
// 输出:Parse error: before "path":"Beijing...
}
4.2 优化方案:轻量级字符串解析
最终我采用了直接字符串查找的方案,核心代码如下:
c复制typedef struct {
char city[32];
char weather[32];
char temperature[8];
char humidity[8];
} WeatherData;
uint8_t Weather_ParseJSON(const char *json, WeatherData *data) {
// 确保包含有效数据
if(!strstr(json, "\"results\"")) return 0;
// 使用自定义的字符串提取函数
if(!GetStringBetween(json, "\"name\":\"", "\"", data->city, sizeof(data->city)))
return 0;
if(!GetStringBetween(json, "\"text\":\"", "\"", data->weather, sizeof(data->weather)))
return 0;
if(!GetStringBetween(json, "\"temperature\":\"", "\"", data->temperature, sizeof(data->temperature)))
return 0;
return 1;
}
static uint8_t GetStringBetween(char *src, char *start, char *end,
char *output, uint16_t maxLen) {
char *p_start = strstr(src, start);
if(!p_start) return 0;
p_start += strlen(start);
char *p_end = strstr(p_start, end);
if(!p_end) return 0;
uint16_t len = p_end - p_start;
if(len >= maxLen) len = maxLen - 1;
strncpy(output, p_start, len);
output[len] = '\0';
return 1;
}
5. 关键调试技巧与经验总结
5.1 串口调试最佳实践
- 分阶段调试:先确保AT指令通信正常,再处理HTTP,最后解析JSON
- 合理控制输出:避免在中断服务程序中打印大量数据
- 使用环形缓冲区:防止串口接收数据溢出
推荐调试代码结构:
c复制void USART1_IRQHandler(void) {
if(USART1->ISR & USART_ISR_RXNE) {
char c = USART1->RDR;
// 存入环形缓冲区
ring_buffer_put(&esp8266_rx_buf, c);
}
}
void Debug_PrintBuffer(const char *prefix, const char *buf, uint16_t len) {
printf("[%s] Length: %d\r\n", prefix, len);
for(uint16_t i = 0; i < len; i += 16) {
printf("%04X: ", i);
for(uint8_t j = 0; j < 16 && (i+j) < len; j++) {
printf("%02X ", buf[i+j]);
}
printf("\r\n");
}
}
5.2 内存优化技巧
- 合理分配缓冲区:根据实际需求调整接收缓冲区大小
- 避免内存碎片:尽量使用静态分配而非动态内存
- 关键数据校验:添加长度检查和边界保护
内存优化示例:
c复制#define MAX_HTTP_RESPONSE 1024
__align(4) static char http_buffer[MAX_HTTP_RESPONSE];
void Process_HTTP_Response() {
uint16_t len = ring_buffer_size(&esp8266_rx_buf);
if(len > MAX_HTTP_RESPONSE) len = MAX_HTTP_RESPONSE;
ring_buffer_get_multiple(&esp8266_rx_buf, http_buffer, len);
http_buffer[len] = '\0';
// 处理数据...
}
6. 完整实现流程示例
6.1 主程序工作流程
c复制int main(void) {
HAL_Init();
SystemClock_Config();
USART1_Init(115200);
ESP8266_Init();
WeatherData current_weather;
while(1) {
if(ESP8266_ConnectWiFi("your_ssid", "your_password")) {
printf("WiFi connected\r\n");
if(Weather_GetData("beijing")) {
if(Weather_ParseJSON(ESP8266_RxBuffer, ¤t_weather)) {
printf("\r\n=== Weather Report ===\r\n");
printf("City: %s\r\n", current_weather.city);
printf("Weather: %s\r\n", current_weather.weather);
printf("Temp: %s°C\r\n", current_weather.temperature);
}
}
}
HAL_Delay(60000); // 每分钟更新一次
}
}
6.2 典型输出结果
code复制[WiFi] Connecting to AP...
[WiFi] Connected with IP: 192.168.1.100
[HTTP] Connecting to api.seniverse.com...
[HTTP] Request sent (128 bytes)
[HTTP] Received 728 bytes response
[JSON] Parsing weather data...
=== Weather Report ===
City: Beijing
Weather: Clear
Temp: 23°C
Humidity: 45%
7. 常见问题与解决方案
7.1 连接不稳定问题
现象:ESP8266频繁断开连接或无法连接WiFi
解决方案:
- 检查电源质量,确保3.3V稳定
- 添加AT+RST指令进行模块复位
- 调整天线位置或添加外置天线
c复制void ESP8266_Reset() {
ESP8266_SendCmd("AT+RST");
HAL_Delay(1000);
ESP8266_WaitResponse("ready", 3000);
}
7.2 数据截断问题
现象:接收到的JSON数据不完整
解决方案:
- 增加接收缓冲区大小
- 实现流式数据接收
- 添加超时机制
c复制uint16_t ESP8266_ReceiveData(uint16_t timeout) {
uint32_t start = HAL_GetTick();
uint16_t len = 0;
while((HAL_GetTick() - start) < timeout) {
if(USART_ReceiveAvailable(USART1)) {
char c = USART_ReceiveChar(USART1);
if(len < ESP8266_RX_BUFFER_SIZE - 1) {
ESP8266_RxBuffer[len++] = c;
// 检查是否接收完成
if(strstr(ESP8266_RxBuffer, "+IPD") &&
strstr(ESP8266_RxBuffer, "CLOSED")) {
break;
}
}
}
}
ESP8266_RxBuffer[len] = '\0';
return len;
}
8. 性能优化进阶技巧
8.1 DMA传输优化
对于更高性能要求的应用,可以使用DMA进行串口数据传输:
c复制void USART1_DMA_Init() {
// 启用USART1和DMA时钟
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_DMA1_CLK_ENABLE();
// 配置DMA
hdma_usart1_rx.Instance = DMA1_Channel3;
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode = DMA_CIRCULAR;
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_usart1_rx);
// 关联DMA到USART
__HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);
HAL_UART_Receive_DMA(&huart1, (uint8_t*)ESP8266_RxBuffer, ESP8266_RX_BUFFER_SIZE);
}
8.2 低功耗优化
对于电池供电设备,可添加以下低功耗措施:
- 间隔采集模式(如每小时获取一次数据)
- 深度睡眠模式
- 动态时钟调整
c复制void Enter_LowPower_Mode() {
// 关闭外设时钟
__HAL_RCC_USART1_CLK_DISABLE();
__HAL_RCC_GPIOA_CLK_DISABLE();
// 配置唤醒源
HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1);
// 进入停止模式
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
// 唤醒后重新初始化系统
SystemClock_Config();
USART1_Init(115200);
}
通过这个项目,我深刻体会到在嵌入式开发中"简单即美"的哲学。相比追求复杂的通用解决方案,针对特定需求定制简单可靠的实现往往能获得更好的效果。希望我的这些经验教训能帮助其他开发者少走弯路。