BLE with WiFi and FreeRTOS on Raspberry Pi Pico-W

The Raspberry Pi Pico RP2040 is a very versatile microcontroller. It is not the least expensive or the most powerful microcontroller, but it is one which is available and has an excellent software and tool ecosystem.

This article shows how to use the Raspberry Pi Pico-W with BLE and optional WiFi, running with FreeRTOS.

Outline

Back in February 2023, the Raspberry Pi Foundation has released the SDK V1.5 for the Pico, which now includes support for Bluetooth (both BLE and Bluetooth Classic) for the Pico-W board. The SDK V1.4 had only WiFi supported for the Infineon CYW43439. As the CYW43439 can do both WiFi and Bluetooth, we had to wait or the SDK 1.5.

The SDK has BlueKitchen’s BTstack integrated, optimized for small and resource-constraint microcontroller systems. BTstack runs both bare-metal or with an RTOS like FreeRTOS.

Raspberry Pi Pico-W with Debug Interface

For a University research project I need to transmit data from a sensor to another device, in a wireless way. Communication shall be as easy as possible. As communication way, the BLE way has been selected. So why not using to Raspberry Pi Pico-W to transmit the data?

There are some BLE examples in the 1.5 SDK, including a ‘Standalone’ one with a BLE client and BLE server. That was a good starting point, but I wanted to have it working with FreeRTOS plus WiFi (at least optionally). I managed to get it work on this weekend, so I decided to document and publish the project as an example.

The project uses CMake with Eclipse. Data is transmitted from the BLE ‘Server’ to a ‘BLE’ client. Both client and server run with FreeRTOS, and server and client can optionally use WiFi. For the sensor data in this example, the temperature value of the internal RP2040 sensor is transmitted.

The project discussed in this article is available on GitHub.

Project

The project is a CMake project in Eclipse, using make files:

Project Structure

It use the Raspberry Pi Pico SDK together with the McuLib.

The main part is inside the ‘src’ folder:

Source Files
  • app_platform: device initialization
  • application: main application logic
  • ble_client: BLE client implementation, receiving the sensor data
  • ble_server: BLE server implementation, sending the sensor data
  • btstack_config: BLE stack configuration for client and server implementation
  • IncludeMcuLibConfig: McuLib configuration
  • Shell: command line interface (SEGGER RTT and USB CDC)

There are some files which are only used if WiFi support is enabled:

  • dns_resolver: resolves IP addresses to names
  • lwipopts: lwIP configuration file
  • MinIniKeys: used to store WiFi credentials in FLASH
  • ntp_client: NTP implementation to get current data and time
  • PicoWifi: WiFi connection and utilities

Application Configuration

The application can be configured using the settings in app_platform.h:

app_platform.h

The most important ones are:

  • PL_CONFIG_STANDALONE_BLE_CLIENT: if server or client
  • PL_CONFIG_USE_WIFI: if WiFi is enabled or not
  • PL_CONFIG_USE_BLE: if BLE is enabled or not

CMakeLists.txt Files

The ‘main CMake file is pretty standard for a Pico-W:

cmake_minimum_required(VERSION 3.13)

# initialize the SDK based on PICO_SDK_PATH
# note: this must happen before project()
include(pico_sdk_import.cmake)

project(pico_W_BLE C CXX ASM) # sets ${CMAKE_PROJECT_NAME}
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

set(PICO_BOARD pico_w)  # needed for Pico-W, otherwise #include "pico/cyw43_arch.h" won't be found

# [Platfrom specific command] Initialize the Raspberry Pi Pico SDK
pico_sdk_init()

set(PROJECT_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
add_compile_options(-include "${PROJECT_ROOT_DIR}/src/IncludeMcuLibConfig.h")

add_executable(${CMAKE_PROJECT_NAME})

# enable USB CDC and disable UART:
pico_enable_stdio_usb(${CMAKE_PROJECT_NAME} 1)
pico_enable_stdio_uart(${CMAKE_PROJECT_NAME} 0)

# add component directories to the list
add_subdirectory(./McuLib)
add_subdirectory(./src)

# generate extra files (map/bin/hex/uf2)
pico_add_extra_outputs(${CMAKE_PROJECT_NAME})

target_link_libraries(
  ${CMAKE_PROJECT_NAME}
  # might have circular library dependencies, see https://stackoverflow.com/questions/45135/why-does-the-order-in-which-libraries-are-linked-sometimes-cause-errors-in-gcc
  SrcLib
  McuLib
  SrcLib  # again because of FreeRTOShooks.c
  pico_stdlib
)

The CMakeLists.txt in the ‘src’ folder is special and integrates both the BLE stack and the lwIP stack:

# file: Collect all files that need to be compiled. 
file(GLOB FILES
  main.c
  app_platform.c
  application.c
  Shell.c
  FreeRTOShooks.c
  ble_client.c
  ble_server.c
  PicoWiFi.c
  ntp_client.c
  dns_resolver.c
)

# add_library: With this declaration, you express the intent to build a library. 
# The first argument is the name of the library, 
# the second argument are the files that will be compiled to create your library.
add_library(SrcLib ${FILES})

# need to add custom compile flags
target_compile_definitions(SrcLib PRIVATE
  CYW43_ENABLE_BLUETOOTH=1  # otherwise cyw43_arch_init() will not initialize BLE
)

# target_link_libraries: If you link with other libraries, list them here
target_link_libraries(
  SrcLib                # this library
  McuLib                # we need the McuLib
  pico_stdlib           # pico standard library
  hardware_adc          # internal temperature and ADC
  pico_btstack_ble      # BLE stack
  pico_btstack_cyw43    # BLE stack with CYW43
  pico_cyw43_arch_lwip_sys_freertos  # full lwIP stack including blocking sockets, with NO_SYS=0
)

# target_include_directories: Libraries need to publish their header files 
# so that you can import them in source code. This statement expresses where to find the files 
# - typically in an include directory of your projects.
target_include_directories(SrcLib PUBLIC .)

pico_btstack_make_gatt_header(SrcLib PRIVATE "${CMAKE_CURRENT_LIST_DIR}/temp_sensor.gatt")

The special entries are:

  • CYW43_ENABLE_BLUETOOTH=1: this is required to have the CYW43 integration with BLE enabled
  • pico_btstack_ble: this adds the BLE stack (only BLE, no classic Bluetooth)
  • pico_btstack_cyw43: this adds the integration of the stack with the CYW43
  • pico_cyw43_arch_lwip_sys_freertos: this adds the lwIP stack with FreeRTOS support and blocking sockets.

A special CMake function is used to run the comile_gatt too. This creates a GATT from the BTstack GATT file:

pico_btstack_make_gatt_header(SrcLib PRIVATE "${CMAKE_CURRENT_LIST_DIR}/temp_sensor.gatt")

The file is generated in the src folder as a GATT file for a temperature sensor.

BLE Client Implementation

The client implementation is in ble_client.c With WiFi enabled, there is already a WiFi task. Otherwise a task for the client gets created. That BLE task does the CYW43 initialization:

#if !PL_CONFIG_USE_WIFI
static void clientTask(void *pv) {
  /* initialize CYW43 driver architecture (will enable BT if/because CYW43_ENABLE_BLUETOOTH == 1) */
  if (cyw43_arch_init()) {
    McuLog_fatal("failed to initialize cyw43_arch");
    for(;;){}
  }
  BleClient_SetupBLE();
  for(;;) {
    btstack_run_loop_execute(); /* does not return */
  }
}
#endif

void BleClient_Init(void) {
#if !PL_CONFIG_USE_WIFI /* if using WiFi, will do the BLE stuff from the WiFi task */
  if (xTaskCreate(
      clientTask,  /* pointer to the task */
      "BLEclient", /* task name for kernel awareness debugging */
      1200/sizeof(StackType_t), /* task stack size */
      (void*)NULL, /* optional task startup argument */
      tskIDLE_PRIORITY+2,  /* initial priority */
      (TaskHandle_t*)NULL /* optional task handle to create */
    ) != pdPASS)
  {
    McuLog_fatal("failed creating task");
    for(;;){} /* error! probably out of memory */
  }
#endif
}

The BLE client setup is a below:

void BleClient_SetupBLE(void) {
  l2cap_init(); /* Set up L2CAP and register L2CAP with HCI layer */
  sm_init(); /* setup security manager */
  sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT); /* set security manager that we want to connect automatically */
  gatt_client_init(); /* initialize client */

  hci_event_callback_registration.callback = &hci_event_handler; /* configure callback structure */
  hci_add_event_handler(&hci_event_callback_registration); /* register callback */

  /* setup timer */
#if PL_CONFIG_USE_WIFI /* use cyw43/lwip timer */
  async_context_add_at_time_worker_in_ms(cyw43_arch_async_context(), &heartbeat_worker, LED_SLOW_FLASH_DELAY_MS);
#else /* use BTStack timer */
  heartbeat.process = &heartbeat_handler;
  btstack_run_loop_set_timer(&heartbeat, LED_SLOW_FLASH_DELAY_MS);
  btstack_run_loop_add_timer(&heartbeat);
#endif
  hci_power_control(HCI_POWER_ON); /* turn it on */
}

Depending on if only BLE or BLE+WiFi is running, different timer callbacks have to be used to flash the LED and to restart the timer:

#if PL_CONFIG_USE_WIFI
  static void heartbeat_handler(async_context_t *context, async_at_time_worker_t *worker);
  static async_at_time_worker_t heartbeat_worker = { .do_work = heartbeat_handler };

  static void heartbeat_handler(async_context_t *context, async_at_time_worker_t *worker) {
    bool useShortDelay;

    useShortDelay = callbackToggleLED();
    /* restart timer */
    async_context_add_at_time_worker_in_ms(context, &heartbeat_worker, useShortDelay?LED_QUICK_FLASH_DELAY_MS:LED_SLOW_FLASH_DELAY_MS);
  }
#else
  static btstack_timer_source_t heartbeat;

  static void heartbeat_handler(struct btstack_timer_source *ts) {
    bool useShortDelay;

    useShortDelay = callbackToggleLED();
    /* restart timer */
    btstack_run_loop_set_timer(ts, useShortDelay?LED_QUICK_FLASH_DELAY_MS:LED_SLOW_FLASH_DELAY_MS);
    btstack_run_loop_add_timer(ts);
  }
#endif

The client then waits for an advertising packet from the server and connects to it:

00:00:01,17 INFO  ble_client.c:178: BTstack up and running on 43:43:A2:12:1F:AC.
00:00:01,17 INFO  ble_client.c:43: Start scanning!
00:00:01,64 INFO  ble_client.c:196: Connecting to device with addr 28:CD:C1:08:13:5D.
00:00:02,69 INFO  ble_client.c:207: Search for env sensing service.
00:00:03,28 INFO  ble_client.c:86: Storing service
00:00:03,35 INFO  ble_client.c:98: Search for env sensing characteristic.
00:00:03,47 INFO  ble_client.c:108: Storing characteristic
00:00:03,47 INFO  ble_client.c:122: Enable notify on characteristic.
00:00:03,60 INFO  ble_client.c:134: Notifications enabled, ATT status 0x00

After that, it receives data items from the server every 10 seconds:

00:00:03,60 INFO  ble_client.c:147: Indication value len 2
00:00:03,60 INFO  ble_client.c:150: read temp 22.36 degc
00:00:08,13 INFO  ble_client.c:147: Indication value len 2
00:00:08,13 INFO  ble_client.c:150: read temp 22.38 degc

BLE Server Implementation

The server implementation in ble_server.c is very similar to the client one. Except that it configures the data source (temperature sensor):

#define HEARTBEAT_PERIOD_MS 1000

static btstack_packet_callback_registration_t hci_event_callback_registration;

static void callbackSendData(void) { /* callback is called every second */
  static uint32_t counter = 0;
  counter++;
  if ((counter%10) == 0) { /* Update the temperature every 10s */
    poll_temperature();
    if (le_notification_enabled) {
      att_server_request_can_send_now_event(con_handle);
    }
  }
}

static void callbackToggleLED(void) { /* called every second */
  /* Invert the led */
  static int led_on = true;
  led_on = !led_on;
  cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, led_on);
}

#if PL_CONFIG_USE_WIFI
  static void heartbeat_handler(async_context_t *context, async_at_time_worker_t *worker);
  static async_at_time_worker_t heartbeat_worker = { .do_work = heartbeat_handler };

  static void heartbeat_handler(async_context_t *context, async_at_time_worker_t *worker) {
    callbackSendData();
    callbackToggleLED();
    /* restart timer */
    async_context_add_at_time_worker_in_ms(context, &heartbeat_worker, HEARTBEAT_PERIOD_MS);
  }
#else
  static btstack_timer_source_t heartbeat;

  static void heartbeat_handler(struct btstack_timer_source *ts) {
    callbackSendData();
    callbackToggleLED();
    /* restart timer */
    btstack_run_loop_set_timer(ts, HEARTBEAT_PERIOD_MS);
    btstack_run_loop_add_timer(ts);
  }
#endif

void BleServer_SetupBLE(void) {
  l2cap_init(); /* Set up L2CAP and register L2CAP with HCI layer */
  sm_init(); /* setup security manager */
  att_server_init(profile_data, att_read_callback, att_write_callback); /* setup attribute callbacks */

  /* inform about BTstack state */
  hci_event_callback_registration.callback = &packet_handler; /* setup callback for events */
  hci_add_event_handler(&hci_event_callback_registration); /* register callback handler */

  /* register for ATT event */
  att_server_register_packet_handler(packet_handler); /* register packet handler */

  /* setup timer */
#if PL_CONFIG_USE_WIFI /* use cyw43 timer */
  async_context_add_at_time_worker_in_ms(cyw43_arch_async_context(), &heartbeat_worker, HEARTBEAT_PERIOD_MS);
#else /* use BTStack timer */
  heartbeat.process = &heartbeat_handler;
  btstack_run_loop_set_timer(&heartbeat, HEARTBEAT_PERIOD_MS);
  btstack_run_loop_add_timer(&heartbeat);
#endif
  hci_power_control(HCI_POWER_ON); /* turn BLE on */
}

static void SetupTemperatureSensor(void) {
  /* Initialize adc for the temperature sensor */
  adc_init();
  adc_select_input(ADC_CHANNEL_TEMPSENSOR);
  adc_set_temp_sensor_enabled(true);
}

#if !PL_CONFIG_USE_WIFI /* if using WiFi, will do the BLE stuff from the WiFi task */
static void serverTask(void *pv) {
  /* initialize CYW43 driver architecture (will enable BT if/because CYW43_ENABLE_BLUETOOTH == 1) */
  if (cyw43_arch_init()) {
    McuLog_fatal("failed to initialize cyw43_arch");
    for(;;) {}
  }
  BleServer_SetupBLE();
  for(;;) {
    btstack_run_loop_execute(); /* does not return */
  }
}
#endif /* !PL_CONFIG_USE_WIFI */

void BleServer_Init(void) {
  SetupTemperatureSensor();
#if !PL_CONFIG_USE_WIFI /* if using WiFi, will do the BLE stuff from the WiFi task */
  if (xTaskCreate(
      serverTask,  /* pointer to the task */
      "BLEserver", /* task name for kernel awareness debugging */
      1200/sizeof(StackType_t), /* task stack size */
      (void*)NULL, /* optional task startup argument */
      tskIDLE_PRIORITY+2,  /* initial priority */
      (TaskHandle_t*)NULL /* optional task handle to create */
    ) != pdPASS)
  {
    McuLog_fatal("failed creating task");
    for(;;){} /* error! probably out of memory */
  }
#endif
}

Data collected and transmitted is then shown in the console:

18:01:58,59 INFO  ble_server.c:51: ADC: temperature 22.36 degc
18:01:58,59 INFO  ble_server.c:86: Can send data now
18:02:09,05 INFO  ble_server.c:51: ADC: temperature 22.36 degc
18:02:09,05 INFO  ble_server.c:86: Can send data now

WiFi Task

The WiFi support with PicoWiFi.c is optional: the example can run without WiFi too. It uses MinINI to read the WiFi credentials from FLASH:

static void WiFiTask(void *pv) {
  int res;
  bool ledIsOn = false;

#if CONFIG_USE_EEE
  if (networkMode == WIFI_PASSWORD_METHOD_WPA2) {
    McuLog_info("using WPA2");
  }
#endif

  McuLog_info("started WiFi task");
  /* initialize CYW43 architecture
      - will enable BT if CYW43_ENABLE_BLUETOOTH == 1
      - will enable lwIP if CYW43_LWIP == 1
   */
  if (cyw43_arch_init_with_country(CYW43_COUNTRY_SWITZERLAND)!=0) {
    McuLog_error("failed setting country code");
    for(;;) {}
  }
#if PL_CONFIG_USE_BLE && PL_CONFIG_STANDALONE_BLE_SERVER
  BleServer_SetupBLE();
#elif PL_CONFIG_USE_BLE && PL_CONFIG_STANDALONE_BLE_CLIENT
  BleClient_SetupBLE();
#endif
  wifi.isInitialized = true;
#if PL_CONFIG_USE_WIFI
  McuLog_info("enabling STA mode");
  cyw43_arch_enable_sta_mode();
#if PL_CONFIG_USE_MINI
  McuMinINI_ini_gets(NVMC_MININI_SECTION_WIFI, NVMC_MININI_KEY_WIFI_HOSTNAME, WIFI_DEFAULT_HOSTNAME, wifi.hostname, sizeof(wifi.hostname), NVMC_MININI_FILE_NAME);
  McuMinINI_ini_gets(NVMC_MININI_SECTION_WIFI, NVMC_MININI_KEY_WIFI_SSID,     WIFI_DEFAULT_SSID,     wifi.ssid, sizeof(wifi.ssid), NVMC_MININI_FILE_NAME);
  McuMinINI_ini_gets(NVMC_MININI_SECTION_WIFI, NVMC_MININI_KEY_WIFI_PASS,     WIFI_DEFAULT_PASS,     wifi.pass, sizeof(wifi.pass), NVMC_MININI_FILE_NAME);
#else
  McuUtility_strcpy(wifi.hostname, sizeof(wifi.hostname), WIFI_DEFAULT_HOSTNAME);
  McuUtility_strcpy(wifi.ssid, sizeof(wifi.ssid), WIFI_DEFAULT_SSID);
  McuUtility_strcpy(wifi.pass, sizeof(wifi.pass), WIFI_DEFAULT_PASS);
#endif
  McuLog_info("setting hostname: %s", wifi.hostname);
  netif_set_hostname(&cyw43_state.netif[0], wifi.hostname);

  vTaskDelay(pdMS_TO_TICKS(1000)); /* give network tasks time to start up */

  McuLog_info("connecting to SSID '%s'...", wifi.ssid);
  res = cyw43_arch_wifi_connect_timeout_ms(wifi.ssid, wifi.pass, CYW43_AUTH_WPA2_AES_PSK, 20000);
  if (res!=0) {
    for(;;) {
      McuLog_error("connection failed after timeout! code %d", res);
      vTaskDelay(pdMS_TO_TICKS(30000));
    }
  } else {
    McuLog_info("success!");
    wifi.isConnected = true;
  #if PL_CONFIG_USE_NTP_CLIENT
    NtpClient_TaskResume();
  #endif
  #if PL_CONFIG_USE_MQTT_CLIENT
    MqttClient_Connect();
  #endif
  }
  for(;;) {
    cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, ledIsOn);
    ledIsOn = !ledIsOn;
    if (wifi.isConnected) {
      vTaskDelay(pdMS_TO_TICKS(1000));
    } else {
      vTaskDelay(pdMS_TO_TICKS(50));
    }
  }
#endif
}

Depending on the BLE (client or server) it calls the BLE setup:

#if PL_CONFIG_USE_BLE && PL_CONFIG_STANDALONE_BLE_SERVER
  BleServer_SetupBLE();
#elif PL_CONFIG_USE_BLE && PL_CONFIG_STANDALONE_BLE_CLIENT
  BleClient_SetupBLE();
#endif

With WiFi and NTP enabled, it gets date and time information from the internet. Below is such a startup sequence of the server with WiFi and NTP client running:

00:00:00,00 INFO  PicoWiFi.c:103: started WiFi task
00:00:00,92 INFO  PicoWiFi.c:119: enabling STA mode
00:00:01,14 INFO  PicoWiFi.c:130: setting hostname: pico
00:00:01,16 INFO  ble_server.c:67: BTstack up and running on 28:CD:C1:08:13:5D.
00:00:01,17 INFO  ble_server.c:51: ADC: temperature 20.96 degc
00:00:02,14 INFO  PicoWiFi.c:135: connecting to SSID 'mySSID'...
00:00:02,86 INFO  ble_server.c:112: Attribute write callback
00:00:02,87 INFO  ble_server.c:86: Can send data now
00:00:05,09 INFO  PicoWiFi.c:143: success!
00:00:05,12 INFO  dns_resolver.c:20: 'pool.ntp.org' resolved to 152.67.73.149
00:00:05,14 INFO  ntp_client.c:42: got ntp response: 19/03/2023 17:41:30
17:41:35,48 INFO  ble_server.c:51: ADC: temperature 20.96 degc
17:41:35,48 INFO  ble_server.c:86: Can send data now

Summary

With this I have a working BLE client-server communication between two Raspberry Pi Pico-W boards, both running with FreeRTOS, and both with optional WiFi support. This makes it a very versatile and cost effective solution for any kind of short range communication devices.

You can find the current code and project on GitHub, together with other Raspberry Pi Pico based projects.

Happy Bluetoothing šŸ™‚

Summary

Links

Advertisement

What do you think?

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.