In my last post on the Nordic Semiconductor Thingy:91 X IoT prototyping platform, I outlined the features and architecture of the device. The combination of wireless protocols on the Thingy:91 X (Bluetooth LE, LTE-M, Wi-Fi) make it a compelling foundation for a wide variety of applications. However, there are a few intricacies to the protocol support.

Namely, the primary stated purpose of the Wi-Fi support is for network-based positioning (i.e. scan for local access points, send them to a server, get approximate location back). This leverages the nRF7002’s radio, but uses it in scan mode, consuming less resources on the host MCU. This is important, particularly if the nRF9151 is acting as the host MCU rather than the nRF5340, as the former has constrained RAM (256 KB) and does not support external execute-in-place (XIP). External XIP allows a device to relocate some of its application code to an external flash memory component, then include the external flash in its memory map in order to execute the relocated code. The nRF5340 supports external XIP, allowing it to accommodate applications with larger code size despite having the same amount of internal flash memory (1 MB) as the nRF9151. It also has 512 KB of RAM compared to the nRF9151’s 256 KB.

There are two aspects of Wi-Fi support on the nRF70 series companion ICs that impact resource consumption on the host MCU: patches and the driver. The firmware patches are loaded onto the nRF70 device by the host MCU and add functionality to the base ROM. They are included in the host MCUs firmware by default as part of the Wi-Fi driver it leverages to communicate with the nRF70 device. This increases the code size, which, as previously mentioned, can be partially mitigated if the host supports external XIP (SB_CONFIG_WIFI_PATCHES_EXT_FLASH_XIP). The nRF70 series also supports loading patches from non-XIP QSPI flash (SB_CONFIG_WIFI_PATCHES_EXT_FLASH_STORE). In this case the Wi-Fi driver on the host loads the patch into RAM, then transfers it to the nRF70 device. As one might expect, the patch that only enables scan mode is significantly smaller (32 KB) than the patch that enables full functionality (77.8 KB).

The second, and potentially more impactful, contributor to resource consumption on the host MCU is the flash and RAM consumption of the driver itself. Specifically, to use an nRF70 device in station mode, WPA supplicant is required, which can add anywhere from 180 KB to 250 KB to code size (flash). The total driver RAM consumption also increases from around 30 KB to around 130 KB. Attempting to fit anything of consequence alongside it into 256 KB of RAM on the nRF9151 would be challenging, which is likely why the current documentation does not indicate station mode support for nRF91 series devices (all of which have 256 KB of RAM).

However, from a raw resource perspective, the simplest station mode application should be able to run on the nRF9151. I noticed that the example included support for the nRF52840 DK (with the nRF7002 EK shield), and the nRF52840 SoC matches the nRF9151 in RAM (256 KB) and flash size (1 MB). Surely we could get it working on the nRF9151 as well.

To enable Wi-Fi on the nRF9151, an overlay file that includes the thingy91x_wifi.dtsi devictree file is necessary.

thingy91x_nrf9151_ns.overlay

#include <thingy91x_wifi.dtsi>

This serves to enable the nRF7002 and its dedicated power management IC (PMIC) (nPM6001), as well as the short range (Bluetooth, Wi-Fi) capabilities of the RF front end.

thingy91x_wifi.dtsi

/*
 * Copyright (c) 2024 Nordic Semiconductor
 * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
 */

/* Enable short range RF */
&ldsw_rf_fe_sr_en {
	/delete-property/ output-low;
	output-high;
};

/* Set pmic_wifi enable signal */
&ldsw_nPM6001_en {
	/delete-property/ regulator-boot-off;
	regulator-boot-on;
};

/* Enable pmic_wifi */
&pmic_wifi {
	status = "okay";
	regulators {
		status = "okay";
	};
};

/* enable nRF70 */
&nrf70 {
	status = "okay";
};

If we look at the schematic, both the Wi-Fi PMIC (ldsw_nPM6001_en) and the RF front end (ldsw_rf_fe_sr_en) enable signals are connected to the main PMIC (nPM1300).

nrf9151-wifi-host-0

ldsw_nPM6001_en is enabled via the regulator-boot-on label in the included devicetree file, which will cause it to be turned on when the nPM1300 driver is initialized.

The RF frontend is not enabled until later alongside the nPM6001 regulators. When CONFIG_WIFI_NRF70=y, the nrf70_support.c “library” is included in the build via the Thingy:91 X board CMakeLists.txt.

CMakeLists.txt

if(CONFIG_WIFI_NRF70)
  if(CONFIG_BOARD_THINGY91X_NRF9151 OR CONFIG_BOARD_THINGY91X_NRF9151_NS)
    zephyr_library()
    zephyr_library_sources(nrf70_support.c)
  endif()
endif()

When the Wi-Fi interface is brought up, the nrf_wifi_if_zep_start_board function is invoked.

nrf70_support.c

static const struct device *wifi_regulator = DEVICE_DT_GET(DT_NODELABEL(reg_wifi));

static const struct gpio_dt_spec ldsw_rf_fe_sr_en = {
	.port = DEVICE_DT_GET(DT_PARENT(DT_NODELABEL(ldsw_rf_fe_sr_en))),
	.pin = 1,
	.dt_flags = GPIO_ACTIVE_LOW | GPIO_OPEN_DRAIN | GPIO_PULL_UP,
};

/* Enable the regulator and set the GPIO pin to enable the RF frontend */
int nrf_wifi_if_zep_start_board(const struct device *dev)
{
	ARG_UNUSED(dev);
	int ret;

	ret = regulator_enable(wifi_regulator);
	if (ret) {
		LOG_ERR("Cannot turn on regulator %s (%d)", wifi_regulator->name, ret);
		return ret;
	}
	ret = regulator_set_mode(wifi_regulator, NPM6001_MODE_PWM);
	if (ret) {
		LOG_ERR("Cannot set mode for regulator %s (%d)", wifi_regulator->name, ret);
		return ret;
	}

	ret = gpio_pin_set_dt(&ldsw_rf_fe_sr_en, 1);
	if (ret) {
		LOG_ERR("Cannot set GPIO %s (%d)", ldsw_rf_fe_sr_en.port->name, ret);
		return ret;
	}

	/* Wait for the load switch to settle on operating voltage. */
	k_sleep(K_USEC(300));

	return 0;
}

The regulator_enable and regulator_set_mode calls utilitze the nPM6001 driver to configure the reg_wifi regulator via the I2C connection to the nPM6001. Then the RF front end short range capabilities are enabled via the gpio_pin_set_dt call. Because the port of ldsw_rf_fe_sr_en is set to the parent of the devicetree node label, the GPIO is pulled high by talking to the nPM1300 over I2C. The generated devicetree file makes these relationships plain.

zephyr.dts

			i2c2: i2c@a000 {
				compatible = "nordic,nrf-twim";
				#address-cells = < 0x1 >;
				#size-cells = < 0x0 >;
				reg = < 0xa000 0x1000 >;
				interrupts = < 0xa 0x1 >;
				easydma-maxcnt-bits = < 0xd >;
				status = "okay";
				zephyr,pm-device-runtime-auto;
				clock-frequency = < 0x186a0 >;
				pinctrl-0 = < &i2c2_default >;
				pinctrl-1 = < &i2c2_sleep >;
				pinctrl-names = "default", "sleep";
				bme680: bme680@76 {
					status = "disabled";
					compatible = "bosch,bme680";
					reg = < 0x76 >;
				};
				pmic_main: npm1300@6b {
					compatible = "nordic,npm1300";
					status = "okay";
					pmic-int-pin = < 0x3 >;
					reg = < 0x6b >;
					host-int-gpios = < &gpio0 0x2 0x0 >;
					gpios_pmic: npm1300_gpios {
						compatible = "nordic,npm1300-gpio";
						status = "okay";
						gpio-controller;
						#gpio-cells = < 0x2 >;
						ngpios = < 0x5 >;
						phandle = < 0x9 >;
						npm13_button: GPIO0 {
							gpio-hog;
							gpios = < 0x0 0x0 >;
						};
						ldsw_rf_fe_sr_en: GPIO1 {
							gpio-hog;
							gpios = < 0x1 0x17 >;
							output-high;
						};
						power_switch: GPIO2 {
							gpio-hog;
							gpios = < 0x2 0x0 >;
						};
						npm6001_ready: GPIO4 {
							gpio-hog;
							gpios = < 0x4 0x0 >;
						};
					};
					regulators {
						compatible = "nordic,npm1300-regulator";
						status = "okay";
						reg_3v3: BUCK2 {
							regulator-min-microvolt = < 0x325aa0 >;
							regulator-max-microvolt = < 0x325aa0 >;
							enable-gpios = < &gpios_pmic 0x2 0x0 >;
						};
						ldsw_nPM6001_en: LDO1 {
							regulator-initial-mode = < 0x3 >;
							regulator-allowed-modes = < 0x3 >;
							regulator-boot-on;
						};
						ldsw_sensors: LDO2 {
							regulator-initial-mode = < 0x3 >;
							regulator-allowed-modes = < 0x3 >;
							regulator-boot-on;
						};
					};
					npm1300_charger: charger {
						compatible = "nordic,npm1300-charger";
						status = "okay";
						vbus-limit-microamp = < 0x7a120 >;
						term-microvolt = < 0x401640 >;
						current-microamp = < 0xa4cb8 >;
						dischg-limit-microamp = < 0x147260 >;
						thermistor-cold-millidegrees = < 0x0 >;
						thermistor-cool-millidegrees = < 0x0 >;
						thermistor-warm-millidegrees = < 0xafc8 >;
						thermistor-hot-millidegrees = < 0xafc8 >;
						thermistor-ohms = < 0x2710 >;
						thermistor-beta = < 0xd6b >;
						charging-enable;
					};
				};
				pmic_wifi: npm6001@70 {
					status = "okay";
					compatible = "nordic,npm6001";
					reg = < 0x70 >;
					regulators {
						compatible = "nordic,npm6001-regulator";
						status = "okay";
						pmic_wifi_buck0: BUCK0 {
							regulator-boot-off;
						};
						pmic_wifi_buck1: BUCK1 {
							regulator-boot-off;
						};
						pmic_wifi_buck2: BUCK2 {
							regulator-boot-off;
						};
						reg_wifi: BUCK3 {
							regulator-min-microvolt = < 0x325aa0 >;
							regulator-max-microvolt = < 0x325aa0 >;
							regulator-initial-mode = < 0x0 >;
							regulator-boot-on;
						};
					};
				};
				accel: accelerometer_lp: adxl367@1d {
					status = "disabled";
					compatible = "adi,adxl367";
					odr = < 0x3 >;
					reg = < 0x1d >;
					int1-gpios = < &gpio0 0xb 0x0 >;
				};
				magnetometer: bmm350@14 {
					status = "disabled";
					compatible = "bosch,bmm350";
					reg = < 0x14 >;
					drdy-gpios = < &gpio0 0x7 0x1 >;
				};
			};

Because this setup is just enabling the hardware necessary to use Wi-Fi capabilities, it is the same whether using the nRF7002 in scan or station mode. Kconfig is used to determine the firmware support, including what patch is included and loaded. By default, CONFIG_NRF70_SYSTEM_MODE=y is selected when using the nRF7002, which results in the default patch being included via the nrf_wifi driver.

CMakeLists.txt

if (CONFIG_NRF_WIFI_PATCHES_BUILTIN)
  zephyr_blobs_verify(MODULE nrf_wifi REQUIRED)
  # RPU FW patch binaries based on the selected configuration
  if(CONFIG_NRF70_SYSTEM_MODE)
    set(NRF70_PATCH ${FW_BINS_BASE}/default/nrf70.bin)
  elseif(CONFIG_NRF70_RADIO_TEST)
    set(NRF70_PATCH ${FW_BINS_BASE}/radio_test/nrf70.bin)
  elseif(CONFIG_NRF70_SCAN_ONLY)
    set(NRF70_PATCH ${FW_BINS_BASE}/scan_only/nrf70.bin)
  elseif (CONFIG_NRF70_SYSTEM_WITH_RAW_MODES)
    set(NRF70_PATCH ${FW_BINS_BASE}/system_with_raw/nrf70.bin)
  elseif(CONFIG_NRF70_OFFLOADED_RAW_TX)
    set(NRF70_PATCH ${FW_BINS_BASE}/offloaded_raw_tx/nrf70.bin)
  else()
    # Error
    message(FATAL_ERROR "Unsupported nRF70 patch configuration")
  endif()

  set(gen_inc_dir ${ZEPHYR_BINARY_DIR}/misc/generated)
  zephyr_include_directories(${gen_inc_dir})
  set(gen_dir ${gen_inc_dir}/nrf70_fw_patch)
  generate_inc_file_for_target(
    nrf_wifi
    ${NRF70_PATCH}
    ${gen_dir}/nrf70.bin.inc
  )
endif()

The station example provides the configuration for this setup in the prj.conf. The application can be built for the nRF9151 on the Thingy:91 X with the following command.

west build -p -b thingy91x/nrf9151/ns .

“Barely” in the title of this post might be an understatement. The build is successful, but utilies 99.11% of available RAM.

[501/501] Linking C executable zephyr/zephyr.elf
Memory region         Used Size  Region Size  %age Used
           FLASH:      542612 B       800 KB     66.24%
             RAM:      201600 B     203416 B     99.11%
        IDT_LIST:          0 GB        32 KB      0.00%

I had set my Wi-Fi SSID (CONFIG_WIFI_CREDENTIALS_STATIC_SSID) and password (CONFIG_WIFI_CREDENTIALS_STATIC_PASSWORD) in the prj.conf, but on my first attempt of programming the Thingy:91 X with west flash --recover, I observed the following output.

*** Booting nRF Connect SDK v2.9.0-7787b2649840 ***
*** Using Zephyr OS v3.7.99-1f8f3dc29142 ***
[00:00:00.565,917] <inf> net_config: Initializing network
[00:00:00.565,948] <inf> net_config: Waiting interface 1 (0x2000f5a0) to be up...
[00:00:00.566,131] <inf> net_config: IPv4 address: 192.168.1.99
[00:00:00.566,192] <inf> net_config: Running dhcpv4 client...
[00:00:00.566,833] <inf> sta: Starting thingy91x with CPU frequency: 64 MHz
[00:00:00.568,542] <inf> wifi_supplicant: wpa_supplicant initialized
[00:00:01.566,986] <inf> sta: Static IP address (overridable): 192.168.1.99/255.255.255.0 -> 192.168.1.1
[00:00:01.567,138] <err> wifi_mgmt_ext: Connection request failed
[00:00:01.567,138] <err> sta: Connection request failed
[00:00:01.567,169] <inf> sta: Status request failed
[00:00:01.620,666] <err> wifi_supplicant: Failed to add iface wlan0

The error was orginating from the following line in the application.

main.c

	if (net_mgmt(NET_REQUEST_WIFI_CONNECT_STORED, iface, NULL, 0)) {
		LOG_ERR("Connection request failed");

		return -ENOEXEC;
	}

The log line from wifi_mgmt_ext provided a hint that this network management command was being handled by Nordic’s extended Wi-Fi command library.

wifi_mgmt_ext.c

static int wifi_ext_command(uint32_t mgmt_request, struct net_if *iface, void *data, size_t len)
{
	int ret = 0;

	ret = add_static_network_config(iface);
	if (ret) {
		return ret;
	}

	wifi_credentials_for_each_ssid(add_stored_network, iface);

	return ret;
};

NET_MGMT_REGISTER_REQUEST_HANDLER(NET_REQUEST_WIFI_CONNECT_STORED, wifi_ext_command);

To quickly diagnose the problem, I used west attach to connect with GDB and step through the call stack. I started by setting a breakpoint on wifi_ext_command and resetting the target.

(gdb) b wifi_ext_command
Breakpoint 1 at 0x6c8e0
(gdb) monitor reset
Resetting target

Shortly after boot, the application hit the breakpoint.

Breakpoint 1, wifi_ext_command (mgmt_request=mgmt_request@entry=1364590702, iface=0x2000f5a0 <__net_if_dts_ord_126_0>, data=data@entry=0x0, len=len@entry=0)

The iface being passed matches the devicetree number of the wlan0 interface accessed via SPI on the nRF7002.

 *   126 /soc/peripheral@40000000/spi@b000/wifi@1/wlan0

Stepping through add_static_network_config, it returns with a call to add_network_from_credentials_struct_personal, which invokes the NET_REQUEST_WIFI_CONNECT command.

wifi_mgmt_ext.c

	if (net_mgmt(NET_REQUEST_WIFI_CONNECT, iface, &cnx_params,
		     sizeof(struct wifi_connect_req_params))) {
		LOG_ERR("Connection request failed\n");

		return -ENOEXEC;
	}

The connect command is handled by Zephyr’s L2 network management library.

wifi_mgmt.c

static int wifi_connect(uint32_t mgmt_request, struct net_if *iface,
			void *data, size_t len)
{
	struct wifi_connect_req_params *params =
		(struct wifi_connect_req_params *)data;
	const struct device *dev = net_if_get_device(iface);

	const struct wifi_mgmt_ops *const wifi_mgmt_api = get_wifi_api(iface);

	if (wifi_mgmt_api == NULL || wifi_mgmt_api->connect == NULL) {
		return -ENOTSUP;
	}

	if (!net_if_is_admin_up(iface)) {
		return -ENETDOWN;
	}

	LOG_HEXDUMP_DBG(params->ssid, params->ssid_length, "ssid");
	LOG_HEXDUMP_DBG(params->psk, params->psk_length, "psk");
	if (params->sae_password) {
		LOG_HEXDUMP_DBG(params->sae_password, params->sae_password_length, "sae");
	}
	NET_DBG("ch %u sec %u", params->channel, params->security);

	if ((params->security > WIFI_SECURITY_TYPE_MAX) ||
	    (params->ssid_length > WIFI_SSID_MAX_LEN) ||
	    (params->ssid_length == 0U) ||
	    ((params->security == WIFI_SECURITY_TYPE_PSK ||
		  params->security == WIFI_SECURITY_TYPE_WPA_PSK ||
		  params->security == WIFI_SECURITY_TYPE_PSK_SHA256 ||
		  params->security == WIFI_SECURITY_TYPE_WPA_AUTO_PERSONAL) &&
	     ((params->psk_length < 8) || (params->psk_length > 64) ||
	      (params->psk_length == 0U) || !params->psk)) ||
	    ((params->security == WIFI_SECURITY_TYPE_SAE_HNP ||
		  params->security == WIFI_SECURITY_TYPE_SAE_H2E ||
		  params->security == WIFI_SECURITY_TYPE_SAE_AUTO) &&
	      ((params->psk_length == 0U) || !params->psk) &&
		  ((params->sae_password_length == 0U) || !params->sae_password)) ||
	    ((params->channel != WIFI_CHANNEL_ANY) &&
	     (params->channel > WIFI_CHANNEL_MAX)) ||
	    !params->ssid) {
		return -EINVAL;
	}

#ifdef CONFIG_WIFI_NM_WPA_SUPPLICANT_ROAMING
	memset(&roaming_params, 0x0, sizeof(roaming_params));
	roaming_params.is_11r_used = params->ft_used;
#endif

	return wifi_mgmt_api->connect(dev, params);
}

NET_MGMT_REGISTER_REQUEST_HANDLER(NET_REQUEST_WIFI_CONNECT, wifi_connect);

The failure was occurring at get_wifi_api, which returned a wifi_mgmt_api, but the connect callback was NULL.

(gdb) p *wifi_mgmt_api
$15 = {scan = 0x68a75 <nrf_wifi_disp_scan_zep>, connect = 0x0, disconnect = 0x0, ap_enable = 0x0, ap_disable = 0x0, ap_sta_disconnect = 0x0, iface_status = 0x0, 
  set_power_save = 0x68f91 <nrf_wifi_set_power_save>, set_twt = 0x6934d <nrf_wifi_set_twt>, get_power_save_config = 0x69119 <nrf_wifi_get_power_save_config>, reg_domain = 0x67ab5 <nrf_wifi_reg_domain>, 
  filter = 0x0, mode = 0x0, channel = 0x0, btm_query = 0x0, get_version = 0x0, get_conn_params = 0x0, set_rts_threshold = 0x69831 <nrf_wifi_set_rts_threshold>, ap_config_params = 0x0, pmksa_flush = 0x0, 
  get_rts_threshold = 0x69959 <nrf_wifi_get_rts_threshold>, wps_config = 0x0}

The connect callback should be provided by the “glue code” that ties wpa_supplicant into Zephyr’s networking stack.

supp_main.c

static const struct wifi_mgmt_ops mgmt_ops = {
	.get_version = supplicant_get_version,
	.scan = supplicant_scan,
	.connect = supplicant_connect,
	.disconnect = supplicant_disconnect,
...

The wpa_supplicant error log was a good hint as to why that may not be happening.

[00:00:01.620,666] <err> wifi_supplicant: Failed to add iface wlan0

The log originates in the wpa_supplicant_add_iface function when attempting to initialize the interface.

wpa_supplicant.c

	if (wpa_supplicant_init_iface(wpa_s, &t_iface)) {
		wpa_printf(MSG_DEBUG, "Failed to add interface %s",
			   iface->ifname);
		wpa_supplicant_deinit_iface(wpa_s, 0, 0);
		return NULL;
	}

I went further down the rabbit hole of the steps required for initialization than I have space to enumerate here, but the issue became obvious after setting a breakpoint and stepping through the call stack.

(gdb) where
#0  l2_packet_init (ifname=ifname@entry=0x2002ca78 <kheap.system_heap+48680> "wlan0", own_addr=<optimized out>, protocol=protocol@entry=34958, rx_callback=0x4a781 <wpa_supplicant_rx_eapol>, 
    rx_callback_ctx=rx_callback_ctx@entry=0x2002ca28 <kheap.system_heap+48600>, l2_hdr=l2_hdr@entry=0)
#1  0x0004ae3a in wpa_supplicant_update_mac_addr (wpa_s=wpa_s@entry=0x2002ca28 <kheap.system_heap+48600>)
#2  0x0004b358 in wpa_supplicant_driver_init (wpa_s=wpa_s@entry=0x2002ca28 <kheap.system_heap+48600>)
#3  0x0004bdf0 in wpa_supplicant_init_iface (iface=<synthetic pointer>, wpa_s=0x2002ca28 <kheap.system_heap+48600>)
#4  wpa_supplicant_add_iface (global=global@entry=0x20028668 <kheap.system_heap+31256>, iface=iface@entry=0x20017af0 <supplicant_thread_stack+6808>, parent=parent@entry=0x0)

l2_packet_init is where we return back to Zephyr land, eventually attempting to create a socket.

l2_packet_zephyr.c

	l2->fd = socket(AF_PACKET, l2_hdr ? SOCK_RAW : SOCK_DGRAM,
			htons(protocol));
	if (l2->fd < 0) {
		wpa_printf(MSG_ERROR,
			   "Failed to open socket: %s, proto: %d, af: %d",
			   strerror(errno), htons(protocol), AF_PACKET);
		goto fail;
	}

Stepping through the socket call, we can see that the creation handler is being provided by the nrf_modem_lib. In other words, we are attempting to create an offloaded socket on the nRF9151 modem, then use it for Wi-Fi via the nRF7002.

Breakpoint 10, socket (family=3, type=2, proto=36488)
394		return zsock_socket(family, type, proto);
(gdb) s
zsock_socket (proto=36488, type=2, family=3)
60		return z_impl_zsock_socket(family, type, proto);
(gdb) s
z_impl_zsock_socket (family=3, type=2, proto=36488)
109		STRUCT_SECTION_FOREACH(net_socket_register, sock_family) {
(gdb) s
112			if (sock_family->family != family &&
(gdb) 
119			if (!sock_family->is_supported(family, type, proto)) {
(gdb) 
nrf9x_socket_is_supported (family=3, type=2, proto=36488)
1045		if (offload_disabled) {
...
0x00057afe in z_impl_zsock_socket (family=3, type=2, proto=36488)
123			errno = 0;
(gdb) s
124			ret = sock_family->handler(family, type, proto);
(gdb) 
nrf9x_socket_create (family=3, type=2, proto=36488)
1079		if (type & SOCK_NATIVE) {
(gdb) s
1090		sd = nrf9x_socket_offload_socket(family, type, proto);
(gdb) s
nrf9x_socket_offload_socket (proto=36488, type=2, family=3)
336		retval = nrf_socket(family, type, proto);
(gdb) s
nrf9x_socket_create (proto=36488, type=2, family=3)
1091		if (sd < 0) {
nrf9x_socket_create (proto=36488, type=<optimized out>, family=3)
1093			return -1;

The small fix here is to disable the nrf_modem_lib, which is enabled by default on the nRF9151. This can be accomplished by adding a board-specific .conf file for the Thingy:91 X.

thingy91x_nrf9151_ns.conf

CONFIG_NRF_MODEM_LIB=n

Building and flashing again results in the expected output, and successful connection to the local Wi-Fi network.

*** Booting nRF Connect SDK v2.9.0-7787b2649840 ***
*** Using Zephyr OS v3.7.99-1f8f3dc29142 ***
[00:00:00.562,683] <inf> net_config: Initializing network
[00:00:00.562,713] <inf> net_config: Waiting interface 1 (0x2000aff0) to be up...
[00:00:00.562,896] <inf> net_config: IPv4 address: 192.168.1.99
[00:00:00.562,957] <inf> net_config: Running dhcpv4 client...
[00:00:00.564,208] <inf> sta: Starting thingy91x with CPU frequency: 64 MHz
[00:00:00.566,162] <inf> wifi_supplicant: wpa_supplicant initialized
[00:00:01.564,483] <inf> sta: Static IP address (overridable): 1��.168.1.99/255.255.255.0 -> 192.168.1.1
[00:00:01.564,514] <inf> sta: Waiting for Wi-Fi to be ready
[00:00:03.163,818] <inf> wifi_mgmt_ext: Connection requested
[00:00:03.163,848] <inf> sta: Connection requested
[00:00:03.163,940] <inf> sta: ==================
[00:00:03.163,970] <inf> sta: State: SCANNING
[00:00:03.464,111] <inf> sta: ==================
[00:00:03.464,141] <inf> sta: State: SCANNING
[00:00:03.764,312] <inf> sta: ==================
[00:00:03.764,343] <inf> sta: State: SCANNING
[00:00:04.064,514] <inf> sta: ==================
[00:00:04.064,544] <inf> sta: State: SCANNING
[00:00:04.364,715] <inf> sta: ==================
[00:00:04.364,746] <inf> sta: State: SCANNING
[00:00:04.664,916] <inf> sta: ==================
[00:00:04.664,947] <inf> sta: State: SCANNING
[00:00:04.965,118] <inf> sta: ==================
[00:00:04.965,148] <inf> sta: State: SCANNING
[00:00:05.265,319] <inf> sta: ==================
[00:00:05.265,350] <inf> sta: State: SCANNING
[00:00:05.565,521] <inf> sta: ==================
[00:00:05.565,551] <inf> sta: State: SCANNING
[00:00:05.865,722] <inf> sta: ==================
[00:00:05.865,753] <inf> sta: State: SCANNING
[00:00:06.165,924] <inf> sta: ==================
[00:00:06.165,954] <inf> sta: State: SCANNING
[00:00:06.466,125] <inf> sta: ==================
[00:00:06.466,156] <inf> sta: State: SCANNING
[00:00:06.766,326] <inf> sta: ==================
[00:00:06.766,357] <inf> sta: State: SCANNING
[00:00:07.066,528] <inf> sta: ==================
[00:00:07.066,558] <inf> sta: State: SCANNING
[00:00:07.366,729] <inf> sta: ==================
[00:00:07.366,760] <inf> sta: State: SCANNING
[00:00:07.666,931] <inf> sta: ==================
[00:00:07.666,961] <inf> sta: State: SCANNING
[00:00:07.967,132] <inf> sta: ==================
[00:00:07.967,163] <inf> sta: State: AUTHENTICATING
[00:00:08.242,462] <inf> sta: Connected
[00:00:08.262,725] <inf> net_dhcpv4: Received: 192.168.1.238
[00:00:08.263,000] <inf> net_config: IPv4 address: 192.168.1.238
[00:00:08.263,000] <inf> net_config: Lease time: 43200 seconds
[00:00:08.263,061] <inf> net_config: Subnet: 255.255.255.0
[00:00:08.263,092] <inf> net_config: Router: 192.168.1.1
[00:00:08.263,214] <inf> sta: DHCP IP address: 192.168.1.238
[00:00:08.275,146] <inf> sta: ==================
[00:00:08.275,177] <inf> sta: State: COMPLETED
[00:00:08.275,207] <inf> sta: Interface Mode: STATION
[00:00:08.275,238] <inf> sta: Link Mode: WIFI 6 (802.11ax/HE)
[00:00:08.275,268] <inf> sta: SSID: <REDACTED>
[00:00:08.275,329] <inf> sta: BSSID: <REDACTED>
[00:00:08.275,329] <inf> sta: Band: 5GHz
[00:00:08.275,360] <inf> sta: Channel: 44
[00:00:08.275,360] <inf> sta: Security: WPA2-PSK
[00:00:08.275,390] <inf> sta: MFP: Optional
[00:00:08.275,390] <inf> sta: RSSI: -52

We also free up some RAM, though we’re stll operating on the margin.

Memory region         Used Size  Region Size  %age Used
           FLASH:      530044 B       800 KB     64.70%
             RAM:      205120 B       216 KB     92.74%
        IDT_LIST:          0 GB        32 KB      0.00%

Stepping through again, we can now see that the socket implementation is provided by zpacket_socket.

0x00057bfe in z_impl_zsock_socket (family=3, type=2, proto=36488)
123			errno = 0;
(gdb) s
124			ret = sock_family->handler(family, type, proto);
(gdb) s
zpacket_socket (family=3, type=2, proto=36488)
49		fd = zvfs_reserve_fd();

There is still more to explore to determine the capabilities and limitations of using a resource constrained MCU host with nRF70 series devices, but knowing that it is possible opens up the door for some interesting multi-protocol designs. If interested in trying it out for yourself, I opened a PR to the nRF Connect SDK add ing support for using the nRF9151 on the Thingy:91 X as Wi-Fi host.