Patrick
Patrick

Reputation: 4377

Terminal I/O (how to emulate terminal read/wrtie with specific memory region?)

I'm working on the Embedded platform, where I have full access to directly read/write physical memory.

I'm also working on asymmetric processing, in which I have a real time application running concurrently with Linux but totally isolated from Linux.

I want to display the messages from RT app to Linux console (and perhaps send command from Linux to RT app). My current solution is to output everything from RT app to serial port. Then, in Linux I will read the serial port input.

This works but it seems unnecessary, because the RT apps and Linux is on the same physical machine. Thinking back how serial port works, it has a memory buffer and application can read/write to that buffer. Thus, I was wondering if it is possible to connect a terminal display to a specific memory region (i.e. 0x10000) and when RT app "print" some message to 0x10000, Linux terminal would display the message?

Upvotes: 1

Views: 762

Answers (3)

Michael Burr
Michael Burr

Reputation: 340218

I've successfully used a shared memory fifo system to communicate between processes (though not the same scenario you have). The key is that only a single thread can be the producer and a single thread can be a consumer. You'll also need to make sure that as Chris Stratton mentioned that any caching is properly dealt with using appropriate memory barriers. Linux has a pretty straight forward API for memory barriers, I don't know what your real time app might have available to it. I've tried to identify where memory barriers might be required.

The following is an untested (and completely non-optimized) implementation of a shared fifo. Your RT app can write characters to the fifo, and the Linux app or driver can read characters out of the fifo. Ideally you'll have a mechanism for the Linux side to be signaled that data is ready (maybe an otherwise unused GPIO that can fire an interrupt when the RT side pokes it?). Otherwise, the Linux side could poll for data in the fifo, but that's probably less than ideal for the usual reasons.

struct fifo {
    char volatile* buf;
    int buf_len;
    int volatile head;  // index to first char in the fifo
    int volatile tail;  // index to next empty slot in fifo
                         // if (head == tail) then fifo is empty
                         // if (tail < head) the fifo has 'wrapped'
};

void fifo_init( struct fifo* pFifo, char* buf, int buf_len)
{
    pFifo->buf = buf;
    pFifo->buf_len = buf_len;
    pFifo->head = 0;
    pFifo->tail = 0;
}

int fifo_is_full( struct fifo* pFifo)
{
    int head;
    int tail;

    // a read barrier may be required here
    head = pFifo->head;
    tail = pFifo->tail;

    // fifo is full if ading another char would cause
    //    tail == head
    ++tail;
    if (tail == pFifo->buf_len) {
        tail = 0;
    }

    return (tail == head);
}


int  fifo_is_empty(  struct fifo* pFifo)
{
    int head;
    int tail;

    // a read barrier may be required here
    head = pFifo->head;
    tail = pFifo->tail;

    return head == tail;
}


// this function is the only one that modifies
// the pFifo->tail index.  It can only be used
// by a single writer thread.
int fifo_putchar( struct fifo* pFifo, char c)
{
    int tail = pFifo->tail;

    if (fifo_is_full(pFifo)) return 0;

    pFifo->buf[tail] = c;
    ++tail;
    if (tail == pFifo->buf_len) {
        tail = 0;
    }

    //note: the newly placed character isn't actually 'in' the fifo
    //  as far as the reader thread is concerned until the following
    //  statement is run    
    pFifo->tail = tail;

    // a write barrier may need to be placed here depending on 
    // the system.  Microsoft compilers place a barrier by virtue of
    // the volatile keyword, on a Linux system a `wmb()` may be needed
    // other systems will have other requirements
    return 1;
}


// this function is the only one that modified the
// pFifo->head index.  It can only be used by a single
// reader thread.
int fifo_getchar( struct fifo* pFifo, char* pC)
{
    char c;
    int head = pFifo->head;

    if (fifo_is_empty(pFifo)) return 0;

    // a read barrier may be required here depending on the system
    c = pFifo->buf[head];

    ++head;
    if (head == pFifo->buf_len) {
        head = 0;
    }

    // as far as the write thread is concerned, the char 
    // hasn't been removed until this statement is executed
    pFifo->head = head;

    // a write barrier might be required

    *pC = c;
    return 1;
}

It might be more appropriate to use platform 'atomic' APIs when updating the indexes.

Some optimizations that can be performed:

  • if the fifo size is restricted to a power of 2, the wrapping can be handled by masking the indexes appropriately
  • the put/get functions could be changed or additional get/put functions could be added to accept a string or an array of data bytes, and the string/array can be copied to (or from) the fifo buffer more efficiently.

The key to this set up is that the reader can read data in the fifo without worry that the writer will overwrite it as long as the head index isn't updated until after the data is read out. Similarly for the writer - it can write into the 'free' portion of the buffer as long as the tail index isn't updated until after the data has be written through to the buffer. The only real complication is making sure that appropriate items are marked volatile and that appropriate memory barriers are called.

Upvotes: 0

num1
num1

Reputation: 4975

There are a few ways of performing IPC in linux, and file descriptors are usually involved. In my opinion your best bet is to continue doing what you're doing, it is probably overkill as you said, but trying to implement your own shared memory solution is definitely even more so overkill.

EDIT:

As mentioned in the comments, the fact that you're running a real-time process throws things off, and native IPC probably isn't your best bet. Here's an article I just Googled that seems to provide the answer you're looking for.

If you don't want to read all of it, it suggests either FIFOs or Shared Memory as the concurrency primitive you should use, depending on what kind of communication you require. From personal experience, FIFOs lead to less headaches in the long run, because you have to worry a lot less about synchronization.

You'd most likely have to write a small program that reads from the fifo/shared memory and sends messages to stdout, if you want to monitor the program in a terminal.

Upvotes: 1

Chris Stratton
Chris Stratton

Reputation: 40357

You could construct a sort of virtual serial port using "mailbox" techniques. I'll describe half of a simple working connection, you probably want one of these going in each direction so you can send commands and get responses.

Reserve a chunk of memory during kernel initialization. Let's say 4k since that's often a page, but 256 or even 16 bytes will work too. Reserve a second for the other direction.

When the "writer" of a channel wants to say something, it first checks if the first 32-bit word is zero. If it is, then starting from the 5th byte, it writes a message - textual or binary data, whatever, up to a maximum of 4k - 4 = 4092 bytes. Then it sets the first word equal to the count of bytes it has written.

The receiver watches the byte count in the first word of the channel it is receiving. When it sees a non-zero byte count, it reads that many bytes from memory. Then it sets the byte count to zero, to signify to the writer that a new message may now be written at its convenience.

The only thing this depends on is that you actually access the real memory or work through the same cache, and that you have an atomic write operation for writing the byte count (if you don't have 32-bit atomic writes, use a 16-bit count which is plenty anyway, or make the buffer smaller and use an 8-bit count). Since the writer can only set it to a nonzero value when it's zero, and the reader can only set it to a zero value when it's non-zero, it all works out.

This is of course a simple mechanism, and either side can be blocked by the other. But you can design the components that source the messages to take that into account. You can also extend it by coming up with a way to have multiple messages in flight, or adding an additional priority or error-reporting channel in parallel.

Oh, before you jump in and code this, do some web searching. I am certain there's already some mechanism like this or something else available for connecting your RT and linux components. But learning to do it yourself can be interesting too - and necessary if you run into this kind of problem on a smaller embedded system without an OS that provides the functionality for you.

Upvotes: 1

Related Questions