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.
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.
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.
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.
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.
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.
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.
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(); } }
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.
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).
44.192.93.109