DTU开发总结
1、Flash_Save函数 - 通用写入函数
HAL_StatusTypeDef Flash_Save(uint32_t addr, void *data, uint32_t size)
{
HAL_StatusTypeDef status; // 定义状态变量
uint32_t page_start = addr & ~(MY_FLASH_PAGE_SIZE - 1); // 计算页起始地址
-
~(MY_FLASH_PAGE_SIZE - 1):创建页对齐掩码 -
例如:2KB=2048,减1得2047,取反后高位为1,低位为0
-
addr & 掩码:得到页对齐的起始地址 -
工作原理
-
MY_FLASH_PAGE_SIZE - 1:创建一个掩码-
假设页大小为 2048 字节(0x800)
-
MY_FLASH_PAGE_SIZE - 1 = 2047 = 0x7FF -
这个值的二进制是低 11 位全为 1,高位全为 0
-
-
~(MY_FLASH_PAGE_SIZE - 1):取反得到页对齐掩码-
~0x7FF = 0xFFFFF800(32位系统) -
这个掩码的低 11 位全为 0,高位全为 1
-
-
addr & 掩码:清除地址的低位,得到页起始地址
| 特性 | 1024字节 (1KB) | 2048字节 (2KB) | 差异影响 |
|---|---|---|---|
| 二进制表示 | 0x400 | 0x800 | 2KB的地址线少1位 |
| 掩码值 | ~(1024-1)=0xFFFFFC00 | ~(2048-1)=0xFFFFF800 | 2KB对齐要求更低 |
| 对齐要求 | 地址低10位为0 | 地址低11位为0 | 2KB更宽松 |
1、掩码计算对比
// 1024字节页 (1KB)
#define PAGE_SIZE_1KB 1024
#define PAGE_MASK_1KB (~(PAGE_SIZE_1KB - 1)) // ~1023 = 0xFFFFFC00
// 2048字节页 (2KB)
#define PAGE_SIZE_2KB 2048
#define PAGE_MASK_2KB (~(PAGE_SIZE_2KB - 1)) // ~2047 = 0xFFFFF800
2KB页 (2048字节) 地址空间划分:
0x00000000 - 0x000007FF: 第0页 (偏移 0-2047)
0x00000800 - 0x00000FFF: 第1页 (偏移 0-2047)
0x00001000 - 0x000017FF: 第2页 (偏移 0-2047)
0x00001800 - 0x00001FFF: 第3页 (偏移 0-2047)
0x00002000 - 0x000027FF: 第4页 (偏移 0-2047)
...
1KB页 (1024字节) 地址空间划分:
0x00000000 - 0x000003FF: 第0页 (偏移 0-1023)
0x00000400 - 0x000007FF: 第1页 (偏移 0-1023)
0x00000800 - 0x00000BFF: 第2页 (偏移 0-1023)
0x00000C00 - 0x00000FFF: 第3页 (偏移 0-1023)
0x00001000 - 0x000013FF: 第4页 (偏移 0-1023)
...
-
0xFFFFF800:确保结果的低11位都是0 → 地址只能是 0xXXX...000(每2KB的起始) -
0xFFFFFC00:确保结果的低10位都是0 → 地址只能是 0xXXX...00(每1KB的起始)
这些掩码就像"筛子",只保留页号部分,筛掉页内偏移,自然就得到了每个页的起始地址!
uint32_t *src = (uint32_t*)data; // 将数据指针转为32位指针
uint32_t words = (size + 3) / 4; // 计算需要写入的32位字数
-
(size + 3) / 4:向上取整到4的倍数 -
例如:size=184,(184+3)/4=187/4=46.75→47字
-
结构体对齐:由编译器根据最大成员和
#pragma pack决定,目的是内存访问效率 -
Flash对齐:由硬件强制,是最小编程单元,必须遵守
关键点:即使结构体是1字节对齐,写入Flash时也必须转换成4字节对齐的缓冲区!这就是为什么代码中会有uint32_t *src = (uint32_t*)data;和(size+3)/4这样的写法。
-
size是结构体的实际字节大小(184字节) -
words是向上取整后的字数(47字),因为Flash必须按32位写入 -
实际写入的物理空间是
words * 4 = 188字节
| 概念 | 计算方式 | 用途 |
|---|---|---|
| 数据大小 (size) | 结构体实际字节数 | 检查数据是否跨页 |
| 写入大小 (words*4) | 向上取整到4的倍数 | 确定要写多少个Flash字 |
// 检查是否跨页
if(addr + size > page_start + MY_FLASH_PAGE_SIZE) {
return HAL_ERROR; // 跨页则返回错误
}
2、Flash操作 - 擦除
HAL_FLASH_Unlock(); // 解锁Flash,允许写入
FLASH_EraseInitTypeDef erase = { // 定义擦除结构体并初始化
.TypeErase = FLASH_TYPEERASE_PAGES, // 擦除类型:页擦除
.PageAddress = page_start, // 擦除的页起始地址
.NbPages = 1 // 擦除1页
};
uint32_t page_error; // 用于返回错误的页
status = HAL_FLASHEx_Erase(&erase, &page_error); // 执行擦除
if(status != HAL_OK || page_error != 0xFFFFFFFF) { // 检查是否成功
HAL_FLASH_Lock(); // 失败则重新锁定
return HAL_ERROR;
}
while(__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY)); // 等待Flash空闲
-
Flash控制器默认是锁定的,防止误操作
-
必须先解锁才能进行擦除/编程操作
-
.TypeErase:可选择页擦除(FLASH_TYPEERASE_PAGES)或块擦除(FLASH_TYPEERASE_MASSERASE) -
.PageAddress:使用之前计算的页起始地址 -
.NbPages:要擦除的页数量
-
HAL_FLASHEx_Erase():执行页擦除 -
page_error:如果擦除失败,返回出错页的地址 -
检查点:
-
status != HAL_OK:操作失败 -
page_error != 0xFFFFFFFF:某个页擦除失败(0xFFFFFFFF表示成功)
-
-
FLASH_FLAG_BSY:Flash忙标志 -
轮询等待直到擦除完成
3、Flash操作 - 写入
for(uint32_t i = 0; i < words; i++) { // 循环写入每个字
while(__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY)); // 等待Flash空闲
status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, // 写入一个字
addr + i*4, src[i]);
if(status != HAL_OK) { // 检查是否成功
HAL_FLASH_Lock();
return HAL_ERROR;
}
// 验证写入是否正确
if(*(uint32_t*)(addr + i*4) != src[i]) { // 读出比较
HAL_FLASH_Lock();
return HAL_ERROR;
}
}
HAL_FLASH_Lock(); // 锁定Flash
return HAL_OK; // 返回成功
}
-
每次写入一个字(4字节)
-
words是要写入的字数量
-
FLASH_TYPEPROGRAM_WORD:字编程模式(32位) -
参数:编程类型、目标地址、源数据
-
注意:地址必须字对齐(4字节对齐)
-
为什么要加延迟?
-
Flash编程后需要一定的恢复时间
-
某些STM32系列对连续编程有最小间隔要求
-
防止编程电压未稳定就进行下次操作
-
-
volatile关键字:防止编译器优化掉这个循环,用于告诉编译器:这个变量的值可能在程序控制之外被改变。
-
(char*)data:将指针转换为char*类型 -
[size-1]:以1字节为单位偏移 -
访问地址:
data + (size-1) * 1
1. 无强转的情况
c
data[size-1] = '\0';
假设data是void*类型(不能直接运算),但如果是其他类型:
c
uint32_t *data; // 假设 data 是 uint32_t* 类型 data[size-1] = '\0'; // 以4字节为单位偏移!
2、参数重定义问题
当你在其他函数
#include "cjson_extract.h"
LTE_Config_t lte_config;
#ifndef __CJSON_EXTRACT_H
#define __CJSON_EXTRACT_H
#include "stm32f1xx_hal.h"
#include "cJSON.h"
#include <string.h>
#include <stdio.h>
// 4G模块需要的配置结构体
typedef struct {
char server_ip[128]; // 华为云域名
uint16_t server_port; // 端口 1883
char client_id[128]; // Client ID
char username[128]; // 用户名
char password[256]; // 密码
char pub_topic[256]; // 发布主题(属性上报)
char sub_topic[256]; // 订阅主题(命令下发)
} LTE_Config_t;
extern LTE_Config_t lte_config;
main.c中
// 先测试一个硬编码的JSON,看看解析函数是否工作
LTE_Config_t test_config;
LTE_Config_t lte_config;
重新定义
flash_config.h中定义了
当你在其他函数定义了,并外部可调用,又在其他函数重新定义了,编译器不会报错,运行时程序会进入
void HardFault_Handler(void)
{
/* USER CODE BEGIN HardFault_IRQn 0 */
/* USER CODE END HardFault_IRQn 0 */
while (1)
{
/* USER CODE BEGIN W1_HardFault_IRQn 0 */
/* USER CODE END W1_HardFault_IRQn 0 */
}
}
错误死循环。
3.CJSON文件使用注意事项
启动文件修改
原
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
新
Stack_Size EQU 0x00001000 ; 4KB
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
Heap_Size EQU 0x00002000 ; 8KB
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
对于复杂的cjson解析
堆大小只有512字节(0x200),这对于cJSON解析来说严重不足!
问题所在
-
Heap_Size EQU 0x200= 512字节 -
cJSON解析一个410字节的JSON,需要分配多个内存块:
-
根对象结构体
-
每个字段的结构体
-
字符串的副本
-
等等
-
512字节的堆绝对不够!
为什么之前cJSON库测试能通过?
简单的测试 {"a":1} 只需要很少的内存,所以512字节的堆勉强够用。但您的完整JSON(410字节)需要更多内存,所以会失败。
增加堆大小后,cJSON解析应该就能正常工作了!
4.h文件中声明的形参没有头文件
如果你的头文件全放在.c文件中,而你的.h文件的函数声明形参用到了,.c文件中头文件,就会报无法找到
.c
#include "flash_config.h"
#include <string.h>
#include <stdio.h>
#include "cjson_extract.h"
.h
#include "stm32f1xx_hal.h"
HAL_StatusTypeDef Flash_SaveConfig(LTE_Config_t *data);
void Flash_ReadStruct(LTE_Config_t *data);
//LTE_Config_t 在cjson_extract中定义就会报LTE_Config_t 找不到的错误
修改
.c
#include "flash_config.h"
.h
#include "stm32f1xx_hal.h"
#include <string.h>
#include <stdio.h>
#include "cjson_extract.h"
HAL_StatusTypeDef Flash_SaveConfig(LTE_Config_t *data);
void Flash_ReadStruct(LTE_Config_t *data);
5、FLAH存储读取稳定问题
// ============ 通用Flash写入函数 ============
HAL_StatusTypeDef Flash_Save(uint32_t addr, void *data, uint32_t size)
{
HAL_StatusTypeDef status;
uint32_t page_start = addr & ~(MY_FLASH_PAGE_SIZE - 1); // 计算页起始地址
uint32_t *src = (uint32_t*)data;
uint32_t words = (size + 3) / 4;
printf("\r\n=== Flash保存 ===\r\n");
printf("地址:0x%08X, 大小:%d字节, 页大小:%d\r\n", addr, size, MY_FLASH_PAGE_SIZE);
if(addr + size > page_start + MY_FLASH_PAGE_SIZE) {
printf("错误:数据跨页,不支持\r\n");
return HAL_ERROR;
}
HAL_FLASH_Unlock();
FLASH_EraseInitTypeDef erase = {
.TypeErase = FLASH_TYPEERASE_PAGES,
.PageAddress = page_start,
.NbPages = 1
};
uint32_t page_error;
status = HAL_FLASHEx_Erase(&erase, &page_error);
if(status != HAL_OK || page_error != 0xFFFFFFFF) {
// printf("擦除失败\r\n");
HAL_FLASH_Lock();
return HAL_ERROR;
}
while(__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY));
for(uint32_t i = 0; i < words; i++) {
while(__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY));
status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,
addr + i*4, src[i]);
if(status != HAL_OK) {
printf("写入失败 at offset %d\r\n", i*4);
HAL_FLASH_Lock();
return HAL_ERROR;
}
if(*(uint32_t*)(addr + i*4) != src[i]) {
printf("验证失败\r\n");
HAL_FLASH_Lock();
return HAL_ERROR;
}
}
HAL_FLASH_Lock();
printf("保存成功\r\n");
return HAL_OK;
}
此代码保留printf代码打印可以正常实现功能,而如果把printf全注释掉则不能工作
// ============ 通用Flash写入函数 ============
HAL_StatusTypeDef Flash_Save(uint32_t addr, void *data, uint32_t size)
{
HAL_StatusTypeDef status;
uint32_t page_start = addr & ~(MY_FLASH_PAGE_SIZE - 1); // 计算页起始地址
uint32_t *src = (uint32_t*)data;
uint32_t words = (size + 3) / 4;
// printf("\r\n=== Flash保存 ===\r\n");
// printf("地址:0x%08X, 大小:%d字节, 页大小:%d\r\n", addr, size, MY_FLASH_PAGE_SIZE);
if(addr + size > page_start + MY_FLASH_PAGE_SIZE) {
// printf("错误:数据跨页,不支持\r\n");
return HAL_ERROR;
}
HAL_FLASH_Unlock();
FLASH_EraseInitTypeDef erase = {
.TypeErase = FLASH_TYPEERASE_PAGES,
.PageAddress = page_start,
.NbPages = 1
};
uint32_t page_error;
status = HAL_FLASHEx_Erase(&erase, &page_error);
if(status != HAL_OK || page_error != 0xFFFFFFFF) {
// printf("擦除失败\r\n");
HAL_FLASH_Lock();
return HAL_ERROR;
}
while(__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY));
for(uint32_t i = 0; i < words; i++) {
while(__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY));
status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,
addr + i*4, src[i]);
if(status != HAL_OK) {
HAL_FLASH_Lock();
return HAL_ERROR;
}
// 等待 BSY 清除
while(__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY));
// 关键:添加微秒级延迟
for(volatile int delay = 0; delay < 100; delay++); // 约几微秒
// 或者使用更精确的微秒延迟
// HAL_Delay(1); // 如果不在乎 1ms 延迟
// 验证读取
if(*(uint32_t*)(addr + i*4) != src[i]) {
HAL_FLASH_Lock();
return HAL_ERROR;
}
}
HAL_FLASH_Lock();
// printf("保存成功\r\n");
return HAL_OK;
}
这个代码修复了这个问题。
根本原因:Flash 读取的"稳定时间"问题
STM32F103 的 Flash 在编程完成后,并不是立即就能稳定读取的。虽然硬件标志 BSY 表示编程操作已完成,但 Flash 存储器内部还需要一个极短的稳定时间(settling time)才能保证读取的数据正确。
时序图理解:
编程命令 → Flash开始编程 → BSY=1 → 编程完成 → BSY=0 → Flash内部稳定 → 数据可稳定读取
|____________编程时间(40-50μs)__________|____稳定时间(几μs)___|
↑
验证读取点
volatile 关键字的作用
for(volatile int delay = 0; delay < 100; delay++);
-
volatile告诉编译器:这个变量可能被意外修改,不要优化这个循环 -
确保循环真实执行,产生实际的延迟时间
-
如果没有
volatile,编译器可能直接优化掉整个循环
2. 延迟循环提供了稳定时间
delay < 100 的循环大约产生几微秒的延迟,正好给了 Flash 内部电路稳定下来的时间:
// 编程完成
while(__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY)); // BSY=0,编程刚完成
// Flash 内部电荷泵还在稳定,存储单元电压未完全稳定
for(volatile int delay = 0; delay < 100; delay++); // 等待几微秒
// Flash 完全稳定,可以正确读取
if(*(uint32_t*)(addr + i*4) != src[i]) { // 验证成功
3. 为什么 printf 之前能工作?
printf("写入失败 at offset %d\r\n", i*4); // 这个函数执行需要几毫秒
-
串口输出需要毫秒级的时间(115200波特率下,每个字符约87μs)
-
这提供了充足的稳定时间(几毫秒,远超需要的几微秒)
-
所以注释掉
printf后,失去了这个"意外"的延迟
官方例程通常有两种情况:
-
不进行即时验证:写完直接返回,下次读取时已经稳定
-
使用更可靠的等待机制:检查
BSY后再次检查EOP(End of Programming)标志
6、串口匹配问题(长记性)
从 CRC计算 → RS485硬件 → 发送函数 → DMA接收 → 空闲中断 → 10ms超时 → 校验位问题...
一路排查下来,最后居然是偶校验
排查之路:
1. ❌ 发送 01 03 00 00 00 03 05 CB
2. ❌ 收到 01 20 00 03 41 F9
3. ✅ 发现 0x55 测试正确(硬件没问题)
4. ✅ 串口助手显示正确(数据没问题)
5. 🎯 最后发现:偶校验没对上!
| 问题现象 | 真正原因 |
|---|---|
| 03 变成 20 | 校验位不匹配导致字节错位 |
| 数据总差一点 | 无校验 vs 偶校验 |
| Modbus Slave 显示乱码 | Mode 设置和校验位双重问题 |
这个案例告诉我:
-
先检查最基本的串口参数(波特率、校验位、停止位)
-
用普通串口助手看原始数据(别依赖协议软件)
-
0x55 测试最管用(快速验证硬件)
-
当使用一个新软件,比如modbus串口软件,不要只确定端口号波特率,还要确认CRC和校验位是否一致,不要想当然的认为校验位是无校验导致数据解析错误。
7、Modbus模式和透传模式切换,modbus模式失效
当使用复杂的状态机转换,为了提高效率,把延时编程非阻塞延时,当Modbus模式发送数据后本来等待一段时间再接收数据,确保一定能收到数据,再执行其他程序。确保这一次数据能完整发送接收和解析。而用了非阻塞状态机后,不排除在发送数据后,在等待接收的非阻塞延时时触发模式切换,就会导致接收到的数据在缓冲区累计。猜测这就是为什么刚开始工作模式是modbus,切换成透传模式(仅做简单的接收转发给发布函数)可以工作,再切换回modbus模式后无法正常触发空闲中断和置标志位,导致DMA空闲中断失效。但再切换回透传模式就可以正常工作。
1.DMA的循环模式特性
// DMA配置通常是这样的 hdma_usart2_rx.Init.Mode = DMA_CIRCULAR; // 循环模式
关键点:DMA在循环模式下,当传输完成时,会自动重置地址继续接收,但某些标志位可能保留。
场景1:Modbus模式正常时
c
DMA接收 → 空闲中断 → flag=1 → Modbus解析 → flag=0 ↑ ↓ └──────────────────────┘
场景2:Modbus模式异常时
c
DMA接收 → 空闲中断 → flag=1 → CRC错误 → 只清flag,没清DMA
↓
DMA指针停在错误位置
↓
下次无法触发空闲中断
场景3:切换到透传模式
c
透传模式启动 → 重新初始化?没有!
↓
DMA继续从错误位置接收
↓
收到数据 → flag=1 → 透传处理 → 清flag
↓
DMA指针被"拉回"正常位置
↓
下次能正常触发空闲中断
2、DMA工作原理示意图
// 初始状态
DMA指针 → [________________________] 缓冲区起始位置
↑
写入位置 = 0
// 收到10字节后
DMA指针 → [XXXXXXXXXX______________]
↑
写入位置 = 10
// 收到50字节后
DMA指针 → [XXXXXXXXXXXXXXXXXXXXXXXX]
↑
写入位置 = 60
// 收到1024字节后(缓冲区满了)
DMA指针 → [XXXXXXXXXXXXXXXXXXXXXXXX]
↑
写入位置 = 0 (自动回绕!)
触发传输完成回调
3、DMA指针不会自动复位
usart2_data.flag = 0;
memset(usart2_data.buff,0,1024); // 只清了软件缓冲区
usart2_data.len = 0;
// DMA硬件指针还在这里 ↓
// [____数据____]________________]
// ↑
// DMA指针实际位置
-
DMA指针不会自动复位,只会按顺序移动
-
软件清空缓冲区不影响DMA指针位置
-
不重新启动DMA,指针会一直往前移动,直到1024后回绕
-
回绕时可能触发传输完成回调,如果你没处理,数据就会错乱
-
透传模式能工作是因为它每次读取数据后,DMA继续从当前位置写,没有破坏DMA的连续性
-
Modbus模式卡死是因为解析失败时,破坏了这种连续性,又没有重新启动DMA
解决方案核心:每次空闲中断后,或者在模式切换时,主动重新启动DMA,让指针回到起始位置!这就是为什么每次空闲中断后要重新启动DMA接收的原因,DMA指针置位。
具体没尝试解决,大不了重启串口也能强制复位接收。采用的阻塞延时发送后必须等待接受完才执行其他内容的笨方法,绕过了这个问题。所以只提供了解决思路。
8、HTTP协议
1、独立完整的 HTTP 上报函数
void BEMFA_HTTP_Report(const char *msg)
{
char http_request[512];
int len;
// ========== 1. 确保在 AT 指令模式 ==========
// 发送 +++ 退出任何透传模式
HAL_UART_Transmit(&huart3, (uint8_t*)"+++", 3, 100);
HAL_Delay(500);
// 清空缓冲区
memset(usart3_data.buff, 0, 1024);
usart3_data.len = 0;
usart3_data.flag = 0;
// ========== 2. 关闭可能的旧连接 ==========
HAL_UART_Transmit(&huart3, (uint8_t*)"AT+IPCLOSE\r\n",
strlen("AT+IPCLOSE\r\n"), 100);
HAL_Delay(1000);
// ========== 3. 建立 TCP 连接 ==========
SetSeverInfomation((uint8_t*)"apis.bemfa.com", 80);
HAL_Delay(2000);
// ========== 4. 进入透传模式 ==========
HAL_UART_Transmit(&huart3, (uint8_t*)"AT+ENTM\r\n",
strlen("AT+ENTM\r\n"), 100);
HAL_Delay(500);
// ========== 5. 发送 HTTP 请求 ==========
len = sprintf(http_request,
"GET /va/sendMessage?uid=%s&topic=DTU&type=1&msg=%s HTTP/1.1\r\n"
"Host: apis.bemfa.com\r\n"
"Connection: close\r\n"
"\r\n",
lte_config.client_id, msg);
Usart3_Tx_Buff((uint8_t*)http_request, len);
// ========== 6. 等待响应 ==========
HAL_Delay(2000);
// ========== 7. 检查结果 ==========
if(strstr((char*)usart3_data.buff, "\"code\":0") != NULL) {
printf("✅ HTTP上报成功: %s\r\n", msg);
} else {
printf("❌ HTTP上报失败\r\n");
}
// ========== 8. 退出透传模式 ==========
HAL_UART_Transmit(&huart3, (uint8_t*)"+++", 3, 100);
HAL_Delay(500);
// ========== 9. 关闭连接 ==========
HAL_UART_Transmit(&huart3, (uint8_t*)"AT+IPCLOSE\r\n",
strlen("AT+IPCLOSE\r\n"), 100);
}
📊 调试历程与错误点
| 阶段 | 尝试 | 结果 | 错误原因 |
|---|---|---|---|
| 1 | SetSeverInfomation("bemfa.com", 80) |
❌ 超时 | 服务器地址错误 |
| 2 | SetSeverInfomation("apis.bemfa.com", 80) |
❌ 超时 | 未理解模块工作模式 |
| 3 | 模块 HTTPD 模式 | ❌ 400 | 缺少 Host 头 |
| 4 | 手动 HTTP + 透传模式 | ✅ 成功 | 完整请求 + 正确模式 |
🔍 关键知识点
1. 服务器地址
text
❌ bemfa.com:80 (MQTT 域名) ✅ apis.bemfa.com:80 (HTTP API 域名)
2. 模块的“双重模式”
text
模块状态:透传模式(一直保持)
↓
数据以 "usr.cn#AT" 开头 → 模块拦截执行
数据不以该前缀开头 → 透传到网络
3. HTTP/1.1 必须包含 Host 头
text
❌ 缺 Host → 400 Bad Request ✅ 有 Host → 200 OK
4. 为什么 MQTT 能通?
text
MQTT 报文首字节 0x10(不可打印)→ 不是 AT 指令 → 自动透传 HTTP 报文首字节 'G'(可打印)→ 需手动确保透传模式
5. AT+ENTM 的作用
-
Lte_Check()中已执行,模块一直处于透传模式 -
不需要每次 HTTP 前都发一次
6. 密码前缀 vs 无前缀
text
usr.cn#AT+xxx → 带密码,在透传模式下执行指令 AT+xxx → 无密码,模块也能识别执行 GET /... → 不是 AT 格式 → 透传到网络
7. AT+S 重启的作用
-
保存配置到 Flash
-
让修改的参数永久生效
-
MQTT 连接、订阅、发布不需要重启
✅ 成功关键点
| 序号 | 关键点 |
|---|---|
| 1 | 使用正确的 API 域名 apis.bemfa.com:80 |
| 2 | 确保模块在透传模式(Lte_Check 已处理) |
| 3 | HTTP 请求必须包含 Host 头 |
| 4 | 手动组装完整 HTTP 报文 |
| 5 | 用 AT+IPCLOSE 关闭旧连接 |
| 6 | 用 +++ 退出透传模式(可选) |
🎯 一句话总结
HTTP 失败是因为:1) 服务器地址用了 MQTT 的域名,2) 没加 Host 头,3) 没理解模块的透传机制。MQTT 能通是因为二进制数据自动透传,HTTP 需要手动确保透传模式并补全 Host 头。
📝 HTTP 请求头逐行解释
c
GET /va/sendMessage?uid=%s&topic=DTU&type=1&msg=%s HTTP/1.1\r\n Host: apis.bemfa.com\r\n User-Agent: WH-LTE-7S1\r\n Connection: close\r\n \r\n
🔍 逐行解析
1. 请求行
text
GET /va/sendMessage?uid=%s&topic=DTU&type=1&msg=%s HTTP/1.1\r\n
| 部分 | 含义 |
|---|---|
GET |
HTTP 方法,表示“获取资源” |
/va/sendMessage?... |
请求的路径(URL 去掉域名后的部分) |
?uid=xxx&topic=DTU&type=1&msg=on |
查询参数,把数据附加在 URL 上 |
HTTP/1.1 |
使用的 HTTP 协议版本 |
\r\n |
换行,表示这一行结束 |
2. Host 头
text
Host: apis.bemfa.com\r\n
| 部分 | 含义 |
|---|---|
Host |
指定要访问的服务器域名 |
apis.bemfa.com |
巴法云 HTTP API 服务器地址 |
为什么需要?
-
一台服务器可能托管多个网站(虚拟主机)
-
通过 Host 头区分你要访问哪个网站
-
HTTP/1.1 协议强制要求,缺失会返回 400 错误
3. User-Agent 头
text
User-Agent: WH-LTE-7S1\r\n
| 部分 | 含义 |
|---|---|
User-Agent |
标识客户端身份 |
WH-LTE-7S1 |
告诉服务器这是你的 4G 模块 |
作用:服务器日志中能看到是哪个设备在访问,便于调试和统计(可选,不加也能通)
4. Connection 头
text
Connection: close\r\n
| 部分 | 含义 |
|---|---|
Connection |
控制连接方式 |
close |
告诉服务器:发完响应后关闭连接 |
对比:
-
Connection: keep-alive→ 保持连接,后续请求复用 -
Connection: close→ 用完就断,节省服务器资源
5. 空行
text
\r\n
| 含义 |
|---|
| 表示 HTTP 头部结束 |
| 空行后面是消息体(GET 请求没有消息体) |
✅ 总结
| 部分 | 必须 | 作用 |
|---|---|---|
| 请求行 | ✅ | 告诉服务器做什么 |
| Host | ✅ | HTTP/1.1 强制要求 |
| User-Agent | ❌ | 标识客户端(可选) |
| Connection | ❌ | 控制连接方式(默认 close) |
| 空行 | ✅ | 标记头部结束 |
MQTT 操作分析
| 操作 | 指令 | 是否需要重启 | 原因 |
|---|---|---|---|
| 检查模块 | AT+ENTM、AT |
❌ 不需要 | 只是查询状态 |
| 关闭心跳 | AT+HEARTEN=OFF |
❌ 不需要 | 立即生效 |
| 设置服务器 | AT+SOCKA=TCP,... |
❌ 不需要 | 立即建立连接 |
| 重启模块 | AT+S |
✅ 需要 | 保存配置并重启 |
| MQTT 连接 | 发送 MQTT 报文 | ❌ 不需要 | 在透传模式下直接发送 |
| MQTT 订阅 | 发送 MQTT 报文 | ❌ 不需要 | 在透传模式下直接发送 |
| MQTT 发布 | 发送 MQTT 报文 | ❌ 不需要 | 在透传模式下直接发送 |
| MQTT 心跳 | 发送 MQTT 报文 | ❌ 不需要 | 在透传模式下直接发送 |
🎯 你的代码中 AT+S 的位置
c
case 3: // 4. 重启4G模组使配置生效
result = RestartLte(); // 发送 AT+S
if(result == 1) {
printf("4G模块重启中,等待5秒...\r\n");
step++;
HAL_Delay(5000);
}
break;
这里重启的目的:
-
保存
AT+HEARTEN=OFF的配置(关闭心跳) -
保存
AT+SOCKA=TCP,bemfa.com,9501的服务器配置 -
让这些配置在模块重启后自动生效
✅ 总结:哪些情况需要 AT+S
| 场景 | 是否需要 |
|---|---|
| 修改后需要长期保存 | ✅ 需要 AT+S |
| 只是临时连接 | ❌ 不需要 |
| MQTT 报文发送 | ❌ 不需要 |
| 查询状态 | ❌ 不需要 |
| 修改工作模式(NET/HTTPD) | ✅ 需要 |
| 修改服务器地址(想下次自动连) | ✅ 需要 |
📝 一句话总结
AT+S是用来“保存配置到 Flash,让模块下次开机自动生效”的。MQTT 的连接、订阅、发布、心跳都是临时操作,不需要保存,所以不需要AT+S。只有你希望模块重启后还记得这些设置时,才需要AT+S。
9.巴法云HTTP
证据确凿
| 测试 | 方法 | 路径 | 结果 |
|---|---|---|---|
| 你之前的测试 | GET | /va/sendMessage?uid=xxx&topic=DTU&type=1&msg=on |
✅ {"code":0,"message":"OK","data":0} |
| 刚刚的测试 | POST | /va/sendMessage + Body |
❌ 404 Not Found + "The incorrect API route" |
📊 巴法云官方文档
巴法云官方 API 文档明确写着:
| 接口 | 方法 | 说明 |
|---|---|---|
| 发送消息 | GET | /va/sendMessage?uid=xxx&topic=xxx&type=1&msg=xxx |
| 获取消息 | GET | /va/getmsg?uid=xxx&topic=xxx&type=1 |
| 图片上传 | POST | /upload/v1/upimages.php |
所以 /va/sendMessage 确实只支持 GET!
✅ 结论
-
✅ GET = 发送消息(支持)
-
❌ POST = 发送消息(不支持,返回 404)
10、串口发送JSON
发送端应该选择文本发送

要是选择HEX十六进制发送则接收乱码

JSON数据提取出来后存储内部Flash,结构体超页了也能写入,但在读出的时候会报硬件错误进死循环。
其中在对HTTP数据进行拼接时,其用JSON库生成的数据当拼接数据时,需要用紧凑型的,因为拼接的数据不能出现\r\n,否则就会把正常的GET的HTTP数据格式隔断。
/va/sendMessage?uid=3c688d5f9e8848418b139d0b14e80619&topic=DTU&type=1&msg=
len = sprintf(http_request,
"GET %s%s HTTP/1.1\r\n"
"Host: %s\r\n"
"Connection: close\r\n"
"\r\n",
lte_config.http.get_url,msg,
lte_config.server_ip);
printf("发送: %s", http_request);
Usart3_Tx_Buff((uint8_t*)http_request, len);
如果msg带\r\n就会把 "GET %s%s HTTP/1.1\r\n"这一行隔断,导致数据被破环。
11、HTTP总结
1.1 服务器地址错误
diff
- bemfa.com:9501 // MQTT 服务器 + apis.bemfa.com:80 // HTTP API 服务器
1.2 模块工作模式理解错误
| 数据特征 | 模块行为 | 结果 |
|---|---|---|
0x10(MQTT 首字节) |
自动透传 | ✅ MQTT 成功 |
'G'(HTTP 首字节) |
误判为 AT 指令 | ❌ HTTP 失败 |
1.3 AT 指令前缀规则
c
// 模块始终处于透传模式,执行 AT 指令必须加密码前缀 usr.cn#AT+xxx // ✅ 正确,在透传模式下执行 AT 指令 AT+xxx // ❌ 错误,会被当成网络数据透传
1.4 HTTP 协议格式错误
diff
- GET /path HTTP/1.1\r\n\r\n // 缺少 Host 头 + GET /path HTTP/1.1\r\n + Host: apis.bemfa.com\r\n // HTTP/1.1 必须 + Connection: close\r\n\r\n
1.5 协议切换时旧连接未断开
c
// MQTT 连接占用了 Socket,HTTP 无法建立新连接 AT+IPCLOSE // 关闭所有连接 AT+S // 重启模块,彻底断开旧连接
二、最终解决方案架构
2.1 模块状态机
text
┌─────────────────────────────────────────────────────────┐ │ 模块始终处于透传模式 │ ├─────────────────────────────────────────────────────────┤ │ 数据以 "usr.cn#AT" 开头 → 模块拦截执行 AT 指令 │ │ 其他数据 → 直接透传到网络 │ └─────────────────────────────────────────────────────────┘
2.2 切换协议流程
c
void Switch_To_HTTP(void) {
+++ // 1. 退出透传(可选)
usr.cn#AT+IPCLOSE // 2. 关闭所有连接
usr.cn#AT+HEARTEN=OFF // 3. 关闭心跳
usr.cn#AT+SOCKA=TCP,apis.bemfa.com,80 // 4. 设置 HTTP 连接
usr.cn#AT+S // 5. 保存并重启
// 等待 15 秒模块重启完成
}
2.3 HTTP 上报流程
c
void HTTP_Report(void) {
usr.cn#AT+IPCLOSE // 1. 关闭旧连接
usr.cn#AT+SOCKA=TCP,apis.bemfa.com,80 // 2. 建立连接
// 3. 直接发送 HTTP 请求(不加 AT+ENTM)
GET /va/sendMessage?uid=xxx&msg=66.6 HTTP/1.1
Host: apis.bemfa.com
Connection: close
// 4. 等待响应
// 5. 检查 {"code":0}
}
三、调试过程中的关键发现
| 现象 | 真正原因 | 解决方法 |
|---|---|---|
响应 usr.cn# OK |
模块在 AT 模式 | 数据被当 AT 指令处理 |
响应 a |
连接的是 UDP 端口 | 确认端口是 80 |
响应 400 Bad Request |
缺少 Host 头 | 手动添加 Host 头 |
| 响应空 | 未进入透传模式 | 不加 AT+ENTM |
| 切换后仍连 MQTT | 旧连接未断开 | AT+IPCLOSE + AT+S |
| 时好时坏 | 模块状态不一致 | 统一用 usr.cn# 前缀 |
| 指令类型 | 是否需要 usr.cn# |
原因 |
|---|---|---|
| AT+S (重启) | ✅ 需要 | 在透传模式下执行 |
| AT+IPCLOSE | ✅ 需要 | 在透传模式下执行 |
| AT+SOCKA=... | ✅ 需要 | 在透传模式下执行 |
| AT+HEARTEN=OFF | ✅ 需要 | 在透传模式下执行 |
| AT+ENTM | ❌ 不需要 | 特殊指令 |
| +++ | ❌ 不需要 | 退出透传 |
模块始终处于透传模式,所有 AT 指令必须加 usr.cn# 前缀;HTTP 上报不需要 AT+ENTM,直接发完整请求(含 Host 头);切换协议必须执行 AT+IPCLOSE 和 AT+S 重启;服务器用 apis.bemfa.com:80。
四、经验教训
-
先确认基础通信:用
AT测试模块是否响应 -
区分 AT 模式和透传模式:指令加前缀
-
HTTP/1.1 必须带 Host 头:协议规范
-
切换协议必须断开旧连接:避免冲突
-
保持代码简单:不要过度设计,能用就行
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)