BLE and nRF5340: Your own characterization
February 17, 2024

In this article, we'll show you how to get a quick start on implementing basic BLE communications using Nordic Semiconductor's nRF5340 processor on the nRF5340-DK development board.

Project preparation

We start our adventure by setting up the environment with nRF Connect SDK and Visual Studio Code, using an extension from Nordic. Getting started instructions are available in the official Getting Started guide (Get Started). We will start the project with the classic "blinky diode" using the Blinky example. We can download it directly from the site or create it by clicking Create a new application in the start menu of the nRF Connect extension and selecting Blinky Sample from the list.

Start menu in nRF Connect extension for VS Code

The Blinky project is ideal for the beginning of our adventure, not only because it contains the key elements we need to get started, but also allows us to check that our development environment is properly configured and that our hardware works flawlessly.
To move forward with building our application, we still need to set up the configuration, specifying for which board we are developing the code. We do this by selecting Create new build configuration from the Applications menu and selecting nrf5340dk_nrf5340_cpuapp in the list of available boards in the Board section. We leave the rest of the settings unchanged and finalize the process by clicking Build Configuration.

Menu snippet for creating a new configuration for an application in VS Code

From this point, the way is opened for us to freely edit the code, compile the application and upload it directly to the microcontroller. This is all thanks to the debugger on the board, which connects to our environment via a USB interface.

View of the finished VSC

By selecting the option Flash from the menu Actions upload the application to the board, and then open the terminal VCOM1, which will display all the logs sent by the function printk. If everything went as we expected, one of the four LEDs available on the nRF5340-DK board will blink, switching between states every second.

Starting basic communication

Now we'll make a few changes to the code that will allow us to start broadcasting the default BLE "broadcast" packets. We start by adding the following lines to the prj.conf file:

CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="nRF5340 DK test"

These options will become part of the Kconfig configuration on which the Zephyr system build is based.

  • CONFIG_BT unlocks support for Bluetooth communication,
  • CONFIG_BT_PERIPHERAL configures the hardware to operate in Peripheral Device mode,
  • CONFIG_BT_DEVICE_NAME allows you to set the name that will be visible during the scanning performed by the Central Device.

We now move to main.c, where we first add the following header files:

#include <zephyr/bluetooth/uuid.h> #include <zephyr/bluetooth/gatt.h>

Then, over the function int main(void), we add two new structures:

static const struct bt_data adv_data[] = { BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)) } static void connected(struct bt_conn *conn, uint8_t err) { if (err) { printk("Connection failed (err 0x%02x)\n", err); } else { printk("Connected\n"); } } static void disconnected(struct bt_conn *conn, uint8_t reason) { printk("Disconnected (reason 0x%02x)\n", reason); } BT_CONN_CB_DEFINE(conn_callbacks) = { .connected = connected, .disconnected = disconnected };

The first of these, the board adv_data, contains the configuration of broadcast (advertising) packets. In turn conn_callbacks stores pointers to the functions called when the connection was established and just after it was broken.

Finally, inside the function int main(void), above the loop while add:

ret = bt_enable(NULL); if (ret) { printk("Bluetooth init failed (err %d)\n", ret); return 0; } ret = bt_le_adv_start(BT_LE_ADV_CONN_NAME, adv_data, ARRAY_SIZE(adv_data), NULL, 0); if (ret) { printk("Advertising failed to start (err %d)\n", ret); } else { printk("Advertising successfully started\n"); }

Function bt_enable initializes the settings and starts Bluetooth communication, a bt_le_adv_start starts sending out broadcast packets, according to the indicated parameters.

Mobile app connection test

After uploading the app, our processor should broadcast its presence to other devices in the vicinity. The easiest way to verify this is by using a BLE-equipped phone and the nRF Connect app.

Android vs iOS

The iOS app looks quite different from that for Android, but the functionality is quite similar. Under the tab Scanner we run a scan and after a while we should see our device in the list, with a predefined name. After clicking on the button Connect the connection will be established and we should see in the application the attributes of the Generic.

Android vs iOS

We add our own characteristics

In this section, we will add a custom characteristic that will allow us to control the second diode available on our board.
Again, we start with the prj.conf file, where we add an option that allows us to configure the device as a GATT (Generic Attribute Profile) client:

CONFIG_BT_GATT_CLIENT=y

Returning to main.c, we first add additional header files:

#include <zephyr/bluetooth/uuid.h> #include <zephyr/bluetooth/gatt.h>

We then duplicate the code responsible for configuring LED0 and use it to configure led1:

#define LED0_NODE DT_ALIAS(led0) #define LED1_NODE DT_ALIAS(led1) ... static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios); static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios); ... if (!gpio_is_ready_dt(&led)) { return 0; } ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE); if (ret < 0) { return 0; } if (!gpio_is_ready_dt(&led1)) { return 0; } ret = gpio_pin_configure_dt(&led1, GPIO_OUTPUT_INACTIVE); if (ret < 0) { return 0; }

The diode control will require two functions with distinctive headers, led_read and led_write:

static uint8_t led1_state = 0; static ssize_t led_read( struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset) { printk("Led read: %d", led1_state); return bt_gatt_attr_read(conn, attr, buf, len, offset, &led1_state, sizeof(led1_state)); } static ssize_t led_write( struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, uint16_t len, uint16_t offset, uint8_t flags) { led1_state = *(uint8_t*)buf; gpio_pin_set_dt(&led1, led1_state); printk("Led write: %d", led1_state); return len; }

At this point we should define a universal unique identification number (UUID) for our characteristic and for the service to which it will be assigned. For the purpose of the test, we can enter any string of numbers, but for future reference we recommend using a special generator. We enter the UUID into our code using coding macros:

#define BT_UUID_CUSTOM_SERVICE_VAL \n-. BT_UUID_128_ENCODE(0x11111111, 0x2222, 0x3333, 0x4444, 0x123400000000) #define BT_UUID_LED_CHRC_VAL #define BT_UUID_LED_CHRC_VAL. BT_UUID_128_ENCODE(0x11111111, 0x2222, 0x3333, 0x4444, 0x123400000001)

It's time for the most important part of the presented code, namely the definition of our own characteristics. Here, too, special macros come to our aid, thanks to which we can easily define all the necessary parameters in one place:

BT_GATT_SERVICE_DEFINE(custom_service, BT_GATT_PRIMARY_SERVICE(BT_UUID_DECLARE_128(BT_UUID_CUSTOM_SERVICE_VAL)), BT_GATT_CHARACTERISTIC( BT_UUID_DECLARE_128(BT_UUID_LED_CHRC_VAL), BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, led_read, led_write, NULL) );

The last step is to add to the previously defined array adv_data additional element that will guarantee the visibility of our own service (with characteristics) for other devices:

static const struct bt_data adv_data[] = { BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_CUSTOM_SERVICE_VAL) };

Characteristics performance test

As in the earlier test, we connect through the application on the phone, but this time we should in the view of the Client to see additional service and our characteristics:

Android vs iOS

We have no choice but to check the diode control. By clicking the down arrow on the right side of the characteristic we should get in the field Value status of the diode. On the other hand, by clicking on the up arrow, we can send the value of the unsigned int, where a value of 1 should immediately turn on the diode.

Summary

In the article, we showed how quickly you can get started with BLE communications if you choose to use a platform based on the nRF5340 processor and the tools included in the nRF Connect SDK.

References

#YourOwnCharacteristicInBLE #GoodByte