Archives

Page Ranking

A Matter of Interrupts and Mutual Exclusion - Part 2

Without doubt, the presence of the FIFO buffer helps (as we have seen before). The pieces of software that are needed to handle the FIFO are:

  • the producer: Rx ISR (the receive interrupt service routine)
  • the consumer: GetChar() function itself.

Assuming the USART is identified by the name USART1 and the implementation does not involve an RTOS yet, their code will look as follows:

Producer code (Rx ISR)

void  Com_ISR  (void)                       // USART Rx handler (ISR)
{/////////////////////////////////////////////////////////////////////////////////////////////
    COM_RX_BUFFER *pcom_buf;                // pointer to COM Rx buffer
    U8             b_char;                  // byte holder
 
    . . . . . .                             // identify the interrupt
    pcom_buf = Com_GetBufPtr (USART1);      // obtain the pointer to the circular buffer
    . . . . . .                             // do other things as required
    b_char = USART_ReceiveData (USART1);    // get one byte from USART1
    Com_WriteRxBuf (pcom_buf, b_char);      // write the byte into the circular buffer
    . . . . . .                             // do other things as required
} ////////////////////////////////////////////////////////////////////////////////////////////

Consumer code (GetChar):

S32   ComGetChar   (U32 timeout)            // the "GetChar()" function
{/////////////////////////////////////////////////////////////////////////////////////////////
    COM_RX_BUFFER *pcom_buf;                // pointer to COM buffer
    U8             b_val;                   // byte holder
 
    pcom_buf = Com_GetBufPtr (USART1);      // obtain the pointer to the circular buffer
    . . . . . .                             // do other things as required
    b_val = Com_ReadRxByte (pcom_buf);      // extract the character from the circular buffer
    return (S32)b_val & 0xFF;               // return the character as a 32-bit signed int
} ////////////////////////////////////////////////////////////////////////////////////////////

FIFO as a Circular BufferSimple, isn’t it? Well, not that simple. A quick look will find two functions that are not implemented yet (Com_WriteRxBuf and Com_ReadRxByte) and one parameter (timeout) that is not used in ComGetChar(). I will ignore the parameter for now and concentrate on the two functions just mentioned. Their role is to be the interface between the code above and the FIFO buffer we intend to use.

At a second glance the embedded developer will realize that the producer and the consumer run in two different contexts independent to each other: the ISR and the main thread where the function ComGetChar() is invoked sharing the FIFO buffer (common resource). As consequence, the access to the buffer needs to disciplined ensuring that only one process — producer or consumer — can access the buffer at one given time, i.e. mutual exclusion policy.

Let’s examine the FIFO buffer implementation. The following picture shows one of the most used methods of designing a FIFO in software. With a buffer of a given size and two pointers (named here Head and Tail) the FIFO can be described as a data structure and, eventually, as a data type — COM_RX_BUFFER:

typedef struct                    // Rx Circular Bufer structure
{
    U8 volatile *rx_buf;          // pointer to (volatile) Rx buffer
    U32          rx_buf_size;     // Rx buffer size
    U32 volatile rx_head;         // Rx head index
    U32 volatile rx_tail;         // Rx tail index
    U32 volatile rx_count;        // Rx byte counter
} COM_RX_BUFFER;

As C programmers know, the relationship between pointers and arrays allow us to interchange subscripting with C pointers — I will prefer the first for clarity. I added a counter to keep track of how many bytes we have in the FIFO without the need of subtracting the head and tail pointers. The idea is simple: whenever an item is inserted I will use the tail pointer (one end of the buffer) and whenever an item is extracted I will use the head pointer (the other end of the buffer).

U8  Com_ReadRxByte  (COM_RX_BUFFER *pcom_buf)
{/////////////////////////////////////////////////////////////////////////////////////////////
    U8     b_val;                                  // byte holder

    b_val = pcom_buf->rx_buf[pcom_buf->rx_head];   // extract one byte and
    pcom_buf->rx_head = (pcom_buf->rx_head + 1) %  // advance the head index and wrap
                         pcom_buf->rx_buf_size;    // around if past end of buffer
    pcom_buf->rx_count--;                          // decrement the byte counter
    return b_val;                                  // return the extracted byte
} ////////////////////////////////////////////////////////////////////////////////////////////

void  Com_WriteRxBuf  (COM_RX_BUFFER *pcom_buf, U8 b_char)
{/////////////////////////////////////////////////////////////////////////////////////////////
    pcom_buf->rx_buf [pcom_buf->rx_tail] = b_char; // insert one byte
    pcom_buf->rx_tail = (pcom_buf->rx_tail + 1) %  // advance the tail index and wrap
                         pcom_buf->rx_buf_size;    // around if past end of buffer
    pcom_buf->rx_count++;                          // increment the byte counter
} ////////////////////////////////////////////////////////////////////////////////////////////

Look at the code: now you’ve got the idea. But don’t rush to use these functions yet. They may hide some flaws or omissions at least. Can you identify them? Please send me your comments. In the next posting I will examine in detail the problem of mutual exclusion and a way to solve it.

2 comments to A Matter of Interrupts and Mutual Exclusion – Part 2

  • Jan Vanek

    Nice page, a lot of work…

    Regarding the flaws you ask: One minor problem with the Com_BufReadRxByte() is that it doesn’t check whether the FIFO is empty so you can call it only when the FIFO is non-empty and thus you make the programmer responsible for checking that. The major problem though is the rx_count variable. This variable is modified in both functions Com_ReadRxByte() and Com_WriteRxBuf(), and due to the fact that Com_WriteRxBuf() is called from an ISR which can interrupt the Com_ReadRxByte(), the rx_count can become inconsistent.

    My view is following, in case you have 2 threads and one thread modifies only one half of the FIFO (one part of members of the FIFO, e.g. the write part), and potentially only reads the other half, and the other thread modifies only the other half of the FIFO (e.g. the read part), then you don’t need any mutual exclusion (of those threads) at all. This is very often the case, you have the main thread and the interrupt thread, one of which is (only) producer, the other one is (only) consumer or vice-versa. I think we should take a benefit of it and construct queues which don’t need mutual exclusion (for this scenario).

    With regards,
    Jan

  • admin

    Reading your message, I noticed one typo in my post: it is not Com_BufReadRxByte() but Com_ReadRxByte(). I fixed it.

    You are right: there is no check if the FIFO is empty (in ComGetChar) or full (in Com_ISR). This was one of the answers.

    You are right again: the rx_count is also a shared variable and needs protection (or atomic access) but this won’t lead to a disaster as rx_count is not used directly to access the FIFO (it was intended for only checking the amount of data in the FIFO). However, its value may be inconsistent as you mentioned.

    The major problem (as I can see it) is the access to the buffer itself — here is where the mutual exclusion problem really occurs. And this is what I will try to cover in my next post(s).

    Your assumption that there is no other concurrency than the main thread vs. interrupt is valid in many cases where there is no RTOS involved. However, even in this simple situation the mutual exclusion problem exists and you need protection. I suggest you analyze the code generated by the compiler for Com_ReadRxByte() and see what an interrupt occurring randomly between machine instructions could do when the function executes. The exercise is always recommended when you have doubts regarding the effects of interrupt preemption.