基于STM32 Freertos实现LWIP的TFTP升级

时间:2024-03-25 10:24:15

硬件平台:
芯片 STM32F429IGT6
IDE:MDK5.23
连接方式:开发板直连电脑网络端口

1:首先利用FreeRtos的函数创建一个线程:IAP_tftpd_init
功能在于:创建一个基于端口69的UDP服务器
void tftp_init(void)
{
myConnectUDPHandler = sys_thread_new(“IAP_tftpd_init”, IAP_tftpd_init, NULL, 256,OSUDPECHO_THREAD_PRIO );
}

2:对IAP_tftpd_init函数实现一下功能:
a:创建UDP控制块,绑定自行设置的ip地址以及69端口,这样用来监听的服务器就准备完成了
b:接下来执行while语句,利用netconn_recv函数接收网络端口数据,若没有接收到来自网络发给端口69的数据,会阻塞在这里(当然在OS当中会跳转到其他任务执行)。
c:当收到数据后,判断buf是否存在数据,若满足则再次创建UDP控制块,端口绑定0,用于接收TFTP数据。此为协议规定,此处不做讲解
d:对数据的操作码进行解析,是否为WRQ请求,不是则直接删除端口0的UDP控制块,重新netconn_recv,若是则执行IAP_tftp_process_write函数,后续对IAP_tftp_process_write进行讲解。里面会新创建线程用于接收TFTP文件数据
e:IAP_tftp_process_write执行完成之后,由于在IAP_tftp_process_write里面也有netconn_recv函数,所以我们需要将原先的netconn_recv函数进行屏蔽,故需要挂起IAP_tftpd_init任务,不在执行。恢复myTransfUDPHandler线程(该线程在后续函数讲解中创立)至此IAP_tftpd_init讲解完毕。

static void IAP_tftpd_init(void *arg)
{
  static struct netbuf *buf;
  err_t err;
  unsigned port = 69; /* 69 is the port used for TFTP protocol initial transaction */
  tftp_opcode op;
  struct netconn *p_conn;//用于连接之后的UDP控制块
  /* create a new UDP PCB structure  */
  printf("创建新的UDP控制块\r\n");
  p_conn = netconn_new(NETCONN_UDP);
  if (!p_conn)
  {
    /* Error creating PCB. Out of Memory  */

    return;
  }
  printf("绑定69号端口\r\n");
  /* Bind this PCB to port 69  */
  err = netconn_bind(p_conn, &ipaddr, port);
  if (err == ERR_OK)
  {
    //准备接收数据

    while (1)
    { 
      netconn_recv(p_conn, &buf);//接收数据
     
      if(buf!=NULL)
      {
        printf("收到数据,创建新的UDP线程\r\n");
        conn = netconn_new(NETCONN_UDP);
        if (conn!=NULL)
        {
          printf("绑定0号端口\r\n");
          err = netconn_bind(conn, &ipaddr, 0);
          op = IAP_tftp_decode_op(buf->p->payload);
          if (op != TFTP_WRQ)
          {
             printf("非TFTP_WRQ命令\r\n");
             netconn_delete(conn);
           }
          else
          {
              printf("TFTP_WRQ命令\r\n");
              IAP_tftp_process_write(conn,&(buf->addr), buf->port);
           }
          netbuf_delete(buf);
          printf("开始挂起连接任务。等待传输完成之后回复\r\n");
          osThreadResume(myTransfUDPHandler);
          osThreadSuspend(myConnectUDPHandler);//挂起自己 ,不在执行
        }
        else
        {
          /* Error creating PCB. Out of Memory  */
          printf("创建新的UDP线程失败\r\n");
        }

      }
    }
  
  }
  else
  {
    ;
  }

}

3:IAP_tftp_process_write函数说明:
形参:struct netconn *upcb, 端口为0的UDP控制块
const ip_addr_t *to,发送方的ip地址,存在于buf->addr里面
int to_port,发送方的端口,后续会原路返回,故此作为参数进行传递
新建args参数进行os传输传递,主要记录发送方ip地址,端口,包块数,操作码等信息
b:创建新线程IAP_wrq_recv_callback,用于接收TFTP发来的文件数据,新建后挂起,等待应答TFTP第一个ACK之后在恢复
c:通过调用IAP_tftp_send_ack_packet函数应答TFTP。

/**
  * @brief  Processes TFTP write request
  * @param  to: pointer on the receive IP address
  * @param  to_port: receive port number
  * @retval None
  */
static int IAP_tftp_process_write(struct netconn *upcb, const ip_addr_t *to, int to_port)
{
  tftp_connection_args *args = NULL;
  /* This function is called from a callback,
  * therefore interrupts are disabled,
  * therefore we can use regular malloc   */
  args = mem_malloc(sizeof *args);
  if (!args)
  {
    IAP_tftp_cleanup_wr(upcb, args);
    return 0;
  }

  args->op = TFTP_WRQ;
  args->to_ip.addr = to->addr;
  args->to_port = to_port;
  /* the block # used as a positive response to a WRQ is _always_ 0!!! (see RFC1350)  */
  args->block = 0;
  args->tot_bytes = 0;
  printf("创建IAP_wrq_recv_callback线程,处于挂起状态\r\n");
  myTransfUDPHandler = sys_thread_new("IAP_wrq_recv_callback", IAP_wrq_recv_callback, args, 512,OSUDPECHO_THREAD_PRIO );
  osThreadSuspend(myTransfUDPHandler);//刚申请,暂时不用执行
  total_count =0;
  printf("初始化flash配置\r\n");
  /* init flash */
  FLASH_If_Init();
  printf("开始擦除flash\r\n");
  /* erase user flash area */
  //FLASH_If_Erase(USER_FLASH_FIRST_PAGE_ADDRESS);
  printf("擦除完毕\r\n");
  Flash_Write_Address = USER_FLASH_FIRST_PAGE_ADDRESS;   
  /* 通过应答第一个ACK来启动写事务
  之后再次收到数据会进入IAP_wrq_recv_callback回调函数*/
  printf("通过应答第一个ACK来启动写事务\r\n");
  IAP_tftp_send_ack_packet(upcb, to, to_port, args->block);
  return 0;
}


4:IAP_tftp_send_ack_packet函数说明
在TFTP源码里面,通过RAW接口对其应答包进行发送,而采用OS之后,需要将其发送接口全部修改为NETCON,故此处不多做讲解

/**
  * @brief Sends TFTP ACK packet 
  * @param upcb: pointer on udp_pcb structure
  * @param to: pointer on the receive IP address structure
  * @param to_port: receive port number
  * @param block: block number
  * @retval: err_t: error code
  */
static err_t IAP_tftp_send_ack_packet(struct netconn *upcb, const ip_addr_t *to, int to_port, int block)
{
  err_t err;
  //struct pbuf *pkt_buf; /* Chain of pbuf's to be sent */
  struct netbuf  *pkt_buf = NULL;
  /* create the maximum possible size packet that a TFTP ACK packet can be */
  char packet[TFTP_ACK_PKT_LEN];
    
  memset(packet, 0, TFTP_ACK_PKT_LEN *sizeof(char));

  /* define the first two bytes of the packet */
  IAP_tftp_set_opcode(packet, TFTP_ACK);

  /* Specify the block number being ACK'd.
   * If we are ACK'ing a DATA pkt then the block number echoes that of the DATA pkt being ACK'd (duh)
   * If we are ACK'ing a WRQ pkt then the block number is always 0
   * RRQ packets are never sent ACK pkts by the server, instead the server sends DATA pkts to the
   * host which are, obviously, used as the "acknowledgement".  This saves from having to sEndTransferboth
   * an ACK packet and a DATA packet for RRQs - see RFC1350 for more info.  */
  IAP_tftp_set_block(packet, block);

  /* PBUF_TRANSPORT - specifies the transport layer */

  pkt_buf = netbuf_new();
  netbuf_alloc(pkt_buf,TFTP_ACK_PKT_LEN);
  /* Copy the original data buffer over to the packet buffer's payload */
  memcpy(pkt_buf->p->payload, packet, TFTP_ACK_PKT_LEN);

  /* Sending packet by UDP protocol */
  err = netconn_sendto(upcb, pkt_buf, to, to_port);

  /* free the buffer pbuf */
  netbuf_delete(pkt_buf);

  return err;
}

当应答第一个ACK之后,TFTP开始传输文件数据,每包长度516字节,2字节操作码+2字节Block数+512Data,故需要利用到上述中说明的新线程进行处理IAP_wrq_recv_callback。
5:IAP_wrq_recv_callback函数讲解
a:执行netconn_recv
b:若收到数据后,开始进行解析。
长度是否大于4且包数是否连续,
c:条件满足的情况下开始拷贝到缓存,记录包数block+1,接收包数总长度累加,写入flash等操作
d:若存在一包数据小于512字节,视为最后一包数据,删除buf及控制块,恢复连接任务线程IAP_tftpd_init,删除自身线程
e:删除自身线程后,在再次执行到IAP_tftpd_init任务中,以此循环。

/**
  * @brief  Processes data transfers after a TFTP write request
  * @param  _args: used as pointer on TFTP connection args
  * @param  upcb: pointer on udp_pcb structure
  * @param  pkt_buf: pointer on a pbuf stucture
  * @param  ip_addr: pointer on the receive IP_address structure
  * @param  port: receive port address
  * @retval None
  */
static void IAP_wrq_recv_callback(void *arg)
{
  static struct netbuf *buf;
  tftp_connection_args *args = (tftp_connection_args *)arg;
  uint32_t data_buffer[128];//存储flash数据
  uint16_t count=0;//每次写入的数据长度  Word型
  printf("创建了一个用来接收数据包的线程\r\n");
  for(;;)
  {
    netconn_recv(conn, &buf);//接收数据,若无数据会阻塞在这里

    if(buf!=NULL)
    {
      if (buf->p->len != buf->p->tot_len)
      {
        return;
      }
    /*TFTP中在传输数据时通过这里的数据块序号来判断数据是否有丢失*/
    /*如果接收方得到的DATA包中的序号不是之前一个DATA包中序号值加1,
    那么就判断为接收到的数据包有误,返回ERROR包让发送方重新发送。
    数据块序号从最开始的1开始累加;
    */
    if ((buf->p->len > TFTP_DATA_PKT_HDR_LEN)//>TFTP_DATA_PKT_HDR_LEN说明存在数据包
      &&//判断所接受的数据包是否是连续的下一包数据
      (IAP_tftp_extract_block(buf->p->payload) == (args->block + 1)))
      {
                /* 复制包数据到data_buffer */
        printf(" 复制包数据到data_buffer\r\n");
        printf(" 包长:%d\r\n",buf->p->len);
        printf(" 包块:%d\r\n",args->block);
        pbuf_copy_partial(buf->p, data_buffer, buf->p->len - TFTP_DATA_PKT_HDR_LEN,
                          TFTP_DATA_PKT_HDR_LEN);
        total_count += buf->p->len - TFTP_DATA_PKT_HDR_LEN;//累积所接受数据长度
        printf(" 总长度:%d\r\n",total_count);
        count = (buf->p->len - TFTP_DATA_PKT_HDR_LEN)/4;//因为是按照字进行写入flash 所以/4
        if (((buf->p->len - TFTP_DATA_PKT_HDR_LEN)%4)!=0)
        count++;
        printf(" count:%d\r\n",count);
        printf(" 将数据写到flash\r\n");
        /* 将数据写到flash  每次增加512字节的偏移地址 */
        //FLASH_If_Write(Flash_Write_Address+(args->block<<9), data_buffer ,count);
          
        /* update our block number to match the block number just received */
        args->block++;//每写完一块,之后+1
        /* update total bytes  */
        (args->tot_bytes) += (buf->p->len - TFTP_DATA_PKT_HDR_LEN);//更新总长度

        /* This is a valid pkt but it has no data.  This would occur if the file being
           written is an exact multiple of 512 bytes.  In this case, the args->block
           value must still be updated, but we can skip everything else.    */
       }
       else if (IAP_tftp_extract_block(buf->p->payload) == (args->block + 1))
      {
          /* update our block number to match the block number just received  */
          args->block++;
       }
 
      /*收完一包数据之后发送ACK*/
      /*应答DATA时数据块由1开始依次往上累加以确认数据包是否有漏传。*/
      IAP_tftp_send_ack_packet(conn,&(buf->addr), buf->port,args->block);
      /* 如果最后一次写入返回的值小于最大tftp数据pkt长度,
        然后我们收到了整个文件,所以我们可以退出(这就是tftp的方式
        发出传输结束的信号!
       */
      if (buf->p->len < TFTP_DATA_PKT_LEN_MAX)
      {
        IAP_tftp_cleanup_wr(conn, args);
        netbuf_delete(buf);
        printf("删除传输任务线程IAP_wrq_recv_callback\r\n");
        printf("恢复连接任务线程IAP_tftpd_init\r\n");
        osThreadResume(myConnectUDPHandler);//恢复连接任务     
        vTaskDelete(myTransfUDPHandler);
      }
      else
      {
        netbuf_delete(buf);
      }

    }

  }

上述代码流程就是如此,笔者没有flash的具体擦除写入,仅实现通过TFTP收到数据,相信后续的工作对于大家来说也比较简单。
调试信息如下:
基于STM32 Freertos实现LWIP的TFTP升级
以上就是该方式的实现方式,由于本人第一次写博客,经验不足,有误之处还请指正。