In the first part of this 3-part series, I gave a brief introduction to the Bluetooth Low Energy protocol. We went over how the messages look like, what the different layers of communication are, and what communication is BLE best suited for. Then, in the second part, I made a tutorial for working with the BLE P Click board. We went over how to use the adapted arduino library, and established a simple TX-RX communication with an Android device. In this third part, we will do one more Click board - BLE 2 Click. The library is significantly smaller and easier to use, so just follow through and we will establish a simple connection with an Android device again.
BLE2 Click with the firmware
BLE2 Click board carries the RN4020 module from Microchip. The firmware developers at Microchip developed and uploaded a protocol stack which is running on the chip itself. This greatly reduces the amount of work needed to get this Click board going. The chip works by communicating with the host MCU through UART. Each string command sent has a response. Sending the appropriate strings configures our board to work as we desire. This way, you don't even have to worry about the packet structures, making the protocol stack etc. The full list of commands can be found here.
Overview of the library
The library has two layers: HAL (Hardware access layer) and HWL (Hardware layer, also know as Logic layer). The HW layer contains functions which make the appropriate strings for configuring the NR4020, the string commands are then sent to the click using the HAL. The HAL accesses the UART initialized for this communication, and sends the string commands over it.
Overview of the library
Let's get to work!
Now that we have a basic overview of how our library works, we can set it up and get it working. We will go over a simple example with BLE2 Click working with a P32MX795F512L MCU, which will run on our EasyPIC Fusion development board. We will set up a service which will show the battery power. The value of the battery will increment every 5 seconds and be sent over BLE to an Android device.
The example looks quite big at first, but don't be intimidated. I will go through every part of the code and explain it thoroughly.
Fist off, we need to include our library and, since we are writing preprocessor directives, we might as well define some constants.
#include "ble2_hw.h" #define INIT_BLE #define MAX_BUFFER_LEN 0x40 #define CR 0x0D #define LF 0x0A #define TIME_OUT 50
Our library is used by including the ble_2.h file. Also, we defined CR - the carriage return and LF - line feed simbols. These are used for standard UART communication. I will get to our MAX_BUFFER_LEN and TIME_OUT values a little bit down the road.
Let's define some variables: we will need a receive buffer which will store the messages coming in from UART. For that we will use a byte array.
char rx_buff[MAX_BUFFER_LEN]; char data_len,data_ready; char tmr_cnt,tmr_flg; char batt_level; char batt_level_txt[10];
That's our rx_buff, and it is the size of MAX_BUFFER_LEN, which we have defined earlier. Our data_len variable holds the index of our array.
Since we are talking about UART, let's set up the interrupt which will catch these messages:
void UART2interrupt() iv IVT_UART_2 ilevel 6 ics ICS_AUTO { char tmprd; tmprd = UART2_Read(); if(tmprd == LF) { rx_buff[data_len] = 0; data_len++; data_ready = 1; }else{ rx_buff[data_len] = tmprd; data_len++; } U2RXIF_bit = 0; }
Here we see the usage of the data_len and data_ready variables. data_len will increment for every byte we receive, and in that way will store the messages in our byte array. data_ready flag is there to be set whenever we receive a full message over UART, signalized by the LF symbol.
Once we get a full message, we need to clear the buffer so that new messages can be received and parsed:
void reset_buff() { memset(rx_buff,0,data_len); data_len = 0; data_ready = 0; }
We will also need a timer:
//Timer2 Prescaler :1874; Preload = 63999; Actual Interrupt Time = 100 ms void InitTimer2(){ T2CON = 0x8070; T2IP0_bit = 1; T2IP1_bit = 1; T2IP2_bit = 1; T2IF_bit = 0; T2IE_bit = 1; PR2 = 39063; TMR2 = 0; } void Timer2Interrupt() iv IVT_TIMER_2 ilevel 7 ics ICS_SRS { T2IF_bit = 0; tmr_cnt++; if (tmr_cnt >= TIME_OUT) { tmr_cnt = 0; tmr_flg = 1; } }
Our timer generates an interrupt for every 100ms. In our example, we want to send messages every 5 seconds. In order to do this, we have set up the tmr_cnt variable, which will count how many interrupts we've got. When we have 50 interrupts (50 * 100ms = 5000ms = 5s), we will reset the counter, and set the timer flag (tmr_flg) to 1.
Our BLE2 Click has some additional pins for controlling it's operation. Those are the WAKE pin, the CMD pin and CONN pin. We have defined those as follows:
sbit RN_WAKE at LATC1_BIT; sbit RN_WAKE_DIR at TRISC1_BIT; sbit RN_CMD at LATD0_BIT; sbit RN_CMD_DIR at TRISD0_BIT; sbit RN_CONN at RB8_BIT; sbit RN_CONN_DIR at TRISB8_BIT;
In our initialization of the click, we will do the following routine:
void InitGPIO() { AD1PCFG = 0XFFFF; RN_WAKE_DIR = 0; // Set WAKE to output RN_CMD_DIR = 0; // Set CMD to output RN_CONN_DIR = 1; // Set CONN to input RN_WAKE = 0; // Set WAKE pin to zero RN_CMD = 0; // Set CMD pin to zero }
Let's look at the whole initialization of our MCU now:
void MCU_Init() { InitGPIO(); delay_ms(5000); UART2_Init(115200); Delay_ms(100); ble2_hal_init(); data_len = 0; data_ready = 0; U2IP0_bit = 0; U2IP1_bit = 1; U2IP2_bit = 1; U2RXIE_bit = 1; EnableInterrupts(); }
We set the GPIOs correctly, we initialize the right UART for communication between our MCU and the RN4020. Then we call the HAL initialization. This function will set the appropriate function pointers which will be used by the HAL to send messages over UART to the RN4020.
We are also setting our array counter and data flag to zero. The last few lines of code are enabling the UART interrupt.
Now that we know how to initialize our MCU, let's initialize the BLE2 Click board:
char wait_response(char *value) { char result = 0; while(!data_ready); if(strstr(rx_buff,value)) { result = 1; } memset(rx_buff,0,data_len); data_len = 0; data_ready = 0; return result; } void BLE_Init() { unsigned int line_no; line_no = 10; TFT_Write_Text("Start initialization",10,line_no); RN_WAKE = 1; while (!wait_response("CMD")); //Wait to response CMD line_no += 20; Delay_ms(100); ble2_reset_to_factory_default(1); //Reset RN4020 to factory settings while (!wait_response("AOK")); //Wait to response AOK TFT_Write_Text("Factory reset",10,line_no); line_no += 20; Delay_ms(100); TFT_Write_Text("Set module name: BLE2_Click",10,line_no); ble2_set_device_name("BLE2_Click"); //Set name BLE2_Click while (!wait_response("AOK")); //Wait to response AOK line_no += 20; Delay_ms(100); TFT_Write_Text("Configure services",10,line_no); /* Example: Table: 2-6 in Datasheet Health Thermometer + Heart Rate + Battery Services */ ble2_set_server_services(40000000); //Battery while (!wait_response("AOK")); //Wait to response AOK line_no += 20; Delay_ms(100); TFT_Write_Text("Configure features",10,line_no); ble2_set_supported_features(20000000); //Auto Advertise (Table: 2-5 in Datasheet) while (!wait_response("AOK")); //wait to response AOK line_no += 20; Delay_ms(100); TFT_Write_Text("Reboot module",10,line_no); ble2_device_reboot(); //reboot while (!wait_response("Reboot")); //wait to response Reboot }
The wait_response funciton will check our receive buffer for a responding message from the BLE2 click board. If the function finds that we have received a response message, it will return 1, otherwise 0. The BLE_Init function is pretty straight forward, we are sending messages and waiting for the appropriate response. We wake the RN4020 up by pulling the WAKE pin high, and wait for a "CMD" response. After that we are resetting the module to it's factory settings, and waiting for the "AOK" response. Next, we set the name of the device which will be shown to other devices in the BLE network, again we wait for "AOK" response. After setting the desired name, we must set the required services. By writing a hex value of 40000000 we are configuring the battery service which was built in with the firmware on the RN4020, again, we wait for "AOK". After that we are setting the supported features, again in reference with the datasheet. For all the changes to take effect, we must reboot the module, we issue a reboot command and wait for "Reboot" response.
Lastly, we will use the TFT display to see what's going on, we have a couple of functions:
void DrawFrame() { TFT_Fill_Screen(CL_WHITE); TFT_Set_Pen(CL_BLACK, 1); TFT_Line(20, 220, 300, 220); TFT_Line(20, 46, 300, 46); TFT_Set_Font(&TFT_defaultFont, CL_RED, FO_HORIZONTAL); TFT_Write_Text("BLE2 Click Board Demo", 25, 14); TFT_Set_Font(&TFT_defaultFont, CL_BLACK, FO_HORIZONTAL); TFT_Write_Text("EasyMx PRO v7", 19, 223); TFT_Set_Font(&TFT_defaultFont, CL_RED, FO_HORIZONTAL); TFT_Write_Text("www.mikroe.com", 200, 223); TFT_Set_Font(&TFT_defaultFont, CL_BLACK, FO_HORIZONTAL); TFT_Write_Text("BATTERY SERVICE", 40, 60); TFT_Write_Text("BATTERY LEVEL:", 40, 100); TFT_Write_Text("RECEIVE:", 40, 120); } void Display_BatteryLevel() { char txt[3]; ByteToStr(batt_level,txt); TFT_Set_Pen(CL_WHITE, 10); TFT_Set_Brush(1, CL_WHITE, 0, 0, 0, 0); TFT_Rectangle(150,100,230,115); TFT_Write_Text(txt,150,100); TFT_Write_Char(0x25,180,100); } void Display_Message() { TFT_Set_Pen(CL_WHITE, 10); TFT_Set_Brush(1, CL_WHITE, 0, 0, 0, 0); TFT_Rectangle(150,120,300,140); TFT_Write_Text(rx_buff,150,120); }
The Display_Message function will display all messages which are transferred from the Android device to our MCU via BLE.
Now let's look at our main:
void main() { Display_Init(); MCU_Init(); #ifdef INIT_BLE BLE_Init(); delay_ms(2000); #else RN_WAKE = 1; wait_response("CMD"); #endif DrawFrame(); InitTimer2(); while(1) { if(data_ready) { //If characteristic is configured as write //received messages come here Display_Message(); reset_buff(); } else { //Test: every 5sec increase baterry level (0 to 100%) //and send value via Bluetooth Low Energy if (tmr_flg) { batt_level++; if(batt_level > 100) { batt_level = 0; } Display_BatteryLevel(); if(RN_CONN) { //send battery level value if BLE connected shorttohex(batt_level, batt_level_txt); ltrim(batt_level_txt); ble2_write_server_characteristic_value_via_UUID("2A19",batt_level_txt); } tmr_flg = 0; } } } }
First we initialize the display by calling Display_Init, then we initialize the MCU. After that, if there is a INIT_BLE defined (and there is, check the first code block again), the program will run the initialization of the BLE2 Click. If not, it will just wake the click up by pulling the WAKE pin high. After that, we draw the basic frame on the TFT, and start our timer.
Our while loop is pretty straight forward: when we receive some messages, the data_ready flag will be set in the interrupt routine, if the flag is set, we display the certain message, and then reset the buffer. The timer flag (tmr_flag) is being set to 1 every 5 seconds. So every 5 seconds battery level (batt_level) will increment, the value will be displayed on the TFT, and then, if our CONN pin is high, it means that the device is connected, it converts the batt_level to a hexadecimal string and sends it over BLE.
You can download the BLE Scanner here. Using the scanner you can connect to the BLE2, and read the battery characteristic value:
So there you have it! BLE2 is pretty easy to use with the libraries we've provided. You can download them on libstock, or find them on GitHub, and remember to have fun!
References
RN4020 User Manual