Rickta59 589 Posted August 1, 2012 Share Posted August 1, 2012 Even though Energia is aimed at new users of the msp430, there is nothing stopping you from using the low level features of msp430-gcc and writing code in msp430 assembler. Looking around the net I'm guessing you will have little luck finding any examples of how to mix msp430 asm and ISR handlers. I'm going to try and fix that. The code below isn't a full working example. I'm presenting it mainly to show you how to use some of the features of GNU asm. I'll also try and explain some techniques I've found useful. The code below is similar to the 'C' version of the TimerSerial receive ISR handler in Energia. It basically sits around and waits for a serial character start bit to appear and drag the RX pin from high to low and then uses the counter compare feature of TimerA to sample the middle of each bit. It assembles these sampled bits into a byte and then stuffs it on the end of a ringbuffer if there is room. To write an ISR handler you need to create your code with a specific name. We will use a macro to setup the proper name as you will see below. The code shown here will setup a handler for the TIMERA1_VECTOR interrupt. The code: ;-------------------------------------------------------------------------------- ; File: rx_isr_asm.S - msp430 gcc asm version of SoftSerial_RX_ISR (faster code) ; ; Desc: This code handles receiving serial data using the TIMERA1 ISR ; It starts out in capture mode waiting for the start bit (transition from ; high to low) then switches to compare mode for 8 bits. It accumulates ; each lsb->msb received bit into a byte and stores it in a ringbuffer ; The code attempts to minimize register usage to just r15. ; ; This code is about ~21 cycles faster than the C version. ; However, the code size is about 22 bytes larger. Useful if you want ; to run your F_CPU at a really low frequency and still receive data ; ; Author: rick@kimballsoftware.com ; Date: 08-01-2012 ; #include #include "config.h" /* for F_CPU, BAUD_RATE, and RX_BUFFER_SIZE */ ;-------------------------------------------------------------------------------- ; --- Defines ;-------------------------------------------------------------------------------- #define PC r0 /* program counter */ #define TIMERA1_VEC 8 /* TIMERA1_VECTOR/2 (0x0010)/2 */ #define BIT_DUR (F_CPU/BAUD_RATE) /* cycle count between samples */ #define BIT_DUR_1_5 (BIT_DUR)+(F_CPU/BAUD_RATE/2) /* 1.5 times a BIT_DUR */ #define rb_head rx_buffer+0 /* offsets into ringbuffer_t struct */ #define rb_tail rx_buffer+2 #define rb_buf rx_buffer+4 ;-------------------------------------------------------------------------------- ; .interrupt - macro to declare an interrupt handler routine entry point ;-------------------------------------------------------------------------------- .macro .interrupt sn .global __isr_\sn __isr_\sn: .endm ;-------------------------------------------------------------------------------- ; --- BSS Data ;-------------------------------------------------------------------------------- .section .bss.rx_isr_asm .p2align 1,0 .lcomm state_vector,2 .lcomm rx_mask,2 .lcomm rx_data,2 ;-------------------------------------------------------------------------------- ; --- Code ;-------------------------------------------------------------------------------- .text .p2align 1,0 .global SoftSerial_RX_ISR .type SoftSerial_RX_ISR,@function .interrupt TIMERA1_VEC ;-------------------------------------------------------------------------------- SoftSerial_RX_ISR: ;-------------------------------------------------------------------------------- ; push, push, jump... ISR entry overhead 6 clocks push r15 ; preserve mov &TAIV, r15 ; reset interrupt flag by reading, we ignore btw add &state_vector,PC ; use state to jump directly to a handler without testing jmp handle_startbit ; 0 start bit jmp handle_databit_0_6 ; 2 bits 0 - 6 jmp handle_databit_0_6 ; 4 jmp handle_databit_0_6 ; 6 jmp handle_databit_0_6 ; 8 jmp handle_databit_0_6 ; 10 jmp handle_databit_0_6 ; 12 jmp handle_databit_0_6 ; 14 jmp handle_databit_7 ; 16 bit 7 handle_startbit: ; handle start bit add #2, &state_vector ; set next handler bic #CAP, &TACCTL1 ; go into compare mode add #BIT_DUR_1_5, &TA0CCR1 ; set next sample in the middle of D0 mov #0, &rx_data ; initialize recv byte mov #1, &rx_mask ; mask lsb -> msb jmp rx_isr_exit handle_databit_0_6: ; handle bits 0-6 add #2, &state_vector ; set next handler add #BIT_DUR, &TA0CCR1 ; set next sample time mov &rx_mask, r15 ; load mask bit #SCCI, &TACCTL1 ; check latched sampled data bit jz 1f bis r15, &rx_data ; if HI then set this bit 1: rla r15 ; make room for next data bit mov r15, &rx_mask jmp rx_isr_exit handle_databit_7: ; handle last bit and store in ringbuffer mov #0, &state_vector ; go back to capture handler bit #SCCI, &TACCTL1 ; check latched sample data bit jz 1f bis #BIT7, &rx_data 1: mov &rb_head, r15 ; load rx_buffer.head mov.b &rx_data, rb_buf(r15) ; rx_buffer.buffer[head] = rx_data inc r15 ; rx_buffer.head++ and #RX_BUFFER_SIZE-1, r15 ; take care of buffer wraparound, assumes power of 2 sized buf cmp r15, &rb_tail ; compare with tail jz 2f mov r15, &rb_head ; save new rx_buffer.head 2: bis #CAP, &TACCTL1 ; go back to capture mode rx_isr_exit: pop r15 ; standard exit ;xor.b #BIT6,&P1OUT ; uncomment to see where function ends. ; xor.b takes 5 clocks reti ; ISR exit overhead 5 cycles .Lfe1: .size SoftSerial_RX_ISR,.Lfe1-SoftSerial_RX_ISR ;-------------------------------------------------------------------------------- ; EOF rx_isr_asm.S ;-------------------------------------------------------------------------------- /* config.h */ #define F_CPU 1000000 #define BAUD_RATE 4800 #define RX_BUFFER_SIZE 16 typedef struct { volatile unsigned head, tail; unsigned char buffer[RX_BUFFER_SIZE] } ringbuffer_t; Energia will compile any '.S' files it finds with msp430-gcc. This allows us to use the C preprocessor. #include #include "config.h" /* for F_CPU, BAUD_RATE, and RX_BUFFER_SIZE */ In the case of the code above, we include the and "config.h" files. This provides our asm code with many constants we can use in the code. It also means we can define our own constants. GNU macros are an alternative to defines. The code above creates a macro called '.interrupt' which takes a number of the ISR you want to handle and does a global define of that name and its address. A little explanation of ISR handling is probably useful. Interrupt Service Routines in the msp430 are handled with the interrupt vector table. The table contains an array of entries that will handle each interrupt. Each ISR has an entry in this table which is really the address of a chunk of code than handles that ISR. The file defines constant with the names like "TIMERA1_VECTOR" which are really just the offsets from the start of the vector table. Depending on your processor the vector table can have 16, 32 or 64 entries. The launchpad chips have 16 entries. Each entry is a word address (2 bytes) that the processor jumps to when the ISR is triggered. To get our ISR wired up, we need to declare the proper global name so our code will get executed. So for the entry TIMERA1_VECTOR, it has a value of offset (0x0010) in the msp430g2231.h header file. We need to convert the offset into a numbered ISR name. The number is just half the offset. For TIMERA1_VECTOR it needs to be 8 and the name for our code will be '__isr_8'. I just defined my own constant called TIMERA1_VEC with a value of 8. There is probably a better way to do this and maybe Peter will chime in here. ;-------------------------------------------------------------------------------- ; .interrupt - macro to declare an interrupt handler routine entry point ;-------------------------------------------------------------------------------- .macro .interrupt sn .global __isr_\sn __isr_\sn: .endm ... then later .interrupt TIMERA1_VEC The code above shows the macro and how you would use it to declare the start of your ISR handler. When you instance the macro it takes any code between the .macro and .endm and uses your arguments to perform a replacement. In the case above, it substitutes '8' for any instances of \sn. Great! now we have defined all the glue for the ISR to get our code invoked. We just need to write the instructions that handles it. ISR handlers written in asm need to be careful of the registers they use. When an ISR routine starts the processor has pushed the address of the main threads next instruction on the stack along with the status register. If you want to use any registers in your code you have to save and restore them. At the end of your ISR handler you must exit with a 'RETI' instruction. This will pop the status register and the push the next saved next instruction into the PC register. The simplest ISR handler would just be a properly named interrupt handler and the 'RETI' instruction .interrupt TIMERA_VEC reti Even this simple handler is going to take away cycles from the main code. Each time this ISR handler is triggered it will use 11 clock cycles. 6 cycles to get from the running main thread to the first instruction you write. Then 5 clock cycles to restore the status register and pc register using the reti instruction. The ISR handler shown here is a lot more involved. It pushes r15 onto the stack so it has a register it can use. Just before reti the code pops that value back to its original value so the main thread isn't affected. I'm not going to walk through each line of the code. Let us take a look at one of the more interesting snippets of code: handle_databit_7: ; handle last bit and store in ringbuffer mov #0, &state_vector ; go back to capture handler bit #SCCI, &TACCTL1 ; check latched sample data bit jz 1f bis #BIT7, &rx_data 1: mov &rb_head, r15 ; load rx_buffer.head mov.b &rx_data, rb_buf(r15) ; rx_buffer.buffer[head] = rx_data inc r15 ; rx_buffer.head++ and #RX_BUFFER_SIZE-1, r15 ; take care of buffer wraparound, assumes power of 2 sized buf cmp r15, &rb_tail ; compare with tail jz 2f mov r15, &rb_head ; save new rx_buffer.head 2: bis #CAP, &TACCTL1 ; go back to capture mode In the code above you can see I'm freely mixing defined constants from config.h, msp430.h and my asm code. The 'handle_databit_7' function uses the rb_head and rb_tail defines to provided offsets into the address of the rx_buffer. 'rx_buffer' is the name of the ringbuffer defined in the external 'C' code you don't see. We just use rx_buffer and the linker will find it. You can look through the rest of the code and see some of the other GNU directives I used to declare some variables with the .lcomm directive. .lcomm variables will end up in the BSS section which uses some of your ram. They will get initialized to 0. The code goes into the .text section and will be stored in flash. Writing code in msp430 asm is going to take you longer and most of the time you don't need to resort to this. The msp430-gcc is very capable of optimizing the code to make it small and fast. Energia uses the -Os option to make small code. However sometimes you can do better. In the case of the code above, I was able to take some very reasonable 'C' code and reduce the cycle count down by about 20 cycles. Sometimes though, when you are doing communication code and running at a very low DCO clock frequency every cycle counts. Some useful tools you might want to look at are included with the msp430-gcc compiler. msp430-objdump, msp430-objcopy, msp430-nm and msp430-size. $ msp430-objdump -S yourprogram.elf | less # gets you an asm listing of your code $ msp430-objcopy -O ihex yourprogram.elf yourprogram.hex ; naken430util -disasm yourprogram.hex | less # cycle counts $ msp430-size yourprogram.elf # shows you the size of your code and how much bss and data you are using Well I hope this post is useful to someone and now there is a place on the net you can find an example of msp430 asm with ISR handling. Experiment with this option. If you find yourself wanting to tweak your code after you have done all the optimization you can in 'C' maybe this will give you the tools you need to make it work. -rick gordon, spirilis, bluehash and 1 other 4 Quote Link to post Share on other sites
pabigot 355 Posted August 2, 2012 Share Posted August 2, 2012 To get our ISR wired up, we need to declare the proper global name so our code will get executed. So for the entry TIMERA1_VECTOR, it has a value of word offset 0x0010 in the msp430g2231.h header file. We need to convert the offset into a numbered ISR name. The number is just half the offset. For TIMERA1_VECTOR it needs to be 8 and the name of our code will be '__isr_8'. I just defined my own constant called TIMERA1_VEC with a value of 8. There is probably a better way to do this and maybe Peter will chime in here. No, that's about what you have to do. As you note, TI provides the vector index through a macro with a value that is the byte offset from the start of the interrupt vector table. The linker symbol uses the word offset. Within mspgcc, arbitrary calculation can be done to construct the symbol from the macro value that's passed in through the __interrupt__ attribute; the assembler does not have such an attribute, so is limited to what can be expressed by preprocessing: mostly string catenation. That the vector value is a hex constant enclosed in parenthesis makes it tougher. I wouldn't consider it an acceptable situation for somebody who wants to write ISRs in assembly language (personally I haven't had that need yet), but the one time this came up on the mailing list the person who raised the question apparently didn't wish to file a tracker ticket requesting an enhancement, so I've never done anything about it. The solution would probably be a new assembler pseudo-op to construct the symbol. FWIW, for relatively small code sequences like this, if portability across devices is desired I'd recommend using inline assembly in a naked interrupt vector defined in C so you don't have to worry about whether you got the right value into the name. (Though I'd probably still do the development in assembly, and transform it to inline when it's finished.) Thanks for showing how to do it the bare-metal way. Rickta59 1 Quote Link to post Share on other sites
Rickta59 589 Posted August 2, 2012 Author Share Posted August 2, 2012 Thanks for the further explanation Peter. FWIW, for relatively small code sequences like this, if portability across devices is desired I'd recommend using inline assembly in a naked interrupt vector defined in C so you don't have to worry about whether you got the right value into the name. (Though I'd probably still do the development in assembly, and transform it to inline when it's finished.) I agree with that assement completely. The only problem with doing inline gnu assembler is the slightly funny syntax required to pass arguments to your asm from C. There are enough barriers to cross to getting started with msp430 asm. I didn't want to add anymore. It is probably the best approach once you feel comfortable with msp430 asm instructions and syntax. It is pretty simple, here is an example of a hello world for the watchdog timer: #include __attribute__((interrupt(WDT_VECTOR),naked)) void timera1_isr(void) { __asm__(" xor.b %[LED],%[PORT]\n" " reti\n" : : [LED] "i" (BIT6), [PORT] "m" (P1OUT) ); } That code would toggle BIT6 on P1OUT every time the WDT ISR triggered. The arguments %[LED] and %[PORT] get transformed into an immediate value of #64 and a memory address of &0x0021. The [LED] is the name you use in your asm, the "i" is how you want the value to be represented in the asm, and the (BIT6) is the C symbol you want to pass into the inline asm. Of course writing something like this in asm is a complete waste of time as the C compiler would produce the same code using the statement "P1OUT ^= BIT6;". -rick Quote Link to post Share on other sites
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.