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';

假设datavoid*类型(不能直接运算),但如果是其他类型:

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 后,失去了这个"意外"的延迟

官方例程通常有两种情况:

  1. 不进行即时验证:写完直接返回,下次读取时已经稳定

  2. 使用更可靠的等待机制:检查 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 设置和校验位双重问题

这个案例告诉我:

  1. 先检查最基本的串口参数(波特率、校验位、停止位)

  2. 用普通串口助手看原始数据(别依赖协议软件)

  3. 0x55 测试最管用(快速验证硬件)

  4. 当使用一个新软件,比如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指针实际位置
  1. DMA指针不会自动复位,只会按顺序移动

  2. 软件清空缓冲区不影响DMA指针位置

  3. 不重新启动DMA,指针会一直往前移动,直到1024后回绕

  4. 回绕时可能触发传输完成回调,如果你没处理,数据就会错乱

  5. 透传模式能工作是因为它每次读取数据后,DMA继续从当前位置写,没有破坏DMA的连续性

  6. 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+ENTMAT ❌ 不需要 只是查询状态
关闭心跳 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;

这里重启的目的

  1. 保存 AT+HEARTEN=OFF 的配置(关闭心跳)

  2. 保存 AT+SOCKA=TCP,bemfa.com,9501 的服务器配置

  3. 让这些配置在模块重启后自动生效


✅ 总结:哪些情况需要 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

四、经验教训

  1. 先确认基础通信:用 AT 测试模块是否响应

  2. 区分 AT 模式和透传模式:指令加前缀

  3. HTTP/1.1 必须带 Host 头:协议规范

  4. 切换协议必须断开旧连接:避免冲突

  5. 保持代码简单:不要过度设计,能用就行

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐