AVR: gas: unusable assembler output and complicated ISR problem...
Ian Molton
binutils-ian@mnementh.co.uk
Fri Dec 17 19:13:06 GMT 2021
Hi folks,
I'm attempting to do something a "bit different" regarding interrupts
on AVR, and I've run into a hard limit in (AFAICT) gas.
I wish to compile C sources to assembler, modify the assembler, and then
assemble and link object produced.
GCC generates additional prologue for ISRs using the __gcc_isr assembler
pseudo-op, which gas will fix up to include saving additional registers
aside - Somewhat irksomely, gcc will fall back to its "traditional"
(internal) prologue generation, should it decide that binutils cannot
complete the job (eg. call to a function in another object file), which
results in inconsistent assembler output, eg.
Anyhow, compiling with the following:
avr-gcc -S irq.c
yields irq.s, containing:
// Vector 35 calls a function that can't be analysed (assumes the worst,
saves everything via the "traditional" method)
__vector_35:
push r1 ;
push r0 ;
in r0,__SREG__ ; ,
push r0 ;
clr __zero_reg__ ;
// note a
in r0,__RAMPZ__ ; ,
push r0 ;
push r18 ;
push r19
push r20 ;
push r21 ;
push r22 ;
push r23 ;
push r24 ;
push r25 ;
push r26 ;
push r27 ;
push r30 ;
push r31 ;
push r28 ;
push r29 ;
call foo_func();
...
popity pop // epilogue...
etc. (removed for brevity)
reti
// Vector 36 doesn't trip the checks above, uses __gcc_isr
// To generate prologue instead.
__vector_36:
__gcc_isr 1 ; // note b
/* prologue: Signal */
/* frame size = 0 */
/* stack size = 0...4 */
.L__stack_usage = 0 + __gcc_isr.n_pushed
; irq.c:106: *i = 5;
// Junk code to make the compiler use a register, ignore.
ldi r24,lo8(5) ; tmp43,
sts 133,r24 ; MEM[(volatile uint8_t *)133B], tmp43
/* epilogue start */
; irq.c:108: }
__gcc_isr 2 ;
reti
The problem is, that what I wish to do isn't possible in the latter case.
I want to post-process the assembler output from GCC (see the comment
"note a" in __vector_35), effectively stripping off the lines before
note a, and replacing them with my own custom prologue (which will
always save r0, SREG, and, say, r26, and r27).
As you can see, the comment "note b" marks the corresponding spot in
__vector_36, but the instruction sequence represented by __gcc_isr has
not been generated at this point, so I can't dissect it. I assume that
irq.s is generated entirely by the compiler, hence the lack of expansion
of __gcc_isr.
Likewise, with the epilogue, in order to match the changes to the
prologue, I need to remove, then insert a few instructions after (most
of) the pops, but before the reti.
Is there some way to get gas to "preprocess" the file to expand the
pseudo operations __gcci_isr* ? If not, how would one go about adding
such support? I've never worked on binutils or gcc internals before.
...
The idea here is that all IRQs would share a single irq stack, freeing
up more stack for threads to use (which is very limited!). Despite that,
IRQs that don't require scheduling to take place, or that don't occur
whilst in user-context, benefit from a (relatively) short path, saving
fewer registers.
Below is a very rough and ready pseudo-assembler outline of what I'd
like to generate. There are probably errors in it, but it should show
the concept;
This is the assembler output I'd like to generate (via my script)
following the script analysing a typical ISR (signal) written in C (lets
call it __vector_34().
The shortest path (nested IRQ) is ~25 insns.
The longest path (IRQ whilst in user context, that sets need_sched) is
~50 insns
So - if the __gcc_isr were expanded, I could analyse the assembler and
generate the following:
...magic_call_used_regs... -> All call-used regs from __vector_34
but with r0, r1, SREG, r26, and r27
removed.
...magic_call_saved_regs... -> All call-saved regs from __vector_34
not including r0, r1.
...everything_else... -> A list of all CPU registers *except* for
r0, r1, SREG, r26, and r27
/// Prototype ISR pro/epilogue idea
.prologue
// Push all relevant Call-used regs, starting with
// r0, r1, SREG, r26, r27
push __zero_reg__
push __tmp_reg__ // 0
in __tmp_reg__, SREG
push __tmp_reg__
clr __zero_reg__
push r26
push r27
// Increment IRQ level (and re-enable IRQs)
lds __tmp_reg__, irq_level
inc __tmp_reg__
sei
sts irq_level, __tmp_reg__
// If in IRQ, jump ahead, else...
subi __tmp_reg__, 1
beq .do_irq
// Leaving user context - switch to irq stack
// Save user stack
lds r26, *this->regs_l
lds r27, *this->regs_h
in __tmp_reg__, SPL
st x+, __tmp_reg__
in __tmp_reg__, SPH
st x, __tmp_reg__
// Install IRQ stack
ldi __tmp_reg__, irq_stack_l
out SP_L, __temp_reg__
ldi __tmp_reg__, irq_stack_h
out SP_H, __temp_reg__
.do_irq
// On IRQ stack at this point.
push ...magic_call_used_regs...
push ...magic_call_saved_regs...
// Fall through into __vector_34()
void __vector_34(void)
{
// Code here may result in further additions to the
// list of call-saved registers (magic_regs_list) in
// use.
...
}
.epilogue
cli;
// note c (see below)
pop ...magic_call_saved_regs...
pop ...magic_call_used_regs...
lds __tmp_reg__, irq_level
dec __tmp_reg__
sts irq_level, __tmp_reg__
beq ret_user
// Returning from IRQ
pop r27
pop r26
pop __tmp_reg__ // SREG
out SREG, __tmp_reg__
pop __tmp_reg__
pop __zero_reg__
reti // return to irq context
.ret_user
// Return to the user stack
lds r26, *this->regs_l
lds r27, *this->regs_h
ld __tmp_reg__, x+
out SPL, __tmp_reg__
ld __tmp_reg__, x+
out SPH, __tmp_reg__
ld __tmp_reg__, x // this->needs_sched
// At this point, we're on pristine user stack,
// except r0, r1, SREG, r26, and r27 remain on the stack.
if(this->needs_sched)
{
// With careful ordering, it might be possible
// to save some pushes here (possibly avoiding
// pop-ing them at note c
push <...everything_else...>
save thread_sp; //
SP = irq_stack;
schedule();
restore next_thread_sp
}
else
{
pop r27
pop r26
pop __tmp_reg__ // SREG
out SREG, __tmp_reg__
pop __tmp_reg__
pop __zero_reg__
}
reti;
More information about the Binutils
mailing list