TFTP
本篇文章,我们将详细介绍如何在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的工作流程。
- 发起请求:客户端向服务器发送读请求(RRQ,Read Request),这些请求包含了要读取或写入的文件名以及传输模式(如二进制或ASCII码)。
- 服务器响应并切换端口: 服务器接收到客户端的请求后随机选择新端口进行数据传输。
- 数据传输:服务器发送第一个DATA包(块编号1),客户端收到DATA包后回复ACK(块编号1)。服务器继续发送下一个DATA包(块编号2),循环往复。
- 传输终止:当服务器发送的DATA包数据长度小于512字节时,表示文件传输完成。
TFTP协议报文解析
常见的操作码:
- 读请求(RRQ),用于请求读取服务器上的文件。
- 写请求(WRQ),用于请求向服务器上写入文件。
- 数据(DATA),用于传输文件数据。
- 回应(ACK),用于确认接收到的数据块。
- 错误信息(ERROR),用于报告传输过程中发生的错误。
常见操作码的报文格式如下:

报文示例:
客户端读请求报文:
| 报文原文 |
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目录和服务器地址。

请注意:
服务器地址必须与程序对应,并且TFTP目录下必须有程序中读取的文件
烧录例程运行后,首先进行了PHY链路检测,然后是DHCP获取网络地址结果,最后打印服务器IP和文本名称,读取文本内容,如下图所示

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