ATmega328 best practices for re-entrant interrupts

Go To Last Post
24 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I am writing some software that processes multiple types of events in realtime. Therefore, I have several interrupt service routines defined: UART_TX, UART_UDRE, ANALOG_COMP, and some timer comparisons. Occasionally, in response to an event which has triggered one of the interrupts, the software must do some fairly costly computation. For example, in response to the ANALOG_COMP event, a costly "rule matching" operation must sometimes occur, and this takes ~10 microseconds. This "rule matching" operation can occur a maximum of once per ~1400 microseconds.

 

Currently, my software is structured such that the interrupts execute as quickly as possible, placing some data in a buffer and then returning. The more costly processing happens in the course of the main loop. However, I would like the relevant processing to occur immediately after the triggering interrupt has occurred. Since the "heavier processing" occurs relatively infrequently, it seems appropriate to move this processing into the triggering interrupt. Of course, this would require that I allow re-entrant interrupts. So I have some questions about re-entrant interrupts and how best to handle them.

 

  • Is it correct that if I declare the ISRs with the ISR_NOBLOCK macro, interrupts are disabled briefly, but then re-enabled? What assembly is emitted as part of the ISR when using ISR_BLOCK versus ISR_NOBLOCK? I see that I can also declare the ISR with ISR_NAKED. This way, am I able to never suspend processing of interrupts, as opposed to disabling and then immediately re-enabling the global interrupt enable flag as is the case with ISR_NOBLOCK? The ave-libc docs say, "the user code is responsible for preservation of the machine state including the SREG register, as well as placing a reti() at the end of the interrupt routine?" Is everything that I need to do to "preserve the machine state" included in the assembly emitted as part of the ISR when using ISR_BLOCK or ISR_NOBLOCK?
  • I have been cautioned against immediately re-enabling interrupts in certain ISR vectors, such as the UART interrupts. If I set (or never disable) the global interrupt enable flag before reading UDR0, for example, will my code recurse infinitely? If so, is the way to handle this by disabling the relevant UART interrupts, then enabling interrupts globally? Is there a similar infinite loop gotcha with the analog comparator or timer interrupts?

 

 

Any advice on this would be greatly appreciated.

 

 

Zane Kaminski

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Welcome to the Forum.

 

I'm sure others have far more experience with this than I, but I'll make a comment or two.

 

If you switched to the Xmega series you would have a priority interrupt controller.

You could set the serial comm's as a high priority interrupt and the Anal Comp as a low level interrupt.

That way, the Anal Comp could execute away within its ISR, and if one of the other interrupts came in, say the serial comm's, it would automatically interrupt the interrupt, (on the next instruction cycle).

No that that way you never miss any serial comm's data.

That would all happen in hardware, and no tricky coding required by you!

 

Stuck with a Mega?

You could always break the Main Loop processing into a bunch of separate tasks that are sequentially executed.

In each sub-task you exit if there is no new data to process.

And, before executing a new sub-task, you always go look to see if the Anal Comp ISR Flag is set.

If so, you can go execute that task next, oblivious to the sub-task que.

The Anal Comp processing would then happen shortly after the Flag was set, depending upon how long each sub-task takes, and where within the current sub-task one was when the Anal Comp ISR flag was set.

The variable jitter may or may not be an issue to you, compared to a more fixed response timing.

 

Gotta love the Xmega.

It makes some of these otherwise somewhat complex tasks trivial.

 

JC 

 

Edit:

First I edited a typo.

Then I added to a sentence.

Then I fixed the typo again.

 

I give up.  The editor / Forum Software seems a bit flaky at the moment... or even recursive

 

JC

Last Edited: Mon. Oct 5, 2015 - 11:26 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

One technique i've used is to test the interrupt flags of the sources of interest whilst in an isr. This saves the entry/exit overhead of another ist call/ret.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Since the "heavier processing" occurs relatively infrequently, it seems appropriate to move this processing into the triggering interrupt.

Personally, I'd avoid nesting interrupts on an AVR8.  10us "hit" is ~1% of total CPU.  Consider the microseconds that add up with the nesting situation.

You can put lipstick on a pig, but it is still a pig.

I've never met a pig I didn't like, as long as you have some salt and pepper.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Thank you everyone for the replies. DocJC, this is the approach I am currently using. As for the Xmega, this project is very cost-sensitive, so I am hoping to get by with only the 328P.

 

Kartman, are you suggesting that I check to see if another interrupt has been triggered while I am in the first interrupt that is executing?

 

theusch, what do you mean by "10us 'hit' is ~1% of total CPU?" The reason I am trying to avoid the 10us delay is because some of the timings are sensitive to ~7us or so, so I was thinking that the nested interrupts would be the way to go, otherwise I would have to make sure to get the main loop aligned in time with the input data that has the sensitive timings. I only have a few ISRs that I plan to write, and this approach would eliminate the main loop part of the code, so it seems reasonable enough to maintain.

When you say, "consider the microseconds that add up with the nesting situation," do you mean that it takes more than a handful of instructions worth of time to begin executing the interrupt? Or do you mean that some delay becomes larger every time a new nested interrupt occurs?

 

Again, thanks everyone for your replies.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Yes.

It takes time to enter/exit the isr. Apart from the stack/unstack of the PC, there is up to 33 registers that may be stacked/unstacked.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

theusch, what do you mean by "10us 'hit' is ~1% of total CPU?"

You said your 10us event happens every 1400us.  About 1% of the CPU.

 

I'm saying just do the processing in the ISR.  Without nesting interrupts.

 

Re the 7us needed response time--hmmm, that is a tough one.  Awfully fast needed response, especially if it takes 10us to come up with the answer. 

 

I guess that in the end, as always, "it depends".

You can put lipstick on a pig, but it is still a pig.

I've never met a pig I didn't like, as long as you have some salt and pepper.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

 

The hardware disables interrupts when an interrupt is handled.

Only software enables interrupts.

I expect that ISR_NOBLOCK will enable interrupts as soon as possible in the code,

i.e. before pushing registers.

ISR_NAKED add the naked attribute.

Unless I needed a static variable, I'd use a .S file.

How much to you love inline assembly?

info gcc wrote:
`naked'
     Use this attribute on the ARM, AVR, IP2K and SPU ports to indicate
     that the specified function does not need prologue/epilogue
     sequences generated by the compiler.  It is up to the programmer
     to provide these sequences. The only statements that can be safely
     included in naked functions are `asm' statements that do not have
     operands.  All other statements, including declarations of local
     variables, `if' statements, and so forth, should be avoided.
     Naked functions should be used to implement the body of an
     assembly function, while allowing the compiler to construct the
     requisite function declaration for the assembler.
You do not have to stick to inline assembly,

but if you do not, you will need to examine the .lss file to discover whether you got the code you wanted.

 

I suspect a more thorough analysis is on order.

What do you need to compute?

When do you need it?

When is the data available?

Clearly more that 10us is available.

Otherwise any nested interrupt would cause the result to be too late.

 

One thought is to use polling instead of an ISR.

Test the interrupt flag in the main loop.

If it's set, clear it and do the computation.

Moderation in all things. -- ancient proverb

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

theusch and skeeve, in the worst case, I have about 54us between when I have all of the data I need to do the computation and when I need to perform some actions using the result (basically just turning a pin to high for 33us). sleeve, thank you for clarifying what happens to the interrupt enable flag when an interrupt is handled. I will go ahead and incorporate this info into the design of my software.

 

Thanks everyone for your advice.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I don't know what you are doing with the ADC, but one way to avoid the needed ISR would be to run it in continues, or just read the isr flag (and data ) from the main loop.(or an already existing timer ISR).

 

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I also have a program which has to respond to interrupts very fast, while processing the resultant data can take a up to a few msec

The solution was to write a pair of functions. enqueue(void *) enqueues a prioritised event and pointer. The main program scans the queue and dispatch(void *) dispatches the event handler function for the first event with the highest priority. It only needs about 3microsec at 18.432MHz clock for enqueue, scan and dispatch.

This is extremely primitive - it does not save the context, one cannot preempt an event handler, the order of execution is indeterminate and an event handler can hang the program if it does not run to completion. The WDT helps here, but only a program reset is possible.

The enqueue() function can be called in an ISR or in an event handler

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Does the Free RTOS have specs on task switching times? That handles all the context switching and interrupt nesting in a configuration managed tested module. What could possibly go wrong? gO wRoNg? GOWRONG? g..g...g...go wrong?

 

Imagecraft compiler user

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

As for the Xmega, this project is very cost-sensitive, so I am hoping to get by with only the 328P.

 

I don't do production projects, as I am mostly a hobbyist, and occasionally do one-off prototypes for people.

Quantity 10 would be a "big number" for me.

 

That said, I still fine your reluctance to switch to an Xmega to be perplexing.

 

As I noted above, it already contains a priority interrupt handler which makes your current problem a non-issue.

 

It runs at 32 MHz, so you have ~ twice the processing power to process your time critical data crunching routines.

 

It costs less.  (At least at DigiKey, in published numbers; perhaps the price ratios are flipped in the 10K-100's of K range, or not).

 

DigiKey:

M328  50 @ $2.19, 2000 @ $1.83

X32E5 50 @ $2.02, 2000 @ $1.55

 

Just saying...

 

JC

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

bobgardner wrote:

Does the Free RTOS have specs on task switching times? That handles all the context switching and interrupt nesting in a configuration managed tested module. What could possibly go wrong? gO wRoNg? GOWRONG? g..g...g...go wrong?

RTOS has to push all 31 registers and some of the task status onto the local stack, and restore them on return. This takes about 80 clock cycles for each, about 9microsec at 20MHz clock, and you haven't processed any data yet. It's nice to use, but a bit slow for this application

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

80 instructions of 50ns is 4usec. Is that just the push, or the push and pull? So if the rtos uses 4 out of 10 usecs, it still runs right?

 

Imagecraft compiler user

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

bobgardner wrote:

80 instructions of 50ns is 4usec. Is that just the push, or the push and pull? So if the rtos uses 4 out of 10 usecs, it still runs right?

 

Sorry to say that your time is wrong, PUSH and POP each need two clock cycles, not one, so 80 instructions need 8µsec. 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Thanks. Guess I'll Never Forget that now.

 

Imagecraft compiler user

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

If, for some reason, you really are stuck with an atmega328,

another possibility is to do time-critical stuff in the ISR and when

necessary set a flag for the main loop to do the 10us/54us stuff.

Moderation in all things. -- ancient proverb

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Since this is a purpose designed program, and we have shelved my idea of using the rtos because it aint fast enough, I will offer that I did in fact incorporate a 4usec interrupt into a video refresher. I used a couple tricks.... use GPIOR0 and 1 for fast register variables... no load and store to ram, and there is a setting in the imagecraft compiler to reserve R20,21,22,23 for global variables. There is a special set of libraries that are compiled with this option so those regs are not used. Don't call any subroutines in the handler so the compiler will only push and pull the regs it uses. I think some old silverback told me a decade or so ago 'the darn gcc compiler will do anything your imagecraft compiler will do', so forgive me for suggesting using a different compiler from the one chosen by the marketing dept in Norway. Maybe they got it for a great price. Also, forgive me in advance for summarizing this obvious stuff, but a nice summary/checklist of speedemup tricks might help some new guys.

 

Imagecraft compiler user

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The best thing you can do is first to find out how long time each thing take in worst case, (Here we don't care about avg). 

I do know that you don't 100% know with a compilers code, but if you structure the code so it never do a lot in one ISR, but spread it out, 

Then you will have some code blocks you can play with like LEGO, an see when it's best to do things.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

this would require that I allow re-entrant interrupts.

What an interesting use of the term re-entrant.

 

Usually you use re-entrant to describe foreground code (printf - that kind of thing) and whether it is safe to be called from two or more separate threads of execution at the same time. The point really is whether it holds any kind if state information in any non-per-instance storage.

 

Say for example, that printf uses some kind of "module global" buffer in which to build its output string before it is fed to stdout then if printf() is called and it starts to build such output in that buffer but then this instance is interrupted (or a task switch occurs or whatever) and then the next thread of execution also calls printf() which uses the buffer does it end up corrupting the already half-constructed output the interrupted copy was already building? If "no" the function is re-entrant. If yes it is not re-entrant - that is you cannot enter it if an instance is already in operation.

 

So if you really mean re-entrant here what "stored state" is it that you are concerned about?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

clawson wrote:

this would require that I allow re-entrant interrupts.

What an interesting use of the term re-entrant.

 

Usually you use re-entrant to describe foreground code (printf - that kind of thing) and whether it is safe to be called from two or more separate threads of execution at the same time. The point really is whether it holds any kind if state information in any non-per-instance storage.

I think it's fairly clear that that is not what OP had in mind.

"Interruptible interrupt" would have been accurate terminology.

Moderation in all things. -- ancient proverb

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

One problem that I had a while ago: Program model was main loop, and using interrupts for serial, timer tic, and I had a bunch of pots to read on an external 12 bit spi a/d, and I had something else on the spi, a display or dac or something. The instant I started calling readnextadchan() in the fast timer tic, it stomped all over the routine that was trying to use the spi to send something out from the main loop. Seems obvious now, but it took a while to debug/figureout. Starts delving into the realm of operating systems and allocating resources. So watch for that. You dont want to change something in an interrupt that might be in the process of being changed in the main program.

Imagecraft compiler user

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I need to do the same thing, that is, in an interrupt routine, immediately enabling further interrupts.

Actually though it is only one interrupt that I want very response for. So I don't allow that one to be further interrupted.

Moreover, you can write a  MACRO called  "ALLOW_ONLY_INTERRUPT_X".  It basically saves all the interrupt enable registers

(about four bytes) and then sets the interrupts you don't want to happen to disabled. Then upon exit you have to use a RESTORE_INTERRUPT_STATUS macro to put the interrupt enables back to where they were, with interrupts inhibited with a CLI, then you do RETI.