全部例程

W55MH32使用TFTP协议读取文件内容

W55MH32

更新于 2026年9月15日

本篇总结

在 W55MH32 上实现 TFTP 客户端,从 TFTP 服务器读取文本文件内容。

🔑 KEIL MDK、UART调试、硬件TOE引擎、TFTP、DHCP

硬件平台:本例程基于 W55MH32L-EVB 开发板运行。

采购推荐:点击前往 WIZnet 官方淘宝店铺 选购 W55MH32L-EVB

本示例演示如何在 W55MH32 上实现 TFTP(简单文件传输协议)功能。通过实战例程,为大家讲解如何使用 TFTP 客户端模式向服务器获取文本文件。该例程用到的其他网络协议,例如 DHCP,请参考相关章节。

前置工具准备

开始本例程前,请确认以下工具已安装:

  • Tftpd32:TFTP 服务器软件,用于提供文件下载服务:点击下载
  • WIZ UartTool V1.0:串口调试工具:点击下载

TFTP 协议简介

TFTP(Trivial File Transfer Protocol)协议是一种轻量级的文件传输协议,它通常用于需要快速、简单的文件交换场景,尤其是在网络设备启动和配置过程中。与 FTP 不同,TFTP 设计得非常简单,仅提供基本的文件读写功能,并且使用 UDP 作为传输层协议(端口 69),因而不具备 TCP 的复杂性和重传机制。

操作码名称说明
1RRQ(读请求)用于请求读取服务器上的文件
2WRQ(写请求)用于请求向服务器上写入文件
3DATA(数据)用于传输文件数据
4ACK(回应)用于确认接收到的数据块
5ERROR(错误)用于报告传输过程中发生的错误
6OACK(选项回应)扩展操作码,回应选项协商

W55MH32 的优势

W55MH32 SDK 内置了 TFTP 客户端库,报文组包/解包、超时重传、状态管理全部由库函数处理。你只需要:注册超时定时器 → 调用 TFTP_init() → 发送读请求 TFTP_read_request() → 主循环调用 TFTP_run() 驱动状态机。

硬件连接与拓扑

请注意:

PC 和 W55MH32 需处于同一网段。Tftpd32 的服务器地址必须与程序中 TFTP_SERVER_IP 一致,且 TFTP 目录下必须有程序中读取的文件。

  1. Micro USB 线连接 DEBUG USB 口到 PC(供电 + 调试串口)
  2. 网线连接开发板 RJ45 网口 到路由器 LAN 口
┌──────────────────────────────────┐
│ 路由器(DHCP Server) │
│ 192.168.1.1 │
└────┬───────────────────┬──────────┘
│ │
┌────┴──────┐ ┌────┴──────────────┐
│ W55MH32 │ │ PC │
│(TFTP Client)│ │ (Tftpd32 Server) │
│ UDP :69 │ │ 192.168.1.20 │
└───────────┘ └───────────────────┘
↑
DEBUG USB ──→ WIZ UartTool
网络拓扑:PC 运行 Tftpd32 作为 TFTP 服务器(192.168.1.20),W55MH32 作为 TFTP 客户端读取文件

工程结构

工程位于 SDK 内 1.SDK\ModuleDemo\WZTOE\TFTP\,核心文件如下:

TFTP/
├── Project/
│   └── Project.uvprojx            ← Keil 工程文件
└── USER/
    ├── main.c                     ← 硬件初始化 + 网络配置 + 主循环
    ├── wiz_interface/
    │   ├── wiz_interface.c        ← PHY 检测 / 网络信息打印 / DHCP
    │   └── wiz_interface.h
    ├── tftp/
    │   ├── tftp.c                 ← TFTP 客户端实现(初始化/请求/运行/解析)
    │   ├── tftp.h
    │   └── tftp_util.c/h          ← TFTP 辅助函数
    ├── ioLibrary/
    │   └── Internet/TFTP/         ← TFTP 协议库
    ├── tim/
    │   └── bsp_tim.c/h            ← TIM3 定时器(DHCP 1s + TFTP 超时)
    └── uart/
        └── bsp_uart.c/h           ← 调试串口 + printf 重定向

本例程的核心:通过 TFTP_init 初始化、TFTP_read_request 发送读请求、TFTP_run 循环处理收发与超时重传,从服务器读取文本文件内容。

步骤1:注册TFTP超时函数

文件:main.c — TFTP 协议的超时重传需要定时器支持。我们在 TIM3 的 1ms 中断中累加实现,每 1 秒调用一次 tftp_timeout_handler()。

/**
* @brief   1ms timer IRQ Handler
* @param   none
* @return  none
*/
void TIM3_IRQHandler(void)
{
    static uint32_t tim3_1ms_count = 0;
    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
    {
        tim3_1ms_count++;
        if (tim3_1ms_count >= 1000)
        {
            DHCP_time_handler();
            tftp_timeout_handler();
            tim3_1ms_count = 0;
        }
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
    }
}

步骤2:在主函数中调用TFTP读取函数

文件:tftp.c — do_tftp_client() 函数是 TFTP 客户端的入口,首先调用 TFTP_init 初始化,然后发送读请求,最后循环调用 TFTP_run 处理传输。

#define TFTP_SERVER_IP                 "192.168.1.20"
#define TFTP_SERVER_FILE_NAME          "tftp_test_file.txt"
void do_tftp_client(uint8_t sn, uint8_t *buff)
{
  uint32_t tftp_server_ip = inet_addr((uint8_t*)TFTP_SERVER_IP);//TFTP服务器地址
  uint8_t  tftp_read_file_name[] = TFTP_SERVER_FILE_NAME;    //读取文件名

  TFTP_init(sn, buff);
  while (1)
  {
      if (tftp_read_flag == 0)
      {
          printf("tftp server ip: %s, file name: %s\r\n", TFTP_SERVER_IP, TFTP_SERVER_FILE_NAME);
          printf("send request\r\n");
          TFTP_read_request(tftp_server_ip, TFTP_SERVER_FILE_NAME);
          tftp_read_flag = 1;
      }
      else
      {
          tftp_state = TFTP_run();
          if (tftp_state == TFTP_SUCCESS)
          {
              printf("tftp read success, file name: %s\r\n", tftp_read_file_name);
              while (1)
              {
              }
          }
          else if (tftp_state == TFTP_FAIL)
          {
              printf("tftp read fail, file name: %s\r\n", tftp_read_file_name);
              while (1)
              {
              }
          }
      }
  }
}

进入 do_tftp_client() 函数后开始进行 TFTP 客户端处理,首先会调用 TFTP_init() 函数对 TFTP 客户端进行初始化,步骤如下:

void TFTP_init(uint8_t socket, uint8_t *buf)
{
    init_tftp();

    g_tftp_socket  = open_tftp_socket(socket);
    g_tftp_rcv_buf = buf;
}

static void init_tftp(void)
{
    g_filename[0] = 0;

    set_server_ip(0);
    set_server_port(0);
    set_local_port(0);

    set_tftp_state(STATE_NONE);
    set_block_number(0);

    // timeout flag
    g_resend_flag  = 0;
    tftp_retry_cnt = tftp_time_cnt = 0;
    g_progress_state = TFTP_PROGRESS;
}

当 tftp_read_flag 为 0 时,表示尚未发送读取请求。此时,打印 TFTP 服务器的 IP 地址和要读取的文件名,然后调用 TFTP_read_request() 函数向服务器发送读取请求。发送请求后,将 tftp_read_flag 设置为 1,表示已发送请求。

void TFTP_read_request(uint32_t server_ip, uint8_t *filename)
{
    set_server_ip(server_ip);
#ifdef __TFTP_DEBUG__
    DBG_PRINT(INFO_DBG, "[%s] Set Tftp Server : %x\r\n", __func__, server_ip);
#endif

    g_progress_state = TFTP_PROGRESS;
    send_tftp_rrq(filename, (uint8_t *)TRANS_BINARY, &default_tftp_opt, 1);
}

步骤3:运行TFTP协议并处理结果

文件:tftp.c — 当 tftp_read_flag 为 1 时调用 TFTP_run() 函数处理 TFTP 协议操作,依据其返回的 tftp_state 判断结果:若为 TFTP_SUCCESS 则打印成功信息并进入无限循环,若为 TFTP_FAIL 则打印失败信息并进入无限循环。

int TFTP_run(void)
{
    int      len;
    uint16_t from_port;
    uint32_t from_ip;

    /* Timeout Process */
    if (g_resend_flag)
    {
        if (tftp_time_cnt >= g_timeout)
        {
            switch (get_tftp_state())
            {
            case STATE_WRQ:
                break;

            case STATE_RRQ:
                send_tftp_rrq(g_filename, (uint8_t *)TRANS_BINARY, &default_tftp_opt, 1);
                break;

            case STATE_OACK:
            case STATE_DATA:
                send_tftp_ack(get_block_number());
                break;

            case STATE_ACK:
                break;

            default:
                break;
            }

            tftp_time_cnt = 0;
            tftp_retry_cnt++;

            if (tftp_retry_cnt >= 5)
            {
                init_tftp();
                g_progress_state = TFTP_FAIL;
            }
        }
    }

    /* Receive Packet Process */
    len = recv_udp_packet(g_tftp_socket, g_tftp_rcv_buf, MAX_MTU_SIZE, &from_ip, &from_port);
    if (len < 0)
    {
#ifdef __TFTP_DEBUG__
        DBG_PRINT(ERROR_DBG, "[%s] recv_udp_packet error\r\n", __func__);
#endif
        return g_progress_state;
    }

    recv_tftp_packet(g_tftp_rcv_buf, len, from_ip, from_port);

    return g_progress_state;
}

在处理接收到的 TFTP 数据包时,首先调用 recv_tftp_packet() 函数。

static void recv_tftp_packet(uint8_t *packet, uint32_t packet_len, uint32_t from_ip, uint16_t from_port)
{
    uint16_t opcode;
    /* Verify Server IP */
    if (from_ip != get_server_ip())
    {
#ifdef __TFTP_DEBUG__
        DBG_PRINT(ERROR_DBG, "[%s] Server IP faults\r\n", __func__);
        DBG_PRINT(ERROR_DBG, "from IP : %08x, Server IP : %08x\r\n", from_ip, get_server_ip());
#endif
        return;
    }
    opcode = ntohs(*((uint16_t *)packet));
    /* Set Server Port */
    if ((get_tftp_state() == STATE_WRQ) || (get_tftp_state() == STATE_RRQ))
    {
        set_server_port(from_port);
#ifdef __TFTP_DEBUG__
        DBG_PRINT(INFO_DBG, "[%s] Set Server Port : %d\r\n", __func__, from_port);
#endif
    }

    switch (opcode)
    {
    case TFTP_RRQ: /* When Server */
        recv_tftp_rrq(packet, packet_len);
        break;
    case TFTP_WRQ: /* When Server */
        recv_tftp_wrq(packet, packet_len);
        break;
    case TFTP_DATA:
        recv_tftp_data(packet, packet_len);
        break;
    case TFTP_ACK:
        recv_tftp_ack(packet, packet_len);
        break;
    case TFTP_OACK:
        recv_tftp_oack(packet, packet_len);
        break;
    case TFTP_ERROR:
        recv_tftp_error(packet, packet_len);
        break;

    default:
        // Unknown Mesage
        break;
    }
}

进入该函数后,第一步验证接收到的数据包的源 IP 地址,只有当它与服务器 IP 地址一致时才继续处理,若不一致则直接返回。接着,从数据包中获取操作码(opcode)。根据获取到的操作码,调用相应的处理函数:如果是 TFTP 读请求(RRQ),则调用 recv_tftp_rrq() 函数;若是写请求(WRQ),则调用 recv_tftp_wrq() 函数;对于接收到的数据数据包,调用 recv_tftp_data() 函数;确认数据包则由 recv_tftp_ack() 函数处理;OACK 数据包由 recv_tftp_oack() 函数处理;若遇到错误数据包,调用 recv_tftp_error() 函数来解析错误代码和错误信息。最后,返回 g_progress_state,以此表示当前 TFTP 操作的状态。

超时重传机制

TFTP 基于 UDP 不保证可靠传输,需在应用层实现超时重传。当 g_resend_flag 置位且计时到达 g_timeout 时,根据当前状态重发对应报文(RRQ 或 ACK)。重试次数累计达到 5 次仍未成功,则调用 init_tftp() 复位并置 TFTP_FAIL。

运行验证

烧录例程运行后,首先进行了 PHY 链路检测,然后是 DHCP 获取网络地址结果,最后打印服务器 IP 和文本名称,读取文本内容。

W55MH32 TFTP Client example

PHY link
The current Mbtis speed : 100Mbps
The current Duplex Mode : Full-Duplex
DHCP running

DHCP success!
====================================================================================================
 W55MH32 network configuration : DHCP

 MAC         : 00:08:DC:00:0E:9A
 IP          : 192.168.1.6
 Subnet Mask : 255.255.255.0
 Gateway     : 192.168.1.1
 DNS         : 192.168.1.1
====================================================================================================

tftp server ip: 192.168.1.20, file name: tftp_test_file.txt
send request
tftp read success, file name: tftp_test_file.txt

请注意

测试实例需要 PC 端和 W55MH32 处于同一网段。首先打开 Tftpd32,选择 TFTP 目录和服务器地址。服务器地址必须与程序对应,并且 TFTP 目录下必须有程序中读取的文件。

Tftpd32 配置 — Current Directory: D:\Tftpd32,Server interface: 192.168.1.20
WIZ UartTool 串口输出 — TFTP 读取 tftp_test_file.txt 成功(内容 "HelloWIZNET",11 字节)

常见问题

Q: TFTP为什么使用UDP而不是TCP

TFTP设计目标是轻量级、简单,UDP无连接开销小、实现容易,适合资源有限的嵌入式设备和启动加载场景。可靠性由TFTP应用层的ACK确认和超时重传机制保证,每个DATA包必须收到对应ACK才发送下一个,因此不需要TCP的复杂状态机。

Q: 如何判断文件传输是否结束

TFTP规定每个DATA报文最多传输512字节数据。当服务器发送的DATA包数据长度小于512字节时,即表示这是文件的最后一个数据块,传输完成。客户端收到该包后回复ACK,传输正常终止。

Q: 服务器端口为什么不是固定的69

TFTP客户端初始向服务器的69端口发送RRQ/WRQ请求。服务器收到请求后,会随机选择一个新的临时端口进行后续数据传输,并通过第一个响应包(OACK或DATA)告知客户端该端口。之后客户端与服务器使用该临时端口通信,避免与其它客户端的请求冲突。

Q: TFTP读取失败如何排查

首先确认PC与W55MH32处于同一网段且服务器IP与程序中TFTP_SERVER_IP一致;其次确认tftpd32已启动并选择了正确的TFTP目录,且目录下存在TFTP_SERVER_FILE_NAME指定的文件;最后检查防火墙是否放行TFTP(UDP 69及动态端口)。若仍失败,可通过tftp_state返回的TFTP_FAIL及重试计数判断是否为超时或丢包问题。

总结

  • 协议定位:TFTP是基于UDP、端口69的轻量级文件传输协议,仅提供基本读写功能,适合固件升级与配置文件传输
  • 工作流程:客户端发RRQ请求→服务器切换端口→DATA/ACK交替传输→数据小于512字节结束
  • 核心函数:TFTP_init初始化、TFTP_read_request发送读请求、TFTP_run循环处理收发与超时、recv_tftp_packet按操作码分发
  • 可靠性机制:应用层ACK确认+超时重传,重试5次失败则置TFTP_FAIL
  • 结果判断:依据tftp_state为TFTP_SUCCESS或TFTP_FAIL决定后续处理

下一篇文章将聚焦 SNMP 协议,解析其核心原理及在网络管理中的应用,同时讲解如何在相关设备上实现 SNMP 功能,敬请期待!

下载本章例程

我们提供完整的工程文件以及配套开发板,方便你随时测试,快速完成产品开发:

开发环境: Keil MDK5 配套开发板

Andy

W5500 技术支持专家