RT-Thread 中 lwIP 2.0.3 网络应用(ping 与 TFTP 服务器)的移植与实战指南

原创2026-10-03 17:39:501,003 阅读
文章标签:操作系统嵌入式物联网嵌入式OSRTOS

RT-Thread 中 lwIP 2.0.3 网络应用(ping 与 TFTP 服务器)的移植与实战指南

本指南围绕 RT-Thread 官方在 lwIP 2.0.3 源码树中维护的 apps 网络应用集合展开,重点剖析其中 RT-Thread 开发团队亲自移植、并可直接在 finsh/msh 命令行下使用的两个核心工具:ping 与 tftp。通过阅读本文,你将理解这两个应用在 RT-Thread 中的编译使能方式、源码级工作流程,并能直接在开发板上复现 "ping 通目标主机" 与 "通过 TFTP 上传/下载文件" 的完整实操方案。

一、src/apps 目录:lwIP 官方应用套件与 RT-Thread 的移植范围

在 RT-Thread 的 lwIP 端口代码树中,components/net/lwip/lwip-2.0.3/src/apps/README.md 明确说明:

Some net apps for current version LwIP. The RT-Thread develop team ported apps. It's easy to use it on finsh/msh.

即:该目录下存放的是与当前 lwIP 2.0.3 版本配套的一组网络应用(net apps),RT-Thread 开发团队已完成移植(ported apps),并重点保证它们可以轻松地在 finsh/msh 命令行中使用。原文档特别点名的两个应用是:

  • ping —— 基于 ICMP Echo 的网络连通性测试工具;
  • tftp —— 基于 RFC 1350 的简单文件传输协议服务器。

从目录结构(components/net/lwip/lwip-2.0.3/src/apps)可以看到,该目录还包含 lwIP 上游的其他标准应用:httpd(内嵌 Web 服务器)、lwiperf(带宽测试)、mdns、mqtt、netbiosns、snmp(含 SNMPv3)与 sntp(NTP 客户端)。本指南以文档点名的 ping 与 tftp 为主线,其余应用仅作背景提及。

RT-Thread 通过 SConscript 构建脚本控制这些应用的编译。在 components/net/lwip/lwip-2.0.3/SConscript 中可以看到:

  • snmp_src = Glob("src/apps/snmp/*.c"),且仅在使能 RT_LWIP_SNMP 时加入编译(第 55、69-71 行);
  • if GetDepend(['RT_LWIP_USING_PING']): src += Glob('src/apps/ping/ping.c'),即 ping 应用只在 RT_LWIP_USING_PING 被打开时参与构建(第 80-81 行);
  • 所有应用源码的公共头文件搜索路径已包含 src/include 等核心目录,应用自身的头文件(如 snmp)在对应条件分支中加入(第 62-71 行)。

也就是说,ping 是唯一被 RT-Thread 显式挂入默认构建系统的 apps 应用;TFTP 则默认不编译,需要用户自行把 tftp 目录下的源文件加入工程(详见下文第四节)。

二、ping:在 RT-Thread 命令行中实现主机连通性测试

2.1 功能使能

ping 应用对应 Kconfig 配置项 RT_LWIP_USING_PING,位于 components/net/lwip/Kconfig:

config RT_LWIP_USING_PING
    bool "Enable ping features"
    default y
    select NETDEV_USING_PING
    select RT_LWIP_ICMP
    select RT_LWIP_RAW

其默认开启,并会自动连带使能 NETDEV_USING_PING(网络设备层 ping)、RT_LWIP_ICMP(ICMP 协议支持)与 RT_LWIP_RAW(原始套接字)。因此打开该配置后,ping.c 中的 #ifdef RT_LWIP_ICMP 编译条件即满足,ping 源码会被 SConscript 纳入编译。

2.2 命令行用法

源码末尾同时导出两种命令行入口(ping.c):

  • FINSH_FUNCTION_EXPORT(ping, ping network host) —— 供 finsh 使用;
  • MSH_CMD_EXPORT_ALIAS(cmd_ping, ping, ping network host) —— 供 msh 使用,其中 cmd_ping 包装函数会打印用法提示并以 4 次、默认报文大小调用 ping()。

在 msh 下直接输入:

msh />ping 192.168.1.1

该命令等价于调用 ping("192.168.1.1", 4, 0),即默认发送 4 个 ICMP Echo 请求,载荷大小取默认值 PING_DATA_SIZE = 32 字节。若参数个数不足,会输出提示 Please input: ping <host address>。典型输出如下:

msh />ping 192.168.1.1
60 bytes from 192.168.1.1 icmp_seq=0 ttl=64 time=1 ms
60 bytes from 192.168.1.1 icmp_seq=1 ttl=64 time=1 ms
60 bytes from 192.168.1.1 icmp_seq=2 ttl=64 time=1 ms
60 bytes from 192.168.1.1 icmp_seq=3 ttl=64 time=1 ms

其中 time 单位是毫秒,由 RT-Thread 内核 tick(rt_tick_get())换算而来。

2.3 源码级流程解析

ping 的实现位于 components/net/lwip/lwip-2.0.3/src/apps/ping/ping.c,核心流程如下:

  1. 域名解析:ping() 使用 lwip_getaddrinfo() 将主机名(如 www.rt-thread.org)或点分十进制 IP 解析为地址,失败时打印 ping: unknown host %s 并返回 -RT_ERROR(第 161-176 行);
  2. 创建原始套接字:lwip_socket(AF_INET, SOCK_RAW, IP_PROTO_ICMP) 创建 ICMP 原始套接字,并设置接收超时 SO_RCVTIMEO(第 177-184 行),超时值由 PING_RCV_TIMEO (2 * RT_TICK_PER_SECOND) 决定,即 2 个系统 tick 周期;
  3. 发送报文:lwip_ping_send() 内部由 ping_prepare_echo() 构造 ICMP Echo 报文——类型设为 ICMP_ECHO、代码为 0、标识符固定为 PING_ID 0xAFAF、序号 htons(++ping_seq_num) 递增,载荷填充 0~31 的字节序列;校验和通过 inet_chksum() 计算,若使能了硬件校验和(RT_LWIP_USING_HW_CHECKSUM)则交由硬件处理(第 50-106 行);
  4. 接收应答:lwip_ping_recv() 在 lwip_recvfrom() 循环中校验 IP 头之后的 ICMP Echo Reply 是否与本机 PING_ID 及当前序号匹配,并从中提取 TTL(iphdr->_ttl)返回(第 108-131 行);
  5. 统计输出:每条回复打印 %d bytes from %s icmp_seq=%d ttl=%d time=%d ms;未收到则打印 From %s icmp_seq=%d timeout;发送间隔由 PING_DELAY (1 * RT_TICK_PER_SECOND) 控制(第 186-217 行)。

该实现依赖 RT-Thread 的 POSIX socket 层(lwip_socket/lwip_sendto/lwip_recvfrom),因此也要求工程使能 SAL(Socket 抽象层)或直接使用 lwIP 原生 socket API。

三、tftp:基于 RFC 1350 的轻量文件传输服务器

3.1 源码构成与架构

tftp 应用由两个文件组成:

  • tftp_server.c:lwIP 上游的 TFTP 服务器实现,使用 lwIP raw API(udp_new_ip_type、udp_bind、udp_recv),完全基于回调驱动,不依赖操作系统线程;
  • tftp_port.c:RT-Thread 开发团队编写的移植层(首个版本由 armink 于 2017-08-17 提交),将 lwIP 的文件读写回调接到 RT-Thread 的 POSIX 文件系统(open/read/write/close)上。

协议本身遵循 RFC 1350(Trivial File Transfer Protocol),实现的关键常量(tftp_server.c):

常量 值 含义
TFTP_MAX_PAYLOAD_SIZE 512 单块数据最大载荷(RFC 1350 规定的数据块上限)
TFTP_HEADER_LENGTH 4 2 字节操作码 + 2 字节块号
TFTP_RRQ/WRQ/DATA/ACK/ERROR 1/2/3/4/5 读请求 / 写请求 / 数据 / 确认 / 错误
错误码 1~7 — 文件未找到、访问违规、磁盘满、非法操作、未知传输 ID、文件已存在、无此用户

3.2 与 RT-Thread 文件系统的对接(tftp_port.c)

RT-Thread 的移植层通过 struct tftp_context 回调集合把服务器与具体文件系统解耦(tftp_port.c):

static void* tftp_open(const char* fname, const char* mode, u8_t write)
{
    int fd = -1;

    if (!rt_strcmp(mode, "octet"))
    {
        if (write)
        {
            fd = open(fname, O_WRONLY | O_CREAT, 0);
        }
        else
        {
            fd = open(fname, O_RDONLY, 0);
        }
    }
    else
    {
        rt_kprintf("tftp: No support this mode(%s).", mode);
    }

    return (void *) fd;
}

static int tftp_write(void* handle, struct pbuf* p)
{
    int fd = (int) handle;

    return write(fd, p->payload, p->len);
}

要点:

  • 传输模式:目前仅支持 octet(二进制)模式,netascii 等其他模式会打印 tftp: No support this mode(%s). 并返回无效句柄;
  • 读写映射:write 请求时以 O_WRONLY | O_CREAT 打开文件(覆盖写),读请求时以 O_RDONLY 打开;
  • 其余回调:ctx.close = close、ctx.read = read 直接复用 RT-Thread libc/POSIX 的标准函数,ctx.write = tftp_write 则将 lwIP 的 struct pbuf 载荷直接写入文件描述符。

服务器启动函数 tftp_server() 依次填充四个回调、调用 tftp_init(&ctx),成功后打印 TFTP server start successfully.,失败打印 TFTP server start failed.。

3.3 服务器状态机与超时重传

tftp_server.c 用全局 struct tftp_state 维护单一连接状态,并明确限制同一时刻只支持一个连接——当收到来自不同端口/地址的报文时,会回送错误 2(Only one connection at a time is supported)。其状态机(recv() 回调,第 203-358 行):

  • RRQ/WRQ(读/写请求):解析以 \0 结尾的文件名与模式字符串,调用 ctx->open();WRQ 回送 ACK(0) 作为握手,RRQ 则立即发送第一块数据;
  • DATA(数据块):仅接受写连接,剥去 4 字节头部后调用 ctx->write(),随后回送 ACK(blknum);当数据块小于 512 字节时判定传输结束并关闭句柄;
  • ACK(确认):仅接受读连接,校验块号后递增 blknum 继续发送下一块,若上一块不足 512 字节则结束传输;
  • 错误处理:文件名过长/未以 \0 终止、模式非法、块号不匹配、未知操作码均会回送对应错误码。

可靠性机制由 tftp_tmr() 定时回调实现(第 360-383 行):每 TFTP_TIMER_MSECS 触发一次,若超时(超过 TFTP_TIMEOUT_MSECS)且重传次数未达 TFTP_MAX_RETRIES,则重发 last_data,否则关闭连接。

3.4 启动服务器

与 ping 相同,tftp_server 也同时注册了 finsh 与 msh 命令(tftp_port.c):

FINSH_FUNCTION_EXPORT(tftp_server, start tftp server.);
MSH_CMD_EXPORT(tftp_server, start tftp server.);

在 msh 下输入:

msh />tftp_server
TFTP server start successfully.

服务器随即监听 UDP 69 号端口(TFTP_PORT),等待 PC 端 TFTP 客户端连接。在 PC 上即可执行:

# 将本地文件上传到开发板
tftp 192.168.1.10 -m binary -c put firmware.bin

# 从开发板下载文件
tftp 192.168.1.10 -m binary -c get log.txt

需要注意的是,tftp 应用默认不随 SConscript 自动编译,需将 tftp 目录下的 tftp_server.c 与 tftp_port.c 加入工程,并确保头文件路径包含 lwip/apps/tftp_server.h 所在目录;同时工程必须使能 lwIP 的 UDP(LWIP_UDP)与 RT-Thread DFS 文件系统(提供 open/read/write/close 及 unistd.h、sys/stat.h)。

四、RT-Thread 移植层的定位与边界

从实现可以归纳出 RT-Thread 移植这两个应用的技术策略:

  1. 应用逻辑零改动:ping.c、tftp_server.c 均为 lwIP 上游代码,RT-Thread 未改动其协议逻辑;
  2. 入口统一到 finsh/msh:通过 FINSH_FUNCTION_EXPORT / MSH_CMD_EXPORT 宏把 C 函数注册为命令行工具,实现文档所述 "easy to use it on finsh/msh";
  3. 系统能力通过标准接口注入:ping 使用 RT-Thread 内核 tick 与 POSIX socket;tftp 通过 tftp_context 回调对接 DFS 文件系统,使服务器与具体存储介质无关;
  4. 构建条件与 Kconfig 联动:ping 通过 RT_LWIP_USING_PING 联动 RT_LWIP_ICMP、RT_LWIP_RAW、NETDEV_USING_PING,保证依赖完整闭合。

五、延伸阅读

登录后查看全文
rt-thread