Advanced Computer Systems project

Remote task loading for CC-1350

Ofir Cohen / Dima Kuznetsov, Fall 2018

Objective

The main goal of our project is to allow a developer to push new code to the board over the air (Bluetooth Low Energy). The rationale behind this project was to allow downloading updates of the sensor software and swapping out one type of software with another.

In both cases, the objective is to replace only part of the software (i.e. one or several tasks) without changing the whole operating system. This serves both to avoid big updates, and to reduce complexity of the update process (e.g. cannot re-flash OS while it is running). Finally, the latter, allowing to swap out sensor software, can be seen as a first step to creating an ecosystem where people write simple snippets of code, share them, and then, the mechanism can combine those and deploy them on the board.

Project overview

In our project we started with the TI’s simple peripheral example bundled with CC1350 SDK. In our solution, we assumed that as long we only add application code, we don’t need to modify the OS / drivers code.

Workflow

Initially when you start developing your app you would:

Then once you want to update the replaceable code, you:

Linker changes (and ota_app/ directory)

We introduced 2 changes to the linker process:

  1. Pre-link step (during the build)
  2. Linker configuration file customization (cc26xx_app.cmd)
  3. Post-link step (during the build)
    see “Extracting OTA image” sub-section  below.

Pre-link step

In our solution we added a new directory to the project. This is the directory that was designated to hold the replaceable code. All the elements (functions, variables, etc) derived from the code are marked in a way that allows easy extraction into an image that can be used for the OTA process.

During the build process, after all the sources are compiled into object files, we perform a pre-linker processing step, namely the following actions:

In this stage we rename .text / .data / .bss / .const  and other sections to their new names starting with .ota.

Linker configuration file customization

The modified simple peripheral project contains a modified version of the linker command file, that places the .ota sections separately (i.e. in separate memory regions) from their regular counterparts. See more in the next sub-section.

Memory Map Changes

After marking all ota_app/* code with custom sections, we updated the linker mapping file to separate the regular sections from the .ota.* sections. To achieve this, we:

This forces the linker to place all .ota.* contents at specific addressed, and, just as important, avoid placing the non-OTA code at addresses designated to the OTA code. The latter allows us to erase the OTA region without breaking the rest of the image.

Following is the modified memory layout of the board flash:

In this new layout, the OTA region is introduced. This region contains 2 OTA zones:

Each OTA zone is composed of 2 parts, the binary part, as generated by the project build process and extracted from the project binary, and the metadata, containing information on how relocate and launch the binary part. Here’s a short outline of the metadata structure:

Field

Description

Type

gen

OTA generation (increasing ordinal number)

uint32_t

entrypoint

Offset of the entrypoint function in the zone’s binary region

void fn (void)

size

Length of the binary region

size_t

loads[MAX_LOADS]

Vector of relocations to be done from the binary region into the SRAM memory

struct ota_load

done

Magic field to mark that region as valid

uint32_t

The loads section specifies what parts of the binary should be copied to the board’s SRAM. These copies are needed to enable use of global variables. Each load element represents an ELF segment that should be loaded into the memory. Here’s the description of the ota_load struct:

Field

Description

Type

dest

Copy destination address

uintptr_t

offset

Offset of the copied data inside the binary region

size_t

len

Number of bytes to copy

size_t

Additionally, to make sure the aforementioned loads do not interfere with the variables of the non-OTA code, we have to reserve part of the memory for the OTA programs. This way the compiler/linker will not allocate this memory to the OS/drivers when compiling the initial image.

OTA boot process

On power-on, the OTA code check its zone for code it execute:

Extracting OTA image

During the build of the firmware, the “.out ELF file”, we perform a post-processing step on the resulting ELF (.out file) and object files under ota_app dir.

The post-processing consists of:

  1. Extracting the raw binary data of the code section (.ota.text)
  2. Extracting the raw binary data of the data sections (e.g. .ota.data)
  3. Concatenating (1) and (2) into one binary blob
  4. Adding metadata fields to describe offsets within the blob

The generated output (ota.json) is in .json format and it describes everything we need for performing OTA update using the blob (data) and metadata (offsets).

BLE task

The background task of the simple peripheral (BLE) stack contains a callback that is triggered when GATT attributes are written. We reserved one of the GATT attributes (actually the 3rd one) to receive our “OTA update package” blob.

The “OTA update package” is transferred via a GATT client that reads the previously generated .json blob (see above) and communicates the entire blob (possibly in chunks) to the “BLE task” running on the board. Once the entire blob is transferred, the “inactive zone” is written with the new code and data and the board gets rebooted.

As we mentioned above, at boot time we determine that there’s a new app to run (via the “inactive zone” gen field), and we copy its contents to the active zone and boot into the new code.

BLE transport protocol

Writing to a BLE attribute allows us to send a packet of a limited length. We tested the board and achieved 80-bytes as the recommended MTU for sending requests to the board. We implemented a simple request based protocol to allow transporting the whole blob of needed information to the board.

Each BLE packet contains of a header and the payload, with the header fields detailed below:

Name

Description

Type

magic

Magic field, to identify OTA blob packets

uint32_t

total_size

Total size of the blob in bytes

uint16_t

cur_chunk

Index of the received blob chunk

uint8_t

num_chunks

Total number of chunks in this update request

uint8_t

chunk_len

Length of the current chunk

uint16_t

Following this header is the payload of the OTA, which is the serialized data found in the JSON document generated during the extraction phase.

The searizalied data can then be read using the following struct:

Name

Description

Type

entrypoint

Entrypoint for the metadata

uint16_t

size

Size of the data stream

uint16_t

loads[]

Array of ota_load structures

struct ota_load[]

data

Binary stream of the OTA

uint8_t[]

All the fields in the aforementioned headers are little endian.

Our OTA  vs TI’s OAD

During our development we came across TI’s implementation of over-the-air updates [2]. There are some similarities between our efforts, although, to our understanding, there are several key differences:

Links

[1] Our GitHub repository: https://github.com/dimakuz/cc1350-ota

[2] Over the Air Download (OAD), Texas Instruments: http://dev.ti.com/tirex/content/simplelink_cc13x0_sdk_1_30_00_06/docs/blestack/html/cc1350/oad/oad.html