STM32 and FreeRTOS USB communication

In the previous post we managed to set up a connection between the board and a PC through USB. The board send the acceleration’s sensor data at a regular interval, and on the PC side this data is plot using a simple program developed with Qt framework.

Now we want to expand it in order to have a code base that uses FreeRTOS and is capable to send and receive data in a more robust way.

board-to-pc-qt_and_gui

FreeRTOS

Configuration

FreeRTOSConfig.h

#define INCLUDE_vTaskPrioritySet		0
#define INCLUDE_uxTaskPriorityGet		0
#define INCLUDE_vTaskDelete				1
#define INCLUDE_vTaskCleanUpResources	0
#define INCLUDE_vTaskSuspend			1
#define INCLUDE_vTaskDelayUntil			1
#define INCLUDE_vTaskDelay				1

In a previous post, about debugging FreeRTOS, the required interrupt handlers were installed overriding the default ones under g_pfnVectors in the startup_stm32f30x.S file.

/* Definitions that map the FreeRTOS port interrupt handlers to their CMSIS
standard names. */
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
#define vSoftwareInterruptHandler WWDG_IRQHandler

This time we use a different approach; the native FreeRTOS handlers are replaced with the CMSIS ones through defines, and prepending (if it is the case) the already defined handlers with the attribute weak.

__attribute__((weak)) void SVC_Handler(void)
{
}

Initialization

The main simply setup the clock, initialize the remote interface and eventually starts the scheduler.

int main(void)
{  
	/* SysTick end of count event each 10ms */
	RCC_GetClocksFreq( &RCC_Clocks );
	SysTick_Config( RCC_Clocks.HCLK_Frequency / 100 );

	prvInitLED();

	vRemoteInterfaceInit();

	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was insufficient heap to start the scheduler. */
	for( ;; );
	return 0;
}

The remote interface main entry simply create a task that initialize two queues (incoming and outgoing data) and the USB connection. The task waits on the incoming data queue, staying idle until we receive external data.

The pucMessage is defined as static to avoid to use extra stack space.

void vRemoteInterfaceInit( void )
{
	xTaskCreate( prvParseIncomingDataTask, ( signed char* ) "ParseIn", configMINIMAL_STACK_SIZE * 2, NULL, parseIncomingData_TASK_PRIORITY, NULL );
}

static void prvParseIncomingDataTask( void *pvParameter )
{
	xTaskHandle xSampleTask = NULL;
	static uint8_t pucMessage[ RECEIVER_BUFFER_SIZE ];


	xInMessagesQueue = xQueueCreate( 10, sizeof( pucReceiveBuffer ) );
	/* We want this queue to be viewable in a RTOS kernel aware debugger, so register it. */
	vQueueAddToRegistry( xInMessagesQueue, (signed char*) "InMessages" );


	xOutMessagesQueue = xQueueCreate( 10, sizeof( portCHAR ) );
	/* We want this queue to be viewable in a RTOS kernel aware debugger, so register it. */
	vQueueAddToRegistry( xOutMessagesQueue, (signed char*) "OutMessages" );

	/* Configure the USB */
	vInitUSB();

	vTaskDelay( DELAY );

	for(;;)
	{
		/* wait forever for incoming messages */
		if( xQueueReceive( xInMessagesQueue, pucMessage, portMAX_DELAY ) == pdPASS )
		{
                      [...]
		}
	}
}

A second task is defined, but not active at start up. Its responsibility is to sample the sensors data at regular intervals and to send the sampled data to the PC.

static void prvSampleDataTask( void* pvParameters )
{
	portTickType xLastWakeTime;

	/* Initialise the xLastWakeTime variable with the current time */
	xLastWakeTime = xTaskGetTickCount();

	static float pfMagBuffer[ 3 ] = { 0.0f };
	static float pfAccBuffer[ 3 ] = { 0.0f };
	static float pfGyroBuffer[ 3 ] = { 0.0f };

	vSensorsCompassConfig();
	vSensorsGyroConfig();

	STM_EVAL_LEDOn( LED9 );

	for(;;)
	{
		vTaskDelayUntil(&xLastWakeTime, DATA_SAMPLE_DELAY);

		vSensorsCompassReadAcc(pfAccBuffer);
		g_sBoardStatus.lAccelerationX = pfAccBuffer[ 0 ];
		g_sBoardStatus.lAccelerationY = pfAccBuffer[ 1 ];
		g_sBoardStatus.lAccelerationZ = pfAccBuffer[ 2 ];

		vSensorsCompassReadMag(pfMagBuffer);
		g_sBoardStatus.lMagneticFieldX = pfMagBuffer[ 0 ];
		g_sBoardStatus.lMagneticFieldY = pfMagBuffer[ 1 ];
		g_sBoardStatus.lMagneticFieldZ = pfMagBuffer[ 2 ];

		vSensorsGyroReadAngRate(pfGyroBuffer);
		g_sBoardStatus.lGyroAngRateX = pfGyroBuffer[ 0 ];
		g_sBoardStatus.lGyroAngRateY = pfGyroBuffer[ 1 ];
		g_sBoardStatus.lGyroAngRateZ = pfGyroBuffer[ 2 ];


		vUsbSerialInterfaceSendRealTimeData();
	}
}

This task is created and deleted on request, depending if the remote PC wants to see the sampled data or not.

The USB end point 3 (receive data) is responsible to detect a correct frame, and to dispatch it to the queue. This operation will woke up the parser task. If, for example, we receive a start command this task will create a sampling task.

void EP3_OUT_Callback(void)
{
        [...]

	if ( goodPacket )
	{
		/* got new data packet -> give semaphore or insert data in queue */

		xQueueSendToBackFromISR( xInMessagesQueue, pucReceiveBuffer, &xHigherPriorityTaskWoken );
	}
}

Communication

The protocol is the same that has been used in the rdk-stepper motor project from Texas Instruments (ex Luminary). They have developed a general GUI and protocol for a different set of development boards, and the code is pretty well written and generic. Unfortunately it is not maintained anymore and it won’t be there for long. It seems that Texas Instruments bets everything on their new Tiva series.

The new line of micro controllers from Texas looks promising, but they only offer LaunchPad Evaluation Kits and not a complete application demo like the rdks.

Parsing data: upon receiving data in the EP3_OUT_Callback we parse it in a dedicated task that keeps waiting for new commands.
Receiving a START or STOP command we create, or delete, a task accordingly. It seems a crude approach, but in the end we won’t stream this data all the time in a real application, so it makes sense to “remove” this task from the system and “add” it only when requested. Any new command can be simply added as needed, like COMMAND_SET_PWM_VALUES.

		/* wait forever for incoming messages */
		if( xQueueReceive( xInMessagesQueue, pucMessage, portMAX_DELAY ) == pdPASS )
		{
			STM_EVAL_LEDOn( LED9 );

			switch ( pucMessage[ 2 ] )
			{
			case COMMAND_START:
				if ( xSampleTask == NULL )
					xTaskCreate( prvSampleDataTask, ( signed char* ) "SampleData", configMINIMAL_STACK_SIZE, NULL, parseIncomingData_TASK_PRIORITY, &xSampleTask );

				break;

			case COMMAND_STOP:
				if ( xSampleTask )
				{
					vTaskDelete( xSampleTask );
					xSampleTask = NULL;
					STM_EVAL_LEDOff( LED10 );
				}

				break;

			case COMMAND_SET_PWM_VALUES:

				vMotorControlSetPwmValues( prvGetInt32FromDataArray( &(pucMessage[ 3 ]) ),
						prvGetInt32FromDataArray( &(pucMessage[ 7 ]) ) );


				break;

			case COMMAND_UNKNOWN:
			default:
				break;
			}
		}

The sampling task fill a structure with the current status of the sensors.

	for(;;)
	{
		vTaskDelayUntil(&xLastWakeTime, DATA_SAMPLE_DELAY);

		vSensorsCompassReadAcc(pfAccBuffer);
		g_sBoardStatus.lAccelerationX = pfAccBuffer[ 0 ];
		g_sBoardStatus.lAccelerationY = pfAccBuffer[ 1 ];
		g_sBoardStatus.lAccelerationZ = pfAccBuffer[ 2 ];

		vSensorsCompassReadMag(pfMagBuffer);
		g_sBoardStatus.lMagneticFieldX = pfMagBuffer[ 0 ];
		g_sBoardStatus.lMagneticFieldY = pfMagBuffer[ 1 ];
		g_sBoardStatus.lMagneticFieldZ = pfMagBuffer[ 2 ];

		vSensorsGyroReadAngRate(pfGyroBuffer);
		g_sBoardStatus.lGyroAngRateX = pfGyroBuffer[ 0 ];
		g_sBoardStatus.lGyroAngRateY = pfGyroBuffer[ 1 ];
		g_sBoardStatus.lGyroAngRateZ = pfGyroBuffer[ 2 ];


		vUsbSerialInterfaceSendRealTimeData();
	}

The board status is a structure containing the data. Any new data will be added here, like lMainVoltage_mV

typedef struct
{
	long lAccelerationX;
	long lAccelerationY;
	long lAccelerationZ;

	long lMagneticFieldX;
	long lMagneticFieldY;
	long lMagneticFieldZ;

	long lGyroAngRateX;
	long lGyroAngRateY;
	long lGyroAngRateZ;

	/*long lMainVoltage_mV;*/
} tBoardStatus;

tBoardStatus g_sBoardStatus;

Protocol

The g_sRealTimeData is an array of structures that define the data that we want to sample (i.e. id, size and pointer to the value). In this way adding new values is straightforward and won’t require any modification to the communication protocol function.

typedef struct
{
	/* The ID of this real-time data item */
	unsigned char ucID;

	/* The size of this real-time data item in bytes */
	unsigned char ucSize;

	/* A pointer to the value of this real-time data item */
	unsigned char *pucValue;
} tUIRealTimeData;

const tUIRealTimeData g_sRealTimeData[];

const tUIRealTimeData g_sRealTimeData[] =
{
		{
				DATA_ACCELERATION_X,
				4,
				(unsigned char *)&g_sBoardStatus.lAccelerationX
		},

		{
				DATA_ACCELERATION_Y,
				4,
				(unsigned char *)&g_sBoardStatus.lAccelerationY
		},

		{
				DATA_ACCELERATION_Z,
				4,
				(unsigned char *)&g_sBoardStatus.lAccelerationZ
		},

		{
				DATA_MAGNETIC_FIELD_X,
				4,
				(unsigned char *)&g_sBoardStatus.lMagneticFieldX
		},

		{
				DATA_MAGNETIC_FIELD_Y,
				4,
				(unsigned char *)&g_sBoardStatus.lMagneticFieldY
		},

		{
				DATA_MAGNETIC_FIELD_Z,
				4,
				(unsigned char *)&g_sBoardStatus.lMagneticFieldZ
		},

		{
				DATA_GYRO_ANGULAR_RATE_X,
				4,
				(unsigned char *)&g_sBoardStatus.lGyroAngRateX
		},

		{
				DATA_GYRO_ANGULAR_RATE_Y,
				4,
				(unsigned char *)&g_sBoardStatus.lGyroAngRateY
		},

		{
				DATA_GYRO_ANGULAR_RATE_Z,
				4,
				(unsigned char *)&g_sBoardStatus.lAccelerationZ
		},
};

const unsigned long g_ulNumRealTimeData = ( sizeof( g_sRealTimeData ) / sizeof( g_sRealTimeData[ 0 ] ) );

The sampled data array is scanned and an array of bytes to be sent is filled. Header, data length and check sum are added.

	/* Loop through the available real-time data items */
	for ( ulItem = 0, ulPos = 2; ulItem < g_ulNumRealTimeData; ulItem++ )
	{

		/* local copy */
		*( (unsigned long*) pucValue ) = *( (unsigned long*) g_sRealTimeData[ ulItem ].pucValue );

		/* Copy the value of this real-time data item, byte by byte, to the packet */
		for ( ulCount = 0; ulCount < g_sRealTimeData[ ulItem ].ucSize; ulCount++ )
		{
			g_pucSerialData[ ulPos++ ] = pucValue[ ulCount ];
		}
	}

	/* Put the header and length on the real-time data packet */
	g_pucSerialData[ 0 ] = TAG_DATA;
	g_pucSerialData[ 1 ] = ulPos + 1;

    /* Compute the checksum for this packet and put it at the end */
    for ( ulIdx = 0, ulSum = 0, ulLength = g_pucSerialData[ 1 ]; ulIdx < ( ulLength - 1 ); ulIdx++)
    {
        ulSum -= g_pucSerialData[ulIdx];
    }
    g_pucSerialData[ ulLength - 1 ] = ulSum;

	prvCdcSendData ( (unsigned char*) g_pucSerialData, g_pucSerialData[ 1 ] );

Here is an example session:

15:54:23.579 INFO: ttyACM0 115200 8 0 1 0
15:54:24.618 DEBUG: writing: ff0440bd
15:54:24.733 DEBUG: reading: fd27f8ffffff11000000ec03000004ffffff5b0000003bfeffff0000000002000000ec03000063
15:54:24.833 DEBUG: reading: fd27f3ffffff10000000ed03000004ffffff5b00000040feffff0000000002000000ed03000062
15:54:24.933 DEBUG: reading: fd27f8ffffff14000000ef03000009ffffff5f0000003bfeffff0000000002000000ef03000051
15:54:25.033 DEBUG: reading: fd27f2ffffff15000000ee03000004ffffff5b0000003bfeffff0000000002000000ee03000061
15:54:25.133 DEBUG: reading: fd27f5ffffff15000000ea03000009ffffff5b00000036feffff0000000002000000ea03000066
15:54:25.233 DEBUG: reading: fd27faffffff11000000ec03000004ffffff5f00000036feffff0000000001000000ec03000063
15:54:25.333 DEBUG: reading: fd27f6ffffff14000000e903000004ffffff5b0000003bfeffff0000000002000000e903000068
[...]
15:56:05.027 DEBUG: writing: ff0441bc

Here we can see the data plotted on a PC. The plots are generated in Qt, using qcustomplot.
board-to-pc-gui

6 thoughts on “STM32 and FreeRTOS USB communication

  1. Hello,
    this looks really amazing. Thank you for this post! To be honest, it would be really nice to have possibility to have into whole source code. Dont you have it somewhere on GIT etc.?

    Thank you.

    1. Hi, thanks, my code is a little bit a mess and I don’t have time to clean it up right now..

      But basically it’s a patchwork of a previous post (with source code), a very simple and basic FreeRTOS project and for the data transfer protocol have a look at TI’s rdk stepper source code, which is pretty good.

      First you need to have a working usb-cdc communication (try with my source code), it is the most difficult part..
      After that starting with FreeRTOS and a simple task is very easy, and you can find examples on the net.
      The sampling of the data comes from STM demo.
      Implementing a code similar to TI’s demo is not strictly necessary, but is a good way of programming in C and you’ll learn a lot!

      Good luck

      pezzino

  2. Can you please provide your Qt source code? I would like to make a similar program therefore your source code would help me significantly! Thanks.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.