Written by Frank Duignan, Electronics Engineer and Lecturer at TU Dublin, Ireland
This is part 4 of a tutorial series by Frank Duignan. Part 1 focused on GPIO – you can read it here; Part 2 focused on analogue input and output – you can read it here; and Part 3 focused on I2C communications – you can read it here.
In this example a BBC Microbit V2 will be programmed to behave as a step counter and it will report these over its BLE interface. This will not be a primer on Bluetooth. There are plenty of those to be found on the Internet. This post will be specifically about how you work with Zephyr and BLE on the Microbit V2.
BLE Roles for the BBC Microbit and a mobile phone
When your phone connects to a bluetooth device such as a pedometer it takes on a role defined by the Bluetooth SIG as “Central”. The pedometer or other device’s role is “Peripheral”. The central device initiates a connection with the peripheral. The peripheral device can advertise services which contain characteristics that can be read or written. They can also send notifications to the central device should a sensor value change. These services are identified using numeric values. In this example a 128 bit UUID of 00000001-0002-0003-0004-000000000000 is used to identify a step-count service which has a single characteristic (the actual step-count value) with a UUID of 00000001-0002-0003-0004-000000000005
This step count service is defined in Zephyr as follows:
// ********************[ Service definition ]********************
#define BT_UUID_CUSTOM_SERVICE_VAL BT_UUID_128_ENCODE(1, 2, 3, 4, (uint64_t)0)
static struct bt_uuid_128 my_service_uuid = BT_UUID_INIT_128( BT_UUID_CUSTOM_SERVICE_VAL);
BT_GATT_SERVICE_DEFINE(my_service_svc,
BT_GATT_PRIMARY_SERVICE(&my_service_uuid),
BT_GATT_CHAR1
);
The characteristic contained within this service is defined as follows:
// ********************[ Start of First characteristic ]**************************************
#define BT_UUID_STEPCOUNT_ID BT_UUID_128_ENCODE(1, 2, 3, 4, (uint64_t)5)
static struct bt_uuid_128 stepcount_id=BT_UUID_INIT_128(BT_UUID_STEPCOUNT_ID); // the 128 bit UUID for this gatt value
uint32_t stepcount_value=0; // the gatt characateristic value that is being shared over BLE
static ssize_t read_stepcount(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset);
static ssize_t write_stepcount(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, uint16_t len, uint16_t offset, uint8_t flags);
// Callback that is activated when the characteristic is read by central
static ssize_t read_stepcount(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset)
{
printk("Got a read %d\n",stepcount_value);
const char *value = (const char *)&stepcount_value; // point at the value in memory
return bt_gatt_attr_read(conn, attr, buf, len, offset, value, sizeof(stepcount_value)); // pass the value back up through the BLE stack
}
// Callback that is activated when the characteristic is written by central
static ssize_t write_stepcount(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset,
uint8_t flags)
{
uint8_t *value = attr->user_data;
printk("Got a write %d\n",len);
if (len == sizeof(stepcount_value)) // check that the incoming data is the correct size
{
memcpy(value, buf, len); // copy the incoming value in the memory occupied by our characateristic variable
}
else
{
printk("Write size is incorrect. Received %d bytes, need %d\n",len,sizeof(stepcount_value));
}
return len;
}
// Arguments to BT_GATT_CHARACTERISTIC = _uuid, _props, _perm, _read, _write, _value
#define BT_GATT_CHAR1 BT_GATT_CHARACTERISTIC(&stepcount_id.uuid,BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | BT_GATT_CHRC_NOTIFY, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, read_stepcount, write_stepcount, &stepcount_value)
// ********************[ End of First characteristic ]****************************************
As you can see from both of these definitions there are lots of C-macros involved. These are included to help the developer avoid some of the lower level details. The characteristic stores its value in the integer stepcount_value. The characteristic is defined as having the following properties: Read, Write and Notify.
If the central device issues a read requent then the function read_stepcount is called. This function copies the the contents of stepcount_value to a buffer (buf) which is then passed back to the central device.
If the central device writes to the microbit the function write_stepcount will be called. This copies data from a buffer passed by the lower level of the BLE stack into the memory occupied by stepcount_value.
void main(void)
{
int err;
int old_stepcount = 0;
err = lsm303_ll_begin();
if (err < 0)
{
printk("\nError initializing lsm303. Error code = %d\n",err);
while(1);
}
err = bt_enable(NULL);
if (err) {
printk("Bluetooth init failed (err %d)\n", err);
return;
}
bt_ready(); // This function starts advertising
bt_conn_cb_register(&conn_callbacks);
printk("Zephyr Microbit V2 minimal BLE example! %s\n", CONFIG_BOARD);
if (lsm303_countSteps(&stepcount_value) < 0)
{
printk("Error starting step counter\n");
while(1);
}
while (1) {
k_msleep(100);
// int bt_gatt_notify(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *data, u16_t len)
// conn: Connection object. (NULL for all)
// attr: Characteristic Value Descriptor attribute.
// data: Pointer to Attribute data.
// len: Attribute value length.
if (active_conn)
{
if (stepcount_value != old_stepcount) // only send a notification if the data changes
{
old_stepcount = stepcount_value;
bt_gatt_notify(active_conn,&my_service_svc.attrs[2], &stepcount_value,sizeof(stepcount_value));
}
}
}
}
It begins by initializing the LSM303 accelerometer on the Microbit. It then starts bluetooth advertising. The main loop sleeps for 100ms and then sends an update to the central device if there is an active BLE connection and if there has been a change to the step-count value.
Some significant changes need to be made to prj.conf to enable Bluetooth. On such change is this:
CONFIG_BT_DEVICE_NAME=”Microbit V2 BLE”
This allows you set the name that the microbit will broadcast when advertising its presence.
Full code for this example can be found github. Or, if you have any questions or comments in general, please feel free to reach out on the Zephyr Discord Channel.