quinta-feira, setembro 15, 2022

Usando o WiFi da Raspberry Pi PIco W (Parte 2)

Para começar o meu estudo das funções do SDK de C/C++ da Pi Pico para comunicação TCP/IP, eu resolvi implementar um cliente SNTP. É algo simples e que eu já tinha feito com as funções do Arduino. É um exercício, já que a biblioteca lwIP (que é usada no Pico W) inclui uma implementação disto. Os exemplos do SDK também incluem isso e foi por este exemplo (que você pode ver aqui) que eu comecei o meu estudo.

Uma primeira lida no código do exemplo não foi muito animador... Vamos ver parte por parte para entender, recomendo ler o texto abaixo junto com o fonte. Estou assumindo que vocês conhecem o básico de TCP/IP.

O exemplo funcionando


A primeira parte a examinar é o main() que contém as seguintes chamadas à biblioteca cyw43_arch:

  • cyw43_arch_init() - inicia as bibliotecas de comunicação WiFi
  • cyw43_arch_enable_sta_mode() - coloca no modo "station" (vai se conectar a um AP WiFi)
  • cyw43_arch_wifi_connect_timeout_ms() - tenta se conectar a um AP
  • cyw43_arch_deinit() - encerra o uso das bibliotecas de comunicação WiFi
Estas rotinas estão documentadas no manual do SDK.

Até aqui, tranquilo. Vamos agora olhar run_ntp_test(), onde está o laço principal do programa. O primeiro passo desta rotina é chamar  ntp_init(). O programa é construído em torno de uma estrutura que contém todo o seu estado (NTP_T). ntp_init() aloca esta estrutura e chama duas rotinas da biblioteca lwIP (cuja documentação está aqui):

  • udp_new_ip_type() - esta rotina retorna um ponteiro para uma estrutura udp_pcb. Pcb corresponde a Protocol Control Block, as pcbs contém as informações necessárias para controle das comunicações (no caso UDP, existem pcbs para outros protocolos). 
  • udp_recv() - Aqui definimos a rotina que será chamada quando um pacote UDP for recebido. Como não definimos nenhuma porta no pcb, a rotina será chamada qualquer que seja a porta. Definimos também que a rotina receberá o estado como parâmetro.
Estas funções são parte da API raw (nativa) do lwIP. O lwIP tem outras APIs de mais alto nível, mas requerem o uso junto com um sistema operacional. Esta API funciona principalmente na base de callbacks ("não me chame, eu chamo você quando tiver novidades").

Voltando ao laço principal (e deixando de lado o tratamento de erro), são chamadas as seguintes funções:
  • cyw43_arch_lwip_begin()
  • dns_gethostbyname() - determina o endereço IP de um nome de host.
  • cyw43_arch_lwip_end()
O begin/end cercam as chamadas a lwIP para evitar problemas de concorrência. Existem duas forma de usar a lwIP. Na mais simples, ela funciona por polling e a rotina cyw43_arch_poll() precisa ser chamada periodicamente e begin/end não fazem nada. No modo background o lwIP vai rodar de forma transparente junto com o seu código e begin/end são essenciais.

Vários retornos são possíveis para a rotina dns_gethostbyname(). A primeira vez ela vai retornar ERR_INPROGRESS enquanto processa o pedido, o resultado final vai vir através de um callback. Dando tudo certo, nas próximas chamadas (por um certo tempo) vai usar o resultado anterior e retornar imediatamente com ERR_OK. Valores diferentes destes indicam erro. O callback ntp_dns_found() guarda o endereço IP e chama ntp_request(). Esta mesma rotina é chamada se retornar diretamente ERR_OK.

Chegamos a ntp_request(), que é onde é feita a solicitação SNTP. Aqui está a rotina inteira:
// Make an NTP request
static void ntp_request(NTP_T *state) {
    cyw43_arch_lwip_begin();
    struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, NTP_MSG_LEN, PBUF_RAM);
    uint8_t *req = (uint8_t *) p->payload;
    memset(req, 0, NTP_MSG_LEN);
    req[0] = 0x1b;
    udp_sendto(state->ntp_pcb, p, &state->ntp_server_address, NTP_PORT);
    pbuf_free(p);
    cyw43_arch_lwip_end();
}
Alguns pontos a destacar:
  • O pacote a transmitir é alocado por pbuf_alloc(), uma rotina da lwIP que aloca espaço para os cabeçalhos de protocolo e toma o cuidado de alocar na região correta da memória, conforme o sistema (no caso do RP2040 pode ser qualquer área da SRAM).
  • O envio é feito por udp_sendto(), devidamente cercado por cw43_arch_lwip_begin/end.
  • Quando udp_sendto() retorna, o buffer pode ser descartado
No programa não há associação direta entre os pacotes transmitidos e recebidos. De tempos em tempo a solicitação SNTP é enviada. Pacotes recebidos são tratados assincronamente no callback.

Do ponto de vista do protocolo SNTP esta implementação é bem pouco robusta. Na próxima parte vou escrever a minha versão.

Nenhum comentário: