FreeRTOS tasks and queues

In my previous homebrew projects I did not use any operating system in the embedded processors. Software was programmed on a bare-metal hardware. In my Talking Clock project I created a simple cooperative event-processing abstraction layer, but it was very limited.

In PIP-Watch there are multiple independent tasks that should execute concurrently and exchange information at specific points. These are: Bluetooth modem task, Display drawing task, and Battery monitoring task. The Bluetooth task sets-up the bluetooth modem, listens for incoming connections, and communicates with a mobile phone. The Battery task measures battery voltage at periodic instants (e.g. once per couple of seconds) using a built-in A/D converter and computes the battery charge level. The Display drawing task receives information from other tasks and from a real-time clock interrupt and redraws the screen when needed. At the least the screen is redrawn once per minute to update a clock face.

PIP-Watch software tasks
PIP-Watch software tasks

I used the open-source FreeRTOS operating systems to implement multitasking. Modern ARM processors include a dedicated SysTick timer for generating periodic interrupts for an operating system use. Therefore a basic operating system such as FreeRTOS may rely solely on ARM-standard hardware and it does not need any vendor-specific hooks, thus simplifying porting effort. I was pleasantly surprised that getting the system up and running on Cortex-M3 was very easy and took me only about one evening. Beside sources I needed only a linker script and a startup assembly code, both of which could be found in FreeRTOS example projects.

PIP-Watch software layers
PIP-Watch software layers

Queues and Interrupts

Besides switching tasks at periodic events (preemptive multitasking) the other basic function of an operating system is supporting communication channels between tasks. One such basic primitive is a queue. Let’s see how a queue can be used to synchronize an interrupt handler and a task:

The Battery task monitors the battery charge by periodiacally measuring its voltage using an integrrated AD converter in microcontroller. One AD conversion in hardware takes some time to finish. One possibility is to let the CPU wait in a polling busy loop until the ADC is done. But it is quite inefficient approach because energy is wasted and other taks that could run are needlessly delayed. A better approach is to start the AD conversion, yield the CPU to some other task, then come back when the end of AD conversion is signalled by an interrupt.

Let’s look at some actual code. This is an interrupt handler that is called when AD conversion is done:

void ADC_IRQHandler(void)
  long xHigherPriorityTaskWoken = pdFALSE;
  if (adcValueQueue != NULL && ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == SET) {
    int val = ADC_GetConversionValue(ADC1);xQueueSendFromISR(adcValueQueue, &val, &xHigherPriorityTaskWoken);
  /* clear ADC1 irq pending bit */
  ADC_ClearITPendingBit(ADC1, ADC_IT_EOC); 
  /* signal end-of-irq and possible reschedule point */
  portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );

The function ADC_GetConversionValue() retrieves the A/D converted data value from a hardware register. The crucial function is xQueueSendFromISR(). It has three parameters. The first is a pointer  (adcValueQueue) to a software queue created in FreeRTOS. The second parameter is a pointer to value that shall be pushed at the end of the queue (&val). The third parameter  (&xHigherPriorityTaskWoken) is a pointer to variable; the variable is set if we need to reschedule a task after the interrupt service routine (ISR) will have ended. It will be accomplished by portEND_SWITCHING_ISR().

So when an AD conversion ends the converted value is pushed at the end of the queue adcValueQueue. Let’s look at the battery-task function:

void BatteryTask(void *pvParameters)
 /* queue of ADC data values received in irq handler */
 adcValueQueue = xQueueCreate(4, sizeof(int));

 while (1) {
 /* measure internal Vref=1.20V */
 ADC_RegularChannelConfig(ADC1, ADC_Channel_17, 1, ADC_SampleTime_71Cycles5); // Vref
 /* wait till the conversion ends */
 int ad_vref = 0;
 xQueueReceive(adcValueQueue, &ad_vref, portMAX_DELAY);

 /* measure battery voltage on channel 8 */
 ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 1, ADC_SampleTime_71Cycles5);
 /* wait till the conversion ends */
 int ad_vbat = 0;
 xQueueReceive(adcValueQueue, &ad_vbat, portMAX_DELAY);

 /* compensate for resistor divider by 2 */
 ad_vbat *= 2;

 /* digital value of ad_vref corresponds to 1.20 Volts */
 /* ad_vref/1.20 = ad_vbat/vbat */
 /* vbat = ad_vbat/ad_vref * 1.20 */
 vbat_measured = ad_vbat * 1200 / ad_vref;

 /* minimum voltage=3.50V = 0%, maximum voltage=4.20V = 100% */
 vbat_percent = (vbat_measured - VBAT_0_PERCENT) * 100 / (VBAT_100_PERCENT - VBAT_0_PERCENT);

 /* wait 4 seconds */
 vTaskDelay( ( TickType_t ) 4000 / portTICK_PERIOD_MS );

Battery voltage is measured in two steps. First we measure an internal voltage reference on channel 17 that we know from a datasheet is 1.20V. Then we measure the battery voltage that is present on ADC channel 8. The real voltage in milli-volts is computed from the two measured values. Each of the two AD conversions (measurement) is started by the function ADC_Cmd(ADC1, ENABLE). Instead of waiting for the end of conversion in a busy loop, we read a value from the queue using the function xQueueReceive(). Normally as the conversion is not done yet the function will block, i.e. it will put the battery task to sleep and possibly switch to another FreeRTOS task. When the conversion ends the measured value will be pushed to the queue in the ISR, causing the battery task to eventually unblock and receive the value.

This is the current layout of various information on the display:


Leave a Reply

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