Monday, April 18, 2016

Nordic nrf8001 library for ST STM32F4xx devices

Recently, I began experimenting with the Nordic nrf8001 BLE chip.  The device contains a BLE stack and manages device state through a command and control interface.  This interface uses native SPI for data transfer and two control signals, a request (“RQN”) sent from the application MCU to the nrf8001, and a ready (“RDY”) signal sent from nrf8001 to MCU, both of which are used to indicate if the respective device is able to communicate.

I was interested in using the nrf8001 as a BLE relay with a low power ST STM32L1xx MCU.  I’m still experimenting with that chip but first I ported it to the more ubiquitous ST STM32F407 discovery kit as a first step.  The library, examples, and makefile for building with GNU gcc arm and flashing with ST link can be found here.

Nordic Semi distributes an Arduino/AVR-based library on this GitHub repository.  The authors also include some general porting instructions in the documentation/libraries/BLE/nRF8001-Porting-ACI-Library.md.  This blog post explains the porting procedure to get the library running with example code on the ST STM32F4 discovery kit.


Picture 1: stm32f4 discovery kit, nrf8001 breakout, and logic analyzer


Picture 2: Screen capture of logic analyzer capture showing stm32f4 communicating with the nrf8001 chip.  You can download a sample capture from reset to idle here.

The process for porting can be summarized as follows:

1. Cloning ble-sdk-arduino repo
2. Setup a working ST STM32f4 template build, and flashing directory with USART and SPI support
3. Revise file extension from .cpp to .c
4. Make file modifications around directory structure
5. IO arduino wrapper library
6. Patch aci_setup function in src/BLE/ with a compile time check if it is STM32F4XX directive
7. SPI init routine and GPIO (REQN and RDYN)
8. Adjust structure field type widths (uint8_t -> uint16_t) to accommodate ST standard peripheral driver pin enumerations
9. Simple example and validate use on bench

These steps are described in more detail below:

1. Start by checking out the master branch of 'ble-sdk-arduino':

git clone https://github.com/NordicSemiconductor/ble-sdk-arduino.git

2. Check out the this template directory for STM32F4 development.  Check out the 'stm32f4_start_template' as a starting point, the 'master' branch has all the changes discussed implemented.  We are going to be building using the GCC ARM Embedded toolchain from Launchpad.  This template has SPI and USART support already added.

git clone https://github.com/shraken/nrf8001-stm32f4.git
cd nrf8001-stm32f4
git checkout -b stm32f4_start_template

3. Copy the source and header files from 'ble-sdk-arduino' to our stm32f4 template directory and rename all .cpp source files to .c file extension.

mkdir src/BLE
mkdir inc/BLE
cp ../ble-sdk-arduino/libraries/BLE/*.h inc/BLE
cp ../ble-sdk-arduino/libraries/BLE/*.cpp src/BLE
find ./BLE/src/ -depth -name "*.cpp" -exec sh -c 'mv "$1" "${1%.cpp}.c"' _ {} \;

4. Add the Nordic nRF8001 library source files to the 'SRC' variable in the Makefile.  Ensure that the source files are added as shown below.

SRC = ./src/main.c \
 ./src/millis.c \
 ./src/usart.c \
 ./src/spi.c \
 ./src/debug.c \
 ./src/stm32f4xx_it.c \
 ./src/system_stm32f4xx.c \
 ./src/BLE/acilib.c \
 ./src/BLE/aci_queue.c \
 ./src/BLE/aci_setup.c \
 ./src/BLE/hal_aci_tl.c \
 ./src/BLE/lib_aci.c \

You must also set the 'LIBPATH' variable to the path of the ST Standard Peripheral Driver library.  The README.md provides a GitHub repository link that you can clone for this library.

5. The 'ble-sdk-arduino' library makes uses of Arduino IO pin routines, namely (digitalRead, digitalWrite, and pinMode).  Dummy shell functions are defined for these routines in the file 'io_support.c' that call the ST ARM standard peripheral driver functions.

Be sure to add the 'io_support.c' file to the SRC variable previously described above in the Makefile.

6. Patch the 'aci_setup_fill' function so that the setup message generated from nRFgo studio and stored in 'services.h' is gets copied into a data buffer from code space.  The first part of 'aci_setup_fill' function should now resemble,

#if defined (__AVR__)
    //For Arduino copy the setup ACI message from Flash to RAM.
    memcpy_P(&msg_to_send, &(aci_stat->aci_setup_info.setup_msgs[*num_cmd_offset]), pgm_read_byte_near(&(aci_stat->aci_setup_info.setup_msgs[*num_cmd_offset].buffer[0]))+2); 
#elif defined(__PIC32MX__)
    //In ChipKit we store the setup messages in RAM
    //Add 2 bytes to the length byte for status byte, length for the total number of bytes
    memcpy(&msg_to_send, &(aci_stat->aci_setup_info.setup_msgs[*num_cmd_offset]), (aci_stat->aci_setup_info.setup_msgs[*num_cmd_offset].buffer[0]+2)); 
#elif defined(STM32F4XX)
    memcpy(&msg_to_send, &(aci_stat->aci_setup_info.setup_msgs[*num_cmd_offset]), (aci_stat->aci_setup_info.setup_msgs[*num_cmd_offset].buffer[0]+2)); 
#endif

The STM32F4XX directive is provided in the Makefile DEFS.

7. The SPI and GPIO configuration must be initialized.  Replace the following Arduino specific code block with a single call to 'init_spi1' function.

SPI.begin();
//Board dependent defines
#if defined (__AVR__)
    //For Arduino use the LSB first
    SPI.setBitOrder(LSBFIRST);
#elif defined(__PIC32MX__)
    //For ChipKit use MSBFIRST and REVERSE the bits on the SPI as LSBFIRST is not supported
  SPI.setBitOrder(MSBFIRST);
#endif
SPI.setClockDivider(a_pins->spi_clock_divider);
SPI.setDataMode(SPI_MODE0);

with the patch below, mode0/0 is assumed by 'init_spi1'.

// bring up the GPIO pins and SPI HW interface
if (init_spi1(NRF8001_SPI, SPI_BaudRatePrescaler_64) != E_SUCCESS) {
    log_err("GPIO and SPI HW bringup failed");
}

The pin assignment for REQN, RDYN, RESET, and SPI (MOSI, MISO, SCLK) are stored in the 'aci_pins_t' structure within the 'aci_state_t' global variable.  The values are stored as uint8_t so just the pin number is represented and the port must be addressed manually in the project.

The port and pins are defined in spi.h

#define NRF8001_SPI  SPI1

#define SPI_COMMON_PORT GPIOA
#define MOSI_PIN        GPIO_Pin_7
#define MISO_PIN        GPIO_Pin_6
#define SCLK_PIN        GPIO_Pin_5

#define RESET_GPIO_PORT GPIOA
#define RESET_PIN  GPIO_Pin_0

#define RDYN_GPIO_PORT  GPIOA
#define RDYN_PIN  GPIO_Pin_4

#define REQN_GPIO_PORT GPIOB
#define REQN_PIN   GPIO_Pin_0

and they are set in the application's setup function, for instance

aci_state.aci_pins.reqn_pin   = REQN_PIN;
aci_state.aci_pins.rdyn_pin   = RDYN_PIN;
aci_state.aci_pins.mosi_pin   = MOSI_PIN;
aci_state.aci_pins.miso_pin   = MISO_PIN;
aci_state.aci_pins.sck_pin    = SCLK_PIN;
aci_state.aci_pins.reset_pin  = RESET_PIN;

The pins are actually configured in the same 'hal_aci_tl_init' function where the 'init_spi1' function is called.

8. The uint8_t structure width we mentioned previously for pin assignments only supports 8 unique bitmasks.  The data width for each pin value is expanded to a uint16_t type to accommodate the 16 possible pins for each port.  In the file 'hal_aci_tl.h',

uint16_t reqn_pin;    //Required
uint16_t rdyn_pin;    //Required
uint16_t mosi_pin;    //Required
uint16_t miso_pin;    //Required
uint16_t sck_pin;    //Required

9. A simple example is included in the 'master' branch under examples/ which advertises and sends a notification every 1 second.  The notification packet payload contains a tick count of the systick timer.

Connect the STM32F4-discovery or STM32-nucleo-F401 to your computer using the onboard USB and ST-link debugger.  Follow the directions in the README.md for building and flashing.

After flashing, launch your favorite Bluetooth central explorer -- I'm using Nordic Master Control Panel.  If everything is connected correctly, you should see a device named 'Hello'.

Connect a serial FTDI adapter (3.3V) with TX/RX to PA2 (TX) and PA3 (RX), i'm using a sparkfun model in the picture above.  Reset the MCU and verify the debug messages are printed to the console.  The characters typed on the console are buffered and sent in a 20 byte notification packet to the central/host after 20 bytes have been entered.

Picture 3 Nordic Master Control Panel showing 'Hello' BLE device name example