全部例程

TFTP

W55MH32 其他标签

2025/02/17 更新

本篇文章,我们将详细介绍如何在W55MH32芯片上面实现TFTP协议。并通过实战例程,为大家讲解如何使用TFTP客户端模式向服务器获取文本文件。

该例程用到的其他网络协议,例如DHCP, 请参考相关章节。有关 W55MH32 的初始化过程,请参考Network install,这里将不再赘述。

TFTP协议简介

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

TFTP协议特点

  • 简单性:TFTP协议设计简单,它的报头格式简洁,操作命令种类少,这使得实现起来相对容易,对资源的需求也较低。
  • 轻量级:TFTP协议不需要复杂的连接建立和管理过程,开销小,因此适合在一些对性能要求不高、资源有限的环境中使用。
  • 基于UDP:TFTP使用UDP作为传输层协议,利用了UDP的快速传输和无连接特性,从而能够快速地传输数据。不过,这也意味着TFTP本身不提供可靠的传输保证,需要在应用层实现可靠性机制。
  • 端口固定:TFTP使用固定的端口69来监听客户端的请求。数据传输使用的端口是动态分配的,每次传输会在此基础上选择一个临时端口。
  • 数据块大小限制:每个数据报文最多只能传输512字节的数据,如果文件较大,会分多次传输,每次发送一个512字节的数据块。最后一个数据块可能小于512字节,表示文件的结束。

TFTP协议应用场景

接下来,我们了解下在W55MH32上,可以使用TFTP协议完成哪些操作及应用呢?

  • 固件升级:对于路由器、交换机等网络设备,TFTP协议常用于将固件传输到这些设备以进行固件更新。TFTP协议能够确保固件文件快速、准确地传输到目标设备。
  • 配置文件传输:TFTP协议也常用于管理网络设备的配置文件。将配置文件传输到网络设备以进行配置更新,或者从网络设备下载配置文件进行备份或分析。

TFTP协议基本工作流程

接下来,我们以从TFTP服务器下载文件为例,了解下TFTP的工作流程。

  1. 发起请求:客户端向服务器发送读请求(RRQ,Read Request),这些请求包含了要读取或写入的文件名以及传输模式(如二进制或ASCII码)。
  2. 服务器响应并切换端口: 服务器接收到客户端的请求后随机选择新端口进行数据传输。
  3. 数据传输:服务器发送第一个DATA包(块编号1),客户端收到DATA包后回复ACK(块编号1)。服务器继续发送下一个DATA包(块编号2),循环往复。
  4. 传输终止:当服务器发送的DATA包数据长度小于512字节时,表示文件传输完成。

TFTP协议报文解析

常见的操作码:

  1. 读请求(RRQ),用于请求读取服务器上的文件。
  2. 写请求(WRQ),用于请求向服务器上写入文件。
  3. 数据(DATA),用于传输文件数据。
  4. 回应(ACK),用于确认接收到的数据块。
  5. 错误信息(ERROR),用于报告传输过程中发生的错误。

常见操作码的报文格式如下:

Blog Image

报文示例:

客户端读请求报文:

| 报文原文 |
00 01 74 66 74 70 5f 74 65 73 74 5f 66 69 6c 65 2e 74 78 74 00 6f 63 74 65 74 00 74 69 6d 65 6f 75 74 00 35 00

| 报文解析 |
Trivial File Transfer Protocol
    Opcode: Read Request (1)	(操作码为01,读请求报文)
    Source File: tftp_test_file.txt	(明确要读取的文件名为tftp_test_file.txt)
    Type: octet	(传输模式为octet)
    Option: timeout = 5

服务器响应报文:

| 报文原文 |
00 06 74 69 6d 65 6f 75 74 00 35 00

| 报文解析 |
Trivial File Transfer Protocol
  Opcode: Option Acknowledgement (6)	(操作码为06,扩展操作码)
  [Destination File: tftp_test_file.txt]
  [Read Request in frame 125]
Option: timeout = 5

客户端响应报文:

| 报文原文 |
00 04 00 00

| 报文解析 |
Trivial File Transfer Protocol
Opcode: Acknowledgement (4)	(操作码为04,回应报文)
[Destination File: tftp_test_file.txt]
[Read Request in frame 125]
Block: 0	(数据块标号为00 00)
[Full Block Number: 0]

服务器响应报文:

|报文原文|
00 03 00 01 73 64 61 66 61 73 64 66 61 73 64 66 61 73 64 66 66 66 66 66 66 66 66 41 61 73 64 66 61 73 66 61 66 73 64 66

|报文解析|
Trivial File Transfer Protocol
    Opcode: Data Packet (3)	(操作码为03,数据报文)
    [Destination File: tftp_test_file.txt]
    [Read Request in frame 125]
    Block: 1	(数据块标号为00 01)
    [Full Block Number: 1]
Data (36 bytes)
    Data: 736461666173646661736466617364666666666666666641617364666173666166736466	(数据)
[Length: 36]

客户端响应报文:

|报文原文|
00 04 00 01

|报文解析|
Trivial File Transfer Protocol
    Opcode: Acknowledgement (4)	(操作码为04,回应报文)
    [Destination File: tftp_test_file.txt]
    [Read Request in frame 125]
    Block: 1	(数据块标号为00 01)
    [Full Block Number: 1]

实现过程

接下来,我们在W55MH32上实现TFTP协议读取文件。

步骤1:注册TFTP超时函数


/**
* @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读取函数do_tftp_client()

do_tftp_client()函数如下:


#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(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_i
#endif
  
    g_progress_state = TFTP_PROGRESS;
    send_tftp_rrq(filename, (uint8_t *)TRANS_BINARY, &default_tftp_opt, 1);
}

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

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

TFTP_run()函数如下:


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()函数。

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操作的状态。

运行结果

请注意:

测试实例需要PC端和W55MH32处于同一网段

首先打开tftpd32,选择TFTP目录和服务器地址。

Blog Image

请注意:

服务器地址必须与程序对应,并且TFTP目录下必须有程序中读取的文件

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

Blog Image

总结

本文讲解了如何在 W55MH32 芯片上实现 TFTP 协议,通过实战例程详细展示了使用 TFTP 客户端模式从服务器获取文本文件的过程,涵盖 TFTP 初始化、发送读请求、运行协议并处理结果等核心步骤。文章还对 TFTP 协议的简介、特点、应用场景、基本工作流程和报文解析进行了分析,帮助读者理解其在文件传输中的实际应用价值。 下一篇文章将聚焦 SNMP 协议,解析其核心原理及在网络管理中的应用,同时讲解如何在相关设备上实现 SNMP 功能,敬请期待!

下载本章例程

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

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