在实际嵌入式项目中ESP32 的固件更新是一个高频需求。传统方式需要将设备连接到电脑进行烧录但在设备部署后或批量更新时这种方式效率极低。OTAOver-The-Air技术允许通过无线网络远程更新固件而 TCP OTA 则是其中一种可靠且可控的实现方式。TCP OTA 的核心是利用 TCP 协议的点对点、可靠传输特性将新的固件文件从服务器分段传输到 ESP32并在设备端完成校验和写入。与 HTTP OTA 相比TCP OTA 更适合内网环境或需要自定义传输协议的场景开发者可以完全控制握手、分包、重传和验证逻辑。本文将基于 ESP-IDF 框架详细讲解如何实现 ESP32 的 TCP OTA 升级功能。内容包括环境准备、TCP 服务器搭建、ESP32 客户端实现、固件传输协议设计、安全校验以及常见问题排查。最终你将掌握一套可部署在生产环境中的可靠 OTA 方案。1. 理解 TCP OTA 的工作机制与适用场景1.1 为什么选择 TCP 作为 OTA 传输协议OTA 升级的本质是将一个较大的二进制文件固件从服务器可靠地传输到设备端。常见的传输协议有 HTTP、HTTPS、MQTT 和 TCPHTTP/HTTPS适合公网场景有现成的库支持但协议头开销较大且需要处理重定向、缓存等逻辑。MQTT适合物联网云平台但需要依赖 Broker传输大文件时可能受消息大小限制。TCP协议简单、可靠适合内网或自定义协议场景可以精细控制传输过程。TCP OTA 的优势在于完全控制数据传输流程便于实现断点续传、进度上报、自定义加密等需求。无需依赖复杂的 Web 服务器或云服务适合私有化部署。协议开销小传输效率高。1.2 ESP32 OTA 的底层流程ESP32 的 OTA 升级依赖于其双分区OTA0 和 OTA1设计当前运行的分区为 A 区ota_0待升级的固件将写入 B 区ota_1。通过 TCP 连接从服务器获取固件数据校验后写入 B 区。写入完成后设置下次启动分区为 B 区并重启设备。设备从 B 区启动如果启动成功则更新引导配置如果失败则回退到 A 区。这一机制保证了即使升级失败设备仍能回退到旧版本正常运行。1.3 适用场景与限制TCP OTA 特别适合以下场景设备部署在内网环境无法访问公网 OTA 服务。需要自定义传输协议如加入设备认证、数据加密、压缩等。对传输效率和可控性有较高要求。需要注意的是TCP OTA 需要设备与服务器在同一网络或可路由可达。需要自行实现服务器端和客户端的通信逻辑。生产环境需考虑传输安全如 TLS 加密和版本兼容性。2. 环境准备与项目结构2.1 硬件与软件要求硬件清单ESP32 开发板如 ESP32-WROOM-32稳定的 WiFi 或以太网连接用于调试的 USB 数据线软件环境ESP-IDF 4.0 或更高版本本文基于 v4.4Python 3.x用于构建脚本或简单服务器串口调试工具如 minicom、screen 或 IDF 自带的 monitorESP-IDF 环境配置如果你尚未安装 ESP-IDF可参考以下步骤以 Linux 为例# 克隆 ESP-IDF git clone -b v4.4 --recursive https://github.com/espressif/esp-idf.git cd esp-idf # 安装工具链 ./install.sh # 设置环境变量 source export.sh2.2 项目目录结构一个典型的 TCP OTA 项目包含以下文件esp32_tcp_ota/ ├── main/ │ ├── CMakeLists.txt │ ├── component.mk │ ├── tcp_ota_client.c │ └── main.c ├── server/ │ ├── tcp_ota_server.py │ └── firmware.bin ├── CMakeLists.txt ├── sdkconfig └── README.mdmain/tcp_ota_client.cESP32 端的 TCP OTA 客户端实现。server/tcp_ota_server.py用于测试的 Python TCP 服务器。server/firmware.bin待升级的固件文件。2.3 关键依赖配置在main/CMakeLists.txt中声明组件依赖idf_component_register(SRCS tcp_ota_client.c main.c INCLUDE_DIRS . REQUIRES esp_ota_ops nvs_flash esp_netif)在main/component.mk中如果使用 Make 构建COMPONENT_SRCDIRS : . COMPONENT_ADD_INCLUDEDIRS : . COMPONENT_REQUIRES : esp_ota_ops nvs_flash esp_netif3. 实现 TCP OTA 服务器端3.1 简单的 Python TCP 服务器由于 TCP OTA 协议是自定义的我们可以先用 Python 快速实现一个测试服务器#!/usr/bin/env python3 # server/tcp_ota_server.py import socket import struct import hashlib import os class TcpOtaServer: def __init__(self, host0.0.0.0, port8070): self.host host self.port port self.firmware_path firmware.bin def get_firmware_info(self): 获取固件文件信息 if not os.path.exists(self.firmware_path): raise FileNotFoundError(fFirmware file {self.firmware_path} not found) with open(self.firmware_path, rb) as f: data f.read() file_size len(data) file_md5 hashlib.md5(data).hexdigest() return file_size, file_md5 def send_firmware(self, client_socket): 向客户端发送固件数据 file_size, file_md5 self.get_firmware_info() # 协议头4字节魔数 4字节文件大小 32字节MD5 magic 0xE32OTA1 # 自定义魔数 header struct.pack(I32sI, magic, file_md5.encode(), file_size) client_socket.send(header) # 分段发送固件数据 chunk_size 4096 # 与 ESP32 接收缓冲区匹配 sent_bytes 0 with open(self.firmware_path, rb) as f: while True: chunk f.read(chunk_size) if not chunk: break client_socket.send(chunk) sent_bytes len(chunk) # 可在此添加进度上报逻辑 progress (sent_bytes / file_size) * 100 print(fSent: {sent_bytes}/{file_size} bytes ({progress:.1f}%)) print(Firmware transfer completed) def start(self): 启动TCP服务器 server_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_socket.bind((self.host, self.port)) server_socket.listen(1) print(fTCP OTA Server listening on {self.host}:{self.port}) try: while True: client_socket, client_addr server_socket.accept() print(fClient connected from {client_addr}) try: self.send_firmware(client_socket) except Exception as e: print(fError handling client: {e}) finally: client_socket.close() print(fClient {client_addr} disconnected) except KeyboardInterrupt: print(Server shutdown) finally: server_socket.close() if __name__ __main__: server TcpOtaServer() server.start()3.2 服务器协议设计说明上述服务器实现了一个简单的 OTA 协议连接建立ESP32 连接到服务器的指定端口如 8070。协议头发送服务器立即发送 40 字节的协议头4 字节魔数0xE32OTA1用于验证协议版本和格式。32 字节 MD5 字符串固件的校验和。4 字节文件大小固件的总字节数。数据分段传输按 4KB 分段发送固件数据。连接关闭传输完成后关闭连接。在实际项目中你可能需要扩展这个协议例如加入设备认证阶段。支持断点续传发送已接收的字节数。添加数据加密如 AES-256。支持压缩传输。4. ESP32 TCP OTA 客户端实现4.1 主要头文件包含与配置在main/tcp_ota_client.h中定义配置和函数声明#ifndef TCP_OTA_CLIENT_H #define TCP_OTA_CLIENT_H #include esp_ota_ops.h #include esp_netif.h #include esp_wifi.h #include nvs_flash.h #include esp_log.h #include esp_system.h #include lwip/sockets.h // 配置参数 #define OTA_SERVER_IP 192.168.1.100 // 服务器IP地址 #define OTA_SERVER_PORT 8070 // 服务器端口 #define OTA_BUFFER_SIZE 4096 // 接收缓冲区大小 #define OTA_MAGIC_NUM 0xE32OTA1 // 协议魔数 // 错误码定义 typedef enum { OTA_SUCCESS 0, OTA_ERR_SOCKET, OTA_ERR_CONNECT, OTA_ERR_RECV_HEADER, OTA_ERR_MAGIC, OTA_ERR_OTA_BEGIN, OTA_ERR_WRITE, OTA_ERR_MD5, OTA_ERR_OTA_END } ota_result_t; // 函数声明 ota_result_t tcp_ota_update(void); void wifi_init_sta(void); #endif4.2 WiFi 连接初始化在main/tcp_ota_client.c中实现 WiFi 连接#include tcp_ota_client.h static const char *TAG TCP_OTA; // WiFi 配置 - 需要根据实际网络修改 #define WIFI_SSID your_wifi_ssid #define WIFI_PASS your_wifi_password static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { if (event_base WIFI_EVENT event_id WIFI_EVENT_STA_START) { esp_wifi_connect(); } else if (event_base WIFI_EVENT event_id WIFI_EVENT_STA_DISCONNECTED) { esp_wifi_connect(); ESP_LOGI(TAG, Retry to connect to AP); } else if (event_base IP_EVENT event_id IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t* event (ip_event_got_ip_t*) event_data; ESP_LOGI(TAG, Got IP: IPSTR, IP2STR(event-ip_info.ip)); } } void wifi_init_sta(void) { esp_netif_init(); esp_event_loop_create_default(); esp_netif_create_default_wifi_sta(); wifi_init_config_t cfg WIFI_INIT_CONFIG_DEFAULT(); esp_wifi_init(cfg); esp_event_handler_instance_t instance_any_id; esp_event_handler_instance_t instance_got_ip; esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL, instance_any_id); esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL, instance_got_ip); wifi_config_t wifi_config { .sta { .ssid WIFI_SSID, .password WIFI_PASS, }, }; esp_wifi_set_mode(WIFI_MODE_STA); esp_wifi_set_config(WIFI_IF_STA, wifi_config); esp_wifi_start(); ESP_LOGI(TAG, wifi_init_sta finished.); }4.3 TCP OTA 核心逻辑ota_result_t tcp_ota_update(void) { int sock -1; struct sockaddr_in server_addr; uint8_t *buffer NULL; esp_ota_handle_t update_handle 0; const esp_partition_t *update_partition NULL; // 分配接收缓冲区 buffer malloc(OTA_BUFFER_SIZE); if (buffer NULL) { ESP_LOGE(TAG, Failed to allocate buffer); return OTA_ERR_SOCKET; } // 创建TCP socket sock socket(AF_INET, SOCK_STREAM, IPPROTO_IP); if (sock 0) { ESP_LOGE(TAG, Failed to create socket); free(buffer); return OTA_ERR_SOCKET; } // 设置服务器地址 server_addr.sin_family AF_INET; server_addr.sin_port htons(OTA_SERVER_PORT); inet_pton(AF_INET, OTA_SERVER_IP, server_addr.sin_addr); // 连接服务器 if (connect(sock, (struct sockaddr *)server_addr, sizeof(server_addr)) ! 0) { ESP_LOGE(TAG, Failed to connect to server); close(sock); free(buffer); return OTA_ERR_CONNECT; } ESP_LOGI(TAG, Connected to OTA server); // 接收协议头40字节魔数4 MD5 32 文件大小4 uint32_t magic; char md5_str[33] {0}; uint32_t file_size; int received recv(sock, buffer, 40, 0); if (received ! 40) { ESP_LOGE(TAG, Failed to receive header, got %d bytes, received); close(sock); free(buffer); return OTA_ERR_RECV_HEADER; } // 解析协议头 memcpy(magic, buffer, 4); memcpy(md5_str, buffer 4, 32); memcpy(file_size, buffer 36, 4); // 验证魔数 if (magic ! OTA_MAGIC_NUM) { ESP_LOGE(TAG, Invalid magic number: 0x%08x, magic); close(sock); free(buffer); return OTA_ERR_MAGIC; } ESP_LOGI(TAG, Firmware size: %d bytes, MD5: %s, file_size, md5_str); // 查找OTA分区 update_partition esp_ota_get_next_update_partition(NULL); if (update_partition NULL) { ESP_LOGE(TAG, Failed to find OTA partition); close(sock); free(buffer); return OTA_ERR_OTA_BEGIN; } ESP_LOGI(TAG, Writing to partition subtype %d at offset 0x%x, update_partition-subtype, update_partition-address); // 开始OTA更新 esp_err_t err esp_ota_begin(update_partition, file_size, update_handle); if (err ! ESP_OK) { ESP_LOGE(TAG, esp_ota_begin failed: 0x%x, err); close(sock); free(buffer); return OTA_ERR_OTA_BEGIN; } // 接收并写入固件数据 uint32_t total_received 0; esp_sha_context_t sha_ctx; unsigned char md5_calc[16]; // 初始化MD5计算 esp_sha_init(sha_ctx); esp_sha_start(sha_ctx, ESP_MD_SHA256); // 实际使用MD5这里示例用SHA256 while (total_received file_size) { int to_receive (file_size - total_received) OTA_BUFFER_SIZE ? OTA_BUFFER_SIZE : (file_size - total_received); received recv(sock, buffer, to_receive, 0); if (received 0) { ESP_LOGE(TAG, Connection closed or error during transfer); esp_ota_abort(update_handle); close(sock); free(buffer); return OTA_ERR_WRITE; } // 写入OTA分区 err esp_ota_write(update_handle, buffer, received); if (err ! ESP_OK) { ESP_LOGE(TAG, esp_ota_write failed: 0x%x, err); esp_ota_abort(update_handle); close(sock); free(buffer); return OTA_ERR_WRITE; } // 更新MD5计算 esp_sha_update(sha_ctx, buffer, received); total_received received; // 上报进度 if (total_received % (100 * 1024) 0) { // 每100KB打印一次 ESP_LOGI(TAG, Progress: %d/%d bytes (%.1f%%), total_received, file_size, (total_received * 100.0) / file_size); } } // 完成MD5计算 esp_sha_finish(sha_ctx, md5_calc); // 这里应比较计算出的MD5与服务器发送的是否一致 // 简化示例中跳过详细MD5比较 // 结束OTA更新 err esp_ota_end(update_handle); if (err ! ESP_OK) { ESP_LOGE(TAG, esp_ota_end failed: 0x%x, err); close(sock); free(buffer); return OTA_ERR_OTA_END; } // 验证固件并设置启动分区 err esp_ota_set_boot_partition(update_partition); if (err ! ESP_OK) { ESP_LOGE(TAG, esp_ota_set_boot_partition failed: 0x%x, err); close(sock); free(buffer); return OTA_ERR_OTA_END; } ESP_LOGI(TAG, OTA update successful, ready to reboot); close(sock); free(buffer); return OTA_SUCCESS; }4.4 主程序入口在main/main.c中实现应用主逻辑#include tcp_ota_client.h void app_main(void) { // 初始化NVS esp_err_t ret nvs_flash_init(); if (ret ESP_ERR_NVS_NO_FREE_PAGES || ret ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); ret nvs_flash_init(); } ESP_ERROR_CHECK(ret); // 初始化WiFi wifi_init_sta(); // 等待WiFi连接 vTaskDelay(10000 / portTICK_PERIOD_MS); // 执行OTA更新 ota_result_t result tcp_ota_update(); if (result OTA_SUCCESS) { ESP_LOGI(MAIN, OTA update completed, rebooting in 3 seconds...); vTaskDelay(3000 / portTICK_PERIOD_MS); esp_restart(); } else { ESP_LOGE(MAIN, OTA update failed with error: %d, result); } // 主循环 - 如果OTA失败则继续运行原有逻辑 while (1) { ESP_LOGI(MAIN, Running main application...); vTaskDelay(10000 / portTICK_PERIOD_MS); } }5. 构建、烧录与测试验证5.1 项目构建配置在项目根目录的CMakeLists.txt中# The following lines of boilerplate have to be in your projects # CMakeLists.txt file. cmake_minimum_required(VERSION 3.5) include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(esp32_tcp_ota)配置串口和分区表可选使用默认配置# 进入项目目录 cd esp32_tcp_ota # 配置项目 idf.py menuconfig在 menuconfig 中需要确认Serial flasher config → Default serial port 设置正确Partition Table → 选择 Factory app, two OTA definitions5.2 构建与烧录初始固件# 构建项目 idf.py build # 烧录到ESP32 idf.py -p /dev/ttyUSB0 flash monitor烧录成功后设备会启动并尝试连接 WiFi。5.3 准备升级固件要测试 OTA 功能需要准备一个新版本的固件修改代码例如更改日志输出内容重新构建但不烧录idf.py build将生成的固件文件复制到服务器目录cp build/esp32_tcp_ota.bin server/firmware.bin5.4 完整测试流程启动服务器cd server python3 tcp_ota_server.py确保ESP32与服务器网络连通检查IP地址配置和网络连接。观察串口日志设备应显示连接服务器、接收数据、写入分区等过程。验证升级结果设备重启后观察串口输出是否为新版本的日志内容。6. 常见问题与排查指南6.1 连接相关问题问题现象可能原因检查方式解决方案连接服务器失败服务器IP/端口错误、网络不通在ESP32上ping服务器IP检查网络配置和防火墙设置连接被拒绝服务器未启动或端口被占用在服务器执行netstat -tlnp确保服务器正确监听指定端口连接超时网络延迟或服务器处理慢检查网络质量增加超时时间调整socket超时设置在代码中添加socket超时设置// 在connect之前设置超时 struct timeval timeout; timeout.tv_sec 10; timeout.tv_usec 0; setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, timeout, sizeof(timeout)); setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, timeout, sizeof(timeout));6.2 数据传输问题问题现象可能原因检查方式解决方案接收数据不完整网络抖动、缓冲区不足检查每次接收的字节数添加重传机制验证数据完整性OTA写入失败分区空间不足、闪存损坏检查分区大小和闪存状态确保分区足够大检查闪存寿命MD5校验失败数据传输错误、算法不一致对比服务器和客户端计算的MD5确保使用相同的哈希算法改进的数据接收逻辑// 确保完整接收指定字节数 int receive_exact(int sock, uint8_t *buffer, int length) { int total_received 0; while (total_received length) { int received recv(sock, buffer total_received, length - total_received, 0); if (received 0) { return -1; // 错误或连接关闭 } total_received received; } return total_received; }6.3 分区与启动问题问题现象可能原因检查方式解决方案找不到OTA分区分区表配置错误检查 partitions.csv 文件使用正确的分区表配置启动后回退到旧版本新固件启动失败检查串口启动日志验证新固件的稳定性和兼容性设备不断重启新固件有严重错误分析coredump确保新固件经过充分测试检查分区信息的代码void print_partition_info(void) { const esp_partition_t *configured esp_ota_get_boot_partition(); const esp_partition_t *running esp_ota_get_running_partition(); if (configured ! NULL) { ESP_LOGI(TAG, Configured OTA partition: %s, offset 0x%x, configured-label, configured-address); } if (running ! NULL) { ESP_LOGI(TAG, Running partition: %s, offset 0x%x, running-label, running-address); } }7. 生产环境最佳实践7.1 安全性增强传输加密// 建议使用TLS加密传输 // 需要包含 esp_tls.h 并使用 esp_tls_conn_* 系列函数 // 或者使用应用层加密 void encrypt_data(uint8_t *data, size_t len, const uint8_t *key) { // 实现AES等加密算法 }设备认证// 在传输固件前进行设备认证 int authenticate_device(int sock, const char *device_id, const char *token) { // 发送设备凭证到服务器验证 // 服务器返回认证结果 }7.2 可靠性保障断点续传// 在协议中支持断点续传 typedef struct { uint32_t magic; uint32_t file_size; char md5[33]; uint32_t resume_offset; // 支持从断点继续 } ota_header_t;完整性验证// 更严格的完整性检查 bool verify_firmware_integrity(const esp_partition_t *partition) { // 验证分区数据完整性 // 检查签名或哈希值 return true; }7.3 监控与日志详细进度上报typedef enum { OTA_STATE_CONNECTING, OTA_STATE_DOWNLOADING, OTA_STATE_WRITING, OTA_STATE_VALIDATING, OTA_STATE_COMPLETED, OTA_STATE_FAILED } ota_state_t; void report_ota_progress(ota_state_t state, int progress, const char *message) { // 上报到服务器或本地日志 ESP_LOGI(TAG, OTA State: %d, Progress: %d%%, Message: %s, state, progress, message); }7.4 版本管理与回滚版本检查机制// 在升级前检查版本兼容性 bool check_version_compatibility(const char *current_version, const char *new_version) { // 实现版本比较逻辑 return true; }自动回滚策略// 如果新固件启动失败自动回滚 void check_and_rollback_if_needed(void) { esp_ota_img_states_t ota_state; if (esp_ota_get_state_partition(esp_ota_get_running_partition(), ota_state) ESP_OK) { if (ota_state ESP_OTA_IMG_PENDING_VERIFY) { // 新固件第一次启动进行验证 if (/* 验证失败 */) { esp_ota_mark_app_invalid_rollback_and_reboot(); } else { esp_ota_mark_app_valid_cancel_rollback(); } } } }TCP OTA 为 ESP32 提供了一种灵活可靠的固件升级方案特别适合需要自定义协议和内网部署的场景。实际项目中需要根据具体需求完善安全机制、错误处理和监控功能。建议先在测试环境充分验证再逐步部署到生产环境。