Manual manipulation of Interrupt Vector Table on AVR platform

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

Hi,

 

I want to make the ISR handling and initialization similar to the ARM Interrupt Vector Table in my AVR (atmega328p) application, to have the final interrupt initialization API and assignment as much similar as possible when using the same firmware ported from the Atmega328P on an ARM (which could be an M3 or M4), and viceversa.

 

For example, when I have explicit addresses of the Vector Table and the offset of the interrupt, I wonder if I can safely assign an array of ISR pointers to the IVT, like the following, in the ARM:

 

void interrupt_Attach(isrFp_t f, uint8_t i_array_app, uint8_t i_iv)
{
    // arrayApp is the position inside isr_ptArray, in which I put my ISR pointer
    // f is the ISR pointer
    // i_iv is the location of the interrupt in the IVT (I have to check datasheet to know that)

	unsigned long *ulNvicTbl;
	isr_ptArray[i_array_app][0] = f;                                                    //an array of function containing the ISR of the system. Is declared somewhere else globally
	isr_ptArray[i_array_app][1] = iv_number;                                            //the iv_number linking to the correct location of the ivTable (see last line )
	ulNvicTbl = (ivTable_t*) base_adx_ivt;                                              //base_adx_ivt is the base address of the IVT, or the address of IVT[0], I presume
	ulNvicTbl[isr_ptArray[i_array_app][1]]= (unsigned long)isr_ptArray[i_array_app][0]; //now in base_adx_ivt+iv_number there will be the pointer to my ISR
}

Now, for the AVR part, I wonder if I can somehow do the same, by knowing the address (which is in flash) of the vector table. The problem is that the ISR() macro links to a compiler macro (I believe), called _vector(number), in which the linker (or the compiler?) recognize that I needed the ISR(ADC_vect), containing the function my_isr() (for example) and so in the vector table will be changed at the line of the ADC with a JUMP my_isr. Sorry for butchering in that way, I am no veteran.

 

The current solution going on the AVR is just assigning manually the ISR (or inside a proper API), in this way:

 

isr_ptArray[i_array_app][0] = f;

// while somewhere I define the ISR

ISR(ADC_vect)
{
   isr_ptArray[CONF_INT_ADC_ID][0]();
}

As you can see is a very different approach, which rely heavily on the GCC macros. This is new as I am trying to understand if I can have a similar API between different families. Normally, regarding the example above, I would put just my ISR inside the ISR() macro.

Reading the nongnu guide of the interrupt, I am aware that I can do any sort of manipulation and also assign the unused interrupts. But I wanted to not rely necessarily on the gcc macros, to achieve a more similar API for the interrupts, in the firmware. I know this could be a big no go for many, but I am doing this to go through a learning process of the ISR structure of different platforms, and see how much (or not) burden is required in the background to have a similar API for different platforms.

 

This topic has a solution.
Last Edited: Thu. Apr 11, 2019 - 07:47 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

thexeno wrote:
The current solution going on the AVR is just assigning manually the ISR (or inside a proper API), in this way:
To me that looks about as close as you can get. To be able to call via function pointers you need to have an array of function pointers in memory to call through. Then you simply have a "shim" that hooks the vector and calls through the pointer. And that's what you've got.

 

When nothing about the hardware is otherwise similar (the UARTs aren't, the timers aren't, the ADCs aren't, etc etc) I see little merit in trying to replicate the IVT mechanism? You are going to have to have a "HAL" anyway and it should "hide" the IVT thing just as much as it hides every other hardware specific detail of the peripherals.

 

(for an example of such a HAL see "Arduino". It offers the same APIs at a hardware abstraction level whether the underlying CPU is AVR or Cortex.

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

clawson wrote:

 

When nothing about the hardware is otherwise similar (the UARTs aren't, the timers aren't, the ADCs aren't, etc etc) I see little merit in trying to replicate the IVT mechanism? You are going to have to have a "HAL" anyway and it should "hide" the IVT thing just as much as it hides every other hardware specific detail of the peripherals.

 

 

 

 

Thanks, that is quite conclusive, but I would like to dig a bit more. I believe that replicating the mechanism would mean taking over the ISR macros of the compiler (warning #1). And using the addresses like the ARM is not really possible as to access the flash it should use the flash API (warning/incompatibiity #2). So basically is the reason why everything happens at compile time with macros within the Atmel Studio software and the tool chain. Am I right? I am also be wrong on the ARM side expectations (being able to allocate the addresses "easily" to the IVT as shown in the code), as I never went deep yet in those.

Last Edited: Mon. Apr 8, 2019 - 02:58 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

thexeno wrote:
I believe that replicating the mechanism would mean taking over the ISR macros of the compiler (warning #1).
The ISR mechanism is really remarkably simple in fact.

 

First ISR() just means "function with "signal" and "used" and "externally_visible" attributes applied. Then all of the INTO_vect, ADC_vect, TIMER0_OVF_vect and so on simply translate to the right __vector_N() function name. You can see that all in action when I build:

ISR(ADC_vect) {
	
}

and the .i file actually contains:

void __vector_21 (void) __attribute__ ((signal,used, externally_visible)) ; void __vector_21 (void) 
# 5 ".././main.c"
             {

}

So ISR() is doing nothing more than creating a __vector_N() function. Meanwhile in the CRT for the device the source says:

 

http://svn.savannah.gnu.org/viewvc/avr-libc/trunk/avr-libc/crt1/gcrt1.S?revision=2519&view=markup

 

The key bit of which is:

42	        .macro  vector name
43	        .if (. - __vectors < _VECTORS_SIZE)
44	        .weak   \name
45	        .set    \name, __bad_interrupt
46	        XJMP    \name
47	        .endif
48	        .endm

and then:

50	        .section .vectors,"ax",@progbits
51	        .global __vectors
52	        .func   __vectors
53	__vectors:
54	        XJMP    __init
55	        vector  __vector_1
56	        vector  __vector_2
57	        vector  __vector_3
58	        vector  __vector_4
59	        vector  __vector_5
60	        vector  __vector_6
...
181	        vector  __vector_127
182	        .endfunc

So this make a jump (XJUMP is RJMP on small AVR and JMP on big ones) to either the real function __vector_21 if there is an implementation or, as a "weak" fall back to __bad_interrupt.

 

As you can see that list of vectors is built in memory section ".vectors" and the linker script has:

  /* Internal text space or external memory.  */
  .text   :
  {
    *(.vectors)
    KEEP(*(.vectors))
    /* For data that needs to reside in the lower 64k of progmem.  */
     *(.progmem.gcc*)
    /* PR 13812: Placing the trampolines here gives a better chance
       that they will be in range of the code that uses them.  */
    . = ALIGN(2);
     __trampolines_start = . ;
    /* The jump trampolines for the 16-bit limited relocs will reside here.  */
    *(.trampolines)
     *(.trampolines*)
...

So .vectors gets placed first at location 0x0000.

 

All this hard codes jumps to the thing you called ADC_vect or whatever.

 

Now one option for you is -nostartfiles so that all this CRT stuff is not linked. But if you do that you will likely want to add a file that puts SOMETHING into .vectors remembering that the AVR only allows room for one RJMP or one JMP at each vector location so you have either 2 or 4 bytes for your indirection jump. I'd say that because of that it's pretty hard to contemplate anything but a hardcoded RJMP/JMP to some "catcher".

 

If you didn't need adjacent vectors you could allow the code for one vector to spill into the next "slot" (but don't ever enable that other interrupt!). But if you want the utility of being able to use any vector in the IVT it's hard to know what better 3/4 byte opcode you could put there than RJMP/JMP. Even if you limit the destination range by using RJMP on a CPU that is really JMP sized you haven't got room to load Z and do an ICALL or anything like that as far as I can see.

 

So ultimately the only way you end up with software vectors is to implement __vector_1 .._vector_<NUM_VECTORS> with calls through function pointers. Either individual ones or an array of them:

typedef void (*fptr_t)(void);

fptr_t fnptrs[NUM_VECTORS];

__attribute__((used, signal, externally_visible)) void __vector_1(void) {
    fnptrs[1]();
}

__attribute__((used, signal, externally_visible)) void __vector_2(void) {
    fnptrs[2]();
}

__attribute__((used, signal, externally_visible)) void __vector_3(void) {
    fnptrs[3]();
}
etc.

As you may spot an array version like that could could lend itself to auto-generation by something like Python in a pre-build event. The other alternative is:

typedef void (*fptr_t)(void);

fptr_t INT0_handler;
fptr_t INT1_handler;
fptr_t PCINT0_handler;
etc.

__attribute__((used, signal, externally_visible)) void __vector_1(void) {
    INT0_handler();
}

__attribute__((used, signal, externally_visible)) void __vector_2(void) {
    INT1_handler();
}

__attribute__((used, signal, externally_visible)) void __vector_3(void) {
    PCINT0_handler();
}
etc.

I personally prefer this as it reads better when you say:

INT1_handler = my_code_for_INT1;

rather than:

fnptrs[1] = my_code_for_INT1;

 

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

Very explanatory, thanks.

 

If I got it right, in the beginning there is this CRT (C-Run Time I believe) assembly which gets executed. This file gets generated, linked and compiled within my project.

That CRT, will depends upon, and be tailored with, with AVR families and their variations at compile time. (I don't find this file in my project, but I do find the initial jumps in the final linked disassembly. I am using Atmel Studio)

 

Regarding the abstraction part of the ISRs, by seeing the FreeRTOS to understand how should I go down in the hardware dependent code, seems it is using the same mechanism provided by the compiler, i.e. let the user take the supported compiler with the supported familiy macros, and put them in the device dependent code (which is the user project this time), with calls to the higher level OS. A typical example I saw is a UART example. With MSP430 uses the macro interrupt, while with an STM with ARM plays with addresses at the NVIC. Seems that, what I want to achieve (dynamic assignment of interrupt at startup, less dependent with compiler macros, eventually configured by filling up an array or a matrix), could be achieved with the ARM interrupt strategy (like the example I posted in the #1). If that is true, would also make sense, as they want to be as much scalable as possible.

But for other devices or compilers, I should rely on their supported macro, even when using an RTOS.

 

In the meantine, if I have to organize code in the application firmware, using an HAL to join the application code to the hardware dependent drivers, so I have like 3 layers, in which the lower are the hardware dependent drivers (bare metal drivers).

 

What would be the right approach, if I want to keep code portability? Using,  inside the ISR(), low level driver functions, the HAL functions or the application one? The application code could call nothing else, or if has to access a register, for sure then has to call another HAL function to access the hardware. So I guess the application code could not be the optimal solution as it would fill more the stack, and mess with cleaner file separation of the firmware. But in the FreeRTOS examples, they are using also calls to higher level RTOS functions.

 

If I get that clear, I think I could also understand what could be accepted as elegant and portable code and then stop to search for alternatives.

 

 

Last Edited: Tue. Apr 9, 2019 - 09:47 AM
This reply has been marked as the solution. 
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

thexeno wrote:

If I got it right, in the beginning there is this CRT (C-Run Time I believe) assembly which gets executed. This file gets generated, linked and compiled within my project.

That CRT, will depends upon, and be tailored with, with AVR families and their variations at compile time. (I don't find this file in my project, but I do find the initial jumps in the final linked disassembly. I am using Atmel Studio)

The .map file hopefully tells you the story. If I build code for 328P then look at the .map I see something like:

Archive member included to satisfy reference by file (symbol)

c:/program files (x86)/atmel/studio/7.0/toolchain/avr8/avr8-gnu-toolchain/bin/../lib/gcc/avr/5.4.0/avr5\libgcc.a(_exit.o)
                              C:/Program Files (x86)/Atmel/Studio/7.0/Packs/Atmel/ATmega_DFP/1.3.300/gcc/dev/atmega328p/avr5/crtatmega328p.o (exit)

So in this case the CRT is being delivered by avr5/crtatmega328p.o. That's one of the 300+ prebuilt copies of CRT that comes with the compiler (all built from that single gcrt1.S who's source I linked to above. It gets built 300+ times, once for each AVR supported (as each potentially has a different IVT with a different number of vectors). 

 

The code in that file is basically:

C:\Program Files (x86)\Atmel\Studio\7.0\packs\atmel\ATmega_DFP\1.3.300\gcc\dev\atmega328p\avr5>avr-objdump -S crtatmega328p.o

crtatmega328p.o:     file format elf32-avr


Disassembly of section .text:

00000000 <__bad_interrupt>:
   0:   0c 94 00 00     jmp     0       ; 0x0 <__bad_interrupt>

Disassembly of section .vectors:

00000000 <__vectors>:
   0:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
   4:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
   8:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
   c:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  10:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  14:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  18:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  1c:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  20:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  24:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  28:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  2c:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  30:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  34:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  38:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  3c:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  40:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  44:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  48:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  4c:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  50:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  54:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  58:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  5c:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  60:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>
  64:   0c 94 00 00     jmp     0       ; 0x0 <__vectors>

Disassembly of section .init2:

00000000 <.init2>:
   0:   11 24           eor     r1, r1
   2:   1f be           out     0x3f, r1        ; 63
   4:   c0 e0           ldi     r28, 0x00       ; 0
   6:   d0 e0           ldi     r29, 0x00       ; 0
   8:   de bf           out     0x3e, r29       ; 62
   a:   cd bf           out     0x3d, r28       ; 61

Disassembly of section .init9:

00000000 <.init9>:
   0:   0e 94 00 00     call    0       ; 0x0 <.init9>
   4:   0c 94 00 00     jmp     0       ; 0x0 <.init9>

(this is unlinked so all the destinations are currently 0)

 

As I said above you can build with -nostartfiles which basically means "don't link crt*.o". But if you do that and you do want some interrupts then you are going to need to rpvide an alternative. Note also that the other job of the CRT is:

00000000 <.init2>:
   0:   11 24           eor     r1, r1
   2:   1f be           out     0x3f, r1        ; 63
   4:   c0 e0           ldi     r28, 0x00       ; 0
   6:   d0 e0           ldi     r29, 0x00       ; 0
   8:   de bf           out     0x3e, r29       ; 62
   a:   cd bf           out     0x3d, r28       ; 61

Disassembly of section .init9:

00000000 <.init9>:
   0:   0e 94 00 00     call    0       ; 0x0 <.init9>
   4:   0c 94 00 00     jmp     0       ; 0x0 <.init9>

That is setting SREG to 0x00 (clears interrupts) then it sets the stack pointer to RAMEND. You probably want to duplicate this action manually if you aren't using CRT. The "call 0" in there is actually the "call main" that enters your C code. You likely want something like that too.

thexeno wrote:
What would be the right approach, if I want to keep code portability? Using,  inside the ISR(), low level driver functions, the HAL functions or the application one?
As I said previously on different architectures, the hardware and the interrupt system are all going to be different so I see no merit in trying to create some form of common interrupt handling. The ISR()s (in the AVR case) should be beneath the HAL API boundary. So you may well use an RXC interrupt on the UART to feed bytes into a ring buffer to buffer the inbound characters but at the HAL API level you simply have a uart_getchar(). On some architectures this may be using interrupt/ring buffering while on another it could be synchronous polling. The user at the HAL level does not care - they just know that the common routine to get a character from the serial is UART_getchar. Perhaps just serial.read() or something? And what do you know, I just invented multi-platform Arduino !! ;-)

 

Bottom line of all this - you are over thinking it - you are looking to duplicate architecture at too deep/raw a level. Think of the high level API your common apps are going to want to call. (like Serial.read() )

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

Note that on AVR, the interrupt vectors are all in Program Memory, and cannot be changed at runtime.  AVRs with "boot section" support have a choice of two possible addresses - I guess that in theory, boot section code could overwrite the vector page in low flash, but you'd need a pretty nasty API to avoid stressing the flash with too many writes.  It's not like those ARM chips where you can relocate (or create) the vector table in RAM.  :-(

 

You CAN set up a secondary vector table in RAM, and call functions via separately maintained pointers to functions - this is how the attachInterrupt() functions in Arduino work.   However, it can be quite expensive on the AVRs, because calling any function (via a pointer, or direct) will result in 12 extra registers being pushed and popped, compared to a trivial function that all fits inside the ISR() itself.  (of course, sometimes you'll need to call additional functions, anyway...)

 

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

It should only be :

push zh

push zl

ld zh

ld zl

ijmp

and at the routine

pop zl

pop zh

'normal ISR code

 

If you reserve two (low)registers the push and pop can just be two movw (save 6 clk).

 

But you need to have many ISR routines ! (they are different than normal routines!)  

 

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

You CAN set up a secondary vector table in RAM, and call functions via separately maintained pointers to functions - this is how the attachInterrupt() functions in Arduino work.   However, it can be quite expensive on the AVRs, because calling any function (via a pointer, or direct) will result in 12 extra registers being pushed and popped, compared to a trivial function that all fits inside the ISR() itself.  (of course, sometimes you'll need to call additional functions, anyway...)

 

So I believe is what I am already doing. I was working now to find a proper separation between the application, my portable libraries and the HAL, which is used by such libraries. So I have kind of 3 layers of software here. The Atmega328p is really immense in terms of 8bit resources, where performance is needed, shortcuts can be allowed too, to speed up the ISR execution and the like, if the CPU is too slow.

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

push zh

push zl

ld zh

ld zl

ijmp

That (or something close) certainly ought to work - but I don't think you can convince avr-gcc to produce it (nor the proper almost-ISR-like functions for the ijmp targets, though that should be fixable by using icall instead, and added the pops/iret afterward.)