Please note, my primary resource for this webpage is Fairhead and James.
There are two libraries that facilitate WiFi on the Pi Pico W. There exists a very low-level library, cyw43_driver
, that allows for interaction with the CYW43 chip via an SPI interface. For almost everyone, this library is probably a layer or two beneath that on which they will be doing most of their engineering. The pico SDK includes a slightly-higher-level library, pico_cyw43_arch
, which implements some functions that use the low-level driver to do things like scan for access points, connect to access points, etc. This is the lowest-level library that most folks will probably use in their projects, and they'll use this library to setup WiFi rather than sending data over WiFi.
Above this sits the pico_lwip
library, which is a wrapper for Adam Dunkels' Lightweight IP. This is a open-sourced TCP/IP stack designed for embedded systems, and ported to the Pi Pico W.
This webpage focuses only on how one uses the pico_cyw43_arch
library to setup a WiFi connection with a Pico W. Subsequent webpages will discuss how to utilize the pico_lwip
library to exchange data over WiFi via UDP and TCIP/IP. As a resource for this documentation, I am making extensive use of Master the Raspberry Pi Pico in C: WiFi with LWIP and mbedutils. I'm also making use of the LWIP examples, and the pi pico examples. Bruce also has some really nice examples from which to start a project.
Before describing this process for the pico in particular, it is helpful to understand the process in general. When we connect a device to a WiFi network, the following occurs:
After connection, of course, there's a whole other protocol by which information is actually exchanged. But let's save that conversation, and focus only on getting a Pico W connected to a network.
Before connecting to a WiFi network, we must initialize the CYW43 chip itself. As a part of this initialization, we must tell the chip which country its in, so that it knows which channels and power levels it should use. In order to do this, we have two options. We could either call the pico_cyw43_arch
library function int cyw43_arch_init_with_country(uint32_t country)
which, as an argument, takes the country code. Macros for these country codes are available at pico-sdk/lib/cyw43-driver/src/cyw43_country.h. Alternatively, we can simply call int cyw43_arch_init(void)
, which uses the country code set by PICO_CYW43_ARCH_DEFAULT_COUNTRY_CODE
in cyw43_arch.h
. By default, this is set to "worldwide." Everything that happens when we call this function is described to excruciating depth on this webpage.
Our Pico can act either as a client in station mode (in which it connects to a network) or as an access point (in which it generates a network to which other devices connect). We will consider each, but let us start with station mode, in which the Pico W will connect to a network. We enable station mode by way of the pico_cyw43_arch
library function cy243_arch_enable_sta_mode()
.
After having initialized the hardware and putting it into station mode, we can now make a connection to an access point. There are three types of connection: blocking, timout, and async. As the names suggest, a blocking connection will block until a connection has been made, timeout will block until a specified time has passed, and async will attempt an asynchronous connection in a non-blocking fashion. Furthermore, each of these three types of connection comes in two versions, one that includes a MAC address and one that does not. All options are enumerated below. Note that the auth
argument represents the security protocol. Macros for all options are available in pico-sdk/lib/cyw43-driver/src/cyw43_ll.h.
int cyw43_arch_wifi_connect_blocking(const char *ssid, const char *pw, uint32_t auth)
int cyw43_arch_wifi_connect_bssid_blocking(const char *ssid, const uint8_t *bssid, const char *pw, uint32_t auth)
int cyw43_arch_wifi_connect_timeout_ms(const char *ssid, const char *pw, uint32_t auth, uint32_t timeout_ms)
int cyw43_arch_wifi_connect_bssid_timeout_ms(const char *ssid, const uint8_t *bssid, const char *pw, uint32_t auth, uint32_t timeout_ms)
int cyw43_arch_wifi_connect_async(const char *ssid, const char *pw, uint32_t auth)
int cyw43_arch_wifi_connect_bssid_async(const char *ssid, const uint8_t *bssid, const char *pw, uint32_t auth)
After having made the connection to a network, we are out of the lower-level pico_cyw43_arch
library and up into the pico_lwip
library. This library contains a number of different modules, but the module of relevance for viewing and customizing our network interface is NETIF. It is via this module that we can learn all the network information that the access point gave us upon connection: our IP, netmask, gateway, etc. In particular, all this information gets stored in a struct called netif_default
. This struct has three fields: ip_attr_t ip_addr
, ip_addr_t netmask
, and ip_addr_t gw
which represent our assigned IP, netmask, and gateway.
The SDK provides some macros for viewing and modifying these network settings. To print the fields of the global netif_default
struct, we can use the following (taken from Fairhead and James:
printf("IP: %s\n", ip4addr_ntoa(netif_ip_addr(netif_default))) ;
printf("Mask: %s\n", ip4addr_ntoa(netif_ip_netmask4(netif_default))) ;
printf("Gateway: %s\n", ip4addr_ntoa(netif_ip_gw4(netif_default))) ;
But perhaps we want to assign our Pico W a fixed IP address! In order to do this, we could do the following (again borrowed from Fairhead and James):
// create an object of type ip_addr_t called ip
ip_addr_t ip ;
// Use an SDK macro to set the value of this ip object to our desired IP address
IP4_ADDR(&ip, 192, 168, 253, 210) ;
// Set the IP field of the netif_default struct to our desired IP address
netif_set_ipaddr(netif_default, &ip) ;
// Use an SDK macro to set the value of the ip object to our desired netmask
IP4_ADDR(&ip, 255, 255, 255, 0) ;
// Set the netmask field of the netif_default struct to our desired netmask
netif_set_netmask(netif_default, &ip) ;
// Use an SDK macro to set the value of this ip object to our desired gateway
IP4_ADDR(&ip, 192, 168, 253, 210) ;
// Set the gateway field of the netif_default struct to our desired gateway
netif_set_gw(netif_default, &ip)
We can consolidate all of the above steps into a single, reusable function. It probably makes sense to consolidate all of this into its own header file, perhaps called something like connect.h
. Please note that this is a minimal example. You might imagine augmenting this such that the user can pass desired IP, gateway, and netmask values in as arguments. You might also use the async connection to provide a bit more feedback during the connection process.
#include <stdio.h>
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
char ssid[] = "hostname" ;
char password[] = "password" ;
uint32_t country = CYW43_COUNTRY_USA ;
uint32_t auth = CYW43_AUTH_WPA2_MIXED_PSK ;
int connectWifi(uint32_t country, const char *ssid, const char *pass, uint32_t auth) {
// Initialize the hardware
if (cyw43_arch_init_with_country(country)) {
printf("Failed to initialize hardware.\n") ;
return 1 ;
}
// Make sure the LED is off
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0) ;
// Put the device into station mode
cyw43_arch_enable_sta_mode() ;
// Print a status message
printf("Attempting connection . . . \n") ;
// Connect to the network
if (cyw43_arch_wifi_connect_blocking(ssid, pass, auth)) {
return 2 ;
}
// Use the LED to indicate connection success
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1) ;
// Report the IP, netmask, and gateway that we've been assigned
printf("IP: %s\n", ip4addr_ntoa(netif_ip_addr4(netif_default))) ;
printf("Mask: %s\n", ip4addr_ntoa(netif_ip_netmask4(netif_default))) ;
printf("Gateway: %s\n", ip4addr_ntoa(netif_ip_gw4(netif_default))) ;
return 0 ;
}
We could now import the above header file into our application and use it to connect to a WiFi network. There's one last configuration that's required, however. The WiFi chip needs periodic attention, and we can give it that attention in a few different ways. If we choose to setup the system in polling mode, then it is our responsibility, in our program, to periodically call cyw43_arch_poll()
(around every 50 ms). Alternatively, we could run the system in threadsafe background mode, which will automatically call the poll function in an interrupt. Finally, we could set this up in FreeRTOS mode, which incorporates lwip into the FreeRTOS infrastructure. We choose which of these three modes we'd like to use by linking the associated library in the project's CMakeLists.txt file. For the above file, the target_link_libraries
line of the CMakeLists would look like the following if we wanted to use threadsafe background mode:
target_link_libraries(project_name pico_stdlib, pico_cyw43_arch_lwip_threadsafe_background)
If instead we wanted to poll, we'd link pico_cyw43_arch_lwip_poll
. And if instead we wanted to use FreeRTOS, we'd use pico_cyw43_arch_lwip_sys_freertos
.
Perhaps we want to scan for available WiFi networks. The pico_cyw43_arch
library includes a function which allows for us to discover available access points. That function looks like the following:
int cyw43_wifi_scan(cyw43_t *self, cyw43_wifi_scan_options_t *opts, void *env, int (*result_cb)(void *, const cyw43_ev_scan_result_t *))
The first argument is a pointer to a cyw43_t struct. During initalization, a global object of this type gets created, and is named cyw43_state
. When we actually call this function, we'll pass a pointer to this global struct for this argument. The second argument, *opts
, must be set to zero. The third argument gets NULL
, and the final argument is a pointer to the scan result callback function. So, calling this function would look something like the following (from Fairhead and James):
cyw43_wifi_scan_options_t scan_options = {0} ;
int err = cyw43_wifi_scan(&cyw43_state, &scan_options, NULL, scan_result) ;
But what about that callback function, scan_result
? This callback function should take two arguments. The first is a null pointer, and the second is a pointer to an object of type cyw43_ev_scan_result_t
, as shown below:
static int scan_result(void *env, const cyw43_ev_scan_result_t *result)
That second argument is a pointer to the following struct:
typedef struct _cyw43_ev_scan_result_t {
uint32_t _0[5];
uint8_t bssid[6]; ///< access point mac address
uint16_t _1[2];
uint8_t ssid_len; ///< length of wlan access point name
uint8_t ssid[32]; ///< wlan access point name
uint32_t _2[5];
uint16_t channel; ///< wifi channel
uint16_t _3;
uint8_t auth_mode; ///< wifi auth mode \ref CYW43_AUTH_
int16_t rssi; ///< signal strength
} cyw43_ev_scan_result_t;
Our callback function could go through and print each field of the struct that it receives the pointer to as an argument to learn about each access point.
The Received Signal Strength Indicator (RSSI) provides a measure of an access point's signal strength. The pico_cyw43_arch
library provides a mechanism for learning this value for the access point with which we've connected. The function for doing this is the following:
cyw43_wifi_get_rssi(&cyw43_state, &rssi)
The first argument is a pointer to the automatically-generated global struct of type cyw43_t
that gets created during initialization. The second argument, &rssi
, is simply a pointer to an int32_t
. This function will dereferece this pointer and set the value at the pointer address to the RSSI of the connected access point.