Написал небольшого TCP клиента для lwip (RAW), до конца он ещё не доработан, но задачу уже выполняет. Есть пара вопросов, ответы на которые я пока не нашел в справке. 
void client::sendTo(ip_addr_t* ip, uint16_t port, uint8_t* pdata, uint16_t size)
{
  tcp_pcb* tpcb = tcp_new();
  tcp_connect(tpcb, ip, port, &callback::connection_done);
  tcp_err    (tpcb, &callback::connection_error);
  tcp_poll   (tpcb, /*&callback::polling_*/NULL, 0);
  tcp_arg (tpcb, (void*)this);
	
  memcpy(tbuf->payload, pdata, size);
  tlen = size;
  state = state_t::sending;
	
  return;
}
 
В функции sendTo я каждый раз прошу стек выдать мне новый protocol control block для TCP. Затем в колбэке connection_done я пишу данные в стек, а в колбэке send_done закрываю pcb.
err_t client::send_done(tcp_pcb *tpcb, u16_t len)
{
  close(tpcb);
  return ERR_OK;	
}
void client::connection_done(tcp_pcb *tpcb, err_t err)
{
  tcp_sent(tpcb, &callback::send_done);
  err = tcp_write(tpcb, tbuf->payload, tlen, TCP_WRITE_FLAG_COPY);
  err = tcp_output(tpcb);
	
  return;
}
void client::close(tcp_pcb* pcb)
{
  tcp_arg   (pcb, NULL);
  tcp_sent  (pcb, NULL);
  tcp_recv  (pcb, NULL);
  tcp_err   (pcb, NULL);
  tcp_poll  (pcb, NULL, 0);
	
  err_t err = tcp_close(pcb);
  if (err == ERR_OK)
    state = state_t::closed;
  return;
}
 
Не уверен в правильности такого подхода, мне кажется есть решение лучше. lwip_stats ничего аномального не показывает, одномоментно используется только один tcp_pcb, утечек памяти нет.