Chapter 12. Embedded Linux Examples

Ty Webb: Don’t be obsessed with your desires, Danny. The Zen philosopher, Basho, once wrote: “A flute with no holes is not a flute...and a doughnut with no hole is a Danish.” He was a funny guy.

the movie Caddyshack

I n this chapter, we will explore some examples using embedded Linux. The examples in this chapter are similar to (or in some cases the same as) the eCos examples we covered in the previous chapter. The idea here is to get an introduction to embedded Linux and understand some basic operating system functionality.

Introduction

The embedded Linux examples demonstrate certain basic APIs for various operations. Additional APIs exist that offer other functionality. You should research the additional APIs on your own to determine whether there are other, better ways to perform the operations necessary for your particular embedded system.

One aspect of Linux you need to be familiar with is its thread model. The Linux API conforms to the key POSIX standard in the space, POSIX 1003.1c, commonly called the pthreads standard. POSIX leaves many of the implementation details up to the operating system implementer. A good source of information on pthreads is the book Pthreads Programming, by Bradford Nichols, Dick Buttlar, and Jacqueline Farrell (O’Reilly).

The version of embedded Linux used on the Arcom board is a standard kernel tree (version 2.6) with additional ARM and XScale support from the ARM Linux Project at http://www.arm.linux.org.uk.

A plethora of books about Linux and embedded Linux are available. Some good resources include Understanding the Linux Kernel, by Daniel P. Bovet and Marco Cesati (O’Reilly), Linux Device Drivers, by Alessandro Rubini and Jonathan Corbet (O’Reilly), and Building Embedded Linux Systems, by Karim Yaghmour (O’Reilly).

The instructions for configuring the embedded Linux build environment and building the example Linux applications are detailed in Appendix E. Additional information about using embedded Linux on the Arcom board can be found in the Arcom Embedded Linux Technical Manual and the VIPER-Lite Technical Manual.

Tip

In order to keep the examples in this chapter shorter and easier to read, we don’t check the return values from function calls. In general, it is a good idea to validate all return codes. This provides feedback about potential problems and allows you, as the developer, to make decisions in the software based on failed calls. Basically, it makes your code more robust and, hopefully, less buggy.

Accessing Hardware in Linux

Before proceeding with the Linux examples, it is important to have a basic understanding of hardware access in Linux. Linux, like most desktop operating systems, partitions its memory management into user space and kernel space.

User space is where applications run (including the Linux examples that follow). User space applications are allowed to access the hardware only through kernel-supported functions. Kernel space is typically where device drivers exist. This allows the device drivers to have direct access to the hardware.

The Linux examples use a function called mmap in order to access a particular address range. The mmap function asks the kernel to provide access to a physical address range contained in the hardware. For details on how we use mmap, refer to the book’s source code.

Task Mechanics

For the first Linux example, we reuse the Blinking LED program. This example shows how to create a task to toggle the LED at a constant rate.

The task blinkLedTask delays for 500 ms and then toggles the green LED. The task uses an infinite loop that calls the function usleep to suspend for the proper amount of time. The usleep function is passed the number of microseconds to suspend, which in this case is 50,000 ms.

After the delay function call, the blinkLedTask is blocked and put in the waiting state. The task is put in the ready state once the time has elapsed and then has been run by the scheduler. After the delay, the ledToggle function toggles the green LED.

#include <unistd.h>
#include "led.h"

/**********************************************************************
 *
 * Function:    blinkLedTask
 *
 * Description: This task handles toggling the green LED at a
 *              constant interval.
 *
 * Notes:       
 *
 * Returns:     None.
 *
 **********************************************************************/ 
void blinkLedTask(void *param)
{
    while (1)
    {
        /* Delay for 500 milliseconds. */
        usleep(50000);

        ledToggle();
    }
}

The main function’s job in this example is to create the LED task. The task is created by calling the function pthread_create. For this example, the default task attributes are used, and no parameters are passed to the task blinkLedTask.

The function pthread_join is used to suspend the main function until the blinkLedTask task terminates. In this case, the blinkLedTask task runs forever, so neither function exits.

#include <pthread.h>

/* Declare the task variables. */
pthread_t ledTaskObj;

/**********************************************************************
 *
 * Function:    main
 *
 * Description: Main routine for Linux Blinking LED program. This
 *              function creates the LED task.
 * 
 * Notes:       
 *
 * Returns:     0.
 *
 **********************************************************************/
int main(void)
{
    /* Configure the green LED control pin. */
    ledInit();

    /* Create the LED task using the default task attributes. Do not
     * pass in any parameters to the task. */
    pthread_create(&ledTaskObj, NULL, (void *)blinkLedTask, NULL);

    /* Allow the LED task to run. */
    pthread_join(ledTaskObj, NULL);

    return 0;
}

Additional task operations are supported in Linux. These operations include terminating tasks, modifying task attributes, yielding to other tasks in the system, and suspending/resuming tasks.

Mutex Task Synchronization

In the Linux mutex example (just as in the eCos example), two tasks share a common variable called gSharedVariable. One task increments the global variable at a set interval, and the other task decrements the variable at a set interval. The mutex protects the shared variable.

The function main starts by creating the mutex, sharedVariableMutex, by calling the function pthread_mutex_init. Because the default attributes are used in the mutex creation, NULL is passed in as the second parameter. In Linux, mutexes have attributes that you can set using the second parameter, but we won’t use them in this book, so we’ll just pass NULL.

Lastly, the two tasks incrementTask and decrementTask are created. It is important to create the mutex before creating the tasks that use it, because otherwise the tasks could crash the program.

#include <pthread.h>

pthread_mutex_t sharedVariableMutex;
int32_t gSharedVariable = 0;

/**********************************************************************
 *
 * Function:    main
 *
 * Description: Main routine for the Linux mutex example. This
 *              function creates the mutex and then the increment and
 *              decrement tasks.
 * 
 * Notes:       
 *
 * Returns:     0. 
 *
 **********************************************************************/
int main(void)
{
    /* Create the mutex for accessing the shared variable using the
     * default attributes. */
    pthread_mutex_init(&sharedVariableMutex, NULL);

    /* Create the increment and decrement tasks using the default task
     * attributes. Do not pass in any parameters to the tasks. */
    pthread_create(&incrementTaskObj, NULL, (void *)incrementTask, NULL);
    pthread_create(&decrementTaskObj, NULL, (void *)decrementTask, NULL);

    /* Allow the tasks to run. */
    pthread_join(incrementTaskObj, NULL);
    pthread_join(decrementTaskObj, NULL);

    return 0;
}

The task incrementTask, shown following, includes an infinite loop that starts by delaying for three seconds by calling sleep, which suspends the task for a specified number of seconds.

Once the delay time elapses, the increment task resumes from where it left off. The task then calls pthread_mutex_lock and passes in the sharedVariableMutex in order to take the mutex and access the shared variable. If the mutex is available, it is locked, and the increment task proceeds to increment gSharedVariable. If the mutex is not available, the increment task blocks until it can acquire the mutex.

After incrementing the shared variable and outputting a message, the mutex is released with a call to pthread_mutex_unlock. The mutex unlock function never blocks.

#include <stdio.h>
#include <unistd.h>

/**********************************************************************
 *
 * Function:    incrementTask
 *
 * Description: This task increments a shared variable.
 *
 * Notes:       
 *
 * Returns:     None.
 *
 **********************************************************************/
void incrementTask(void *param)
{
    while (1)
    {
        /* Delay for 3 seconds. */
        sleep(3);

        /* Wait for the mutex before accessing the GPIO registers. */
        pthread_mutex_lock(&sharedVariableMutex);

        gSharedVariable++;

        printf("Increment Task: shared variable value is %d\n",
               gSharedVariable);

        /* Release the mutex for other task to use. */
        pthread_mutex_unlock(&sharedVariableMutex);
    }
}

The task decrementTask is similar to the increment task. In its infinite loop, the task first suspends for seven seconds, then waits to acquire the sharedVariableMutex. After taking the mutex, the task decrements the value of gSharedVariable, outputs a message, and then releases the mutex, as shown here:

/**********************************************************************
 *
 * Function:    decrementTask
 *
 * Description: This task decrements a shared variable.
 *
 * Notes:       
 *
 * Returns:     None.
 *
 **********************************************************************/ 
void decrementTask(void *param)
{
    while (1)
    {
        /* Delay for 7 seconds. */
        sleep(7);

        /* Wait for the mutex to become available. */
        pthread_mutex_lock(&sharedVariableMutex);

        gSharedVariable--;

        printf("Decrement Task: shared variable value is %d\n",
               gSharedVariable);

        /* Release the mutex. */
        pthread_mutex_unlock(&sharedVariableMutex);
    }
}

The Linux pthread API supports additional mutex functions that provide other functionality. For example, the function pthread_mutex_trylock can be used to attempt to get a mutex. If the mutex is available, the task acquires the mutex; if the mutex is not available, the task can proceed with other work without waiting for the mutex to be freed up.

Semaphore Task Synchronization

The Linux semaphore example is similar to a push button light switch, using an LED for the light, as was the case in the eCos semaphore example. There are two tasks in this example: a producer and a consumer. The producer task, producerTask, monitors the button labeled SW0 on the Arcom board’s add-on module. Figure 11-1 in Chapter 11 shows the button used in this example.

When the SW0 button is pressed, the producer task signals the consumer task using a semaphore. The consumer, consumerTask, waits for the semaphore signal from the producer task; once received, the consumer task outputs a message and toggles the green LED.

The main function first initializes the LED by calling ledInit. Next, the semaphore is initialized with a call to sem_init. The initial value of the semaphore, semButton, is set to zero by the last parameter so that the consumer task that is waiting does not execute until the semaphore is signaled by the producer task. The second parameter notifies the operating system that this semaphore may be used by this process only. Lastly, the two tasks are created and a message is output signifying the start of the program.

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include "led.h"

sem_t semButton;

/**********************************************************************
 *
 * Function:    main
 *
 * Description: Main routine for the Linux semaphore example. This
 *              function creates the semaphore and then the increment
 *              and decrement tasks.
 * 
 * Notes:       
 *
 * Returns:     0.
 *
 **********************************************************************/
int main(void)
{
    /* Configure the green LED control pin. */
    ledInit();

    /* Create the semaphore for this process only and with an initial
     * value of zero. */
    sem_init(&semButton, 0, 0);

    /* Create the producer and consumer tasks using the default task
     * attributes. Do not pass in any parameters to the tasks. */
    pthread_create(&producerTaskObj, NULL, (void *)producerTask, NULL);
    pthread_create(&consumerTaskObj, NULL, (void *)consumerTask, NULL);

    printf("Linux semaphore example - press button SW0.\n");

    /* Allow the tasks to run. */
    pthread_join(producerTaskObj, NULL);
    pthread_join(consumerTaskObj, NULL);

    return 0;
}

The producerTask function shown next contains an infinite loop that first delays for 10 ms and then checks to see whether the SW0 button has been pressed, by calling the function buttonDebounce. For additional information about selecting sampling intervals and button debouncing, read the sidebar “Switch Debouncing” in Chapter 11.

When the SW0 button is pressed, the producerTask function signals the semButton semaphore by calling sem_post. This increments the semaphore value and wakes the consumer task. The producer task then returns to monitoring the SW0 button.

#include <unistd.h>
#include "button.h"

/**********************************************************************
 *
 * Function:    producerTask
 *
 * Description: This task monitors button SW0. Once pressed, the button
 *              is debounced and the semaphore is signaled, waking the
 *              waiting consumer task.
 *
 * Notes:       
 *
 * Returns:     None.
 *
 **********************************************************************/ 
void producerTask(void *param)
{
    int buttonOn;

    while (1)
    {
        /* Delay for 10 milliseconds. */
        usleep(10000);

        /* Check whether the SW0 button has been pressed. */
        buttonOn = buttonDebounce();

        /* If button SW0 was pressed, signal the consumer task. */
        if (buttonOn)
            sem_post(&semButton);
    }
}

The following consumerTask function contains an infinite loop that waits for the semaphore to be signaled by calling sem_wait. The wait function blocks the task if the value of the semaphore is 0.

Once the semaphore signal is received, the consumer task outputs a message and toggles the green LED by calling ledToggle. After toggling the LED, the consumer task returns to waiting for another semaphore signal.

/**********************************************************************
 *
 * Function:    consumerTask
 *
 * Description: This task waits for the semaphore signal from the
 *              producer task. Once the signal is received, the task
 *              outputs a message and toggles the green LED.
 *
 * Notes:       
 *
 * Returns:     None.
 *
 **********************************************************************/ 
void consumerTask(void *param)
{
    while (1)
    {
        /* Wait for the signal. */
        sem_wait(&semButton);

        printf("Button SW0 was pressed.\n");

        ledToggle();
    }
}
            
            

Message Passing

The Linux message passing example is a light switch, as is the eCos example, with a producer and a consumer task. The producer task monitors button SW0. Once the button is pressed, the producer sends the button press count to the consumer task. The consumer task waits for this message, outputs the button count, and then toggles the green LED. This example uses a POSIX message queue for passing the message from the producer to the consumer task.

Tip

The source code for the message queue example is not included on the book’s web site because it does not work on the Arcom board as shipped. In order to get the message queue code running on the Arcom board, the Linux kernel needs to be rebuilt with message queue support included.

The main routine demonstrates how to create the message queue. First, the LED is initialized by calling ledInit. Then the message queue is created by calling mq_open. The first parameter specifies the name of the queue as message queue. The second parameter, which is the OR of the file status flags and access modes, specifies the following:

O_CREAT

Create the message queue if it does not exist.

O_EXCL

Used with O_CREAT to create and open a message queue if a queue of the same name does not already exist. If a queue does exist with the same name, the message queue is not opened.

O_RDWR

Open for read and write access.

After the message queue is created successfully, the tasks are created as shown previously. The techniques we’ve just discussed are shown in the following main function:

#include <pthread.h>
#include <mqueue.h>
#include "led.h"

int8_t messageQueuePath[] = "message queue";

/**********************************************************************
 *
 * Function:    main
 *
 * Description: Main routine for the Linux message queue program. This
 *              function creates the message queue and the producer
 *              and consumer tasks.
 * 
 * Notes:       
 *
 * Returns:     0.
 *
 **********************************************************************/
int main(void)
{
    mqd_t messageQueueDescr;

    /* Configure the green LED control pin. */
    ledInit();

    /* Create the message queue for sending information between tasks. */
    messageQueueDescr = mq_open(messageQueuePath, (O_CREAT | O_EXCL | O_RDWR));

    /* Create the producer task using the default task attributes. Do not
     * pass in any parameters to the task. */
    pthread_create(&producerTaskObj, NULL, (void *)producerTask, NULL);

    /* Create the consumer task using the default task attributes. Do not
     * pass in any parameters to the task. */
    pthread_create(&consumerTaskObj, NULL, (void *)consumerTask, NULL);

    /* Allow the tasks to run. */
    pthread_join(producerTaskObj, NULL);
    pthread_join(consumerTaskObj, NULL);

    return 0;
}

Prior to entering its infinite loop, the producerTask starts by initializing the variable that keeps track of the number of button presses, buttonPressCount, to zero. Then the producer task opens the message queue, whose name is specified by messageQueuePath, which was created in the main function. The queue is opened with write-only permission, specified by the second parameter flag O_WRONLY, because the producer sends only messages. Because the flag O_NONBLOCK is not specified in the second parameter to mq_open, if a message cannot be inserted into the queue, the producer task blocks. The function mq_open returns a message queue descriptor that is used in subsequent accesses to the message queue.

In the infinite loop, the producer task first delays for 10 ms by calling usleep. The delay interval selected ensures the task is responsive to button presses. Next, the function buttonDebounce is called to determine if the SW0 button has been pressed.

Each time the SW0 button is pressed, buttonPressCount is incremented and the value is sent to the waiting consumer task, using the message queue. To accommodate the message queue send function, the union msgbuf_t is used to contain the message. This union consists of a 32-bit count and a 4-byte buffer array. The message is sent using the function mq_send, with the message queue descriptor, messageQueueDescr, in the first parameter, the button press count in the second parameter, the size of the message in the third parameter, and the priority of the message in the last parameter.

Unlike the eCos message queue implementation, messages under Linux have a priority (which is specified in the last parameter passed to mq_send). You can use this parameter to insert higher-priority messages at the front of the message queue, to be read by the receiving task.

After the message is successfully sent, the loop returns to calling the delay function. Here is the producerTask function:

#include <unistd.h>
#include "button.h"

typedef union
{
    uint32_t count;
    uint8_t  buf[4];
} msgbuf_t;

/**********************************************************************
 *
 * Function:    producerTask
 *
 * Description: This task monitors button SW0. Once pressed, the button
 *              is debounced and a message is sent to the waiting
 *              consumer task.
 *
 * Notes:       
 *
 * Returns:     None.
 *
 **********************************************************************/ 
void producerTask(void *param)
{
    uint32_t buttonPressCount = 0;
    mqd_t messageQueueDescr;
    uint8_t button;
    msgbuf_t msg;

    /* Open the existing message queue using the write-only flag because
     * this task only sends messages. Set the queue to block if
     * a send cannot take place immediately. */
    messageQueueDescr = mq_open(messageQueuePath, O_WRONLY);

    while (1)
    {
        /* Delay for 10 milliseconds. */
        usleep(10000);

        /* Check whether the SW0 button has been pressed. */
        button = buttonDebounce();

        /* If button SW0 was pressed, send a message to the consumer task. */
        if (button & BUTTON_SW0)
        {
            buttonPressCount++;
            msg.count = buttonPressCount;
         
            mq_send(messageQueueDescr, &msg.buf[0], sizeof(buttonPressCount), 0);
        }
    }
}

The task consumerTask in the following function begins like the producer task by opening the message queue with a call to mq_open. But the queue is opened with read-only permission by the second parameter flag, O_RDONLY, because the consumer task receives only messages. Since the flag O_NONBLOCK isn’t specified in the second parameter to mq_open, if no message is available in the queue when the consumer calls the message queue receive function, it blocks until a message is present. The function mq_open returns a message queue descriptor used in subsequent accesses to the message queue.

The consumer task then enters an infinite loop where it waits for a message by calling mq_receive. Once a message is available, it is copied [1] (by Linux) into the rcvMsg variable. The consumerTask then outputs the message and toggles the green LED. The task then returns to waiting for the next message.

#include <stdio.h>

/**********************************************************************
 *
 * Function:    consumerTask
 *
 * Description: This task waits for a message from the producer task.
 *              Once the message is received via the message queue,
 *              the task outputs a message.
 *
 * Notes:       
 *
 * Returns:     None.
 *
 **********************************************************************/ 
void consumerTask(void *param)
{
    mqd_t messageQueueDescr;
    msgbuf_t rcvMsg;

    /* Open the existing message queue using the read-only flag because
     * this task only receives messages. Set the queue to block if
     * a message is not available. */
    messageQueueDescr = mq_open(messageQueuePath, O_RDONLY);

    while (1)
    {
        /* Wait for a new message. */
        mq_receive(messageQueueDescr, &rcvMsg.buf[0], 4, NULL);

        printf("Button SW0 pressed %d times.\n", rcvMsg.count);

        ledToggle();
    }
}

Linux includes numerous other API functions that offer additional functionality for the mechanisms covered previously, as well as other types of synchronization mechanisms. Other mechanisms include condition variables and reader-writer locks. Condition variables allow multiple tasks to wait until a specific event occurs or until the variable reaches a specific value. Reader-writer locks allow multiple tasks to read data concurrently, whereas any task writing data has exclusive access.

Interrupt handling in Linux is more complex than that found in RTOSes. For this reason, we have omitted an interrupt example using Linux. For a better understanding of Linux interrupt handling, take a look at Linux Device Drivers, by Alessandro Rubini and Jonathan Corbe t (O’Reilly).



[1] There may be an implementation of the message queue routine that does not use a copy.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
44.192.93.109