In this lab, we will explore another driver option: TI drivers with RTOS. We will know the differences between register access, TI driverlib, and TI Drivers.
- TI Drivers is a set of common, consistent functional APIs that are supported across the TI SimpleLink portfolio, enabling maximum code portability.
- TI Drivers today require an RTOS.
- All devices in the SimpleLink platform have support for TI-RTOS. Most SimpleLink devices also support FreeRTOS.
Task1 Check the project file- To start this lab, let’s look inside the “Software” category in TI Resource Explorer. Expand the “TI Drivers” folder, locate the “empty” example.
- Import the “empty” example-> TI-RTOS->CCS to CCS Cloud.
- Two projects will be loaded into the CCS cloud. In addition to the empty project, tirtos project will also be added
- Inside the empty project, there are two main c files: empty.c and main_tirtos.c.
- Let’s take a look at the empty.c source file. The project
provides a framework for your main loop, which is running inside of a single thread called “mainThread.”- Inside of mainThread, we see that we have to initialize the GPIO driver before we can use it by calling the GPIO_init() function. Once the driver has been initialized, we can use the driver to set the GPIO high using the GPIO_write() API. We also notice that we are using the Board_PIN_LED0designator, which was declared in our Board.h file.
- These APIs are TI drivers and the full document can be accessed via this link.
- Further down mainThread, we see our while(1) loop, which is our main loop for this simple example. Within this loop, we use another GPIO Driver API to toggle the pin. We do this once every second, which is determined by the sleep() function, which takes a parameter for number of seconds. The sleep() function is provided by the unistd.h header that is included in the “empty” project by default. .
- Further down mainThread, we see thewhile(1) loop, which is our main loop in this simple example. Within this loop, we use another GPIO Driver API to toggle the pin. We do this once every second, which is determined by the sleep() function, which takes a parameter for number of seconds. The sleep() function is provided by the unistd.h header that is included in the “empty” project by default.
- Another file is “main_tirtos.c” or “main_freertos.c” (if using freertos).
- These files depending on the underlying kernel being used. However, since we are using POSIX in our TI Driver examples, these files are largely identical.
- Within this file, we configure the kernel, create & configure our thread(s) and set their priorities. For instance, we see that we use the POSIX API pthread_create() to create our mainThread, which is what we saw in our main empty.c source file.
The TI Drivers are divided into two levels: - TI Driver Interface: The TI Drivers interface contains the APIs, data structures and macros that the application uses. These interfaces are the same for all SimpleLink Devices.
- Low Level Implementations (LLI): The LLI Drivers contains the bulk of the driver implementation. It contains device/peripheral data structures and macros that are used when configuring the driver instance. The application does not call LLI APIs directly. The TI Driver Interface APIs call LLI APIs. This allows multiple types of driver implementations for a single peripheral.
- The TI Drivers use the Data Porting Layer (DPL) to communicate with the RTOS kernel. The application should not use DPL directly.
- “Board.h” file includes the enums and constants from the board.c file.
- Other two files are MSP_EXP432P401R.h and MSP_EXP432P401R.c, and these file are with the TI Drivers configuration.
- User only need to change these files as needed (especially for their custom boards).
- The board.c (
MSP_EXP432P401R.c) file contains all the TI Drivers configuration code. While the following will try to cover the main points, please refer to the TI Drivers Runtime APIs in the Documentation Overview. - The driver configuration is broken into four parts.
- LLI driver specific objects
- LLI driver specific hardware attributes
- TI Driver configuration structure
- TI Driver instance count
- LLI driver-specific objects: These structures provide the memory to the driver. This allows the drivers to avoid memory allocation. The application should not initialize or access these directly. They are used strictly in the driver modules. Here is an example
- LLI driver-specific hardware attributes: These structures provide the configuration to the driver. Typically the macros are from the source/ti/devices/specific device/driverlib and the LLI Driver’s header files. The drivers take this information and configure the peripherals as requested. The actual fields in this structure are driver-specific. Please refer to the LLI Driver documentation for more details about specific fields. The I2C example is
- Driver Configuration Structure: This is the structure that knits the configuration pieces together. The driver module is expecting this structure. You see that the above objects and hardware attributes are passed in here. The fxnTablePtr is used by the TI Driver interface to know what LLI driver to use. For a list of the names of the LLI drivers, please refer to the TI Drivers Runtime APIs. The I2C example is
- Driver Instance Count: This is the variable that tells the driver how many instances exist for a specific driver.
- To program our LaunchPad, we can click on the “Run” button in CCS Cloud. This will build our project & flash our hardware with the newly compiled image. Once programmed, your LaunchPad will automatically start to execute the code & the LaunchPad’s LED should start blinking!
Task 2: UART via TI DriversBased on the previous TI Driver empty project - Enable the include of the uart header
#include <ti/drivers/UART.h>- Enable uart init function
UART_init();- Add the following code after the UART_init();
char input; const char echoPrompt[] = "Echoing characters:\r\n"; UART_Handle uart; UART_Params uartParams; /* Create a UART with data processing off. */ UART_Params_init(&uartParams); uartParams.writeDataMode = UART_DATA_BINARY; uartParams.readDataMode = UART_DATA_BINARY; uartParams.readReturnMode = UART_RETURN_FULL; uartParams.readEcho = UART_ECHO_OFF; uartParams.baudRate = 115200; uart = UART_open(Board_UART0, &uartParams); if (uart == NULL) { /* UART_open() failed */ while (1); } UART_write(uart, echoPrompt, sizeof(echoPrompt));- Inside the while loop, add the following code
/* Loop forever echoing */ UART_read(uart, &input, 1); UART_write(uart, &input, 1);- Run the code, and connect the COM port
- After the code running, you will see the following output information.
- The final code is shown as follows
/* * ======== 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/SPI.h>#include <ti/drivers/UART.h>// #include <ti/drivers/Watchdog.h>#include <ti/display/Display.h>/* Board Header file */#include "Board.h"/* * ======== gpioButtonFxn0 ======== * Callback function for the GPIO interrupt on Board_GPIO_BUTTON0. */ void gpioButtonFxn0(uint_least8_t index) { GPIO_toggle(Board_GPIO_LED0); }/* * ======== mainThread ======== */void *mainThread(void *arg0){ /* 1 second delay */ uint32_t time = 1; /* Call driver init functions */ GPIO_init(); // I2C_init(); // SPI_init(); UART_init(); // Watchdog_init(); char input; const char echoPrompt[] = "Echoing characters:\r\n"; UART_Handle uart; UART_Params uartParams; /* Create a UART with data processing off. */ UART_Params_init(&uartParams); uartParams.writeDataMode = UART_DATA_BINARY; uartParams.readDataMode = UART_DATA_BINARY; uartParams.readReturnMode = UART_RETURN_FULL; uartParams.readEcho = UART_ECHO_OFF; uartParams.baudRate = 115200; uart = UART_open(Board_UART0, &uartParams); if (uart == NULL) { /* UART_open() failed */ while (1); } UART_write(uart, echoPrompt, sizeof(echoPrompt)); Display_Handle displayHandle; Display_Params displayParams; Display_Params_init(&displayParams); displayHandle = Display_open(Display_Type_UART, NULL);//print to UART uint32_t num = 1; //displayHandle = Display_open(Display_Type_HOST, NULL);//print to HOST /* install Button callback */ GPIO_setCallback(Board_GPIO_BUTTON0, gpioButtonFxn0); /* Enable interrupts */ GPIO_enableInt(Board_GPIO_BUTTON0);//board/h MSP_EXP432P401R_GPIO_S1 /* Configure the LED pin */ GPIO_setConfig(Board_GPIO_LED0, GPIO_CFG_OUT_STD | GPIO_CFG_OUT_LOW); /* Turn on user LED */ GPIO_write(Board_GPIO_LED0, Board_GPIO_LED_ON); while (1) { sleep(time); //GPIO_toggle(Board_GPIO_LED0); //printf("Hello world via printf\n"); //Display_printf(displayHandle, 1, 0, "Helloworld %d", num++);//1 line, 0 column /* Loop forever echoing */ UART_read(uart, &input, 1); UART_write(uart, &input, 1); }}
Task3 Add UART printA Display Driver is available in the TI Drivers, which offers a consistent API set for displaying data across multiple mediums (Serial UART, LCD, etc.). In this case, we will use the Display API to send our ADC readings back to a terminal window. - We can learn more about the Display Driver within the TI Driver API Guide. Click on Display.h to learn about how to use the driver to send data over a serial UART.
- Add the required header file to the top of our empty.c source file:
#include <ti/display/Display.h>- The TI Driver API Guides show how to initialize, open, use & close each of the TI Drivers.
- The basic steps are: Init, Open, Use, Close
- We see that we need to initialize & open the driver before we can use it.
- Add the following code after GPIO_init();
- We need to create a handle for our Display driver, and need to initialize & open the driver. Note that we need to use the “Display_Type_UART” parameter when we open the Display driver to use it for serial communication.
Display_Handle displayHandle; Display_Params displayParams; Display_Params_init(&displayParams); displayHandle = Display_open(Display_Type_UART, NULL);//print to UART uint32_t num = 1;- We can now use the Display_printf() API to send data over serial UART. Add the following code inside the while loop
Display_printf(displayHandle, 1, 0, "Helloworld %d", num++);//1 line, 0 column- After we run the code, we can open up a terminal by going to: CCS Cloud->Target > Connect COM Port
- Select COM4 and setup the baudrate=115200.
- In the Windows10 device manager, you can see the COM4 is “Application/User UART”. You may have different name for COM4.
- After you open the COM port, the output will be shown in the serial terminal in the CCS Cloud.
Task4 GPIO Interrupt via TI Drivers- Let’s add one callback function to the top of our project:
* ======== gpioButtonFxn0 ======== * Callback function for the GPIO interrupt on Board_GPIO_BUTTON0. */ void gpioButtonFxn0(uint_least8_t index) { GPIO_toggle(Board_GPIO_LED0); }- Once our callback functions are created, we need to install it using the GPIO_setCallback() function. We pass in 2 parameters, the IO that will trigger the interrupt & the callback function that we want to execute when the interrupt occurs.
- We need to enable the interrupt using the GPIO_enableInt() API.
/* install Button callback */ GPIO_setCallback(Board_GPIO_BUTTON0, gpioButtonFxn0); /* Enable interrupts */ GPIO_enableInt(Board_GPIO_BUTTON0);//board/h MSP_EXP432P401R_GPIO_S1- Comment the previous GPIO_toggle code //GPIO_toggle(Board_GPIO_LED0);
- When you run the code, you can toggle the LED via the button S1.
- The final code looks like this
/* * ======== 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/SPI.h>// #include <ti/drivers/UART.h>// #include <ti/drivers/Watchdog.h>#include <ti/display/Display.h>/* Board Header file */#include "Board.h"/* * ======== gpioButtonFxn0 ======== * Callback function for the GPIO interrupt on Board_GPIO_BUTTON0. */ void gpioButtonFxn0(uint_least8_t index) { GPIO_toggle(Board_GPIO_LED0); }/* * ======== mainThread ======== */void *mainThread(void *arg0){ /* 1 second delay */ uint32_t time = 1; /* Call driver init functions */ GPIO_init(); // I2C_init(); // SPI_init(); // UART_init(); // Watchdog_init(); Display_Handle displayHandle; Display_Params displayParams; Display_Params_init(&displayParams); displayHandle = Display_open(Display_Type_UART, NULL);//print to UART uint32_t num = 1; /* install Button callback */ GPIO_setCallback(Board_GPIO_BUTTON0, gpioButtonFxn0); /* Enable interrupts */ GPIO_enableInt(Board_GPIO_BUTTON0);//board/h MSP_EXP432P401R_GPIO_S1 /* Configure the LED pin */ GPIO_setConfig(Board_GPIO_LED0, GPIO_CFG_OUT_STD | GPIO_CFG_OUT_LOW); /* Turn on user LED */ GPIO_write(Board_GPIO_LED0, Board_GPIO_LED_ON); while (1) { sleep(time); //GPIO_toggle(Board_GPIO_LED0); //printf("Hello world via printf\n"); Display_printf(displayHandle, 1, 0, "Helloworld %d", num++);//1 line, 0 column }}Task5 FreeRTOSTI MSP432 Software Development Kits (SDKs) include the ability to use FreeRTOS as well as the TI-RTOS. As the FreeRTOSproject cannot be build in CCS Cloud, we need to use the CCS local desktop version (please refer previous lab for the CCS installation process). The following figure shows one DPL layer is used to wrap the FreeRTOS and TI-RTOS - Rather than make direct calls to kernel-specific API calls, the TI Driver code examples leverage POSIX & a Driver Porting Layer (DPL) to provide a consistent API layer on top of a desired kernel.
- The SimpleLink SDK is compliant with POSIX, an industry standard abstraction layer, which exposes common kernel-related functions through a common set of APIs.
- POSIX is an abstraction layer that offers source code compatibility between different RTOS Kernels, so regardless of which RTOS variant you select for this example, the application source code is identical & fully portable between TI-RTOS or FreeRTOS.
To utilize the FreeRTOS in our project, we need to import the FreeRTOS TI SDK example first. - Import the FreeRTOS “empty” project from the TI Resource Explorer under MSP432SDK->Example->Development Tools->MSP432 LaunchPad->TI Drivers->empty->FreeRTOS
- After import the empty project, there will be two projects automatically imported into the CCS workspace: one is the empty main project, another is the FreeRTOS kernel project (as shown in the following figure). Inside the imported kernel project, you see the FreeRTOSConfig.h file. The dpl and posix directories contain the files in the SimpleLink SDK to support those features. The freertosdirectory contains files in the FreeRTOS installation.
- The empty project points to the kernel project via the Project Properties → Build → Dependencies setting.
- Before we compile these projects, these two projects require that FREERTOS_INSTALL_DIR be defined.
- Download the latest FreeRTOS from www.freertos.org, the latest version is FreeRTOSv10.2.0 (download link)
- Extract the downloaded file into one local folder, e.g., “C:\TI\FreeRTOSv10.2.0”
- Setup the FreeRTOS path variable in both projects
- Right click the project, go to “properties”, then select the build tab, click define a new variable
- Define the FREERTOS_INSTALL_DIR as the local path of your freertos folder “C:\TI\FreeRTOSv10.2.0” (as shown in the following figure)
- After you setup the FREERTOS_INSTALL_DIR in both projects, you can compile and run the code without any error.
- Copy the code from Task4 (GPIO and Interrupt) to the FreeRTOS project, and run the code. Make sure you can run the code successfully to ensure FreeRTOS environment is correct.
Task6 SPIReplace the empty.c code with the following spiloopback.c code to check the SPI Master and slave usage. spiloopbackDownload
You can either download the previous code or copy-paste the following code /* * ======== spiloopback.c ======== */#include <string.h>#include <stdint.h>#include <stddef.h>/* POSIX Header files */#include <pthread.h>#include <unistd.h>/* Driver Header files */#include <ti/drivers/GPIO.h>#include <ti/drivers/SPI.h>#include <ti/display/Display.h>/* Example/Board Header files */#include "Board.h"#define SPI_MSG_LENGTH 50#define THREADSTACKSIZE 1024#define MAX_LOOP 5#define MASTER_MSG "Hello from master, msg#: "#define SLAVE_MSG "Hello from slave, msg#: "static pthread_barrier_t barrier;static Display_Handle display;unsigned char masterRxBuffer[SPI_MSG_LENGTH];unsigned char masterTxBuffer[SPI_MSG_LENGTH];unsigned char slaveRxBuffer[SPI_MSG_LENGTH];unsigned char slaveTxBuffer[SPI_MSG_LENGTH];/* ======== slaveThread ======== * This thread runs on a higher priority, since slave * has to be ready for master. Slave SPI sends a * message to master and also receives message from * master. */void *slaveThread(void *arg0){ SPI_Handle slaveSpi; SPI_Params slaveSpiParams; SPI_Transaction slaveTransaction; int i; bool transferOK; /* Initialize SPI handle with slave mode */ SPI_Params_init(&slaveSpiParams); slaveSpiParams.frameFormat = SPI_POL0_PHA1; slaveSpiParams.mode = SPI_SLAVE; slaveSpi = SPI_open(Board_SPI1, &slaveSpiParams); if (slaveSpi == NULL) { Display_printf(display, 0, 0, "Error initializing slave SPI\n"); while (1); } else { Display_printf(display, 0, 0, "Slave SPI initialized\n"); } /* Wait for master to open spi */ pthread_barrier_wait(&barrier); strncpy((char *)slaveTxBuffer, SLAVE_MSG, SPI_MSG_LENGTH); for (i = 0; i < MAX_LOOP; i++) { /* Initialize slave SPI transaction structure */ slaveTxBuffer[sizeof(SLAVE_MSG) - 1] = (i % 10) + '0'; slaveTransaction.count = SPI_MSG_LENGTH; slaveTransaction.txBuf = (void *)slaveTxBuffer; slaveTransaction.rxBuf = (void *)slaveRxBuffer; /* Initiate SPI transfer */ transferOK = SPI_transfer(slaveSpi, &slaveTransaction); if (transferOK) { /* Print contents of slave receive buffer */ Display_printf(display, 0, 0, "Slave: %s\n", slaveRxBuffer); } else { Display_printf(display, 0, 0, "Unsuccessful slave SPI transfer"); } } /* Deinitialize SPI */ SPI_close(slaveSpi); return (NULL);}/* * ======== masterThread ======== * This thread runs at a lower priority after the slave * task to ensure it is ready for a transaction. * Master SPI sends a message to slave and also * receives message from slave. */void *masterThread(void *arg0){ SPI_Handle masterSpi; SPI_Params masterSpiParams; SPI_Transaction masterTransaction; int i; bool transferOK; /* Initialize SPI handle as default master */ SPI_Params_init(&masterSpiParams); masterSpiParams.frameFormat = SPI_POL0_PHA1; masterSpi = SPI_open(Board_SPI0, &masterSpiParams); if (masterSpi == NULL) { Display_printf(display, 0, 0, "Error initializing master SPI\n"); while (1); } else { Display_printf(display, 0, 0, "Master SPI initialized\n"); } pthread_barrier_wait(&barrier); strncpy((char *)masterTxBuffer, MASTER_MSG, SPI_MSG_LENGTH); for (i = 0; i < MAX_LOOP; i++) { /* Initialize master SPI transaction structure */ masterTxBuffer[sizeof(MASTER_MSG) - 1] = (i % 10) + '0'; masterTransaction.count = SPI_MSG_LENGTH; masterTransaction.txBuf = (void *)masterTxBuffer; masterTransaction.rxBuf = (void *)masterRxBuffer; /* Turn on user LED, indicating a SPI transfer is in progress */ GPIO_write(Board_GPIO_LED1, Board_GPIO_LED_ON); /* Initiate SPI transfer */ transferOK = SPI_transfer(masterSpi, &masterTransaction); if (transferOK) { // Print contents of master receive buffer Display_printf(display, 0, 0, "Master: %s\n", masterRxBuffer); } else { Display_printf(display, 0, 0, "Unsuccessful master SPI transfer"); } /* Sleep a short time to allow the LED to flash */ usleep(500000); /* Turn off user LED, indicating the SPI transfer is done */ GPIO_write(Board_GPIO_LED1, Board_GPIO_LED_OFF); /* Sleep for a bit before starting the next SPI transfer */ sleep(3); } /* Deinitialize SPI */ SPI_close(masterSpi); Display_printf(display, 0, 0, "Done\n"); return (NULL);}/* * ======== mainThread ======== */void *mainThread(void *arg0){ pthread_t thread0, thread1; pthread_attr_t attrs; struct sched_param priParam; int retc; int detachState; /* Call driver init functions. */ Display_init(); GPIO_init(); SPI_init(); /* Configure the LED pins */ GPIO_setConfig(Board_GPIO_LED0, GPIO_CFG_OUT_STD | GPIO_CFG_OUT_LOW); GPIO_setConfig(Board_GPIO_LED1, GPIO_CFG_OUT_STD | GPIO_CFG_OUT_LOW); /* * Create a barrier to allow the master task to open its SPI * handle before the slave task issues a SPI transfer. Since the * pin muxing is done in the call to SPI_open(), we need to make * sure all pins are configured before the first transfer. This * will prevent the possibility of erroneous data being transfered. */ pthread_barrier_init(&barrier, NULL, 2); /* Open the display for output */ display = Display_open(Display_Type_UART, NULL); if (display == NULL) { /* Failed to open display driver */ while (1); } /* Turn on user LED */ GPIO_write(Board_GPIO_LED0, Board_GPIO_LED_ON); Display_printf(display, 0, 0, "Starting the SPI loop-back example\n"); Display_printf(display, 0, 0, "This example requires external wires to be " "connected to the header pins. Please see the Board.html for details.\n"); /* Create application threads */ pthread_attr_init(&attrs); detachState = PTHREAD_CREATE_DETACHED; /* Set priority and stack size attributes */ retc = pthread_attr_setdetachstate(&attrs, detachState); if (retc != 0) { /* pthread_attr_setdetachstate() failed */ while (1); } retc |= pthread_attr_setstacksize(&attrs, THREADSTACKSIZE); if (retc != 0) { /* pthread_attr_setstacksize() failed */ while (1); } /* Create master thread */ priParam.sched_priority = 1; pthread_attr_setschedparam(&attrs, &priParam); retc = pthread_create(&thread0, &attrs, masterThread, NULL); if (retc != 0) { /* pthread_create() failed */ while (1); } priParam.sched_priority = 2;//higher priority, the Slave SPI must be ready before the Master SPI starts the first transaction pthread_attr_setschedparam(&attrs, &priParam); /* Create slave thread */ retc = pthread_create(&thread1, &attrs, slaveThread, (void* )0); if (retc != 0) { /* pthread_create() failed */ while (1); } return (NULL);}Before running the code, we need to connect the SPI Master and SPI slave. In this example, we utilized one single board for testing, i.e., connect two SPI interfaces together via external jumper cables. - The SPI master utilize the Board_SPI0 (P1.5 – CLK, P1.6 – MOSI, P1.7 – MISO), the SPI slave utilize Board_SPI1 (P3.5 – CLK, P3.6 – MOSI, P3.7 – MISO).
- Using the jumper cable to connect the pin P1.5 to P3.5, P1.6 to P3.6, P1.7 to P3.7.
- After the physical connection is done, you can run or debug the code.
This application uses two threads: - masterThread – creates the Master SPI message and initiates the first SPI transfer. When the transaction completes, the Master sleeps for 3 seconds, before sending the next transaction.
- slaveThread – creates the Slave SPI message and waits for the Master to start the SPI transaction. This thread runs at a higher priority than the masterThread, since the Slave SPI must be ready before the Master SPI starts the first transaction.
To synchronize the master and slave thread, we utilize the standard POSIX function pthread_barrier_wait to sync the master and slave. - The pthread_barrier_wait() function shall synchronize participating threads at the barrier referenced by barrier.
- The calling thread shall block until the required number of threads have called pthread_barrier_wait() specifying the barrier.
- When slave and master all arrived at the barrier, they can move forward together.
You can try the SPIloopback via TI-RTOS as well as Free-RTOS.
|