BLE application with nRF51822: Firmware flashing

In the last post we have developed our first led blinking application, now we will see how to flash the firmware in the nRF51822 memory. You can use different technics to upgrade the firmware, either by using a SWD key or by using the pre installed DFU bootloader supporting OTA update (update over the air), shipped by default with the nRF51822 Smart Beacon Kit. In this post I’ll only explain how to use a SWD key to flash and debug your program.

First if you don’t have any SWD key compatible with Jlink, you can buy one for only 13$ on the following website : http://www.hotmcu.com/usbminijtagex-jlink-jtagswd-debuggeremula%E2%80%8Btor-p-46.html?cPath=3_25. The first thing you need to do is soldering 4 wires on the Smart Beacon Kit in order to have access to the nRF51822’s SWD port:

nrf51 swd jtag pinout

Here is the result on my Beacon Kit:

bluetooth low energy nrf51 beacon kit

You now have to connect your key to the SWD port of the Beacon kit. The connection depends of the key you are using but the pinout should be as following:

segger jtag swd pinout

A photo of my USB-MiniJTAG-EX JTAG/SWD Debugger/Emula​tor (I removed the PCB from its plastic case):

jtag swd usb programer arm

Finally a photo with the SWD key connected to the Beacon kit:

bluetooth nrf51 beacon kit jtag swd

The application we will use to flash our kit is Jlink Debugger developed by Segger, here is the link where you can download this software: https://www.segger.com/jlink-software.html

Once you’ve downloaded and installed Jlink Debugger, you should have in the terminal path JLinkExe, if it’s not the case please be sure JLinkExe binary directory is present in the PATH variable of your computer.

We are now ready to flash the led blinking firmware into the nRF51’s memory. Place the battery in your Smart Beacon kit battery holder and connect your SWD key to your computer.

        $ cd nrf51/
        $ JLinkExe
        $ J-Link> device nrf51
        $ J-Link> r
        $ J-Link> w4 4001e504 2
        $ J-Link> w4 4001e50c 1
        $ J-Link> w4 4001e514 1
        $ J-Link> w4 4001e504 0
        $ J-Link> r
        $ J-Link> loadbin led_blinking/led_blinking.bin 0x00000000
        $ J-Link> r

By typing the command JLinkExe you will enter into the Jlink terminal. From this terminal you’ll be able to flash, execute and debug your program. The command “device nrf51” allows to tell the debugger which target we will use. The command “r” allows to reset the target. Then we need to erase the whole flash memory, to do that we directly wrote at specific register address. If you look in the reference manual you will see that the NVMC config register, NVMC eraseall register and NVMC eraseuicr register are respectively located at 0x4001e504, 0x4001e50c and 0x4001e514 adresses. Writing the value 2 in NVMC config register enables the erase command. Writing the value 1 in NVMC eraseall register starts the chip erase. Writing the value 1 in NVMC eraseuicr register starts uicr erase (I’ll explain in another post what is the purpose of the UICR on the nRF51). Finally by writing the value 0 in NVMC config register we set back the chip memory in read only mode. After having erased the chip memory we can flash our firmware by typing the “loadbin” command following by two arguments, the first one is the firmare binary path and the second one the memory address at which the firmware will be placed, in our case 0x00000000.

  • To execute the firmware we just need to type the following command:
        $ J-Link> g

You should now see the green led blinking on the Smart Beacon kit ;-). The “g” command allows you to run the firmware on the target. If you want to halt the target to read some reasons you can type “h”. By typing “?” command in the Jlink terminal you’ll be abe to see all the commands supported by the target.

Since it’s pretty annoying to type all these commands before being able to execute and test a firmware, we can place all these commands in a file which will be passed as argument to Jlink.

        $ cd nrf51/
        $ mkdir sh
        $ cd sh/
        $ touch nrf51_erase.sh
  • Add the following commands in nrf51_erase.sh file:
device nrf51822
r
w4 4001e504 2
Sleep 200
w4 4001e50c 1
Sleep 200
w4 4001e514 1
Sleep 200
w4 4001e504 0
Sleep 200
r

To use this script during JLinkExe start, you just have to type the following command in a terminal:

        $ cd nrf51/
        $ JLinkExe sh/nrf51_erase.sh   
Advertisements

BLE application with nRF51822: Blinking a led

Since your development environment is almost completely setup, we can now start to develop a simple led blinking application for our demo board. The purpose of this application is extremely simple but allows to setup the nRF51’s SDK and see how to manipulate nRF51’s registers. First you need to download the SDK v9.0 at the following address: http://developer.nordicsemi.com/nRF51_SDK/

  • Uncompress and place the nRF51’s SDK in sdk folder:
        $ cd nrf51/
        $ mkdir sdk
        $ unzip nRF51_SDK_9.0.0_2e23562.zip -d nRF51_SDK_9.0.0_2e23562
        $ mv nRF51_SDK_9.0.0_2e23562 YOUR_PROJECT_PATH/nrf51/sdk

To be able to use the SDK from our application we need to include SDK’s source files in our Makefile. To do that we will develop a new Makefile which will be included from our project Makefile. The reason to split the Makefile is to manage dependencies in a better way. For example in a case we use only one Makefile located in led_blinking project directory, it would mean that for each new project we would create, we would need to copy the Makefile part corresponding to the SDK sources. Thanks to this new dedicated Makefile we will not have to copy it to each of our future projects we will develop.

  • Create new Makefile in sdk/ folder
        $ cd sdk/
        $ touch sdk.mk
  • Add the following code in sdk.mk
SDK_DIRECTORY = ../sdk/nRF51_SDK_9.0.0_2e23562/components

# System
INCLUDES  += $(SDK_DIRECTORY)/toolchain
INCLUDES  += $(SDK_DIRECTORY)/toolchain/arm
INCLUDES  += $(SDK_DIRECTORY)/toolchain/gcc
INCLUDES  += $(SDK_DIRECTORY)/device

SRC       += $(SDK_DIRECTORY)/toolchain/system_nrf51.c

# Drivers
ifeq ($(USE_DRV_BLE_FLASH), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/ble_flash
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/ble_flash -name *.c)
CPFLAGS   += -DUSE_DRV_BLE_FLASH
endif
ifeq ($(USE_DRV_CLK), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/clock
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/clock -name *.c)
CPFLAGS   += -DUSE_DRV_CLK
endif
ifeq ($(USE_DRV_GPIOTE), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/gpiote
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/gpiote -name *.c)
CPFLAGS   += -DUSE_DRV_GPIOTE
endif
ifeq ($(USE_DRV_LPCOMP), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/lpcomp
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/lpcomp -name *.c)
CPFLAGS   += -DUSE_DRV_LPCOMP
endif
ifeq ($(USE_DRV_NOSD), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/nrf_soc_nosd
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/nrf_soc_nosd -name *.c)
CPFLAGS   += -DUSE_DRV_NOSD
endif
ifeq ($(USE_DRV_PPI), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/ppi
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/ppi -name *.c)
CPFLAGS   += -DUSE_DRV_PPI
endif
ifeq ($(USE_DRV_PSTORAGE), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/pstorage
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/pstorage -name *.c)
CPFLAGS   += -DUSE_DRV_PSTORAGE
endif
ifeq ($(USE_DRV_QDEC), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/qdec
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/qdec -name *.c)
CPFLAGS   += -DUSE_DRV_QDEC
endif
ifeq ($(USE_DRV_RADIO_CFG), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/radio_config
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/radio_config -name *.c)
CPFLAGS   += -DUSE_DRV_RADIO_CFG
endif
ifeq ($(USE_DRV_RNG), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/rng
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/rng -name *.c)
CPFLAGS   += -DUSE_DRV_RNG
endif
ifeq ($(USE_DRV_RTC), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/rtc
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/rtc -name *.c)
CPFLAGS   += -DUSE_DRV_RTC
endif
ifeq ($(USE_DRV_SDIO), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/sdio
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/sdio -name *.c)
CPFLAGS   += -DUSE_DRV_SDIO
endif
ifeq ($(USE_DRV_SPI_SLV), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/spi_slave
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/spi_slave -name *.c)
CPFLAGS   += -DUSE_DRV_SPI_SLV
endif
ifeq ($(USE_DRV_SPI_MSTR), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/spi_master
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/spi_master -name *.c)
CPFLAGS   += -DUSE_DRV_SPI_MSTR
endif
ifeq ($(USE_DRV_SWI), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/swi
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/swi -name *.c)
CPFLAGS   += -DUSE_DRV_SWI
endif
ifeq ($(USE_DRV_TIMER), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/timer
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/timer -name *.c)
CPFLAGS   += -DUSE_DRV_TIMER
endif
ifeq ($(USE_DRV_TWI_MSTR), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/twi_master
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/twi_master -name *.c)
CPFLAGS   += -DUSE_DRV_TWI_MSTR
endif
ifeq ($(USE_DRV_UART), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/uart
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/uart -name *.c)
CPFLAGS   += -DUSE_DRV_UART
endif
ifeq ($(USE_DRV_WDT), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/wdt
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/wdt -name *.c)
CPFLAGS   += -DUSE_DRV_WDT
endif

INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/hal
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/hal -name *.c)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/common
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/common -name *.c)


# Libraries
ifeq ($(USE_LIB_DFU), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/bootloader_dfu
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/bootloader_dfu -name *.c)
CPFLAGS   += -DUSE_LIB_DFU
endif
ifeq ($(USE_LIB_BUTTON), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/button
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/button -name *.c)
CPFLAGS   += -DUSE_LIB_BUTTON
endif
ifeq ($(USE_LIB_CONSOLE), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/console
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/console -name *.c)
CPFLAGS   += -DUSE_LIB_CONSOLE
endif
ifeq ($(USE_LIB_CRC16), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/crc16
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/crc16 -name *.c)
CPFLAGS   += -DUSE_LIB_CRC16
endif
ifeq ($(USE_LIB_FIFO), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/fifo
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/fifo -name *.c)
CPFLAGS   += -DUSE_LIB_FIFO
endif
ifeq ($(USE_LIB_GPIOTE), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/gpiote
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/gpiote -name *.c)
CPFLAGS   += -DUSE_LIB_GPIOTE
endif
ifeq ($(USE_LIB_HCI), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/hci
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/hci -name *.c)
CPFLAGS   += -DUSE_LIB_HCI
endif
ifeq ($(USE_LIB_IC_INFO), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/ic_info
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/ic_info -name *.c)
CPFLAGS   += -DUSE_LIB_IC_INFO
endif
ifeq ($(USE_LIB_MEM_MNG), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/mem_manager
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/mem_manager -name *.c)
CPFLAGS   += -DUSE_LIB_MEM_MNG
endif
ifeq ($(USE_LIB_PWM), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/pwm
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/pwm -name *.c)
CPFLAGS   += -DUSE_LIB_PWM
endif
ifeq ($(USE_LIB_SCHEDULER), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/scheduler
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/scheduler -name *.c)
CPFLAGS   += -DUSE_LIB_SCHEDULER
endif
ifeq ($(USE_LIB_SENSORSIM), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/sensorsim
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/sensorsim -name *.c)
CPFLAGS   += -DUSE_LIB_SENSORSIM
endif
ifeq ($(USE_LIB_SH256), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/sha256
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/sha256 -name *.c)
CPFLAGS   += -DUSE_LIB_SH256
endif
ifeq ($(USE_LIB_SIMPLE_TIMER), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/simple_timer
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/simple_timer -name *.c)
CPFLAGS   += -DUSE_LIB_SIMPLE_TIMER
endif
ifeq ($(USE_LIB_TIMER), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/timer
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/timer -name *.c)
CPFLAGS   += -DUSE_LIB_TIMER
endif
ifeq ($(USE_LIB_TRACE), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/trace
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/trace -name *.c)
CPFLAGS   += -DUSE_LIB_TRACE
endif
ifeq ($(USE_LIB_UART), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/uart
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/uart -name *.c)
CPFLAGS   += -DUSE_LIB_UART
endif

INCLUDES  += $(SDK_DIRECTORY)/libraries/util
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/util -name *.c)

ifeq ($(USE_SD110), y)
# SD110, BLE stack
INCLUDES  += $(SDK_DIRECTORY)/softdevice/common
INCLUDES  += $(SDK_DIRECTORY)/softdevice/s110
endif

Because each project doesn’t necessarily need to use same drivers and/or libraries, drivers and libraries sources are compiled only if a dedicated flag is defined. Fo example if we want to use a uart driver in our project we will need to add USE_DRV_UART := y in the project’ Makefile.
We also need to specify our processor family to the SDK since it has been developed to support several processors. If you have a look to nrf.h file placed in sdk/nRF51_SDK_9.0.0_2e23562/components/device/nrf.h you will see the following code:

/* Family selection for family includes. */
#if defined (NRF51)
    #include "nrf51.h"
    #include "nrf51_bitfields.h"
    #include "nrf51_deprecated.h"
#elif defined (NRF52)
    #include "nrf52.h"
    #include "nrf52_bitfields.h"
    #include "nrf51_to_nrf52.h"
#else
    #error "Device family must be defined. See nrf.h."
#endif /* NRF51, NRF52 */

As you can see if we don’t specify the processor family it will raise an exception during build. So we need to add a flag in our project Makefile as following:

...
# Define target processor
TARGET 	:= 	NRF51
CPFLAGS += -D$(TARGET) 

# Generate dependency information
CPFLAGS += -MD -MP -MF .dep\$(@F).d
...

If you look in sdk/nRF51_SDK_9.0.0_2e23562/components/drivers_nrf/config directory you’ll find the nrf_drv_config.h file. This file allows you to setup some specific drivers, since it’s project dependent we will copy this file directly in our project directory:

        $ cd nrf51/led_blinking/
        $ mkdir cfg
        $ cp sdk/nRF51_SDK_9.0.0_2e23562/components/drivers_nrf/config/nrf_drv_config.h nrf51/led_blinking/cfg/

To include this header in our project we will add “INCLUDES += cfg/ ” in the project’ makefile:

...
# include directory
INCLUDES = include/   
INCLUDES += cfg/    
...

Since our first project won’t use the bluetooth low energy stack we have to use nrf_soc_nosd driver. To use it we just have to add “USE_DRV_NOSD := y” flag in the project’s makefile, as below:

...
# Drivers
USE_DRV_NOSD := y

include ../sdk/sdk.mk
...

We can now code the led blinking feature in our project. In a first time We’re gonna use the green led as blinking led. As you can see in PCA20006_Schematic_And_PCB.pdf document the green led is connected to gpio P0.12 (you can download this document from Nordic semiconductor website). The goal of the code below is to set gpio P0.12 to 1 wait 300ms then set gpio P0.12 to 0 and wait again 300ms. This code will be place in an infinite loop.

#include "nrf51.h"
#include "nrf51_bitfields.h"  // nrf51 register declaration
#include "nrf_delay.h"

#define GREEN_LED_PIN	12  // Pin P0.12

int main(void){
	// setup led gpio in output mode
	NRF_GPIO->PIN_CNF[GREEN_LED_PIN] = (GPIO_PIN_CNF_SENSE_Disabled << GPIO_PIN_CNF_SENSE_Pos)
                                            | (GPIO_PIN_CNF_DRIVE_S0S1 << GPIO_PIN_CNF_DRIVE_Pos)
                                            | (GPIO_PIN_CNF_PULL_Disabled << GPIO_PIN_CNF_PULL_Pos)
                                            | (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos)
                                            | (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos);

	do{
		// set to 1 led gpio (bit 12 corresponding to pin 12)
		NRF_GPIO->OUTSET = (1UL << GREEN_LED_PIN);
		// wait 300ms
		nrf_delay_ms(300);
		// set to 0 led gpio (bit 12 corresponding to pin 12)
		NRF_GPIO->OUTCLR = (1UL << GREEN_LED_PIN);
		// wait 300ms
		nrf_delay_ms(300);
	}while(1); //infinite loop

    return 0;
}

As explained in nrf51 reference manual to setup a pin you need to access to PIN_CNF register and set different parameters as the pin direction, input buffer activation or not, pull configuration, drive configuration or pin sensing mechanism. In our example we just need to setup the led pin in output mode, we don’t need to use a pull up or a pull down resistor neither an input buffer and since we are in output mode we don’t need the sensing mechanism. So to set up pin 12 in output mode, accordingly to the reference manual, we need to set bit 0 to 1. Since we don’t use other features the register’s bit remain to 0.

Screen Shot 2015-11-22 at 6.41.56 PM
Screen Shot 2015-11-22 at 6.48.55 PM

It’s extremely simple to put a gpio to a high level, thanks to OUTSET register, we only need to set to 1 the bit corresponding to the pin number. To set a gpio to a low level, we use the same mechanism than the one we use for setting a high level except that we use OUTCLR register instead.

You can now make your project, in the next post I will explain how to flash the binary file into nrf51 flash memory.

        $ cd nrf51/
        $ make

If every thing is og you should be able to read this line on you terminal:

../toolchain/arm_cm0/bin/arm-none-eabi-objcopy     -O binary -S       led_blinking.elf led_blinking.bin

BLE application with nRF51822: Makefile

The last element mandatory before being able to compile a nrf51 project is the Makefile. This file allows to tell the compiler what and where are the source and object files, it also allows to tell the linker how to generate the executable regarding the linker script. All the target information are also described in this file. I’m not going to explain all the options used in this Makefile but if you want more information you can visit the following website: http://www.opussoftware.com/tutorial/TutMakefile.htm#Rules

  • Create a Makefile file in led_blinking project folder:
        $ cd led_blinking
        $ touch Makefile
  • Declare the toolchain information and options:
# relative location of the toolchain binary
TOOLCHAIN = ../toolchain/arm_cm0/bin/arm-none-eabi-
# arm-none-eabi-gcc executable
CC   = $(TOOLCHAIN)gcc
# arm-none-eabi-objcopy executable          
CP   = $(TOOLCHAIN)objcopy    
# output format is raw binary and we strip the symbol information from source files                      
BIN  = $(CP) -O binary -S     
  • Declare the source directories:
# Sources
# linker script directory
LDSCRIPT = ../linker_script/nrf51_ld_script.ld   
# startup folder directory     
STARTUP_DIR = ../startup

# main.c directory
SRC  = src/main.c          
# startup.c directory                         
SRC  += $(STARTUP_DIR)/startup.c                      

# include directory
INCLUDES = include/   
# add option "-I" before each included folders                    
INCLUDE_DIR  = $(patsubst %,-I%,$(INCLUDES))   
  • Declare the compiler options
# Project name
PROJECT_NAME = led_blinking

# Options
OBJS  = $(SRC:.c=.o)

CPFLAGS = -mcpu=cortex-m0 -Os -gdwarf-2 -mthumb -fomit-frame-pointer -Wall -Wstrict-prototypes -fverbose-asm -Wa,-ahlms=$(<:.c=.lst) $(DEFS)
LDFLAGS = -mcpu=cortex-m0 -lc -mthumb -nostartfiles -T$(LDSCRIPT) -Wl,-Map=$(PROJECT_NAME).map,--cref,--no-warn-mismatch $(LIBDIR)
 
# Generate dependency information
CPFLAGS += -MD -MP -MF .dep\$(@F).d
  • Declare the project rules
# Rules
all: $(OBJS) $(PROJECT_NAME).elf  $(PROJECT_NAME).bin $(PROJECT_NAME).map
 
 %o: %c
	$(CC) -c $(CPFLAGS) -I . $(INCLUDE_DIR) $< -o $@

%elf: $(OBJS)
	$(CC) $(OBJS) $(LDFLAGS) -o $@
	
%bin: %elf
	$(BIN)  $< $@

clean:
	find ./ -name '*~' | xargs rm -f
	rm -f $(OBJS)
	rm -f $(PROJECT_NAME).elf
	rm -f $(PROJECT_NAME).map
	rm -f $(PROJECT_NAME).hex
	rm -f $(PROJECT_NAME).bin
	rm $(SRC:.c=.lst)
	rm -r  .dep*

At this point we should be able to build our simple project. To do so, in the project folder directory type make and press enter.

  • Build your project:
        $ cd led_blinking
        $ make
  • The output you should see:
-Iinclude/ -I../startup
../toolchain/arm_cm0/bin/arm-none-eabi-gcc -c -mcpu=cortex-m0 -Os -gdwarf-2 -mthumb -fomit-frame-pointer -Wall -Wstrict-prototypes -fverbose-asm -Wa,-ahlms=src/main.lst  -MD -MP -MF .dep\main.o.d -I . -Iinclude/ -I../startup            src/main.c -o src/main.o
../toolchain/arm_cm0/bin/arm-none-eabi-gcc -c -mcpu=cortex-m0 -Os -gdwarf-2 -mthumb -fomit-frame-pointer -Wall -Wstrict-prototypes -fverbose-asm -Wa,-ahlms=../startup/startup.lst  -MD -MP -MF .dep\startup.o.d -I . -Iinclude/ -I../startup            ../startup/startup.c -o ../startup/startup.o
../startup/startup.c:54:2: warning: taking address of expression of type 'void'
  &_end_of_stack,        // The initial stack pointer
  ^
../toolchain/arm_cm0/bin/arm-none-eabi-gcc src/main.o ../startup/startup.o -mcpu=cortex-m0 -lc -mthumb -nostartfiles -T../linker_script/nrf51_ld_script.ld    -Wl,-Map=led_blinking.map,--cref,--no-warn-mismatch  -o led_blinking.elf
../toolchain/arm_cm0/bin/arm-none-eabi-objcopy     -O binary -S       led_blinking.elf led_blinking.bin

As you can see, thanks to the Makefile, 3 new files have been generated. The most important one is the “.bin” file since it will be the one you will use to flash the nrf51. The “.map” file contains the memory map of your project, it could help you to find linker script bugs or to debug your application.

BLE application with nRF51822: Startup code and the Main entry

If you remember what we talked about in the previous post, before running the application, the processor needs to setup several things, like initialize the “.data” section in RAM and set “.bss” values to zero. Also if you remember we defined a section for the ISR vectors, so now we need to declare all the nRF51822 supported ISR.

  • Create a startup.c file in startup folder:
        $ cd nrf51
        $ mkdir startup
        $ cd startup
        $ touch startup.c

We need to declare few variables to properly setup “.data”, “.bss” sections and the top of the stack address. For your information, these variables are defined and set by the linker script. We also need to declare the main function as an extern function.

extern unsigned long _etext; // End of the code section
extern unsigned long _sdata; // Start address for the .data section
extern unsigned long _edata; // End address for the .data section
extern unsigned long _sbss; // Start address for the .bss section
extern unsigned long _ebss; // End address for the .bss section
extern void _end_of_stack; // Top of stack

extern int main(void);

I’ll speak later about how to setup the nRF51 SDK provided by Nordic but the following informations are coming from the SDK source code. In the linker script we declare a specific section for the interrupt vectors. As we need to have access to these vectors in our application we can declare a function pointer table in startup.c. In that way we will be able to implement these function later in our code.

  • ISR vectors prototypes:
void default_irq(void);
void Reset_Handler(void);
void NMI_Handler(void) __attribute__ ((weak, alias ("default_irq")));
void HardFault_Handler(void) __attribute__ ((weak, alias ("default_irq")));
void SVC_Handler(void) __attribute__ ((weak, alias ("default_irq")));
void PendSV_Handler(void) __attribute__ ((weak, alias ("default_irq")));
void SysTick_Handler(void) __attribute__ ((weak, alias ("default_irq")));
void POWER_CLOCK_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void RADIO_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void UART0_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void SPI0_TWI0_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void SPI1_TWI1_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void GPIOTE_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void ADC_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void TIMER0_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void TIMER1_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void TIMER2_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void RTC0_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void TEMP_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void RNG_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void ECB_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void CCM_AAR_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void WDT_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void RTC1_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void QDEC_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void LPCOMP_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void SWI0_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void SWI1_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void SWI2_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void SWI3_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void SWI4_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void SWI5_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));

As you can see the attribute weak and alias are used for the ISR vector prototypes. Actually for an application we don’t necessary need all the IRQ, so by default, thanks to alias attribute, the function called in case of interruption will be default_irq. The weak attribute means that we can override this function. For example POWER_CLOCK_IRQHandler can either point to default_irq (in case we don’t used it in our code) or point to POWER_CLOCK_IRQHandler if we have implemented it.

  • ISR vectors table:
__attribute__ ((section(".isr_vector")))
void (* const g_pfnVectors[])(void) =
{
	&_end_of_stack,        // The initial stack pointer
	Reset_Handler,         // Reset handler
	NMI_Handler,           // NMI handler
	HardFault_Handler,     // Hard fault handler
	0,0,0,0,0,0,0,         // 7 unused addresses   
	SVC_Handler,           // SVC handler
	0,0,                   // 2 unused addresses
	PendSV_Handler,        // Pend SV handler
	SysTick_Handler,       // Sys tick handler
	POWER_CLOCK_IRQHandler,// Power clock handler
	RADIO_IRQHandler,      // Radio handler
	UART0_IRQHandler,      // Uart 0 handler
	SPI0_TWI0_IRQHandler,  // SPI 0 handler
	SPI1_TWI1_IRQHandler,  // SPI 1 handler
	0,                     // 1 unused address
	GPIOTE_IRQHandler,     // GPIO handler
	ADC_IRQHandler,        // ADC handler
	TIMER0_IRQHandler,     // TIMER 0 handler
	TIMER1_IRQHandler,     // TIMER 1 handler
	TIMER2_IRQHandler,     // TIMER 2 handler
	RTC0_IRQHandler,       // RTC0 handler
	TEMP_IRQHandler,       // TEMP handler
	RNG_IRQHandler,        // RNG handler
	ECB_IRQHandler,        // ECB handler
	CCM_AAR_IRQHandler,    // CCM AAR handler
	WDT_IRQHandler,        // WDT handler
	RTC1_IRQHandler,       // RTC1 handler
	QDEC_IRQHandler,       // QDEC handler
	LPCOMP_IRQHandler,     // LDCOMP handler
	SWI0_IRQHandler,       // SWI0 handler
	SWI1_IRQHandler,       // SWI1 handler
	SWI2_IRQHandler,       // SWI2 handler
	SWI3_IRQHandler,       // SWI3 handler
	SWI4_IRQHandler,       // SWI4 handler
	SWI5_IRQHandler,       // SWI5 handler
	0,0,0,0,0,0,           // 6 unused addresses
};

The attribute section is used to explicitly tell the compiler to place this tab into “.isr_section”.

Now we have declared all the variables and prototype functions in our code we can implement the Reset_Handler function. On ARM Cortex M0 architecture the first instruction called is the reset IRQ. In that function we have to initialize “.data” and “.bss” sections.

  • Reset handler:
void Reset_Handler(void)
{
	unsigned long *src, *dest;
	
	src = &_etext; // place pointer to the end of .text section, data value are stored after 
                       // code section
	
	//init RAM data with value contained in FLASH
	for(dest = &_sdata; dest < &_edata; )
	{
		*(dest++) = *(src++);
	}
	
	//init .bss section with 0 value
	for(dest = &_sbss; dest < &_ebss; )
	{
		*(dest++) = 0;
	}
	
	main(); // call main application entry point
}

The last function we need to implement is the default_isr one. This is a dummy function just here in case an interrupt is triggered without the appropriate handler.

  • Default isr handler:
void default_irq(void){
    // place warning debug here
}

This is it for the startup code. The last function called by the reset handler is the main function which is the entry point of your application. So we will create a main.c file and we will place it in led_blinking/src folder.

  • Create main.c file in led_blinking/src:
        $ cd nrf51</code>
        $ mkdir led_blinking
        $ cd led_blinking
        $ mkdir src
        $ cd src
        $ touch main.c

In the main.c source file we only for the moment declare the main function.

  • main.c:
int main(void){
    return 0;
}

To resume, to properly boot up the processor we need to retrieve specific memory addresses and sections from the linker script and then declare the interrupt vectors table and finally implement the reset handler to be able to call the main function of our application.

BLE application with nRF51822 – The linker script

linker script c code
In embedded C langage development, you have to convert the humain readable file into a binary file that could be executed on microcontroller. The compiling step allows to convert this C code into a machine understandable code. But that’s not enough to get this code running on a microcontroller because the memory mapping is still not done. That’s why it’s mandatory to write the linker script in order to help you to place functions and variables at the right place in memory.

  • A quick look of the nRF51822′ memory mapping:

nrf51 memory mapping

Regarding the memory mapping above, code start in flash at 0x00000000 and end at start + 256ko (nRF51 flash size memory), this means functions could be placed between addresses 0x00000000 and 0x00040000 (hex values). The RAM memory is place at 0x20000000 and end at 0x20000000 + 16ko (nRF51 RAM size memory), this means variables could be placed between addresses 0x20000000 and 0x20002000.

Basically with GCC there are three sections. The first one is called .text, and contains functions. The second one is called .data and contains global and static variables initialized by application, and values are placed at compilation in flash. This means that during boot these variables should init from FLASH to RAM. The third one is called .bss and contains non initialized global and static variables. This means that during boot these variables should be explicitly set to zero. So now let’s see how we can specifies these addresses to linker script.

  • First we are gonna create the linker script file:
        $ cd nrf51</code>
        $ mkdir linker_script</code>
        $ cd linker_script</code>
        $ touch nrf51_ld_script.ld</code>
  • Once the file is created, open it and add the followingnRF51822′ memory layout description:
        MEMORY
        {
             RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 16k
             FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 256k
        }

The first memory region is the RAM with orgin 0x20000000 and length 16ko, x parameter means this region can execute function, r and w parameters mean that we can read and write in it. The second region is the FLASH with origin 0x00000000 and length 256ko, r parameter means that we can read in it and x means that we can execute function in it.

We also have to set manually the stack and heap sizes. To be short, the stack in the RAM area dedicated for allocated memory during function called (local variables), while heap RAM area is dedicated for dynamic memory allocation.

        _end_of_stack = (0x20000000 + 16k);/*RAM start address+16k*/
        _stack_size = 2k;
        _heap_size = 2k;
  • Once we specified the memory region we have to detail section regions:
        SECTIONS
        {
        }

Between these brackets we can declare all sections needed by application. The first section we have to declare is called .isr_vector and should be placed at the beginning of flash memory since the first instruction executed by ARM Cortex M0 is the Reset handler.

        .isr_vector : <
        {
            KEEP(*(.isr_vector)) /* vector table */
            . = ALIGN(4);
        } > FLASH</code>

KEEP command allow to tell ldscript that this section should not be eliminated (by garbage collection for example). ALIGN(4) command is used to tell to ldscript that this section is 4 bytes aligned. The >FLASH symbol means the section have to be placed in Flash memory.

  • After interrupt vectors we can directly place functions and constants located in flash:
        .text :
        { /*code and constants*/
            . = ALIGN(4);
            *(.text) /*.text sections (code)*/
            *(.text*) /*.text sections (code) */
            *(.rodata) /*.rodata sections (constants, strings, etc.)*/
            *(.rodata*) /*.rodata sections (constants, strings, etc.)*/
            . = ALIGN(4);
            _etext = .;/*Used by .data, to place initialized variable RAM values*/
        } > FLASH
  • We can now defined the section used to store uninitialized data in RAM:
        .bss :
        { /* code and constants */
            . = ALIGN(4);
            _sbss = .;/*Used by the startup code to initialize the .bss section*/
            *(.bss)
            *(.bss*)
            *(COMMON)
            . = ALIGN(4);
            _ebss = .;/*Used by the startup code to initialize the .bss section*/
        } > RAM
  • The .data section contains initialized data. This section is located in RAM but initialized values are placed in FLASH so at startup, the processor has to relocated these values in RAM.
        .data : AT ( _etext )
        {
            . = ALIGN(4);
            _sdata = .;/* Used by the startup in order to initialize the .data */
            *(.data)
            *(.data*)
            . = ALIGN(4);
            _edata = .;/* Used by the startup in order to initialize the .data */
        } > RAM
  • We can also add a DISCARD section in order to avoid some informations generated by standard librairies:
        /DISCARD/ :
        {
            libc.a ( * )
            libm.a ( * )
            libgcc.a ( * )
        }

After this post you should have a basic and functional linker script. This one works fine with ARM cortex M0 core and especially with the nRF51822 processor. Just keep in mind that with slight modifications this script can work with pretty much all ARM Cortex M family since the boot process is the same. If you need more informations about linker script I recommend you the following web site:

http://www.scoberlin.de/content/media/http/informatik/gcc_docs/ld_3.html#SEC5

If you want to learn more about the Cortex M0 boot up process:

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0553a/index.html

You can retrieve linker script file directly on my github repo:

https://github.com/jocelynmass/nrf51