3

I'm working on project, where some interrupt service has to be handled in assembler. The handler function is called from interrupt vector wrapper. The handler body is written in assembler and it receives single (pointer) parameter in specific register.

The code target is MSP430 and it has to compile with both MSP430-gcc and TI compiler. I already have working solution for MSP430-gcc and it looks like this:

static void __attribute__((naked)) _shared_vector_handler(Timer_driver_t *driver) {

__asm__(
    "   MOVX.W %c[iv_register_offset](R12),R14 ; \n"
    "   ADD @R14,PC ; \n"
    "   RETA ; \n"
    "   JMP CCIFG_1_HND ; Vector 2 \n"
    "   JMP CCIFG_2_HND ; Vector 4 \n"
    "   JMP CCIFG_3_HND ; Vector 6 \n"
    "   JMP CCIFG_4_HND ; Vector 8 \n"
    "   JMP CCIFG_5_HND ; Vector 10 \n"
    "   JMP CCIFG_6_HND ; Vector 12 \n"
    "TIFG_HND: \n"
    "   MOVX.A %c[overflow_handle_offset](R12),R14 ; \n"
    "   MOVX.A %c[handler_param_offset](R14),R12 ; \n"
    "   MOVX.A %c[handler_offset](R14),R14 ; \n"
    "   CALLA R14 ; \n"
    "   RETA ; \n"
    "CCIFG_1_HND: \n"
    "   MOVX.A %c[ccr1_handle_offset](R12),R14 ; \n"
    "   MOVX.A %c[handler_param_offset](R14),R12 ; \n"
    "   MOVX.A %c[handler_offset](R14),R14 ; \n"
    "   CALLA R14 ; \n"
    "   RETA ; \n"
    "CCIFG_2_HND: \n"
    "   MOVX.A %c[ccr2_handle_offset](R12),R14 ; \n"
    "   MOVX.A %c[handler_param_offset](R14),R12 ; \n"
    "   MOVX.A %c[handler_offset](R14),R14 ; \n"
    "   CALLA R14 ; \n"
    "   RETA ; \n"
    "CCIFG_3_HND: \n"
    "   MOVX.A %c[ccr3_handle_offset](R12),R14 ; \n"
    "   MOVX.A %c[handler_param_offset](R14),R12 ; \n"
    "   MOVX.A %c[handler_offset](R14),R14 ; \n"
    "   CALLA R14 ; \n"
    "   RETA ; \n"
    "CCIFG_4_HND: \n"
    "   MOVX.A %c[ccr4_handle_offset](R12),R14 ; \n"
    "   MOVX.A %c[handler_param_offset](R14),R12 ; \n"
    "   MOVX.A %c[handler_offset](R14),R14 ; \n"
    "   CALLA R14 ; \n"
    "   RETA ; \n"
    "CCIFG_5_HND: \n"
    "   MOVX.A %c[ccr5_handle_offset](R12),R14 ; \n"
    "   MOVX.A %c[handler_param_offset](R14),R12 ; \n"
    "   MOVX.A %c[handler_offset](R14),R14 ; \n"
    "   CALLA R14 ; \n"
    "   RETA ; \n"
    "CCIFG_6_HND: \n"
    "   MOVX.A %c[ccr6_handle_offset](R12),R14 ; \n"
    "   MOVX.A %c[handler_param_offset](R14),R12 ; \n"
    "   MOVX.A %c[handler_offset](R14),R14 ; \n"
    "   CALLA R14 ; \n" ::
    [iv_register_offset] "i" (offsetof(Timer_driver_t, _IV_register)),
    [overflow_handle_offset] "i" (offsetof(Timer_driver_t, _overflow_handle)),
    [ccr1_handle_offset] "i" (offsetof(Timer_driver_t, _CCR1_handle)),
    [ccr2_handle_offset] "i" (offsetof(Timer_driver_t, _CCR2_handle)),
    [ccr3_handle_offset] "i" (offsetof(Timer_driver_t, _CCR3_handle)),
    [ccr4_handle_offset] "i" (offsetof(Timer_driver_t, _CCR4_handle)),
    [ccr5_handle_offset] "i" (offsetof(Timer_driver_t, _CCR5_handle)),
    [ccr6_handle_offset] "i" (offsetof(Timer_driver_t, _CCR6_handle)),
    [handler_offset] "i" (offsetof(Timer_channel_handle_t, _handler)),
    [handler_param_offset] "i" (offsetof(Timer_channel_handle_t, _handler_param)) :
);
}

Translated to English: the driver structure contains address of IV register on some specific offset. Content on that address is added to PC, so jump to specific label (depending on which interrupt flag is set) occurs. This is recommended usage as described by TI in user's guide, page 653. All labels do the same: they take pointer to some handle from driver structure from specific offset. The handle has again on some specific offset function pointer (interrupt service handler) and pointer to some parameter, that shall be passed to handler. The structures in short:

typedef struct Timer_driver {
// enable dispose(Timer_driver_t *)
Disposable_t _disposable;
// base of HW timer registers, (address of corresponding TxCTL register)
uint16_t _CTL_register;
...
// interrupt vector register
uint16_t _IV_register;
// stored mode control
uint8_t _mode;
// amount of CCRn registers
uint8_t _available_handles_cnt;

// main (CCR0) handle
Timer_channel_handle_t *_CCR0_handle;
// up to six (CCRn) handles sharing one interrupt vector
Timer_channel_handle_t *_CCR1_handle;
Timer_channel_handle_t *_CCR2_handle;
...
}

and

struct Timer_channel_handle {
// vector wrapper, enable dispose(Timer_channel_handle_t *)
Vector_handle_t vector;
// HW timer driver reference
Timer_driver_t *_driver;
// capture / compare control register
uint16_t _CCTLn_register;
// capture / compare register
uint16_t _CCRn_register;

// vector interrupt service handler
void (*_handler)(void *);
// vector interrupt service handler parameter
void *_handler_param;
...
}

Now the problem.

  • offsets are not known until compile time
  • I can't pass to assembler some offsetof(s, m)
  • offsets depend on memory model used (size of pointers 16bit or 32bit)
  • offsets depend on size of first member of both structures and this size depends on preprocessor definitions (1 pointer or 4 pointers)
  • offsets cannot be precomputed, because each compiler adds some alignment and padding to the first member structure
  • the first member must be first member (reordening is not allowed)
  • TI compiler does not support passing compile-time vars to inline assembly code

The goal:

  • support both compilers
  • do not duplicate code, do not hardcode offsets
  • if possible, avoid extracting whole handler to asm file and including headers via .cdecls (or #include in case of gcc). Both compilers handle including of C headers in a lot different way, structure offsets are also defined in a lot different way and some non-trivial restructuring of headers would be required, which I believe is near impossible.

When I compile this with TI compiler I get following error:

"../module/driver/src/timer.c", line 274: error #18: expected a ")"
"../module/driver/src/timer.c", line 285: warning #12-D: parsing restarts here after previous syntax error
1 error detected in the compilation of "../module/driver/src/timer.c".
gmake: *** [module/driver/src/timer.obj] Error 1

My build is handled by CMake and I can think of one solution - just to pregenerate those offsets to some header file, that shall be included in the driver. The way how to do that is described here. But if possible I'd like to also avoid this one step, since it needs to compile in Code Composer Studio, that does not run cmake.

So how do I create CMake target to pregenerate those offsets? Or any other ideas?

CL.
  • 173,858
  • 17
  • 217
  • 259
  • The error message is simply because there is an extraneous : right before the closing ) of the asm statement. – prl Nov 07 '18 at 04:13
  • Well with gcc you can do it like this, because of it's [extended assembly](https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html). With TI compiler this is not possible, inline assembly is limited to __asm("assembler string"), so that no compile-time variables can be passed to it. – mutant-industries Nov 07 '18 at 07:00
  • Note that your Extended asm is missing clobbers for all the registers it steps on, so it's not a safe example even if you were using a compiler that supported GNU C extensions. Oh nvm, you're doing it inside a naked function, so that's fine but stops it from inlining. – Peter Cordes Nov 07 '18 at 07:25
  • 1
    That add-IV-to-PC mechanism is useful only when the handlers for the various interrupt sources are different. In this case, you could make the handles an array, and use the IV value as an offset into this array. And CALLA+RETA can be optimized into a jump. Anyway, why do you think that it is necessary to use assembler? Is the code generated by the compiler so much worse? – CL. Nov 07 '18 at 13:20
  • Would it be possible to reorder the structure fields? – CL. Nov 07 '18 at 13:21
  • @CL I wanted to avoid switch statement and also the array of handles approach. It is actually very good idea, however it involves multiplication and MSP430 has no such instruction (usage of external multiplier MPY32 only adds some more performance penalty). But since pointers are either 2B or 4B, I could get the offset by simple bit shift of IV and get rid of assembler. Thanks, I'll have to think about it, but It seems, that there is finally pretty good solution to this. – mutant-industries Nov 07 '18 at 15:55
  • @CL Reordering is not possible unfortunately. I use this approach when some structure embeds it's 'parent' structure, as the first member. This way it is always possible to cast to parent, for example in this case Timer_channel_handle to Vector_handle_t. The topmost parent is Disposable_t the first member of which is pointer to function, that can release the specific handle and return pointer to function, that can release higher layer etc. This way I can call dispose on any handle type and I don't even need to know the type of that handle. – mutant-industries Nov 07 '18 at 16:04

2 Answers2

2

Thanks to everyone, special thanks to @CL. I've been stuck with the thought that this has to be done in assembler for so many reasons and that I only need to get those offsets somehow. The solution is simple:

static void _shared_vector_handler(Timer_driver_t *driver) {
    uint16_t interrupt_source;
    Timer_channel_handle_t *handle;

    if ( ! (interrupt_source = hw_register_16(driver->_IV_register))) {
        return;
    }

    handle = *((Timer_channel_handle_t **) 
        (((uintptr_t)(&driver->_CCR0_handle)) + (interrupt_source * _POINTER_SIZE_ / 2)));

    (*handle->_handler)(handle->_handler_param);
}

translated to assembler (TI compiler, memory model large):

        _shared_vector_handler():
011ef6:   4C1F 0008           MOV.W   0x0008(R12),R15
011efa:   4F2F                MOV.W   @R15,R15
011efc:   930F                TST.W   R15
011efe:   240D                JEQ     (0x1f1a)
231         (*handle->_handler)(handle->_handler_param);
011f00:   F03F 3FFF           AND.W   #0x3fff,R15
011f04:   025F                RLAM.W  #1,R15
011f06:   4F0F                MOV.W   R15,R15
011f08:   00AC 000C           ADDA    #0x0000c,R12
011f0c:   0FEC                ADDA    R15,R12
011f0e:   0C0F                MOVA    @R12,R15
011f10:   0F3C 003E           MOVA    0x003e(R15),R12
011f14:   00AF 003A           ADDA    #0x0003a,R15
011f18:   0F00                BRA     @R15
        $C$L12:
011f1a:   0110                RETA    

The original assembler takes 7 instructions to execute the handler, but the add-IV-to-PC breaks the pipeline. Here we have 13 instructions, therefore the effeciency is almost equal.

BTW the actual commit is here.

0

For constants that are available in numeric form when the C preprocessor runs, you can use #define macros to stringify and concat them with the inline-asm string, like asm("blah blah " stringify(MYCONST) "\nblah blah");, but that won't work for offsetof, which requires the compiler proper to evaluate it to a number.


FIXME: this won't work as easily when cross-compiling. You'd have to parse compiler-generated asm, or dump static data from a .o Both of those are possible as minor modifications to this method, but are kinda ugly. I'm going to leave this answer here in case it's useful for non-cross-compiling use-cases.


However, since you tagged this cmake, you have a build system that can handle a chain of dependencies. You could write a program that uses offsetof to create a .h with contents like this, using some simple printf statements

// already stringified to simplify
// if you want them as numeric literals, leave out the double quotes and use a STR() macro
#define OFFSET_Timer_driver_t__CCR1_handle  "12"
#define OFFSET_Timer_driver_t__CCR2_handle  "16"
...

Then you can #include "struct_offsets.h" in files that need it, and use it for inline asm like

asm("insn " OFFSET_Timer_driver_t__CCR1_handle "\n\t"
    "insn blah, blah \n\t"
    "insn foo " OFFSET_Timer_driver_t__CCR2_handle "\n\t"
   );

Or use pure asm instead of a naked function, since you're.

Use CMake build dependencies so that any files that need struct_offsets.h are rebuild if it changes.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • @CL.: Oh good point, I don't think it does the way I described without running code on the compile-target during the build process. You could make a simple `.c` that defines globals like `long offset_foo_bar = offsetof(blah, blah)`, and compile that to asm (`.s`) and look at the labeled `.long 12` code, or compile to a `.o` and hexdump that or something. Surely there's a better / cleaner way without having to parse asm for whatever target, or dump static data from a `.o` for that target, but I can't think of one. – Peter Cordes Nov 07 '18 at 13:23
  • @Peter Cordes This was my original idea - to pre-generate those offsets to some header file and include it in driver. But the question is - how do I do that with cmake? It seems that [configure_file()](https://cmake.org/cmake/help/latest/command/configure_file.html) is not the best tool for this and I can't seem to find anything else suitable. – mutant-industries Nov 07 '18 at 16:08
  • @mutant-industries: you need an actual C compiler to parse the struct and know the ABI rules to get the offsets. You can't do it with pure CMake, unless you want to re-implement the ABI's struct alignment/padding rules yourself using some kind of pre-processor. – Peter Cordes Nov 07 '18 at 16:28
  • 1
    One ugly trick to solve the cross compilation case would be to use the value of `offsetof` in some context where you know the compiler will output it, e.g., as the output of an error message. For example, use the negative of the value as an array size, and maybe the compiler outputs `-123 is not a valid array size` or whatever (you can do similar things with assignment to get messages about "truncation of constant values, etc"). Then you can parse the result out of the error message and generate your `.h` without every having to "run" the foreign binary. Writing this makes me feel dirty... – BeeOnRope Nov 07 '18 at 17:32
  • @BeeOnRope: the phrase "delightfully horrible" comes to mind. – Peter Cordes Nov 07 '18 at 17:33