MODSERIAL Feature Request

20 Apr 2011

This topic started on the MODSERIAL cookbook page. I am continuing it here at Andy's request:

Quote:

Also, for reasons I don't know, this website doesnt send me an email when people add comments to cookbook pages. So please ask questions in the Formum section where I do get email notifications :)

To re-iterate, I want to use the mbed USB serial port to provide a basic debug monitor in a project. I cannot use getc() and wait for input because my main loop needs to run continuously to service various events. I could use getcNb() or readable() to make the input stuff non-blocking, but this has the limitation that if control has passed from my main routine to an event handler, the debug port will not be serviced.

The rx callback feature of MODSERIAL allows me to echo user input and do basic command line processing outside of the main loop. It is always active, no matter what the main loop is doing.

I want to support basic command line editing such as backspace (erase last character) and delete (erase entire line). Delete is easy, since I can flush the rx buffer, but for backspace I need a way of discarding the last character to go into the rx buffer. Is this possible?

If not, any chance of you adding a new function, rxDiscardLastChar() ?

Thanks, Paul

20 Apr 2011

Ok, lets see if I've understood this correctly. The buffer acts as a FIFO which has a head (where getc() pops chars from) and a tail (where MODSERIAL pushes chars into). What you want is a function that acts like getc() but on the tail rather than the head?

[Edit. the only problem I see here is what if another char arrives and gets inserted before your application manages to make the call to remove that last char? It's possible and more likely as the baud rate increases. The only real way I can see to make it work properly and avoid a potential race condition is hook this into the rx callback and allow the application the choice of blocking the operation at insert time).]

As for the rx callback. When it activates you know MODSERIAL has just inserted a byte into it's RX buffer. You can "peek" at that byte that just went into the FIFO tail (ie read it without removing it from the buffer) with rxGetLastChar()). You can then act on that accordingly.

20 Apr 2011

Andy, OK, we're getting there!

Quote:

Ok, lets see if I've understood this correctly. The buffer acts as a FIFO which has a head (where getc() pops chars from) and a tail (where MODSERIAL pushes chars into). What you want is a function that acts like getc() but on the tail rather than the head?

Correct

Quote:

[Edit. the only problem I see here is what if another char arrives and gets inserted before your application manages to make the call to remove that last char? It's possible and more likely as the baud rate increases. The only real way I can see to make it work properly and avoid a potential race condition is hook this into the rx callback and allow the application the choice of blocking the operation at insert time).]

Good point but I don't think there will be a race condition as the processing is taking place in the callback. In any case the input is coming from a human, so the character rate will be low.

Quote:

As for the rx callback. When it activates you know MODSERIAL has just inserted a byte into it's RX buffer. You can "peek" at that byte that just went into the FIFO tail (ie read it without removing it from the buffer) with rxGetLastChar()). You can then act on that accordingly.

Yes, I am aware of this. Time to share my test program. This should explain matters.

/* Test of command line input, echo and basic editing using MODSERIAL library
 *
 * Backspace removes itself and previous character from buffer and updates terminal
 * Delete flushes buffer and updates terminal
 * CR or LF denotes end of line. It is removed from buffer and end of line flag is set
 * Other characters are put into buffer and echoed on terminal
 */

#define DATE __DATE__
#define TIME __TIME__
#define VERSION "0.04"
 
#include "mbed.h"
#include "MODSERIAL.h"

void rxCallback(void);

DigitalOut led1(LED1);
DigitalOut led2(LED2);

MODSERIAL pc(USBTX, USBRX);

bool eol_flag = false;
char buf[100];

int main() {
    char *cp;
    
    pc.printf("%c[2J", 0x1B);    //VT100 erase screen
    pc.printf("%c[H", 0x1B);     //VT100 home
    pc.printf("MODSERIAL test program (Version %s, %s %s)\n",
        VERSION, DATE, TIME);

    //set up serial rx callback
    pc.baud(9600);
    pc.attach(&rxCallback, MODSERIAL::RxIrq);

    pc.printf("> ");
    while(1) {
        if (eol_flag) {
            led2 = 1;       //light LED2 to show command processing
            pc.printf("\n");
            //read input into buffer (gets() does not seem to be supported!)
            cp = buf;
            while (pc.readable() ) {
                *cp++ = pc.getc();
            }
            --cp;           //move back to CR or LF
            *cp = '\0';     //terminate buffer
            //print results
            int i = (int) (cp - buf);
            pc.printf("cnt = %d, buf = <%s>\n", i, buf);
            cp = buf;
            while (i--) {
                pc.printf("%02x ", (int) *cp++);
            }
            pc.printf("\n");
            led2 = 0;
            pc.printf("> ");
            eol_flag = false;
        }
    }
}

// Rx callback routine

void rxCallback(void) {
    led1 = !led1;   //toggle LED1 to show char received
    char c = pc.rxGetLastChar();
    if (c == 0x7f)      //flush input buffer and erase line on terminal
    {
        int i = pc.rxBufferGetCount();
        pc.rxBufferFlush();
        while (i--) {
            pc.putc(0x8);
            pc.putc(' ');
            pc.putc(0x8);
        }
    }
    else if (c == 0x8)  //remove last char from buffer and erase on terminal
    {
        //remove the BS and the previous char
        //rxDiscardLastChar();
        //rxDiscardLastChar();
        pc.putc(0x8);
        pc.putc(' ');
        pc.putc(0x8);
    }
    else if (c == '\n' || c == '\r') //end of line, process command
    {
        //remove line terminator from buffer
        //rxDiscardLastChar();
        eol_flag = true;
    }
    else    //echo char on terminal
    {
        pc.putc(c);
    }
}

// END

Hopefully, this will make sense to you. The commented out rxDiscardLastChar() shows where and how I will use the new function :).

Paul

20 Apr 2011

Hi Paul,

Ok, that makes sense and is easy to do. However, to avoid any race condition I'll have to add code to ensure mal-operation (ie, I'll only allow rxDiscardLastChar() to be called from within the RX callback context). I know you probably will never see a race occur but as a library I really have to think about how others may use it in the furture.

I should have this done pretty soon. I'll update this thread when ready for testing.

Andy

20 Apr 2011

Hi Andy,

I look forward to testing it.

On second thoughts it may be best to leave the line terminator in the buffer. Otherwise you have to get rid of the cp statement in the main loop.

Paul

20 Apr 2011

Sorry about the strike through. I did not bother to preview it before posting!

I meant it to be minus minus cp (i.e. pre-decrement cp).

Paul

21 Apr 2011

Hi Paul,

Ok, this is done. However, I made a pretty noticable change in version 1.18 so I'll describe it here.

First up, originally callbacks were stored and invoked from MODSERIAL using Mbed's standard FunctionPointer type class. That meant that all callback function prototypes must be of the form void func(void); which means MODSERIAL cannot send any information to the callback. Initially this was ok. However, as more features get added I really wanted to be able to make this mechanism more flexible.

For example, in you rxCallback function to access the MODSERIAL object you had to use the global variable pc. Now, for aplications this is generally ok. But let's imagine you wanted to convert your code to a library so you could simply bind it to any serial port. Those globals now become a problem for you. So I have now changed the callback to use a different callback system using my own class placeholder.

Callback function prototypes are now of the form void func(MODSERIAL_IRQ_INFO *);. As you can see, I pass to the callback a pointer to an object of type MODSERIAL_IRQ_INFO. As you can see, a public property of this class is a pointer to the MODSERIAL object that invoked the callback. This makes it far more flexibale as callbacks now no longer have to use the global instance of MODSERIAL, it can simply use the pointer passed in.

See the two examples example3a.cpp and example3b.cpp I did to make this clear.

Now, onto the "protection". I wanted rxDiscardLastChar() to only be called from with the callback context to avoid potential race conditions. The simplest and most obvious way would be to set a flag on IRQ entry and clear it on IRQ exit. And then the function could test this flag. However, for me at least, this is clumsie and, somewhere down the road, may lead to other bugs and pitfalls. So I decided to let the C++ compiler do the work for me.

With MODSERIAL rxDiscardLastChar() is defined as "protected". This means if an application tries to call it the compiler will throw an error (at compile time) that the function is inaccessable. This prevents basic application access. However, the object pointer MODSERIAL_IRQ_INFO that gets passed to your callback has it's own public version of rxDiscardLastChar(). Because MODSERIAL_IRQ_INFO and MODSERIAL are friend classes callback functions can use this to invoke the protected version :) Simple.

Now, I am aware that there are "workarounds" that allow a cunning programmer to get around this protection. However, you have to go some distance to implement it. And if a programmer did this then they should know what they are doing and be aware of the potential consequences (and not get much support from me for subverting the protection!).

As shown earlier, the two examples (3a and 3b) show how rxDiscardLastChar() can be used with the IRQ callback context.

Let me know how you get on and if you get any problems.

21 Apr 2011

Hi Andy,

Nice one! The new feature works superbly. Your examples were very helpful.

I have tarted up my test program to make it clearer and have published it as CanonicalInputProcessing so that others may benefit from your excellent work.

I think this fits in nicely with your article on interrupt contexts. By moving work into interrupt contexts and treating the main loop as an event handler, it is possible to get the mbed to do multiple things at the same time. You don't need an RTOS!

Many thanks, Paul