What isn't compiler allowed to do with cli() and sei()?

Go To Last Post
17 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0
cli();
// lots of code
sei();

From what I've read, the compiler is allowed to move
the sei instruction to immediately after the cli instruction.
Under what circumstances, if any,
can one do something reliable with cli() and sei()?
Is the comma operator required to work reliably?

( ({ cli(); 0 }),
  ({ {
       // lots of code
     } 0; }),
  ({ sei(); 0 })
);

In the preceding mess,
is gcc allowed to move the sei instruction before other instructions?

"Demons after money.
Whatever happened to the still beating heart of a virgin?
No one has any standards anymore." -- Giles

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

First, I'd look at atomic.h .
https://www.avrfreaks.net/index.p...

I've never heard of this situation. As SEI and CLI are a single instruction it is hard to see why it would be advantageous for the compiler to move them around.

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

I suspect the compiler is trying to keep the interrupts-disabled time to an absolute minimum, so that interrupt services are burdened with the least amount of extra latency possible. I think if you checked, you'd find that there's a builtin delay between when a SEI instruction executes and when interrupts are actually allowed to happen (probably there to guarantee that a RETI instruction fully executes or something). So, if the instruction being protected fit within that delay, it could simply be preceded by a "CLI; SEI;" sequence

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

Quote:
SEI instruction executes and when interrupts are actually allowed to happen (probably there to guarantee that a RETI instruction fully executes or something).

There is guaranteed to be at least 1 instruction run after a RETI or a SEI.

Regards,
Steve A.

The Board helps those that help themselves.

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

theusch wrote:
First, I'd look at atomic.h .
https://www.avrfreaks.net/index.p...
I've read atomic.h and the linked topic and similar topics.
Reading similar topics was what inspired this one.
atomic.h is fairly complex.
It took me a while to wrap my brain around it.
Do all reliable uses of cli() and sei() require
the use of "memory" in a clobber list?
I'm not trying to dispute the utility of atomic.h.
I'm trying to understand what makes it necessary.
Given that it is necessary,
I'm trying to discover the point of even having sei() and cli().
Quote:
I've never heard of this situation. As SEI and CLI are a single instruction it is hard to see why it would be advantageous for the compiler to move them around.

"Demons after money.
Whatever happened to the still beating heart of a virgin?
No one has any standards anymore." -- Giles

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

Quote:

atomic.h is fairly complex.
It took me a while to wrap my brain around it.

Given that, is the >>use<< of it complex? Or are there cases where the implementation causes undesirable side effects?

Quote:

I'm trying to discover the point of even having sei() and cli().

I guess you've lost me. Are you referring to the SEI and CLI AVR op codes? Or the provided implementation via sei() and cli()?

If you don't have sei() or some other way to carry out SEI, how can you ever write an interrupt-driven AVR app? I suppose you could call an ISR that ends with an RETI but that sounds a bit contrived.

While I always just CLI/SEI around my fetches of ISR data, some others will disable that interrupt source instead.

[the tricky one I ran into a while back and solved with the help of 'Freaks was the power-down race with the sleep instruction, and the safe way is to count on the one instruction (sleep) being carried out after an SEI.]

Lee

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

theusch wrote:
Quote:

atomic.h is fairly complex.
It took me a while to wrap my brain around it.

Given that, is the >>use<< of it complex? Or are there cases where the implementation causes undesirable side effects?

Quote:

I'm trying to discover the point of even having sei() and cli().
I wrote:
Given that it is necessary,
I'm trying to discover the point of even having sei() and cli().
theusch wrote:
I guess you've lost me. Are you referring to the SEI and CLI AVR op codes? Or the provided implementation via sei() and cli()?
What part of "sei()" or "cli()" don't you understand?
theusch wrote:
If you don't have sei() or some other way to carry out SEI, how can you ever write an interrupt-driven AVR app? I suppose you could call an ISR that ends with an RETI but that sounds a bit contrived.

While I always just CLI/SEI around my fetches of ISR data, some others will disable that interrupt source instead.

As I usually write in C, I usually use cli() and sei().
So far, it has always worked.
From what I have read, I have just been lucky.
gcc is allowed to move them around a lot.
So far as I can tell,
there is no rule that would keep the compiler from
putting the SEI instruction before the CLI instruction.
theusch wrote:
[the tricky one I ran into a while back and solved with the help of 'Freaks was the power-down race with the sleep instruction, and the safe way is to count on the one instruction (sleep) being carried out after an SEI.]

"Demons after money.
Whatever happened to the still beating heart of a virgin?
No one has any standards anymore." -- Giles

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

Quote:

What part of "sei()" or "cli()" don't you understand?

This part:
Quote:

I'm trying to discover the point of even having sei() and cli().

As I said,

Quote:

If you don't have sei() or some other way to carry out SEI, how can you ever write an interrupt-driven AVR app?

=================
Quote:

So far as I can tell,
there is no rule that would keep the compiler from
putting the SEI instruction before the CLI instruction.

that's the part that puzzles me. I generally understand where you are coming from: An optimizing compiler is free to re-order stuff. [Is that close to the point?]

Wouldn't those accesses be to a "volatile" I/O register, and then the compiler needs to keep them in order? But I guess that wouldn't it prevent it from changing cli()--stuff--sei() into cli()--sei()--stuff as you mentioned earlier.

Back in a previous life it was common to have memory-mapped peripheral functions, that had nothing to do with an actual memory location. An AVR analogy might be the R-W line on a character LCD. If the compiler can do as much re-ordering with (volatile) I/O addresses as you imply, then how can we ever do something like strobe the EN in a wait-for-ready loop changing the direction of the LCD data port (or at least that bit)? Bits of PORTx and PINx will be accessed the same way as the I bit of SREG, right? Why wouldn't/doesn't the compiler reorder those sequences? (Or maybe it does, and I'm just lucky?)

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

if cli/sei are declared as "asm volatile", they will not be re-ordered or re-located. If they are not, the compiler could shift them, but is unlikely to do so, because it cannot easily determine what the side-effects would be.

Writing code is like having sex.... make one little mistake, and you're supporting it for life.

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

glitch wrote:
if cli/sei are declared as "asm volatile", they will not be re-ordered or re-located. If they are not, the compiler could shift them, but is unlikely to do so, because it cannot easily determine what the side-effects would be.
They are "asm volatile" and sometimes the compiler does shift them:
See Jevin's post in https://www.avrfreaks.net/index.p...
See stu_san's post in https://www.avrfreaks.net/index.p...
I even saw an example, but I couldn't find it again.
atomic.h appears to contain the only reliable uses of cli() and sei(),
but atomic.h doesn't really need them.
They are adjacent to inline assembly and could easily have been absorbed.
So far, I've used cli() and sei() as though
they were guaranteed to stay put.
So far, I haven't been bitten.

"Demons after money.
Whatever happened to the still beating heart of a virgin?
No one has any standards anymore." -- Giles

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

Yes, you need to add the "memory" option to the clobber list of the ASM volatile statement, to indicate that the compiler may not reorder the commands. That's how part of the magic of atomic.h works.

- Dean :twisted:

Make Atmel Studio better with my free extensions. Open source and feedback welcome!

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

abcminiuser wrote:
Yes, you need to add the "memory" option to the clobber list of the ASM volatile statement, to indicate that the compiler may not reorder the commands. That's how part of the magic of atomic.h works.
cli()...sei() usually works.
How can one tell that atomic.h macros do better?
Neither cli() nor sei() claims to change a register or memory.
Why would '__asm__ volatile ("" ::: "memory");'
be a barrier their reordering?
In abcminiuser's example,
what rule keeps the compiler from putting atomic.h's SEI
and CLI instructions anywhere in the body of the do loop?
What rule keeps the compiler from putting
the first SEI instruction after the do loop?
#include 
#include 
#include 
#include 

volatile uint16_t ctr;

ISR(TIMER1_OVF_vect)
{
  ctr--;
}

...
int
main(void)
{
   ...
   ctr = 0x200;
   start_timer();
   sei();
   uint16_t ctr_copy;
   do
   {
     ATOMIC_BLOCK(ATOMIC_FORCEON)
     {
       ctr_copy = ctr;
     }
   }
   while (ctr_copy != 0);
   ...
}

It seems to me that what one needs are

#define cli_mem() __asm__ __volatile__ ("CLI" ::: "memory")
#define sei_mem() __asm__ __volatile__ ("SEI" ::: "memory")

"Demons after money.
Whatever happened to the still beating heart of a virgin?
No one has any standards anymore." -- Giles

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

skeeve wrote:
Neither cli() nor sei() claims to change a register or memory.
Why would '__asm__ volatile ("" ::: "memory");'
be a barrier their reordering?

Because thy are declared as:

# define sei()  __asm__ __volatile__ ("sei" ::)
# define cli()  __asm__ __volatile__ ("cli" ::)

Asm statements declared volatile cannot be reordered relative to themselves or to accesses to volatile variables.

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

From the original discussion thread in the avr-libc development lists:

Quote:
The main problem is that the generated code doesn't always do what is
expected. Something like:

> uint16_t counter1, counter2;
>
> ...
>

> int main(void)
> {
> uint16_t a, b;
>
> ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
> a = counter1;
> b = counter2;
> }
>
> PORTB = a;
> PORTC = b;
>
> return 0;
> }

compiles to (using gcc 4.2.0):

> ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
> 64: 8f b7 in r24, 0x3f ; 63
> 66: f8 94 cli
> 68: 8f bf out 0x3f, r24 ; 63
> a = counter1;
> b = counter2;
> }
>
> PORTB = a;
> 6a: 80 91 60 00 lds r24, 0x0060
> 6e: 88 bb out 0x18, r24 ; 24
> PORTC = b;
> 70: 80 91 62 00 lds r24, 0x0062
> 74: 85 bb out 0x15, r24 ; 21

Which shows why the memory barrier is needed - without it the compiler will always do the SEI after the CLI, but doesn't always put the code shown inbetween them between the two, as they aren't specifically dependant on the volatile operations.

- Dean :twisted:

Make Atmel Studio better with my free extensions. Open source and feedback welcome!

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

What Dean does in the code above, is this:
"Dear compiler, I have two variables, counter1 and counter2. I promise, they won't be changed outside of program flow, or have effects outside of program flow. That is, they are not volatile, and you may thus move them across volatile operations all you like."

Then in the ATOMIC_BLOCK macro, he says:
"Here is a CLI. It is a really special CLI instruction that needs everything committed to memory to function, and it may clobber every memory variable in the machine. Because the declaration above is actually incorrect, the variables actually DO change outside of program flow. Yes I lied."
The downside to this is that there is no control over which global variables are forced out to memory, and which are not. Instead they are all forced out. The upside is more efficient code generation in a few cases.

If Dean had not lied to the compiler, the compiler would not have had the option of moving the (then volatile) accesses across the also-volatile cli/sei.

The third option is to cast the rvalue to volatile through an address operator. Then the program becomes 'compiler rules correct' again, and the compiler cannot move them across the cli/sei.

Search the forum for COMPUTE_BARRIER for another thing one may need to know.

/Kasper

(My C teacher at eng. college would go all the way into purple when one did the tricks above. Since you can't specify implementation in C, he was a firm believer that one shouldn't try. "If you need to control implementation, you use assembler.")

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

abcminiuser wrote:
Which shows why the memory barrier is needed - without it the compiler will always do the SEI after the CLI, but doesn't always put the code shown inbetween them between the two, as they aren't specifically dependant on the volatile operations.
TimothyEBaldwin wrote:
Because thy are declared as:
# define sei()  __asm__ __volatile__ ("sei" ::)
# define cli()  __asm__ __volatile__ ("cli" ::)

Asm statements declared volatile cannot be reordered relative to themselves or to accesses to volatile variables.

Ah. That rule.
I think I've got it now.
cli()...sei() usually works because the three dots
usually represent accesses to volatile variables.
It doesn't work in the general case because
sometimes the three dots include code that does
not depend on accesses to volatile variables.

:::"memory" barriers will prevent code motion
into and out of a critical section.
They will also prevent code motion from one
side to the other of a critical section.

If one wishes to allow code motion from
one side to another of a critical section,
one will need to find a mechanism that
does not include a :::"memory" barrier.
Adding volatile-ity within a critical section would work,
but might reduce otherwise available
optimizations within the critical section.

Correct?

"Demons after money.
Whatever happened to the still beating heart of a virgin?
No one has any standards anymore." -- Giles

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

skeeve wrote:

:::"memory" barriers will prevent code motion
into and out of a critical section.
They will also prevent code motion from one
side to the other of a critical section.

Not quite, :::"memory" barriers will not prevent movement of calculations or accesses to local variables for which no pointer has escaped. It will prevent movement of accesses of all variables that the compiler can't prove aren't accessed by the asm statement - without considering the assembler instructions that are (not) in the statement.