Complete Communications Engineering

In general, double-buffering must be implemented in a language that provides access to the shared memory region, such as C or C++.  When two processors have access to the same region of memory, double-buffering is a simple way to move data from one to the other, and is good enough in many applications.  Double-buffering is an example of a producer-consumer algorithm.  It is a flexible algorithm that is used in many applications.  An example application is when a real-time co-processor must transfer live data to a CPU running an operating system such as Linux.  The CPU is usually busy with other tasks, so it can’t receive each sample as it’s generated.

Double-buffering uses two buffers, buffer A and buffer B.  The producer starts by filling buffer A over a period of time.  When buffer A is full, the producer signals the consumer and begins filling buffer B over the next period of time.  When buffer B is full, the producer switches back to buffer A and fills it again.  The producer continues switching between the two buffers, signaling the consumer each time a buffer is full.  When the consumer is signaled, it must read and process one of the buffers before the producer is done filling the other.  The buffer size determines how much time the consumer has to do this.  Larger buffers give the consumer more time, but there is more latency between when a sample is generated and when it’s processed.

The following example code shows an implementation of double-buffering:

producer.c

#define BUFFER_SIZE 1024

 

/* Compiler-specific method of declaring that a variable should be in a

 * shared memory region */

#define SHARED_MEM __attribute__(“SHARED”)

 

/* The double buffer, in shared memory */

SHARED_MEM int double_buffer[2 * BUFFER_SIZE];

 

/* Signal for the CPU, in shared memory */

SHARED_MEM int signal = 1;

 

/* This function waits for the next sample and returns it */

int generate_next_sample() { /* TODO */ }

 

void main (void)

{

    int buffer_index = 0;

    while (1) {

        if (buffer_index == (2 * BUFFER_SIZE)) {

            signal = 1;     /* Done filling the second buffer */

            buffer_index = 0;

        }

        else if (buffer_index == BUFFER_SIZE) {

            signal = 0;     /* Done filling the first buffer */

        }

        double_buffer[buffer_index] = generate_next_sample();

        buffer_index++;

    }

}

 

consumer.c

#define BUFFER_SIZE 1024

 

/* These functions map the producer’s buffer and signal into our memory

 * space */

int *map_buffer (void) { /* TODO */ }

int *map_signal (void) { /* TODO */ }

 

/* This function is called to process a block of samples */

void process_buffer (volatile int *buf) { /* TODO */ }

 

void main (void)

{

    int cur_signal;

    volatile int *buffer = map_buffer();

    volatile int *signal = map_signal();

    cur_signal = *signal;

    while (1) {

        if (*signal != cur_signal) {

            cur_signal = *signal;

            if (cur_signal == 0) {

                /* Process the first buffer */

                process_buffer(buffer);

            }

            else {

                /* Process the second buffer */

                process_buffer(buffer + BUFFER_SIZE);

            }

        }

        /* TODO – other things while we wait */

    }

}