Last week, we did a (not so) brief introduction to Bluetooth Low Energy, the super efficient younger brother of Bluetooth. Every radio module is complicated and involves multiple functioning layers. We went over all the layers, from physical and link layers on the controller then the application itself. We talked about how BLE advertises and establishes connections. We went over services and characteristics, which are crucial information for those connections.
Today, we will take what we learned and put it to use, as we implement a library for the BLE P click board. We will set up a connection between the click board and an Android device, the two devices will exchange information, and simulate a simple UART connection, with simulated TX and RX lines. With the BLE P you will need to download and use the Nordics NRF UART application for BLE devices. This will be ran on your mobile device (tablet, smartphone etc..).
Overview of the library
The BLE P Click carries the Nordic nRF8001 Bluetooth Low Energy chip. The developers in Nordic did a great job not only in developing the hardware, but also in developing a library for working with the radio module, so a big thank you goes to them. Let's take an overview of all the files included in this library, as well as the functions that they have provided in the structure.
The basic library consists of 7 source files, and 18 header files. I know, it sounds like horror movie for the budding developer, but don't worry, we will break it all down and help you to understand it fully. We will categorize these files in 3 different sections:
1.HAL - Hardware access layer
HAL - the layer responsible for establishing communication with the peripheral device. This layer provides us with functions for sending and reading bytes to and from the device.
2. HW - Hardware layer (Logic layer)
The logical layer, this layer is directly dependent on the HAL. It takes data from the HAL, parses it, constructs messages to be sent to the slave, and passes them to the HAL.
3. API Layer
The highest layer, a function in this layer usually encapsulates several functions in the HW layer, to make the usage of the module easier for the user of the library.
It all begins with the HAL
In order for us to port the library successfully to MikroE compilers, we need to change the HAL. For this case, I have created what we in development called wrappers. The HAL wrapper contains functions which are called the same as the HAL routines for Arduino IDE, but have their proper mikroC implementations. The wrapper is contained in the "blep_hal.c" and "blep_hal.h" files. These files contain functions for SPI communication, as well as gpio manipulation. Speaking of pins, there's a little trick here. In Arduino IDE, there are digitalWrite and digitalRead functions, which read and write to pins. The implementation of this function in our project is a bit different. In our compiler, the user must declare sbit types for each pin. And then, when doing digitalWrite or digitalRead, the user passes an enumerated type to the functions. Then the functions will manipulate the sbits depending of the enumerated type passed.
// sbit definitions for BLE P Click pins sbit CS at GPIOD_ODR.B13; sbit RST at GPIOC_ODR.B2; sbit ACT at GPIOA_IDR.B4; sbit RDYN at GPIOD_IDR.B10;
Here's the enumerated type:
typedef enum { REQN_PIN = 0, RDYN_PIN, MOSI_PIN, MISO_PIN, SCK_PIN, RESET_PIN, ACTIVE_PIN, OPTIONAL_CS, }pin_t;
Here's the implementation of the digitalWrite/digitalRead funcitons:
void digitalWrite(uint8_t pin_num, uint8_t level) { switch ( pin_num) { case REQN_PIN: CS = level; break; case RDYN_PIN: RDYN = level; break; case RESET_PIN: RST = level; break; case ACTIVE_PIN: ACT = level; break; } }
The second (first) HAL file is from Nordic, it is called "hal_aci_tl.c", it has several functions, but what interests us mostly is the static spi_readwrite function. This function would usually call Arduinos spi.Transfer routine. Here we return a function from my HAL wrapper:
static uint8_t spi_readwrite(const uint8_t aci_byte) { return blep_hal_transfer( aci_byte ); }
Here's the blep_hal_transfer function:
uint8_t blep_hal_transfer (uint8_t _payload) { return read_spi_p(_payload); }
It sends the payload, and catches whatever the SPI slave is returning to us. The read_spi_p is a function pointer pointing to the appropriate mikroC SPI_Read function.
We have now made "hal_aci_tl.c" work on the mikroC compiler, by adding our "blep_hal" wrapper. We can now move on higher levels.
Constructing and sending configuration commands
The nRF8001 has a big set of commands for a whole lot of different configurations. All of those commands are being parsed in the "acilib.c" file. The functions mostly take uint8_t pointers to buffers where the message is going to be stored. Then, using the defined commands from "aci_cmds.h", as well as the offsets and lengths of the messages from "aci_protocol_defines.h" and "acilib.h", the message is being stored in the buffer. Let's take a simple example of this, we will examine the message for getting the device version from it:
void acil_encode_cmd_get_device_version(uint8_t *buffer) { *(buffer + OFFSET_ACI_CMD_T_LEN) = 1; *(buffer + OFFSET_ACI_CMD_T_CMD_OPCODE) = ACI_CMD_GET_DEVICE_VERSION; }
As you can see, the buffer pointer is being incremented by the number defined as the offset of the command, in the first row we assign it the length of the command, which is 1. In the second row, the pointer is again incremented by the offset, and being assigned the command for getting the device version. This command will then be sent over SPI, using a different file.
In "lib_aci.c", the functions call "acilib.c" procedures to set up the commands, and then call the HAL to send those commands. Let's follow our example through, here we have lib_aci_sleep function:
bool lib_aci_sleep() { acil_encode_cmd_sleep(&(msg_to_send.buffer[0])); return hal_aci_tl_send(&msg_to_send); }
As you can see, it calls the previously described function for encoding the message, and calls the HAL to send that specific message.
Please wait for your turn in the queue!
The library implements a queue, as a main data structure to hold events. These events can either be the master transmitting data to the slave, or the slave sending data to the master. The functions for working with the queue are located in "aci_queue.c" and "aci_queue.h" files. Whenever there's an incoming or outgoing message event, the MCU dequeues it off the queue before executing it.
The setup
When the device powers up, and we have our pins and SPI communication ready, we need to set up the device for it to network properly. This is done with the " aci_setup.c " file. The file has one static function " aci_setup_fill ", which fills the outgoing message buffer with setup messages, and a " do_aci_setup " function, which will then send these messages and act according to the response.
I'm sorry to interrupt, but...
...what about interrupts? Arduino functions for handling interrupts are attatchInterrupt, detachInterrupt, noInterrupts() and interrupts(). MikroC implementations for these functions are located in the files "mikroc_interrupts.c/h". They are intentionally left blank, for the user to configure depending on the needed interrupt. In case of our example, the interrupt on pin for the stm32f107 would be like this:
void attachInterrupt () { RCC_APB2ENR.AFIOEN = 1; // Enable clock for alternate pin functions AFIO_EXTICR3 = 0x0300; EXTI_FTSR |= ( 1 << TR10 ); // Set interrupt on Rising edge EXTI_IMR |= ( 1 << MR10 ); // Set mask NVIC_IntEnable( IVT_INT_EXTI15_10 );// Enable External interrupt }
The detachInterrupt would be the opposite of this. As for interrupts() and noInterrupts(), those would be equivalent to mikroC's Enable/DisableInterrupts functions.
Building the example
Okay, now that we have our library, we can build our example. However, we will need a couple more files. Remember services and characteristics? You don't need to worry about those. Nordic has provided an application from which to generate those in the form of a services.h file. We will also need functions for simulating an uart connection. These functions are stored in "ble_uart.c/h" files, these are the main API for simulating an UART connection over BLE. One more file is needed, this file will run the infinite loop of checking and responding to our events, it is called "uart_aci.c". The example works with Nordics NRF UART application, it's free and can be downloaded here for devices which support BLE.
Let's now look at our example.
We first need to set up an UART communication between our computer and the MCU, so that we can monitor what's going on. Also we need to set the pins correctly, and initialize SPI:
UART1_Init(57600); UART1_Write_Text("Uart initialized"); UART1_Write(10); UART1_Write(13); GPIO_Digital_Output(&GPIOD_BASE, _GPIO_PINMASK_13); GPIO_Digital_Input(&GPIOA_BASE, _GPIO_PINMASK_4); GPIO_Digital_Input(&GPIOD_BASE, _GPIO_PINMASK_10); GPIO_Digital_Output(&GPIOC_ODR, _GPIO_PINMASK_2); // Set PC2 (RST) as digital output GPIO_Digital_Output(&GPIOA_BASE, _GPIO_PINMASK_0); // Set PA0 (CONN) SPI3_Init_Advanced(_SPI_FPCLK_DIV128, _SPI_MASTER | _SPI_8_BIT | _SPI_CLK_IDLE_LOW | _SPI_FIRST_CLK_EDGE_TRANSITION | _SPI_LSB_FIRST | _SPI_SS_DISABLE | _SPI_SSM_ENABLE | _SPI_SSI_1, &_GPIO_MODULE_SPI3_PC10_11_12);
In order for our HAL wrapper to work, we have to initialize it:
blep_hal_init();
Now we need to call the local setup() function, which looks something like this:
void setup(void) { // Do the BLE UART setup: ble_uart_setup(MOSI_PIN, MISO_PIN, SCK_PIN, REQN_PIN, RDYN_PIN, RESET_PIN); // Clear the serial buffer: clear_serial_buffer(); }
The BLE uart setup will take care of mapping our pins right, and thus finishing the HAL configuration. Note: If you want to change from polling to interrupt based functioning, you must change this line in the imeplementation of ble_uart_setup function:
aci_state.aci_pins.interface_is_interrupt = false;
Okay, so now that we have set up the HAL, we can run our infinite while loop:
while(1) { // Run the BLE UART loop once in every loop, // to let it handle any BLE events ble_uart_loop(); // handle all serial reads in one separate function: handle_serial_input(); }
Let's follow and see what happens in the ble_uart_loop():
void ble_uart_loop() { //Process any ACI commands or events aci_loop(); }
The ble_uart_loop calls the aci_loop() functions. The aci_loop is defined in "ble_uart.c". It is quite a big function. What it does is tracks all events and responds according to them. The first time this function is called in while(1), this function will run the BLE setup, every other time it will react to events which happen during networking.
The handle_serial_input just takes whatever you typed in your serial monitor and sends it over BLE to the mobile device.
When you start your program, the output on your serial monitor should look like this:
When you see "SETUP DONE" that means that the HAL has finished initializing, but not the whole module! Wait until you get the second message, then you can connect on Nordics NRF UART application. When connected, the BLE P click will send a hello message.
UART ACI Functions
We have already covered ble_uart_init() and ble_uart_loop, let's look at the remaining functions of our most highest application layer.
bool ble_uart_tx(uint8_t *buffer, uint8_t buffer_len);
This is the function with which you can send messages through BLE to your connected device, you just need a buffer to hold the message, and the lenght of the message, here's an example:
char hello[]="hello android!"; ble_uart_tx((uint8_t *)&hello[0], strlen(hello));
Now, wherever there's a tx, we also need an rx:
void ble_uart_rx(uint8_t *buffer, uint8_t len);
This function will take the received data over BLE and show them on our serial monitor.
And finally:
void ble_uart_name_set(const char* device_name, uint8_t name_length);
This function allows you to set the visible name of the device. The name provided will be shown to the devices which are scanning for other BLE devices.
Conclusion
There you have it! A full working BLE example, with the library ready for you. Whether you want to send heart rate data to your main medical unit, or just receive room temperature from a sensor over the air, BLE is surely a good solution. Now you can implement your project easily with the BLE P Click, and the library which Nordic provided, and which we ported over to our MikroC compilers.
Once again, a big thank you goes to the people at Nordic for providing a well working library with documentation, as well as other software tools with it.
You can find our ported library on GitHub. And a compiled and packaged version on Libstock.
What will we explore next? Wifi? Classic Bluetooth? Stick around and see for yourself!