This is an old revision of the document!
You will need to implement a queue data structure for your laser-tag system. You should remember the basics of the queue data structure from some of your CS classes. This web-page provides a quick review of queues. This web page explains the “circular buffer” implementation strategy for queues. I suggest that implement the queue as a circular buffer though it is not required. If you do not use the circular-buffer strategy, then you won't use the queue.c
code that I provided below.
The queue that you must implement will be just a little different than a typical queue. It must have the following properties:
Why the extra location? In the queue.c code that I provided you will note that I allocate one additional memory location. My presumption is that you will implement the queue as a circular buffer that uses an empty location to deal with the empty/full problem. Please see this web page on circular buffers as it explains this issue. You are not required to implement the queue using a circular buffer. However, it is usually about the easiest thing to debug and get running.
The circular-buffer approach uses an array to store the data contained within the queue. However, we want to be able to set the size of the array when the queue is initialized. We do that by allocating a block of memory of the desired size during initialization and then using that block as our array. While a complete treatment of 'C' pointers is beyond this simple explanation, the code shown in queue_init()
is pretty simple.
queue_data_t * data
The code above declares a variable named data that should ultimately “point” to a chunk of memory that contains something of type queue_data_t
(typedef double queue_data_t
). However, at this point, all we have done is to tell the compiler that, when we refer to the pointer data
, we will treat what it points to as a queue_data_t
. This allows the compiler to ensure that we are consistent when we use this pointer later in our code. By way of reminder, double
refers to a number that is double-precision floating point.
It is important to note that when the pointer data
is declared, it actually points to nothing. At run-time, we need to allocate memory that data
will “point” to. This is the job of malloc()
(malloc()
stands for “memory-allocation”, if you like). Here is an example where the declaration of the pointer and allocation of memory are done in one line of code:
queue_data_t * data = (queue_data_t *) malloc(4 * sizeof(queue_data_t));
In the simple example shown above, I have told malloc()
that I want a chunk of memory that is big enough to hold 4 items, where each item is the size of a queue_data_t
(again, queue_data_t
is a double). sizeof()
is a compiler primitive that will return the number of bytes that a particular data-type requires. If your curious about how many bytes are occupied by a variable of size double, you can write a simple program and find out:
#include <stdio.h> void main() { printf("A double requires %d bytes.\n\r", sizeof(double)); }
If you run this code on the ECEN 330 board, it will print out:
“A double requires 8 bytes.”
In any case, once we have executed the malloc()
code above, the data
pointer now points to a block of memory that is 32 bytes in size (4 doubles). In 'C', a pointer and a 1-dimensional array are essentially the same thing. With memory properly allocated, I can now treat the data
pointer exactly as an array. For example, I could initialize my data
array using the code below:
for (int16_t i=0; i<4; i++) { data[i] = 0.0; }
Or, I could print out the contents of my data
array using the code below:
#include <stdio.h> for (int16_t i=0; i<4; i++) { printf("data[%d]:%le\n\r", i, data[i]); }
Let's look at a a few simple examples of how the an array can be used to implement a circular buffer, using the queue_t
data structure that I provide below. First, to keep things simple, let's make our queue big enough to hold 3 elements (3 elements of type double, in our case). Using the code that I provided below, you would say the following:
#include "queue.h" queue_t myQueue; queue_init(&myQueue, 4);
One thing to note in the code above is that we pass the address of myQueue
to queue_init
. We do this for the sake of efficiency in 'C', and because this is because this is what the queue_init()
code expects. Once we executed the code above, a pictorial view of myQueue
would look something like right after queue_init()
has been called) is shown below. Note that enough memory for 5 doubles has been allocated. But, as I said before, this queue can only hold 4 doubles.
This queue, is by definition, empty because indexIn
and indexOut
are the same value (0, in this case). Now, let's look at what the queue looks like after we have “pushed” 4 values (23.4, 3,14, 72.1, 89.4).
Even though there appears to be one empty location in the queue (location 4), the queue is now considered to be full. Why is this so? Because indexIn
always points to the empty location where the next “push” will store data. If we were to do another “push” when the queue is in its current state, indexIn
would be wrapped around so that is back to 0, and because the queue is considered to be empty anytime indexIn
and indexOut
are the same value, the queue would become empty. Thus, whenever we “push” a new value onto the queue, the queue needs to do a simple test: if indexIn
were incremented, would it be equal to indexOut
? If so, the queue is currently full and it is not possible to “push” another value onto the queue.
Now, let's perform two “pops” on our queue, removing two elements. After two “pops”, our queue would be shown pictorially below:
As shown, indexOut
has moved up two locations. Now, let's say that we want to “push” two new values onto our queue. When “pushing” a new value, the queue would need to check if it is full. It is not because, if indexOut is incremented and wraps around to location 0, it will not be equal to indexOut
because indexOut
now has the value of 2.
Now, let's look at the queue after “pushing” two new values (51.5 and 49.2).
indexIn
now has the value of 1 and you can see that two values have been “pushed” onto the queue. Is our queue full? It must be, because the queue can only hold 4 elements. Our “empty” location is location 1 (currently showing 3.14 but remember that value was previously “popped” off). Let's try the “full test”. If we were to increment indexIn
it would have the same value as indexOut
, so the queue is currently full.
Carefully study the example above so you can see how the circular buffer works with an array, and also so you can see why there is always an empty location in the queue when implemented this way.
queue_overwritePush()
correctly. If the queue is full, remove an element and push the new element on the queue. If it is not full, just push the new value.queue_init()
, e.g., a queue of size 4 must be allocated with 5 locations. This does not apply to students who are using elementCount to determine full/empty.I have provided the source code for your ”.h” file and part of the code for your ”.c” file. As I said before, you are not required to implement the queue as I have shown here. However, you must implement all of the functions that are contained in the ”.h” file.
/* * queue.h * */ #ifndef QUEUE_H_ #define QUEUE_H_ #include <stdint.h> #include <stdbool.h> typedef uint32_t queue_index_t; typedef double queue_data_t; typedef uint32_t queue_size_t; typedef struct { queue_index_t indexIn; queue_index_t indexOut; queue_size_t size; queue_size_t elementCount; queue_data_t * data; } queue_t; // Allocates the memory to you queue (the data* pointer) and initializes all parts of the data structure. void queue_init(queue_t* q, queue_size_t size); // Returns the size of the queue.. queue_size_t queue_size(queue_t* q); // Returns true if the queue is full. bool queue_full(queue_t* q); // Returns true if the queue is empty. bool queue_empty(queue_t* q); // Pushes a new element into the queue. Reports an error if the queue is full. void queue_push(queue_t* q, queue_data_t value); // Removes the oldest element in the queue. queue_data_t queue_pop(queue_t* q); // If the queue is full, remove the oldest element and then push the new element. void queue_overwritePush(queue_t* q, queue_data_t value); // Provides random-access read capability to the queue. // Low-valued indexes access older queue elements while higher-value indexes access newer elements // (according to the order that they were added). queue_data_t queue_readElementAt(queue_t* q, queue_index_t index); // Returns a count of the elements currently contained in the queue. queue_size_t queue_elementCount(queue_t* q); // Frees the storage that you malloc'd before. void queue_garbageCollect(queue_t* q); // Prints the current contents of the queue. Handy for debugging. // This must print out the contents of the queue in the order of oldest element first to newest element last. void queue_print(queue_t* q); // Performs a comprehensive test of all queue functions. Returns -1 if the test fails, 0 otherwise. int16_t queue_runTest(); #endif /* QUEUE_H_ */
/* * queue.c * */ #include <stdio.h> #include <stdlib.h> #include "queue.h" // Standard queue implementation that leaves one spot empty so easier to check for full/empty. void queue_init(queue_t* q, queue_size_t size) { q->indexIn = 0; q->indexOut = 0; q->elementCount = 0; q->size = size+1; // Add one additional location for the empty location. q->data = (queue_data_t *) malloc(q->size * sizeof(queue_data_t)); } // Just free the malloc'd storage. void gueue_garbageCollect(queue_t* q) { free(q->data); }
You must write your own queue_runTest() function to thoroughly test your code. Once you have tested the code using your own queue_runTest() function, I suggest testing your queue implementation with the following test code. It verifies the queue against itself. It creates a chain of smaller queues and a single large queue with the same capacity as all of the small queues. It then simultaneously pushes data through the chain of small queues and the large queue and compares the two.
Notes:
Common Bugs:
#include <stdio.h> #include <stdlib.h> #include "queue.h" #define SMALL_QUEUE_SIZE 10 #define SMALL_QUEUE_COUNT 10 static queue_t smallQueue[SMALL_QUEUE_COUNT]; static queue_t largeQueue; // smallQueue[SMALL_QUEUE_COUNT-1] contains newest value, smallQueue[0] contains oldest value. // Thus smallQueue[0](0) contains oldest value. smallQueue[SMALL_QUEUE_COUNT-1](SMALL_QUEUE_SIZE-1) contains newest value. // Presumes all queue come initialized full of something (probably zeros). static double popAndPushFromChainOfSmallQueues(double input) { // Grab the oldest value from the oldest small queue before it is "pushed" off. double willBePoppedValue = queue_readElementAt(&(smallQueue[0]), 0); // Sequentially pop from the next newest queue and push into next oldest queue. for (int i=0; i<SMALL_QUEUE_COUNT-1; i++) { queue_overwritePush(&(smallQueue[i]), queue_pop(&(smallQueue[i+1]))); } queue_overwritePush(&(smallQueue[SMALL_QUEUE_COUNT-1]), input); return willBePoppedValue; } static bool compareChainOfSmallQueuesWithLargeQueue(uint16_t iterationCount) { bool success = true; static uint16_t oldIterationCount; static bool firstPass = true; // Start comparing the oldest element in the chain of small queues, and the large queue // and move towards the newest values. for (uint16_t smallQIdx=0; smallQIdx<SMALL_QUEUE_COUNT; smallQIdx++) { for (uint16_t smallQEltIdx=0; smallQEltIdx<SMALL_QUEUE_SIZE; smallQEltIdx++) { double smallQElt = queue_readElementAt(&(smallQueue[smallQIdx]), smallQEltIdx); double largeQElt = queue_readElementAt(&largeQueue, (smallQIdx*SMALL_QUEUE_SIZE) + smallQEltIdx); if (smallQElt != largeQElt) { if (firstPass || (iterationCount != oldIterationCount)) { printf("Iteration:%d\n\r", iterationCount); oldIterationCount = iterationCount; firstPass = false; } printf("largeQ(%d):%lf", (smallQIdx*SMALL_QUEUE_SIZE) + smallQEltIdx, largeQElt); printf(" != "); printf("smallQ[%d](%d): %lf\n\r", smallQIdx, smallQEltIdx, smallQElt); success = false; } } } return success; } #define TEST_ITERATION_COUNT 105 #define FILLER 5 int queue_runTest() { int success = 1; // Be optimistic. // Let's make this a real torture test by testing queues against themselves. // Test the queue against an array to make sure there is agreement between the two. double testData[SMALL_QUEUE_SIZE + FILLER]; queue_t q; queue_init(&q, SMALL_QUEUE_SIZE); // Generate test values and place the values in both the array and the queue. for (int i=0; i<SMALL_QUEUE_SIZE + FILLER; i++) { double value = (double)rand()/(double)RAND_MAX; queue_overwritePush(&q, value); testData[i] = value; } // Everything is initialized, compare the contents of the queue against the array. for (int i=0; i<SMALL_QUEUE_SIZE; i++) { double qValue = queue_readElementAt(&q, i); if (qValue != testData[i+FILLER]) { printf("testData[%d]:%lf != queue_readElementAt(&q, %d):%lf\n\r", i, testData[i+FILLER], i+FILLER, qValue); success = 0; } } if (!success) { printf("Test 1 failed. Array contents not equal to queue contents.\n\r"); } else { printf("Test 1 passed. Array contents match queue contents.\n\r"); } success = 1; // Remain optimistic. // Test 2: test a chain of 5 queues against a single large queue that is the same size as the cumulative 5 queues. for (int i=0; i<SMALL_QUEUE_COUNT; i++) queue_init(&(smallQueue[i]), SMALL_QUEUE_SIZE); for (int i=0; i<SMALL_QUEUE_COUNT; i++) { for (int j=0; j<SMALL_QUEUE_SIZE; j++) queue_overwritePush(&(smallQueue[i]), 0.0); } queue_init(&largeQueue, SMALL_QUEUE_SIZE * SMALL_QUEUE_COUNT); for (int i=0; i<SMALL_QUEUE_SIZE*SMALL_QUEUE_COUNT; i++) queue_overwritePush(&largeQueue, 0.0); for (int i=0; i<TEST_ITERATION_COUNT; i++) { double newInput = (double)rand()/(double)RAND_MAX; popAndPushFromChainOfSmallQueues(newInput); queue_overwritePush(&largeQueue, newInput); if (!compareChainOfSmallQueuesWithLargeQueue(i)) { // i is passed to print useful debugging messages. success = 0; } } if (success) printf("Test 2 passed. Small chain of queues behaves identical to single large queue.\n\r"); else printf("Test 2 failed. The content of the chained small queues does not match the contents of the large queue.\n\r"); return success; }