Assembler code for Arduino
Posted: 07 Nov 2020, 00:24
I never had to bother with AVR assembler before, but for the FrSkyV8Tx project, I wanted to implement the option to bit-bang SPI output. Normally it uses hardware SPI, but this means you have to use pins 11, 12, 13. Some people might want to use different pins - for example if you have an old AtMega328 based multiprotocol module that you want to run my library on, then it needs to use pins 4, 6, and 7.
I wrote a C (normal Arduino language) bit bang routine, and it worked fine on a 16MHz Arduino, but didn't work with the CC2500 module on an 8MHz one. I'm still not really sure why - according to the data sheet, there's no minimum speed limit for talking SPI to the CC2500 module, providing you can manage the required data throughput (which it could, easily).
Anyway, I decided to get my hands dirty with some AVR assembler, and it was surprisingly easy. The AVR chips have a nice clean architecture and a simple instruction set.
All you need to do in your (normal C++) sketch is declare a function extern "C" like this
Then you add a separate file in the same folder - the filename can be anything but it must end with .S - the Arduino IDE automatically assembles and links such files when you compile your sketch.
Parameters passed to the function are stuffed into registers r25, r24, r23, ..., where the assembly code can read and work with them. If you want to return a value (which I didn't) then it also goes in the r24 r25 pair if it's 16 bits or fewer.
There are a few rules about which registers your assembler code has to preserve, but r0, r18-r27, and r30-r31 are fair game, so I stuck to using those. The r30 and r31 pair also work as the Z pointer which is able to access any port register. There are faster ways of manipulating ports directly, but you need to know which bit of which port at assembly time - and I didn't want that for my library.
Here's my assembler code. I was amazed that once I got rid of the typos so that it would assemble, it ran first time!
I don't really recommend using assembler unless you really need the speed, or you just want to experience some old-time "back to the bare metal" computing!
I wrote a C (normal Arduino language) bit bang routine, and it worked fine on a 16MHz Arduino, but didn't work with the CC2500 module on an 8MHz one. I'm still not really sure why - according to the data sheet, there's no minimum speed limit for talking SPI to the CC2500 module, providing you can manage the required data throughput (which it could, easily).
Anyway, I decided to get my hands dirty with some AVR assembler, and it was surprisingly easy. The AVR chips have a nice clean architecture and a simple instruction set.
All you need to do in your (normal C++) sketch is declare a function extern "C" like this
Code: Select all
extern "C" {
void bitBangSPIout(volatile uint8_t* mosiPort, uint8_t mosiMask, volatile uint8_t* sckPort, uint8_t sckMask, uint8_t b);
}
Parameters passed to the function are stuffed into registers r25, r24, r23, ..., where the assembly code can read and work with them. If you want to return a value (which I didn't) then it also goes in the r24 r25 pair if it's 16 bits or fewer.
There are a few rules about which registers your assembler code has to preserve, but r0, r18-r27, and r30-r31 are fair game, so I stuck to using those. The r30 and r31 pair also work as the Z pointer which is able to access any port register. There are faster ways of manipulating ports directly, but you need to know which bit of which port at assembly time - and I didn't want that for my library.
Here's my assembler code. I was amazed that once I got rid of the typos so that it would assemble, it ran first time!
Code: Select all
.global bitBangSPIout
bitBangSPIout: ; (mosiPort, mosiMask, sckPort, sckMask, b)
mov r23, r22 ; r22 is mosiOrMask
com r23 ; make r23 mosiAndmask
mov r19, r18 ; r18 is sckOrMask
com r19 ; make r19 sckAndMask
mov r0, r16 ; byte to transmit in r0
ldi ZH, 0 ; high byte of Port addresses always zero
ldi r25, 8 ; loop counter
loop:
mov ZL, r24
ld r21, Z ; existing mosiPort contents to r21
lsl r0 ; transmitting MSB first - this puts the next bit to send into the carry
brcs bitHigh
and r21, r23 ; mask the mosi bit low
rjmp driveMosi
bitHigh:
or r21, r22 ; mask the mosi bit high
driveMosi:
st Z, r21 ; write to mosiPort
mov ZL, r20
ld r21, Z ; existing sckPort contents to r21
or r21, r18 ; mask the sck bit high
st Z, r21 ; write to sckPort - pulse SCK high
and r21, r19 ; mask sck low again
st Z, r21 ; write to sckPort - make SCK low again
dec r25
brne loop ; loop 8 bits
mov ZL, r24 ; finish by seting MOSI high
ld r21, Z
or r21, r22
st Z, r21
ret