Exercise 2

Updating the connection parameters

In the previous exercise, we established a connection between the Nordic board, as a peripheral, and your smartphone, as a central. We were able to send some simple data using the pre-implemented LED Button Service. Although we didn’t see it, the central already selected some connection parameters when it connected to our peripheral. In this exercise, we will look into what those parameters were, and we will also look at what we can do to change our connection parameters.

We will cover Bluetooth LE services and sending data in more detail in lesson 4. For now, it is only relevant to know that we are sending some data to a connected device.

Exercise steps

In the GitHub repository for this course, go to the base code for this exercise, found in lesson3/blefund_less3_exer2.

1. Get the connection parameters for the current connection.

1.1 Declare a structure to store the connection parameters.

In your on_connected() callback function, declare a structure info of type bt_conn_info to store the connection parameters. Then use the function bt_conn_get_info() to populate the info struct with the connection parameters used in the connection.

struct bt_conn_info info;
err = bt_conn_get_info(conn, &info);
if (err) {
   LOG_ERR("bt_conn_get_info() returned %d", err);
   return;
}

1.2 Add the connection parameters to your log.

Let’s log the three main connection parameters that we talked about in Connection parameters. Note that the connection interval is represented by a unit of 1.25 ms, and the supervision timeout in units of 10 ms. So we will do some calculations to make it more readable.

Add the following lines to the end of your on_connected() callback in main.c

double connection_interval = info.le.interval*1.25; // in ms
uint16_t supervision_timeout = info.le.timeout*10; // in ms
LOG_INF("Connection parameters: interval %.2f ms, latency %d intervals, timeout %d ms", connection_interval, info.le.latency, supervision_timeout);

Note that in order for the log to be able to process float (double) numbers, we have added the config CONFIG_FPU=y to the prj.conf file. If you used the template from GitHub, this was already added.

2. Build and flash the sample to your board.

Your log should look something like this:

3. Connect to the device via your smartphone

Use nRF Connect for Mobile to locate the device, called “Nordic_Peripheral” and connect to it.

The log output will show us the connection parameters for this connection

Note that your connection parameters may differ from the ones shown here.

4. Modify the callbacks to be notified when the parameters for an LE connection have been updated.

You may have noticed that the bt_conn_cb structure we defined in the previous exercise, also has the member le_param_updated. This is used to tell our application that the connection parameters for an LE connection have been updated.

Note

There is also the event le_param_req callback member, which is called when the connected device requests an update to the connection parameters. You probably want to have this in your application, but we will not look into that event in this exercise.

4.1 Modify your connection_callbacks parameter, by adding the following line

.le_param_updated       = on_le_param_updated,

4.2 Add the on_le_param_updated() event.

Define the callback function on_le_param_updated() to log the new connection parameters.

Add the following function in main.c

void on_le_param_updated(struct bt_conn *conn, uint16_t interval, uint16_t latency, uint16_t timeout)
{
    double connection_interval = interval*1.25;         // in ms
    uint16_t supervision_timeout = timeout*10;          // in ms
    LOG_INF("Connection parameters updated: interval %.2f ms, latency %d intervals, timeout %d ms", connection_interval, latency, supervision_timeout);
}

4.3 Already now, if we flash the application and connect to it, we may see that after some seconds, this callback will trigger, and you might see something like the log below. Note that this will vary depending on your smartphone.

So this entire time, connection parameters changes were in fact being requested by your device. This is because, even though we didn’t actively request the parameter updates when you enable the Bluetooth stack in nRF Connect SDK, default values for a lot of parameters are set, including the peripheral’s preferred connection parameters. And if these parameters don’t match the ones given by the central in the initial connection, the peripheral will automatically request changes to the connection parameters to try to get the preferred values.

Let’s take a look at what their default values are:

Connection interval:

Peripheral latency:

Supervision timeout:

This is all found in Kconfig.gatt, in <install_path>zephyrsubsysbluetoothhost.

So we can see from the log output above that the initial connection had supervision timeout of 240 ms. Therefore, the peripheral requested a change in the parameters, and after this update, all the parameters turned out to satisfy our preferences.

5. Change the preferred connection parameters.

Let’s change our preferred connection parameters by adding the following lines to our prj.conf

CONFIG_BT_PERIPHERAL_PREF_MIN_INT=800
CONFIG_BT_PERIPHERAL_PREF_MAX_INT=800
CONFIG_BT_PERIPHERAL_PREF_LATENCY=0
CONFIG_BT_PERIPHERAL_PREF_TIMEOUT=400
CONFIG_BT_GAP_AUTO_UPDATE_CONN_PARAMS=y

The last config, CONFIG_BT_GAP_AUTO_UPDATE_CONN_PARAMS, responsible for automatically sending requests for connection parameters updates, is not really needed as it is set to y by default. This is why we saw the update take place even though we didn’t manually request it.

You can disable this Kconfig if you do not want your application to ask for updates automatically. In that case, you can request parameter changes manually in your application by using bt_conn_le_param_update().

The above parameters will set both the minimum and maximum preferred connection interval to 1 second. It will set the preferred peripheral latency to 0 connection intervals, and a preferred supervision timeout of 4 seconds.

6. Build and flash your application, and connect to it with your smartphone.

The log will output something like this. Note that it will take around 5 seconds after the connected event before the connection parameters are updated.

So we can see that the phone initially connected with a connection interval of 30ms and a supervision timeout of 240ms. After we requested some changes, the connection interval was changed to 1.005s and the supervision timeout to 4s. Although the 1005ms is outside of our minimum and maximum preferences, it is up to the central to decide how to reply to your request. These numbers will vary depending on the central (i.e your smartphone) in the connection, but you will most likey see 1000 ms. It is up to the phone, acting as the central, to set the connection parameters.

Note

To actually observe the connection interval, nRF Blinky is a mobile app that enables you to actually observe the changes in the connection interval when pressing the button on the device.
Due to the graphical refresh rate of the nRF Connect for Mobile application, you won’t notice any difference in the connection interval when using this app.

We have now made the application less responsive. It may not make sense that an application that feels slower is better, and – in many cases – it isn’t. But remember that in order to decrease the latency, the devices need to communicate more often, thus using more power. This is something to consider when developing your own Bluetooth LE application, For instance, a temperature sensor does not need to update its value 10 times per second. Play around with these connection parameters to find something that suits your application.

7. Set our preferred PHY.

First, we need to set up a set of our preferred PHY, we will use 2M PHY.

7.1 Define the function update_phy() to update the connection’s PHY

Create the following function in main.c

static void update_phy(struct bt_conn *conn)
{
    int err;
    const struct bt_conn_le_phy_param preferred_phy = {
        .options = BT_CONN_LE_PHY_OPT_NONE,
        .pref_rx_phy = BT_GAP_LE_PHY_2M,
        .pref_tx_phy = BT_GAP_LE_PHY_2M,
    };
    err = bt_conn_le_phy_update(conn, &preferred_phy);
    if (err) {
        LOG_ERR("bt_conn_le_phy_update() returned %d", err);
    }
}

What we are doing here is that we are triggering the PHY update directly from the connected callback event. We set our preferred PHY parameter saying that we prefer to use BT_GAP_LE_PHY_2M.

7.2 Call the function to update PHY during the connection.

Call the function update_phy() from the end of the on_connected() callback function, with the my_conn as the input parameter. Add the following line

update_phy(my_conn);

8. Modify the callbacks to be notified when the PHY of the connection has changed.

In theory, this should work, but we want some way to check whether the PHY actually changes. So, let’s add the le_phy_updated callback to the connection_callbacks, which will tell us if the PHY of the connection changes.

8.1 Implement the on_le_phy_updated() callback function. It can look something like this

void on_le_phy_updated(struct bt_conn *conn, struct bt_conn_le_phy_info *param)
{
    // PHY Updated
    if (param->tx_phy == BT_CONN_LE_TX_POWER_PHY_1M) {
        LOG_INF("PHY updated. New PHY: 1M");
    }
    else if (param->tx_phy == BT_CONN_LE_TX_POWER_PHY_2M) {
        LOG_INF("PHY updated. New PHY: 2M");
    }
    else if (param->tx_phy == BT_CONN_LE_TX_POWER_PHY_CODED_S8) {
        LOG_INF("PHY updated. New PHY: Long Range");
    }
}

8.2 Enable the ability to update the PHY

The callback is ready, but we are currently not able to add it to our connection_callbacks struct. If you look at the declaration of bt_conn_cb in conn.h, you will see that this callback is only defined if CONFIG_BT_USER_PHY_UPDATE is defined, and by default, it is not.

Add this line to your prj.conf:

CONFIG_BT_USER_PHY_UPDATE=y

8.3 Add the le_phy_updated event to the connection_callbacks parameter, by adding the following line to your connection_callbacks structure.

.le_phy_updated         = on_le_phy_updated,

9. Build and flash the application.

Now try connecting to your device, and see whether the PHY is updated. What does the log say? It should look something like this:

More on this

It is not by chance that we chose 2M as the preferred PHY. Most new phones have support for 2M, while only some phones have support for Coded PHY. If you want to check whether your phone supports Coded PHY, you can replace the BT_GAP_LE_PHY_2M in your preferred_phy with BT_GAP_LE_PHY_CODED. In addition, you need to enable the Kconfig symbol CONFIG_BT_CTLR_PHY_CODED.

Lastly, we want to increase our Data Length and MTU size. Even though the LBS service which we are currently using only supports sending one byte of payload data, this is useful in many applications.

10. Define the function update_data_length() to update the data length

Add this function to your main.c:

static void update_data_length(struct bt_conn *conn)
{
    int err;
    struct bt_conn_le_data_len_param my_data_len = {
        .tx_max_len = BT_GAP_DATA_LEN_MAX,
        .tx_max_time = BT_GAP_DATA_TIME_MAX,
    };
    err = bt_conn_le_data_len_update(my_conn, &my_data_len);
    if (err) {
        LOG_ERR("data_len_update failed (err %d)", err);
    }
}

Here we are setting the number of bytes and the amount of time to the maximum. Here we are using the defined values BT_GAP_DATA_LEN_MAX and BT_GAP_DATA_TIME_MAX, which are set to 251 bytes and 17040µs, respectively. You can also set custom parameters if you like. Note that the negotiation that this triggers will result in the maximum parameter that both devices in the connection will support, so you may not get the full 251 bytes that you request. The peer that supports the shortest data length will have the final say.

11.1 Define the function update_data_mtu() to trigger the MTU negotiation

Add the following function in main.c

static void update_mtu(struct bt_conn *conn)
{
    int err;
    exchange_params.func = exchange_func;

    err = bt_gatt_exchange_mtu(conn, &exchange_params);
    if (err) {
        LOG_ERR("bt_gatt_exchange_mtu failed (err %d)", err);
    }
}

Similarly to how we requested the data length update, we tell our device to request an MTU update. During an MTU update negotiation, both devices will declare their supported MTU size, and the actual MTU will be set to the lower of the two, since that will be the limiting factor. You may note that this function doesn’t contain the actual MTU size. This is because this needs to be set in your prj.conf file, which we will set shortly.

11.2 We also need to declare the exchange_params parameter. This needs to be defined outside our update_mtu() function, so we will place it close to the top of our main.c

static struct bt_gatt_exchange_params exchange_params;

12. Configure the application to enable data length extension

Add the following to your prj.conf file

General

We are first enabling the data length extension by setting CONFIG_BT_USER_DATA_LEN_UPDATE=y. Then we set the actual data length by setting CONFIG_BT_CTLR_DATA_LENGTH_MAX=251. Then we set the size of the actual buffers that will be used, and lastly, we set the MTU size that we want to use in our application.

nRF5340 DK

We are first enabling the data length extension by setting CONFIG_BT_USER_DATA_LEN_UPDATE=y. Then we set the size of the actual buffers that will be used, and lastly, we set the MTU size that we want to use in our application.

In addition to this, we also need to add some configurations to the network core, through the file found in child_image/hci_rpmsg.conf.

Add the following configs to hci_rpmsg.conf

Here we set the data length we want to use in addition to setting the size of the buffers, since this is the core that actually holds the buffer. The network core will by default set the maximum number of connections to 16, which in addition to increasing the data length would cause our application to quickly run out of RAM. Therefore we need to set CONFIG_BT_MAX_CONN=2.

Note that in this exercise, that file was created for you, but if it is not present in your application, you can just create it, and it will automatically be included in the build for the network core.

13. Implement the two callback functions that will trigger when the data length is updated and when the MTU is updated.

13.1 Let us start by adding the data length update callback.

void on_le_data_len_updated(struct bt_conn *conn, struct bt_conn_le_data_len_info *info)
{
    uint16_t tx_len     = info->tx_max_len; 
    uint16_t tx_time    = info->tx_max_time;
    uint16_t rx_len     = info->rx_max_len;
    uint16_t rx_time    = info->rx_max_time;
    LOG_INF("Data length updated. Length %d/%d bytes, time %d/%d us", tx_len, rx_len, tx_time, rx_time);
}

We also need to include it in our connection_callbacks:

.le_data_len_updated    = on_le_data_len_updated,

13.3 Then we need to implement the callback that we set in the update_mtu() function:

static void exchange_func(struct bt_conn *conn, uint8_t att_err,
			  struct bt_gatt_exchange_params *params)
{
	LOG_INF("MTU exchange %s", att_err == 0 ? "successful" : "failed");
    if (!att_err) {
        uint16_t payload_mtu = bt_gatt_get_mtu(conn) - 3;   // 3 bytes used for Attribute headers.
        LOG_INF("New MTU: %d bytes", payload_mtu);
    }
}

13.4 Forward the declaration of exchange_func().

Since the MTU changed callback, exchange_func(), is placed after the implementation of update_mtu() in main.c, we need to add a declaration of it before the update_mtu() function’s implementation.

Add this code line in main.c

static void exchange_func(struct bt_conn *conn, uint8_t att_err, struct bt_gatt_exchange_params *params);

13.5 Call the functions to update the data length and MTU size in on_connected().

Make sure to call all of the parameter exchange functions that we just implemented from the on_connected() callback function

update_data_length(my_conn);
update_mtu(my_conn);

14. Build and flash the application to your device, and connect to it using your smartphone.

Your log output should ook something like this

You should see a lot of logs from the different callbacks, stating all the different connection parameters for your connection.

nRF5340 Notice

Note that if you look at the solution for this exercise, you probably put all your configurations into your prj.conf. In the project Lesson3 Exercise2, you will see a prj_nrf5340dk_nrf5340_cpuapp.conf, which will be the config file if you are using the nRF5340 DK. The prj.conf file will not be included in the build if a prj_<BOARD_NAME>.conf is present in the same folder.
Register an account
Already have an account? Log in
(All fields are required unless specified optional)

  • 8 or more characters
  • Upper and lower case letters
  • At least one number or special character

Forgot your password?
Enter the email associated with your account, and we will send you a link to reset your password.