Enable Alternate Pins for Interrupts

I am trying to enable additional pins to be used for interrupts on the board besides the predetermined ones: (0, 1, 4, 5, 6, 7, 8, A1 -or 16-, A2 - or 17)

I need a total of 12, so I need to modify the appropriate register(s) to set 3 more pins to be used as interrupts. I know that the SAMD21 can handle up to 16 external interrupts, and looking at the schematic for the MKRZERO it appears that more than 9 of these pads on the IC are wired to pins on the board, so it should be possible. Other boards based around this microprocessor support 9+ interrupts.

Let's take pin 2 for example: Pin 2 is wired to PA10, so I should be able to enable it to use EXTINT[10]. What register do I need to write to to do this?

Hi gavinremme,

The MKRZero should just about be able to support 12 interrupts, if you include the Non Maskable Interrupt (NMI) on D11.

The 16 interrupt channels (0 to 15) and NMI are allocated as follows on the MKRZero:

0: D8
1: D9
2: A0 & A1
3: A2 & D10
4: A3 & D6
5: A4 & D7
6: A5, D0 & D14
7: A6, D1 & D13
8: -
9: D12
10: D2 & D4
11: D3 & D5
12: -
13: -
14: -
15: -
NMI: D11

This provides 11 unshared interrupt channels, plus the NMI.

It's also possible for more than one pin to share the same interrupt channel. If your pulses are long enough, you can test which pin is causing the interrupt from within the interrupt function using digitalRead().

The following example code shows how to activate the NMI:

// SAMD21 Non Maskable Interrupt (NMI) Test on PA08 
void setup() {
  PORT->Group[g_APinDescription[LED_BUILTIN].ulPort].DIRSET.reg = 
    1 << g_APinDescription[LED_BUILTIN].ulPin;      // Set digital pin LED_BUILTIN to an OUTPUT

  PORT->Group[PORTA].PINCFG[8].bit.INEN = 1;        // Enable the (optional) pull-up resistor on PA08
  PORT->Group[PORTA].PINCFG[8].bit.PULLEN = 1;      
  PORT->Group[PORTA].PINCFG[8].bit.PMUXEN = 1;      // Enable the port multiplexer on PA08
  PORT->Group[PORTA].PMUX[8 >> 1].reg |= PORT_PMUX_PMUXE_A; // Switch the port multiplexer to EIC on PA08
 
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_ID_EIC |         // Disconnect GCLK0 from the EIC
                      GCLK_CLKCTRL_GEN_GCLK0;       // Select GCLK0                                          
  while (GCLK->STATUS.bit.SYNCBUSY);                // Wait for synchronization

  EIC->NMICTRL.reg = EIC_NMICTRL_NMISENSE_RISE;     // Trigger an NMI interrupt on the a rising edge
  // EIC->NMICTRL.reg |= EIC_NMICTRL_NMIFILTEN;       // Enable NMI filter

  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |          // Enable GCLK
                      GCLK_CLKCTRL_ID_EIC |         // Connect GCLK0 to the EIC
                      GCLK_CLKCTRL_GEN_GCLK0;       // Select GCLK0                                          
  while (GCLK->STATUS.bit.SYNCBUSY);                // Wait for synchronization
}

void loop() {}

void NMI_Handler() {
  EIC->NMIFLAG.bit.NMI = 1;                         // Clear the NMI interrupt flag
  PORT->Group[g_APinDescription[LED_BUILTIN].ulPort].OUTTGL.reg = 
    1 << g_APinDescription[LED_BUILTIN].ulPin;      // Toggle the LED_BUILTIN
}

Thank you! So can D11 now be used with attachInterrupt()?

How would I enable other pins not setup to be used as interrupts besides the NMI to act as interrupts, like PA10/EXTINT[10]? Just change the 8 to a 10, like this? PORT->Group[PORTA].PINCFG[10].bit.INEN = 1;

What file contains the register address definitions? How did you know specifically to use EIC_NMICTRL_NMISENSE_RISE for example. Basically I'd like to be able to do this myself in the future.

Hi gavinremme,

Thank you! So can D11 now be used with attachInterrupt()?

The Non Maskable Interrupt (NMI) isn't used by the attachInterrupt() function, as it's usually employed as an high priority (second only to reset) interrupt, in order to handle emergency hardware fault conditions. Nevertheless it can be used as an additional interrupt.

In the absence of a specific NMI attachInterrupt() function, it's necessary to use register manipulation instead.

How would I enable other pins not setup to be used as interrupts besides the NMI to act as interrupts, like PA10/EXTINT[10]? Just change the 8 to a 10, like this? PORT->Group[PORTA].PINCFG[10].bit.INEN = 1;

Ok, looking at the MKRZero's "variant.cpp" file, I now see what you mean. Only some of the board's pins are configured for interrupts.

I was using a custom SAMD21 board with old Arduino core code to test some interrupt code. In the past it was possible to configure two pins with the same interrupt channel and get them to call the same interrupt function. In the lastest Arduino core code this doesn't work. I'm currently looking at finding a solution.

In the meantime it's possible to get interrupt functionality out of D12, by going the the MKRZero's "variant.cpp" located (on my Windows machine) at:

C:\Users\Computer\AppData\Local\Arduino15\packages\arduino\hardware\samd\1.8.6\variants\mkrzero

and changing the entry for D12 to:

{ PORTA,  9, PIO_SERCOM_ALT, (PIN_ATTR_DIGITAL                             ), ADC_Channel17,  NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_9 }, // SCL:  SERCOM2/PAD[1]

This will enable interrupt channel EXTINT[9] on D12. You should also be able to obtain interrupts on pins: D0, D1, D4, D5, D6, D7, D8, D9, A3 & A4 by default.

What file contains the register address definitions? How did you know specifically to use EIC_NMICTRL_NMISENSE_RISE for example. Basically I'd like to be able to do this myself in the future.

An explanation of how to access the SAMD21 resgister definitions is provided on post #3 of the following thread on the Arduino Zero forum: Addressing SAM D21 ADC - Arduino Zero - Arduino Forum.

The register definition files can be used in conjuction with the SAMD21 datasheet and board schematic. I normally use a simple editor like Notepad++ to view the register definitions then cut 'n' paste them into a sketch on the Arduino IDE.

After looking into the situation, it appears that the only really practical solution is to change the entry for D12 in the MKRZero's "variant.cpp" file. However, this will be overwritten each time the Arduino SAMD21 core updated (currently version 1.8.6).

Also, it's not possible to simply bypass the Arduino attachInterrupt() function with register manipulation, as the EIC_Handler() interrupt service routine is used by and is tightly integrated with the Arduino core code.

The easiest way change a pin's interrupt functionality, is to simply change the pin entries in the "variant.cpp" file. In this way it's possible to get a two pins with the same interrupt channel to call the same interrupt function, by altering the pin's EXTERNAL_INT_NONE attribute to is corresponding EXTERNAL_INT_X channel.

At least changing the "variant.cpp" file for D12 and the NMI should provide you with 12 separate interrupts.

I got it working! Editing the variant.cpp is allowing me to use other pins with attachInterrupts(). Thanks once again. I'll have to pay attention when updating to make sure the file doesn't get overwritten.

I'm back haha - with more issues. I have moved from a MKRZero board and onto an Atmel Xplained Pro. It's still a SAMD21 board, but with more pins broken out. I have flashed it with the MKRZero firmware, and cannot get interrupt 3 on D10/MISO to work after editing variant.cpp.

Here's my test program so I can make sure my ultrasonic sensors are working on their individual pins. It allows me to test one of them at a time.

#define CH0 10
volatile unsigned long ch_0_rising, ch0_duty_cycle;
void ch0_rising_interrupt();
void ch0_falling_interrupt();

void setup() {
  SerialUSB.begin(9600);
  while(!SerialUSB);
  pinMode(CH0, INPUT);
  attachInterrupt(digitalPinToInterrupt(CH0), ch0_rising_interrupt, RISING);
  config_pwm();
  SerialUSB.println("Setup complete");
}

void loop() {
  SerialUSB.println("CH0: " + String(ch0_duty_cycle));
  //SerialUSB.println(ch0_duty_cycle));
}

//ch0
void ch0_rising_interrupt() {
  ch_0_rising = micros();
  attachInterrupt(digitalPinToInterrupt(CH0), ch0_falling_interrupt, FALLING);
}

void ch0_falling_interrupt() {
  ch0_duty_cycle = micros() - ch_0_rising;
  attachInterrupt(digitalPinToInterrupt(CH0), ch0_rising_interrupt, RISING);
}

void config_pwm() {

  //PWM resolution = frequency_gclock/N(TOP+1) N is clock divider which is 256 for us

  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(256) |          // Divide the 48MHz clock source by divisor 480: 48MHz/480=0.1MHz (same number as below)
                    GCLK_GENDIV_ID(4);              // Select Generic Clock (GCLK) 4

  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC |           // Set the duty cycle to 50/50 HIGH/LOW
                     GCLK_GENCTRL_GENEN |         // Enable GCLK4
                     GCLK_GENCTRL_SRC_DFLL48M |   // Set the 48MHz clock source
                     GCLK_GENCTRL_ID(4);          // Select GCLK4

  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Enable the port multiplexer for the digital pin D7
  SerialUSB.print("Port ");
  SerialUSB.println(g_APinDescription[7].ulPort); // shoudl be 0
  SerialUSB.print("Pin ");
  SerialUSB.println(g_APinDescription[7].ulPin); //should be 21
  PORT->Group[0].PINCFG[21].bit.PMUXEN = 1;

  // Connect the TCC0 timer to digital output D7 - port pins are paired odd PMUO and even PMUXE
  // F & E specify the timers: TCC0, TCC1 and TCC2
  // g_APinDescription[6] = (0,20)
  PORT->Group[0].PMUX[20 >> 1].reg = PORT_PMUX_PMUXO_F;

  // Feed GCLK4 to TCC0 and TCC1
  REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4 to TCC0 and TCC1
                     GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                     GCLK_CLKCTRL_ID_TCC0_TCC1;   // Feed GCLK4 to TCC0 and TCC1

  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Dual slope PWM operation: timers countinuously count up to PER register value then down 0
  REG_TCC0_WAVE |= TCC_WAVE_WAVEGEN_NPWM;    // Setup single slope PWM on TCC0

  while (TCC0->SYNCBUSY.bit.WAVE);               // Wait for synchronization

  // Each timer counts up to a maximum or TOP value set by the PER register,
  // this determines the frequency of the PWM operation:
  REG_TCC0_PER = 9000;         // Set the frequency of the PWM on TCC0 to 10 (v)(4500 for 24ms)

  while (TCC0->SYNCBUSY.bit.PER);                // Wait for synchronization

  // Set the PWM signal to output 10us duty cycle
  REG_TCC0_CC3 = 2;         // TCC0 CC3 - on D7 (y)

  while (TCC0->SYNCBUSY.bit.CC3);                // Wait for synchronization

  // Divide the 48MHz signal by 256 giving 48MHz (5.33us) TCC0 timer tick and enable the outputs
  REG_TCC0_CTRLA |= TCC_CTRLA_PRESCALER_DIV256 |    // Divide GCLK4 by 1 Available: (1, 2, 4, 8, 16, 64, 256, 1024)
                    TCC_CTRLA_ENABLE;               // Enable the TCC0 output

  while (TCC0->SYNCBUSY.bit.ENABLE);              // Wait for synchronization

}

It works on D8/PA16 for example (and a few others) with the same ultrasonic sensor setup and everything (I'm just moving the jumper), but then switching over to D10/PA19 it does not work. Here's my modified variant.cpp line for PA19 as well as the original right below it, commented out.

  { PORTA, 19, PIO_SERCOM,  (PIN_ATTR_DIGITAL                                ), No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_3    }, // SCK:  SERCOM1/PAD[1]
  //*ORIGINAL LINE*{ PORTA, 19, PIO_SERCOM,  (PIN_ATTR_DIGITAL|PIN_ATTR_PWM|PIN_ATTR_TIMER    ), No_ADC_Channel, PWM3_CH1,   TC3_CH1,      EXTERNAL_INT_NONE }, // MISO: SERCOM1/PAD[3]

Any ideas as to why the interrupt won't work? Thank you!!