000000widow-hackspacecon/fw/HackSpaceCon_AS7/Arduino/megatinycore/WInterrupts.c

536 lines
24 KiB
C

/* Completely rewritten 2021 Spence Konde;
* This is part of DxCore/megaTinyCore. Open source softeare (LGPL 2.1) see License.md for more info.
*
* attachInterrupt() that uses less flash and doesn't assimilate every interrupt vector...
*
* The ultimate goal was not achieved, but we can manually force the effect I wanted.
* Someone with deeper knowledge of how C/C++ decides that it will include the ISR from a file
* but it does give *a way* to write code that can use an attachInterrupt library and manually define pin interrupts.
* thisis important because attachInterrupt interrupts are miserably slow. Even in assembly, it's like 42 clocks to
* get to the loop, 7+ per pass through it, and a little over 40 more on the way out!
* Using lower numbered pins within a port minimizes how crappy this is. A port with 1 interrupt on bit 7 will take
* almost 42 clocks longer to be reached as it checks lower bits than one with it's one interrupt on bit 0
*/
#include <inttypes.h>
#include <Arduino.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <stdio.h>
#include "wiring_private.h"
#if !defined(CORE_ATTACH_OLD)
/* Well, I couldn't get it to autodetect awhere you're putting constant pins on interrupts, but there are tiny
* that you can call when using manual enabling;. Referencing them causes it to grab the associated vector, though it does have to be called to actually enable it - that's how we
* avoid wasting resourceses - each one not used also drops an array of 8 pointers to ISRs.
*
* attachPortAEnable() attachPortBEnable() attachPortCEnable() attachPortDEnable()
* attachPortEEnable() attachPortFEnable() attachPortGEnable()
*
* So you get the option wehether to enable it for each port. Each port also doesn't get it's own bloated ISR composed almost entirely of pushes and pops for calling a function
* it was done by doing rather evil things here but it does appear to work!
*/
/*Only create arrays for ports that exist) */
#if !defined(CORE_ATTACH_ALL)
extern voidFuncPtr * intFunc_A;
#ifdef PORTB_PINS
extern voidFuncPtr * intFunc_B;
#endif
#ifdef PORTC_PINS
extern voidFuncPtr * intFunc_C;
#endif
#else
voidFuncPtr intFunc_A[8];
#ifdef PORTB_PINS
voidFuncPtr intFunc_B[PORTB_PINS];
#endif
#ifdef PORTC_PINS
voidFuncPtr intFunc_C[PORTC_PINS];
#endif
volatile voidFuncPtr * intFunc[] = {
intFunc_A,
#ifdef PORTB_PINS
intFunc_B,
#endif
#ifdef PORTC_PINS
intFunc_C
#endif
};
#endif
#if !defined(CORE_ATTACH_ALL)
#if defined(PORTC_PINS)
volatile voidFuncPtr * intFunc[] = {NULL, NULL, NULL};
#elif defined(PORTB_PINS)
volatile voidFuncPtr * intFunc[] = {NULL, NULL};
#else
volatile voidFuncPtr * intFunc[] = {NULL};
#endif
#endif
volatile uint8_t* portbase = (volatile uint8_t*)((uint16_t)0x400);
/* On modern AVRs, the PORT registers start at 0x400
* At 32 bytes per port, with plenty of room to spare, up to 8 ports will fit before we are forced into 0x500.
* And the rest of this implementation falls over if there's an 8th port because it relies on VPORTs
* of which there are only 7 ports worth of registers available. So this implementation is guaranteed not to work on a
* future part with 8 ports anyway. We will cross that bridge once Microchip has announced intent to build it.
*/
void attachInterrupt(uint8_t pin, void (*userFunc)(void), uint8_t mode) {
uint8_t bitpos = digitalPinToBitPosition(pin);
if (bitpos == NOT_A_PIN) {
return;
}
uint8_t port = digitalPinToPort(pin);
switch (mode) {
case CHANGE:
mode = PORT_ISC_BOTHEDGES_gc;
break;
case FALLING:
mode = PORT_ISC_FALLING_gc;
break;
case RISING:
mode = PORT_ISC_RISING_gc;
break;
case LOW:
mode = PORT_ISC_LEVEL_gc;
break;
default:
return;
}
if (intFunc[port] != NULL && userFunc != NULL) {
// if it is null the port is not enabled for attachInterrupt, and obviously a null user function is invalid too.
intFunc[port][bitpos] = userFunc;
uint8_t portoffset = ((port << 5) & 0xE0) + 0x10 + bitpos;
uint8_t oldSREG = SREG;
cli();
// We now have the port, the mode, the bitpos and the pointer
uint8_t settings = *(portbase + portoffset) & 0xF8;
*(portbase + portoffset) = settings | mode;
SREG = oldSREG;
}
}
#if !defined(CORE_ATTACH_EARLYCLEAR) // late clear.
void __attribute__((naked)) __attribute__((used)) __attribute__((noreturn)) isrBody() {
asm volatile (
"AttachedISR:" "\n\t" // as the scene opens, we have r16 on the stack already, portnumber x 2 in the r16
"push r0" "\n\t" // so we start with a normal prologue
"in r0, 0x3f" "\n\t" // The SREG
"push r0" "\n\t" // on the stack
#if PROGMEM_SIZE > 0x10000
"in r0, 0x3b" "\n\t" // RAMPZ
"push r0" "\n\t" // on the stack.
#endif
"push r1" "\n\t" // We don't need r1 but the C code we call
"eor r1, r1" "\n\t" // is going to want this to be zero....
"push r15" "\n\t" // push r15 (we use it - it's call-saved)
"push r17" "\n\t" // and now we push all call used registers
"push r18" "\n\t" // except r16 which was pused over in WInterrupts_Px
"push r19" "\n\t"
"push r20" "\n\t"
"push r21" "\n\t"
"push r22" "\n\t"
"push r23" "\n\t"
"push r24" "\n\t"
"push r25" "\n\t"
"push r26" "\n\t"
"push r27" "\n\t"
"push r28" "\n\t" // Not call used, but we use it.
"push r29" "\n\t" // same thing.
"push r30" "\n\t"
"push r31" "\n\t"
::);
asm volatile ( // This gets us the address of intFunc in Y pointer reg.
"add r26, r16" "\n\t" // get the address of the functions for this port (r 16 is 2x the port number)
"adc r27, r1" "\n\t" // by adding that offset to the address we had the compiler generate the ldi's for
"ld r28, X+" "\n\t" // load the pointer to this port's function array...
"ld r29, X" "\n\t" // ... to the Y pointer reg.
"add r16, r16" "\n\t" // double r16, so it is 4x port number - that's the address of the start of the VPORT
"subi r16, 253" "\n\t" // Now this is the address of the VPORTx.INTFLAGS
"mov r26, r16" "\n\t" // r16 to x reg low byte ASM always lists the destination operand first.
"ldi r27, 0" "\n\t" // clear x high byte
"ld r15, X" "\n\t" // Load flags to r15"
"sbiw r26, 0" "\n\t" // subtract 0 from it - this serves as a single-word way to test if it's 0, because it will still set the Zero flag. It's no faster than cpi r26, 0, cpc r27, r1 but takes less flash.
"breq AIntEnd" "\n\t" // port not enabled, null pointer, just clear flags end hit the exit ramp.
"mov r17, r15" "\n\t" // copy the flags to r17
"AIntLoop:" "\n\t"
"lsr r17" "\n\t" // shift it right one place, now the LSB is in carry.
"brcs .+6" "\n\t" // means we have something to do this time.
"breq AIntEnd" "\n\t" // This means carry wasn't set and r17 is 0. - we're done.
"adiw r28, 2" "\n\t" // otherwise it's not a the int we care about, increment Y by 2, so it will point to the next element.
"rjmp AIntLoop" "\n\t" // restart the loop in that case.
"ld r30, Y+" "\n\t" // load the function pointer;
"ld r31, Y+" "\n\t" // load the function pointer;
"sbiw r30, 0" "\n\t" // zero-check it.
"breq AIntLoop" "\n\t" // restart loop if it is, don't call the null pointer
"icall" "\n\t" // call their function, which is allowed to shit on any upper registers other than 28, 29, 16, and 17.
"rjmp AIntLoop" "\n\t" // Restart loop after.
"AIntEnd:" "\n\t" // sooner or later r17 will be 0 and we'll branch here.
"mov r26, r16" "\n\t" // We previously stashed the VPORT address in r16, so copy that to low byte of X pointer <<BUG WAS HERE, r26 was copied to r16 instead of the other way around>>
"ldi r27, 0" "\n\t" // high byte is 0, cause we're targeting the VPORT, address < 0x20
"st X, r15" "\n\t" // store to clear the flags.... that we recorded at the start of the interrupt. (LATECLEAR)
"pop r31" "\n\t" // clean up a million registers
"pop r30" "\n\t"
"pop r29" "\n\t"
"pop r28" "\n\t"
"pop r27" "\n\t"
"pop r26" "\n\t"
"pop r25" "\n\t"
"pop r24" "\n\t"
"pop r23" "\n\t"
"pop r22" "\n\t"
"pop r21" "\n\t"
"pop r20" "\n\t"
"pop r19" "\n\t"
"pop r18" "\n\t"
"pop r17" "\n\t" // skip 16 again - it's way down at the end, because it was pushed earlier
"pop r15" "\n\t"
"pop r1" "\n\t"
#if PROGMEM_SIZE > 0x10000
"pop r0" "\n\t"
"out 0x3b, r0" "\n\t"
#endif
"pop r0" "\n\t"
"out 0x3f, r0" "\n\t" // between these is where there had been stuff added to the stack that we flushed.
"pop r0" "\n\t"
"pop r16" "\n\t" // this was the reg we pushed back in the port-specific file.
"reti" "\n" // now we should have the pointer to the return address fopr the ISR on top of the stack, so reti're
:: "x" ((uint16_t)(&intFunc))
);
__builtin_unreachable();
}
#else // EARLYCLEAR
void __attribute__((naked)) __attribute__((used)) __attribute__((noreturn)) isrBody() {
asm volatile (
"AttachedISR:" "\n\t" // as the scene opens, we have r16 on the stack already, portnumber x 2 in the r16
"push r0" "\n\t" // so we start with a normal prologue
"in r0, 0x3f" "\n\t" // The SREG
"push r0" "\n\t" // on the stack
"in r0, 0x3b" "\n\t" // RAMPZ
"push r0" "\n\t" // on the stack.
"push r1" "\n\t" // We don't need r1 but the C code we call
"eor r1, r1" "\n\t" // is going to want this to be zero....
// "push r15" "\n\t" // We don't use r15 with EARLYCLEAR.
// "push r17" "\n\t" // Eyyyyy lookit dat, we don't need r17 for EARLYCLEAR either. So that's two fewer call-saved registers that we need to save and restore.
"push r18" "\n\t" // and now we push all call used registers
"push r19" "\n\t" // except r16 which was pushed over in WInterrupts_Px
"push r20" "\n\t"
"push r21" "\n\t"
"push r22" "\n\t"
"push r23" "\n\t"
"push r24" "\n\t"
"push r25" "\n\t"
"push r26" "\n\t"
"push r27" "\n\t"
"push r28" "\n\t" // Not call used, but we use it.
"push r29" "\n\t" // And we need it's call-saved-ness ourselves to maintain state through the
"push r30" "\n\t"
"push r31" "\n\t"
::);
asm volatile ( // This gets us the address of intFunc in Y pointer reg.
"add r26, r16" "\n\t" // get the address of the functions for this port (r 16 is 2x the port number)
"adc r27, r1" "\n\t" // by adding that offset to the address we had the compiler generate the ldi's for;
"ld r28, X+" "\n\t" // That was the address of the start of array of pointers to arrays of pointers
"ld r29, X" "\n\t" // Now we loaded Y with the start of our port's array of function pointers
"add r16, r16" "\n\t" // double r16, so it is 4x port number - that's the address of the start of the VPORT
"subi r16, 253" "\n\t" // Add 3; now this is the address of the VPORTx.INTFLAGS
"mov r26, r16" "\n\t" // r16 holds the INTFLAGs address, copy it to X reg low byte.
"ldi r27, 0" "\n\t" // clear x high byte
"ld r16, X" "\n\t" // Load flags to r16"
"st X, r16" "\n\t" // EARLYCLEAR
"sbiw r26, 0" "\n\t" // this will set flag if it's zero.
"breq AIntEnd" "\n\t" // port not enabled, null pointer, just clear flags end hit the exit ramp.
"AIntLoop:" "\n\t"
"lsr r16" "\n\t" // shift it right one place, now the LSB is in carry.
"brcs .+6" "\n\t" // means we have something to do this time.
"breq AIntEnd" "\n\t" // This means carry wasn't set and r17 is 0. - we're done.
"adiw r28, 2" "\n\t" // otherwise it's not the int we care about, increment Y by 2, so it will point to the next element.
"rjmp AIntLoop" "\n\t" // restart the loop in that case.
"ld r30, Y+" "\n\t" // load the function pointer simultaneously advancing the Y pointer so next iteration it will
"ld r31, Y+" "\n\t" // be pointing to the next function pointer.
"sbiw r30, 0" "\n\t" // zero-check the pointer before we call it.
"breq AIntLoop" "\n\t" // restart loop if pointer is null. There is no interrupt handler yet interrupt is enabled and fired; Don't call the null pointer
"icall" "\n\t" // call their function, which is allowed to shit on any upper registers other than 28, 29, 16, and 17.
"rjmp AIntLoop" "\n\t" // Restart loop after.
"AIntEnd:" "\n\t" // sooner or later r17 will be 0 and we'll branch here.
// with EARLYCLEAR variant, we don't need to do anything other than cleaning up working registers - flags already cleared.
"pop r31" "\n\t" // clean up a million registers
"pop r30" "\n\t"
"pop r29" "\n\t"
"pop r28" "\n\t"
"pop r27" "\n\t"
"pop r26" "\n\t"
"pop r25" "\n\t"
"pop r24" "\n\t"
"pop r23" "\n\t"
"pop r22" "\n\t"
"pop r21" "\n\t"
"pop r20" "\n\t"
"pop r19" "\n\t" // skip 16 again - it's way down at the end, because it was pushed earlier
"pop r18" "\n\t"
//"pop r17" "\n\t" // Early clear doesn't need the extra registers.
//"pop r15" "\n\t" // Early clear doesn't need the extra registers.
"pop r1" "\n\t"
"pop r0" "\n\t"
"out 0x3b, r0" "\n\t"
"pop r0" "\n\t"
"out 0x3f, r0" "\n\t" // between these is where there had been stuff added to the stack that we flushed.
"pop r0" "\n\t"
"pop r16" "\n\t" // this was the reg we pushed back in the port-specific file.
"reti" "\n" // now we should have the pointer to the return address fopr the ISR on top of the stack, so reti're
:: "x" ((uint16_t)(&intFunc))
);
__builtin_unreachable();
}
#endif
void detachInterrupt(uint8_t pin) {
/* Get bit position and check pin validity */
uint8_t bitpos = digitalPinToBitPosition(pin);
if (bitpos == NOT_A_PIN) {
return;
}
uint8_t port = digitalPinToPort(pin);
uint8_t p = (port << 5) + bitpos;
*(((volatile uint8_t*) &PORTA_PIN0CTRL) + p) &= 0xF8; // int off....
*((volatile uint8_t*) ((uint16_t)((port << 4) + 3))) = (1 << bitpos);// flag clear
intFunc[port][bitpos] = 0; // clear pointer
}
/* If not enabling attach on all ports always, instead the identical ISR definitions are in the WInterruptsA/B/C/D/E/F/G.c files.
* Okay, so what the f-- is going on here?
* To avoid each interrupt vector having it's own lengthy prologue and epilog separaely, which is needed in order for a function call to be made in an ISR
* All we do is push an upper register onto the stack so can load a value twice the PORT number there, and jump to actual function that does the work here.
*
* The isrBody() has two consecutive blocks of inline assembly, First, do what is basically a standard prologue, for something that calls a function (thus there is only one prologue, instead of one per port).
* We finish the prologue but we need to push the Y pointer and r15 (call saved) for this routine. Then we slip out out of that assembly block just to grab the pointer to IntFunc array, which we need in a pointer reg;
* The second block of inline ASM specifies that the pointer to the array of pointers to arrays of pointers to interrupt functions be passed in the X pointer register
*
* Assembly is split up only so we can grab that address through that in a constraint.
* We couldn't have done that any sooner, we had nowhere to put it. To that we add the pre-doubled port number - pointers are 2 bytes so we need that
* doubling.
*
* We can then load the pointer to this port's int functions to the call-saved Y register.
* Sutract 0 from that pointer to check that it's not 0. A null pointer is of no use. Assuming that there's a pointer, we continue.
* Next we need the INTFLAGs. We will get them from the VPORT, not the PORT. This is much easier. We double r16 for address of VPORT and add 3 with subi to
* get INTFLAGs, copy to r26, and load the 0 high byte to r27, read the flags into r15.
*
* Copy flags to r17 and then start the loop. We will check the intflags one at a time. We rightshift once, and check if carry is set, indicating we pushed a 1 out, if so we jump over
* three instructions handling the case if we it was a 0: We check then check first for the carry bit. If not, then we check if the zero flag is set, If THAT is then what's left of flags
* is and we've handled them all, and we jump to end. and if it's not, we increment the Y pointer by 2 and return to start of the loop
*
* For each of the ones that we do have a flag for, we load that pointer into Z with postincrement, subtract 0 from it and look at zero flag to make sure it's not null.
* assuming it's not, we fire icall to call the user function. Either way we then repeat the loop until out of flags.
* which at latest will happen when we're also at end of the ports intfunc array....
* Then, with the initial flags still in 15 and the the VPORT address in r16 copy that once more to a pointer register, 0 the high byte, and store the flags value we read to clear it.
* then it's just a matter of making sure we pop everything we pushed onto the stack in the reverse order, including r16 followed by the reti to exit the interrupt..
*/
#if defined(CORE_ATTACH_ALL)
ISR(PORTA_PORT_vect, ISR_NAKED) {
asm volatile(
"push r16" "\n\t"
"ldi r16, 0" "\n\t"
#if PROGMEM_SIZE > 8192
"jmp AttachedISR" "\n\t"
#else
"rjmp AttachedISR" "\n\t"
#endif
::);
__builtin_unreachable();
}
#ifdef PORTB_PINS
ISR(PORTB_PORT_vect, ISR_NAKED) {
asm volatile(
"push r16" "\n\t"
"ldi r16, 2" "\n\t"
#if PROGMEM_SIZE > 8192
"jmp AttachedISR" "\n\t"
#else
"rjmp AttachedISR" "\n\t"
#endif
::);
__builtin_unreachable();
}
#endif
#ifdef PORTC_PINS
ISR(PORTC_PORT_vect, ISR_NAKED) {
asm volatile(
"push r16" "\n\t"
"ldi r16, 4" "\n\t"
#if PROGMEM_SIZE > 8192
"jmp AttachedISR" "\n\t"
#else
"rjmp AttachedISR" "\n\t"
#endif
::);
__builtin_unreachable();
}
#endif
#endif
#else /* This is the old implementation, and it's copyright boilerplate. */
/* -*- mode: jde; c-basic-offset: 2; indent-tabs-mode: nil -*- */ //<---- FFS! That was in the original!
/*
Part of the Wiring project - http://wiring.uniandes.edu.co
Copyright (c) 2004-05 Hernando Barragan
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General
Public License along with this library; if not, write to the
Free Software Foundation, Inc., 59 Temple Place, Suite 330,
Boston, MA 02111-1307 USA
Modified 24 November 2006 by David A. Mellis
Modified 1 August 2010 by Mark Sproul
(Spence: and presumably someone who didn't sign to adapt it to modernAVRs,
since nothing that this implementation would work with had been released until
2016)
*/
#include <inttypes.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <stdio.h>
#include "wiring_private.h"
static volatile voidFuncPtr intFunc[EXTERNAL_NUM_INTERRUPTS];
void attachInterrupt(uint8_t pin, void (*userFunc)(void), uint8_t mode) {
/* Get bit position and check pin validity */
uint8_t bit_pos = digitalPinToBitPosition(pin);
if (bit_pos == NOT_A_PIN) return;
/* Get interrupt number from pin */
uint8_t interruptNum = (digitalPinToPort(pin) * 8) + bit_pos;
/* Check interrupt number and apply function pointer to correct array index */
if (interruptNum < EXTERNAL_NUM_INTERRUPTS) {
intFunc[interruptNum] = userFunc;
// Configure the interrupt mode (trigger on low input, any change, rising
// edge, or falling edge)
switch (mode) {
case CHANGE:
mode = PORT_ISC_BOTHEDGES_gc;
break;
case FALLING:
mode = PORT_ISC_FALLING_gc;
break;
case RISING:
mode = PORT_ISC_RISING_gc;
break;
case LOW:
mode = PORT_ISC_LEVEL_gc;
break;
default:
return;
}
// Enable the interrupt.
/* Get pointer to correct pin control register */
PORT_t *port = digitalPinToPortStruct(pin);
volatile uint8_t* pin_ctrl_reg = getPINnCTRLregister(port, bit_pos);
/* Clear any previous setting */
*pin_ctrl_reg &= ~(PORT_ISC_gm);
/* Apply ISC setting */
*pin_ctrl_reg |= mode;
}
}
void detachInterrupt(uint8_t pin) {
/* Get bit position and check pin validity */
uint8_t bit_pos = digitalPinToBitPosition(pin);
if (bit_pos == NOT_A_PIN) return;
/* Get interrupt number from pin */
uint8_t interruptNum = (digitalPinToPort(pin) * 8) + bit_pos;
if (interruptNum < EXTERNAL_NUM_INTERRUPTS) {
// Disable the interrupt.
/* Get pointer to correct pin control register */
PORT_t *port = digitalPinToPortStruct(pin);
volatile uint8_t* pin_ctrl_reg = getPINnCTRLregister(port, bit_pos);
/* Clear ISC setting */
*pin_ctrl_reg &= ~(PORT_ISC_gm);
intFunc[interruptNum] = 0;
}
}
static void port_interrupt_handler(uint8_t port) {
PORT_t *portStruct = portToPortStruct(port);
/* Copy flags */
uint8_t int_flags = portStruct->INTFLAGS;
uint8_t bit_pos = PIN0_bp, bit_mask = PIN0_bm;
/* Iterate through flags */
while(bit_pos <= PIN7_bp) {
/* Check if flag raised */
if (int_flags & bit_mask) {
/* Get interrupt */
uint8_t interrupt_num = port*8 + bit_pos;
/* Check if function defined */
if (intFunc[interrupt_num] != 0) {
/* Call function */
intFunc[interrupt_num]();
}
}
bit_pos++;
bit_mask = (bit_mask << 1);
}
/* Clear flags that have been handled */
portStruct->INTFLAGS = int_flags;
}
#define IMPLEMENT_ISR(vect, port) \
ISR(vect) { \
port_interrupt_handler(port);\
} \
IMPLEMENT_ISR(PORTA_PORT_vect, PA)
#if defined(PORTB_PORT_vect)
IMPLEMENT_ISR(PORTB_PORT_vect, PB)
#endif
#if defined(PORTC_PORT_vect)
IMPLEMENT_ISR(PORTC_PORT_vect, PC)
#endif
#if defined(PORTD_PORT_vect)
IMPLEMENT_ISR(PORTD_PORT_vect, PD)
#endif
#if defined(PORTE_PORT_vect)
IMPLEMENT_ISR(PORTE_PORT_vect, PE)
#endif
#if defined(PORTF_PORT_vect)
IMPLEMENT_ISR(PORTF_PORT_vect, PF)
#endif
// Nope, there was never attachInterrupt for PORTG and nobody complained for 2 years. Not going to change that -Speence 3/5/22
#endif