Introduction

This workshop will show you how to create and integrate a basic Sensor Controller ADC driver with a blank TI-RTOS project. The training is expected to take about 2 h to complete. There should be at least an beginner level of knowledge of the C programming language as well experience with embedded software development to be able to complete the tasks.

The Sensor Controller ADC driver will measure an analog input voltage on one pin (DIO29) and set the green LED if the input ADC value is below a set threshold to indicate LOW input. If the ADC input value is above the set threshold it will notify the main application processor which then set the Red LED to indicate HIGH input. To vary the input voltage applied to the pin, an external voltage source can be connected to the analog input pin. In this workshop a jumper from the LaunchPad is used to short the analog input pin with adjacent pins (DIO28 and DIO30). There are two additional bonus tasks: one for Bluetooth® low energy and one for proprietary operation. Refer to the table below to find the required HW for each bonus task.

Compatible Connected MCU LaunchPad kits

This workshop can be completed with any one of the SimpleLink™ Wireless MCU with Sensor Controller devices described in the table below. Install the required Associated SimpleLink Software Development Kit matching your device. For more details on LaunchPads please visit the LaunchPad overview page.

Abbreviations / terminology

Abbreviation / terminology Definition
CCS Code Composer Studio
SC Sensor Controller
SCS Sensor Controller Studio
AUX RAM Sensor Controller Memory
RTC Real-Time Clock
RTOS Real-Time Operating System
TI-RTOS RTOS for TI microcontrollers
SDK Software Development Kit
S-W-MCU SimpleLink Wireless Micro Controller Unit

Prerequisites

Completed material

Software for desktop development

In order to start with this exercise you will need to download the correct Software Development Kit (SDK) for your LaunchPad. This training covers the CC13x0, CC13x2, CC2640R2 and the CC26x2 device families.

Device SDK downloads
CC13x0 SimpleLink CC13x0 Software Development Kit
CC2640R2 SimpleLink CC2640R2 Software Development Kit
CC13x2/ CC26x2 SimpleLink CC13x2/ CC26x2 Software Development Kit

Except for the relevant SDK for your choice of Launchpad, you also need the following software:

The following software applies for all device families:

  • Code Composer Studio version 9.2 or later
    Make sure that CCS is using the latest updates: HelpCheck for Updates

  • Sensor Controller Studio version 2.5.0 or later

  • For Bonus Task 1:

    • Bluetooth mobile app:
      • Android: BLE Scanner by Bluepixel Technology LLP - available on the Google Play store
      • iOS: LightBlue Explorer - Bluetooth Low Energy by Punch Through - available on the App Store

    OR

    • BTool (located in tools/blestack directory of the SimpleLink SDK installation)

Hardware

One LaunchPad connected with a USB micro cable:

For bonus task1:

For bonus task2:

  • Two CC13xx LaunchPads.

Getting started – Desktop

Install the software

1 - Run the Associated SDK installer (refer to table in introduction) to the default directory: C:\ti\

2 - Install Sensor Controller Studio and enable all patches. Patches can be enabled by first clicking UpdatesCheck for Updates. If any new patches are available, click UpdatesManage Updates... and apply all new patches. Note that the picture below is only used as an example. You may have a newer version, and there may be no patches available.

Getting started – Hardware

Connect the LaunchPad to your computer via the micro-USB cable. DIO29 on LaunchPad will be the ADC input. Connect the external variable voltage source to DIO29. As an alternative, a jumper or wire can be used. In this training DIO29 is connected to DIO28 or DIO30 via a jumper, borrowed from RXD<< on the LaunchPad. DIO28 will be set as digital output HIGH and DIO30 as digital output LOW, which will be used to test and verify the ADC driver and main application. Note that any jumper that does not interfere with the application can be used. See picture below for reference.

Task 1 – Import, build and download clean TI-RTOS Project

In this task you will import a clean TI-RTOS Driver project called Empty and run the program on your LaunchPad. This project will be used as a clean template to integrate the SC driver. It is a minimal TI-RTOS example project that has one single task that toggles the red LED on the LaunchPad once every second.

  1. In CCS, go to Project &rdash; Properties, or press Alt+Enter.
  2. Click on Build section to the left.
  3. Navigate to the Variables tab.
  4. Click on Show system variables checkbox at the bottom.
  5. Scroll down until you find the WORKSPACE_LOC variable, and note the value. This is your CCS workspace path for your project.

Do the following:

  1. Open CCS and press ProjectImport CCS Project

  2. Import the Empty Project found in your SDK install path folder subdirectory as shown below:

    • <SDK>\examples\rtos\CCxxxx_LAUNCHXL\drivers\empty

  1. Build and download the project to your LaunchPad by pressing F11. Then press F8 to allow the program to run. The red LED should now toggle once every second.

If you failed to download and run the "Empty" Project

  • Make sure you have connected the LaunchPad to your computer with a micro-USB Cable.
  • Make sure that you have selected the correct project, e.g.

    • called empty_CC2640R2_LAUNCHXL_tirtos_ccs for CC2640R2F.
    • called empty_CC1352R1_LAUNCHXL_tirtos_ccs for CC1352R1.
    • etc.
  • Unplug and plug in the USB cable again.

  • If this fails, you can try to run a "Erase Entire Flash" operation with UniFlash and retry.

Task 2 – Create and setup SCS project

In this task we will create a new Sensor Controller project in SCS.

SCS Documentation and Help

At any time in SCS, HelpSensor Controller Studio Help or press F1 for documentation and help.

Create SCS project

Do the following:

  1. Start SCS and open a new project, FileNew Project or Ctrl+N.
  2. Set the Project Name to ADC Level Trigger.
  3. Set the Operating system to TI-RTOS.
  4. Set Source code output directory to ./.
  5. Set Chip name corresponding to the device in use, e.g.:
    • CC2640R2F if using CC2640R2F.
    • CC1352R1F3 if using CC1352R1.
  6. Set Chip package to QFN48 7x7 RGZ.
  7. Add one task by clicking Add new, name it adc level trigger.
  8. Save the project, FileSave Project or Ctrl+S, to the CCS project base folder in your CCS workspace. See Task 1 on how to find your CCS workspace path.

Correct project file save path

The path set in Project file must be the CCS project base folder located in the CCS workspace.

Refer to the screen shot below for the CC2640R2F LaunchPad.

Setup SC ADC Driver

Task Properties

We need to specify the resources to be used. Go to Task Properties for the adc level trigger task, which can be accessed by clicking on the task name in the directory on the left hand side, above the Initialization Code. Select the task resources in the list below.

  • Analog Pins
    • Create one pin and name it ADC_INPUT.
  • Digital Output Pins
    • Create three pins and name them GREEN_LED, LOW, and HIGH. You can add more pins by clicking Add I/O usage to the right of the task resource name.
  • ADC
  • System CPU Alert
  • RTC-Based Execution Scheduling

Unnecessary pins

If you are using an external variable voltage source connected to the ADC input, then the two digital pins LOW and HIGH are not needed.

Make sure the Task resource settings match the screenshot below:

I/O mapping

Go to the I/O Mapping window and set the following pins to:

  • Analog pin ADC_INPUT to DIO29.
  • Digital pin GREEN_LED to DIO7.
  • Digital pin LOW to DIO28.
  • Digital pin HIGH to DIO30.

The pin order can vary. In the I/O mapping view you can at the top select which board you are using. For instance, select the CC1310 LaunchPad if you are using a CC1310 LaunchPad. It should look something like the picture below if the I/O mapping is displayed in grid mode.

Or, if I/O mapping is displayed in list mode, it should look something like the picture below.


Task 3 – Create Sensor Controller Driver

In this task you will implement and test the ADC driver in Sensor Controller Studio.

Specify the RTC Trigger Period

The RTC period is specified by Minimum task iteration interval in preferences, FilePreferences or Ctrl+P. See picture below for reference. A RTC period of 250 milliseconds is more than enough for this training.

Task testing specific

Specifying the RTC period in SCS only affects the task testing in SCS. This does not set the RTC period when the SC driver is integrated into other projects or applications, as this is done through the functions scifStartRtcTicks() and then started with scifStartRtcTicksNow().

RTC scheduling

The RTC period determines the execution interval of the SC task, if the SC task is scheduled by the RTC with the fwScheduleTask() function. fwScheduleTask(N) schedules the next task iteration in N RTC ticks. So for instance, let's say the RTC is operating at 200Hz. Scheduling the SC task with fwScheduleTask(1); would run the execution code at a 200Hz rate. With fwScheduleTask(5); the execution code would run at a (200/5)=40Hz rate, and so on.

However, to minimize current consumption it is recommended to select the highest possible tickPeriod so that the argument in fwScheduleTask() can be as low as possible.

Quiz

If a given SC task is scheduled with fwScheduleTask(2);, and the RTC period is set to 250 ms, what is the SC task period?

If a SC task period is 700 ms, and the RTC is set at 20 Hz, for which N is the SC task scheduled with the function fwScheduleTask(N);?

If a SC task is scheduled with fwScheduleTask(3);, and the SC task period is 1500 ms, what is the RTC period?

Adding a data structure member

For a given SC Task, data structure members are used in a SC task to store variables in AUX RAM. This allows the SC Task to store data between task iterations, and to exchange data between the SC and the main application processor. There are four different types of data structures that represent different types of data, shown in the table below. All data structure types have the same format (16 bit word).

Data structure Intended use
cfg Configuration of SC Task
input Input data for SC Task
output Output data from SC Task
state Internal state of SC Task

To add a data structure member, locate the Data structures box to the right in either code window. This is the same for both Initialization Code, Execution Code, and Termination Code. Click Add. Select which Data structure, and set Member name and Value. Optionally, but not necessary, give a Description and choose a Type. See example picture for ADC output value below.

Quiz

A SC Task is keeping track of how many ADC readings it has done between each task iteration. In which data structure should the variable be placed in?

A SC task is set to notify the main application when a given threshold is crossed, determined by the main application. In which data structure should the threshold be placed in?

Implement the SC ADC driver

It is time to create the ADC driver. The Sensor Controller ADC driver shall measure the analog voltage on DIO28 periodically and set/clear the green LED when the measured input voltage is below/above a set threshold value. The SC will alert the main application when the threshold is crossed, and the main application (TI-RTOS application in this case) will set/clear the red LED if the ADC input value was above/below the threshold.

The threshold is specified by the main application, stored in a cfg structure data member. Being under/above the threshold sets the state to be either low/high. The state is stored in a state structure member. The analog sensor value is sampled by the ADC and stored in an output structure data member.

Create data structure members

You need to create three data structure members: threshold in cfg structure, adcValue in output structure, and high in state structure. See Task 3 on how to create data structure members.

Initialization code

The initialization code will run once when the task is started by the TI-RTOS application. The initialization code should set the digital values for the HIGH and LOW pins, select the sampling pin for the ADC, and schedule the first execution of the execution code to the next RTC tick. See Task 2 for a more detailed explanation on how RTC scheduling and how fwScheduleTask() works.

The initialization code is presented below. Copy and paste the code snippet into the Initialization Code in SCS.

// Set `DIO28` High
gpioSetOutput(AUXIO_O_HIGH);

// Set `DIO30` Low
gpioClearOutput(AUXIO_O_LOW);

// Select ADC input
adcSelectGpioInput(AUXIO_A_ADC_INPUT);

// Schedule the first execution
fwScheduleTask(1);

Initialization code

Execution code

The execution code should enable the ADC, sample and store the ADC value, compare the converted ADC input value with the threshold, and alert the main application if a transition happens. As explained in Task 3, it is scheduled through the fwScheduleTask() function every RTC tick.

Copy and paste the code snippet below into the Execution Code in SCS. Note: There is an intentional error in the code, see next section.

// Enable the ADC
adcEnableSync(ADC_REF_FIXED, ADC_SAMPLE_TIME_2P7_US, ADC_TRIGGER_MANUAL);

// Sample the analog sensor
adcGenManualTrigger();
adcReadFifo(output.adcValue);

// Disable the ADC
adcDisable();

U16 oldState = state.high;
if (input.adcValue > cfg.threshold) {
    state.high = 1; // High input - > High state
    gpioClearOutput(AUXIO_O_GREEN_LED);
} else {
    state.high = 0; // Low input -> Low state
    gpioSetOutput(AUXIO_O_GREEN_LED);
}

if (oldState != state.high) {
    // Signal the application processor.
    fwGenAlertInterrupt();
}

// Schedule the next execution
fwScheduleTask(1);

adc level trigger Execution Code

Termination code

The termination code will run once when the SC task is either terminated by the main application, or executed without scheduling. The main purpose for the termination code is to do necessary cleanup.

For this task, there is no necessary cleanup, and the termination code is therefore left empty.

Compilation Error

With the SC driver implemented, it is time for test. However, nobody makes perfect code. In the execution code above there is a subtle syntax error. At first glance it's not obvious where the error is, but SCS can help. Go to the Task Testing window, ViewTask Testing or Ctrl+T. Select the ADC Level Trigger project if this is not already set.

You should now be present with an event log, such as the picture below. It shows the different stages of validating and compiling the project. This event log is only shown in this window when something went wrong during validation and compiling. As we see at the bottom of the log, during compilation of the execution code, it encountered a syntax error at line 12. Note that the line number can vary. See picture below for reference.

So, go back to the execution code window and find the specified line number. If you are observant, you should see that the code line

if (input.adcValue > cfg.threshold) {

is accessing the adcValue from the input structure, and not the output structure. A subtle bug indeed. Fix the line by changing input to output.

Now, when going back to the Task Testing window, the event log should no longer be there and you should be presented with the actual Task Testing screen.

More code errors?

If you got more syntax errors present, then you might have forgotten to create or name the pins correctly in the Task Properties window, or to create or name the data structure members in the code windows. Go back and double check.

Sensor Controller Task Testing

Now we test the task to verify it works as intended. Do the following:

  1. Go to the Task Testing window if not already there, ViewTask Testing or Ctrl+T.
  2. Select the ADC Level Trigger project if not already set.
  3. Select the Use simplified workflow. See picture below.
  4. Add Run Execution Code to the Task iteration action sequence.
  5. Click on Connect F12. You should now be in the graph tab. If this fails, make sure your LaunchPad is not used by Code Composer Studio.
  6. On the right side of the graph window you should find all data structure members created. If there are no variables you might have forgotten to create them. Configure the cfg.threshold value by double clicking the variable, set it to 400, and press Enter.
  7. Then, click Run Task Iterations Continuously F5.
  8. Select output.adcValue in the member struct box. A graph of the variable should appear.
  9. Place the wire or jumper between DIO29 and DIO30. The output.adcValue should be high and state.high should be 1. Now move the wire or jumper between DIO28 and DIO29. The output.adcValue should drop down to about 0 and state.high should be 0. The green LED should also turn on. See the picture below on what you should observe.
  10. When you are content that the driver is working, click Disconnect F11.

Generate Sensor Controller Driver

Now that everything is working well, the next step is to generate the driver from the SCS project.

Do the following:

  1. Go to the Code Generator window, ViewCode Generator or Ctrl+G.
  2. Select ADC Level Trigger as Current project.
  3. Either select Output automatically to auto generate code each time visiting the window, or click Generate driver source code.
  4. Click View output directory and double check it is the project base folder of the Empty Project. The files should also be visible in the project explorer in CCS, shown in the picture below. Try refreshing the project F5 if they do not show up.
  5. Build the project and see that it compiles without errors.

SCIF files do not appear in CCS project folder?

If no files are generated during code generation, then the output directory is most likely wrong. Go back to project settings and make sure Project file and Source code output directory path are correct. See Task 2 for help.


Task 4 – Implement TI-RTOS Application

Now we will write the TI-RTOS application and integrate the sensor controller driver you created in Task 3. This task will show you how to communicate with the SC, and the different ways to process the SC data, with focus on execution context.

Code documentation regarding the SC can be found at

  • scif_framework.h file from code generation.
  • SC Interface documentation (doxygen), found on the start page.
  • scif_how_to_use.html file from code generation. This file contains code snippets for most of the topics below with the tasks and variables names used in the SC code.

Refactoring and Cleanup TI-RTOS "empty" Project

Rename the single thread defined in main_tirtos.c called mainThread to a more descriptive and relevant name for this project. Refactor the function to tirtosScThread, by right click function and RefactorRename..., or click function and Alt+Shift+R.

TI-RTOS Task and POSIX Thread

The TI-RTOS example use POSIX threads instead of TI-RTOS tasks, but the POSIX thread has an underlying Task object and has support for many of the TI-RTOS kernel task APIs. This workshop will refer to the two terms, thread and task, Interchangeably. To summarize, a POSIX thread in TI-RTOS is practically a TI-RTOS task and a sensor controller task is not related to this as it describes a small program running on the sensor controller. Click here for more info about use of POSIX Thread in TI-RTOS

Also, replace the main loop in the newly renamed tirtosScThread() task with an empty loop while (1) {}.

I/O configuration

The green LED is controlled by the SC. Because of this the TI-RTOS application GPIO Driver must not initialize and open the green LED pin. To fix this, go to CCS and comment out the GPIOCCXXXX_DIO_07 line in gpioPinConfigs, CCxxx0_LAUNCHXL_PIN_GLED line in BoardGpioInitTable and CCxxx0_LAUNCHXL_GPIO_LED_GREEN in CCxxx0_LAUNCHXL_GPIOName. See code snippets below for reference (do not copy and paste, but comment out lines with // or Ctrl+Shift+/).

For example, if you have a CC2640R2 it looks like this, but it is the same steps for every LaunchPad.

GPIO_PinConfig gpioPinConfigs[] = {
    /* Input pins */
    GPIOCC26XX_DIO_13 | ...,  /* Button 0 */
    GPIOCC26XX_DIO_14 | ...,  /* Button 1 */

    /* Output pins */
//  GPIOCC26XX_DIO_07 | ....,  /* Green LED */
    GPIOCC26XX_DIO_06 | ....,  /* Red LED */
};

gpioPinConfigs in CC2640R2_LAUNCHXL.c

const PIN_Config BoardGpioInitTable[] = {
    CC2640R2_LAUNCHXL_PIN_RLED | PIN_GPIO_OUTPUT_EN | ...
//  CC2640R2_LAUNCHXL_PIN_GLED | PIN_GPIO_OUTPUT_EN | ...
    ....
    PIN_TERMINATE
};

BoardGpioInitTable in CC2640R2_LAUNCHXL.c

typedef enum CC2640R2_LAUNCHXL_GPIOName {
    CC2640R2_LAUNCHXL_GPIO_S1 = 0,
    CC2640R2_LAUNCHXL_GPIO_S2,
//  CC2640R2_LAUNCHXL_GPIO_LED_GREEN,
    CC2640R2_LAUNCHXL_GPIO_LED_RED,
    CC2640R2_LAUNCHXL_GPIOCOUNT
} CC2640R2_LAUNCHXL_GPIOName;

CC2640R2_LAUNCHXL_GPIOName in CC2640R2_LAUNCHXL.h

SC driver interaction and processing

There are two aspects with SC interaction and processing that needs to be addressed before implementation: how the SC and main application interact, and how to exchange data.

SC interaction

SC interaction is exclusively intended to be done through the two interrupt callbacks Control READY and Task ALERT. This way the SC can signal the main application at appropriate times.

The Control READY interrupt is signaled when one of the non-blocking task control functions, such as scifStartTasksNbl(), has executed successfully on the SC. However, the interrupt is encapsulated in the scifWaitOnNbl() function, and is in most cases, such as this training, not necessary to handle.

The Task ALERT interrupt is signaled when a SC task calls one of the functions fwGenAlertInterrupt(), fwGenQuickAlertInterrupt() or fwSwitchOutputBuffer(), as seen done in Task 3. This interrupt is used to signal the main application when some type of computation is finished, or some event has happened. This is what is interesting for us, as this allows us to free the main application from waiting for the SC driver to finish or some certain SC event to happen.

In the SC initialization part for this project, we handle both signals by registering a callback for each interrupt. This is only done for the sake of completeness. As only the Task ALERT signal is of importance for this project, we are only processing that signal.

Quiz

Which interrupt signal(s) is associated with the task control function scifStopTasksNbl(), issued from the main application?

Which interrupt signal(s) can a SC Task explicitly generate through procedure calls?

SC task data structure

See Task 3 for an overview of the data structures.

Accessing data stored in the SC AUX RAM can be done in two ways: either direct or indirect access. Which type of access is determined by the type of data structures used: single or multi-buffered. The only requirement is for multi-buffered data, where only indirect access can be used. Single-buffered can use either way. However, direct access is preferred as this is much faster.

Direct access in done through the variable scifTaskData structure, defined in scif.h. The hierarchy in which the data structure members are stored are as such: scifTaskData as base, next is the name of the SC task in camelCase, then the type of the data structure, and lastly the name of the data structure member.

For instance, the data structure member adcValue, located in the output structure, defined in the SC task ADC, is accessed as such: scifTaskData.adcLevelTrigger.output.adcValue. For a complete view of the scifTaskData structure, see the scif.h file.

The hierarchy and structure of the scifTaskData struct is highly dependent on the SC task implementation, and will vary from SC driver to SC driver.

Indirect access can be done through the Task data structure access functions defined in the scif_framework.h file, mainly by the scifGetTaskStruct function. By specifying the SC Task ID and the structure type, the relevant structure pointer is returned. As mentioned, this must be used to access multi-buffered data structures.

Quiz

A data structure member is defined in a SC Task, and is called counter. It is stored in the output structure. The SC Task is called Pin Counter. What is the resulting direct access code?

Initialize and setup of SC driver

To initialize and setup the SC driver a couple of necessary steps need to be done. First, add

#include "scif.h"
#define BV(x)    (1 << (x))

Scif header and macro

at the top of the empty.c file. The scif.h file is the main interface to the SC driver compiled and generated from the SCS project. BV() is a bit vector macro, which will be useful soon.

Create callback functions

The initialization code for the SC driver uses two callback functions to handle the two interrupt signals Task ALERT and Control READY. Therefore, copy and paste the code snippet above the tirtosScThread(), shown below.

void scCtrlReadyCallback(void)
{

} // scCtrlReadyCallback

void scTaskAlertCallback(void)
{

} // scTaskAlertCallback

SC callback functions

Initialize driver and register callbacks

Then, in the tirtosScThread(), before the main loop while(1), copy and paste the following code snippet. It is important to note that the initialization code should preferrably be run in a TI-RTOS context, such as a task context.

// Initialize the Sensor Controller
scifOsalInit();
scifOsalRegisterCtrlReadyCallback(scCtrlReadyCallback);
scifOsalRegisterTaskAlertCallback(scTaskAlertCallback);
scifInit(&scifDriverSetup);

// Set the Sensor Controller task tick interval to 1 second
uint32_t rtc_Hz = 1;  // 1Hz RTC
scifStartRtcTicksNow(0x00010000 / rtc_Hz);

// Configure Sensor Controller tasks
scifTaskData.adcLevelTrigger.cfg.threshold = 600;

// Start Sensor Controller task
scifStartTasksNbl(BV(SCIF_ADC_LEVEL_TRIGGER_TASK_ID));

SC Driver Initialization

So what does the code do? Let's study each code line.

// Initialize the Sensor Controller
scifOsalInit();
scifOsalRegisterCtrlReadyCallback(scCtrlReadyCallback);
scifOsalRegisterTaskAlertCallback(scTaskAlertCallback);
scifInit(&scifDriverSetup);

Driver specific initialization

The first line simply initializes the OSAL of the scif framework. The next two lines registers two callbacks for the two interrupt signals Control READY and Task ALERT from the SC. This is how the main application can communicate and work together with the SC, explained in the Section above. The fourth line initializes the SC with our SC task driver created in Task 3.

// Set the Sensor Controller task tick interval to 1 second
uint32_t rtc_Hz = 1;  // 1Hz RTC
scifStartRtcTicksNow(0x00010000 / rtc_Hz);

RTC initialization

The next line configures the RTC tick interval. This is more thoroughly explained in Task 3.

For this training we are setting the RTC interval to 1 second. However, feel free to play around with the RTC interval by modifying the rtc_Hz variable or the function argument directly. The function scifStartRtcTicksNow() takes a 32-bit value as an argument. The argument represents a tick interval, where bits 31:16 are the seconds, and bits 15:0 are the 1/65536 of a second.

// Configure Sensor Controller tasks
scifTaskData.adcLevelTrigger.cfg.threshold = 600;

// Start Sensor Controller task
scifStartTasksNbl(BV(SCIF_ADC_LEVEL_TRIGGER_TASK_ID));

Task initialization

Next line is the SC task configuration. This part is optional, and can be done at multiple appropriate times depending on the SC task. We configure the cfg.threshold variable, just as we did in Task 3. The SC task configuration can practically be done at any time in the main application, but is highly recommended to be done at appropriate times, such as during initialization or before SC task execution.

The last line starts the actual execution of the SC task. All scif functions handling task IDs, such as scifStartTasksNbl(), takes a bit vector as an argument. The macro BV() performs this transformation, and this is why it was included. The SC Task IDs can be found in the generated scif.h file.

Quiz

Which statement(s) are true?

Implement SC driver application processing

Now we can begin implementing the SC processing. We will do this in increments, implementing it in different application contexts and methods, and at the end discuss the pros and cons for each solution.

All three solutions are interrupt based, where the idea is the same:

  1. Wait for a Task ALERT signal.
  2. Clear the interrupt source.
  3. Process the SC task.
  4. Acknowledge the ALERT event to the scif framework.

The actual task processing shall be encapsulated in a function called processTaskAlert(). Copy and paste the code shown below.

void processTaskAlert(void)
{
    // Clear the ALERT interrupt source
    scifClearAlertIntSource();

    // Do SC Task processing here

    // Acknowledge the ALERT event
    scifAckAlertEvents();
} // processTaskAlert

SC Task Alert Handling

The processing is simple: fetch the state.high variable and copy the value to the Red LED pin. See Task 4 on how to access SC data. Copy and paste the code snippet below in processTaskAlert() above, where the SC task processing is marked.

// Fetch 'state.high' variable from SC
uint8_t high = scifTaskData.adcLevelTrigger.state.high;
// Set Red LED state equal to the state.high variable
GPIO_write(Board_GPIO_RLED, high);

adc level trigger processing

This will not run just yet, as we need to connect the processing to the interrupt signal. Below are the three different solutions presented.

Solution 1 – HWI

For the first solution the SC task alert handling and processing will be done in the scTaskAlertCallback() function. The scTaskAlertCallback() is executed in a HWI context, as the title suggests. So in scTaskAlertCallback(), call the processTaskAlert() function. See code snippet below.

void scTaskAlertCallback(void)
{
    // Call process function
    processTaskAlert();
} // scTaskAlertCallback

HWI SC Task alert process

Build and debug the project. Move the wire or jumper between DIO28 and DIO30 on the LaunchPad. You should see the green and red LED toggle in-between each other.

The application is now doing nothing until the SC task generates a Task ALERT signal. The HWI callback scTaskAlertCallback() is then called. In the HWI context the callback clears the interrupt source, read the state variable state.high, set or clears the red LED, and acknowledges the ALERT event.

Solution 2 – SWI

Doing extensive processing in a HWI process is not advised, as it blocks other high priority processes while running. For this solution, we will move the processing to a SWI process, and the HWI process will signal the SWI process.

At the top of the empty.c file, add the following

#include <ti/sysbios/knl/Swi.h>

// SWI Task Alert
Swi_Struct swiTaskAlert;
Swi_Handle hSwiTaskAlert;

// Function prototype
void processTaskAlert(void);

void swiTaskAlertFxn(UArg a0, UArg a1)
{
    // Call process function
    processTaskAlert();
} // swiTaskAlertFxn

SWI variables and function

In the top of the tirtosScThread task, add the following code snippet to initialize the SWI process.

// SWI Initialization
Swi_Params swiParams;
Swi_Params_init(&swiParams);
swiParams.priority = 3;
Swi_construct(&swiTaskAlert, swiTaskAlertFxn, &swiParams, NULL);
hSwiTaskAlert = Swi_handle(&swiTaskAlert);

SWI initialization

Now, copy and paste the following into scTaskAlertCallback(). Whenever the Task ALERT signal is raised, the HWI callback will signal the SWI process, where the entirety of the processing will be done.

void scTaskAlertCallback(void)
{
    // Post a SWI process
    Swi_post(hSwiTaskAlert);
} // scTaskAlertCallback

SWI signalling

Again, build and debug the project. Move the wire or jumper on the LaunchPad. You should observe the same behavior from the HWI solution.

But why was this whole process of moving the task processing to SWI context necessary if the behavior was the same? This would be much more obvious if the actual task processing was much more extensive computational wise. The current task is substantially small, and therefore will not impact the execution whatever the context it runs in. It is still advised to run the task processing at least in SWI context.

Solution 3 – Task (Thread)

Next solution is to move the processing in a task context. The idea is to signal the main task when the HWI is run, and then in turn run the processing in the task. We will use a Semaphore to synchronize the signaling.

Remove SWI Code

The SWI relevant code, such as the struct and handle variables, swiTaskAlertFxn() function, and initialization, can now be commented out or removed. This does not include the processTaskAlert() function.

First up, at the top of the empty.c file, add the relevant header file and variables.

#include <ti/sysbios/knl/Semaphore.h>
#include <ti/sysbios/BIOS.h>


// Main loop Semaphore
Semaphore_Struct semMainLoop;
Semaphore_Handle hSemMainLoop;

Semaphore variables

Next, In the top of the tirtosScThread task, initialize the semaphore struct and store the handle.

// Semaphore initialization
Semaphore_Params semParams;
Semaphore_Params_init(&semParams);
Semaphore_construct(&semMainLoop, 0, &semParams);
hSemMainLoop = Semaphore_handle(&semMainLoop);

Semaphore initialization

Now, in the scTaskAlertCallback() function, you will post to the Semaphore which should trigger the execution in the tirtosScThread while-loop.

void scTaskAlertCallback(void)
{
    // Post to main loop semaphore
    Semaphore_post(hSemMainLoop);
} // scTaskAlertCallback

Semaphore signalling

Now in the main loop in tirtosScThread(), we wait on the Semaphore indefinitely and afterwards call processTaskAlert(), see code snippet below.

while (1) {
    // Wait on sem indefinitely
    Semaphore_pend(hSemMainLoop, BIOS_WAIT_FOREVER);

    // Call process function
    processTaskAlert();
}

tirtosScThread while-loop

Build and debug the project. Move the wire or jumper on the LaunchPad. The same behavior again should be observed. This way of using the HWI process to signal a task process (or a SWI process in solution 2) is the way to go. Not only does it free up high priority processes, but it also streamlines the application processing, which is important in bigger applications.

main_tirtos.c:

/*
 *  ======== main_tirtos.c ========
 */
#include <stdint.h>

/* POSIX Header files */
#include <pthread.h>

/* RTOS header files */
#include <ti/sysbios/BIOS.h>

/* Example/Board Header files */
#include "Board.h"

extern void *tirtosScThread(void *arg0);

/* Stack size in bytes */
#define THREADSTACKSIZE    1024

/*
 *  ======== main ========
 */
int main(void)
{
    pthread_t           thread;
    pthread_attr_t      pAttrs;
    struct sched_param  priParam;
    int                 retc;
    int                 detachState;

    /* Call driver init functions */
    Board_initGeneral();

    /* Set priority and stack size attributes */
    pthread_attr_init(&pAttrs);
    priParam.sched_priority = 1;

    detachState = PTHREAD_CREATE_DETACHED;
    retc = pthread_attr_setdetachstate(&pAttrs, detachState);
    if (retc != 0) {
        /* pthread_attr_setdetachstate() failed */
        while (1);
    }

    pthread_attr_setschedparam(&pAttrs, &priParam);

    retc |= pthread_attr_setstacksize(&pAttrs, THREADSTACKSIZE);
    if (retc != 0) {
        /* pthread_attr_setstacksize() failed */
        while (1);
    }

    retc = pthread_create(&thread, &pAttrs, tirtosScThread, NULL);
    if (retc != 0) {
        /* pthread_create() failed */
        while (1);
    }

    BIOS_start();

    return (0);
}

empty.c:

/*
 *  ======== empty.c ========
 */

/* For usleep() */
#include <unistd.h>
#include <stdint.h>
#include <stddef.h>

/* Driver Header files */
#include <ti/drivers/GPIO.h>
// #include <ti/drivers/I2C.h>
// #include <ti/drivers/SDSPI.h>
// #include <ti/drivers/SPI.h>
// #include <ti/drivers/UART.h>
// #include <ti/drivers/Watchdog.h>

/* Board Header file */
#include "Board.h"

#include "scif.h"
#define BV(x)    (1 << (x))
#include <ti/sysbios/knl/Swi.h>
#include <ti/sysbios/knl/Semaphore.h>
#include <ti/sysbios/BIOS.h>

// Main loop Semaphore
Semaphore_Struct semMainLoop;
Semaphore_Handle hSemMainLoop;

// SWI Task Alert
Swi_Struct swiTaskAlert;
Swi_Handle hSwiTaskAlert;


void processTaskAlert(void)
{
    // Clear the ALERT interrupt source
    scifClearAlertIntSource();

    // Do SC Task processing here
    // Fetch 'state.high' variable from SC
    uint8_t high = scifTaskData.adcLevelTrigger.state.high;
    // Set Red LED to the state variable
    GPIO_write(Board_GPIO_RLED, high);

    // Acknowledge the ALERT event
    scifAckAlertEvents();
} // processTaskAlert


void scCtrlReadyCallback(void)
{

} // scCtrlReadyCallback

void scTaskAlertCallback(void)
{

    // Post to main loop semaphore
    Semaphore_post(hSemMainLoop);

} // scTaskAlertCallback


/*
 *  ======== mainThread ========
 */
void *tirtosScThread(void *arg0)
{

    // Semaphore initialization
    Semaphore_Params semParams;
    Semaphore_Params_init(&semParams);
    Semaphore_construct(&semMainLoop, 0, &semParams);
    hSemMainLoop = Semaphore_handle(&semMainLoop);

    /* Call driver init functions */
    GPIO_init();
    // I2C_init();
    // SDSPI_init();
    // SPI_init();
    // UART_init();
    // Watchdog_init();

    // Initialize the Sensor Controller
    scifOsalInit();
    scifOsalRegisterCtrlReadyCallback(scCtrlReadyCallback);
    scifOsalRegisterTaskAlertCallback(scTaskAlertCallback);
    scifInit(&scifDriverSetup);

    // Set the Sensor Controller task tick interval to 1 second
    uint32_t rtc_Hz = 1;  // 1Hz RTC
    scifStartRtcTicksNow(0x00010000 / rtc_Hz);

    // Configure Sensor Controller tasks
    scifTaskData.adcLevelTrigger.cfg.threshold = 600;

    // Start Sensor Controller task
    scifStartTasksNbl(BV(SCIF_ADC_LEVEL_TRIGGER_TASK_ID));


    while (1) {
        // Wait on sem indefinitely
        Semaphore_pend(hSemMainLoop, BIOS_WAIT_FOREVER);

        // Call process function
        processTaskAlert();
    }
}

Solutions summary

We have now implemented and tested three different solutions.

HWI Context: This gave us the fastest possible response time, as the actual processing was done as close to the Task ALERT signal as possible. However, this is never advised, as any HWI process should be kept to minimal execution time and not block other high priority processes from executing.

SWI Context: This solution does not have as fast response time as the HWI solution, since the HWI process must signal the SWI process. This is however preferred over the HWI solution, as it does not block other high priority processes.

Task Context: This solution can result in slower response time than the SWI context solution, but does not block other high priority processes. This solution can safely scale with increasingly more complex projects.

Solution two (SWI) is a viable method, but solution 3 (Task) is the preferred solution in most cases. It is flexible as the task priority can be adjusted, scalable with complex projects and can handle computational heavy processing without blocking time-critical high priority processes or interrupts.

Quiz

Which context(s) gives the fastest response time?

Which context(s) are preferred for computational heavy processing?

Which context(s) is the NOT recommended for computational heavy processing?


Bonus Tasks 1 – Integrate with BLE (CC2640R2F, CC13x2 or CC26x2)

In this training, we are porting our simple application into the BLE project Project Zero. It is meant to show you how the application would operate within a bigger and more complex project. We are going to use the already existing service Data Service to communicate the current state of the ADC input, high or low.

Hardware requirement

For this lab, you need one Bluetooth-enabled development boards. Supported development boards are:

Instructions for CC260R2 and CC13x2/CC26x2 are different. Please select the correct tab below.

Import and modify Project Zero

First, import a fresh version of Project Zero. You can use Resource Explorer in CCS. It can be found in the SimpleLink Academy package, under the Bluetooth LE section. Expand box below for reference.

Do the following:

  • Copy and paste (you can drag and drop with the mouse in CCS) all 6 scif related code files from the empty project into the Application/ folder in Project Zero.

  • Open the project_zero.c file.

  • Add #include "scif.h" at the top.

  • Add APP_MSG_SC_CTRL_READY and APP_MSG_SC_TASK_ALERT enum in the app_msg_types_t enum typedef.

  • Find the ledPinTable[] array. Comment out the Board_GLED member. Remember that this is the LED that the SC task controls.

  • Add the following function declarations

// Sensor Controller functions
static void scCtrlReadyCallback(void);
static void scTaskAlertCallback(void);
static void processTaskAlert(void);

Sensor Controller Function Declarations

  • Copy and paste the SC Driver initialization into ProjectZero_taskFxn().

  • In user_processApplicationMessage(), add the following case in the switch statement

    • Note that a case for APP_MSG_SC_CTRL_READY is not added because it is not needed in this application.
case APP_MSG_SC_TASK_ALERT:
  processTaskAlert();
  break;

Sensor Controller Switch Case

  • Add the following functions
static void scCtrlReadyCallback(void)
{
  // Notify application `Control READY` is active
  user_enqueueRawAppMsg(APP_MSG_SC_CTRL_READY, NULL, 0);
} // scCtrlReadyCallback

static void scTaskAlertCallback(void)
{
  // Notify application `Task ALERT` is active
  user_enqueueRawAppMsg(APP_MSG_SC_TASK_ALERT, NULL, 0);
} // scTaskAlertCallback

static void processTaskAlert(void)
{
  // Clear the ALERT interrupt source
  scifClearAlertIntSource();

  // Get 'state.high', and set highStr to appropriate string
  uint16_t high = scifTaskData.adcLevelTrigger.state.high;
  char *highStr = (high != 0) ? "HIGH" : "LOW";
  // Set the highStr to the String characteristic in Data Service
  DataService_SetParameter(DS_STRING_ID, strlen(highStr), highStr);

  // Set/clear red LED.
  PIN_setOutputValue(ledPinHandle, Board_GPIO_RLED, high);

  // Acknowledge the ALERT event
  scifAckAlertEvents();
} // processTaskAlert

Sensor Controller Functions

  • Build and debug the project. The first build may take a while.

Import and Modify Project Zero

First, import a fresh version of Project Zero. Import it to CCS from the SDK installation directory: <SDK>\examples\rtos\CC26X2R1_LAUNCHXL\ble5stack\project_zero.

Do the following:

  • Copy and paste (you can drag and drop with the mouse in CCS) all 6 scif related code files from the empty project into the Application/ folder in Project Zero.

  • Open the project_zero.c file.

  • Add #include "scif.h" at the top.

  • Add PZ_SC_CTRL_READY and PZ_SC_TASK_ALERT in the application messages defines list.

    // Types of messages that can be sent to the user application task from other
    // tasks or interrupts. Note: Messages from BLE Stack are sent differently.
    #define PZ_SERVICE_WRITE_EVT     0  /* A characteristic value has been written     */
    #define PZ_SERVICE_CFG_EVT       1  /* A characteristic configuration has changed  */
    #define PZ_UPDATE_CHARVAL_EVT    2  /* Request from ourselves to update a value    */
    #define PZ_BUTTON_DEBOUNCED_EVT  3  /* A button has been debounced with new value  */
    #define PZ_PAIRSTATE_EVT         4  /* The pairing state is updated                */
    #define PZ_PASSCODE_EVT          5  /* A pass-code/PIN is requested during pairing */
    #define PZ_ADV_EVT               6  /* A subscribed advertisement activity         */
    #define PZ_START_ADV_EVT         7  /* Request advertisement start from task ctx   */
    #define PZ_SEND_PARAM_UPD_EVT    8  /* Request parameter update req be sent        */
    #define PZ_CONN_EVT              9  /* Connection Event End notice                 */
    #define PZ_READ_RPA_EVT         10  /* Read RPA event                              */
    #define PZ_SC_CTRL_READY        11  /* Sensor controller control ready             */
    #define PZ_SC_TASK_ALERT        12  /* Sensor controller task alert                */
    

    Add two sensor controller message defines.

  • Find the ledPinTable[] array. Comment out the CONFIG_PIN_GLED member. Remember that this is the LED that the SC task controls.

  • Add the following function declarations

    // Sensor Controller functions
    static void scCtrlReadyCallback(void);
    static void scTaskAlertCallback(void);
    static void processTaskAlert(void);
    

    Sensor Controller Function Declarations

  • Add the following functions

    static void scCtrlReadyCallback(void)
    {
      // Notify application `Control READY` is active
        ProjectZero_enqueueMsg(PZ_SC_CTRL_READY, 0);
    } // scCtrlReadyCallback
    
    static void scTaskAlertCallback(void)
    {
      // Notify application `Task ALERT` is active
        ProjectZero_enqueueMsg(PZ_SC_TASK_ALERT, 0);
    } // scTaskAlertCallback
    
    static void processTaskAlert(void)
    {
      // Clear the ALERT interrupt source
      scifClearAlertIntSource();
    
      // Get 'state.high', and set highStr to appropriate string
      uint16_t high = scifTaskData.adcLevelTrigger.state.high;
      char *highStr = (high != 0) ? "HIGH" : "LOW";
      // Set the highStr to the String characteristic in Data Service
      DataService_SetParameter(DS_STRING_ID, strlen(highStr), highStr);
    
      // Set/clear red LED.
      PIN_setOutputValue(ledPinHandle, CONFIG_PIN_RLED, high);
    
      // Acknowledge the ALERT event
      scifAckAlertEvents();
    } // processTaskAlert
    

    Sensor Controller Functions

  • Copy and paste the SC Driver initialization into ProjectZero_taskFxn().

  • In ProjectZero_processApplicationMessage(), add the following case in the switch statement:

    • Note that a case for PZ_SC_CTRL_READY is not added because it is not needed in this application.
    case PZ_SC_TASK_ALERT:
      processTaskAlert();
      break;
    

    Sensor Controller Switch Case

  • Build and debug the project. The first build may take a while.

Test BLE device

There is only a specific guide here for using the iOS app LightBlue Explorer, but any of the other test solutions described in Bluetooth Low Energy Fundamentals workshop can also be used.

1. Start LightBlue Explorer

Using your iOS device, find the LightBlue Explorer app and open it.

2. Scan for BLE devices

The app should begin scanning for BLE devices automatically but you can refresh the list by pulling down. The device name is Project Zero or Project Zero R2.

Connect to the device by clicking on the name. You can press the filter button in the top right corner to filter out all devices out of reach by setting a RSSI threshold (for example -50 dBm).

3. Find Data Service (UUID 1130) Scroll down until you find the data service and open the string characteristic which is marked with an orange box below.

4. Read String Characteristic In the top right corner, change the format to UTF-8 and then press the "Read again" button to read the current characteristic value. This value will mirror the state.high variable in the sensor controller driver taht indicate either HIGH or LOW input value onn the ADC input pin. Move the header to change the ADC input between high and low input and press the "Read again" button to retrieve the new state.


Bonus Tasks 2 – Integrate with Proprietary RF (CC13x0 or CC13x2)

In this training, we are again porting our simple application. However, now we are connecting it with Proprietary RF instead of BLE. We are modifying an already existing Proprietary Tx example project, called RF Packet TX. The application will be the transmitter, reading and storing the dawn state in a RF packet, while the second device will be the receiver, using SmartRF Studio to listen for the packet.

Hardware requirement

This bonus task requires two CC13xx LaunchPads; one for Tx and one for Rx.

Import and modify RF Packet TX

First, import a fresh version of the project RF Packet TX from the Resource Explorer in CCS. It can be found in the SimpleLink CC13x0 SDK or [SimpleLink CC13x2 SDK][CC13x2 SDK] package. Expand box below for reference.

In CCS, do the following:

  • Copy and paste all 6 scif related code files from Empty (Minimal) Project into the project base folder in RF Packet TX.

  • Open up rfPacketTx.c in CCS.

  • Copy and paste the following code snippet at the top of the file.

#include <string.h>   // strlen() and memcpy()
#include <ti/sysbios/knl/Semaphore.h>
#include "scif.h"

#define BV(x)    (1 << (x))

Semaphore_Struct semMainLoop;
Semaphore_Handle hSemMainLoop;

Header Includes and Variable Declarations

  • Go to the already existing pinTable[] array and change the first PIN instance from Board_LED1 to Board_LED0. Remember that LED1 is controlled by the SC.

  • Copy and paste the SC callbacks.

void scCtrlReadyCallback(void)
{
    // Do nothing
} // scCtrlReadyCallback

void scTaskAlertCallback(void)
{
    // Signal main loop
    Semaphore_post(hSemMainLoop);
} // scTaskAlertCallback

SC Callbacks

  • In TxTask_init(), add initialization for the semaphore
// Main loop Semaphore initialization
Semaphore_Params semParams;
Semaphore_Params_init(&semParams);
semParams.mode = Semaphore_Mode_BINARY;
Semaphore_construct(&semMainLoop, 0, &semParams);
hSemMainLoop = Semaphore_handle(&semMainLoop);

Semaphore Initialization

  • In txTaskFunction(), copy and paste the SC Driver initialization at the top of the function.

  • At the bottom of txTaskFunction(), replace the whole main loop with the code below

// Main loop
while(1) {
  // Wait for signal
  Semaphore_pend(hSemMainLoop, BIOS_WAIT_FOREVER);

  // Clear the ALERT interrupt source
  scifClearAlertIntSource();

  // Get 'state.high', and set highStr to appropriate string
  uint16_t high = scifTaskData.adcLevelTrigger.state.high;
  const char *highStr = (high != 0) ? "HIGH" : "LOW";
  uint16_t highStrLen = strlen(highStr);

  // Populate packet, and set pktlen
  packet[0] = (uint8_t)(seqNumber >> 8);
  packet[1] = (uint8_t)(seqNumber++);
  memcpy(packet + 2, highStr, highStrLen);
  RF_cmdPropTx.pktLen = 2 + highStrLen;

  // Send packet Tx
  RF_runCmd(rfHandle, (RF_Op*)&RF_cmdPropTx, RF_PriorityNormal, NULL, 0);

  // Toggle pin
  PIN_setOutputValue(ledPinHandle, Board_LED0, high);

  // Acknowledge the ALERT event
  scifAckAlertEvents();
}

Application Main Loop

  • Build and debug project.

Test Proprietary RF application

To test the application, do the following:

  • While the transmitter is running on one of the devices, open up SmartRF Studio. If both devices show up on the list of connected devices, disconnect the transmitter while setting up the receiver.
  • Double click the available device, and choose Proprietary Mode.
  • Choose the 50 kbps, 2-GFSK, 25 KHz deviation setting at the top. This is usually the default.
  • Go to the Packet RX tab.
  • In the tab, check of infinite packet count, set viewing format to Text, and click Start.
  • Try moving the wire or jumper on the transmitter. Packets with the dawn state should show up in SmartRF Studio.

You should see something like the picture below. Note the red markings.

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.