Skip to content

Programming

SAMD vs Arduino pin numbering

The actual pin numbering of samd21 chips (e.g. portA21 or portB30) is in no way related to arduino pin numbering used in sketches.
To get to the arduino pin number, see the variant.cpp file. In this file you’ll find the PinDescription table of this microcontroller.
Look for the actual pin number you want to program and check the commented line above it to find the arduino numbering. e.g.:
PORTA, 21 on line 130 -> // 12..Digital functions (line 120) -> meaning actual pin number PA21 = arduino number 21
PORTB, 30 on line 166 -> // 43..46 - SPI Header (line 165) -> meaning actual pin number PB30 = arduino number 43

Arduino equivalent: Arduino Zero, Adafruit feather M0

To make sure I can lookup interesting example code, it’s a good idea to check if there’s an Arduino equivalent of the microcontrollers that I’m using. I found the Arduino Zero

SAMD21J18A Microcontroller for sensor controller board

My sensor boards are equipped with SAMD21J18A 64 pin microcontrollers. They have 1 12-bit ADC that is connected to 20 input pins. I’m going to continuously measure 19 pressure sensors with one sensor board (which equals to 1 octave of my Marimbatron) and send the result over I2C bus to the main controller. I’m also using this microcontroller to send RGB color information to the 19 WS2812B RGB LEDs of each individual sensor.

analog input sampling (ADC)

Test

First loading the example that I made on the SAMD11C14:

#include <Adafruit_NeoPixel.h>
#define LEDPIN 43 // PB30 = arduino 43
#define STARTLEDDELAY 100
#define SENSORPIN 6 // AIN14 on PB6 = arduino 6
#define LOOPDELAY 10

#define NEOPIXELPIN 39 // PB10 = arduino 39
#define NUMPIXELS 1
Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIXELPIN, NEO_GRB + NEO_KHZ800);

const unsigned long intervalSensor1 = 50;
const unsigned long intervalLED = 1000;
unsigned long previousTimeSensor1 = 0;
unsigned long previousTimeLED = 0;
bool ledStatus;
void setup() {
  pinMode(LEDPIN, OUTPUT); // set LED pin to output
  digitalWrite(LEDPIN, LOW); // turn LED off
  delay(STARTLEDDELAY); // wait
  Serial.begin(15200); // enable serial communication
  pinMode(SENSORPIN, INPUT); // set sensor pin to input.
  pixels.begin();
  pixels.clear();
  pixels.setBrightness(50); // Set BRIGHTNESS to about 1/5 (max = 255)
}

void loop() {
  int ADCMeasurement;
  unsigned long currentTime = millis();

  /* This is the event for sensor 1*/
  if (currentTime - previousTimeSensor1 >= intervalSensor1) {
    /* Event code */
    ADCMeasurement = analogRead(SENSORPIN);
    Serial.println((String)"Measurement value = " + ADCMeasurement);
    int pixelColor = map(ADCMeasurement, 900, 1023, 0, 65535); // map(value, fromLow, fromHigh, toLow, toHigh)
    int color = pixels.gamma32(pixels.ColorHSV(pixelColor)); // hue -> RGB
    pixels.setPixelColor(0, color); // Set pixel 'c' to value 'color'
    pixels.show(); // Update strip with new contents

   /* Update the timing for the next time around */
    previousTimeSensor1 = currentTime;
  }

  /* This is the event for sensor 2*/
  if (currentTime - previousTimeLED >= intervalLED) {
    /* Event code */
    digitalWrite(LEDPIN, ledStatus);
    ledStatus = !ledStatus;

   /* Update the timing for the next time around */
    previousTimeLED = currentTime;
  }

}

The firmware of the sensor module

V1 Read all sensors

Next is to do it in a less “arduino” way. Analogread works fine, but it also hides some of the advanced features that I might or might not need. From what I understood, the Analogread function also enables the ADC and disables it after every readout. I don’t think I want that (for speed purposes).
So diving deeper. good resources I’ve read regarding the ADC:
https://blog.thea.codes/understanding-the-sam-d21-clocks/
https://blog.thea.codes/reading-analog-values-with-the-samd-adc/
https://blog.thea.codes/getting-the-most-out-of-the-samd21-adc/
Atmel AT11481 application note on ADC configurations with examples
and of course the datasheet

From the above mentioned blog posts, the procedure to get the ADC to work is:

  1. Enable the bus clock to the ADC.
  2. Wire up a peripheral clock to the ADC.
  3. Load the ADC’s calibration values.
  4. Configure the measurement parameters.
  5. Configure the pin for the ADC function.
  6. Start the ADC and trigger a measurement.

Code that is required to do these steps are mentioned in the blogpost.
Important note to measuring using the ADC of the SAMD devices can be found in the datasheet and in the AT11481 application sheet linked above:

  • Discard the first conversion result whenever there is a change in ADC configuration like voltage reference / ADC channel change
  • When switching to a differential channel (with gain settings), the first conversion result may have a poor accuracy due to the required settling time for the automatic offset cancellation circuitry. It is better to discard the first sample result.

As I’m going to continuously change ADC channels (from 0 to 19), I’ll always have to “measure twice” and disregard the first measurement.

The ADC has the following block diagram (from the datasheet):

V1 of the firmware of the sensorboard has the above applied. What it does:

  • blink the red and green leds
  • drive 1 WS2812B RGB led and cycle the colorwheel
  • read out 20 analog sensor inputs (12 bit ADC) and output their value in the serial monitor

Here is a screenshot of the serial monitor with 1 sensor attached to input 8. The ADC is set to 12 bit so the maximum value is 4095. Without the graphite sprayed PET circle (no conductance at all), the output varies between 4085 and 4095. With the PET circle and the top sensor part the output is around 3870 (this is because there is a bit of conductance due to the graphite and a bit of pressure due to the weight of the sensor top part).
When pressing, the measured value goes down into the 700’s.

Find the sketch (firmware V1) here in my final-project repo

V2 Hit detection

Now it’s time for hit detection.
Hit detection is usually done by using these variables:

  • threshold
  • scantime
  • mask time

Great resources that I used (next to my knowledge of Roland drumtriggers) are these sites:
megadrum -> a drumtrigger that I own. hellodrum arduino project -> an arduino library for drum triggers

This image that I took from the hellodrum project explains hit detection very well:

Although this is based on a piezo, my sensors will work similarly. With less highs and lows before and after the peak though. So sensing should even be easier.
The idea is that you measure in high speed (microsecond speed). As soon as the measured level is above the threshold level, the scan time begins. During this time, keep measuring and detect the peak level of the signal (local maximum, or in my case a local minimum as my sensor works inverted). As soon as you find the maximum peak level and scantime is over, you store the peak level. Next, the mask time begins. During this time you don’t measure anything. This is to prevent false triggering. Again, in my sensor the signal is much cleaner, but false triggering is no good anyway.

First I’m going to connect a pressure sensor to an oscilloscope and hit it. To see what measurement results I get.

Here’s a hard hit on my sensor. Note that this signal is inverted because my sensors works that way (pressing harder = voltage goes lower). sm

Same hit but zoomed in:
sm sm

So the peak is at around 600uS after the hit. And a nice and clean signal.

A very soft hit gets its peak after around 2ms.
sm

A hard hit gets its peak after around 1ms:
sm

A very hard hit gets its peak after around 600uS:
sm

This basically means that any peak will be within 500uS and 2ms after it exceeds the threshold (trigger level on my osilloscope).
I also noticed that the voltage levels for medium to very hard hits are actually quite close to each other. About within 500mV. I think I will need to make some adjustments in software, e.g. by mapping the measured values to a curve instead of linearly go from measured value to MIDI velocity. this is very common to do by the way. All major e-drum manufacturers allow you to select a velocity curve.

So I implemented this procedure in V2 firmware.
I implemented a timer interrupt service routine (ISR) so that on specific times all sensor inputs are going to be measured. In this ISR function, the threshold, scantime and masktimes are taken care of:

void TimerHandler() {
  uint32_t padThreshold;
  unsigned long startScan;
  uint32_t currentMeasurement;
  for (int i = 0; i < sensorArrayLength; i++) {
    startScan = micros();
    // true if the measurement should not be masked. i.e. the measurement should be done.
    // startscan - lastADCTime >= masktime is done in this order to prevent issues from micros() overflow.
    // note (unsigned long) is typecasting the calculation between () to make sure the overflow prevention works.
    // see https://www.norwegiancreations.com/2018/10/arduino-tutorial-avoiding-the-overflow-issue-when-using-millis-and-micros/
    if ((unsigned long)(startScan - pads[i].getLastADCTime()) >= pads[i].getMaskTime()) {
      padThreshold = pads[i].getThreshold();
      setADCToPin(sensorArray[i]);
      currentMeasurement = readADC(true); // true = first measurement should be disregarded due to channel change
      if (currentMeasurement < padThreshold) { // measurement below threshold = hit detected. Start peak detection for scantime us.
        ITimer.detachInterrupt(); // disable interrupt
        uint32_t scanTime = pads[i].getScanTime();
        uint32_t peakMeasurement = padThreshold;
        while ((unsigned long)(micros() - startScan) <= scanTime) { // again using micros() overflow prevention
          currentMeasurement = readADC(false); // disregarding first measurement is not neccesary because we didn't change the input channel
          if (currentMeasurement < peakMeasurement) { // peak detection for local minimum
            peakMeasurement = currentMeasurement;
          }
        }
        pads[i].setVelocity(peakMeasurement); // peak detection done. Store peak value.
        pads[i].setHit(true); // set hit to true to indicate the pad was hit.
        pads[i].setLastADCTime(startScan + scanTime); // store lastADCTime, required for masktime calculation.
        ITimer.reattachInterrupt(); // enable interrupt
      } 
    }
  }
}

In the main loop I check if pads.getHit() of any of the pads is true. If so, it prints a “hit detected” to the serial monitor:

    for (int i = 0; i < sensorArrayLength; i++) {
      if (pads[i].getHit()){ // if true: hit is detected
        Serial.println((String)"HIT on pad " + i + " with velocity " + pads[i].getVelocity());
        pads[i].setHit(false); // reset hit
      }
    }

Here’s a screenshot of V2 working:

Find the sketch of V2 here in my final-project repo.

HARDWARE TIMERS on SAMD21

Now I need to speed things up. To do that I want to make sure that all clocks are running at their programmed speeds and want to check the lowest ISR Timer setting when things are still happy.
As mentioned in this blogpost of thea.codes the SAMD chips can output a generic clock(GCLK) to a pin. Then you can use an oscilloscope to measure the clock.
From the blog:
To enable clock output you need to set the OE bit when configuring a generic clock through GCLK->GENCTRL. For example, this configures GCLK1 and enables I/O output:

GCLK->GENCTRL.reg =
    GCLK_GENCTRL_ID(1) |
    GCLK_GENCTRL_SRC_XOSC32K |
    GCLK_GENCTRL_IDC |
    GCLK_GENCTRL_GENEN |
    /* Enable outputting the clock to an I/O pin. */
    GCLK_GENCTRL_OE;

/* Wait for the write to complete */
while(GCLK->STATUS.bit.SYNCBUSY);

Once that’s configured you’ll need to configure the port multiplexer to connect the GCLK output to a pin. For example, this sets up PA15 to output GCLK1:

PORT->Group[0].DIRSET.reg = (1 << 15);
PORT->Group[0].PINCFG[15].reg |= PORT_PINCFG_PMUXEN;
PORT->Group[0].PMUX[15 >> 1].bit.PMUXO |= PORT_PMUX_PMUXO_H;

There’s only a certain set of pins you can use for this. See this table. I’ve also indicated if I can use this pin (if the pin is in use for analog input, I cannot use it).

GCLK I/O Pins can I use it:
GCLK0 PA14, PB14, PB22, PA27, PA28, PA30 PA14, PB14, PB22, PA27, PA28 where PA14 is easiest as it is connected to a pad.
GCLK1 PA15, PB15, PB23 PA15, PB15, PB23 where PA14 and PB15 are easiest as they’re connected to a pad.
GCLK2 PA16, PB16 PA16, PB16
GCLK3 PA17, PB17 PA17, PB17
GCLK4 PA10, PB10, PA20 PB10, PA20 where PB10 is easiest as it is connected to the WS2812B data out connector.
GCLK5 PA11, PB11, PA21 PB11, PA21
GCLK6 PA12, PA22 PA22
GCLK7 PB13, PA23 PB13, PA23
Note the warning in the blog:
When debugging this way I highly recommend that you don’t mess with GCLK0 at all so that you don’t have to worry about a bad configuration breaking the CPU clock.

Let’s set GCLK4 the same way as GCLK0 so that I can measure if it outputs 48Mhz. If it does, I’m confident that the CPU clock GCLK0 is set correctly as well.

void setup48MhzUSBClockRecoveryMode(){
/* !!! removed some important code for clarity /*

  /* temporarily set 48MHz clock to GCLK4 as well and output to a pin to check accuracy. */
  GCLK->GENCTRL.reg =
      GCLK_GENCTRL_ID(4) |
      GCLK_GENCTRL_SRC_XOSC32K |
      GCLK_GENCTRL_IDC |
      GCLK_GENCTRL_GENEN |
      /* Enable outputting the clock to an I/O pin. */
      GCLK_GENCTRL_OE;

  /* Wait for the write to complete */
  while(GCLK->STATUS.bit.SYNCBUSY);
  PORT->Group[1].DIRSET.reg = (1 << 10);
  PORT->Group[1].PINCFG[10].reg |= PORT_PINCFG_PMUXEN;
  PORT->Group[1].PMUX[10 >> 1].bit.PMUXE |= PORT_PMUX_PMUXE_H;

}

Oscilloscope output. That’s a nice 48 MHz clock.
sm

GCLK1 is used for the ADC clock and is set to 8Mhz. I wasn’t able to output this clock to a pin at the same time as setting it to the ADC. So I used the same settings for GCLK4 and checked that:

void setup8MhzGCLK(){
  // from https://blog.thea.codes/understanding-the-sam-d21-clocks/
  /* Configure GCLK1's divider - in this case, no division - so just divide by one */
  GCLK->GENDIV.reg =
      GCLK_GENDIV_ID(1) |
      GCLK_GENDIV_DIV(1);

  /* Setup GCLK1 using the internal 8 MHz oscillator */
  GCLK->GENCTRL.reg =
      GCLK_GENCTRL_ID(1) |
      GCLK_GENCTRL_SRC_OSC8M |
      /* Improve the duty cycle. */
      GCLK_GENCTRL_IDC |
      GCLK_GENCTRL_GENEN;

  /* Wait for the write to complete */
  while(GCLK->STATUS.bit.SYNCBUSY) {};

  // temporarily set 8MHz clock to GCLK4 as well and output to a pin to check accuracy.
  GCLK->GENDIV.reg =
      GCLK_GENDIV_ID(4) |
      GCLK_GENDIV_DIV(1);
  GCLK->GENCTRL.reg =
      GCLK_GENCTRL_ID(4) |
      GCLK_GENCTRL_SRC_OSC8M |
      GCLK_GENCTRL_IDC |
      GCLK_GENCTRL_GENEN |
      /* Enable outputting the clock to an I/O pin. */
      GCLK_GENCTRL_OE;

  /* Wait for the write to complete */
  while(GCLK->STATUS.bit.SYNCBUSY);
  PORT->Group[1].DIRSET.reg = (1 << 10);
  PORT->Group[1].PINCFG[10].reg |= PORT_PINCFG_PMUXEN;
  PORT->Group[1].PMUX[10 >> 1].bit.PMUXE |= PORT_PMUX_PMUXE_H;

}

Again a solid 8 MHz clock. Actually a bit under 8 MHz :-)
sm

Now let’s set the other timers.
This is a bit of a pain because the SAMD21 combines timers and counters. I don’t want to use a library but examples on the internet are scarce. With a bit of help of thea.codes blog, the datasheet, and lot’s of wrong examples that copilot AI spit out, I was able to get hardware timer TC3, TC4 and TC5 to work nicely. I check it with a oscilloscope by toggling a pin.

To do the toggling with minimal impact on time, I directly manipulate the PORT registers instead of using Arduinos digitalWrite(). The commands to use are:

#define TESTPIN PORT_PB10 // temp test pin
PORT->Group[1].DIRSET.reg |= TESTPIN; // set test pin as output

PORT->Group[1].OUTCLR.reg = TESTPIN; // turn off TEST pin
PORT->Group[1].OUTSET.reg = TESTPIN; // turn test pin on              
PORT->Group[1].OUTTGL.reg = TESTPIN; // toggle TEST pin

This is the code where the toggling happens in their respective interrupt handlers:

#define CPU_HZ 48000000 // The cpu clock is running at 48 MHz
#define TICK_HZ_1MS 1000 // We want a 1 ms tick.
#define COUNTS_PER_TICK_1MS (CPU_HZ / TICK_HZ_1MS) // This is the number of clock cycles per tick
#define TICK_HZ_1US 1000000 // We want a 1 us tick.
#define COUNTS_PER_TICK_1US (CPU_HZ / TICK_HZ_1US) // This is the number of clock cycles per tick
#define TICK_HZ_ADC 20000 // We want a 200 us tick for the ADC.
#define COUNTS_PER_TICK_ADC (CPU_HZ / TICK_HZ_ADC) // This is the number of clock cycles per tick



#define TESTPIN PORT_PB10 // temp test pin
void TC3_Handler() {
  //  PORT->Group[1].OUTTGL.reg = TESTPIN; // toggle TEST pin to be able to check with oscilloscope
  // Clear the interrupt
  TC3->COUNT16.INTFLAG.bit.MC0 = 1;
}
void TC4_Handler() {
  // PORT->Group[1].OUTTGL.reg = TESTPIN; // toggle TEST pin to be able to check with oscilloscope
  // Clear the interrupt
  TC4->COUNT16.INTFLAG.bit.MC0 = 1;
}
void TC5_Handler() {
  PORT->Group[1].OUTTGL.reg = TESTPIN; // toggle TEST pin to be able to check with oscilloscope
  // Clear the interrupt
  TC5->COUNT16.INTFLAG.bit.MC0 = 1;
}


void setup() {
    PORT->Group[1].DIRSET.reg |= TESTPIN; // set test pin as output
    // Set up the generic clock (GCLK4) used to clock timers
    GCLK->GENDIV.reg = GCLK_GENDIV_DIV(1) |          // Divide the 48MHz clock source by divisor 1: 48MHz/1=48MHz
                       GCLK_GENDIV_ID(4);            // Select Generic Clock (GCLK) 4
    while (GCLK->STATUS.bit.SYNCBUSY);               // Wait for synchronization

    GCLK->GENCTRL.reg = 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

    // Feed GCLK4 to TC3
    GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4
                        GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                        GCLK_CLKCTRL_ID_TCC2_TC3;     // Feed the GCLK4 to TC3 (and TCC2)
    while (GCLK->STATUS.bit.SYNCBUSY);               // Wait for synchronization

    // Feed GCLK4 to TC4 and TC5
    GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4
                        GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                        GCLK_CLKCTRL_ID_TC4_TC5;     // Feed the GCLK4 to TC4 and TC5
    while (GCLK->STATUS.bit.SYNCBUSY);               // Wait for synchronization

    // specific for TC3: 1ms
    TC3->COUNT16.CTRLA.reg |= TC_CTRLA_MODE_COUNT16;  // Set Timer counter Mode to 16 bits
    while (TC3->COUNT16.STATUS.bit.SYNCBUSY);         // Wait for synchronization

    TC3->COUNT16.CTRLA.reg |= TC_CTRLA_WAVEGEN_MFRQ;  // Set TC as Match Frequency
    while (TC3->COUNT16.STATUS.bit.SYNCBUSY);         // Wait for synchronization

    TC3->COUNT16.CC[0].reg = COUNTS_PER_TICK_1MS;     // Set the TC3 CC0 register as the TOP value in match frequency mode
    while (TC3->COUNT16.STATUS.bit.SYNCBUSY);         // Wait for synchronization

    // Enable the TC3 interrupt request
    TC3->COUNT16.INTENSET.bit.MC0 = 1;
    while (TC3->COUNT16.STATUS.bit.SYNCBUSY);         // Wait for synchronization

    // Enable TC3
    TC3->COUNT16.CTRLA.reg |= TC_CTRLA_ENABLE;        // Enable TC3
    while (TC3->COUNT16.STATUS.bit.SYNCBUSY);         // Wait for synchronization

    // Enable the TC3 interrupt vector use priority 2
    NVIC_SetPriority(TC3_IRQn, 2);                    // Set the NVIC priority for TC3 to 2
    NVIC_EnableIRQ(TC3_IRQn);

    // specific for TC4: 1us
    TC4->COUNT16.CTRLA.reg |= TC_CTRLA_MODE_COUNT16;  // Set Timer counter Mode to 16 bits
    while (TC4->COUNT16.STATUS.bit.SYNCBUSY);         // Wait for synchronization

    TC4->COUNT16.CTRLA.reg |= TC_CTRLA_WAVEGEN_MFRQ;  // Set TC as Match Frequency
    while (TC4->COUNT16.STATUS.bit.SYNCBUSY);         // Wait for synchronization

    TC4->COUNT16.CC[0].reg = COUNTS_PER_TICK_1US;     // Set the TC4 CC0 register as the TOP value in match frequency mode
    while (TC4->COUNT16.STATUS.bit.SYNCBUSY);         // Wait for synchronization

    // Enable the TC4 interrupt request
    TC4->COUNT16.INTENSET.bit.MC0 = 1;
    while (TC4->COUNT16.STATUS.bit.SYNCBUSY);         // Wait for synchronization

    // Enable TC4
    TC4->COUNT16.CTRLA.reg |= TC_CTRLA_ENABLE;        // Enable TC3
    while (TC4->COUNT16.STATUS.bit.SYNCBUSY);         // Wait for synchronization

    // Enable the TC4 interrupt vector use priority 0
    NVIC_SetPriority(TC4_IRQn, 0);                    // Set the NVIC priority for TC4 to 0
    NVIC_EnableIRQ(TC4_IRQn);

    // specific for TC5: 200us
    TC5->COUNT16.CTRLA.reg |= TC_CTRLA_MODE_COUNT16;  // Set Timer counter Mode to 16 bits
    while (TC5->COUNT16.STATUS.bit.SYNCBUSY);         // Wait for synchronization

    TC5->COUNT16.CTRLA.reg |= TC_CTRLA_WAVEGEN_MFRQ;  // Set TC as Match Frequency
    while (TC5->COUNT16.STATUS.bit.SYNCBUSY);         // Wait for synchronization

    TC5->COUNT16.CC[0].reg = COUNTS_PER_TICK_ADC;     // Set the TC5 CC0 register as the TOP value in match frequency mode
    while (TC5->COUNT16.STATUS.bit.SYNCBUSY);         // Wait for synchronization

    // Enable the TC5 interrupt request
    TC5->COUNT16.INTENSET.bit.MC0 = 1;
    while (TC5->COUNT16.STATUS.bit.SYNCBUSY);         // Wait for synchronization

    // Enable TC4
    TC5->COUNT16.CTRLA.reg |= TC_CTRLA_ENABLE;        // Enable TC5
    while (TC5->COUNT16.STATUS.bit.SYNCBUSY);         // Wait for synchronization

    // Enable the TC5 interrupt vector use priority 1
    NVIC_SetPriority(TC5_IRQn, 1);                    // Set the NVIC priority for TC5 to 1
    NVIC_EnableIRQ(TC5_IRQn);
}

void loop() {
    // nothing here
}

note that the timers are set to a specific priority to aid me in my Marimbatron firmware.
This codes create a 1us (TC4), 200us (TC5) and 1ms (TC3) timer.

sm
sm
sm

Find the sketch here in my final-project repo

Note that a 1us timer turned out not to be helpful. See this stackexchange post where the following is mentioned:

  • The CPU clock is set to 48 Mhz
  • a 1 us timer means it runs at 1 Mhz
  • this means the interrupt occurs every 48 CPU clockcycles
  • Cortex-M0+ takes 15 clockcycles to get into the interrupt routine, assuming the bus is free
  • plus another 13 cycles to leave the interrupt
  • This totals to 28 cycles, leaving 20 clockcycles to handle whatever is in the ISR function
  • The remainder of the cycles can be used for other stuff. e.g. other interrupts.

A great blog on interrupt latency in the ARM cortex family can be found at arm.com.
With this said, there’s almost no cycles left to do anything. The 1us interrupt uses too much CPU time for anything else to happen. This means I cannot use such a fast timer ISR combined with any useful programs :-). But taking things a bit slower should work fine in my situation.

V3 fast reading with ISR timers

Next is to check how fast the ISR can go. I do this by toggling the output of a pin and check the signal on an oscilloscope.
I’ve set TC3 (my “main” timer) to 100us instead of 1us. I also removed the 1ms timer and use the main timer to set my intervals (just do everything on this clock instead of using another clock that consumes CPU time).
The microcontroller seems to happily measure all 19 sensors at a speed of 200us to trigger the measurement loop. So all 19 sensors are measured within 200us.
As soon as one of the sensors trigger the threshold value, this specific sensor is measured much faster in order to catch it’s peak value. In the code below I’m toggling the output of my testpin to check if this is really happening and to check at what speed this occurs.

      currentMeasurement = readADC(true); // true = first measurement should be disregarded due to channel change
      if (currentMeasurement < padThreshold) { // measurement below threshold = hit detected. Start peak detection for scantime us.
        uint32_t scanTime = pads[i].getScanTime();
        uint32_t peakMeasurement = padThreshold;
        while ((unsigned long)(micros_counter - startScan) <= scanTime) { // stay measuring for as long as scanTime is. Again using micros_counter overflow prevention
          PORT->Group[1].OUTTGL.reg = TESTPIN; // toggle TEST pin to be able to check with oscilloscope
          currentMeasurement = readADC(false); // disregarding first measurement is not neccesary because we didn't change the input channel
          if (currentMeasurement < peakMeasurement) { // peak detection for local minimum
            peakMeasurement = currentMeasurement;
          }
        }

The oscilloscope output (set to single trigger to keep the measurements on the screen):
sm
The frequency of this burst is 21,93 kHz. At every fast measurement, the pin output is inverted, so the actual ADC measurement frequency is twice this value: 43,86 kHz. This equals to one measurement every 23 microseconds. That is actually very good! A hard hit gets to it’s peak in around 600 us. Every 200 microseconds the sensor is checked if it triggers the threshold. Worst-case the hit was just before the last check, leaving 400 microseconds worth of speed-measurements. With 23 microseconds per measurement, the microcontroller can do 17 measurements up until the peak is reached. To me that sounds enough to catch the height of the peak!
In the serial monitor output you can see all 19 sensor outputs. In the red rectangle you see sensor 8. When a hit is detected, a * is printed next to the velocity. Every 50ms the sensor array is read out in the main loop and printed to the serial port:

This is a video of me hitting the pad. The red LED on the PCB blinks every 100ms (toggled on/of in the 50ms event in the main loop). As soon as a hit is detected, the green LED is turned on. It is turned of in the next 50ms event. As you can see this looks promising!

Of course I haven’t programmed I2C communication to the main controller and WS2812B LED control in this program. Adding this might affect the maximum speeds that can be achieved. On the other hand, I’m using Serial communication now and that’s not going to be active on this sensorboard in “production” mode. And I haven’t “unrolled” the for loop in the ISR yet. But we’ll find out how much influence this will have.

WS2812B LED control

Each pressure sensor has one WS2812B RGB LED. The microcontroller will have to drive these LEDs so things look nice :D
Controlling it will be done either thru or library.
I found out that Adafruit Neopixel library sometimes disables interrupts to be able to get the timing correct. This interferes with my timer ISR of the ADC. I found this Adafruit Neopixel ZeroDMA library that should not do this. Instead it relies on DMA transfer (direct memory access) to get data to the neopixels. This only works if the neopixel data ouput pin of the microcontroller is tied to a specific pin. This is because the library uses SERCOM peripherals for SPI output, and the hardware only supports this on specific pins (plus, some SERCOMs are in use for Serial, I2C, etc.). Haven’t checked if I have it connected to a supported pin yet…

I2C communication

There are 3 pads on the sensor board that I can populate with 0 ohm resistors and read their value as digital input. That way I can define the I2C address of each board (max 2^3 = 8 addresses available).
Good info on I2C can be found in the arduino docs. I read this to get a quick start.
The Wire library is what Arduino uses to communicate with I2C devices. It is included in all board packages, so you don’t need to install it manually in order to use it.
What you need to know is that in the past I2C was all about master and slaves. Now the Arduino docs talk about controller and peripherals. I actually like Main and Secondary better because in Dutch, a similar word to peripheral isn’t really used that much. Also with Main and Secondary, the abbreviations MISO and MOSI are still useable.
The examples have sketches on controller reader and controller writer. Both of which are exactly what I want to accomplish. Nice kick start!
I’m going to first use my SAMD11C14A test PCB that I made in programming week as the I2C main. My Sensor PCB with the SAMD21J18A will be the I2C secondary.
The SAMD11C14A has PA14 as SDA and PA15 as SCL, which I wired to a connector. No more work on that.
The SAMD21J18A has multiple pins that can be used for I2C. It also has multiple SERCOMs so you can even have multiple I2C busses simultaneously. The Arduino framework (by means of the variant.h file) has default pins set up to be used for I2C. For the SAMD21J18A (using ArduinoCore-fab-sam), the default pins for I2C (also known as wire.h) are PA16 and PA17. But I’m using those. I could have. Would have made my life easier. But I didn’t check the default pins when I routed my board. So I routed them to PA12 and PA13 (SERCOM2)…
Fortunately, the variant.h also creates a wire1 and sets PA12 and PA13 to wire1. This means I can use it like this:

#include <Wire.h> 
void setup() {
    Wire1.begin();
}
void loop() {
  Serial.println("requesting data ... ");
  Wire1.requestFrom(1, 12);    // request 12 bytes from peripheral device #1
  Serial.println("request sent ... ");
  while (Wire1.available()) { // peripheral may send less than requested
    char c = Wire1.read(); // receive a byte as character
    Serial.print(c);         // print the character
  }
  Serial.println("done ... ");}

This only works if you specifically enable two I2C ports in the Arduino board configuration! You’ll get an error if you don’t set this correctly.
Find this in Arduino IDE under Tools -> Serial Config -> ONE_UART_TWO_WIRE_ONE_SPI.
Or in VSCode using Arduino plugin:

So I got my microcontrollers connected to my computer, used the SAMD11 as secondary, the SAMD21 as main and voila:
On the main
sm

On the secondary
sm

Find the code in these repos:
Master code
Secondary code

With that out of the way, it’s time to send real data!

The thing that gets this to work is to do this on the I2C main:

  Wire.requestFrom(1, 5);    // requesting 5 bytes from address 1. Max number of bytes in a single transmission cannot be more than 32.
  while(Wire.available()) {
      sensorNumber = Wire.read(); // Wire.read reads 1 byte at a time. uint8_t is 1 byte.
      sensorVelocity = Wire.read() | (Wire.read() << 8) | (Wire.read() << 16) | (Wire.read() << 24); // sensorVelocity is uint32_t, which is 4 bytes. So need to combine 4 reads into one uint32_t.
      Serial.print(sensorNumber);         // Print the value
      Serial.print(" ");
      Serial.println(sensorVelocity);
      if (sensorNumber < 255) {
        Serial.print("Hit on sensor "); Serial.print(sensorNumber); Serial.print(" -> "); Serial.println(sensorVelocity);
        Serial.println("Sending note on");
        noteOn(0, 48, 64);   // Channel 0, middle C, normal velocity
        delay(100);
        Serial.println("Sending note off");
        noteOff(0, 48, 64);  // Channel 0, middle C, normal velocity
      }
      else{
        Serial.println("no hit");
      }
  }

and do this on the I2C Secondary ISR:

void I2CEvent(){ // send [255, 0, 0, 0, 0] when no hit is detected. Otherwise send sensor number and velocity of just 1 sensor and reset the hit state of that sensor
  uint32_t velocity;
  bool hitDetected = false;
  uint8_t data[] = {255,0,0,0,0};
  // Serial.println("Received I2C event");
  for (uint8_t i = 0; i < sensorArrayLength; i++) {
    if (pads[i].getHit()){ // if true: hit is detected
      hitDetected = true;
      velocity = pads[i].getVelocity();
      Serial.print("HIT s"); Serial.print(i); Serial.print(" ");Serial.print(velocity);
      Serial.println();
      // set sensor number and velocity to data variable when hit is detected
      data[0] = i;
      data[1] = velocity & 0xFF;
      data[2] = (velocity >> 8) & 0xFF;
      data[3] = (velocity >> 16) & 0xFF;
      data[4] = (velocity >> 24) & 0xFF;
      PORT->Group[1].OUTSET.reg = GREENLEDPIN; // turn green led pin on
      flashLED = true;
      pads[i].setHit(false); // reset hit
      break; // break the for loop
    }
  }
  Wire1.write(data, 5); // even when no hit is detected an answer will still be send (255, 0, 0, 0, 0)
}

This example uses the SAMD11C14-I2Cmaster-MIDIout on the SAMD11 board and sensormodule_FW_v4 on the SAMD21 sensorboard.
Find the code in these repos:
SAMD11C14-I2Cmaster-MIDIout
sensormodule_FW_v4

SAMD21E18A Microcontroller for main controller board

My sensor boards are connected to a main controller board using I2C. Their addresses are defined using their address pads.
The main controller acts as I2C main, continuously polling the sensor boards for sensor values. The main controller will then convert these sensor values to MIDI note messages with appropriate velocity (volume). These MIDI messages are then send to a host computer over USB. The host computer should run a piece of software that converts MIDI messages into audible sound. I’m going to use Ableton Live for this.
A footswitch can be connected to the main controller. This will act as sustain pedal. Next, there are 4 buttons and a round LCD connected to the main microcontroller for user interaction. Finally, the main board will supply power to each sensor board.

Sending MIDI

Sending MIDI using the arduino framework is actually very easy. There are 2 libraries: which is used when you’re using a hardware UART (meaning you have a physical MIDI connector on your board) and which is used if you’re microcontroller is has USB capabilities. It’ll make the microcontroller to act as a USB MIDI device.
I’m using the last one, as I want my SAMD controller to connect via USB.
The example is very easy:

/*
 * MIDIUSB_test.ino
 *
 * Created: 4/6/2015 10:47:08 AM
 * Author: gurbrinder grewal
 * Modified by Arduino LLC (2015)
 */ 
#include "MIDIUSB.h"

// First parameter is the event type (0x09 = note on, 0x08 = note off).
// Second parameter is note-on/note-off, combined with the channel.
// Channel can be anything between 0-15. Typically reported to the user as 1-16.
// Third parameter is the note number (48 = middle C).
// Fourth parameter is the velocity (64 = normal, 127 = fastest).

void noteOn(byte channel, byte pitch, byte velocity) {
  midiEventPacket_t noteOn = {0x09, 0x90 | channel, pitch, velocity};
  MidiUSB.sendMIDI(noteOn);
}

void noteOff(byte channel, byte pitch, byte velocity) {
  midiEventPacket_t noteOff = {0x08, 0x80 | channel, pitch, velocity};
  MidiUSB.sendMIDI(noteOff);
}

void setup() {
  Serial.begin(115200);
}

// First parameter is the event type (0x0B = control change).
// Second parameter is the event type, combined with the channel.
// Third parameter is the control number number (0-119).
// Fourth parameter is the control value (0-127).

void controlChange(byte channel, byte control, byte value) {
  midiEventPacket_t event = {0x0B, 0xB0 | channel, control, value};
  MidiUSB.sendMIDI(event);
}

void loop() {
  Serial.println("Sending note on");
  noteOn(0, 48, 64);   // Channel 0, middle C, normal velocity
  MidiUSB.flush();
  delay(500);
  Serial.println("Sending note off");
  noteOff(0, 48, 64);  // Channel 0, middle C, normal velocity
  MidiUSB.flush();
  delay(1500);

  // controlChange(0, 10, 65); // Set the value of controller 10 on channel 0 to 65
}

But wait. That is too easy. And indeed it is. When you’re on a mac, no worries. Linux most probably no worries either. The microcontroller will be detected as a USB MIDI device and will happily send out MIDI notes. However, when you’re on Windows…
The microcontroller should present itself in two ways. A serial device and a MIDI device. As you can see from the program above, it sends information (“Sending note on”) to the serial device, and it sends MIDI notes to the MIDI device. Connecting the microcontroller to a USB port on my macbook works great. I see two devices, and it send both serial as well as MIDI stuff over.
On Windows not so much.
I had my SAMD11C14 programmed with the example above. On Macbook this worked fine. On Windows my serial data output was this:
sm

And no USB MIDI device to be found. I went searching for an answer on Google and I’m not the only one with this issue:
After extended trouble shooting and device manager fiddling, It turned out that Windows was auto installing the wrong (or partially wrong) drivers that made the native port not be recognised as a USB device.
And SAM21 as a USB HID device has driver issues too: here and here To be able to get some more insights into windows USB stuff, I downloaded Nirsoft USBDeview.
And indeed my SAMD only appeared as serial device. I tried reinstalling some device drivers and rebooting windows (why not), but that didn’t quite work. Until I decided to had a better look in Arduino IDE and found the following settings in TOOLS -> USB-CONFIG:

  • CDC_ONLY (default)
  • CDC_HID
  • WITH_CDC
  • HID_ONLY
  • WITHOUT_CDC
  • USB_DISABLED

CDC means Communication Device Class. So a serial port for example. Interestingly, this class ONLY is set by default. I changed this setting to WITH_CDC and all of a sudden, my SAMD was detected both as a serial port as well as an USB Audio Device (meaning capable to do MIDI). And it actually worked!
What I still don’t get is why this isn’t an issue on MacOS. Just on Windows. Well, who cares. Got this done.