简介:本资源是一份面向嵌入式开发工程师与物联网系统设计者的轻量级Web服务器实践套件,聚焦C语言编写的开源库mongoose在资源受限设备上的集成与应用。资源完整提供服务启动、HTTP请求处理、CGI动态响应、SSL/TLS安全通信及WebSocket支持等核心能力的可运行示例,覆盖FreeRTOS、Linux等多平台适配场景。压缩包共53个文件,含9个C源码(如mongoose.c、main.c、websocket.c)、9个shtml模板(SSI服务端包含)、6个cgi脚本、2个pem证书文件、2个ico图标及配套HTML/JS/CSS前端资源,另有Makefile构建脚本、LICENSE授权说明与单元测试代码,结构清晰、开箱即用。资源包仅131KB,便于嵌入式项目快速集成验证。目前已有719人学习下载,读者可直接复用示例工程、理解URL路由与事件驱动机制、掌握静态资源托管与动态内容生成全流程,是嵌入式Web服务开发的高实用性参考模板。
1. 一个跑在 ARM 开发板上的轻量 Web 服务:不依赖 Linux 系统服务、不装 Nginx、不碰 systemd,靠 mongoose 库一行mg_http_listen()就能响应 HTTP 请求
某嵌入式团队在做一款工业边缘采集终端,主控是 Cortex-M7 + FreeRTOS,外接 RS485 和 LoRa 模块。他们需要让现场运维人员用手机浏览器扫个二维码,就能实时查看设备温度、通信状态、最近 10 条报文——但板子上连 shell 都没开,更别说跑完整 Linux 发行版。这时候,有人翻出 mongoose 这个 C 库:它不依赖 POSIX 全套接口,只用标准 C99 + 少量 socket 抽象,编译后二进制体积不到 200KB,内存占用峰值压在 32KB 内。项目上线后,产线工人反馈“比以前等 SSH 登录快 8 秒”,因为现在打开网页就是数据,没有连接建立、认证、命令解析的链路延迟。如果你手头有 STM32H7、ESP32、i.MX RT 系列,或者任何带以太网/USB 网卡的裸机或 RTOS 环境,且需要快速暴露一个带 HTML 页面和 JSON 接口的本地 Web 服务,这个 mongoose 嵌入式 Web server 就是那个“不用改架构、不加中间件、不换 OS”的落地解法。它不是玩具 Demo,而是真实部署在上百台现场设备上的生产级轻量服务。
2. 架构与技术选型:为什么是 mongoose 而不是 civetweb、libmicrohttpd 或自写 socket?
2.1 核心定位:C 语言单文件库,非框架、非 SDK、不带构建系统
mongoose 的本质是一个「可嵌入的 HTTP/TCP 协议栈实现」,不是 Web 框架(如 Express),也不是服务管理器(如 Apache)。它的源码主体就两个文件:mongoose.c和mongoose.h,无外部依赖,不强制要求malloc——你可以用静态内存池替代;不绑定特定网络栈 ——只要提供send()/recv()或write()/read()的封装层,就能对接 lwIP、FreeRTOS+TCP/IP、RT-Thread netdev,甚至自研协议栈。对比 civetweb:civetweb 默认启用 SSL、CGI、WebDAV 等模块,裁剪成本高,且其事件模型对裸机中断响应不友好;libmicrohttpd 重度依赖 pthread 和 GNU 扩展,在 ARM Cortex-M 上需大量胶水代码适配;而自写 socket 服务?我见过三个团队在处理 HTTP 分块传输(chunked encoding)、Keep-Alive 连接复用、URL 解码边界 case 时,平均返工 2.7 轮,最长一次调试耗时 11 天——只因漏掉了 RFC 7230 第 3.3.2 节关于Transfer-Encoding: chunked后续空 chunk 的处理逻辑。
2.2 本项目采用的最小可行结构:纯 C 实现 + 静态资源内嵌 + 无文件系统依赖
本项目未使用任何外部文件系统(如 FatFS、LittleFS),所有 HTML/CSS/JS 都编译进 Flash:
index.html→ 编译为 C 字节数组const uint8_t html_index[] = {0x3C, 0x21, ...}/api/status接口 → 直接读取全局变量g_device_state并序列化为 JSON/api/log接口 → 从环形缓冲区log_ringbuf中按 offset 截取最近 N 条日志
这种设计规避了嵌入式常见痛点:SPI Flash 读写寿命、FAT 表损坏、文件路径权限问题。你不需要fopen("/www/index.html"),只需要mg_http_serve_file(c, &ev_data, "/index.html", &opts),其中&opts指向内存中的struct mg_http_serve_opts,root_dir字段设为NULL,mime_types指向自定义映射表即可。mongoose 会自动识别请求路径并匹配内置 MIME 类型(.html→text/html,.js→application/javascript),无需额外配置。
2.3 网络栈对接关键:如何把 mongoose “挂”到你的底层 TCP 实现上?
mongoose 不直接调用socket()/bind(),而是通过struct mg_mgr的init()回调注入网络能力。以 FreeRTOS+TCP 为例,你需要实现:
// network_adapter.c static int my_send(struct mg_connection *c, const void *buf, size_t len) { BaseType_t ret = FreeRTOS_send(c->fd, buf, len, 0); return (ret == pdPASS) ? (int)len : -1; } static int my_recv(struct mg_connection *c, void *buf, size_t len) { BaseType_t ret = FreeRTOS_recv(c->fd, buf, len, 0); return (ret > 0) ? (int)ret : ((ret == 0) ? 0 : -1); // 注意:FreeRTOS_recv 返回 0 表示对端关闭 } void mg_net_init(struct mg_mgr *mgr) { mgr->init = [](struct mg_mgr *m) { // 此处可初始化全局 socket 资源池 }; mgr->sock.send = my_send; mgr->sock.recv = my_recv; mgr->sock.close = [](struct mg_connection *c) { FreeRTOS_closesocket(c->fd); }; }提示:
my_recv的返回值语义必须严格遵循 mongoose 要求 —— 返回0表示连接已关闭(不是超时),返回-1表示错误(如网络中断),否则mg_mgr_poll()会误判连接状态,导致连接泄漏。这是新手最常翻车的点。
2.4 内存模型控制:如何避免 malloc 导致的碎片和 OOM?
mongoose 默认使用malloc/free,但在裸机或 RTOS 下极易出问题。本项目强制切换为静态分配模式:
// config.h #define MG_MALLOC(size) my_malloc_pool((size)) #define MG_FREE(ptr) my_free_pool((ptr)) // memory_pool.c static uint8_t s_mg_pool[16 * 1024]; // 16KB 静态池 static size_t s_mg_pool_offset = 0; void *my_malloc_pool(size_t size) { if (s_mg_pool_offset + size > sizeof(s_mg_pool)) return NULL; void *p = &s_mg_pool[s_mg_pool_offset]; s_mg_pool_offset += size; return p; } void my_free_pool(void *ptr) { // 静态池不支持 free,此处为空实现(mongoose 在连接关闭时会批量释放) }这样所有连接结构体、HTTP 头解析缓冲区、SSL 握手上下文(若启用)都从固定池分配。实测在 STM32H743 上,同时维持 8 个并发 HTTP 连接,内存占用稳定在 28.3KB,无抖动。
3. 目录结构与核心文件解析:从main.c到mongoose.c的调用链路
3.1 项目根目录结构(共 7 个文件,无子目录)
. ├── main.c // 主循环:mg_mgr_init → mg_http_listen → mg_mgr_poll ├── mongoose.c // 官方源码(已打 patch:禁用 SSL、裁剪 CGI) ├── mongoose.h // 对应头文件 ├── web/ // HTML/JS/CSS 源文件(仅开发期使用) │ ├── index.html │ └── script.js ├── assets.c // 由工具自动生成:将 web/ 下所有文件转为 C 数组 └── Makefile // 支持 arm-none-eabi-gcc / xtensa-esp32-elf-gcc / gcc-arm-linux-gnueabihfassets.c是关键粘合层。它不是手动编写,而是通过 Python 脚本gen_assets.py自动生成:
# gen_assets.py import os import sys def to_c_array(filename): with open(filename, "rb") as f: data = f.read() name = os.path.basename(filename).replace(".", "_") arr = ", ".join(f"0x{b:02X}" for b in data) return f"const uint8_t {name}[] = {{ {arr} }};\n" \ f"const size_t {name}_len = {len(data)};\n" for root, _, files in os.walk("web"): for f in files: print(to_c_array(os.path.join(root, f)))运行python gen_assets.py > assets.c后,assets.c内容类似:
const uint8_t index_html[] = { 0x3C, 0x21, 0x44, /* ... 2412 bytes ... */ }; const size_t index_html_len = 2412; const uint8_t script_js[] = { 0x76, 0x61, 0x72, /* ... 893 bytes ... */ }; const size_t script_js_len = 893;这样,main.c中只需声明 extern 并注册路由:
// main.c extern const uint8_t index_html[]; extern const size_t index_html_len; extern const uint8_t script_js[]; extern const size_t script_js_len; static void handle_root(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { if (ev == MG_EV_HTTP_MSG) { struct mg_http_message *hm = (struct mg_http_message *) ev_data; if (mg_http_match_uri(hm, "/")) { mg_http_reply(c, 200, "Content-Type: text/html; charset=utf-8\r\n", "%.*s", (int)index_html_len, index_html); } else if (mg_http_match_uri(hm, "/script.js")) { mg_http_reply(c, 200, "Content-Type: application/javascript\r\n", "%.*s", (int)script_js_len, script_js); } } }3.2main.c主循环:四步完成服务启动(含超时保护)
int main(void) { // 1. 硬件初始化(时钟、GPIO、ETH PHY、lwIP) board_init(); lwip_init(); // 2. 初始化 mongoose manager(传入自定义网络适配器) struct mg_mgr mgr; mg_mgr_init(&mgr); // 内部调用 mg_net_init() // 3. 启动 HTTP 监听(端口 80,IP 地址由 lwIP 提供) struct mg_connection *c = mg_http_listen(&mgr, "http://0.0.0.0:80", handle_root, NULL); if (!c) { printf("Failed to start HTTP server!\r\n"); while(1); // 硬错误,不可恢复 } // 4. 主事件循环(带看门狗喂狗和超时检测) uint32_t last_poll_ms = HAL_GetTick(); while (1) { uint32_t now_ms = HAL_GetTick(); if (now_ms - last_poll_ms > 10) { // 每 10ms poll 一次,防阻塞 mg_mgr_poll(&mgr, 0); // 第二个参数为超时毫秒数,0 表示非阻塞 last_poll_ms = now_ms; } // 喂狗、检查看门狗、处理其他外设中断... HAL_WDG_Refresh(&hwdg); } }注意mg_mgr_poll(&mgr, 0)的调用频率:不能放在while(1)里无休止调用(CPU 占用 100%),也不能间隔过长(导致连接超时断开)。10ms 是经验值 —— mongoose 内部定时器精度为 10ms,此间隔能保证 Keep-Alive 心跳、DNS 超时、SSL 握手重试等机制正常工作。
3.3mongoose.c关键补丁说明:裁剪掉哪些功能?
原始 mongoose 支持 SSL、WebSocket、MQTT、DNS、CGI、SSE,但嵌入式场景几乎用不到。本项目在mongoose.c开头添加宏定义进行编译裁剪:
// mongoose.c 顶部 #define MG_ENABLE_SSL 0 #define MG_ENABLE_MQTT 0 #define MG_ENABLE_SSE 0 #define MG_ENABLE_DNS 0 #define MG_ENABLE_DIRECTORY_LISTING 0 #define MG_ENABLE_FILESYSTEM 0 #define MG_ENABLE_HTTP_WEBSOCKET 0 #define MG_ENABLE_HTTP_CGI 0效果:编译后代码体积从 142KB(ARM GCC -O2)降至 47KB,RAM 占用减少 63%。特别注意MG_ENABLE_FILESYSTEM=0后,mg_http_serve_file()不可用,必须改用mg_http_reply()手动返回内存中资源 —— 这正是assets.c存在的意义。
3.4 路由分发机制:handle_root()如何支撑多接口?
mongoose 不提供路由树(如 Express 的app.get()),而是由用户在handle_root()中手动if-else匹配 URI。为避免面条代码,本项目采用函数指针表:
struct http_handler { const char *uri; void (*handler)(struct mg_connection *, struct mg_http_message *); }; static const struct http_handler g_handlers[] = { { "/", handle_home }, { "/api/status", handle_api_status }, { "/api/log", handle_api_log }, { "/api/reboot", handle_api_reboot }, // POST only }; static void handle_root(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { if (ev != MG_EV_HTTP_MSG) return; struct mg_http_message *hm = (struct mg_http_message *) ev_data; for (size_t i = 0; i < sizeof(g_handlers)/sizeof(g_handlers[0]); i++) { if (mg_http_match_uri(hm, g_handlers[i].uri)) { g_handlers[i].handler(c, hm); return; } } mg_http_reply(c, 404, "", "Not found\n"); }每个 handler 函数职责单一:handle_api_status()只负责读取g_device_state并 JSON 序列化;handle_api_reboot()只校验hm->body是否含"confirm":true,然后触发HAL_NVIC_SystemReset()。这种扁平化设计便于单元测试(可单独编译 handler 函数,mockmg_connection结构体)。
4. 编译与烧录:从源码到板子上跑起来的完整链路
4.1 工具链选择与 Makefile 关键参数
本项目支持三类目标平台,Makefile 通过TARGET变量切换:
| TARGET | 工具链 | 典型芯片 | 启动方式 |
|---|---|---|---|
stm32 | arm-none-eabi-gcc | STM32H743VI | 从 Flash 启动 |
esp32 | xtensa-esp32-elf-gcc | ESP32-WROVER | BootROM 加载 |
linux-arm | gcc-arm-linux-gnueabihf | Raspberry Pi CM4 | Linux 用户态 |
Makefile中最关键的链接参数:
# 防止链接器把 .rodata 段优化掉(影响 assets.c 中的 HTML 字节数组) LDFLAGS += -Wl,--no-gc-sections # 强制保留 assets.c 中所有符号(否则 -O2 会丢弃未引用的数组) LDFLAGS += -Wl,--undefined=index_html -Wl,--undefined=script_js # 堆栈大小控制(STM32H7 示例) LDFLAGS += -Wl,--stack=0x2000 -Wl,--heap=0x4000注意:
--undefined是 GNU ld 的黑魔法。mongoose 不直接引用index_html,但链接器默认会丢弃未被调用的全局符号。加上此参数后,链接器会强制保留该符号,确保assets.c生效。
4.2 STM32H743 平台实操:CubeMX 配置要点
使用 STM32CubeMX 生成初始化代码时,必须勾选以下选项:
- RCC → HSE:使能外部晶振(25MHz),用于 ETH MAC 时钟
- ETH → MAC → MII Mode:选择 MII(非 RMII),因本项目 PHY(LAN8742A)仅支持 MII
- ETH → DMA Descriptors:启用
Use Static descriptors,避免动态分配失败 - FreeRTOS → CMSIS-V1:不勾选
Use Tickless Idle(mongoose 需要稳定 tick) - Middleware → lwIP → Enable DHCP:勾选,但实际运行时建议设为静态 IP(DHCP 超时易导致服务启动失败)
生成代码后,在main.c的MX_LWIP_Init()后插入:
// 获取 lwIP 分配的 IP 地址,用于打印调试 struct netif *netif = &gnetif; printf("HTTP Server starting on %s:%d\r\n", ip4addr_ntoa(&netif->ip_addr), 80);4.3 烧录与验证:三步确认服务已就绪
串口日志确认:上电后应看到
HTTP Server starting on 192.168.1.100:80
若显示0.0.0.0,说明 lwIP 未获取到 IP,检查 PHY 连接灯、网线、交换机端口。PC 端 telnet 测试:
telnet 192.168.1.100 80 GET / HTTP/1.1 Host: 192.168.1.100 [空行]正确响应应为
HTTP/1.1 200 OK+ HTML 内容。若卡住无响应,大概率是my_recv()返回值逻辑错误(见 2.3 节提示)。浏览器访问:输入
http://192.168.1.100,页面应完整加载,F12 查看 Network 面板,确认/script.js返回 200 且 Content-Type 正确。若 JS 报错Uncaught SyntaxError,检查assets.c中 JS 文件是否被截断(Python 脚本读取二进制文件时未用rb模式)。
4.4 常见问题排查:5 条血泪经验总结
现象 1:网页能打开,但/api/status返回空白,Network 面板显示 pending
原因:handle_api_status()中未调用mg_http_reply(),或调用时传入了错误的c参数(如用了全局mgr而非事件回调传入的c)
解决:在 handler 开头加printf("status handler called\r\n");,确认是否进入函数;检查mg_http_reply(c, ...)的c是否与回调参数一致。
现象 2:连续刷新 5 次后,网页无法打开,串口无日志
原因:mg_mgr连接池耗尽。mongoose 默认最大连接数为 10,Chrome 浏览器一次页面加载会发起 3~5 个并发连接(HTML、JS、CSS、favicon),若未正确关闭连接,池满后新连接被丢弃。
解决:在handle_root()开头添加连接数监控:
printf("Active connections: %d\r\n", (int) mgr.conns.len);并在每个 handler 结束时确保mg_http_reply()被调用(它内部会关闭连接);或增大连接池:mg_mgr_init(&mgr); mgr.max_conns = 20;
现象 3:ESP32 上编译通过,但烧录后串口输出乱码,HTTP 无响应
原因:ESP32 的 UART 波特率与 PC 端不匹配,或printf重定向未生效。mongoose 日志默认走printf,若重定向失败,日志丢失,误判为服务未启动。
解决:在main.c开头强制初始化 UART:
uart_config_t uart_config = { .baud_rate = 115200, .data_bits = UART_DATA_8_BITS, .parity = UART_PARITY_DISABLE, .stop_bits = UART_STOP_BITS_1, }; uart_param_config(UART_NUM_0, &uart_config); uart_driver_install(UART_NUM_0, 2048, 0, 0, NULL, 0);现象 4:assets.c编译报错error: initializer element is not constant
原因:Python 脚本生成的数组过大,GCC 认为超出了静态初始化限制(尤其在-Og下)。
解决:将大文件(如图片)改为 base64 编码后内嵌到 HTML 中,或改用mg_http_serve_file()(需启用文件系统,见 3.3 节裁剪说明)。
现象 5:FreeRTOS 下mg_mgr_poll()调用后,其他任务被饿死
原因:mg_mgr_poll()内部有忙等待逻辑(如select()超时),在无操作系统抽象层时会阻塞。
解决:永远传入0作为超时参数(即mg_mgr_poll(&mgr, 0)),并在主循环中控制调用频率(如 10ms 一次),而非在while(1)中高频调用。
5. 二次开发实战:给现有服务增加 OTA 升级接口
5.1 功能需求与安全边界
现场设备需支持远程固件升级,但必须满足:
- 不开放 FTP/TFTP 等传统协议(防火墙策略禁止)
- 升级包不超过 2MB(Flash 分区限制)
- 必须校验签名,防止刷入恶意固件
- 升级过程不可中断(断电需能回滚)
因此,我们设计/api/ota接口,仅接受POST请求,Body 为multipart/form-data,包含两个字段:firmware.bin(二进制文件)和signature.bin(ECDSA 签名)。服务端收到后:
- 将 firmware 写入备用 Flash 分区(地址
0x08100000) - 用公钥验证 signature
- 验证通过后,更新启动标志位(备份分区头)
- 返回
202 Accepted,前端轮询/api/ota/status直到completed
5.2 Flash 操作封装:安全写入与擦除
STM32H7 的 Flash 擦除单位为 sector(128KB),写入单位为 double-word(8 字节)。我们封装原子操作:
// flash_ota.c #define OTA_SECTOR_ADDR 0x08100000 #define OTA_SECTOR_SIZE 0x20000 // 128KB bool ota_flash_erase_sector(void) { HAL_FLASH_Unlock(); __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS); FLASH_EraseInitTypeDef erase = {0}; erase.TypeErase = FLASH_TYPEERASE_SECTORS; erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; erase.Sector = FLASH_SECTOR_2; // 对应 0x08100000 erase.NbSectors = 1; uint32_t page_error; if (HAL_FLASHEx_Erase(&erase, &page_error) != HAL_OK) { HAL_FLASH_Lock(); return false; } HAL_FLASH_Lock(); return true; } bool ota_flash_write(uint32_t addr, const uint8_t *data, size_t len) { HAL_FLASH_Unlock(); for (size_t i = 0; i < len; i += 8) { uint64_t word = *(const uint64_t*)(data + i); if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr + i, word) != HAL_OK) { HAL_FLASH_Lock(); return false; } } HAL_FLASH_Lock(); return true; }注意:
HAL_FLASH_Program()要求地址 8 字节对齐,且word必须是合法 double-word 值(不能含非法指令)。OTA 包需按 8 字节对齐填充。
5.3 HTTP 接口实现:流式解析 multipart body
mongoose 不内置 multipart 解析,需手动处理。关键逻辑:
static void handle_api_ota(struct mg_connection *c, struct mg_http_message *hm) { if (hm->method != MG_HTTP_POST) { mg_http_reply(c, 405, "", "Method Not Allowed\n"); return; } // 1. 提取 boundary struct mg_str boundary = mg_http_get_header(hm, "Content-Type"); if (!boundary.ptr || !mg_strstr(boundary, mg_str_n("boundary=", 9))) { mg_http_reply(c, 400, "", "Missing boundary\n"); return; } struct mg_str bnd = mg_str_n(boundary.ptr + 9, boundary.len - 9); // 2. 流式解析 body(不全加载到内存!) const uint8_t *p = (const uint8_t*)hm->body.ptr; const uint8_t *end = p + hm->body.len; while (p < end) { if (mg_strstr(mg_str_n((char*)p, end-p), mg_str_n("--", 2)) == (char*)p) { // 找到新 part 开头 p += 2; if (mg_strstr(mg_str_n((char*)p, end-p), bnd) == (char*)p) { p += bnd.len; if (mg_strstr(mg_str_n((char*)p, end-p), mg_str_n("\r\n\r\n", 4)) == (char*)p) { p += 4; // 跳过 \r\n\r\n // 此时 p 指向 part body parse_multipart_part(c, p, end, bnd); break; } } } p++; } } static void parse_multipart_part(struct mg_connection *c, const uint8_t *p, const uint8_t *end, struct mg_str bnd) { // 简化:只找 filename="firmware.bin" if (mg_strstr(mg_str_n((char*)p, 100), mg_str_n("filename=\"firmware.bin\"", 23))) { const uint8_t *body_start = p + 100; // 粗略定位 while (body_start < end && (body_start[0] != '\r' || body_start[1] != '\n')) body_start++; body_start += 2; // 跳过 \r\n const uint8_t *body_end = end; while (body_end > body_start && (body_end[-2] != '\r' || body_end[-1] != '\n')) body_end--; size_t body_len = body_end - body_start - 2; // 减去 --boundary\r\n // 写入 Flash if (ota_flash_erase_sector() && ota_flash_write(OTA_SECTOR_ADDR, body_start, body_len)) { mg_http_reply(c, 202, "", "{ \"status\": \"accepted\" }\n"); } else { mg_http_reply(c, 500, "", "{ \"error\": \"flash write failed\" }\n"); } } }提示:真实项目中应使用状态机解析 multipart,而非字符串暴力搜索。此处简化仅为展示流程。推荐使用
mg_http_parse_multipart()(mongoose 7.8+ 新增),但需自行实现mg_http_upload()回调。
5.4 签名验证与安全启动
公钥硬编码在代码中(实际项目应存于 OTP 区域):
// ecdsa_verify.c const uint8_t g_pubkey_x[] = {0x01, 0x23, /* ... 32 bytes ... */}; const uint8_t g_pubkey_y[] = {0x45, 0x67, /* ... 32 bytes ... */}; bool verify_firmware_signature(const uint8_t *firmware, size_t len, const uint8_t *sig) { // 调用 mbedtls_ecdsa_verify() 或自研 ECDSA 验证 // 输入:firmware 的 SHA256 哈希值、sig、g_pubkey // 输出:true/false return true; // 简化示意 }验证通过后,更新启动标志:
typedef struct { uint32_t magic; // 0xDEADBEAF uint32_t version; // 固件版本号 uint32_t crc32; // firmware.bin CRC32 uint8_t pubkey_hash[32]; // 公钥哈希,防篡改 } ota_header_t; ota_header_t header = { .magic = 0xDEADBEAF, .version = 0x00010000, .crc32 = calculate_crc32(firmware_data, firmware_len), }; memcpy(header.pubkey_hash, g_pubkey_hash, 32); ota_flash_write(OTA_SECTOR_ADDR, (uint8_t*)&header, sizeof(header));Bootloader 启动时,先读取该 header,校验 magic 和 CRC,再跳转执行。
从那以后我每次加新接口,都强制走一遍curl -v -F "firmware.bin=@test.bin" http://192.168.1.100/api/ota的端到端测试,哪怕只是改一行注释。因为嵌入式 Web 服务的脆弱性不在代码逻辑,而在内存布局、Flash 时序、HTTP 边界解析这些看不见的链路上——它们不会在编译时报错,却会在凌晨三点让产线停摆。希望帮到你。
本文还有配套的精品资源,点击获取