Designing An Arm-Based Multithreaded Video/Audio/ Motion Recording System - Part 2[Editor's Note: In Part 1, the author described the basic physical parameters of a video/audio/motion recording system (VAM) and the basic hardware and software building blocks that will be needed before actual implementation and programming of the application.]
Our implementation will be simplified because we are primarily interested in developing a control structure for this system. Thus, we will omit all file handling details, represent files as arrays, and simulate capture of data once per second. (An actual implemented system would capture data about 20 to 40 times per second.) For convenience, we will represent each clock timer-tick as one second.
For this system, we will display information on the screen to show when events are generated and how they are processed. We will also display summary information on a periodic basis. Figure 15 below contains sample diagnostic output for our system that we could use during development.
|Figure 15. Sample output produced by VAM system.|
We will arbitrarily schedule a summary of system statistics every 1,000 timer-ticks. Note that in Figure 15, warning events occur at times 410 and 820. Recall that a warning event has priority 3. Also, an unsafe event occurs at time 760 and a manually triggered event occurs at time 888. When the system summary is displayed at time 1,000, we note that the four detected events have been stored in protected memory. The repeated data values of 4660 (or 0x1234) are used only to suggest the capture of data from the VAM unit. Figure 16 below contains an overview of this structure.
|Figure 16. Basic structure for the VAM system.|
We will begin by creating the declarations, definitions, and prototypes needed for our system. Figure 17, below contains the first part of this section. The values we have chosen for the stack size, memory byte pool size, the size of the protected memory, and the size of the temporary memory can be modified if desired.
|Figure 17. Declarations and definitions—Part 1.|
Many of the entries in Figure 17 are definitions of public resources, such as threads, timers, the message queue, the mutex, and the memory byte pool. (This is a good use of a memory byte pool because space is allocated from the pool only once, thus eliminating fragmentation problems. Furthermore, the memory allocations are of different sizes.
Figure 18 below contains the second part of the program section, devoted to declarations, definitions, and prototypes. We declare the counters, variables, and arrays for our system, as well as the prototypes for our thread entry functions, the timer expiration functions, and the function to display periodic system statistics.
|Figure 18 Declarations, definitions, and prototypes—Part 2.|
The two arrays declared in Figure
18 represent file systems that would be used in an actual
implementation. The array named temp_array
represents the temporary
memory used by the system. As noted earlier, the primary purpose of
temporary memory is to provide an ongoing repository of video, audio,
and motion data.
When an event is detected or manually triggered, the 12 seconds of data before and after that event are copied to protected memory. Temporary memory is overwritten repeatedly because it is limited in size. Figure 19 below illustrates the organization of temporary memory.
|Figure 19. Organization of temporary memory.|
The array named protected_memory represents the protected memory in our system. Each entry in this array represents an event that has occurred. Figure 20 below illustrates the organization of protected memory. Each stored event contains the time the event occurred, the event priority, and the captured data for the event.
|Figure 20 Organization of protected memory.|
The size of the protected memory is relatively small and is specified as MAX_EVENT in Figure 17. Having a small size is reasonable because the number of vehicle events should also be relatively small.
This is the entry into the ThreadX kernel. Note that the call to tx_kernel_enter does not return, so do not place any processing after it. Figure 21 below contains the main entry point for the VAM system.
|Figure 21. The main entry point.|
The next portion of our system is the application definitions section, which we will divide into two parts. In the first part, we will define the public resources needed for our system. This includes the memory byte pool, the three threads, the nine timers, and the message queue. Figure 22, below contains the first part of the applications definitions section.
|Figure 22. Application definitions—Part 1.|
This part of our system consists of the application definition function called tx_application_define. This function defines all the application resources in the system. This function has a single input parameter, which is the first available RAM address. This is typically used as a starting point for run-time memory allocations of thread stacks, queues, and memory pools. The first declaration in Figure 22 is the following:
This pointer is used when allocating memory from the byte pool for
the threads and for the message queue. We then create the memory byte
pool, as follows:
We need to allocate stack space from the byte pool and create the initializer thread, as follows:
byte_pointer, STACK_SIZE, 11, 11,
We assign the initializer thread the highest priority of the three threads in our system. This thread needs to perform its operations immediately, and then it terminates. The other two threads continue operating for the life of the system. We create the data_capture thread and the event_recorder thread in a similar fashion.
However, we assign the event_recorder thread a higher priority than the data_capture thread because it is essential that the event recording operation be performed in a timely manner. (The actual values of the priorities are not important, provided that the initializer thread has the highest priority, and the event_recorder thread has a higher priority than the data_capture thread.)
Note that we use neither the time-slicing option nor the
preemption-threshold option for any of these threads. However, the
event_recorder thread is given
option, but the other two threads are given the TX_AUTO_START option.
The second part of the applications definitions section appears in Figure 23, below; this part contains definitions of our timers and the message queue.
|Figure 23. Application definitions—Part 2.|
We create nine timers and one message queue in Figure 23. There are four timers dedicated to simulating interrupts, four timers to schedule event recording, and one timer to display system statistics. We first create the crash_interrupt timer, which simulates crash events. We arbitrarily simulate crash events every 1,444 timer-ticks—recall that we simulate one second with one timer-tick. Following is the definition of the crash_interrupt timer.
(&crash_interrupt, "crash_interrupt", crash_ISR,
0x1234, 1444, 1444, TX_AUTO_ACTIVATE);
The expiration function associated with the crash_interrupt timer is crash_ISR, which we will define in the next section of our system. This function is activated every 1,444 timer-ticks. We do not use a parameter in the expiration function, so we will specify an arbitrary argument value when we create the timer—in this case, the value 0x1234.
The unsafe_interrupt timer, the warning_interrupt timer, and the manual_interrupt timer are created in a similar manner. However, we specify different expiration values for each of these timers. Note that we give each of these timers the TX_AUTO_ACTIVATE option. We create four timers to schedule the copying of an event from temporary memory to protected memory. We first create the crash_copy_scheduler timer as follows:
crash_copy_activate, 0x1234, 12, 12, TX_NO_ACTIVATE);
When an event is detected (or generated, in our system), the associated ISR activates the corresponding scheduling timer—note that the TX_NO_ACTIVATE option is specified for each of the scheduling timers. Each of the scheduling timers has an expiration value of 12 timer-ticks, so it will expire exactly 12 timer-ticks after an event has been detected.
As before, we pass a dummy argument to the expiration function. The unsafe_copy_scheduler timer, the warning_copy_scheduler timer, and the manual_copy_scheduler timer are each created in a similar fashion. The timer to print system statistics on a periodic basis is created as follows:
0x1234, 1000, 1000, TX_AUTO_ACTIVATE);
We create this timer with the TX_AUTO_ACTIVATE option, so it expires every 1,000 timer-ticks. When this timer does expire, it invokes the expiration function print_stats, which displays the summary statistics.
The last definition in Figure 23 creates the message queue event_notice. We must first allocate space from the memory byte pool for the message queue, and then create the queue. Following is the creation of this queue:
(VOID **) &byte_pointer,
tx_queue_create (&event_notice, "event_notice", TX_2_ULONG,
computes the amount of space needed by the message queue, based on the maximum number of events in protected memory, the size of each message, and the size of the ULONG data type. We use this same expression to specify the amount of space for the queue when it is created. The tx_byte_allocate service creates a block of bytes specified by this expression; the block pointer that receives the address of this block is pointed to by &byte_pointer.
We specify the TX_NO_WAIT option as the wait option. (The TX_NO_WAIT option is the only valid wait option when the tx_byte_allocate service is called from initialization.)
The TX_2_ULONG option specifies the size of each message in the queue. The location of memory space for the queues is pointed to by byte_pointer. Figure 24 below illustrates the contents of the event_notice message queue.
|Figure 24. Message queue event_notice.|
The frame index contains the location in temporary memory where the event began. The event priority indicates the type of event that occurred.
The last portion of our system is the function definitions section, which we will divide into five parts. In the first part, we will define the initializer entry function, which performs basic initialization operations. The second part contains the definition of the data_capture_process entry function, which simulates data capture. The third part contains definitions of the crash_ISR expiration function and the crash_copy_activate expiration function.
The fourth part contains the definition of the event_recorder_process entry function, which copies an event from temporary memory to protected memory. The fifth part contains the print_stats expiration function, which displays system statistics at periodic intervals.
Figure 25 below contains the definition for the initializer entry function. This is the first function that is executed. This function merely initializes the frame_index and event_count global variables. These variables point to the current position of temporary memory and protected memory, respectively. If an actual file system were used, the initializer function would have considerably more work to do.
|Figure 25. Function definitions part 1—initializer entry function.|
Figure 26 below contains the definition for the data_capture_process entry function. This function runs constantly and simulates the capture of data from the VAM unit every timer-tick. We will use the value 0x1234 to suggest data that is being captured and we will get the current value of the system clock.
We will then store those two values in the temp_memory array, which represents the temporary memory file. The value of the frame_index is advanced by one during each timer-tick, and it wraps around to the beginning when it encounters the last position of the array.
|Figure 26. Function definitions part 2—data_capture_process entry function.|
Figure 27 below contains definitions for the crash_ISR expiration function and the crash_copy_activate expiration function. These two functions are placed together because they are closely related to each other. The crash_ISR function sends the frame_index value and the priority (i.e., 1 for a crash) to the event_notice message queue.
It then activates the crash_copy_scheduler timer, which expires in 12 seconds and then invokes the crash_copy_activate expiration function. The crash_copy_activate function resumes the event_recorder thread and then deactivates the crash_copy_scheduler timer. We do not show the corresponding expiration functions for the unsafe, warning, and manual events because they are quite similar. One difference is that different priority values are associated with these events.
|Figure 27. Function definitions part 3: crash_ISR and crash_copy_scheduler expiration functions.|
Figure 28 below contains the definition for the event_recorder_process entry function, which is part of the event_recorder thread. When this thread is resumed by one of the scheduler timers, this function copies a crash event, an unsafe event, a warning event, or a manual event from temporary memory to protected memory. Following are the operations performed by this function:
1. Get the first message on
2. Extract the frame index and priority for the event from the message.
3. Get the frame in temporary memory (containing the time and captured data) pointed to by the frame index.
4. Compute the copy starting point, which is 12 seconds before the current frame.
5. If protected memory is not full, do the following:
a. Get the mutex.
b. Store the event time and priority in the next available position in protected memory.
c. Store the 24 seconds of frame data from temporary memory to protected memory.
d. Increment the event counter.
e. Release the mutex.
|Figure 28. Function definitions part 4: event_recorder entry function.|
We will not implement the overwrite rules in the case when protected memory is full. These rules depend on the actual file system used, and are left as an exercise for the reader. When protected memory is full, we will display a message to that effect, but will not copy the event.
Figure 29 below contains the definition for the print_stats expiration function, which is part of the timer called stats_timer. This timer expires every 1,000 timer-ticks and the function displays a statistical summary of the system.
|Figure 29 Function definitions part 5—print_stats expiration function.|
We used only three threads for this case study. The initializer thread initializes the system, the data_capture thread coordinates the capture of data from the VAM unit to temporary memory, and the event_recorder thread copies event data from temporary memory to protected memory.
We created the data_capture thread with the TX_AUTO_START option, so it would become ready immediately after initialization. We created the event_recorder thread with the TX_DON'T_START option, which means it cannot start until it is resumed by an application timer. When the event_recorder thread completes its data copying operation, it suspends itself and remains suspended until it is resumed by a timer.
We used one message queue to store information about events that had
occurred. When the event_recorder
thread is resumed, it takes the first message from the front of the
message queue. This message contains information about the event data
being copied from temporary memory to protected memory.
Although we used only one message queue in this case study, typical applications tend to use a number of message queues. We used one mutex to ensure that the event_recorder thread had exclusive access to protected memory before beginning the copying operation.
We used one memory byte pool to provide space for the thread stacks and for the message queue. This application is an excellent use of a memory byte pool because we need memory space of different sizes for the thread stacks and the message queue.
Furthermore, these entities remain in existence for the life of the system, so there is no possibility of the fragmentation problems that can occur when bytes are repeatedly allocated and released.
We also demonstrated several examples of communication between resources. For example, timers send data to the message queue, a thread receives data from the message queue, a timer activates another timer, a timer resumes a thread, and a thread obtains ownership of a mutex. All these entities interact to form an efficient and effective system.
To read Part 1 in series go to "Defining the video, audio, control constraints."This series is reprinted in two parts from Chapter 14 of "Real-Time Embedded Multithreading, Using ThreadX and ARM" with permission of Elsevier.
Dr. Edward L. Lamie is professor emeritus and former department chair of California State University's Computer Science Department at the Stanislaus Campus. He directs educational services at Express Logic, where he is responsible for development and delivery of customer training.
A complete listing of the system described here is included in Lamie's
book as well as on the CD in the back of the book.