Buffered Serial with RTOS

Information

Special Thanks to Jim Hamblen and his Cookbook page on Serial Interrupts

This is a simple demo project, building off of the work of Jim Hamblen, showing how to first make a buffered serial library. The biggest boon here is that it is working with the RTOS in a thread, and instead of polling and waiting for the interrupts it blocks using semaphores and allows other threads to run while waiting on Serial input. Secondly, by encapsulating it in a library you are able to obtain device driver like abstraction of the serial interface. The thread looks like it is running simple pooling functions, where in reality it is blocking and waiting on interrupts and allowing other processes to run.

There is something about including the mbed RTOS that causes the serial interrupts to not work. Simply importing the mbed-rtos library in the project without even including it causes the software to hang in the serial rx interrupt. After much reading on the forums and the LPC1768 Users Manual and trying different things I got it to work. The library in the project only works with UART1 (p13, p14). Simply by searching through the library and changing all the UART1 references to the one you need will make it work with any other one. Below is the table. I'd like to find a more elegant solution, but as of now this works.

PinConnectionIRQn
p9TxD3UART3_IRQn
p10RxD3
p13TxD1UART1_IRQn
p14RxD1
p28TxD2UART2_IRQn
p27RxD2

The code in main.cpp is as follows:

main.cpp

#include "mbed.h"
#include "buffered_serial.h"
#include "rtos.h"
// Setup: connect p13 and p14 together to create a serial loopback
// Sends out ASCII numbers in a loop and reads them back
// If not the same number LED4 goes on
// LED1 and LED2 indicate RX and TX interrupt routine activity
// LED3 changing indicate main loop running
// if the program is working correctly you should see LED 1, 2, & 3 dimly lit and LED 4 off

BufferedSerial device(p13,p14);  // UART1 - p13, p14

DigitalOut led3(LED3);
DigitalOut led4(LED4);

// Line buffers for sprintf and sscanf
char tx_line[80];
char rx_line[80];

void serial_thread(const void *args)
{
    int i=0;
    int rx_i=0;
    device.baud(9600);

// Formatted IO test using send and receive serial interrupts
// with sprintf and sscanf
    while (1) {
// Loop to generate different test values - send value in hex, decimal, and octal and  then read back
        for (i=0; i<0xFFFF; i++) {
            led3=1;
// Print ASCII number to tx line buffer in hex
            sprintf(tx_line,"%x\r\n",i);
// Copy tx line buffer to large tx buffer for tx interrupt routine
            device.send_line(tx_line);
// Print ASCII number to tx line buffer in decimal
            sprintf(tx_line,"%d\r\n",i);
// Copy tx line buffer to large tx buffer for tx interrupt routine
            device.send_line(tx_line);
// Print ASCII number to tx line buffer in octal
            sprintf(tx_line,"%o\r\n",i);
// Copy tx line buffer to large tx buffer for tx interrupt routine
            device.send_line(tx_line);
            led3=0;
// Read a line from the large rx buffer from rx interrupt routine
            device.read_line(rx_line);
// Read ASCII number from rx line buffer
            sscanf(rx_line,"%x",&rx_i);
// Check that numbers are the same
            if (i != rx_i) led4=1;
// Read a line from the large rx buffer from rx interrupt routine
            device.read_line(rx_line);
// Read ASCII number from rx line buffer
            sscanf(rx_line,"%d",&rx_i);
// Check that numbers are the same
            if (i != rx_i) led4=1;
// Read a line from the large rx buffer from rx interrupt routine
            device.read_line(rx_line);
// Read ASCII number from rx line buffer
            sscanf(rx_line,"%o",&rx_i);
// Check that numbers are the same
            if (i != rx_i) led4=1;
        }
    }
}

// main test program
int main()
{
    Thread thread(serial_thread);
    
    while(1);
}

The buffered serial application I needed was to interface with a NMEA GPS that outputs serial lines of data, it has been tested and works for that as well.

Excerpt from GPS test project

Serial pc(USBTX, USBRX);
BufferedSerial gps(NC, p14);

void gps_thread(void const *args)
{
    char buffer[80];

    DigitalOut gps_led(LED4);

    gps.baud(4800);
    pc.baud(9600);

    while(true) {
        gps.read_line(buffer);
        gps_led = !gps_led;
        pc.puts(buffer);
    }
}

The buffered serial library works by using a curricular buffer. An rx interrupt is used to populate the buffer and a tx interrupt allows us to use the processor, pushing only as many characters as fit in the 16 byte FIFO buffer and then allowing other processes to run until we are ready to transmit again. Below is the .h file showing the structure of the BufferedSerial class:

buffered_serial.h

/*
    Buffered serial 1 - Setup to work with UART1 (p13,p14)
*/

#ifndef BUFFERED_SERIAL_H
#define BUFFERED_SERIAL_H

#define BUFFER_SIZE     255
#define LINE_SIZE       80
#define NEXT(x)         ((x+1)&BUFFER_SIZE)
#define IS_TX_FULL      (((tx_in + 1) & BUFFER_SIZE) == tx_out)
#define IS_RX_FULL      (((rx_in + 1) & BUFFER_SIZE) == rx_out)
#define IS_RX_EMPTY     (rx_in == rx_out)

#include "mbed.h"
#include "rtos.h"

class BufferedSerial : public Serial
{
public:
    BufferedSerial(PinName tx, PinName rx);

    void send_line(char*);
    void read_line(char*);

private:
    void Tx_interrupt();
    void Rx_interrupt();

// for disabling the irq
    IRQn device_irqn;
    
// Circular buffers for serial TX and RX data - used by interrupt routines
// might need to increase buffer size for high baud rates
    char tx_buffer[BUFFER_SIZE];
    char rx_buffer[BUFFER_SIZE];
// Circular buffer pointers
// volatile makes read-modify-write atomic
    volatile int tx_in;
    volatile int tx_out;
    volatile int rx_in;
    volatile int rx_out;

    char tx_line[LINE_SIZE];
    char rx_line[LINE_SIZE];

    DigitalOut led1;
    DigitalOut led2;
    
    Semaphore rx_sem;
    Semaphore tx_sem;
};

#endif

To get the serial interrupts to work with the RTOS all library calls had to be removed from the interrupts and the IIR flag had to be cleared.

Room for Improvement!

I am not satisfied with the logic in the read_line and rx_interrupt function and how it handles it's semaphore. If you see a more elegant solution please let me know.

buffered_serial.cpp

#include "buffered_serial.h"

BufferedSerial::BufferedSerial(PinName tx, PinName rx) : Serial(tx,rx), led1(LED1), led2(LED2), rx_sem(0), tx_sem(0)
{
    tx_in=0;
    tx_out=0;
    rx_in=0;
    rx_out=0;

    device_irqn = UART1_IRQn;

    // attach the interrupts
    Serial::attach(this, &BufferedSerial::Rx_interrupt, Serial::RxIrq);
    Serial::attach(this, &BufferedSerial::Tx_interrupt, Serial::TxIrq);
}

// Copy tx line buffer to large tx buffer for tx interrupt routine
void BufferedSerial::send_line(char *c)
{
    int i;
    char temp_char;
    bool empty;
    i = 0;
    strncpy(tx_line,c,LINE_SIZE);
    // Start Critical Section - don't interrupt while changing global buffer variables
    NVIC_DisableIRQ(device_irqn);
    empty = (tx_in == tx_out);
    while ((i==0) || (tx_line[i-1] != '\n')) {
        // Wait if buffer full
        if (IS_TX_FULL) {
            // End Critical Section - need to let interrupt routine empty buffer by sending
            NVIC_EnableIRQ(device_irqn);
            //while (IS_TX_FULL) ; // buffer is full
            tx_sem.wait();
            // Start Critical Section - don't interrupt while changing global buffer variables
            NVIC_DisableIRQ(device_irqn);
        }
        tx_buffer[tx_in] = tx_line[i];
        i++;
        tx_in = NEXT(tx_in);
    }
    if (Serial::writeable() && (empty)) {
        temp_char = tx_buffer[tx_out];
        tx_out = NEXT(tx_out);
        // Send first character to start tx interrupts, if stopped
        LPC_UART1->THR = temp_char;
    }
    // End Critical Section
    NVIC_EnableIRQ(device_irqn);
}

// Read a line from the large rx buffer from rx interrupt routine
void BufferedSerial::read_line(char *c)
{
    int i;
    i = 0;
    // Start Critical Section - don't interrupt while changing global buffer variables
    NVIC_DisableIRQ(device_irqn);
    // Loop reading rx buffer characters until end of line character
    while ((i==0) || (rx_line[i-1] != '\n')) {
        // Wait if buffer empty
        if (IS_RX_EMPTY) { // buffer empty
            // End Critical Section - need to allow rx interrupt to get new characters for buffer
            NVIC_EnableIRQ(device_irqn);
            //while (rx_in == rx_out) ; // buffer empty
            rx_sem.wait();
            // Start Critical Section - don't interrupt while changing global buffer variables
            NVIC_DisableIRQ(device_irqn);
        } else {
            rx_sem.wait();
        }
        rx_line[i] = rx_buffer[rx_out];
        i++;
        rx_out = NEXT(rx_out);
        
        // prevent overflow on rx_line
        if(i == LINE_SIZE) 
        {
            i--;
            break;
        }
    }
    rx_line[i++] = 0;
    // End Critical Section
    NVIC_EnableIRQ(device_irqn);
    strncpy(c,rx_line,i);
}

// Interupt Routine to read in data from serial port
void BufferedSerial::Rx_interrupt()
{
    uint32_t IRR1 = LPC_UART1->IIR;
    led1=1;
    while (readable() && !(IS_RX_FULL)) {
        rx_buffer[rx_in] = LPC_UART1->RBR;
        rx_in = NEXT(rx_in);
        rx_sem.release();
    }
    led1=0;
}

// Interupt Routine to write out data to serial port
void BufferedSerial::Tx_interrupt()
{
    uint32_t IRR = LPC_UART1->IIR;
    led2=1;
    while ((writeable()) && (tx_in != tx_out)) { // while serial is writeable and there are still characters in the buffer
        LPC_UART1->THR = tx_buffer[tx_out]; // send the character
        tx_out = NEXT(tx_out);
    }
    if(!IS_TX_FULL) // if not full
        tx_sem.release();
    led2=0;
}

Finally here is project listing:

Import programSerial_interrupts_buffered

Serial Interrupt Library with mbed RTOS for multi-threading

And the library listing: (remember this only works for UART1)

Import librarybuffered-serial1

UART1 buffered serial driver, requires RTOS


6 comments on Buffered Serial with RTOS:

11 Dec 2012

Quote:

Simply by searching through the library and changing all the UART1 references to the one you need will make it work with any other one. Below is the table. I'd like to find a more elegant solution, but as of now this works.

You can have a look at the MODSERIAL library, since it is done there. Doesnt help that the UARTs arent identical, but in principle you should be able to use the same method as is used there.

11 Dec 2012

I'll look more closely at the MODSERIAL library, but from reading the user manual it seems that UART1 is different from the other three and that's the one I needed for my project. The solution in MODSERIAL seemed rather involved with quite a few #define macros. I was looking for something more lightweight than MODSERIAL and that blocked using the rtos, that's why I wrote this.

Also, since the mbed libraries are being changed I was having quite a bit of difficulty getting MODSERIAL to work consistently the way I needed. I could get it to work with old versions of the mbed library but then that broke other parts of my project that relied on some of the more recent changes.

13 Dec 2012

Tyler, In an attempt to send a text line from pc to uart:

Serial pc(USBTX, USBRX); BufferedSerial uart(p13,p14); UART1 - p13, p14

The code gets stuck at: tx_sem.wait();

From: buffered_serial.cpp

Copy tx line buffer to large tx buffer for tx interrupt routine

void BufferedSerial::send_line(char *c)

{

int i;

char temp_char;

bool empty;

i = 0;

strncpy(tx_line,c,LINE_SIZE);

Start Critical Section - don't interrupt while changing global buffer variables

NVIC_DisableIRQ(device_irqn);

empty = (tx_in == tx_out);

while ((i==0) || (tx_line[i-1] != '\n')) {

Wait if buffer full

if (IS_TX_FULL) {

End Critical Section - need to let interrupt routine empty buffer by sending

NVIC_EnableIRQ(device_irqn);

while (IS_TX_FULL) ; buffer is full

tx_sem.wait();

Start Critical Section - don't interrupt while changing global buffer variables

NVIC_DisableIRQ(device_irqn);

}

......

...kevin

13 Dec 2012

Hu, it's working for me. When you say stuck do you mean it hangs or it doesn't get any data? Could you send me a copy of your project and I'll take a look at it and see if there is anything I see. Please use the code brackets, it makes it easier to read.

06 Feb 2013

I had the same problem. Everything was working fine until i add a new serial device.
The mbed-rtos library in my program is updated to the latest. I tried your library but the debugger shows " Not enough Stdlib mutexes ". Can't figure out why. Hope you can help out, thanks.

06 Feb 2013

Have a look at: http://mbed.org/forum/mbed/topic/3980/

At the bottom there the issue and a fix is discussed. You do have the latest library, since that error message is only added in the latest version, (before that it was unknown why it stopped working). I havent really done anything with this myself, but I think using that topic you can manually increase the amount of mutexes. (Dont ask me why you specifically require more than the default, maybe also other objects that inherit from Stream?).

Please log in to post comments.