4. emNet配置宏与编译期裁剪

emNet 的一个核心工程优势,是它把绝大多数协议功能都做成了“编译期开关”。也就是说,协议栈里哪些模块最终被编译进固件,不是运行时才决定的,而是在预处理阶段由一组配置宏(#define)筛选出来的。理解这套宏体系,直接决定了你能否把有限的 Flash 和 RAM 留给真正需要的功能,同时避免把用不到的协议代码带进产品。

emNet 的配置宏通常集中在一个配置文件里,常见做法是项目自建一个 IP_Config.h 或 IP_Conf.h,在包含 emNet 头文件之前先定义这些宏。它的组织逻辑可以分成三层:第一层是“基础运行环境”,决定 CPU 架构、字节序、RTOS 适配、内存分配方式;第二层是“协议功能开关”,决定 IP、TCP、UDP、DHCP、DNS、Web 服务器等模块是否参与编译;第三层是“容量与参数”,决定 socket 数量、缓冲区大小、ARP 表项、TCP 窗口等运行时资源。裁剪时应当先定第一层,再按功能需求勾选第二层,最后根据并发量和吞吐目标调第三层。

裁剪的基本原则是:先保证基础层正确,再按“用不到就不编译”的原则关闭功能宏,最后才去压缩缓冲区尺寸。顺序颠倒容易导致编译报错或运行不稳定。

下面这张图给出了 emNet 配置宏从“项目需求”到“最终固件”的裁剪关系。左侧是需求侧,中间是宏开关层,右侧是编译后实际进入固件的模块。可以看到,关闭一个功能宏,对应的源文件就不会被编译,代码体积和静态 RAM 占用会同步下降。

emNet 配置宏裁剪关系图 需求 → 宏开关 → 编译结果 项目需求 以太网通信 必需 可靠传输 TCP 客户端/服务端 实时数据上报 UDP 单播/广播 自动获取地址 DHCP 客户端 域名访问 DNS 解析 本地网页配置 HTTP 服务器 安全通信 TLS/SSL(可选) 配置宏开关(IP_Config.h) IP_SUPPORT 基础 IP 层,始终开启 TCP_SUPPORT TCP 协议模块 UDP_SUPPORT UDP 协议模块 DHCP_CLIENT_SUPPORT DHCP 客户端 DNS_CLIENT_SUPPORT DNS 解析客户端 HTTP_SERVER_SUPPORT Web 服务器模块 TLS_SUPPORT 加密传输(可选) 编译进固件的模块 IP 核心层 约 8–12 KB Flash TCP 模块 约 10–15 KB Flash UDP 模块 约 4–6 KB Flash DHCP 客户端 约 3–5 KB Flash DNS 客户端 约 2–4 KB Flash HTTP 服务器 约 8–20 KB Flash TLS 模块 约 20–40 KB Flash 虚线表示可选模块:关闭后对应源文件不参与编译,Flash/RAM 占用同步减少

从图中可以看出,宏开关是需求与固件之间的“闸门”。下面结合一个典型的工业数据采集终端场景,说明如何具体落地。假设目标硬件是一颗 Cortex-M4、Flash 512 KB、RAM 128 KB 的 MCU,产品需要:通过以太网向服务器上报采集数据(TCP 长连接)、接收服务器下发的配置指令(TCP)、支持本地网页查看状态(HTTP)、上电后自动获取 IP(DHCP),但不需要 DNS 解析,也不需要 TLS 加密。那么配置可以写成:

#define IP_SUPPORT              1
#define TCP_SUPPORT             1
#define UDP_SUPPORT             0
#define DHCP_CLIENT_SUPPORT     1
#define DNS_CLIENT_SUPPORT      0
#define HTTP_SERVER_SUPPORT     1
#define TLS_SUPPORT             0

/* 容量参数 */
#define IP_MAX_SOCKETS          6
#define TCP_MAX_SOCKETS         4
#define TCP_WINDOW_SIZE         4096
#define IP_RX_BUFFER_SIZE       2048
#define IP_TX_BUFFER_SIZE       2048
#define ARP_TABLE_SIZE          8

这里有几个关键点值得展开。第一,UDP_SUPPORT 置 0 后,UDP 相关源文件不会被编译,但要注意某些上层模块(例如部分 DHCP 实现或 SNMP)可能依赖 UDP,关闭前要确认依赖链。第二,IP_MAX_SOCKETS 是全局 socket 上限,TCP_MAX_SOCKETS 是其中 TCP 可用的数量,两者要匹配:如果 TCP 需要 4 个,而 IP_MAX_SOCKETS 只给了 3,编译或运行时会出问题。第三,TCP_WINDOW_SIZE 直接影响吞吐和 RAM 占用,4 KB 窗口在 100 Mbps 链路上通常够用,但如果要做大文件传输,可以适当加大到 8 KB 或 16 KB,代价是每个 TCP 连接多占相应缓冲区。

裁剪时不要只看 Flash。emNet 的很多缓冲区是静态分配的,关闭功能宏能省 Flash,但真正影响 RAM 的是 socket 数量、窗口大小和收发缓冲区。建议每次裁剪后都查看 map 文件中的 .bss 段变化。

对于 Web 服务器,emNet 的 HTTP 模块还支持进一步的子裁剪。例如可以只保留静态页面服务,关闭 CGI 或 SSI 动态处理;可以限制同时处理的 HTTP 连接数;可以关闭文件上传接口。这些子开关通常以 HTTP_ 前缀出现,具体名称随版本略有差异,但思路一致:先关大模块,再关子功能,最后调缓冲区。

裁剪完成后,如何验证结果是否符合预期?可以从三个层面检查。第一,编译层面:确认没有出现“未定义符号”或“重复定义”,这通常说明某个宏的依赖关系没处理干净。第二,资源层面:对比裁剪前后的 map 文件,记录 .text、.rodata、.bss 三个段的大小变化,确认节省量符合预期。第三,功能层面:用实际网络工具验证保留的功能仍然可用,例如用 ping 验证 IP 层、用 TCP 客户端连接验证 TCP、用浏览器访问验证 HTTP、用 DHCP 服务器分配地址验证 DHCP。下面给出一个简化的裁剪检查表,可作为项目中的配置记录:

检查项 裁剪前 裁剪后 判断依据
Flash 占用(.text+.rodata) 约 96 KB 约 58 KB map 文件对比,节省约 38 KB
静态 RAM(.bss) 约 42 KB 约 26 KB socket 与缓冲区减少
TCP 连接 可用 可用 客户端连接服务器成功
UDP 通信 可用 不可用 符合预期,已关闭
DHCP 获取地址 可用 可用 上电后获得 192.168.1.x
HTTP 页面访问 可用 可用 浏览器打开状态页正常
DNS 解析 可用 不可用 符合预期,已关闭

需要强调的是,表中的数值是示例性估算,用于说明裁剪前后的量级差异,实际数值必须以具体工程编译结果为准。不同编译器优化等级、不同 emNet 版本、不同 CPU 架构都会影响最终大小。

最后给出几条工程经验。第一,配置宏尽量集中在一个头文件中管理,不要散落在多个源文件里,否则后期维护和版本对比会非常困难。第二,每次裁剪只改一类宏,改完立即编译并记录资源变化,避免一次改太多导致问题难以定位。第三,保留一份“全功能配置”作为对照基线,当裁剪后出现异常时,可以快速切回基线判断是配置问题还是代码问题。第四,如果项目后期可能增加功能,建议在硬件资源允许的前提下预留一定的 socket 和缓冲区余量,避免功能回归时被迫重新调整内存布局。掌握这套编译期裁剪方法,就能让 emNet 在资源受限的嵌入式产品中既跑得稳,又占得少。