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.
Pin | Connection | IRQn |
---|---|---|
p9 | TxD3 | UART3_IRQn |
p10 | RxD3 | |
p13 | TxD1 | UART1_IRQn |
p14 | RxD1 | |
p28 | TxD2 | UART2_IRQn |
p27 | RxD2 |
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:
Please log in to post comments.
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.