interrupt based on serial input

Status
Not open for further replies.

virtualdave

Well-known member
I need some advice / pointers in dealing with interrupts when a particular value comes in one of the serial ports.

Quick overview: I have lots of sensor data coming in via Serial1. ~100 bytes @ 921600 baud @100Hz. Takes about 1.3ms to read that data. Then LOTS of data processing to produce some magic numbers. On serial2 I am listening for a message from another board. It's 9-bit data (yeah Paul & Nick for the 9-bit serial option) via rs485. When I get a message addressed for me, I am to report some those magic numbers immediately. Problem is this could be delayed by up to 1.3ms if I am tied up reading the data on Serial1.

Question: What I need to have my sketch do is when I receive 0x0109 on serial2, stop everything, listen for the remaining 3 ints to check what I am being asked to do, pass along 6 bytes, then return to what I was doing. I'm assuming I need to edit serial2.c (or maybe serial1.c to stop it?) to make this happen (I've already added some extra code to serial2.c to handle the enable pin for rs485, so I'm not afraid to make these edits...now :). I just know very little about interrupts (or if this is even the right path?) and would love some gentle nudges on how I might go about tackling this.

Here's a whittled down sketch that shows how things are set-up in my main sketch.

Thanks,
David

Code:
byte gesture = 0;

byte vnavIn;
const int vbuff_size = 120;
byte vbuff[vbuff_size];
byte vbuff_idx = 0;

// RS-485 Transmit variables
byte gTxBuffer[8];

// RS-485 Receive variables
int gRxByteCount = 0;
int gRxBuffer[16];


//*****************************************************************************
void setup() {


  Serial2.begin(38400, SERIAL_9N1);
  Serial.begin(38400);   
}

//*****************************************************************************
void loop() {
  
byte b;
int c;
 
  // =========== Task 1 ==========
  // Service the serial1 sensor data
 while (Serial1.available() > 0) { // mail call 
    vnavIn = Serial1.read();
    vbuff[vbuff_idx] = vnavIn;   
    if (vnavIn == 10) { // = line feed = end of packet from vn100
      vnavIn = 0;
      doSomeMagic();
      vbuff_idx = 0; // reset vn100 buffer index and get ready to receive next packet
    }
    else { // keep filling the buffer, more data coming in
      vbuff_idx++;  
    }
  } // end vn100 check
  
  // =========== Task 2 ==========
  // Service incoming RS-485 serial data
  while (Serial2.available()) {
    
    // Read a 9-bit byte
    c = Serial2.read();

    // If we're waiting for the start of a new messaqe
    //  then the first 9-bit byte must be 0x0109    
    if (gRxByteCount == 0) {
      if (c == 0x109) {
        gRxBuffer[0] = c;
        gRxByteCount++;
      }
    }
    
    else { // keep filling the buffer, more data coming in
      gRxBuffer[gRxByteCount++] = c;
      
      // If the received bytecount is 4, then we have an
      //  entire message. We should check the values to
      //  validate, but let's just assume it's correct.
      if (gRxByteCount >= 3) {
        
        // reset the byte count
        gRxByteCount = 0;
        
        // Pause to let RS-485 settle, then send the response
        //  with the current gesture number
        delayMicroseconds(10);
        gTxBuffer[0] = gesture;
        gTxBuffer[1] = 0;
        gTxBuffer[2] = 0;
        gTxBuffer[3] = 0x0055;
        Serial2.write(gTxBuffer, 4);
        
      } // if (gRxByteCount >= 4)
      
    } // if (gRxByteCount != 0)
    
  } // while (Serial2.available()) 
   
}

void doSomeMagic() {
  // just a bucket for the actual data processing
  gesture++;
  if (gesture > 99) {
    gesture = 0;
  }
}
 
Last edited:
Hi Steve,
It's actually fine. I read 100 byte @ 100Hz, process 28 gestures (or possible gestures) and push a bunch of debugging info out the serial port in about 3-4ms, leaving 6 or so ms to make coffee, wash the car, etc. That part isn't a problem (and neither is reading data coming in serial2). Just trying to tighten up the time between when a request is received at the serial2 port and when I send the response.
David
 
100 bytes x 100 per second = 10K bytes/sec, via UART 8N1 ~ 100Kilobits/sec.
On an ARM7 with a FIFO'd UART, I used 115,200 baud but the data arrival was bursts of bytes. I measured/estimated the average was about 60% utilization. It worked OK. Now and then my code would fall behind due to other work the CPU had to do on the concurrent Ethernet interface. But the nature of the data was that it wasn't perishable and the sender would re-transmit if need he, as the application protocol had a reliable datagram method despite being UART based.

Using 2/3 of the hardware FIFO was key to keeping the interrupt rate lower, and the interrupt handler surely read all FIFO'd bytes in one interrupt. And vice-versa, for UART TX which was also concurrent and high rate. The TX FIFO was filled in one interrupt.

I used FeeRTOS for all of this, with about 8 tasks running on a humble (by today's standards), 128KB flash ARM7 at 72MHz.
Code was all pretty lean C, no C++.
 
On Teensy 3.1, using the ordinary Arduino functions (not FreeRTOS), the overhead is nowhere near 60% CPU.

In fact, I just did a very quick test with this sketch:

Code:
void setup() {
  pinMode(3, OUTPUT);
  Serial1.begin(115200);
}

void loop() {
  if (Serial1.available()) {
    char c = Serial1.read();
  }
  digitalWriteFast(3, HIGH);
  if (Serial1.available()) {
    char c = Serial1.read();
  }
  digitalWriteFast(3, LOW);
}

The idea is pin 3 will toggle rapidly when the CPU is free. When time is spent in the interrupt, or more time is spent inside Serial1.available() or Serial1.read(), the time between changes on pin 3 will lengthen. Yeah, it's a pretty crude and simple test, but I only spent a couple minutes doing this very quickly.

Here's what it looks like on my oscilloscope, with data arriving at the maximum rate:

scope_5.png
(click for full res)

Those little gaps are where the CPU spent extra time. You can see they're about 340 us apart, which corresponds well with 4 bytes at 115200 baud. The Serial1 driver configures the 8 byte FIFO with a watermark of 4 bytes.

Here's a zoom in to one of those little gaps:

scope_6.png
(click for full res)

The long time high is the interrupt that receives 4 bytes into the buffer. Then it's followed by 4 longer-than-normal low and high times, for the CPU overhead of Serial.available() and Serial.read() fetching each byte.

If we count the looping overhead, the total CPU busy time is about 4.5 divisions, or 4.5 us since it's 1 us/div at this scale. If we take out the looping overhead, which is normally about 1.2 us for 2 pulses high and low, that puts the CPU overhead estimate at 3.3 us.

Since this overhead, somewhere between 3.3 to 4.5 us, occurs every 340 us, the CPU overhead for Teensy 3.1 to receive sustained 115200 baud serial data is between 0.97% to 1.32%
 
would love some gentle nudges on how I might go about tackling this.

Playing with interrupts always comes with the complexity of properly sharing data and other thorny issues. Know this before you begin on this path.....

To improve your odds, I'd suggest commandeering one of the unused interrupts, configured for a low priority, for your own code. You could also just put your code directly inside the serial2.c, but that carries a lot more risk of disturbing timing for those higher priority interrupts.

First, choose an interrupt you know the rest of your program will never need. In this message, I'll use the RTC alarm. Any unused interrupt will do.

In setup(), you'll need to configure that interrupt for a low priority, and enable it, like this:

Code:
  NVIC_SET_PRIORITY(IRQ_RTC_ALARM, 208);
  NVIC_ENABLE_IRQ(IRQ_RTC_ALARM);

The available priorities are: 0, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240. Smaller numbers are higher priority.

Inside the serial2.c interrupt, use this to cause your function to run:

Code:
  NVIC_SET_PENDING(IRQ_RTC_ALARM);

This causes the RTC alarm interrupt be become pending, the same as would happen if you were actually using the RTC and had the alarm configured and it triggered. But you don't need to have the RTC in use, or even the 32.768 kHz crystal.

The huge advantage of doing it this way is the serial2.c interrupt will finish normally with minimal disruption. Your interrupt will run later, when no higher priority interrupts need to run, but before returning to the main program.

If you had set your interrupt to a higher priority (lower numerical priority value), NVIC_SET_PENDING would cause it to immediately run, and it would prevent the serial interrupts from being able to move data while you code does its work. Obviously you don't want that. It's important to get the relative priorities right.

NVIC_SET_PENDING can manually cause any interrupt to happen. You could cause havoc if you use this on interrupts where the handlers assume the hardware actually caused the interrupt, so be careful.

Then just put the RTC alarm interrupt handler into your program.

Code:
void rtc_alarm_isr(void)
{
   // do something here...
}

Because the priority is low, but higher than your main program, your code will run before your main program continues. The ARM processor even has an optimization called "tail chaining" where it basically jumps right to the lower priority interrupt (rather than needlessly restoring all the registers and then saving them again). If more data keeps flowing on Serial1 and Serial2, and other stuff keeps happening (USB, timers for millis, other libraries, etc), those other higher priority interrupts will keep working normally. That's the magic of nested priority interrupts. This gived you the best odds of making things work without causing a deadlock or hurting the performance of those well tuned & tested serial interrupts and any other interrupt-based libraries you might be using.

Of course, even at a low priority on a commandeered interrupt, your code is interrupting your main program. If you exchange any data with your main program, like reading those "magic numbers", you still have to do all the usual work to safely share data. Usually the minimum is at least 2 things. #1: The data needs to be "volatile", and #2: your main program generally needs to disable interrupts while accessing the data, so your interrupt code can't run at exactly the wrong moment and see a partially updated copy.

The IntervalTimer page as more info about these issues with sharing data with interrupts:

http://www.pjrc.com/teensy/td_timing_IntervalTimer.html
 
Last edited:
Hello,
From my humble software programmer experience, using interrupts does not change processing time... It only change the order in wich code is executed (and adds a little overhead for managing interrupts and accessing shared data).
If you problem can be solved by changing the order of execution maybe you can just change the order without using interupts!

For instance, you could try to replace the "while(serialX.available)" by "if( serialX.available)".

I don't know if that would change anything with your actual baud rates but it is easy to try... ;)

My idea is to replace your logic:
- if something is comming on serial 1, buffer all pending bytes (and process message if complete)
- if something is comming on serial 2, buffer all pending bytes (and process message if complete)
By:
- if something is comming on serial 1, buffer one byte
- if something is comming on serial 2, buffer one byte
- if message2 is complete reply and empty buffer
- if message1 is complete process and empty buffer
 
Thank you all for the input! (and Paul: your explanations are always outstanding...I learn a ridiculous amount about the t3 and micros in general from reading all your posts...the time you spend on these is VERY much appreciated).

As was the case before, seems like I might be in for a lot of pain if I venture down this rabbit hole. The process I would need to interrupt when a message comes in on Serial2 is the reading of the data on Serial1. That's where my delay in responding to the inquiry is coming from. My set-up is part of a larger rs485 ecosystem where a master controller is pinging all the various devices for their current state, so even the (up to) 1.3ms delay in my response can be an issue since the master controller can't wait that long without causing issues elsewhere in the system. Plus interrupting the reading of the data possibly 33 times a second (the rate at which these inquiries come in) would probably have an adverse affect on the gesture processing since much of it is time-based.

So since I have the space for it...what I think is best is to let this t3 continue doing what it does as far as the reading & processing of the data, then within every cycle it passes the 3 bytes defining its state out to another micro whose sole purpose is to read these 3 bytes from the gesture micro and respond to requests from the rs485 network. For now I'll use another t3 (talk about overkill) since I have the 9-bit serial communication working well with the t3 and need 2 hardware serial ports.

Thank you all again for your help.

David
 
Last edited:
First, choose an interrupt you know the rest of your program will never need. In this message, I'll use the RTC alarm. Any unused interrupt will do.
Code:
  NVIC_SET_PRIORITY(IRQ_RTC_ALARM, 208);
  NVIC_ENABLE_IRQ(IRQ_RTC_ALARM);
Inside the serial2.c interrupt, use this to cause your function to run:
Code:
  NVIC_SET_PENDING(IRQ_RTC_ALARM);
Wow I didn't know that I could do that! So I see in the teensy3.1 "MK20DX256" has a few "unused_isr" that would make that real nice to use for this if that would work? Even if not, knowing this really helps me out!!!!

I'm working on DMA Serial library that will let you send and recieve in the "background" for problems like you have. I'll post it once i'm comfortable it works good.
 
On Teensy 3.1, using the ordinary Arduino functions (not FreeRTOS), the overhead is nowhere near 60% CPU.
My 60% number was the percentage of serial data link capacity that was used at 115200 baud. This was for an XBee S1 which was receiving an average of about 60 messages per second, were each message was data wrapped in the XBee binary API data frame format.

The Ethernet interface in SPI was an issue, as there was a non-interrupt driven, but w/interrupts enabled, loop to transfer 100's of data bytes to/from the Wiz5100 chip. I just let other interrupts interrupt the SPI transfer loop. That included UART TX/RX FIFO full/empty, recurring timer ticks, and W5100 chip interrupts due to arriving packets.

The problem with transferring to/from UARTs without using interrupts is a potential for CPU hogging in a cooperative or preemptive multi-tasking system.
 
Last edited:
Status
Not open for further replies.
Back
Top